49963 lines
No EOL
2.4 MiB
49963 lines
No EOL
2.4 MiB
/*! For license information please see hls.js.LICENSE.txt
|
||
TL,DR: Don't misuse this file for piracy purpose, all rights of this file and its usage belong to Apple and media copyright holders*/
|
||
(function __HLS_UMD_BUNDLE__(__IN_WORKER__){const self = this;
|
||
(function (global, factory) {
|
||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||
typeof define === 'function' && define.amd ? define(factory) :
|
||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Hls = factory());
|
||
})(this, (function () { 'use strict';
|
||
|
||
function hasUMDWorker() {
|
||
return typeof __HLS_UMD_BUNDLE__ === 'function';
|
||
}
|
||
|
||
const check = (it) => it && it.Math === Math && it;
|
||
var global$1 = // eslint-disable-next-line no-undef
|
||
(check(typeof globalThis == 'object' && globalThis) ||
|
||
check(typeof window == 'object' && window) ||
|
||
check(typeof self == 'object' && self) ||
|
||
check(typeof global == 'object' && global) ||
|
||
Function('return this')());
|
||
|
||
|
||
class JSAESDecryptor {
|
||
constructor() {
|
||
this.keySize = null;
|
||
this.ksRows = null;
|
||
this.keySchedule = null;
|
||
this.invKeySchedule = null;
|
||
// Static after running initTable
|
||
this.rcon = [0, 1, 2, 4, 8, 16, 32, 64, 128, 27, 54];
|
||
this.subMix = [new Uint32Array(256), new Uint32Array(256), new Uint32Array(256), new Uint32Array(256)];
|
||
this.invSubMix = [new Uint32Array(256), new Uint32Array(256), new Uint32Array(256), new Uint32Array(256)];
|
||
this.sBox = new Uint32Array(256);
|
||
this.invSBox = new Uint32Array(256);
|
||
// Changes during runtime
|
||
this.key = new Uint32Array(0);
|
||
this.initTable();
|
||
}
|
||
// Using view.getUint32() also swaps the byte order.
|
||
uint8ArrayToUint32Array_(arrayBuffer) {
|
||
const view = new DataView(arrayBuffer);
|
||
const length = Math.floor(view.byteLength / 4);
|
||
const newArray = new Uint32Array(length);
|
||
for (let i = 0; i < length; i++) {
|
||
newArray[i] = view.getUint32(i * 4);
|
||
}
|
||
return newArray;
|
||
}
|
||
initTable() {
|
||
const { sBox } = this;
|
||
const { invSBox } = this;
|
||
const { subMix } = this;
|
||
const subMix0 = subMix[0];
|
||
const subMix1 = subMix[1];
|
||
const subMix2 = subMix[2];
|
||
const subMix3 = subMix[3];
|
||
const { invSubMix } = this;
|
||
const invSubMix0 = invSubMix[0];
|
||
const invSubMix1 = invSubMix[1];
|
||
const invSubMix2 = invSubMix[2];
|
||
const invSubMix3 = invSubMix[3];
|
||
const d = new Uint32Array(256);
|
||
let x = 0;
|
||
let xi = 0;
|
||
let i = 0;
|
||
for (i = 0; i < 256; i++) {
|
||
if (i < 128) {
|
||
d[i] = i << 1;
|
||
}
|
||
else {
|
||
d[i] = (i << 1) ^ 283;
|
||
}
|
||
}
|
||
for (i = 0; i < 256; i++) {
|
||
let sx = xi ^ (xi << 1) ^ (xi << 2) ^ (xi << 3) ^ (xi << 4);
|
||
sx = (sx >>> 8) ^ (sx & 255) ^ 99;
|
||
sBox[x] = sx;
|
||
invSBox[sx] = x;
|
||
// Compute multiplication
|
||
const x2 = d[x];
|
||
const x4 = d[x2];
|
||
const x8 = d[x4];
|
||
// Compute sub/invSub bytes, mix columns tables
|
||
let t = (d[sx] * 257) ^ (sx * 16843008);
|
||
subMix0[x] = (t << 24) | (t >>> 8);
|
||
subMix1[x] = (t << 16) | (t >>> 16);
|
||
subMix2[x] = (t << 8) | (t >>> 24);
|
||
subMix3[x] = t;
|
||
// Compute inv sub bytes, inv mix columns tables
|
||
t = (x8 * 16843009) ^ (x4 * 65537) ^ (x2 * 257) ^ (x * 16843008);
|
||
invSubMix0[sx] = (t << 24) | (t >>> 8);
|
||
invSubMix1[sx] = (t << 16) | (t >>> 16);
|
||
invSubMix2[sx] = (t << 8) | (t >>> 24);
|
||
invSubMix3[sx] = t;
|
||
// Compute next counter
|
||
if (!x) {
|
||
x = xi = 1;
|
||
}
|
||
else {
|
||
x = x2 ^ d[d[d[x8 ^ x2]]];
|
||
xi ^= d[d[xi]];
|
||
}
|
||
}
|
||
}
|
||
expandKey(keyBuffer) {
|
||
// convert keyBuffer to Uint32Array
|
||
const key = this.uint8ArrayToUint32Array_(keyBuffer);
|
||
let sameKey = true;
|
||
let offset = 0;
|
||
while (offset < key.length && sameKey) {
|
||
sameKey = key[offset] === this.key[offset];
|
||
offset++;
|
||
}
|
||
if (sameKey) {
|
||
return;
|
||
}
|
||
this.key = key;
|
||
const keySize = (this.keySize = key.length);
|
||
if (keySize !== 4 && keySize !== 6 && keySize !== 8) {
|
||
throw new Error('Invalid aes key size=' + keySize);
|
||
}
|
||
const ksRows = (this.ksRows = (keySize + 6 + 1) * 4);
|
||
let ksRow;
|
||
let invKsRow;
|
||
const keySchedule = (this.keySchedule = new Uint32Array(ksRows));
|
||
const invKeySchedule = (this.invKeySchedule = new Uint32Array(ksRows));
|
||
const sbox = this.sBox;
|
||
const { rcon } = this;
|
||
const { invSubMix } = this;
|
||
const invSubMix0 = invSubMix[0];
|
||
const invSubMix1 = invSubMix[1];
|
||
const invSubMix2 = invSubMix[2];
|
||
const invSubMix3 = invSubMix[3];
|
||
let prev;
|
||
let t;
|
||
for (ksRow = 0; ksRow < ksRows; ksRow++) {
|
||
if (ksRow < keySize) {
|
||
prev = keySchedule[ksRow] = key[ksRow];
|
||
continue;
|
||
}
|
||
t = prev;
|
||
if (ksRow % keySize === 0) {
|
||
// Rot word
|
||
t = (t << 8) | (t >>> 24);
|
||
// Sub word
|
||
t = (sbox[t >>> 24] << 24) | (sbox[(t >>> 16) & 255] << 16) | (sbox[(t >>> 8) & 255] << 8) | sbox[t & 255];
|
||
// Mix Rcon
|
||
t ^= rcon[(ksRow / keySize) | 0] << 24;
|
||
}
|
||
else if (keySize > 6 && ksRow % keySize === 4) {
|
||
// Sub word
|
||
t = (sbox[t >>> 24] << 24) | (sbox[(t >>> 16) & 255] << 16) | (sbox[(t >>> 8) & 255] << 8) | sbox[t & 255];
|
||
}
|
||
keySchedule[ksRow] = prev = (keySchedule[ksRow - keySize] ^ t) >>> 0;
|
||
}
|
||
for (invKsRow = 0; invKsRow < ksRows; invKsRow++) {
|
||
ksRow = ksRows - invKsRow;
|
||
if (invKsRow & 3) {
|
||
t = keySchedule[ksRow];
|
||
}
|
||
else {
|
||
t = keySchedule[ksRow - 4];
|
||
}
|
||
if (invKsRow < 4 || ksRow <= 4) {
|
||
invKeySchedule[invKsRow] = t;
|
||
}
|
||
else {
|
||
invKeySchedule[invKsRow] = invSubMix0[sbox[t >>> 24]] ^ invSubMix1[sbox[(t >>> 16) & 255]] ^ invSubMix2[sbox[(t >>> 8) & 255]] ^ invSubMix3[sbox[t & 255]];
|
||
}
|
||
invKeySchedule[invKsRow] = invKeySchedule[invKsRow] >>> 0;
|
||
}
|
||
}
|
||
// Adding this as a method greatly improves performance.
|
||
networkToHostOrderSwap(word) {
|
||
return (word << 24) | ((word & 65280) << 8) | ((word & 16711680) >> 8) | (word >>> 24);
|
||
}
|
||
decrypt(inputArrayBuffer, offset, aesIV) {
|
||
const nRounds = this.keySize + 6;
|
||
const { invKeySchedule } = this;
|
||
const invSBOX = this.invSBox;
|
||
const { invSubMix } = this;
|
||
const invSubMix0 = invSubMix[0];
|
||
const invSubMix1 = invSubMix[1];
|
||
const invSubMix2 = invSubMix[2];
|
||
const invSubMix3 = invSubMix[3];
|
||
const initVector = this.uint8ArrayToUint32Array_(aesIV);
|
||
let initVector0 = initVector[0];
|
||
let initVector1 = initVector[1];
|
||
let initVector2 = initVector[2];
|
||
let initVector3 = initVector[3];
|
||
const inputInt32 = new Int32Array(inputArrayBuffer);
|
||
const outputInt32 = new Int32Array(inputInt32.length);
|
||
let t0, t1, t2, t3;
|
||
let s0, s1, s2, s3;
|
||
let inputWords0, inputWords1, inputWords2, inputWords3;
|
||
let ksRow, i;
|
||
const swapWord = this.networkToHostOrderSwap;
|
||
while (offset < inputInt32.length) {
|
||
inputWords0 = swapWord(inputInt32[offset]);
|
||
inputWords1 = swapWord(inputInt32[offset + 1]);
|
||
inputWords2 = swapWord(inputInt32[offset + 2]);
|
||
inputWords3 = swapWord(inputInt32[offset + 3]);
|
||
s0 = inputWords0 ^ invKeySchedule[0];
|
||
s1 = inputWords3 ^ invKeySchedule[1];
|
||
s2 = inputWords2 ^ invKeySchedule[2];
|
||
s3 = inputWords1 ^ invKeySchedule[3];
|
||
ksRow = 4;
|
||
// Iterate through the rounds of decryption
|
||
for (i = 1; i < nRounds; i++) {
|
||
t0 = invSubMix0[s0 >>> 24] ^ invSubMix1[(s1 >> 16) & 255] ^ invSubMix2[(s2 >> 8) & 255] ^ invSubMix3[s3 & 255] ^ invKeySchedule[ksRow];
|
||
t1 = invSubMix0[s1 >>> 24] ^ invSubMix1[(s2 >> 16) & 255] ^ invSubMix2[(s3 >> 8) & 255] ^ invSubMix3[s0 & 255] ^ invKeySchedule[ksRow + 1];
|
||
t2 = invSubMix0[s2 >>> 24] ^ invSubMix1[(s3 >> 16) & 255] ^ invSubMix2[(s0 >> 8) & 255] ^ invSubMix3[s1 & 255] ^ invKeySchedule[ksRow + 2];
|
||
t3 = invSubMix0[s3 >>> 24] ^ invSubMix1[(s0 >> 16) & 255] ^ invSubMix2[(s1 >> 8) & 255] ^ invSubMix3[s2 & 255] ^ invKeySchedule[ksRow + 3];
|
||
// Update state
|
||
s0 = t0;
|
||
s1 = t1;
|
||
s2 = t2;
|
||
s3 = t3;
|
||
ksRow = ksRow + 4;
|
||
}
|
||
// Shift rows, sub bytes, add round key
|
||
t0 = (invSBOX[s0 >>> 24] << 24) ^ (invSBOX[(s1 >> 16) & 255] << 16) ^ (invSBOX[(s2 >> 8) & 255] << 8) ^ invSBOX[s3 & 255] ^ invKeySchedule[ksRow];
|
||
t1 = (invSBOX[s1 >>> 24] << 24) ^ (invSBOX[(s2 >> 16) & 255] << 16) ^ (invSBOX[(s3 >> 8) & 255] << 8) ^ invSBOX[s0 & 255] ^ invKeySchedule[ksRow + 1];
|
||
t2 = (invSBOX[s2 >>> 24] << 24) ^ (invSBOX[(s3 >> 16) & 255] << 16) ^ (invSBOX[(s0 >> 8) & 255] << 8) ^ invSBOX[s1 & 255] ^ invKeySchedule[ksRow + 2];
|
||
t3 = (invSBOX[s3 >>> 24] << 24) ^ (invSBOX[(s0 >> 16) & 255] << 16) ^ (invSBOX[(s1 >> 8) & 255] << 8) ^ invSBOX[s2 & 255] ^ invKeySchedule[ksRow + 3];
|
||
ksRow = ksRow + 3;
|
||
// Write
|
||
outputInt32[offset] = swapWord(t0 ^ initVector0);
|
||
outputInt32[offset + 1] = swapWord(t3 ^ initVector1);
|
||
outputInt32[offset + 2] = swapWord(t2 ^ initVector2);
|
||
outputInt32[offset + 3] = swapWord(t1 ^ initVector3);
|
||
// reset initVector to last 4 unsigned int
|
||
initVector0 = inputWords0;
|
||
initVector1 = inputWords1;
|
||
initVector2 = inputWords2;
|
||
initVector3 = inputWords3;
|
||
offset = offset + 4;
|
||
}
|
||
return outputInt32.buffer;
|
||
}
|
||
destroy() {
|
||
this.key = undefined;
|
||
this.keySize = undefined;
|
||
this.ksRows = undefined;
|
||
this.sBox = undefined;
|
||
this.invSBox = undefined;
|
||
this.subMix = undefined;
|
||
this.invSubMix = undefined;
|
||
this.keySchedule = undefined;
|
||
this.invKeySchedule = undefined;
|
||
this.rcon = undefined;
|
||
}
|
||
}
|
||
|
||
function removePadding(decryptedData) {
|
||
const decryptedDataView = new Uint8Array(decryptedData);
|
||
const padding = decryptedDataView[decryptedData.byteLength - 1];
|
||
const endOffset = decryptedData.byteLength - 1;
|
||
// Check for PKCS-7 padding: https://tools.ietf.org/html/rfc2315#section-10.3
|
||
let checkedBytes = 0;
|
||
if (padding >= 1 && padding <= 16) {
|
||
for (let i = endOffset; i > endOffset - padding; i--) {
|
||
if (decryptedDataView[i] !== padding) {
|
||
break;
|
||
}
|
||
checkedBytes++;
|
||
}
|
||
}
|
||
if (checkedBytes === padding) {
|
||
decryptedData = decryptedData.slice(0, endOffset - padding + 1); // keep 0 till 'endOffset - padding', both inclusive, to remove the padding bytes
|
||
}
|
||
return decryptedData;
|
||
}
|
||
|
||
// CryptoRPCServer can run in either Worker or main process depending on config.
|
||
// So we want to minimize the dependency for it in case it's running in Worker.
|
||
// Thus we use the vanilla RPCService interface that uses old-school callback
|
||
// patterns. It has zero dependency: no Promise, no RxJS, just plain JavaScript.
|
||
class CryptoRPCServer {
|
||
constructor(rpc, logger) {
|
||
this.rpc = rpc;
|
||
this.logger = logger;
|
||
this.decrypt = (key, iv, alg, cipherText, options) => callback => {
|
||
const crypto = global$1.crypto;
|
||
if ((options === null || options === void 0 ? void 0 : options.useJSCrypto) || !(crypto === null || crypto === void 0 ? void 0 : crypto.subtle)) {
|
||
/**
|
||
* JSCrypto
|
||
*/
|
||
const jsCrypto = new JSAESDecryptor();
|
||
let plainText;
|
||
jsCrypto.expandKey(key);
|
||
const result = jsCrypto.decrypt(cipherText, 0, iv);
|
||
if (options.plainTextLength) {
|
||
plainText = result.slice(0, options.plainTextLength); // The input buffer doesn't have padding, just return the expected bytes.
|
||
}
|
||
else {
|
||
plainText = removePadding(result);
|
||
}
|
||
this.logger.info(`[JSCrypto]: ${cipherText.byteLength} => ${plainText.byteLength} bytes`);
|
||
callback(plainText, undefined, [plainText]);
|
||
}
|
||
else {
|
||
/**
|
||
* WebCrypto
|
||
*/
|
||
crypto.subtle
|
||
.importKey('raw', key, alg, false, ['decrypt'])
|
||
.then((key) => crypto.subtle.decrypt({ name: alg, iv }, key, cipherText))
|
||
.then((plainText) => {
|
||
this.logger.info(`[WebCrypto]: ${cipherText.byteLength} => ${plainText.byteLength} bytes`);
|
||
callback(plainText, undefined, [plainText]);
|
||
})
|
||
.catch((error) => callback(undefined, error));
|
||
}
|
||
};
|
||
rpc.register('decrypt', this.decrypt);
|
||
}
|
||
}
|
||
|
||
const LoggerRPCClient = (rpcService) => {
|
||
const bindLogger = (bindings = []) => {
|
||
const logFn = (level) => (...args) => {
|
||
rpcService.invoke('logger.log', [bindings, level, ...args])((res, err) => {
|
||
if (err != null)
|
||
throw err;
|
||
});
|
||
};
|
||
const logger = {};
|
||
['fatal', 'error', 'warn', 'info', 'debug', 'trace', 'qe'].forEach((k) => (logger[k] = logFn(k)));
|
||
logger.child = (b) => bindLogger([...bindings, b]);
|
||
return logger;
|
||
};
|
||
return bindLogger();
|
||
};
|
||
|
||
/**
|
||
* HLS Events
|
||
*
|
||
*
|
||
*
|
||
*
|
||
*/
|
||
/**
|
||
* @readonly
|
||
* @enum {string}
|
||
*/
|
||
var HlsEvent;
|
||
(function (HlsEvent) {
|
||
// fired before MediaSource is attaching to media element - data: { media }
|
||
HlsEvent["MEDIA_ATTACHING"] = "hlsMediaAttaching";
|
||
// fired when MediaSource has been succesfully attached to media element - data: { media }
|
||
HlsEvent["MEDIA_ATTACHED"] = "hlsMediaAttached";
|
||
// fired before detaching MediaSource from media element - data: { }
|
||
HlsEvent["MEDIA_DETACHING"] = "hlsMediaDetaching";
|
||
// fired when MediaSource has been detached from media element
|
||
HlsEvent["MEDIA_DETACHED"] = "hlsMediaDetached";
|
||
// fired when sourcebuffers have been created - data: { tracks : tracks }
|
||
HlsEvent["BUFFER_CREATED"] = "hlsBufferCreated";
|
||
// fired when we append a segment to the buffer - data: { segment: segment object }
|
||
HlsEvent["BUFFER_APPENDING"] = "hlsBufferAppending";
|
||
// fired when we are done with appending a media segment to the buffer - data : { parent : segment parent that triggered BUFFER_APPENDING, pending : nb of segments waiting for appending for this segment, timeRanges : bufferd time ranges }
|
||
HlsEvent["BUFFER_APPENDED"] = "hlsBufferAppended";
|
||
// fired when the media buffer has been flushed
|
||
HlsEvent["BUFFER_FLUSHED"] = "hlsBufferFlushed";
|
||
// fired to signal that a manifest loading starts - data: { url: manifestURL }
|
||
HlsEvent["MANIFEST_LOADING"] = "hlsManifestLoading";
|
||
// fired after manifest has been loaded - data: { levels : [available quality levels including iframe levels], audioTracks : [ available audio tracks], subtitleTracks : [ available subtitle tracks], url : manifestURL, stats : { trequest, tfirst, tload, mtime}, audioMediaSelectionGroup: audio selection group, subtitleMediaSelectionGroup: subtitle selection group}
|
||
HlsEvent["MANIFEST_LOADED"] = "hlsManifestLoaded";
|
||
// fired after manifest has been parsed - data: { levels : [available quality levels including iframe levels], firstLevel : index of first quality level appearing in Manifest, stats: stats, audio: audio codec, video: video codec, altAudio: if alternate audio present (true/false), audioTracks: audio tracks, audioMediaSelectionGroup: audio selection group}
|
||
HlsEvent["MANIFEST_PARSED"] = "hlsManifestParsed";
|
||
// fired when a level switch is requested - data: { level : id of new level }
|
||
HlsEvent["LEVEL_SWITCHING"] = "hlsLevelSwitching";
|
||
// fired when a level switch is effective - data: { level : id of new level }
|
||
HlsEvent["LEVEL_SWITCHED"] = "hlsLevelSwitched";
|
||
// fired when a level playlist loading starts - data: { url : level URL, level : level, persistentId: persistent id of level being loaded}
|
||
HlsEvent["LEVEL_LOADING"] = "hlsLevelLoading";
|
||
// fired when a level playlist loading finishes - data: { details : levelDetails object, level : level, persistentId : persistent id of loaded level, stats : { trequest, tfirst, tload, mtime}, playlistType : playlist type }
|
||
HlsEvent["LEVEL_LOADED"] = "hlsLevelLoaded";
|
||
// fired when a level's details have been updated based on previous details, after it has been loaded - data: { details : levelDetails object, level : id of updated level }
|
||
HlsEvent["LEVEL_UPDATED"] = "hlsLevelUpdated";
|
||
// fired when levels list has changed due to any error
|
||
HlsEvent["LEVELS_CHANGED"] = "hlsLevelsChanged";
|
||
// fired to notify that audio track lists has been updated - data: { audioTracks : audioTracks }
|
||
HlsEvent["AUDIO_TRACKS_UPDATED"] = "hlsAudioTracksUpdated";
|
||
// fired when an audio track switch occurs - data: { id : audio track id, type: track type, url: url } // deprecated in favor AUDIO_TRACK_SWITCHING
|
||
HlsEvent["AUDIO_TRACK_SWITCH"] = "hlsAudioTrackSwitch";
|
||
// fired when an audio track switch actually occurs - data: { id : audio track id }
|
||
HlsEvent["AUDIO_TRACK_SWITCHED"] = "hlsAudioTrackSwitched";
|
||
// fired when an audio track loading finishes - data: { details : levelDetails object, id : audio track id, stats : { trequest, tfirst, tload, mtime } }
|
||
HlsEvent["AUDIO_TRACK_LOADED"] = "hlsAudioTrackLoaded";
|
||
// fired to notify that subtitle track lists has been updated - data: { subtitleTracks : subtitleTracks }
|
||
HlsEvent["SUBTITLE_TRACKS_UPDATED"] = "hlsSubtitleTracksUpdated";
|
||
// fired to notify that subtitle HTML5 text tracks have been created - data: undefined
|
||
HlsEvent["SUBTITLE_TRACKS_CREATED"] = "hlsSubtitleTracksCreated";
|
||
// fired when an subtitle track switch occurs - data: { track : subtitle track }
|
||
HlsEvent["SUBTITLE_TRACK_SWITCH"] = "hlsSubtitleTrackSwitch";
|
||
// fired when inline webvtt styles have been parsed - data: { styles: styles }
|
||
HlsEvent["INLINE_STYLES_PARSED"] = "hlsInlineStylesParsed";
|
||
// fired when all the session data URI loading finished - data: { }
|
||
HlsEvent["SESSION_DATA_COMPLETE"] = "hlsSessionDataComplete";
|
||
// fired when a fragment loading starts - data: { frag : fragment object }
|
||
HlsEvent["FRAG_LOADING"] = "hlsFragLoading";
|
||
// fired when a fragment loading is completed - data: { frag : fragment object, payload : fragment payload, stats : { trequest, tfirst, tload, length } }
|
||
HlsEvent["FRAG_LOADED"] = "hlsFragLoaded";
|
||
// fired when fragment remuxed MP4 boxes have all been appended into SourceBuffer - data: { id : demuxer id, frag : fragment object, stats : { trequest, tfirst, tload, tparsed, tappendStart, tbuffered, length, bwEstimate } }
|
||
HlsEvent["FRAG_BUFFERED"] = "hlsFragBuffered";
|
||
// fired when fragment matching with current media position is changing - data : { frag : fragment object }
|
||
HlsEvent["FRAG_CHANGED"] = "hlsFragChanged";
|
||
// Identifier for an internal error event - data: { type : error type, details : error details, fatal : emitter's suggestion whether the error is fatal or not. can be overruled by handler }
|
||
HlsEvent["INTERNAL_ERROR"] = "hlsInternalError";
|
||
// Identifier for an public error event - data: { type : error type, details : error details, fatal : if true, hls.js cannot/will not try to recover, if false, hls.js will try to recover,other error specific data }
|
||
HlsEvent["ERROR"] = "hlsError";
|
||
// fired when hls.js instance starts destroying. Different from MEDIA_DETACHED as one could want to detach and reattach a media to the instance of hls.js to handle mid-rolls for example - data: { }
|
||
HlsEvent["DESTROYING"] = "hlsDestroying";
|
||
// Key request started data: { keyuri: '' , decryptdata: DecryptData object, timestamp: timestamp of the event }
|
||
HlsEvent["KEY_REQUEST_STARTED"] = "hlsKeyRequestStarted";
|
||
// data: { 'keyuri': 'licenseChallenge': Uint8Array(challengeBytes) keysystem: }
|
||
HlsEvent["LICENSE_CHALLENGE_CREATED"] = "hlsLicenseChallengeCreated";
|
||
// EME:
|
||
// Session remove() was called and returned license-release message. data: { keysystem: '', itemId: itemId, releaseRecord: { CDM specific record destruction } }
|
||
HlsEvent["LICENSE_RELEASED"] = "hlsLicenseReleased";
|
||
// fired when a decrypt key loading is completed - data: { decryptdata: '', keyuri: '', stats : { trequest, tfirst, tload }, timestamp: timestamp of the event }
|
||
HlsEvent["KEY_LOADED"] = "hlsKeyLoaded";
|
||
// We encountered a url that doesn't start with http data: { uri: ''}
|
||
HlsEvent["UNRESOLVED_URI_LOADING"] = "hlsUnresolvedUriLoading";
|
||
// fired when trickplay rate changing completed
|
||
HlsEvent["DESIRED_RATE_CHANGED"] = "hlsDesiredRateChanged";
|
||
// data: { playbackLikelyToKeepUp }
|
||
HlsEvent["PLAYER_STATE_CHANGE"] = "hlsPlayerStateChange";
|
||
// When seek starts data: {}
|
||
HlsEvent["SEEKING"] = "hlsSeeking";
|
||
// When seek ends data: {}
|
||
HlsEvent["SEEKED"] = "hlsSeeked";
|
||
// When stall detected. Stream controller decides if it will report an error. data: { isLowBufferStall: true|false }
|
||
HlsEvent["STALLED"] = "hlsStalled";
|
||
// When resumed from stall, either playback restart or user hit pause.
|
||
HlsEvent["RESUME_FROM_STALL"] = "hlsResumeFromStall";
|
||
// Item fully appended
|
||
HlsEvent["READY_FOR_NEXT_ITEM"] = "hlsReadyForNextItem";
|
||
// Playback transitioned to next item
|
||
HlsEvent["ITEM_TRANSITIONED"] = "hlsItemTransitioned";
|
||
// Evict loading item
|
||
HlsEvent["ITEM_EVICTED"] = "hlsItemEvicted";
|
||
// New EXT-X-DATERANGE data available
|
||
HlsEvent["DATERANGE_UPDATED"] = "hlsDaterangeUpdated";
|
||
})(HlsEvent || (HlsEvent = {}));
|
||
var HlsEvent$1 = HlsEvent;
|
||
|
||
/*
|
||
* Demuxer Events
|
||
*
|
||
*
|
||
*/
|
||
/**
|
||
* @readonly
|
||
* @enum {string}
|
||
*/
|
||
var DemuxerEvent;
|
||
(function (DemuxerEvent) {
|
||
// fired when Init Segment has been extracted from fragment - data: { tracks: tracks }
|
||
DemuxerEvent["FRAG_PARSING_INIT_SEGMENT"] = "hlsFragParsingInitSegment";
|
||
// fired when data have been extracted from fragment - data: { data1: segment, startPTS: start PTS, endPTS: end PTS, startDTS: start DTS, endDTS: end DTS, type: video / audio, nb: number of samples, dropped: is dropped }
|
||
DemuxerEvent["FRAG_PARSING_DATA"] = "hlsFragParsingData";
|
||
// fired when fragment parsing is completed
|
||
DemuxerEvent["FRAG_PARSED"] = "hlsFragParsed";
|
||
// fired when the first timestamp is found - data: { initPTS90k: initPTS90k }
|
||
DemuxerEvent["INIT_PTS_FOUND"] = "hlsInitPtsFound";
|
||
})(DemuxerEvent || (DemuxerEvent = {}));
|
||
|
||
/**
|
||
* HLS Error
|
||
*
|
||
*
|
||
*
|
||
*
|
||
*/
|
||
class HlsError extends Error {
|
||
constructor(type, details, fatal, reason, response) {
|
||
super(reason);
|
||
this.type = type;
|
||
this.details = details;
|
||
this.fatal = fatal;
|
||
this.reason = reason;
|
||
this.response = response;
|
||
this.handled = false;
|
||
}
|
||
}
|
||
// Inherited error codes from Core Media
|
||
const PlaylistNotReceived = { code: -12884, text: 'Playlist not received' };
|
||
const CryptResponseReceivedSlowly = { code: -16833, text: 'Crypt key received slowly' };
|
||
const LivePlaylistUpdateError = { code: -12888, text: 'Live playlist not updated' };
|
||
const NoResponseFromMediaRequest = { code: -12889, text: 'No response for fragment' };
|
||
const IncompatibleAsset = { code: -12927, text: 'IncompatibleAsset' };
|
||
const CorruptStream = { code: -16041, text: 'Corrupt fragment' };
|
||
const InternalError = { code: -12645, text: 'InternalException' };
|
||
const CantSwitchInTime = { code: -12644, text: 'CantSwitchInTime' };
|
||
const VideoDecoderBadDataErr = { code: -12909, text: 'Buffer error' };
|
||
const InsufficientDataAvailable = { code: -12928, text: 'Incomplete data' };
|
||
const AllocationFailed = { code: -12862, text: 'AllocationFailed' };
|
||
const PlaylistErrorMissingEXTM3U = { code: -12269, text: 'Response doesnt have #EXTM3U tag' };
|
||
const PlaylistErrorInvalidEntry = { code: -12264, text: 'Invalid entry' };
|
||
const PlaylistErrorBadTargetDuration = { code: -12271, text: 'Invalid targetduration' };
|
||
const NoValidAlternates = { code: -12925, text: 'No valid alternates' };
|
||
const FormatError = { code: -12642, text: 'Incorrect playlist format' };
|
||
// HLSJS Specific error codes
|
||
const UnsupportedKeySystemError = { code: -60000, text: 'Unsupported Key System' };
|
||
const EmptyLoadSourceError = { code: -60001, text: 'Empty loadSource url' };
|
||
const UndefinedItemIdError = { code: -60002, text: 'Undefined itemId' };
|
||
const ManifestParseError = { code: -60003, text: 'Manifest parse error' };
|
||
const DemuxWorkerError = { code: -60004, text: 'Demux worker error' };
|
||
const DecryptWorkerError = { code: -60005, text: 'Decrypt worker error' };
|
||
const OutOfRangeSeekError = { code: -60006, text: 'Seeked out of playable range' };
|
||
const ExceptionInKeyLoadError = { code: -60007, text: 'Exception in Key load' };
|
||
const FragmentAbortError$1 = { code: -60008, text: 'Fragment abort error' };
|
||
const ManifestTimeoutError = { code: -60009, text: 'Manifest Timeout Error' };
|
||
const PlaylistTimeoutError = { code: -60010, text: 'Playlist Timeout Error' };
|
||
const FragmentTimeoutError = { code: -60011, text: 'Fragment Timeout Error' };
|
||
const IncompleteSessionData = { code: -60012, text: 'Session data not complete after loading all items' };
|
||
const SessionDataLoadTimeout = { code: -60013, text: 'Session data load timeout' };
|
||
const FailedDemuxerSanityCheck = { code: -60014, text: 'Failed demuxer sanity check' };
|
||
const InvalidADTSSamplingIndex = { code: -60015, text: 'Invalid ADTS sampling index' };
|
||
const DemuxerNotFound = { code: -60016, text: 'No demux matching with content found' };
|
||
const InvalidInitTimestamp = { code: -60017, text: 'Invalid initPTS or initDTS' };
|
||
const NoAVSamplesFound = { code: -60018, text: 'no audio/video samples found' };
|
||
const NoTSSyncByteFound = { code: -60019, text: 'TS packet did not start with 0x47' };
|
||
const PESDidNotStartWithADTS = { code: -60020, text: 'AAC PES did not start with ADTS header' };
|
||
const NoADTSHeaderInPES = { code: -60021, text: 'No ADTS header found in AAC PES' };
|
||
const InvalidDolbyAudioMagic = { code: -60022, text: 'Invalid dolby audio magic' };
|
||
const FailedToAllocateVideoMdat = { code: -60023, text: 'Fail allocating video mdat' };
|
||
const FailedToAllocateAudioMdat = { code: -60024, text: 'Fail allocating audio mdat' };
|
||
const InsufficientEC3Data = { code: -60025, text: 'Error parsing ec-3, not enough data' };
|
||
const InvalidEC3Magic = { code: -60026, text: 'Invalid ec-3 magic' };
|
||
const ReservedStreamType = { code: -60027, text: 'Reserved stream type' };
|
||
const InsufficientAC3Data = { code: -60028, text: 'error parsing ac-3, not enough data' };
|
||
const InvalidAC3Magic = { code: -60029, text: 'Invalid ac-3 magic' };
|
||
const InvalidAC3SamplingRateCode = { code: -60030, text: 'Invalid ac-3 samplingRateCode' };
|
||
const PlaylistErrorInvalidEXTXDEFINE = { code: -61000, text: 'Encountered undefined/not imported EXT-X-DEFINE property' };
|
||
const PlaylistErrorMissingImportReference = { code: -61001, text: 'IMPORT references variable not in master playlist and/or NAME' };
|
||
const PlaylistErrorInvalidSERVERURI = { code: -61002, text: 'Encountered undefined/invalid SERVER-URI attribute for EXT-X-CONTENT-STEERING tag' };
|
||
const PlaylistErrorInvalidPATHWAYID = { code: -61003, text: 'Encountered invalid PATHWAY-ID attribute for EXT-X-CONTENT-STEERING tag' };
|
||
const PlaylistErrorInvalidSCORE = { code: -61004, text: 'Encountered negative/non-number SCORE property' };
|
||
const KeySystemFailedToUpdateSession = { code: -62000, text: 'KeySystem: Promise Rejected while updating session' };
|
||
const KeySystemFailedToGenerateLicenseRenewal = { code: -62001, text: 'KeySystem: Failed to generate license renewal' };
|
||
const KeySystemFailedToGenerateLicenseRequest = { code: -62002, text: 'KeySystem: Failed to generate license request' };
|
||
const KeySystemAbort = { code: -62003, text: 'KeySystem: Aborted' };
|
||
const KeySystemUnexpectedStateTransition = { code: -62004, text: 'KeySystem: Unexpected state transition' };
|
||
const KeySystemUnexpectedState = { code: -62005, text: 'KeySystem: Unexpected state' };
|
||
const KeySystemCDMUnknownError = { code: -62006, text: 'KeySystem: Unknown error from CDM' };
|
||
const KeySystemRequestTimedOut = { code: -62007, text: 'Key request timed out' };
|
||
const KeySystemUnexpectedMETHOD = { code: -62008, text: 'Unexpected METHOD attribute' };
|
||
const KeySystemUnmatchedString = { code: -62009, text: 'KeySystem: string does not match' };
|
||
const KeySystemInternalError = { code: -62010, text: 'KeySystem: internal-error' };
|
||
const KeySystemOutputRestricted = { code: -62011, text: 'KeySystem: output-restricted' };
|
||
const KeySystemSetupError = { code: -62012, text: 'KeySystem: setup error' };
|
||
const KeySystemFailedToInitialize = { code: -62013, text: 'KeySystem: could not initialize' };
|
||
const KeySystemFailedToCreateSession = { code: -62014, text: 'KeySystem: could not create session' };
|
||
const KeySystemUndefinedNavigator = { code: -62015, text: 'KeySystem: navigator undefined' };
|
||
const KeySystemNoKeySystemsToTry = { code: -62016, text: 'KeySystem: no key systems to try' };
|
||
const KeySystemNoConstructor = { code: -62017, text: 'KeySystem: No constructor' };
|
||
const KeySystemNoKeySystemAccess = { code: -62018, text: 'KeySystem: No KeySystemAccess' };
|
||
const KeySystemCertificateLoadError = { code: -62019, text: 'KeySystem: Certificate Load Error' };
|
||
class PlaylistParsingError extends HlsError {
|
||
constructor(type, details, fatal, reason, response) {
|
||
super(type, details, fatal, reason, response);
|
||
this.response = response;
|
||
}
|
||
}
|
||
const ErrorResponses = {
|
||
// Inherited error codes from Core Media
|
||
PlaylistNotReceived,
|
||
CryptResponseReceivedSlowly,
|
||
LivePlaylistUpdateError,
|
||
NoResponseFromMediaRequest,
|
||
IncompatibleAsset,
|
||
CorruptStream,
|
||
InternalError,
|
||
CantSwitchInTime,
|
||
VideoDecoderBadDataErr,
|
||
InsufficientDataAvailable,
|
||
AllocationFailed,
|
||
PlaylistErrorMissingEXTM3U,
|
||
PlaylistErrorInvalidEntry,
|
||
PlaylistErrorBadTargetDuration,
|
||
NoValidAlternates,
|
||
FormatError,
|
||
// HLSJS Specific error codes
|
||
UnsupportedKeySystemError,
|
||
EmptyLoadSourceError,
|
||
UndefinedItemIdError,
|
||
ManifestParseError,
|
||
DemuxWorkerError,
|
||
DecryptWorkerError,
|
||
OutOfRangeSeekError,
|
||
ExceptionInKeyLoadError,
|
||
FragmentAbortError: FragmentAbortError$1,
|
||
ManifestTimeoutError,
|
||
PlaylistTimeoutError,
|
||
FragmentTimeoutError,
|
||
IncompleteSessionData,
|
||
SessionDataLoadTimeout,
|
||
FailedDemuxerSanityCheck,
|
||
InvalidADTSSamplingIndex,
|
||
DemuxerNotFound,
|
||
InvalidAC3Magic,
|
||
InvalidInitTimestamp,
|
||
NoAVSamplesFound,
|
||
NoTSSyncByteFound,
|
||
PESDidNotStartWithADTS,
|
||
NoADTSHeaderInPES,
|
||
InvalidDolbyAudioMagic,
|
||
FailedToAllocateVideoMdat,
|
||
FailedToAllocateAudioMdat,
|
||
InsufficientEC3Data,
|
||
InvalidEC3Magic,
|
||
ReservedStreamType,
|
||
InsufficientAC3Data,
|
||
InvalidAC3SamplingRateCode,
|
||
PlaylistErrorInvalidEXTXDEFINE,
|
||
PlaylistErrorMissingImportReference,
|
||
PlaylistErrorInvalidSERVERURI,
|
||
PlaylistErrorInvalidPATHWAYID,
|
||
PlaylistErrorInvalidSCORE,
|
||
KeySystemFailedToUpdateSession,
|
||
KeySystemFailedToGenerateLicenseRenewal,
|
||
KeySystemFailedToGenerateLicenseRequest,
|
||
KeySystemAbort,
|
||
KeySystemUnexpectedStateTransition,
|
||
KeySystemUnexpectedState,
|
||
KeySystemCDMUnknownError,
|
||
KeySystemRequestTimedOut,
|
||
KeySystemUnexpectedMETHOD,
|
||
KeySystemUnmatchedString,
|
||
KeySystemInternalError,
|
||
KeySystemOutputRestricted,
|
||
KeySystemSetupError,
|
||
KeySystemFailedToInitialize,
|
||
KeySystemFailedToCreateSession,
|
||
KeySystemUndefinedNavigator,
|
||
KeySystemNoKeySystemsToTry,
|
||
KeySystemNoConstructor,
|
||
KeySystemNoKeySystemAccess,
|
||
KeySystemCertificateLoadError,
|
||
};
|
||
const ErrorTypes = {
|
||
// Identifier for a network error (loading error / timeout ...)
|
||
NETWORK_ERROR: 'networkError',
|
||
// Identifier for a media Error (video/parsing/mediasource error)
|
||
MEDIA_ERROR: 'mediaError',
|
||
// Identifier for a mux Error (demuxing/remuxing)
|
||
MUX_ERROR: 'muxError',
|
||
// Identifier for a key system error (EME)
|
||
KEY_SYSTEM_ERROR: 'keySystemError',
|
||
// Identifier for all other errors
|
||
OTHER_ERROR: 'otherError',
|
||
};
|
||
const ErrorDetails = {
|
||
// Identifier for a manifest load error - data: { url : faulty URL, response : { code: error code, text: error text }}
|
||
MANIFEST_LOAD_ERROR: 'manifestLoadError',
|
||
// Identifier for a manifest load timeout - data: { url : faulty URL, response : { code: error code, text: error text }}
|
||
MANIFEST_LOAD_TIMEOUT: 'manifestLoadTimeOut',
|
||
// Identifier for a manifest parsing error - data: { url : faulty URL, reason : error reason, response : { code: error code, text: error text }}
|
||
MANIFEST_PARSING_ERROR: 'manifestParsingError',
|
||
// Identifier for a manifest with only incompatible codecs error - data: { url : faulty URL, reason : error reason, response : { code: error code, text: error text }}
|
||
MANIFEST_INCOMPATIBLE_CODECS_ERROR: 'manifestIncompatibleCodecsError',
|
||
// Identifier for a manifest with only incompatible video-range error - data: { url : faulty URL, reason : error reason, response : { code: error code, text: error text }}
|
||
MANIFEST_INCOMPATIBLE_VIDEO_RANGE_ERROR: 'manifestIncompatibleVideoRangeError',
|
||
// Identifier for a level load error - data: { url : faulty URL, response : { code: error code, text: error text }}
|
||
LEVEL_LOAD_ERROR: 'levelLoadError',
|
||
// Identifier for a level load timeout - data: { url : faulty URL, response : { code: error code, text: error text }}
|
||
LEVEL_LOAD_TIMEOUT: 'levelLoadTimeOut',
|
||
// Identifier for a level switch error - data: { level : faulty level Id, event : error description, response : { code: error code, text: error text }}
|
||
LEVEL_SWITCH_ERROR: 'levelSwitchError',
|
||
// Identifier for an audio track load error - data: { url : faulty URL, response : { code: error code, text: error text }}
|
||
AUDIO_TRACK_LOAD_ERROR: 'audioTrackLoadError',
|
||
// Identifier for an audio track load timeout - data: { url : faulty URL, response : { code: error code, text: error text }}
|
||
AUDIO_TRACK_LOAD_TIMEOUT: 'audioTrackLoadTimeOut',
|
||
// Identifier for an subtitle track load error - data: { url : faulty URL, response : { code: error code, text: error text }}
|
||
SUBTITLE_TRACK_LOAD_ERROR: 'subtitleTrackLoadError',
|
||
// Identifier for an subtitle track load timeout - data: { url : faulty URL, response : { code: error code, text: error text }}
|
||
SUBTITLE_TRACK_LOAD_TIMEOUT: 'subtitleTrackLoadTimeout',
|
||
// Identifier for fragment load error - data: { frag : fragment object, response : { code: error code, text: error text }}
|
||
FRAG_LOAD_ERROR: 'fragLoadError',
|
||
// Identifier for fragment loop loading error - data: { frag : fragment object, fatal:, response: { code: error code, text: error text }}
|
||
FRAG_LOOP_LOADING_ERROR: 'fragLoopLoadingError',
|
||
// Identifier for fragment load timeout error - data: { frag : fragment object, response : { code: error code, text: error text }}
|
||
FRAG_LOAD_TIMEOUT: 'fragLoadTimeOut',
|
||
// Identifier for a fragment decryption error event - data: {id : demuxer Id,frag: fragment object, reason : parsing error description , response : { code: error code, text: error text }}
|
||
FRAG_DECRYPT_ERROR: 'fragDecryptError',
|
||
// Identifier for a fragment parsing error event - data: { id : demuxer Id, reason : parsing error description, response : { code: error code, text: error text } }
|
||
// will be renamed DEMUX_PARSING_ERROR and switched to MUX_ERROR in the next major release
|
||
FRAG_PARSING_ERROR: 'fragParsingError',
|
||
// Identifier for a remux alloc error event - data: { id : demuxer Id, frag : fragment object, bytes : nb of bytes on which allocation failed , reason : error text, response: { code: error code, text: error text }}
|
||
REMUX_ALLOC_ERROR: 'remuxAllocError',
|
||
// Triggered when an exception occurs while adding a sourceBuffer to MediaSource - data : { err : exception , mimeType : mimeType , response : { code: error code, text: error text }}
|
||
BUFFER_ADD_CODEC_ERROR: 'bufferAddCodecError',
|
||
// Identifier for a buffer append error - data: {append error description, response : { code: error code, text: error text }}
|
||
BUFFER_APPEND_ERROR: 'bufferAppendError',
|
||
// Identifier for a buffer appending error event - data: {appending error description, response : { code: error code, text: error text }}
|
||
BUFFER_APPENDING_ERROR: 'bufferAppendingError',
|
||
// Identifier for a buffer stalled error event - data: {fatal:, bufferLen: , response : { code: error code, text: error text }}
|
||
BUFFER_STALLED_ERROR: 'bufferStalledError',
|
||
// Identifier for a buffer full event - data: {fatal:, response : { code: error code, text: error text }}
|
||
BUFFER_FULL_ERROR: 'bufferFullError',
|
||
// Identifier for a buffer seek over hole event- data: {fatal:, response : { code: error code, text: error text }}
|
||
BUFFER_SEEK_OVER_HOLE: 'bufferSeekOverHole',
|
||
// Identifier for a buffer nudge on stall (playback is stuck although currentTime is in a buffered area) data: {fatal:, response : { code: error code, text: error text }}
|
||
BUFFER_NUDGE_ON_STALL: 'bufferNudgeOnStall',
|
||
// Identifier for an internal exception happening inside hls.js while handling an event
|
||
// data: { url: , fatal:, response: { code: error code, text: error text } }
|
||
INTERNAL_EXCEPTION: 'internalException',
|
||
// Malformed WebVTT contents
|
||
WEBVTT_EXCEPTION: 'webVTTException',
|
||
// KEY REQUEST ERRORS
|
||
// Identifier for decrypt key load error - data: { decryptdata: DecryptData object, stats: { tfirstissue, tlastissue, failCount } response: {code: status code returned from server, text: error message}}
|
||
KEY_LOAD_ERROR: 'keyLoadError',
|
||
// Identifier for decrypt key load timeout error - data: { decryptdata: DecryptData object, stats: { tfirstissue, tlastissue, failCount }, response: { code: error code, text: error message }}
|
||
KEY_LOAD_TIMEOUT: 'keyLoadTimeOut',
|
||
// Identifier for CDM error - data: { decryptdata: DecryptData object, stats: { tfirstissue, tlastissue, failCount }, reason: error message, response: { code: error code, text: error message }}
|
||
KEY_SYSTEM_GENERIC_ERROR: 'keySystemGenericError',
|
||
// data: { url: , fatal:, response: { code: error code, text: error text } }
|
||
CERT_LOAD_ERROR: 'certificateLoadError',
|
||
// data: { url: , fatal:, response: { code: error code, text: error text } }
|
||
CERT_LOAD_TIMEOUT: 'certificateLoadTimeOut',
|
||
// Identifier for session data item load error - data: { sessionId: '', response : { code: error code, text: error text }}
|
||
SESSION_DATA_LOAD_ERROR: 'sessionDataLoadError',
|
||
// Identifier for session data item load timeout error - data: { sessionId: '' , response : { code: error code, text: error text }}
|
||
SESSION_DATA_LOAD_TIMEOUT: 'sessionDataLoadTimeOut',
|
||
// Identifier for a Conetent Steering manifest load error - data: { url : faulty URL, response : { code: error code, text: error text }}
|
||
STEERING_MANIFEST_LOAD_ERROR: 'steeringManifestLoadError',
|
||
// Identifier for a Conetent Steering manifest load timeout - data: { url : faulty URL, response : { code: error code, text: error text }}
|
||
STEERING_MANIFEST_LOAD_TIMEOUT: 'steeringManifestLoadTimeOut',
|
||
// Identifier for a Conetent Steering manifest parsing error - data: { url : faulty URL, reason : error reason, response : { code: error code, text: error text }}
|
||
STEERING_MANIFEST_PARSING_ERROR: 'steeringManifestParsingError',
|
||
};
|
||
/**
|
||
* @brief If no other error fits, use this error.
|
||
*/
|
||
class ExceptionError extends HlsError {
|
||
constructor(fatal, reason, response) {
|
||
super(ErrorTypes.OTHER_ERROR, ErrorDetails.INTERNAL_EXCEPTION, fatal, reason, response);
|
||
}
|
||
}
|
||
|
||
class FragParsingError extends HlsError {
|
||
constructor(fatal, reason, response) {
|
||
super(ErrorTypes.MEDIA_ERROR, ErrorDetails.FRAG_PARSING_ERROR, fatal, reason, response);
|
||
}
|
||
}
|
||
class RemuxAllocError extends HlsError {
|
||
constructor(fatal, reason, response, bytes) {
|
||
super(ErrorTypes.MUX_ERROR, ErrorDetails.REMUX_ALLOC_ERROR, fatal, reason, response);
|
||
this.bytes = bytes;
|
||
}
|
||
}
|
||
|
||
function convertTimestampToSeconds(ts) {
|
||
return ts.baseTime / ts.timescale;
|
||
}
|
||
function convertSecondsToTimestamp(sec, timescale) {
|
||
return {
|
||
baseTime: Math.floor(sec * timescale),
|
||
timescale,
|
||
};
|
||
}
|
||
function convertTimescale(ts, timescale) {
|
||
return {
|
||
baseTime: Math.floor((ts.baseTime * timescale) / ts.timescale),
|
||
timescale,
|
||
};
|
||
}
|
||
function determineMinTimestamp(a, b) {
|
||
const aSec = convertTimestampToSeconds(a);
|
||
const bSec = convertTimestampToSeconds(b);
|
||
return aSec < bSec ? a : b;
|
||
}
|
||
function determineMaxTimestamp(a, b) {
|
||
const aSec = convertTimestampToSeconds(a);
|
||
const bSec = convertTimestampToSeconds(b);
|
||
return aSec > bSec ? a : b;
|
||
}
|
||
/**
|
||
* @returns returns a - b in seconds
|
||
*/
|
||
function diffSeconds(a, b) {
|
||
return convertTimestampToSeconds(a) - convertTimestampToSeconds(b);
|
||
}
|
||
|
||
class EventEmitterPolyfill {
|
||
constructor() {
|
||
this.eventMap = {};
|
||
}
|
||
_on(eventName, eventRecord, prepend = false) {
|
||
if (this.eventMap[eventName] == null) {
|
||
this.eventMap[eventName] = [];
|
||
}
|
||
if (prepend) {
|
||
this.eventMap[eventName].splice(0, 0, eventRecord);
|
||
}
|
||
else {
|
||
this.eventMap[eventName].push(eventRecord);
|
||
}
|
||
return this;
|
||
}
|
||
_off(eventName, record) {
|
||
if (this.eventMap[eventName] != null) {
|
||
this.eventMap[eventName] = this.eventMap[eventName].filter(r => r.listener !== record.listener);
|
||
if (this.eventMap[eventName].length === 0) {
|
||
delete this.eventMap[eventName];
|
||
}
|
||
}
|
||
return this;
|
||
}
|
||
on(eventName, listener) {
|
||
return this._on(eventName, { listener, once: false });
|
||
}
|
||
off(eventName, listener) {
|
||
return this._off(eventName, { listener });
|
||
}
|
||
addListener(eventName, listener) {
|
||
return this.on(eventName, listener);
|
||
}
|
||
once(eventName, listener) {
|
||
return this._on(eventName, { listener, once: true });
|
||
}
|
||
removeListener(eventName, listener) {
|
||
return this.off(eventName, listener);
|
||
}
|
||
removeAllListeners(event) {
|
||
delete this.eventMap[event];
|
||
return this;
|
||
}
|
||
setMaxListeners(n) {
|
||
return this;
|
||
}
|
||
getMaxListeners() {
|
||
return Infinity;
|
||
}
|
||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||
listeners(eventName) {
|
||
if (this.eventMap[eventName] == null) {
|
||
return [];
|
||
}
|
||
return this.eventMap[eventName].map(r => r.listener);
|
||
}
|
||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||
rawListeners(eventName) {
|
||
return this.listeners(eventName);
|
||
}
|
||
emit(eventName, ...args) {
|
||
if (this.eventMap[eventName] == null) {
|
||
return false;
|
||
}
|
||
let emitted = false;
|
||
for (const record of this.eventMap[eventName]) {
|
||
try {
|
||
record.listener.apply(this, args);
|
||
// eslint-disable-next-line no-empty
|
||
}
|
||
catch (err) { }
|
||
emitted = true;
|
||
}
|
||
return emitted;
|
||
}
|
||
listenerCount(eventName) {
|
||
if (this.eventMap[eventName] == null) {
|
||
return 0;
|
||
}
|
||
return this.eventMap[eventName].length;
|
||
}
|
||
prependListener(eventName, listener) {
|
||
return this._on(eventName, { listener, once: false }, true);
|
||
}
|
||
prependOnceListener(eventName, listener) {
|
||
return this._on(eventName, { listener, once: true }, true);
|
||
}
|
||
eventNames() {
|
||
return Object.keys(this.eventMap);
|
||
}
|
||
}
|
||
const EventEmitter = typeof global$1.Buffer !== 'undefined' ? (r => r('events'))(require).EventEmitter : EventEmitterPolyfill;
|
||
|
||
/**
|
||
* Simple adapter sub-class of Nodejs-like EventEmitter.
|
||
*/
|
||
class Observer extends EventEmitter {
|
||
/**
|
||
* We simply want to pass along the event-name itself
|
||
* in every call to a handler, which is the purpose of our `trigger` method
|
||
* extending the standard API.
|
||
*/
|
||
trigger(event, data) {
|
||
this.emit(event, data);
|
||
}
|
||
}
|
||
|
||
|
||
const loggerName$l = { name: 'ADTS' };
|
||
const ADTS = {
|
||
getAudioConfig: function (observer, data, offset, audioCodec, logger) {
|
||
let adtsObjectType; // :int
|
||
let extensionSamplingIndex; // :int
|
||
let adtsChanelConfig; // :int
|
||
let config;
|
||
const userAgent = navigator.userAgent.toLowerCase();
|
||
const adtsSamplingRates = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
|
||
// byte 2
|
||
adtsObjectType = ((data[offset + 2] & 192) >>> 6) + 1;
|
||
const adtsSamplingIndex = (data[offset + 2] & 60) >>> 2;
|
||
if (adtsSamplingIndex > adtsSamplingRates.length - 1) {
|
||
const payload = new FragParsingError(true, `invalid ADTS sampling index:${adtsSamplingIndex}`, ErrorResponses.InvalidADTSSamplingIndex);
|
||
observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return;
|
||
}
|
||
adtsChanelConfig = (data[offset + 2] & 1) << 2;
|
||
// byte 3
|
||
adtsChanelConfig |= (data[offset + 3] & 192) >>> 6;
|
||
logger.info(loggerName$l, `manifest codec:${audioCodec},ADTS data:type:${adtsObjectType},samplingIndex:${adtsSamplingIndex}[${adtsSamplingRates[adtsSamplingIndex]}Hz],channelConfig:${adtsChanelConfig}`);
|
||
// firefox: freq less than 24kHz = AAC SBR (HE-AAC)
|
||
if (/firefox/i.test(userAgent)) {
|
||
if (adtsSamplingIndex >= 6) {
|
||
adtsObjectType = 5;
|
||
config = new Array(4);
|
||
// HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies
|
||
// there is a factor 2 between frame sample rate and output sample rate
|
||
// multiply frequency by 2 (see table below, equivalent to substract 3)
|
||
extensionSamplingIndex = adtsSamplingIndex - 3;
|
||
}
|
||
else {
|
||
adtsObjectType = 2;
|
||
config = new Array(2);
|
||
}
|
||
// Android : always use AAC
|
||
}
|
||
else if (userAgent.indexOf('android') !== -1) {
|
||
adtsObjectType = 2;
|
||
config = new Array(2);
|
||
}
|
||
else {
|
||
/* for other browsers (Chrome/Vivaldi/Opera ...)
|
||
always force audio type to be HE-AAC SBR, as some browsers do not support audio codec switch properly (like Chrome ...)
|
||
*/
|
||
adtsObjectType = 5;
|
||
config = new Array(4);
|
||
// if (manifest codec is HE-AAC or HE-AACv2) OR (manifest codec not specified AND frequency less than 24kHz)
|
||
if ((audioCodec && (audioCodec.indexOf('mp4a.40.29') !== -1 || audioCodec.indexOf('mp4a.40.5') !== -1)) || (!audioCodec && adtsSamplingIndex >= 6)) {
|
||
// HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies
|
||
// there is a factor 2 between frame sample rate and output sample rate
|
||
// multiply frequency by 2 (see table below, equivalent to substract 3)
|
||
extensionSamplingIndex = adtsSamplingIndex - 3;
|
||
}
|
||
else {
|
||
// if (manifest codec is AAC) OR (manifest codec not specified and mono audio)
|
||
// Chrome fails to play back with low frequency AAC LC mono when initialized with HE-AAC. This is not a problem with stereo.
|
||
if ((audioCodec && audioCodec.indexOf('mp4a.40.2') !== -1) || (!audioCodec && adtsChanelConfig === 1)) {
|
||
adtsObjectType = 2;
|
||
config = new Array(2);
|
||
}
|
||
extensionSamplingIndex = adtsSamplingIndex;
|
||
}
|
||
}
|
||
/* refer to http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Audio_Specific_Config
|
||
ISO 14496-3 (AAC).pdf - Table 1.13 — Syntax of AudioSpecificConfig()
|
||
Audio Profile / Audio Object Type
|
||
0: Null
|
||
1: AAC Main
|
||
2: AAC LC (Low Complexity)
|
||
3: AAC SSR (Scalable Sample Rate)
|
||
4: AAC LTP (Long Term Prediction)
|
||
5: SBR (Spectral Band Replication)
|
||
6: AAC Scalable
|
||
sampling freq
|
||
0: 96000 Hz
|
||
1: 88200 Hz
|
||
2: 64000 Hz
|
||
3: 48000 Hz
|
||
4: 44100 Hz
|
||
5: 32000 Hz
|
||
6: 24000 Hz
|
||
7: 22050 Hz
|
||
8: 16000 Hz
|
||
9: 12000 Hz
|
||
10: 11025 Hz
|
||
11: 8000 Hz
|
||
12: 7350 Hz
|
||
13: Reserved
|
||
14: Reserved
|
||
15: frequency is written explictly
|
||
Channel Configurations
|
||
These are the channel configurations:
|
||
0: Defined in AOT Specifc Config
|
||
1: 1 channel: front-center
|
||
2: 2 channels: front-left, front-right
|
||
*/
|
||
// audioObjectType = profile => profile, the MPEG-4 Audio Object Type minus 1
|
||
config[0] = adtsObjectType << 3;
|
||
// samplingFrequencyIndex
|
||
config[0] |= (adtsSamplingIndex & 14) >> 1;
|
||
config[1] |= (adtsSamplingIndex & 1) << 7;
|
||
// channelConfiguration
|
||
config[1] |= adtsChanelConfig << 3;
|
||
if (adtsObjectType === 5) {
|
||
// extensionSamplingIndex
|
||
config[1] |= (extensionSamplingIndex & 14) >> 1;
|
||
config[2] = (extensionSamplingIndex & 1) << 7;
|
||
// adtsObjectType (force to 2, chrome is checking that object type is less than 5 ???
|
||
// https://chromium.googlesource.com/chromium/src.git/+/master/media/formats/mp4/aac.cc
|
||
config[2] |= 2 << 2;
|
||
config[3] = 0;
|
||
}
|
||
const audioConfig = { esdsConfig: config, samplerate: adtsSamplingRates[adtsSamplingIndex], channelCount: adtsChanelConfig, segmentCodec: 'aac', codec: 'mp4a.40.' + adtsObjectType };
|
||
return audioConfig;
|
||
},
|
||
};
|
||
|
||
class DemuxerBase {
|
||
constructor(observer, remuxer, config, typeSupported, logger) {
|
||
this.observer = observer;
|
||
this.remuxer = remuxer;
|
||
this.config = config;
|
||
this.typeSupported = typeSupported;
|
||
this.logger = logger;
|
||
}
|
||
static probe(data, logger) {
|
||
throw new Error('Method not implemented');
|
||
}
|
||
resetTimeStamp(initPTS90k) { }
|
||
resetInitSegment(initSegment, duration, keyTagInfo, discontinuity) { }
|
||
destroy() { }
|
||
}
|
||
class EsDemuxer extends DemuxerBase {
|
||
constructor(observer, remuxer, config, typeSupported, logger) {
|
||
super(observer, remuxer, config, typeSupported, logger);
|
||
this.observer = observer;
|
||
this.remuxer = remuxer;
|
||
this.config = config;
|
||
this.typeSupported = typeSupported;
|
||
this.logger = logger;
|
||
this.esRemuxer = remuxer;
|
||
}
|
||
}
|
||
class RemuxerBase {
|
||
constructor(observer, config, logger) {
|
||
this.observer = observer;
|
||
this.config = config;
|
||
this.logger = logger;
|
||
}
|
||
resetInitSegment() { }
|
||
resetTimeStamp(initPTS90k) { }
|
||
destroy() { }
|
||
}
|
||
|
||
/*
|
||
* 2018 Apple Inc. All rights reserved.
|
||
*/
|
||
var _a$1;
|
||
let te;
|
||
let td;
|
||
const BrowserBufferUtils = {
|
||
/**
|
||
* Convert a string object into a uint8Array containing utf-8 encoded text
|
||
* @param str The string to convert
|
||
* @returns A uint8Array containing utf-8 encoded text
|
||
|
||
|
||
*/
|
||
strToUtf8array(str) {
|
||
if (!te) {
|
||
te = new TextEncoder();
|
||
}
|
||
return te.encode(str);
|
||
},
|
||
/**
|
||
* Convert a uint8Array containing utf-8 encoded text into a string object
|
||
* @param array The array to convert containing utf-8 encoded text
|
||
* @returns A DOMString containing the decoded text
|
||
*/
|
||
utf8arrayToStr(array) {
|
||
if (!td) {
|
||
td = new TextDecoder('utf-8');
|
||
}
|
||
return td.decode(array);
|
||
},
|
||
};
|
||
const NodeJSBufferUtils = {
|
||
/**
|
||
* Convert a string object into a uint8Array containing utf-8 encoded text
|
||
* @param str The string to convert
|
||
* @returns A uint8Array containing utf-8 encoded text
|
||
*/
|
||
strToUtf8array(str) {
|
||
const buffer = global$1.Buffer.from(str, 'utf-8');
|
||
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
||
},
|
||
/**
|
||
* Convert a uint8Array containing utf-8 encoded text into a string object
|
||
* @param array The array to convert containing utf-8 encoded text
|
||
* @returns A DOMString containing the decoded text
|
||
*/
|
||
utf8arrayToStr(array) {
|
||
return global$1.Buffer.from(array).toString('utf-8');
|
||
},
|
||
};
|
||
const FallbackBufferUtils = {
|
||
/**
|
||
* Convert a string object into a uint8Array containing utf-8 encoded text
|
||
* @param str The string to convert
|
||
* @returns A uint8Array containing utf-8 encoded text
|
||
*/
|
||
strToUtf8array(str) {
|
||
const utf8 = unescape(encodeURIComponent(str));
|
||
const result = new Uint8Array(utf8.length);
|
||
for (let i = 0; i < utf8.length; i++) {
|
||
result[i] = utf8.charCodeAt(i);
|
||
}
|
||
return result;
|
||
},
|
||
/**
|
||
* Convert a uint8Array containing utf-8 encoded text into a string object
|
||
* @param array The array to convert containing utf-8 encoded text
|
||
* @returns A DOMString containing the decoded text
|
||
*/
|
||
utf8arrayToStr(array) {
|
||
return String.fromCharCode.apply(null, Array.from(array));
|
||
},
|
||
};
|
||
let BufferUtils = FallbackBufferUtils;
|
||
if (typeof TextEncoder !== 'undefined' && typeof TextDecoder !== 'undefined') {
|
||
BufferUtils = BrowserBufferUtils;
|
||
}
|
||
else if (typeof ((_a$1 = global$1.Buffer) === null || _a$1 === void 0 ? void 0 : _a$1.from) === 'function') {
|
||
BufferUtils = NodeJSBufferUtils;
|
||
}
|
||
|
||
|
||
const loggerName$k = { name: 'ID3' };
|
||
class ID3 {
|
||
// TODO: <jgainfort 18Mar2021> I am not comfortable with infinite loop ....
|
||
constructor(data, logger) {
|
||
this.logger = logger;
|
||
this._hasTimeStamp = false;
|
||
this._audioType = null;
|
||
this._length = 0;
|
||
this._frames = [];
|
||
let offset = 0, tagSize, endPos, header, len;
|
||
do {
|
||
header = ID3.readUTF(data, offset, 3);
|
||
offset += 3;
|
||
// first check for ID3 header
|
||
if (header === 'ID3') {
|
||
// v2.* tags only
|
||
// skip 16 bit version
|
||
this._minor = data[offset++];
|
||
this._revision = data[offset++];
|
||
const tagFlags = data[offset++]; // 1 byte flag (top 3 bits used)
|
||
if (tagFlags & 128) {
|
||
// is unsynchroized
|
||
this._unsynchronized = true;
|
||
this.logger.error(loggerName$k, 'id3 tag is unsynchronized');
|
||
}
|
||
if (tagFlags & 64) {
|
||
this._hasExtendedHeader = true;
|
||
this.logger.warn(loggerName$k, 'id3 tag has extended header');
|
||
}
|
||
// retrieve tag(s) length
|
||
// The ID3v2 tag size is the sum of the byte length of the extended
|
||
// header, the padding and the frames after unsynchronisation. If a
|
||
// footer is present this equals to ('total size' - 20) bytes, otherwise
|
||
// ('total size' - 10) bytes.
|
||
tagSize = ID3.readSynchSafeUint32(data.subarray(offset, offset + 4));
|
||
offset += 4;
|
||
endPos = offset + tagSize; // tagSize accounts for extended header
|
||
if (this._hasExtendedHeader) {
|
||
// has extended header
|
||
const extendedHeaderSize = ID3.readSynchSafeUint32(data.subarray(offset, offset + 4));
|
||
this.logger.warn(loggerName$k, `id3 tag has ${extendedHeaderSize}-byte extended header. usually 6 or 10 bytes`);
|
||
offset += extendedHeaderSize;
|
||
}
|
||
// read ID3 frames
|
||
if (this.minor > 2) {
|
||
this._parseID3Frames(data, offset, endPos);
|
||
}
|
||
else {
|
||
this.logger.error(loggerName$k, '[id3] doesn\'t support older than v2.3 tags');
|
||
}
|
||
offset = endPos;
|
||
}
|
||
else if (header === '3DI') {
|
||
// http://id3.org/id3v2.4.0-structure chapter 3.4. ID3v2 footer
|
||
offset += 7;
|
||
}
|
||
else {
|
||
offset -= 3;
|
||
len = offset;
|
||
if (len) {
|
||
if (!this.hasTimeStamp) {
|
||
this.logger.warn(loggerName$k, 'ID3 tag found, but no timestamp');
|
||
}
|
||
this._length = len;
|
||
this._payload = data.slice(0, len);
|
||
}
|
||
return;
|
||
}
|
||
} while (true); // eslint-disable-line
|
||
}
|
||
static isHeader(data, offset) {
|
||
if (data[offset] === 73 && data[offset + 1] === 68 && data[offset + 2] === 51) {
|
||
// check version is within range
|
||
if (data[offset + 3] < 255 && data[offset + 4] < 255) {
|
||
// check size is within range
|
||
if (data[offset + 6] < 128 && data[offset + 7] < 128 && data[offset + 8] < 128 && data[offset + 9] < 128) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
static readSynchSafeUint32(data) {
|
||
return (data[0] & 127) * 2097152 + (data[1] & 127) * 16384 + (data[2] & 127) * 128 + (data[3] & 127);
|
||
}
|
||
static readUTF(data, start, len) {
|
||
let result = '', offset = start;
|
||
const end = start + len;
|
||
do {
|
||
result += String.fromCharCode(data[offset++]);
|
||
} while (offset < end);
|
||
return result;
|
||
}
|
||
isID3Frame(data, offset) {
|
||
if (data[offset + 4] < 128 && data[offset + 5] < 128 && data[offset + 6] < 128 && data[offset + 7] < 128) {
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
decodeID3Frame(frame) {
|
||
if (frame.type === 'TXXX') {
|
||
return this.decodeTxxxFrame(frame);
|
||
}
|
||
else if (frame.type === 'WXXX') {
|
||
return this.decodeWxxxFrame(frame);
|
||
}
|
||
else if (frame.type === 'PRIV') {
|
||
return this.decodePrivFrame(frame);
|
||
}
|
||
else if (frame.type[0] === 'T') {
|
||
return this.decodeTextFrame(frame);
|
||
}
|
||
else {
|
||
return { key: frame.type, data: frame.data };
|
||
}
|
||
}
|
||
decodeTxxxFrame(frame) {
|
||
/*
|
||
Format:
|
||
[0] = {Text Encoding}
|
||
[1-?] = {Description}\0{Value}
|
||
*/
|
||
if (frame.size < 2) {
|
||
return undefined;
|
||
}
|
||
if (frame.data[0] !== 3) {
|
||
// only support UTF-8
|
||
return undefined;
|
||
}
|
||
let index = 1;
|
||
const description = this.id3utf8ArrayToStr(frame.data.subarray(index));
|
||
index += description.length + 1;
|
||
const value = this.id3utf8ArrayToStr(frame.data.subarray(index));
|
||
return { key: 'TXXX', description, data: value };
|
||
}
|
||
decodeWxxxFrame(frame) {
|
||
/*
|
||
Format:
|
||
[0] = {Text Encoding}
|
||
[1-?] = {Description}\0{Value}
|
||
*/
|
||
if (frame.size < 2) {
|
||
return undefined;
|
||
}
|
||
if (frame.data[0] !== 3) {
|
||
// only support UTF-8
|
||
return undefined;
|
||
}
|
||
let index = 1;
|
||
const description = this.id3utf8ArrayToStr(frame.data.subarray(index));
|
||
index += description.length + 1;
|
||
// Need to use the BufferUtils version of utf8arrayToStr since it works
|
||
// with arrays that doesn't have a '\0' in the end of the array.
|
||
const value = BufferUtils.utf8arrayToStr(frame.data.subarray(index));
|
||
return { key: 'WXXX', description, data: value };
|
||
}
|
||
decodeTextFrame(frame) {
|
||
/*
|
||
Format:
|
||
[0] = {Text Encoding}
|
||
[1-?] = {Value}
|
||
*/
|
||
if (frame.size < 2) {
|
||
return undefined;
|
||
}
|
||
if (frame.data[0] !== 3) {
|
||
// only support UTF-8
|
||
return undefined;
|
||
}
|
||
const data = frame.data.subarray(1);
|
||
return { key: frame.type, data: this.id3utf8ArrayToStr(data) };
|
||
}
|
||
decodePrivFrame(frame) {
|
||
/*
|
||
Format: <text string>\0<binary data>
|
||
*/
|
||
if (frame.size < 2) {
|
||
return undefined;
|
||
}
|
||
const owner = this.id3utf8ArrayToStr(frame.data);
|
||
const privateData = frame.data.slice(owner.length + 1);
|
||
return { key: 'PRIV', info: owner, data: privateData };
|
||
}
|
||
_extractID3Frame(data, frameId, frameLen, frameBodyOffset, endPos) {
|
||
const frameEnd = frameBodyOffset + frameLen;
|
||
let frame;
|
||
if (frameEnd <= endPos) {
|
||
frame = { type: frameId, data: data.slice(frameBodyOffset, frameEnd) };
|
||
}
|
||
else {
|
||
this.logger.error(loggerName$k, `id3 frame ${frameId} size ${frameLen} exceeded ${endPos}`);
|
||
}
|
||
return frame;
|
||
}
|
||
_parseID3Frames(data, offset, endPos) {
|
||
let tagId, tagLen, tagStart, timestamp;
|
||
while (offset + 8 <= endPos) {
|
||
this.logger.info(loggerName$k, `[id3] _parseID3Frames ${offset} ${endPos}`);
|
||
if (!this.isID3Frame(data, offset)) {
|
||
this.logger.error(loggerName$k, `[id3] illegal id3 frame @ offset ${offset}. skip this id3 tag`);
|
||
return;
|
||
}
|
||
tagId = ID3.readUTF(data, offset, 4);
|
||
offset += 4;
|
||
if (tagId === '') {
|
||
this.logger.info(loggerName$k, '[id3] empty tagId. padding.');
|
||
return;
|
||
}
|
||
tagLen = ID3.readSynchSafeUint32(data.subarray(offset, offset + 4));
|
||
if (tagLen === 0) {
|
||
this.logger.info(loggerName$k, '[id3] zero tag length. padding.');
|
||
return;
|
||
}
|
||
offset += 4;
|
||
data[offset++] << (8 + data[offset++]);
|
||
tagStart = offset;
|
||
this.logger.info(loggerName$k, '[id3] tag id:' + tagId + ' tagLen ' + tagLen + ' offset ' + offset + ' endPos ' + endPos);
|
||
this.logger.qe({ critical: true, name: 'id3Parsed', data: { tagId, tagLen, offset, endPos } });
|
||
const frame = this._extractID3Frame(data, tagId, tagLen, tagStart, endPos);
|
||
if (frame) {
|
||
const id3Frame = this.decodeID3Frame(frame);
|
||
this._frames.push(id3Frame);
|
||
}
|
||
switch (tagId) {
|
||
case 'PRIV':
|
||
// this.logger.info(loggerName, 'parse frame:' + Hex.hexDump(data.subarray(offset,endPos)));
|
||
// owner should be "com.apple.streaming.transportStreamTimestamp"
|
||
if (tagLen === 53 && ID3.readUTF(data, offset, 44) === 'com.apple.streaming.transportStreamTimestamp') {
|
||
offset += 44;
|
||
// smelling even better ! we found the right descriptor
|
||
// skip null character (string end) + 3 first bytes
|
||
offset += 4;
|
||
// timestamp is 33 bit expressed as a big-endian eight-octet number, with the upper 31 bits set to zero.
|
||
const pts33Bit = data[offset++] & 1;
|
||
this._hasTimeStamp = true;
|
||
timestamp = ((data[offset++] << 23) + (data[offset++] << 15) + (data[offset++] << 7) + data[offset++]) / 45;
|
||
if (pts33Bit) {
|
||
timestamp += 47721858.84; // 2^32 / 90
|
||
}
|
||
timestamp = Math.round(timestamp);
|
||
this.logger.info(loggerName$k, `ID3 timestamp found: ${timestamp}`);
|
||
this._timeStamp = timestamp;
|
||
}
|
||
else if (tagLen >= 45 && ID3.readUTF(data, offset, 36) === 'com.apple.streaming.audioDescription') {
|
||
offset += 37; // skip tag and null terminator
|
||
this._audioType = ID3.readUTF(data, offset, 4);
|
||
offset += 4;
|
||
// skip everything else for now, don't think we need anything from the audio setup
|
||
offset += tagLen - 41;
|
||
this.logger.info(loggerName$k, `ID3 audio description found: ${this._audioType}`);
|
||
}
|
||
else {
|
||
offset += tagLen;
|
||
}
|
||
break;
|
||
default:
|
||
{
|
||
offset += tagLen;
|
||
}
|
||
break;
|
||
}
|
||
this.logger.info(loggerName$k, `[id3] ${tagId} default tagLen ${tagLen} offset ${offset} endPos ${endPos}`);
|
||
}
|
||
}
|
||
// id3utf8ArrayToStr is different from BufferUtils.utfarrayToStr. It exists when the string ends.
|
||
// BufferUtils.utfarrayToStr will decode the entire array.
|
||
// http://stackoverflow.com/questions/8936984/uint8array-to-string-in-javascript/22373197
|
||
// http://www.onicos.com/staff/iz/amuse/javascript/expert/utf.txt
|
||
/** utf.js - UTF-8 <=> UTF-16 convertion
|
||
*
|
||
* Copyright (C) 1999 Masanao Izumo <iz@onicos.co.jp>
|
||
* Version: 1.0
|
||
* LastModified: Dec 25 1999
|
||
* This library is free. You can redistribute it and/or modify it.
|
||
*/
|
||
id3utf8ArrayToStr(array) {
|
||
let char2;
|
||
let char3;
|
||
let out = '';
|
||
let i = 0;
|
||
const length = array.length;
|
||
while (i < length) {
|
||
const c = array[i++];
|
||
switch (c >> 4) {
|
||
case 0:
|
||
return out;
|
||
case 1:
|
||
case 2:
|
||
case 3:
|
||
case 4:
|
||
case 5:
|
||
case 6:
|
||
case 7:
|
||
// 0xxxxxxx
|
||
out += String.fromCharCode(c);
|
||
break;
|
||
case 12:
|
||
case 13:
|
||
// 110x xxxx 10xx xxxx
|
||
char2 = array[i++];
|
||
out += String.fromCharCode(((c & 31) << 6) | (char2 & 63));
|
||
break;
|
||
case 14:
|
||
// 1110 xxxx 10xx xxxx 10xx xxxx
|
||
char2 = array[i++];
|
||
char3 = array[i++];
|
||
out += String.fromCharCode(((c & 15) << 12) | ((char2 & 63) << 6) | ((char3 & 63) << 0));
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
get hasTimeStamp() {
|
||
return this._hasTimeStamp;
|
||
}
|
||
get timeStamp() {
|
||
return this._timeStamp;
|
||
}
|
||
get audioType() {
|
||
return this._audioType;
|
||
}
|
||
get length() {
|
||
return this._length;
|
||
}
|
||
get payload() {
|
||
return this._payload;
|
||
}
|
||
get frames() {
|
||
return this._frames;
|
||
}
|
||
get minor() {
|
||
return this._minor;
|
||
}
|
||
get revision() {
|
||
return this._revision;
|
||
}
|
||
}
|
||
var ID3$1 = ID3;
|
||
|
||
|
||
const loggerName$j = { name: 'AACDemuxer' };
|
||
class AACDemuxer extends EsDemuxer {
|
||
resetInitSegment(initSegment, duration) {
|
||
this.audioConfig = undefined;
|
||
this.audioTrack = undefined;
|
||
this.duration = duration;
|
||
}
|
||
static probe(data, logger) {
|
||
// check if data contains ID3 timestamp and ADTS sync word
|
||
const id3 = new ID3$1(data, logger);
|
||
let offset, length;
|
||
// Look for ADTS header | 1111 1111 | 1111 X00X | where X can be either 0 or 1
|
||
// Layer bits (position 14 and 15) in header should be always 0 for ADTS
|
||
// More info https://wiki.multimedia.cx/index.php?title=ADTS
|
||
for (offset = id3.length, length = Math.min(data.length - 1, offset + 100); offset < length; offset++) {
|
||
if (data[offset] === 255 && (data[offset + 1] & 246) === 240) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
// feed incoming data to the front of the parsing pipeline
|
||
append(data, timeOffset, contiguous, accurateTimeOffset, keyTagInfo) {
|
||
const id3 = new ID3$1(data, this.logger);
|
||
const pts = id3.hasTimeStamp ? 90 * id3.timeStamp : 90000 * timeOffset;
|
||
if (!id3.hasTimeStamp) {
|
||
this.logger.info(loggerName$j, `missing id3 timestamp at timeOffset ${timeOffset.toFixed(3)}`);
|
||
}
|
||
let frameLength, frameIndex, offset, headerLength, stamp, length;
|
||
let aacSample;
|
||
let id3Track = undefined;
|
||
let frames = undefined;
|
||
let payload = undefined;
|
||
if (id3.length) {
|
||
payload = id3.payload;
|
||
if (id3.frames.length) {
|
||
frames = id3.frames;
|
||
}
|
||
this.logger.info(loggerName$j, `[id3] init id3 tag pts=${pts} frames=${id3.frames.length}`);
|
||
id3Track = { id3Samples: [{ pts: pts, dts: pts, data: payload, frames: frames }], inputTimescale: 90000 };
|
||
}
|
||
// Look for ADTS header
|
||
for (offset = id3.length, length = data.length; offset < length - 1; offset++) {
|
||
if (data[offset] === 255 && (data[offset + 1] & 246) === 240) {
|
||
break;
|
||
}
|
||
}
|
||
if (!this.audioConfig) {
|
||
this.audioConfig = ADTS.getAudioConfig(this.observer, data, offset, undefined, this.logger);
|
||
if (!this.audioConfig) {
|
||
throw 'failed to parse adts config';
|
||
}
|
||
this.logger.info(loggerName$j, `parsed codec:${this.audioConfig.codec},rate:${this.audioConfig.samplerate},nb channel:${this.audioConfig.channelCount}`);
|
||
}
|
||
if (!this.audioTrack) {
|
||
const info = { id: 258, inputTimescale: 90000, timescale: NaN, duration: this.duration, encrypted: false, keyTagInfo };
|
||
const parsingData = { len: 0, sequenceNumber: 0, esSamples: [] };
|
||
this.audioTrack = { info, parsingData, type: 'audio', config: this.audioConfig };
|
||
}
|
||
if (id3.audioType === 'zaac' || id3.audioType === 'zach' || id3.audioType === 'zacp') {
|
||
this.audioTrack.info.encrypted = true;
|
||
this.logger.info(loggerName$j, 'found encrypted aac');
|
||
}
|
||
frameIndex = 0;
|
||
const frameDuration = 92160000 / this.audioConfig.samplerate;
|
||
while (offset + 5 < length) {
|
||
// The protection skip bit tells us if we have 2 bytes of CRC data at the end of the ADTS header
|
||
headerLength = data[offset + 1] & 1 ? 7 : 9;
|
||
// retrieve frame size
|
||
frameLength = ((data[offset + 3] & 3) << 11) | (data[offset + 4] << 3) | ((data[offset + 5] & 224) >>> 5);
|
||
frameLength -= headerLength;
|
||
// stamp = pes.pts;
|
||
if (frameLength > 0 && offset + headerLength + frameLength <= length) {
|
||
stamp = pts + frameIndex * frameDuration;
|
||
aacSample = { unit: data.subarray(offset + headerLength, offset + headerLength + frameLength), pts: stamp, dts: stamp, keyTagInfo: keyTagInfo };
|
||
this.audioTrack.parsingData.esSamples.push(aacSample);
|
||
this.audioTrack.parsingData.len += frameLength;
|
||
offset += frameLength + headerLength;
|
||
frameIndex++;
|
||
// look for ADTS header (0xFFFx)
|
||
for (; offset < length - 1; offset++) {
|
||
if (ID3$1.isHeader(data, offset)) {
|
||
const embedId3 = new ID3$1(data.subarray(offset), this.logger);
|
||
if (embedId3.length > 0) {
|
||
offset += embedId3.length; // parses the interleaved ID3 packet
|
||
const localPts = embedId3.hasTimeStamp ? 90 * embedId3.timeStamp : pts;
|
||
id3Track.id3Samples.push({ pts: localPts, dts: localPts, data: embedId3.payload, frames: embedId3.frames });
|
||
}
|
||
else {
|
||
this.logger.error(loggerName$j, `[id3] invalid length ${length}`);
|
||
}
|
||
}
|
||
if (data[offset] === 255 && (data[offset + 1] & 246) === 240) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
break;
|
||
}
|
||
}
|
||
this.esRemuxer.remuxEsTracks(this.audioTrack, undefined, id3Track, undefined, timeOffset, contiguous, accurateTimeOffset, keyTagInfo);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* Utility methods to perform bit manipulations.
|
||
*
|
||
* 2019 Apple Inc. All rights reserved.
|
||
*/
|
||
class BitstreamUtils {
|
||
/**
|
||
* This method is used to read a value from a bit-range in the data buffer.
|
||
*
|
||
* @param {Uint8Array} data - Data buffer.
|
||
* @param {BitStream} bitStream - Current position in the data buffer; gets updated by this method.
|
||
* @param {number} numBits - Total number of bits to read.
|
||
*
|
||
* @return {number} Numeric value of the bits read.
|
||
*
|
||
* Usage: bsReadAndUpdate(data, {byteOffset: 1, usedBits: 3}, 7)
|
||
* => will read 5 bits from data[1] and 2 bits from data[2] and return the numeric value of those 7 bits.
|
||
* => will update the bitStream to {byteOffset: 2, usedBits: 2}
|
||
*/
|
||
bsReadAndUpdate(data, bitStream, numBits) {
|
||
const result = this.readBits(data, bitStream, numBits);
|
||
this.updateOffset(bitStream, numBits);
|
||
return result;
|
||
}
|
||
/**
|
||
* This method is used to write a value to a bit-range in the data buffer.
|
||
*
|
||
* @param {Uint8Array} data - Data buffer.
|
||
* @param {BitStream} bitStream - Current position in the data buffer; gets updated by this method.
|
||
* @param {number} numBits - Total number of bits to write.
|
||
* @param {number} value - The value to write.
|
||
*
|
||
* Usage: bsWriteAndUpdate(data, {byteOffset: 1, usedBits: 3}, 7)
|
||
* => will write most significant 5 bits of value to data[1] and remaining 4 bits to data[2].
|
||
* => will update the bitStream to {byteOffset: 2, usedBits: 2}
|
||
*/
|
||
bsWriteAndUpdate(data, bitStream, numBits, value) {
|
||
const result = this.writeBits(data, bitStream, numBits, value);
|
||
this.updateOffset(bitStream, numBits);
|
||
return result;
|
||
}
|
||
/**
|
||
* This method is used to update the bitStream offsets.
|
||
*
|
||
* @param {BitStream} bitStream - Current offset (In) / New offset (Out).
|
||
* @param {number} numBits - Total number of bits to skip.
|
||
*
|
||
* Usage: bsSkip({byteOffset: 1, usedBits: 3}, 7)
|
||
* => will update the bitStream to {byteOffset: 2, usedBits: 2}
|
||
*/
|
||
bsSkip(bitStream, numBits) {
|
||
this.updateOffset(bitStream, numBits);
|
||
}
|
||
// private helper methods
|
||
readBits(data, bitStream, numBits) {
|
||
if (!data || !bitStream) {
|
||
return undefined;
|
||
}
|
||
let offset = bitStream.byteOffset;
|
||
const { usedBits } = bitStream;
|
||
if (usedBits >= 8 || usedBits + numBits > 32) {
|
||
return undefined;
|
||
}
|
||
let result;
|
||
// some strong typed variables for reliable bit manipulation
|
||
const temp = new Uint32Array(1); // unsigned 32 bit for temporary storage
|
||
const mask = new Uint32Array(1); // unsigned 32 bit mask value
|
||
const byte = new Uint8Array(1); // unsigned 8 bit for temporary storage
|
||
if (usedBits >= 8 || numBits > 32) {
|
||
return undefined;
|
||
}
|
||
/*
|
||
* read msb to lsb from data[offset] starting from the first unused bit of data[offset]
|
||
* for e.g. data[0] => 11110010
|
||
* data[1] => 00111101
|
||
* data[2] => 00101001
|
||
* numBits => 18
|
||
* usedBits => 2
|
||
* byteOffset => 0
|
||
* will fetch bits
|
||
* data[0] => xx100011
|
||
* data[1] => 11011100
|
||
* data[2] => 1011xxxx
|
||
* and return
|
||
* 00000000 00000010 00111101 11001011
|
||
*/
|
||
if (usedBits) {
|
||
// read unused bits from the partial byte
|
||
const bits = 8 - usedBits;
|
||
const shift = numBits < bits ? bits - numBits : 0;
|
||
mask[0] = 4278190080 >>> (32 - bits);
|
||
result = (data[offset] & mask[0]) >>> shift;
|
||
offset += 1;
|
||
numBits -= bits;
|
||
}
|
||
while (numBits > 0) {
|
||
byte[0] = data[offset];
|
||
// read remaining bits, upto 8 bits at a time
|
||
const bits = Math.min(numBits, 8);
|
||
const shift = 8 - bits;
|
||
mask[0] = (4278190080 >>> (24 + shift)) << shift;
|
||
temp[0] = (byte[0] & mask[0]) >> shift;
|
||
result = !result ? temp[0] : (result << bits) | temp[0];
|
||
offset += 1;
|
||
numBits -= bits;
|
||
}
|
||
return result;
|
||
}
|
||
writeBits(data, bitStream, numBits, value) {
|
||
if (!data || !bitStream) {
|
||
return undefined;
|
||
}
|
||
let offset = bitStream.byteOffset;
|
||
const { usedBits } = bitStream;
|
||
if (usedBits >= 8 || usedBits + numBits > 32) {
|
||
return undefined;
|
||
}
|
||
// some strong typed variables for reliable bit manipulation
|
||
const tval = new Uint32Array(1); // unsigned 32 bit to store the incoming value
|
||
const temp = new Uint32Array(1); // unsigned 32 bit for temporary storage
|
||
const mask = new Uint32Array(1); // unsigned 32 bit mask value
|
||
const byte = new Uint8Array(1); // unsigned 8 bit for temporary storage
|
||
tval[0] = value;
|
||
/*
|
||
* write msb to lsb from value into data[offset] starting from the first unused bit of data[offset]
|
||
* for e.g. value => 00000000 00000010 00111101 11001011
|
||
* numBits => 18
|
||
* usedBits => 2
|
||
* byteOffset => 0
|
||
* will get written as
|
||
* data[0] => xx100011
|
||
* data[1] => 11011100
|
||
* data[2] => 1011xxxx
|
||
*/
|
||
if (usedBits) {
|
||
// left align the value, mask the bits, and then right align to exclude the used bits
|
||
temp[0] = tval[0] << (32 - numBits);
|
||
mask[0] = 4278190080;
|
||
byte[0] = (temp[0] & mask[0]) >>> (24 + usedBits);
|
||
// clear the bits and write
|
||
data[offset] &= ~(mask[0] >>> (24 + usedBits));
|
||
data[offset] |= byte[0];
|
||
offset += 1;
|
||
numBits -= 8 - usedBits;
|
||
}
|
||
while (numBits > 0) {
|
||
// left align the remaining bits of value and write in blocks of 8
|
||
temp[0] = tval[0] << (32 - numBits);
|
||
mask[0] = 4278190080;
|
||
byte[0] = (temp[0] & mask[0]) >>> 24;
|
||
// right align the mask, and then right shift and left shift to clear the used bits
|
||
const shift = numBits < 0 ? 8 - numBits : 0;
|
||
data[offset] &= ~(((mask[0] >>> 24) >>> shift) << shift);
|
||
// write the bits
|
||
data[offset] |= byte[0];
|
||
numBits -= 8;
|
||
offset += 1;
|
||
}
|
||
return 0;
|
||
}
|
||
updateOffset(bitStream, numBits) {
|
||
if (!bitStream || !numBits || bitStream.usedBits + numBits > 32) {
|
||
return;
|
||
}
|
||
// calculate the number of bits seen in the current byte offset
|
||
const bitsSeenInByte = bitStream.usedBits % 8;
|
||
// calculate the bytes and bits based on the last read/write operation
|
||
const bytesAdvanced = Math.floor((bitsSeenInByte + numBits) / 8);
|
||
const bitsAdvanced = (bitsSeenInByte + numBits) % 8;
|
||
// update the new position
|
||
bitStream.byteOffset += bytesAdvanced;
|
||
bitStream.usedBits = bitsAdvanced;
|
||
}
|
||
}
|
||
|
||
const loggerName$i = { name: 'Dolby' };
|
||
const samplingRateMap = [48000, 44100, 32000];
|
||
const frameSizeMap = [
|
||
64,
|
||
69,
|
||
96,
|
||
64,
|
||
70,
|
||
96,
|
||
80,
|
||
87,
|
||
120,
|
||
80,
|
||
88,
|
||
120,
|
||
96,
|
||
104,
|
||
144,
|
||
96,
|
||
105,
|
||
144,
|
||
112,
|
||
121,
|
||
168,
|
||
112,
|
||
122,
|
||
168,
|
||
128,
|
||
139,
|
||
192,
|
||
128,
|
||
140,
|
||
192,
|
||
160,
|
||
174,
|
||
240,
|
||
160,
|
||
175,
|
||
240,
|
||
192,
|
||
208,
|
||
288,
|
||
192,
|
||
209,
|
||
288,
|
||
224,
|
||
243,
|
||
336,
|
||
224,
|
||
244,
|
||
336,
|
||
256,
|
||
278,
|
||
384,
|
||
256,
|
||
279,
|
||
384,
|
||
320,
|
||
348,
|
||
480,
|
||
320,
|
||
349,
|
||
480,
|
||
384,
|
||
417,
|
||
576,
|
||
384,
|
||
418,
|
||
576,
|
||
448,
|
||
487,
|
||
672,
|
||
448,
|
||
488,
|
||
672,
|
||
512,
|
||
557,
|
||
768,
|
||
512,
|
||
558,
|
||
768,
|
||
640,
|
||
696,
|
||
960,
|
||
640,
|
||
697,
|
||
960,
|
||
768,
|
||
835,
|
||
1152,
|
||
768,
|
||
836,
|
||
1152,
|
||
896,
|
||
975,
|
||
1344,
|
||
896,
|
||
976,
|
||
1344,
|
||
1024,
|
||
1114,
|
||
1536,
|
||
1024,
|
||
1115,
|
||
1536,
|
||
1152,
|
||
1253,
|
||
1728,
|
||
1152,
|
||
1254,
|
||
1728,
|
||
1280,
|
||
1393,
|
||
1920,
|
||
1280,
|
||
1394,
|
||
1920,
|
||
];
|
||
const Dolby = {
|
||
getFrameDuration: function (config, timescale) {
|
||
return (1536 / config.samplerate) * timescale;
|
||
},
|
||
getAudioConfig: function (observer, data, offset, logger) {
|
||
let payload;
|
||
if (offset + 8 > data.length) {
|
||
payload = new FragParsingError(true, 'error parsing ac-3, not enough data', ErrorResponses.InsufficientAC3Data);
|
||
observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return undefined;
|
||
}
|
||
if (data[offset] !== 11 || data[offset + 1] !== 119) {
|
||
// payload = { type: ErrorTypes.MEDIA_ERROR, details: ErrorDetails.FRAG_PARSING_ERROR, fatal: true, reason: 'invalid ac-3 magic' };
|
||
payload = new FragParsingError(true, 'invalid ac-3 magic', ErrorResponses.InvalidAC3Magic);
|
||
observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return undefined;
|
||
}
|
||
const samplingRateCode = data[offset + 4] >> 6;
|
||
if (samplingRateCode >= 3) {
|
||
payload = new FragParsingError(true, `invalid ac-3 samplingRateCode:${samplingRateCode}`, ErrorResponses.InvalidAC3SamplingRateCode);
|
||
observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return undefined;
|
||
}
|
||
// get frame size
|
||
const frameSizeCode = data[offset + 4] & 63;
|
||
const frameLength = frameSizeMap[frameSizeCode * 3 + samplingRateCode] * 2;
|
||
const channelMode = data[offset + 6] >> 5;
|
||
let skipCount = 0;
|
||
if (channelMode === 2) {
|
||
skipCount += 2;
|
||
}
|
||
else {
|
||
if (channelMode & 1 && channelMode !== 1) {
|
||
skipCount += 2;
|
||
}
|
||
if (channelMode & 4) {
|
||
skipCount += 2;
|
||
}
|
||
}
|
||
const lfeon = (((data[offset + 6] << 8) | data[offset + 7]) >> (12 - skipCount)) & 1;
|
||
const channelsMap = [2, 1, 2, 3, 3, 4, 4, 5];
|
||
const channelCount = channelsMap[channelMode] + lfeon;
|
||
const bsid = data[offset + 5] >> 3;
|
||
const bsmod = data[offset + 5] & 7;
|
||
const extraData = (samplingRateCode << 22) | (bsid << 17) | (bsmod << 14) | (channelMode << 11) | (lfeon << 10) | ((frameSizeCode >> 1) << 5);
|
||
const samplerate = samplingRateMap[samplingRateCode];
|
||
logger.info(loggerName$i, `parsed codec: ac-3, rate:${samplerate}, nb channel:${channelCount}, first frameLength:${frameLength}`);
|
||
const audioConfig = { samplerate: samplerate, channelCount: channelCount, segmentCodec: 'ac3', codec: 'ac-3', extraData: extraData };
|
||
return audioConfig;
|
||
},
|
||
getFrameLength: function (observer, data, offset) {
|
||
let payload;
|
||
if (offset + 8 > data.length) {
|
||
payload = new FragParsingError(true, 'error parsing ac-3, not enough data', ErrorResponses.InsufficientAC3Data);
|
||
observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return undefined;
|
||
}
|
||
if (data[offset] !== 11 || data[offset + 1] !== 119) {
|
||
payload = new FragParsingError(true, 'invalid ac-3 magic', ErrorResponses.InvalidAC3Magic);
|
||
observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return undefined;
|
||
}
|
||
const samplingRateCode = data[offset + 4] >> 6;
|
||
if (samplingRateCode >= 3) {
|
||
payload = new FragParsingError(true, `invalid ac-3 samplingRateCode:${samplingRateCode}`, ErrorResponses.InvalidAC3SamplingRateCode);
|
||
observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return undefined;
|
||
}
|
||
// get frame size
|
||
const frameSizeCode = data[offset + 4] & 63;
|
||
return frameSizeMap[frameSizeCode * 3 + samplingRateCode] * 2;
|
||
},
|
||
};
|
||
|
||
const loggerName$h = { name: 'AC3Demuxer' };
|
||
class AC3Demuxer extends EsDemuxer {
|
||
resetInitSegment(initSegment, duration) {
|
||
this.audioConfig = undefined;
|
||
this.audioTrack = undefined;
|
||
this.duration = duration;
|
||
}
|
||
static probe(data, logger) {
|
||
// check if data contains ID3 timestamp and AC3 sync bytes
|
||
const id3 = new ID3$1(data, logger), offset = id3.length;
|
||
// look for the ac-3 sync bytes
|
||
if (id3.hasTimeStamp && data[offset] === 11 && data[offset + 1] === 119) {
|
||
// check the bsid to confirm ac-3
|
||
const bu = new BitstreamUtils();
|
||
const bsid = bu.bsReadAndUpdate(data, { byteOffset: offset + 5, usedBits: 0 }, 5);
|
||
if (bsid < 16) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
// feed incoming data to the front of the parsing pipeline
|
||
append(data, timeOffset, contiguous, accurateTimeOffset, keyTagInfo) {
|
||
const id3 = new ID3$1(data, this.logger);
|
||
const pts = 90 * id3.timeStamp;
|
||
const length = data.byteLength;
|
||
let frameIndex = 0;
|
||
let offset = id3.length;
|
||
if (!this.audioConfig) {
|
||
this.audioConfig = Dolby.getAudioConfig(this.observer, data, offset, this.logger);
|
||
}
|
||
if (!this.audioConfig) {
|
||
throw 'failed to parse ac3 config';
|
||
}
|
||
if (!this.audioTrack) {
|
||
const info = { id: 258, inputTimescale: 90000, timescale: NaN, duration: this.duration, encrypted: false, keyTagInfo };
|
||
const parsingData = { len: 0, sequenceNumber: 0, esSamples: [] };
|
||
this.audioTrack = { info, parsingData, type: 'audio', config: this.audioConfig };
|
||
}
|
||
const frameDuration = Dolby.getFrameDuration(this.audioConfig, this.audioTrack.info.inputTimescale); // (1536 / this.audioConfig.samplerate) * this.audioTrack.inputTimescale;
|
||
if (id3.audioType === 'zac3') {
|
||
this.audioTrack.info.encrypted = true;
|
||
this.logger.info(loggerName$h, 'found encrypted ac3');
|
||
}
|
||
while (offset < length) {
|
||
if (ID3$1.isHeader(data, offset)) {
|
||
const id3 = new ID3$1(data.subarray(offset), this.logger);
|
||
offset += id3.length; // skip the interleaved ID3 packet
|
||
}
|
||
if (data[offset] !== 11 || data[offset + 1] !== 119) {
|
||
const payload = new FragParsingError(true, 'invalid ac-3 magic', ErrorResponses.InvalidAC3Magic);
|
||
this.observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return;
|
||
}
|
||
const frameLength = Dolby.getFrameLength(this.observer, data, offset);
|
||
const stamp = pts + frameIndex * frameDuration;
|
||
const ac3Sample = { unit: data.subarray(offset, offset + frameLength), pts: stamp, dts: stamp, keyTagInfo: keyTagInfo };
|
||
this.audioTrack.parsingData.esSamples.push(ac3Sample);
|
||
this.audioTrack.parsingData.len += frameLength;
|
||
offset += frameLength;
|
||
frameIndex++;
|
||
}
|
||
this.esRemuxer.remuxEsTracks(this.audioTrack, undefined, { id3Samples: [{ pts: pts, dts: pts, data: id3.payload, frames: id3.frames }], inputTimescale: this.audioTrack.info.inputTimescale }, undefined, timeOffset, contiguous, accurateTimeOffset, keyTagInfo);
|
||
}
|
||
}
|
||
|
||
const loggerName$g = { name: 'DDPlus' };
|
||
const DDPlus = {
|
||
getFrameLength: function (observer, data, offset, logger) {
|
||
const bs = new BitstreamUtils();
|
||
let firstIndSubstream = false;
|
||
let totalFrameLength = 0;
|
||
let payload;
|
||
while (offset < data.length) {
|
||
if (offset + 8 > data.length) {
|
||
payload = new FragParsingError(true, 'error parsing ec-3, not enough data', ErrorResponses.InsufficientEC3Data);
|
||
observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return undefined;
|
||
}
|
||
// skip the ID3 packet, if present
|
||
let id3Length = 0;
|
||
if (ID3$1.isHeader(data, offset)) {
|
||
const id3 = new ID3$1(data.subarray(offset), logger);
|
||
id3Length = id3.length || 0;
|
||
offset += id3Length;
|
||
}
|
||
// get syncword (16 bits)
|
||
if (data[offset] !== 11 || data[offset + 1] !== 119) {
|
||
payload = new FragParsingError(true, 'invalid ec-3 magic', ErrorResponses.InvalidEC3Magic);
|
||
observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return undefined;
|
||
}
|
||
// skip the syncword and start parsing
|
||
const bitStream = { byteOffset: offset + 2, usedBits: 0 };
|
||
// get strmtyp & substreamid
|
||
const strmtyp = bs.bsReadAndUpdate(data, bitStream, 2);
|
||
const substreamid = bs.bsReadAndUpdate(data, bitStream, 3);
|
||
if (strmtyp === 0 || strmtyp === 2) {
|
||
if (firstIndSubstream === true) {
|
||
if (substreamid === 0) {
|
||
// we're seen all dependent sub-streams
|
||
break;
|
||
}
|
||
}
|
||
else {
|
||
firstIndSubstream = true; // mark that the first independent substream is seen
|
||
}
|
||
}
|
||
else if (strmtyp !== 1) {
|
||
payload = new FragParsingError(true, 'reserved stream type', ErrorResponses.ReservedStreamType);
|
||
observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return undefined;
|
||
}
|
||
// get frmsiz
|
||
const frmsiz = bs.bsReadAndUpdate(data, bitStream, 11);
|
||
// advance to the next syncframe
|
||
const frameLength = (frmsiz + 1) * 2;
|
||
offset += frameLength;
|
||
totalFrameLength += frameLength + (id3Length || 0);
|
||
}
|
||
return totalFrameLength;
|
||
},
|
||
getAudioConfig: function (observer, data, offset, logger) {
|
||
const frameInfo = {
|
||
frmsiz: 0,
|
||
fscod: 0,
|
||
numblkscod: 0,
|
||
acmod: 0,
|
||
lfeon: 0,
|
||
bsid: 0,
|
||
strmtyp: 0,
|
||
substreamid: 0,
|
||
chanmape: 0,
|
||
chanmap: 0,
|
||
mixdef: 0,
|
||
mixdeflen: 0,
|
||
bsmod: 0,
|
||
};
|
||
const sampleInfo = {
|
||
fscod: 0,
|
||
acmod: 0,
|
||
lfeon: 0,
|
||
bsid: 0,
|
||
bsmod: 0,
|
||
chan_loc: 0,
|
||
data_rate: 0,
|
||
num_ind_sub: 0,
|
||
num_dep_sub: [],
|
||
complexity_index_type_a: 0,
|
||
};
|
||
const bs = new BitstreamUtils();
|
||
let firstIndSubstream = false;
|
||
let totalFrameLength = 0;
|
||
let payload;
|
||
while (offset < data.length) {
|
||
if (offset + 8 > data.length) {
|
||
payload = new FragParsingError(true, 'error parsing ec-3, not enough data', ErrorResponses.InsufficientEC3Data);
|
||
observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return undefined;
|
||
}
|
||
// skip the ID3 packet, if present
|
||
let id3Length = 0;
|
||
if (ID3$1.isHeader(data, offset)) {
|
||
const id3 = new ID3$1(data.subarray(offset), logger);
|
||
id3Length = id3.length || 0;
|
||
offset += id3Length;
|
||
}
|
||
// get syncword (16 bits)
|
||
if (data[offset] !== 11 || data[offset + 1] !== 119) {
|
||
payload = new FragParsingError(true, 'invalid ec-3 magic', ErrorResponses.InvalidEC3Magic);
|
||
observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return undefined;
|
||
}
|
||
// skip the syncword and start parsing
|
||
const bitStream = { byteOffset: offset + 2, usedBits: 0 };
|
||
// get strmtyp & substreamid
|
||
frameInfo.strmtyp = bs.bsReadAndUpdate(data, bitStream, 2);
|
||
frameInfo.substreamid = bs.bsReadAndUpdate(data, bitStream, 3);
|
||
if (frameInfo.strmtyp === 0 || frameInfo.strmtyp === 2) {
|
||
if (firstIndSubstream === true) {
|
||
if (frameInfo.substreamid === 0) {
|
||
// we're seen all dependent sub-streams
|
||
break;
|
||
}
|
||
}
|
||
else {
|
||
firstIndSubstream = true; // mark that the first independent substream is seen
|
||
}
|
||
sampleInfo.num_ind_sub++; // independent substream
|
||
sampleInfo.num_dep_sub.push(0); // initialize the dependent sub-stream count to 0
|
||
}
|
||
else if (frameInfo.strmtyp === 1) {
|
||
sampleInfo.num_dep_sub[sampleInfo.num_ind_sub - 1]++; // dependent substream
|
||
}
|
||
else {
|
||
payload = new FragParsingError(true, 'reserved stream type', ErrorResponses.ReservedStreamType);
|
||
observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return undefined;
|
||
}
|
||
// get frmsiz
|
||
frameInfo.frmsiz = bs.bsReadAndUpdate(data, bitStream, 11);
|
||
// get fscod, numblkscod
|
||
frameInfo.fscod = bs.bsReadAndUpdate(data, bitStream, 2);
|
||
if (frameInfo.fscod === 3) {
|
||
bs.bsSkip(bitStream, 2);
|
||
frameInfo.numblkscod = 3;
|
||
}
|
||
else {
|
||
frameInfo.numblkscod = bs.bsReadAndUpdate(data, bitStream, 2);
|
||
}
|
||
// get acmod
|
||
frameInfo.acmod = bs.bsReadAndUpdate(data, bitStream, 3);
|
||
// get lfeon
|
||
frameInfo.lfeon = bs.bsReadAndUpdate(data, bitStream, 1);
|
||
// get bsid
|
||
frameInfo.bsid = bs.bsReadAndUpdate(data, bitStream, 5);
|
||
bs.bsSkip(bitStream, 5);
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 8);
|
||
}
|
||
if (frameInfo.acmod === 0) {
|
||
bs.bsSkip(bitStream, 5);
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 8);
|
||
}
|
||
}
|
||
if (frameInfo.strmtyp === 1) {
|
||
// get chanmape
|
||
frameInfo.chanmape = bs.bsReadAndUpdate(data, bitStream, 1);
|
||
if (frameInfo.chanmape) {
|
||
// get chanmap
|
||
frameInfo.chanmap = bs.bsReadAndUpdate(data, bitStream, 16);
|
||
}
|
||
}
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
if (frameInfo.acmod > 2) {
|
||
bs.bsSkip(bitStream, 2);
|
||
}
|
||
if (frameInfo.acmod & 1 && frameInfo.acmod > 2) {
|
||
bs.bsSkip(bitStream, 6);
|
||
}
|
||
if (frameInfo.acmod & 4) {
|
||
bs.bsSkip(bitStream, 6);
|
||
}
|
||
if (frameInfo.lfeon) {
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 5);
|
||
}
|
||
}
|
||
if (frameInfo.strmtyp === 0) {
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 6);
|
||
}
|
||
if (frameInfo.acmod === 0) {
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 6);
|
||
}
|
||
}
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 6);
|
||
}
|
||
// get mixdef
|
||
frameInfo.mixdef = bs.bsReadAndUpdate(data, bitStream, 2);
|
||
if (frameInfo.mixdef === 1) {
|
||
bs.bsSkip(bitStream, 5);
|
||
}
|
||
else if (frameInfo.mixdef === 2) {
|
||
bs.bsSkip(bitStream, 12);
|
||
}
|
||
else if (frameInfo.mixdef === 3) {
|
||
// get mixdeflen
|
||
frameInfo.mixdeflen = bs.bsReadAndUpdate(data, bitStream, 5);
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 5);
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 4);
|
||
}
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 4);
|
||
}
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 4);
|
||
}
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 4);
|
||
}
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 4);
|
||
}
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 4);
|
||
}
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 4);
|
||
}
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 4);
|
||
}
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 4);
|
||
}
|
||
}
|
||
}
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 5);
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 7);
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 8);
|
||
}
|
||
}
|
||
}
|
||
// skip entire block that includes mixdata and mixdatafill
|
||
const skipBytes = frameInfo.mixdeflen + 2 + (bitStream.usedBits ? 1 : 0);
|
||
bitStream.byteOffset += skipBytes;
|
||
}
|
||
if (frameInfo.acmod < 2) {
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 14);
|
||
}
|
||
if (frameInfo.acmod === 0) {
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 14);
|
||
}
|
||
}
|
||
}
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
if (frameInfo.numblkscod === 0) {
|
||
bs.bsSkip(bitStream, 5);
|
||
}
|
||
else {
|
||
for (let i = 0; i < frameInfo.numblkscod; i++) {
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 5);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
frameInfo.bsmod = 0;
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
frameInfo.bsmod = bs.bsReadAndUpdate(data, bitStream, 3);
|
||
bs.bsSkip(bitStream, 2);
|
||
if (frameInfo.acmod === 2) {
|
||
bs.bsSkip(bitStream, 4);
|
||
}
|
||
if (frameInfo.acmod >= 6) {
|
||
bs.bsSkip(bitStream, 2);
|
||
}
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 8);
|
||
}
|
||
if (frameInfo.acmod === 0) {
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
bs.bsSkip(bitStream, 8);
|
||
}
|
||
}
|
||
if (frameInfo.fscod < 3) {
|
||
bs.bsSkip(bitStream, 1);
|
||
}
|
||
}
|
||
if (frameInfo.strmtyp === 0 && frameInfo.numblkscod !== 3) {
|
||
bs.bsSkip(bitStream, 1);
|
||
}
|
||
if (frameInfo.strmtyp === 2) {
|
||
let blkid;
|
||
if (frameInfo.numblkscod === 3) {
|
||
blkid = 1;
|
||
}
|
||
else {
|
||
blkid = bs.bsReadAndUpdate(data, bitStream, 1);
|
||
}
|
||
if (blkid) {
|
||
bs.bsReadAndUpdate(data, bitStream, 6);
|
||
}
|
||
}
|
||
if (bs.bsReadAndUpdate(data, bitStream, 1)) {
|
||
const addbsil = bs.bsReadAndUpdate(data, bitStream, 6);
|
||
if (frameInfo.strmtyp === 0 && frameInfo.substreamid === 0 && addbsil === 1) {
|
||
const flag_ec3_extension_type_reserved = bs.bsReadAndUpdate(data, bitStream, 7);
|
||
const flag_ec3_extension_type_a = bs.bsReadAndUpdate(data, bitStream, 1);
|
||
const complexity_index_type_a = bs.bsReadAndUpdate(data, bitStream, 8);
|
||
// Make sure the values are in range and if yes, flag ATMOS
|
||
if (flag_ec3_extension_type_reserved === 0 && flag_ec3_extension_type_a === 1 && complexity_index_type_a >= 1 && complexity_index_type_a <= 16) {
|
||
sampleInfo.complexity_index_type_a = complexity_index_type_a;
|
||
}
|
||
}
|
||
}
|
||
// find channel map
|
||
if (frameInfo.chanmape) {
|
||
sampleInfo.chan_loc |= frameInfo.chanmap;
|
||
}
|
||
else {
|
||
// look up channel map using acmod
|
||
const acmodToChannelMap = [
|
||
40960,
|
||
16384,
|
||
40960,
|
||
57344,
|
||
41472,
|
||
57856,
|
||
47104,
|
||
63488,
|
||
];
|
||
sampleInfo.chan_loc |= acmodToChannelMap[frameInfo.acmod];
|
||
}
|
||
if (frameInfo.strmtyp === 0) {
|
||
sampleInfo.fscod = frameInfo.fscod;
|
||
sampleInfo.bsid = frameInfo.bsid;
|
||
sampleInfo.bsmod = frameInfo.bsmod;
|
||
sampleInfo.acmod = frameInfo.acmod;
|
||
sampleInfo.lfeon = frameInfo.lfeon;
|
||
}
|
||
sampleInfo.chan_loc |= frameInfo.lfeon ? 1 : 0;
|
||
// advance to the next syncframe
|
||
const frameLength = (frameInfo.frmsiz + 1) * 2;
|
||
offset += frameLength;
|
||
totalFrameLength += frameLength + (id3Length || 0);
|
||
}
|
||
let channelCount = 0;
|
||
// get channel count
|
||
for (let i = 0; i < 16; i++) {
|
||
if (sampleInfo.chan_loc & (1 << i)) {
|
||
channelCount++;
|
||
}
|
||
}
|
||
if (sampleInfo.lfeon) {
|
||
channelCount++;
|
||
}
|
||
// generate DD+ magic cookie
|
||
let cookieSize = 10 + sampleInfo.num_ind_sub * 3;
|
||
const samplingRateMap = [48000, 44100, 32000];
|
||
const samplerate = samplingRateMap[sampleInfo.fscod];
|
||
sampleInfo.data_rate = (samplerate / 1536) * totalFrameLength * 8;
|
||
cookieSize = 10 + sampleInfo.num_ind_sub * 3;
|
||
for (let i = 0; i < sampleInfo.num_ind_sub; i++) {
|
||
if (sampleInfo.num_dep_sub[i] > 0) {
|
||
cookieSize++;
|
||
}
|
||
}
|
||
// for ATMOS
|
||
if (sampleInfo.complexity_index_type_a > 0) {
|
||
cookieSize += 2;
|
||
}
|
||
// write the cookie
|
||
const extraDataBytes = new Uint8Array(cookieSize);
|
||
const bitStream = { byteOffset: 0, usedBits: 0 };
|
||
bs.bsWriteAndUpdate(extraDataBytes, bitStream, 32, cookieSize);
|
||
bs.bsWriteAndUpdate(extraDataBytes, bitStream, 32, 1684366131); // 'dec3'
|
||
bs.bsWriteAndUpdate(extraDataBytes, bitStream, 13, sampleInfo.data_rate); // data_rate
|
||
bs.bsWriteAndUpdate(extraDataBytes, bitStream, 3, sampleInfo.num_ind_sub); // num_ind_sub
|
||
for (let i = 0; i < sampleInfo.num_ind_sub; i++) {
|
||
bs.bsWriteAndUpdate(extraDataBytes, bitStream, 2, sampleInfo.fscod); // fscod
|
||
bs.bsWriteAndUpdate(extraDataBytes, bitStream, 5, sampleInfo.bsid); // bsid
|
||
bs.bsWriteAndUpdate(extraDataBytes, bitStream, 1, 0); // reserved
|
||
bs.bsWriteAndUpdate(extraDataBytes, bitStream, 1, i === 0 ? 0 : 1); // asvc
|
||
bs.bsWriteAndUpdate(extraDataBytes, bitStream, 3, sampleInfo.bsmod); // bsmod
|
||
bs.bsWriteAndUpdate(extraDataBytes, bitStream, 3, sampleInfo.acmod); // acmod
|
||
bs.bsWriteAndUpdate(extraDataBytes, bitStream, 1, sampleInfo.lfeon); // lfeon
|
||
bs.bsWriteAndUpdate(extraDataBytes, bitStream, 3, 0); // reserved
|
||
bs.bsWriteAndUpdate(extraDataBytes, bitStream, 4, sampleInfo.num_dep_sub[i]); // num_dep_sub
|
||
if (sampleInfo.num_dep_sub[i] > 0) {
|
||
bs.bsWriteAndUpdate(extraDataBytes, bitStream, 9, sampleInfo.chan_loc); // chan_loc
|
||
}
|
||
else {
|
||
bs.bsWriteAndUpdate(extraDataBytes, bitStream, 1, 0); // reserved
|
||
}
|
||
}
|
||
if (sampleInfo.complexity_index_type_a > 0) {
|
||
bs.bsWriteAndUpdate(extraDataBytes, bitStream, 7, 0); // flag_ec3_extension_type_reserved; reserved as 0
|
||
bs.bsWriteAndUpdate(extraDataBytes, bitStream, 1, 1); // flag_ec3_extension_type_a
|
||
bs.bsWriteAndUpdate(extraDataBytes, bitStream, 8, sampleInfo.complexity_index_type_a); // complexity_index_type_a
|
||
}
|
||
logger.debug(loggerName$g, `EC3 sampleInfo:${JSON.stringify(sampleInfo)}`);
|
||
logger.info(loggerName$g, `parsed codec:ec-3, isAtmos: ${sampleInfo.complexity_index_type_a > 0}, rate:${samplerate}, nb channel:${channelCount}, first totalFrameLength:${totalFrameLength}`);
|
||
const audioConfig = { samplerate: samplerate, channelCount: channelCount, segmentCodec: 'ec3', codec: 'ec-3', extraDataBytes: extraDataBytes };
|
||
return audioConfig;
|
||
},
|
||
};
|
||
var DDPlus$1 = DDPlus;
|
||
|
||
const loggerName$f = { name: 'EC3Demuxer' };
|
||
class EC3Demuxer extends EsDemuxer {
|
||
resetInitSegment(initSegment, duration) {
|
||
this.audioConfig = undefined;
|
||
this.audioTrack = undefined;
|
||
this.duration = duration;
|
||
}
|
||
static probe(data, logger) {
|
||
// check if data contains ID3 timestamp and EC3 sync bytes
|
||
const id3 = new ID3$1(data, logger), offset = id3.length;
|
||
// look for the ec-3 sync bytes
|
||
if (id3.hasTimeStamp && data[offset] === 11 && data[offset + 1] === 119) {
|
||
// check the bsid to confirm ec-3
|
||
const bu = new BitstreamUtils();
|
||
const bsid = bu.bsReadAndUpdate(data, { byteOffset: offset + 5, usedBits: 0 }, 5);
|
||
if (bsid === 16) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
// feed incoming data to the front of the parsing pipeline
|
||
append(data, timeOffset, contiguous, accurateTimeOffset, keyTagInfo) {
|
||
const id3 = new ID3$1(data, this.logger);
|
||
const pts = 90 * id3.timeStamp;
|
||
const length = data.length;
|
||
let frameIndex = 0;
|
||
let offset = id3.length;
|
||
if (!this.audioConfig) {
|
||
this.audioConfig = DDPlus$1.getAudioConfig(this.observer, data, offset, this.logger);
|
||
}
|
||
if (!this.audioConfig) {
|
||
throw 'failed to parse ec-3 config';
|
||
}
|
||
if (!this.audioTrack) {
|
||
const info = { id: 258, inputTimescale: 90000, timescale: NaN, duration: this.duration, encrypted: false, keyTagInfo };
|
||
const parsingData = { len: 0, sequenceNumber: 0, esSamples: [] };
|
||
this.audioTrack = { info, parsingData, type: 'audio', config: this.audioConfig };
|
||
}
|
||
const frameDuration = Dolby.getFrameDuration(this.audioConfig, this.audioTrack.info.inputTimescale); // (1536 / this.audioConfig.samplerate) * this.audioTrack.inputTimescale;
|
||
if (id3.audioType === 'zec3') {
|
||
this.audioTrack.info.encrypted = true;
|
||
this.logger.info(loggerName$f, 'found encrypted ec3');
|
||
}
|
||
while (offset < length) {
|
||
const frameLength = DDPlus$1.getFrameLength(this.observer, data, offset, this.logger);
|
||
const stamp = pts + frameIndex * frameDuration;
|
||
const ec3Sample = { unit: data.subarray(offset, offset + frameLength), pts: stamp, dts: stamp, keyTagInfo: keyTagInfo };
|
||
this.audioTrack.parsingData.esSamples.push(ec3Sample);
|
||
this.audioTrack.parsingData.len += frameLength;
|
||
offset += frameLength;
|
||
frameIndex++;
|
||
}
|
||
this.esRemuxer.remuxEsTracks(this.audioTrack, undefined, { id3Samples: [{ pts: pts, dts: pts, data: id3.payload, frames: id3.frames }], inputTimescale: this.audioTrack.info.inputTimescale }, undefined, timeOffset, contiguous, accurateTimeOffset, keyTagInfo);
|
||
}
|
||
}
|
||
|
||
|
||
const MpegAudio = {
|
||
BitratesMap: [
|
||
32,
|
||
64,
|
||
96,
|
||
128,
|
||
160,
|
||
192,
|
||
224,
|
||
256,
|
||
288,
|
||
320,
|
||
352,
|
||
384,
|
||
416,
|
||
448,
|
||
32,
|
||
48,
|
||
56,
|
||
64,
|
||
80,
|
||
96,
|
||
112,
|
||
128,
|
||
160,
|
||
192,
|
||
224,
|
||
256,
|
||
320,
|
||
384,
|
||
32,
|
||
40,
|
||
48,
|
||
56,
|
||
64,
|
||
80,
|
||
96,
|
||
112,
|
||
128,
|
||
160,
|
||
192,
|
||
224,
|
||
256,
|
||
320,
|
||
32,
|
||
48,
|
||
56,
|
||
64,
|
||
80,
|
||
96,
|
||
112,
|
||
128,
|
||
144,
|
||
160,
|
||
176,
|
||
192,
|
||
224,
|
||
256,
|
||
8,
|
||
16,
|
||
24,
|
||
32,
|
||
40,
|
||
48,
|
||
56,
|
||
64,
|
||
80,
|
||
96,
|
||
112,
|
||
128,
|
||
144,
|
||
160,
|
||
],
|
||
SamplingRateMap: [44100, 48000, 32000, 22050, 24000, 16000, 11025, 12000, 8000],
|
||
SamplesCoefficients: [
|
||
// MPEG 2.5
|
||
[
|
||
0,
|
||
72,
|
||
144,
|
||
12,
|
||
],
|
||
// Reserved
|
||
[
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
],
|
||
// MPEG 2
|
||
[
|
||
0,
|
||
72,
|
||
144,
|
||
12,
|
||
],
|
||
// MPEG 1
|
||
[
|
||
0,
|
||
144,
|
||
144,
|
||
12,
|
||
],
|
||
],
|
||
BytesInSlot: [
|
||
0,
|
||
1,
|
||
1,
|
||
4,
|
||
],
|
||
onFrame: function (parsingData, data, bitRate, samplerate, channelCount, frameIndex, pts) {
|
||
const frameDuration = 103680000 / samplerate;
|
||
const stamp = pts + frameIndex * frameDuration;
|
||
parsingData.esSamples.push({ unit: data, pts: stamp, dts: stamp });
|
||
parsingData.len += data.length;
|
||
},
|
||
onNoise: function (data, logger) {
|
||
logger.warn('mpeg audio has noise: ' + data.length + ' bytes');
|
||
},
|
||
parseFrames: function (parsingData, data, start, end, frameIndex, pts, logger) {
|
||
const BitratesMap = [
|
||
32,
|
||
64,
|
||
96,
|
||
128,
|
||
160,
|
||
192,
|
||
224,
|
||
256,
|
||
288,
|
||
320,
|
||
352,
|
||
384,
|
||
416,
|
||
448,
|
||
32,
|
||
48,
|
||
56,
|
||
64,
|
||
80,
|
||
96,
|
||
112,
|
||
128,
|
||
160,
|
||
192,
|
||
224,
|
||
256,
|
||
320,
|
||
384,
|
||
32,
|
||
40,
|
||
48,
|
||
56,
|
||
64,
|
||
80,
|
||
96,
|
||
112,
|
||
128,
|
||
160,
|
||
192,
|
||
224,
|
||
256,
|
||
320,
|
||
32,
|
||
48,
|
||
56,
|
||
64,
|
||
80,
|
||
96,
|
||
112,
|
||
128,
|
||
144,
|
||
160,
|
||
176,
|
||
192,
|
||
224,
|
||
256,
|
||
8,
|
||
16,
|
||
24,
|
||
32,
|
||
40,
|
||
48,
|
||
56,
|
||
64,
|
||
80,
|
||
96,
|
||
112,
|
||
128,
|
||
144,
|
||
160,
|
||
];
|
||
const SamplingRateMap = [44100, 48000, 32000, 22050, 24000, 16000, 11025, 12000, 8000];
|
||
if (start + 2 > end) {
|
||
return -1; // we need at least 2 bytes to detect sync pattern
|
||
}
|
||
if (data[start] === 255 || (data[start + 1] & 224) === 224) {
|
||
// Using http://www.datavoyage.com/mpgscript/mpeghdr.htm as a reference
|
||
if (start + 24 > end) {
|
||
return -1;
|
||
}
|
||
const headerB = (data[start + 1] >> 3) & 3;
|
||
const headerC = (data[start + 1] >> 1) & 3;
|
||
const headerE = (data[start + 2] >> 4) & 15;
|
||
const headerF = (data[start + 2] >> 2) & 3;
|
||
const headerG = !!(data[start + 2] & 2);
|
||
if (headerB !== 1 && headerE !== 0 && headerE !== 15 && headerF !== 3) {
|
||
const columnInBitrates = headerB === 3 ? 3 - headerC : headerC === 3 ? 3 : 4;
|
||
const bitRate = BitratesMap[columnInBitrates * 14 + headerE - 1] * 1000;
|
||
const columnInSampleRates = headerB === 3 ? 0 : headerB === 2 ? 1 : 2;
|
||
const sampleRate = SamplingRateMap[columnInSampleRates * 3 + headerF];
|
||
const padding = headerG ? 1 : 0;
|
||
const channelCount = data[start + 3] >> 6 === 3 ? 1 : 2; // If bits of channel mode are `11` then it is a single channel (Mono)
|
||
const frameLength = headerC === 3 ? (((headerB === 3 ? 12 : 6) * bitRate) / sampleRate + padding) << 2 : (((headerB === 3 ? 144 : 72) * bitRate) / sampleRate + padding) | 0;
|
||
if (start + frameLength > end) {
|
||
return -1;
|
||
}
|
||
MpegAudio.onFrame(parsingData, data.subarray(start, start + frameLength), bitRate, sampleRate, channelCount, frameIndex, pts);
|
||
return frameLength;
|
||
}
|
||
}
|
||
// noise or ID3, trying to skip
|
||
let offset = start + 2;
|
||
while (offset < end) {
|
||
if (data[offset - 1] === 255 && (data[offset] & 224) === 224) {
|
||
// sync pattern is found
|
||
MpegAudio.onNoise(data.subarray(start, offset - 1), logger);
|
||
return offset - start - 1;
|
||
}
|
||
offset++;
|
||
}
|
||
return -1;
|
||
},
|
||
parse: function (parsingData, data, offset, pts, logger) {
|
||
const length = data.length;
|
||
let frameIndex = 0;
|
||
let parsed;
|
||
while (offset < length && (parsed = MpegAudio.parseFrames(parsingData, data, offset, length, frameIndex++, pts, logger)) > 0) {
|
||
offset += parsed;
|
||
}
|
||
},
|
||
getAudioConfig: function (data, offset) {
|
||
const headerB = (data[offset + 1] >> 3) & 3;
|
||
const headerC = (data[offset + 1] >> 1) & 3;
|
||
const headerE = (data[offset + 2] >> 4) & 15;
|
||
const headerF = (data[offset + 2] >> 2) & 3;
|
||
const headerG = (data[offset + 2] >> 1) & 1;
|
||
if (headerB !== 1 && headerE !== 0 && headerE !== 15 && headerF !== 3) {
|
||
const columnInBitrates = headerB === 3 ? 3 - headerC : headerC === 3 ? 3 : 4;
|
||
const bitRate = MpegAudio.BitratesMap[columnInBitrates * 14 + headerE - 1] * 1000;
|
||
const columnInSampleRates = headerB === 3 ? 0 : headerB === 2 ? 1 : 2;
|
||
const samplerate = MpegAudio.SamplingRateMap[columnInSampleRates * 3 + headerF];
|
||
const channelCount = data[offset + 3] >> 6 === 3 ? 1 : 2; // If bits of channel mode are `11` then it is a single channel (Mono)
|
||
const sampleCoefficient = MpegAudio.SamplesCoefficients[headerB][headerC];
|
||
const bytesInSlot = MpegAudio.BytesInSlot[headerC];
|
||
const frameLength = parseInt(((sampleCoefficient * bitRate) / samplerate + headerG), 10) * bytesInSlot;
|
||
const result = { segmentCodec: 'mp3', codec: 'mp3', samplerate, channelCount, frameLength };
|
||
return result;
|
||
}
|
||
return undefined;
|
||
},
|
||
isHeaderPattern: function (data, offset) {
|
||
return data[offset] === 255 && (data[offset + 1] & 224) === 224 && (data[offset + 1] & 6) !== 0;
|
||
},
|
||
probe: function (data, offset) {
|
||
// same as isHeader but we also check that MPEG frame follows last MPEG frame
|
||
// or end of data is reached
|
||
if (offset + 1 < data.length && MpegAudio.isHeaderPattern(data, offset)) {
|
||
// MPEG header Length
|
||
const headerLength = 4;
|
||
// MPEG frame Length
|
||
const header = MpegAudio.getAudioConfig(data, offset);
|
||
let frameLength = headerLength;
|
||
if (header && header.frameLength) {
|
||
frameLength = header.frameLength;
|
||
}
|
||
const newOffset = offset + frameLength;
|
||
if (newOffset === data.length || (newOffset + 1 < data.length && MpegAudio.isHeaderPattern(data, newOffset))) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
},
|
||
};
|
||
var MpegAudio$1 = MpegAudio;
|
||
|
||
|
||
const loggerName$e = { name: 'MP3Demuxer' };
|
||
class MP3Demuxer extends EsDemuxer {
|
||
resetInitSegment(initSegment, duration) {
|
||
this.audioConfig = undefined;
|
||
this.audioTrack = undefined;
|
||
this.duration = duration;
|
||
}
|
||
static probe(data, logger) {
|
||
// check if data contains ID3 timestamp and MPEG sync word
|
||
const id3 = new ID3$1(data, logger);
|
||
let offset, length;
|
||
if (id3.hasTimeStamp) {
|
||
// Look for MPEG header | 1111 1111 | 111X XYZX | where X can be either 0 or 1 and Y or Z should be 1
|
||
// Layer bits (position 14 and 15) in header should be always different from 0 (Layer I or Layer II or Layer III)
|
||
// More info http://www.mp3-tech.org/programmer/frame_header.html
|
||
for (offset = id3.length, length = Math.min(data.length - 1, offset + 100); offset < length; offset++) {
|
||
if (MpegAudio$1.probe(data, offset)) {
|
||
logger.warn(loggerName$e, 'MPEG Audio sync word found !');
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
// feed incoming data to the front of the parsing pipeline
|
||
append(data, timeOffset, contiguous, accurateTimeOffset, keyTagInfo) {
|
||
const id3 = new ID3$1(data, this.logger);
|
||
const pts = 90 * id3.timeStamp;
|
||
if (!this.audioConfig) {
|
||
this.audioConfig = MpegAudio$1.getAudioConfig(data, id3.length);
|
||
}
|
||
if (!this.audioConfig) {
|
||
throw 'unable to parse mp3 header';
|
||
}
|
||
if (!this.audioTrack) {
|
||
const info = { id: 258, inputTimescale: 90000, timescale: NaN, duration: this.duration, encrypted: false, keyTagInfo };
|
||
const parsingData = { len: 0, sequenceNumber: 0, esSamples: [] };
|
||
this.audioTrack = { info, parsingData, type: 'audio', config: this.audioConfig };
|
||
}
|
||
MpegAudio$1.parse(this.audioTrack.parsingData, data, id3.length, pts, this.logger);
|
||
this.esRemuxer.remuxEsTracks(this.audioTrack, undefined, { id3Samples: [{ pts: pts, dts: pts, data: id3.payload, frames: id3.frames }], inputTimescale: 90000 }, undefined, timeOffset, contiguous, accurateTimeOffset);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* AAC Helper
|
||
*
|
||
*
|
||
*
|
||
*
|
||
*/
|
||
function getSilentFrame(codec, channelCount) {
|
||
switch (codec) {
|
||
case 'mp4a.40.2':
|
||
if (channelCount === 1) {
|
||
return new Uint8Array([0, 200, 0, 128, 35, 128]);
|
||
}
|
||
else if (channelCount === 2) {
|
||
return new Uint8Array([33, 0, 73, 144, 2, 25, 0, 35, 128]);
|
||
}
|
||
else if (channelCount === 3) {
|
||
return new Uint8Array([0, 200, 0, 128, 32, 132, 1, 38, 64, 8, 100, 0, 142]);
|
||
}
|
||
else if (channelCount === 4) {
|
||
return new Uint8Array([0, 200, 0, 128, 32, 132, 1, 38, 64, 8, 100, 0, 128, 44, 128, 8, 2, 56]);
|
||
}
|
||
else if (channelCount === 5) {
|
||
return new Uint8Array([0, 200, 0, 128, 32, 132, 1, 38, 64, 8, 100, 0, 130, 48, 4, 153, 0, 33, 144, 2, 56]);
|
||
}
|
||
else if (channelCount === 6) {
|
||
return new Uint8Array([0, 200, 0, 128, 32, 132, 1, 38, 64, 8, 100, 0, 130, 48, 4, 153, 0, 33, 144, 2, 0, 178, 0, 32, 8, 224]);
|
||
}
|
||
break;
|
||
// handle HE-AAC below (mp4a.40.5 / mp4a.40.29)
|
||
default:
|
||
if (channelCount === 1) {
|
||
// ffmpeg -y -f lavfi -i "aevalsrc=0:d=0.05" -c:a libfdk_aac -profile:a aac_he -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac
|
||
return new Uint8Array([
|
||
1,
|
||
64,
|
||
34,
|
||
128,
|
||
163,
|
||
78,
|
||
230,
|
||
128,
|
||
186,
|
||
8,
|
||
0,
|
||
0,
|
||
0,
|
||
28,
|
||
6,
|
||
241,
|
||
193,
|
||
10,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
94,
|
||
]);
|
||
}
|
||
else if (channelCount === 2) {
|
||
// ffmpeg -y -f lavfi -i "aevalsrc=0|0:d=0.05" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac
|
||
return new Uint8Array([
|
||
1,
|
||
64,
|
||
34,
|
||
128,
|
||
163,
|
||
94,
|
||
230,
|
||
128,
|
||
186,
|
||
8,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
149,
|
||
0,
|
||
6,
|
||
241,
|
||
161,
|
||
10,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
94,
|
||
]);
|
||
}
|
||
else if (channelCount === 3) {
|
||
// ffmpeg -y -f lavfi -i "aevalsrc=0|0|0:d=0.05" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 "0x%x," "\n"' -v output.aac
|
||
return new Uint8Array([
|
||
1,
|
||
64,
|
||
34,
|
||
128,
|
||
163,
|
||
94,
|
||
230,
|
||
128,
|
||
186,
|
||
8,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
149,
|
||
0,
|
||
6,
|
||
241,
|
||
161,
|
||
10,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
90,
|
||
94,
|
||
]);
|
||
}
|
||
break;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function isFiniteNumber(value) {
|
||
return typeof value === 'number' && isFinite(value);
|
||
}
|
||
/**
|
||
* For getting float string for a given value with type checking
|
||
* @param val Number
|
||
* @param precision Number of digits after decimal
|
||
*/
|
||
function toFixed(val, precision) {
|
||
if (isFiniteNumber(val)) {
|
||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||
return val.toFixed(precision);
|
||
}
|
||
return `${val}`;
|
||
}
|
||
/**
|
||
* Stringify except print numbers with fixed precision
|
||
* @param obj the value to stringify
|
||
* @param precision Number of digits after decimal. Default 3
|
||
*/
|
||
function stringifyWithPrecision(obj, precision = 3) {
|
||
return JSON.stringify(obj, (_key, value) => {
|
||
return !isNaN(value) && (value === null || value === void 0 ? void 0 : value.toFixed) ? Number(value === null || value === void 0 ? void 0 : value.toFixed(precision)) : value;
|
||
});
|
||
}
|
||
/**
|
||
* Replace all occurrences of an instance in a string
|
||
*
|
||
* @param {string|RegExp} search - What to change
|
||
* @param {string} replacement - Replace search for this
|
||
* @param {string} target - Target string to have elements replaced
|
||
* @returns {string}
|
||
*/
|
||
const replaceAll = (search, replacement, target = '') => target.split(search).join(replacement);
|
||
let shouldRedactUrl = true;
|
||
function setupRedactUrl(buildType) {
|
||
shouldRedactUrl = buildType === 'production';
|
||
}
|
||
function redactUrl(url) {
|
||
return shouldRedactUrl ? '<redacted>' : url;
|
||
}
|
||
// Naive deep copy of any serializable object
|
||
// shallow copy for function, symbol
|
||
function deepCpy(obj) {
|
||
if (!obj) {
|
||
return obj;
|
||
}
|
||
switch (typeof obj) {
|
||
case 'object':
|
||
if (Array.isArray(obj)) {
|
||
return obj.map(deepCpy);
|
||
}
|
||
const result = {};
|
||
for (const [key, value] of Object.entries(obj)) {
|
||
result[key] = deepCpy(value);
|
||
}
|
||
return result;
|
||
default:
|
||
return obj;
|
||
}
|
||
}
|
||
function urlRedactedLevelInfo(indata) {
|
||
const outdata = [...indata];
|
||
for (let i = 0; i < outdata.length; i++) {
|
||
outdata[i] = Object.assign({}, outdata[i]);
|
||
outdata[i].url = redactUrl(outdata[i].url);
|
||
if (outdata[i].attrs) {
|
||
outdata[i].attrs = Object.assign({}, outdata[i].attrs);
|
||
outdata[i].attrs.URI = redactUrl(outdata[i].attrs.URI);
|
||
}
|
||
}
|
||
return outdata;
|
||
}
|
||
function urlRedactedAltMediaOption(indata) {
|
||
const outdata = [...indata];
|
||
for (let i = 0; i < outdata.length; i++) {
|
||
outdata[i] = Object.assign({}, outdata[i]);
|
||
outdata[i].url = redactUrl(outdata[i].url);
|
||
}
|
||
return outdata;
|
||
}
|
||
|
||
/**
|
||
* Generate MP4 Box
|
||
*
|
||
*
|
||
*
|
||
*
|
||
*/
|
||
const UINT32_MAX$1 = Math.pow(2, 32) - 1;
|
||
class MP4 {
|
||
static init() {
|
||
MP4.types = {
|
||
avc1: [],
|
||
avcC: [],
|
||
btrt: [],
|
||
dinf: [],
|
||
dref: [],
|
||
esds: [],
|
||
free: [],
|
||
ftyp: [],
|
||
hdlr: [],
|
||
mdat: [],
|
||
mdhd: [],
|
||
mdia: [],
|
||
mfhd: [],
|
||
minf: [],
|
||
moof: [],
|
||
moov: [],
|
||
mp4a: [],
|
||
'.mp3': [],
|
||
dac3: [],
|
||
'ac-3': [],
|
||
dec3: [],
|
||
'ec-3': [],
|
||
mvex: [],
|
||
mvhd: [],
|
||
pasp: [],
|
||
sdtp: [],
|
||
stbl: [],
|
||
stco: [],
|
||
stsc: [],
|
||
stsd: [],
|
||
stsz: [],
|
||
stts: [],
|
||
tfdt: [],
|
||
tfhd: [],
|
||
traf: [],
|
||
trak: [],
|
||
trun: [],
|
||
trex: [],
|
||
tkhd: [],
|
||
vmhd: [],
|
||
smhd: [],
|
||
uuid: [],
|
||
encv: [],
|
||
enca: [],
|
||
// map encryption boxes
|
||
frma: [],
|
||
schm: [],
|
||
schi: [],
|
||
senc: [],
|
||
saio: [],
|
||
saiz: [],
|
||
sinf: [],
|
||
tenc: [],
|
||
// moof encryption boxes
|
||
sbgp: [],
|
||
seig: [],
|
||
sgpd: [],
|
||
pssh: [],
|
||
};
|
||
let i;
|
||
for (i in MP4.types) {
|
||
// eslint-disable-next-line no-prototype-builtins
|
||
if (MP4.types.hasOwnProperty(i)) {
|
||
MP4.types[i] = [i.charCodeAt(0), i.charCodeAt(1), i.charCodeAt(2), i.charCodeAt(3)];
|
||
}
|
||
}
|
||
const videoHdlr = new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
118,
|
||
105,
|
||
100,
|
||
101,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
86,
|
||
105,
|
||
100,
|
||
101,
|
||
111,
|
||
72,
|
||
97,
|
||
110,
|
||
100,
|
||
108,
|
||
101,
|
||
114,
|
||
0,
|
||
]);
|
||
const audioHdlr = new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
115,
|
||
111,
|
||
117,
|
||
110,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
83,
|
||
111,
|
||
117,
|
||
110,
|
||
100,
|
||
72,
|
||
97,
|
||
110,
|
||
100,
|
||
108,
|
||
101,
|
||
114,
|
||
0,
|
||
]);
|
||
MP4.HDLR_TYPES = {
|
||
video: videoHdlr,
|
||
audio: audioHdlr,
|
||
};
|
||
const dref = new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
1,
|
||
0,
|
||
0,
|
||
0,
|
||
12,
|
||
117,
|
||
114,
|
||
108,
|
||
32,
|
||
0,
|
||
0,
|
||
0,
|
||
1,
|
||
]);
|
||
const stco = new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
]);
|
||
MP4.STTS = MP4.STSC = MP4.STCO = stco;
|
||
MP4.STSZ = new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
]);
|
||
MP4.VMHD = new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
1,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
]);
|
||
MP4.SMHD = new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
]);
|
||
MP4.STSD = new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
1,
|
||
]); // entry_count
|
||
const majorBrand = new Uint8Array([105, 115, 111, 109]); // isom
|
||
const avc1Brand = new Uint8Array([97, 118, 99, 49]); // avc1
|
||
const minorVersion = new Uint8Array([0, 0, 0, 1]);
|
||
MP4.FTYP = MP4.box(MP4.types.ftyp, majorBrand, minorVersion, majorBrand, avc1Brand);
|
||
MP4.DINF = MP4.box(MP4.types.dinf, MP4.box(MP4.types.dref, dref));
|
||
}
|
||
static set16(num, data, index) {
|
||
data[index] = (num >> 8) & 255;
|
||
data[index + 1] = num & 255;
|
||
return index + 2;
|
||
}
|
||
static set32(num, data, index) {
|
||
data[index] = (num >> 24) & 255;
|
||
data[index + 1] = (num >> 16) & 255;
|
||
data[index + 2] = (num >> 8) & 255;
|
||
data[index + 3] = num & 255;
|
||
return index + 4;
|
||
}
|
||
static box(type, ...params) {
|
||
// eslint-disable-next-line prefer-rest-params
|
||
const payload = Array.prototype.slice.call(arguments, 1);
|
||
let size = 8, i = payload.length;
|
||
const len = i;
|
||
// calculate the total size we need to allocate
|
||
while (i--) {
|
||
size += payload[i].byteLength;
|
||
}
|
||
const result = new Uint8Array(size);
|
||
result[0] = (size >> 24) & 255;
|
||
result[1] = (size >> 16) & 255;
|
||
result[2] = (size >> 8) & 255;
|
||
result[3] = size & 255;
|
||
result.set(type, 4);
|
||
// copy the payload into the result
|
||
for (i = 0, size = 8; i < len; i++) {
|
||
// copy payload[i] array @ offset size
|
||
result.set(payload[i], size);
|
||
size += payload[i].byteLength;
|
||
}
|
||
return result;
|
||
}
|
||
static hdlr(type) {
|
||
return MP4.box(MP4.types.hdlr, MP4.HDLR_TYPES[type]);
|
||
}
|
||
static mdat(data) {
|
||
return MP4.box(MP4.types.mdat, data);
|
||
}
|
||
static mdhd(timescale, duration) {
|
||
duration *= timescale;
|
||
const upperWordDuration = Math.floor(duration / (UINT32_MAX$1 + 1));
|
||
const lowerWordDuration = Math.floor(duration % (UINT32_MAX$1 + 1));
|
||
return MP4.box(MP4.types.mdhd, new Uint8Array([
|
||
1,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
2,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
3,
|
||
(timescale >> 24) & 255,
|
||
(timescale >> 16) & 255,
|
||
(timescale >> 8) & 255,
|
||
timescale & 255,
|
||
upperWordDuration >> 24,
|
||
(upperWordDuration >> 16) & 255,
|
||
(upperWordDuration >> 8) & 255,
|
||
upperWordDuration & 255,
|
||
lowerWordDuration >> 24,
|
||
(lowerWordDuration >> 16) & 255,
|
||
(lowerWordDuration >> 8) & 255,
|
||
lowerWordDuration & 255,
|
||
85,
|
||
196,
|
||
0,
|
||
0,
|
||
]));
|
||
}
|
||
static mdia(track) {
|
||
const mdhd = MP4.mdhd(track.info.timescale, track.info.duration);
|
||
const hdlr = MP4.hdlr(track.type);
|
||
const minf = MP4.minf(track);
|
||
return MP4.box(MP4.types.mdia, mdhd, hdlr, minf);
|
||
}
|
||
static mfhd(sequenceNumber) {
|
||
return MP4.box(MP4.types.mfhd, new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
sequenceNumber >> 24,
|
||
(sequenceNumber >> 16) & 255,
|
||
(sequenceNumber >> 8) & 255,
|
||
sequenceNumber & 255, // sequence_number
|
||
]));
|
||
}
|
||
static minf(track) {
|
||
if (track.type === 'audio') {
|
||
return MP4.box(MP4.types.minf, MP4.box(MP4.types.smhd, MP4.SMHD), MP4.DINF, MP4.stbl(track));
|
||
}
|
||
else {
|
||
return MP4.box(MP4.types.minf, MP4.box(MP4.types.vmhd, MP4.VMHD), MP4.DINF, MP4.stbl(track));
|
||
}
|
||
}
|
||
static moof(baseMediaDecodeTime, track) {
|
||
if (!MP4.types) {
|
||
MP4.init();
|
||
}
|
||
const traf = MP4.traf(track, baseMediaDecodeTime);
|
||
const moof = MP4.box(MP4.types.moof, MP4.mfhd(track.sequenceNumber), traf);
|
||
return moof;
|
||
}
|
||
/**
|
||
* @param tracks... (optional) {array} the tracks associated with this movie
|
||
*/
|
||
static moov(tracks) {
|
||
let i = tracks.length;
|
||
const boxes = [];
|
||
while (i--) {
|
||
boxes[i] = MP4.trak(tracks[i]);
|
||
}
|
||
return MP4.box.apply(null, [MP4.types.moov, MP4.mvhd(tracks[0].info.timescale, tracks[0].info.duration)].concat(boxes).concat(MP4.mvex(tracks)));
|
||
}
|
||
static mvex(tracks) {
|
||
let i = tracks.length;
|
||
const boxes = [];
|
||
while (i--) {
|
||
boxes[i] = MP4.trex(tracks[i]);
|
||
}
|
||
return MP4.box(MP4.types.mvex, ...boxes);
|
||
// return MP4.box.apply(null, [MP4.types.mvex, .concat(boxes));
|
||
}
|
||
static mvhd(timescale, duration) {
|
||
duration *= timescale;
|
||
const upperWordDuration = Math.floor(duration / (UINT32_MAX$1 + 1));
|
||
const lowerWordDuration = Math.floor(duration % (UINT32_MAX$1 + 1));
|
||
const bytes = new Uint8Array([
|
||
1,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
2,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
3,
|
||
(timescale >> 24) & 255,
|
||
(timescale >> 16) & 255,
|
||
(timescale >> 8) & 255,
|
||
timescale & 255,
|
||
upperWordDuration >> 24,
|
||
(upperWordDuration >> 16) & 255,
|
||
(upperWordDuration >> 8) & 255,
|
||
upperWordDuration & 255,
|
||
lowerWordDuration >> 24,
|
||
(lowerWordDuration >> 16) & 255,
|
||
(lowerWordDuration >> 8) & 255,
|
||
lowerWordDuration & 255,
|
||
0,
|
||
1,
|
||
0,
|
||
0,
|
||
1,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
1,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
1,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
64,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
255,
|
||
255,
|
||
255,
|
||
255,
|
||
]);
|
||
return MP4.box(MP4.types.mvhd, bytes);
|
||
}
|
||
static sdtp(track) {
|
||
const samples = track.samples || [], bytes = new Uint8Array(4 + samples.length);
|
||
let flags, i;
|
||
// leave the full box header (4 bytes) all zero
|
||
// write the sample table
|
||
for (i = 0; i < samples.length; i++) {
|
||
flags = samples[i].flags;
|
||
bytes[i + 4] = (flags.dependsOn << 4) | (flags.isDependedOn << 2) | flags.hasRedundancy;
|
||
}
|
||
return MP4.box(MP4.types.sdtp, bytes);
|
||
}
|
||
static stbl(track) {
|
||
const stsd = MP4.stsd(track);
|
||
const stts = MP4.box(MP4.types.stts, MP4.STTS);
|
||
const stsc = MP4.box(MP4.types.stsc, MP4.STSC);
|
||
const stsz = MP4.box(MP4.types.stsz, MP4.STSZ);
|
||
const stco = MP4.box(MP4.types.stco, MP4.STCO);
|
||
return MP4.box(MP4.types.stbl, stsd, stts, stsc, stsz, stco);
|
||
}
|
||
static avc1(track) {
|
||
let sps = [], pps = [], i, data, len;
|
||
// assemble the SPSs
|
||
const codingName = track.info.encrypted ? MP4.types.encv : MP4.types.avc1;
|
||
for (i = 0; i < track.config.sps.length; i++) {
|
||
data = track.config.sps[i];
|
||
len = data.byteLength;
|
||
sps.push((len >>> 8) & 255);
|
||
sps.push(len & 255);
|
||
sps = sps.concat(Array.prototype.slice.call(data)); // SPS
|
||
}
|
||
// assemble the PPSs
|
||
for (i = 0; i < track.config.pps.length; i++) {
|
||
data = track.config.pps[i];
|
||
len = data.byteLength;
|
||
pps.push((len >>> 8) & 255);
|
||
pps.push(len & 255);
|
||
pps = pps.concat(Array.prototype.slice.call(data));
|
||
}
|
||
const avcc = MP4.box(MP4.types.avcC, new Uint8Array([
|
||
1,
|
||
sps[3],
|
||
sps[4],
|
||
sps[5],
|
||
255,
|
||
224 | track.config.sps.length, // 3bit reserved (111) + numOfSequenceParameterSets
|
||
]
|
||
.concat(sps)
|
||
.concat([
|
||
track.config.pps.length, // numOfPictureParameterSets
|
||
])
|
||
.concat(pps))), // "PPS"
|
||
width = track.config.width, height = track.config.height, hSpacing = track.config.pixelRatio[0], vSpacing = track.config.pixelRatio[1];
|
||
// console.log('avcc:' + Hex.hexDump(avcc));
|
||
const sinf = track.info.encrypted && track.info.keyTagInfo ? MP4.sinf(track.info.keyTagInfo, track.type, MP4.types.avc1) : new Uint8Array();
|
||
// console.log('video sinf:' + Hex.hexDump(sinf));
|
||
return MP4.box(codingName, new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
1,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
(width >> 8) & 255,
|
||
width & 255,
|
||
(height >> 8) & 255,
|
||
height & 255,
|
||
0,
|
||
72,
|
||
0,
|
||
0,
|
||
0,
|
||
72,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
1,
|
||
18,
|
||
100,
|
||
97,
|
||
105,
|
||
108,
|
||
121,
|
||
109,
|
||
111,
|
||
116,
|
||
105,
|
||
111,
|
||
110,
|
||
47,
|
||
104,
|
||
108,
|
||
115,
|
||
46,
|
||
106,
|
||
115,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
24,
|
||
17,
|
||
17,
|
||
]), // pre_defined = -1
|
||
avcc, sinf, MP4.box(MP4.types.btrt, new Uint8Array([
|
||
0,
|
||
28,
|
||
156,
|
||
128,
|
||
0,
|
||
45,
|
||
198,
|
||
192,
|
||
0,
|
||
45,
|
||
198,
|
||
192,
|
||
])), // avgBitrate
|
||
MP4.box(MP4.types.pasp, new Uint8Array([
|
||
hSpacing >> 24,
|
||
(hSpacing >> 16) & 255,
|
||
(hSpacing >> 8) & 255,
|
||
hSpacing & 255,
|
||
vSpacing >> 24,
|
||
(vSpacing >> 16) & 255,
|
||
(vSpacing >> 8) & 255,
|
||
vSpacing & 255,
|
||
])));
|
||
}
|
||
static esds(config) {
|
||
const configlen = config.esdsConfig.length;
|
||
return new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
3,
|
||
23 + configlen,
|
||
0,
|
||
1,
|
||
0,
|
||
4,
|
||
15 + configlen,
|
||
64,
|
||
21,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
5,
|
||
]
|
||
.concat([configlen])
|
||
.concat(config.esdsConfig)
|
||
.concat([6, 1, 2])); // GASpecificConfig)); // length + audio config descriptor
|
||
}
|
||
static audioStsd(config) {
|
||
const samplerate = config.samplerate;
|
||
return new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
1,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
config.channelCount,
|
||
0,
|
||
16,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
(samplerate >> 8) & 255,
|
||
samplerate & 255,
|
||
0,
|
||
0,
|
||
]);
|
||
}
|
||
static dac3(config) {
|
||
const extraData = config.extraData;
|
||
return new Uint8Array([(extraData >> 16) & 255, (extraData >> 8) & 255, extraData & 255]);
|
||
}
|
||
static dec3(config) {
|
||
return config.extraDataBytes;
|
||
}
|
||
static mp4a(info, config) {
|
||
let codingName = MP4.types.mp4a;
|
||
let sinf = null;
|
||
if (info.encrypted && info.keyTagInfo) {
|
||
codingName = MP4.types.enca;
|
||
sinf = MP4.sinf(info.keyTagInfo, 'audio', MP4.types.mp4a);
|
||
}
|
||
else {
|
||
sinf = new Uint8Array();
|
||
}
|
||
const stsd = MP4.audioStsd(config);
|
||
const esds = MP4.box(MP4.types.esds, MP4.esds(config));
|
||
return MP4.box(codingName, stsd, esds, sinf);
|
||
}
|
||
static mp3(config) {
|
||
return MP4.box(MP4.types['.mp3'], MP4.audioStsd(config));
|
||
}
|
||
static ac3(info, config) {
|
||
let codingName = MP4.types['ac-3'];
|
||
let sinf = null;
|
||
if (info.encrypted && info.keyTagInfo) {
|
||
codingName = MP4.types.enca;
|
||
sinf = MP4.sinf(info.keyTagInfo, 'audio', MP4.types['ac-3']);
|
||
}
|
||
else {
|
||
sinf = new Uint8Array();
|
||
}
|
||
return MP4.box(codingName, MP4.audioStsd(config), MP4.box(MP4.types.dac3, MP4.dac3(config)), sinf);
|
||
}
|
||
static ec3(info, config) {
|
||
let codingName = MP4.types['ec-3'];
|
||
let sinf = null;
|
||
if (info.encrypted && info.keyTagInfo) {
|
||
codingName = MP4.types.enca;
|
||
sinf = MP4.sinf(info.keyTagInfo, 'audio', MP4.types['ec-3']);
|
||
}
|
||
else {
|
||
sinf = new Uint8Array();
|
||
}
|
||
return MP4.box(codingName, MP4.audioStsd(config), MP4.box(MP4.types.dec3, MP4.dec3(config)), sinf);
|
||
}
|
||
static stsd(track) {
|
||
if (track.type === 'audio') {
|
||
if (track.config.segmentCodec === 'mp3' && track.config.codec === 'mp3') {
|
||
return MP4.box(MP4.types.stsd, MP4.STSD, MP4.mp3(track.config));
|
||
}
|
||
if (track.config.segmentCodec === 'ac3') {
|
||
return MP4.box(MP4.types.stsd, MP4.STSD, MP4.ac3(track.info, track.config));
|
||
}
|
||
else if (track.config.segmentCodec === 'ec3') {
|
||
return MP4.box(MP4.types.stsd, MP4.STSD, MP4.ec3(track.info, track.config));
|
||
}
|
||
else if (track.config.segmentCodec === 'aac') {
|
||
return MP4.box(MP4.types.stsd, MP4.STSD, MP4.mp4a(track.info, track.config));
|
||
}
|
||
else {
|
||
throw `unknown segmentCodec ${track.config.segmentCodec}`;
|
||
}
|
||
}
|
||
else {
|
||
return MP4.box(MP4.types.stsd, MP4.STSD, MP4.avc1(track));
|
||
}
|
||
}
|
||
static tkhd(track) {
|
||
const id = track.info.id;
|
||
const duration = track.info.duration * track.info.timescale;
|
||
const upperWordDuration = Math.floor(duration / (UINT32_MAX$1 + 1));
|
||
const lowerWordDuration = Math.floor(duration % (UINT32_MAX$1 + 1));
|
||
let width = 0;
|
||
let height = 0;
|
||
if (track.type === 'video') {
|
||
width = track.config.width;
|
||
height = track.config.height;
|
||
}
|
||
return MP4.box(MP4.types.tkhd, new Uint8Array([
|
||
1,
|
||
0,
|
||
0,
|
||
7,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
2,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
3,
|
||
(id >> 24) & 255,
|
||
(id >> 16) & 255,
|
||
(id >> 8) & 255,
|
||
id & 255,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
upperWordDuration >> 24,
|
||
(upperWordDuration >> 16) & 255,
|
||
(upperWordDuration >> 8) & 255,
|
||
upperWordDuration & 255,
|
||
lowerWordDuration >> 24,
|
||
(lowerWordDuration >> 16) & 255,
|
||
(lowerWordDuration >> 8) & 255,
|
||
lowerWordDuration & 255,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
1,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
1,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
64,
|
||
0,
|
||
0,
|
||
0,
|
||
(width >> 8) & 255,
|
||
width & 255,
|
||
0,
|
||
0,
|
||
(height >> 8) & 255,
|
||
height & 255,
|
||
0,
|
||
0,
|
||
]));
|
||
}
|
||
static traf(track, baseMediaDecodeTime) {
|
||
const sencOffset = 76; // mdat header
|
||
const sampleEncryptionBoxTuple = MP4.senc(track);
|
||
const sampleDependencyTable = MP4.sdtp(track), sampleEncryptionBox = sampleEncryptionBoxTuple.boxData, sampleEncryptionOffsetBox = sampleEncryptionBox.length ? MP4.saio(sencOffset) : new Uint8Array(), sampleEncryptionSizeBox = sampleEncryptionBox.length ? MP4.saiz(sampleEncryptionBoxTuple.defaultSampleInfoSize, sampleEncryptionBoxTuple.sampleInfoSizes) : new Uint8Array(), sampleToGroupBox = MP4.sbgp(track), sampleGroupDescriptionBox = MP4.sgpd(track), id = track.id, upperWordBaseMediaDecodeTime = Math.floor(baseMediaDecodeTime / (UINT32_MAX$1 + 1)), lowerWordBaseMediaDecodeTime = Math.floor(baseMediaDecodeTime % (UINT32_MAX$1 + 1));
|
||
// console.log('sampleToGroupBox:' + Hex.hexDump(sampleToGroupBox));
|
||
// console.log('sampleGroupDescriptionBox:' + Hex.hexDump(sampleGroupDescriptionBox));
|
||
return MP4.box(MP4.types.traf, MP4.box(MP4.types.tfhd, new Uint8Array([
|
||
0,
|
||
2,
|
||
0,
|
||
0,
|
||
id >> 24,
|
||
(id >> 16) & 255,
|
||
(id >> 8) & 255,
|
||
id & 255, // track_ID
|
||
])), MP4.box(MP4.types.tfdt, new Uint8Array([
|
||
1,
|
||
0,
|
||
0,
|
||
0,
|
||
upperWordBaseMediaDecodeTime >> 24,
|
||
(upperWordBaseMediaDecodeTime >> 16) & 255,
|
||
(upperWordBaseMediaDecodeTime >> 8) & 255,
|
||
upperWordBaseMediaDecodeTime & 255,
|
||
lowerWordBaseMediaDecodeTime >> 24,
|
||
(lowerWordBaseMediaDecodeTime >> 16) & 255,
|
||
(lowerWordBaseMediaDecodeTime >> 8) & 255,
|
||
lowerWordBaseMediaDecodeTime & 255,
|
||
])), sampleEncryptionBox, sampleEncryptionOffsetBox, sampleEncryptionSizeBox, sampleToGroupBox, sampleGroupDescriptionBox, MP4.trun(track, sampleDependencyTable.length +
|
||
sampleEncryptionBox.length +
|
||
sampleToGroupBox.length +
|
||
sampleGroupDescriptionBox.length +
|
||
sampleEncryptionOffsetBox.length +
|
||
sampleEncryptionSizeBox.length + 16 + // tfhd
|
||
20 + // tfdt
|
||
8 + // traf header
|
||
16 + // mfhd
|
||
8 + // moof header
|
||
8), // mdat header
|
||
sampleDependencyTable);
|
||
}
|
||
/**
|
||
* Generate a track box.
|
||
* @param track {object} a track definition
|
||
* @return {Uint8Array} the track box
|
||
*/
|
||
static trak(track) {
|
||
if ('trakData' in track) {
|
||
// cached trak
|
||
return track.trakData;
|
||
}
|
||
track.info.duration = track.info.duration || 4294967295;
|
||
const trak = MP4.types.trak;
|
||
const thkd = MP4.tkhd(track);
|
||
const mdia = MP4.mdia(track);
|
||
return MP4.box(trak, thkd, mdia);
|
||
}
|
||
static trex(track) {
|
||
const id = track.info.id;
|
||
return MP4.box(MP4.types.trex, new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
id >> 24,
|
||
(id >> 16) & 255,
|
||
(id >> 8) & 255,
|
||
id & 255,
|
||
0,
|
||
0,
|
||
0,
|
||
1,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
1,
|
||
0,
|
||
1,
|
||
]));
|
||
}
|
||
static trun(track, offset) {
|
||
const samples = track.samples || [], len = samples.length, arraylen = 12 + 16 * len, array = new Uint8Array(arraylen);
|
||
let i, sample, duration, size, flags, cts;
|
||
offset += 8 + arraylen;
|
||
array.set([
|
||
0,
|
||
0,
|
||
15,
|
||
1,
|
||
(len >>> 24) & 255,
|
||
(len >>> 16) & 255,
|
||
(len >>> 8) & 255,
|
||
len & 255,
|
||
(offset >>> 24) & 255,
|
||
(offset >>> 16) & 255,
|
||
(offset >>> 8) & 255,
|
||
offset & 255, // data_offset
|
||
], 0);
|
||
for (i = 0; i < len; i++) {
|
||
sample = samples[i];
|
||
duration = sample.duration;
|
||
size = sample.size;
|
||
flags = sample.flags;
|
||
cts = sample.cts;
|
||
array.set([
|
||
(duration >>> 24) & 255,
|
||
(duration >>> 16) & 255,
|
||
(duration >>> 8) & 255,
|
||
duration & 255,
|
||
(size >>> 24) & 255,
|
||
(size >>> 16) & 255,
|
||
(size >>> 8) & 255,
|
||
size & 255,
|
||
(flags.isLeading << 2) | flags.dependsOn,
|
||
(flags.isDependedOn << 6) | (flags.hasRedundancy << 4) | (flags.paddingValue << 1) | flags.isNonSync,
|
||
flags.degradPrio & (240 << 8),
|
||
flags.degradPrio & 15,
|
||
(cts >>> 24) & 255,
|
||
(cts >>> 16) & 255,
|
||
(cts >>> 8) & 255,
|
||
cts & 255, // sample_composition_time_offset
|
||
], 12 + 16 * i);
|
||
}
|
||
return MP4.box(MP4.types.trun, array);
|
||
}
|
||
static initSegment(tracks) {
|
||
if (!MP4.types) {
|
||
MP4.init();
|
||
}
|
||
const movie = MP4.moov(tracks);
|
||
const result = new Uint8Array(MP4.FTYP.byteLength + movie.byteLength);
|
||
result.set(MP4.FTYP);
|
||
result.set(movie, MP4.FTYP.byteLength);
|
||
return result;
|
||
}
|
||
// encryption boxes
|
||
static saio(sencOffset) {
|
||
const subOffset = sencOffset + 4 + 4; // skip version/flags and sample_count
|
||
return MP4.box(MP4.types.saio, new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
1,
|
||
(subOffset >> 24) & 255,
|
||
(subOffset >> 16) & 255,
|
||
(subOffset >> 8) & 255,
|
||
subOffset & 255,
|
||
]));
|
||
}
|
||
static saiz(defaultSampleInfoSize, sampleInfoSizes) {
|
||
if (!isFiniteNumber(defaultSampleInfoSize)) {
|
||
defaultSampleInfoSize = 0;
|
||
}
|
||
const sampleCount = sampleInfoSizes.length;
|
||
const perSampleSizeData = defaultSampleInfoSize === 0 ? new Uint8Array(sampleInfoSizes) : new Uint8Array();
|
||
return MP4.box(MP4.types.saiz, new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
defaultSampleInfoSize,
|
||
(sampleCount >> 24) & 255,
|
||
(sampleCount >> 16) & 255,
|
||
(sampleCount >> 8) & 255,
|
||
sampleCount & 255,
|
||
]), perSampleSizeData);
|
||
}
|
||
static senc(track) {
|
||
const samples = track.samples || [], sampleCount = samples.length;
|
||
let totalSubsamples = 0;
|
||
let lastSize = NaN;
|
||
let hasDefaultSampleSize = true;
|
||
const sampleInfoSizes = [];
|
||
if (!track.encrypted || sampleCount <= 0) {
|
||
return { boxData: new Uint8Array(), sampleInfoSizes, defaultSampleInfoSize: 0 };
|
||
}
|
||
const defaultPerSampleIVSize = track.defaultPerSampleIVSize ? track.defaultPerSampleIVSize : 0;
|
||
for (const sample of samples) {
|
||
if (sample.subsamples) {
|
||
totalSubsamples += sample.subsamples.length;
|
||
}
|
||
}
|
||
if (totalSubsamples <= 0) {
|
||
// don't create a senc if there are no subsamples
|
||
return { boxData: new Uint8Array(), sampleInfoSizes, defaultSampleInfoSize: 0 };
|
||
}
|
||
// 4 bytes for sample_count
|
||
// 2 bytes per sample for subsample_count
|
||
// 6 bytes per subsample for unsigned int(16) BytesOfClearData and unsigned int(32) BytesOfProtectedData;
|
||
// defaultPerSampleIVSize bytes per sample
|
||
const boxdata = new Uint8Array(4 + (sampleCount * 2 + sampleCount * defaultPerSampleIVSize + totalSubsamples * 6));
|
||
let offset = this.set32(sampleCount, boxdata, 0);
|
||
for (const sample of samples) {
|
||
const subsamples = sample.subsamples ? sample.subsamples : [];
|
||
let subsampleSize = 2; // size in bytes of the subsample entry - start with 2 for the subsample count
|
||
if (sample.iv) {
|
||
// per sample IV from cenc aux data
|
||
boxdata.set(sample.iv, offset);
|
||
offset += sample.iv.byteLength;
|
||
subsampleSize += sample.iv.byteLength;
|
||
}
|
||
offset = this.set16(subsamples.length, boxdata, offset);
|
||
for (const subsample of subsamples) {
|
||
offset = this.set16(subsample[0], boxdata, offset);
|
||
offset = this.set32(subsample[1], boxdata, offset);
|
||
subsampleSize += 6;
|
||
}
|
||
sampleInfoSizes.push(subsampleSize);
|
||
if (!isFiniteNumber(lastSize)) {
|
||
lastSize = subsampleSize;
|
||
}
|
||
hasDefaultSampleSize = hasDefaultSampleSize && lastSize === subsampleSize;
|
||
lastSize = subsampleSize;
|
||
}
|
||
const boxData = MP4.box(MP4.types.senc, new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
2,
|
||
]), boxdata);
|
||
return { boxData, defaultSampleInfoSize: hasDefaultSampleSize ? lastSize : 0, sampleInfoSizes };
|
||
}
|
||
static sinf(decryptdata, type, originalCodingName) {
|
||
return MP4.box(MP4.types.sinf, MP4.frma(originalCodingName), MP4.schm(), MP4.schi(decryptdata, type));
|
||
}
|
||
static frma(originalCodingName) {
|
||
return MP4.box(MP4.types.frma, new Uint8Array(originalCodingName));
|
||
}
|
||
static schm() {
|
||
return MP4.box(MP4.types.schm, new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
99,
|
||
98,
|
||
99,
|
||
115,
|
||
0,
|
||
1,
|
||
0,
|
||
0,
|
||
]));
|
||
}
|
||
static schi(decryptdata, type) {
|
||
return MP4.box(MP4.types.schi, MP4.tenc(decryptdata, type));
|
||
}
|
||
static tenc(decryptdata, trackType) {
|
||
let skipPattern = 0;
|
||
if (trackType === 'video') {
|
||
skipPattern = 25; // 1 default_crypt_byte_block, 9 default_skip_byte_block
|
||
}
|
||
const defaultIV = new Uint8Array(17);
|
||
defaultIV[0] = 16; // default_constant_IV_size (16)
|
||
if (decryptdata.iv && decryptdata.iv.byteLength === 16) {
|
||
defaultIV.set(decryptdata.iv, 1);
|
||
}
|
||
if (!decryptdata.keyId) {
|
||
throw 'tenc: no key id found in decryptdata';
|
||
}
|
||
return MP4.box(MP4.types.tenc, new Uint8Array([
|
||
1,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
skipPattern,
|
||
1,
|
||
0,
|
||
]), decryptdata.keyId, // default_KID, 16 bytes
|
||
defaultIV);
|
||
}
|
||
// only using this to write seig entries
|
||
static sbgp(track) {
|
||
if (!track.encrypted || track.samples.length === 0 || !track.samples[0].keyTagInfo) {
|
||
return new Uint8Array();
|
||
}
|
||
// at this point we're assuming all samples in the track have the same key id
|
||
// this should hold true for the current ts/mp4 remux path
|
||
const sampleCount = track.samples.length;
|
||
return MP4.box(MP4.types.sbgp, new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
]), new Uint8Array(MP4.types.seig), // grouping_type
|
||
new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
1,
|
||
(sampleCount >> 24) & 255,
|
||
(sampleCount >> 16) & 255,
|
||
(sampleCount >> 8) & 255,
|
||
sampleCount & 255,
|
||
0,
|
||
1,
|
||
0,
|
||
1,
|
||
]));
|
||
}
|
||
// only using this to write seig entries
|
||
static sgpd(track) {
|
||
if (!track.encrypted || track.samples.length === 0 || !track.samples[0].keyTagInfo) {
|
||
return new Uint8Array();
|
||
}
|
||
const sDecryptdata = track.samples[0].keyTagInfo;
|
||
let skipPattern = 0;
|
||
if (track.type === 'video') {
|
||
skipPattern = 25; // 1 default_crypt_byte_block, 9 default_skip_byte_block
|
||
}
|
||
const sizeAndIv = new Uint8Array(17);
|
||
sizeAndIv[0] = 16;
|
||
if (sDecryptdata.iv) {
|
||
sizeAndIv.set(sDecryptdata.iv, 1);
|
||
}
|
||
if (!sDecryptdata.keyId) {
|
||
throw 'sgpd: no keyid in decryptdata';
|
||
}
|
||
return MP4.box(MP4.types.sgpd, new Uint8Array([
|
||
1,
|
||
0,
|
||
0,
|
||
0,
|
||
]), new Uint8Array(MP4.types.seig), // grouping_type
|
||
new Uint8Array([
|
||
0,
|
||
0,
|
||
0,
|
||
37,
|
||
0,
|
||
0,
|
||
0,
|
||
1,
|
||
]), new Uint8Array([
|
||
0,
|
||
skipPattern,
|
||
1,
|
||
0,
|
||
]), sDecryptdata.keyId, sizeAndIv);
|
||
}
|
||
/**
|
||
* @param {Uint8Array} systemId 16 bytes
|
||
* @param {array of Uint8Array} keyids The list of key ids described by this PSSH box
|
||
* @param {Uint8Array} An arbitrary buffer of data to be used in the data field of the PSSH box
|
||
* @returns {Uint8Array} Bytes representing a PSSH box
|
||
*/
|
||
static pssh(systemId, keyids, data) {
|
||
if (!MP4.types) {
|
||
MP4.init();
|
||
}
|
||
if (!systemId) {
|
||
throw new TypeError('Bad system id');
|
||
}
|
||
if (systemId.byteLength !== 16) {
|
||
throw new RangeError('Invalid system id');
|
||
}
|
||
let version;
|
||
let kids;
|
||
if (keyids) {
|
||
version = 1;
|
||
kids = new Uint8Array(keyids.length * 16);
|
||
for (let ix = 0; ix < keyids.length; ix++) {
|
||
const k = keyids[ix]; // uint8array
|
||
if (k.byteLength !== 16) {
|
||
throw new RangeError('Invalid key');
|
||
}
|
||
kids.set(k, ix * 16);
|
||
}
|
||
}
|
||
else {
|
||
version = 0;
|
||
kids = new Uint8Array();
|
||
}
|
||
let kidCount;
|
||
if (version > 0) {
|
||
kidCount = new Uint8Array(4);
|
||
if (keyids.length > 0) {
|
||
new DataView(kidCount.buffer).setUint32(0, keyids.length, false); // Big endian
|
||
}
|
||
}
|
||
else {
|
||
kidCount = new Uint8Array();
|
||
}
|
||
const dataSize = new Uint8Array(4); // Mandatory field
|
||
if (data && data.byteLength > 0) {
|
||
new DataView(dataSize.buffer).setUint32(0, data.byteLength, false); // Big endian
|
||
}
|
||
return MP4.box(MP4.types.pssh, new Uint8Array([
|
||
version,
|
||
0,
|
||
0,
|
||
0,
|
||
]), systemId, // 16 bytes
|
||
kidCount, kids, dataSize, data || new Uint8Array());
|
||
}
|
||
}
|
||
var MP4$1 = MP4;
|
||
|
||
var VideoDynamicRangeType;
|
||
(function (VideoDynamicRangeType) {
|
||
// should be same as AirPlayVideoDynamicRangeFormat
|
||
VideoDynamicRangeType[VideoDynamicRangeType["SDR"] = 0] = "SDR";
|
||
VideoDynamicRangeType[VideoDynamicRangeType["HDR"] = 1] = "HDR";
|
||
VideoDynamicRangeType[VideoDynamicRangeType["HDR10"] = 2] = "HDR10";
|
||
VideoDynamicRangeType[VideoDynamicRangeType["DolbyVision"] = 3] = "DolbyVision";
|
||
VideoDynamicRangeType[VideoDynamicRangeType["HLG"] = 4] = "HLG";
|
||
})(VideoDynamicRangeType || (VideoDynamicRangeType = {}));
|
||
var CompressionType;
|
||
(function (CompressionType) {
|
||
// should be same as AirPlayCompressionType
|
||
CompressionType[CompressionType["H264"] = 16] = "H264";
|
||
CompressionType[CompressionType["HEVC"] = 64] = "HEVC";
|
||
CompressionType[CompressionType["VP09"] = 65] = "VP09";
|
||
})(CompressionType || (CompressionType = {}));
|
||
|
||
// import { AudioSegmentCodecType } from '../types/tracks';
|
||
/*
|
||
* 2018 Apple Inc. All rights reserved.
|
||
*/
|
||
const ac3Codecs = new Set(['ac-3', 'mp4a.a5', 'mp4a.A5']);
|
||
const ec3Codecs = new Set(['ec-3', 'mp4a.a6', 'mp4a.A6']);
|
||
const SampleDurationMap = { aac: 1024, mp3: 1024, ac3: 1536, ec3: 1536 };
|
||
const MediaUtil = {
|
||
isAC3(codec) {
|
||
return Boolean(codec && ac3Codecs.has(codec));
|
||
},
|
||
isEC3(codec) {
|
||
return Boolean(codec && ec3Codecs.has(codec));
|
||
},
|
||
isDolbyAtmos(codec, channels) {
|
||
const parameters = channels.split('/');
|
||
return Boolean(MediaUtil.isEC3(codec) && parameters.length > 1 && parameters[1].split(',').find((x) => x === 'JOC'));
|
||
},
|
||
isAAC(codec) {
|
||
let match;
|
||
return Boolean(codec && (codec === 'aac' || ((match = codec.match(/^mp4a\.40\.(.*)/)) !== null && match[1] !== '34')));
|
||
},
|
||
isMP3(codec) {
|
||
let match;
|
||
return Boolean(codec && (codec === 'mp3' || ((match = codec.match(/^mp4a\.40\.(.*)/)) !== null && match[1] === '34')));
|
||
},
|
||
isAVC(codec) {
|
||
// basic match without validation
|
||
return Boolean(codec && codec.match(/^avc[13]\.(.*)/));
|
||
},
|
||
isXHEAAC: function (codec) {
|
||
return Boolean(codec === 'mp4a.40.42');
|
||
},
|
||
isALAC: function (codec) {
|
||
return Boolean(codec === 'alac');
|
||
},
|
||
isFLAC: function (codec) {
|
||
return Boolean(codec === 'fLaC');
|
||
},
|
||
isHEVC(codec) {
|
||
// basic match without validation
|
||
return Boolean(codec && codec.match(/^(hev|hvc)1\..*/));
|
||
},
|
||
isDolby(codec) {
|
||
return Boolean(codec && codec.match(/^dv(h1|he|a1|av)\..*/));
|
||
},
|
||
isVP09(codec) {
|
||
// basic match without validation
|
||
return Boolean(codec && codec.match(/^vp09\..*/));
|
||
},
|
||
isCompatibleCodecString(c1, c2) {
|
||
const codecs1 = c1.split(',');
|
||
const codecs2 = c2.split(',');
|
||
const videoCodec1 = codecs1.filter((codec) => MediaUtil.isVideoCodec(codec));
|
||
const videoCodec2 = codecs2.filter((codec) => MediaUtil.isVideoCodec(codec));
|
||
const audioCodec1 = codecs1.filter((codec) => MediaUtil.isAudioCodec(codec));
|
||
const audioCodec2 = codecs2.filter((codec) => MediaUtil.isAudioCodec(codec));
|
||
const videoOk = (videoCodec1.length === 0 && videoCodec2.length === 0) || (videoCodec1.length === videoCodec2.length && MediaUtil.isCompatibleVideoCodec(videoCodec1[0], videoCodec2[0]));
|
||
const audioOk = (audioCodec1.length === 0 && audioCodec2.length === 0) || (audioCodec1.length === audioCodec2.length && MediaUtil.isCompatibleAudioCodec(audioCodec1[0], audioCodec2[0]));
|
||
return videoOk && audioOk;
|
||
},
|
||
isVideoCodec(codec) {
|
||
return MediaUtil.isAVC(codec) || MediaUtil.isDolby(codec) || MediaUtil.isHEVC(codec) || MediaUtil.isVP09(codec);
|
||
},
|
||
isAudioCodec(codec) {
|
||
return MediaUtil.isAC3(codec) || MediaUtil.isEC3(codec) || MediaUtil.isAAC(codec) || MediaUtil.isMP3(codec);
|
||
},
|
||
isCompatibleVideoCodec(c1, c2) {
|
||
return Boolean(c1 &&
|
||
c2 &&
|
||
(c1 === c2 ||
|
||
(MediaUtil.isDolby(c1) && MediaUtil.isDolby(c2)) ||
|
||
(MediaUtil.isHEVC(c1) && MediaUtil.isHEVC(c2)) ||
|
||
(MediaUtil.isAVC(c1) && MediaUtil.isAVC(c2)) ||
|
||
(MediaUtil.isVP09(c1) && MediaUtil.isVP09(c2))));
|
||
},
|
||
isCompatibleAudioCodec(c1, c2) {
|
||
return Boolean(c1 &&
|
||
c2 &&
|
||
(c1 === c2 ||
|
||
(MediaUtil.isAAC(c1) && MediaUtil.isAAC(c2)) ||
|
||
(MediaUtil.isAC3(c1) && MediaUtil.isAC3(c2)) ||
|
||
(MediaUtil.isEC3(c1) && MediaUtil.isEC3(c2)) ||
|
||
(MediaUtil.isMP3(c1) && MediaUtil.isMP3(c2))));
|
||
},
|
||
getSegmentCodec(audioCodec) {
|
||
let segmentCodec;
|
||
if (MediaUtil.isAAC(audioCodec)) {
|
||
segmentCodec = 'aac';
|
||
}
|
||
else if (MediaUtil.isAC3(audioCodec)) {
|
||
segmentCodec = 'ac3';
|
||
}
|
||
else if (MediaUtil.isEC3(audioCodec)) {
|
||
segmentCodec = 'ec3';
|
||
}
|
||
else if (audioCodec === 'mp3') {
|
||
segmentCodec = 'mp3';
|
||
}
|
||
else {
|
||
throw new Error(`invalid audio config, codec ${audioCodec}`);
|
||
}
|
||
return segmentCodec;
|
||
},
|
||
getChannelCount(channels) {
|
||
if (!channels) {
|
||
return 0;
|
||
}
|
||
const parameters = channels.split('/');
|
||
const channelCount = parseInt(parameters[0]);
|
||
if (!isFiniteNumber(channelCount)) {
|
||
return 0;
|
||
}
|
||
return channelCount;
|
||
},
|
||
avc1toavcoti(codec) {
|
||
var _a, _b;
|
||
const avcdata = codec.split('.');
|
||
let result;
|
||
if (avcdata.length > 2) {
|
||
result = avcdata.shift() + '.';
|
||
result += parseInt((_a = avcdata.shift()) !== null && _a !== void 0 ? _a : '').toString(16);
|
||
result += ('000' + parseInt((_b = avcdata.shift()) !== null && _b !== void 0 ? _b : '').toString(16)).substr(-4);
|
||
}
|
||
else {
|
||
result = codec;
|
||
}
|
||
return result;
|
||
},
|
||
getDynamicRangeType(videoRange, videoCodec) {
|
||
let type = VideoDynamicRangeType.SDR; // default to SDR
|
||
if (videoRange === 'PQ' && MediaUtil.isDolby(videoCodec)) {
|
||
type = VideoDynamicRangeType.DolbyVision;
|
||
}
|
||
else if (videoRange === 'PQ' && (MediaUtil.isHEVC(videoCodec) || MediaUtil.isVP09(videoCodec))) {
|
||
type = VideoDynamicRangeType.HDR10;
|
||
}
|
||
else if (videoRange === 'HLG' && (videoCodec.indexOf('hvc1') !== -1 || MediaUtil.isVP09(videoCodec))) {
|
||
type = VideoDynamicRangeType.HLG;
|
||
}
|
||
return type;
|
||
},
|
||
getCompressionType(videoCodec) {
|
||
let compressionType = CompressionType.H264;
|
||
if (MediaUtil.isHEVC(videoCodec) || MediaUtil.isDolby(videoCodec)) {
|
||
compressionType = CompressionType.HEVC;
|
||
}
|
||
else if (MediaUtil.isVP09(videoCodec)) {
|
||
compressionType = CompressionType.VP09;
|
||
}
|
||
return compressionType;
|
||
},
|
||
isHigherCodecByFamily(currentCodec, newCodec) {
|
||
if (!currentCodec) {
|
||
return true;
|
||
}
|
||
const splitCurrentCodec = currentCodec.split('.');
|
||
const splitNewCodec = newCodec.split('.');
|
||
if (splitCurrentCodec[0] !== splitNewCodec[0]) {
|
||
throw new Error(`mismatch in codec family current/new: ${splitCurrentCodec[0]}/${splitNewCodec[0]}`);
|
||
}
|
||
switch (splitCurrentCodec[0]) {
|
||
case 'avc1':
|
||
case 'avc3':
|
||
return splitNewCodec[1] > splitCurrentCodec[1];
|
||
case 'vp09':
|
||
return newCodec > currentCodec;
|
||
case 'hvc1':
|
||
case 'hev1':
|
||
const currentTier = splitCurrentCodec[3].substring(0, 1) === 'H' ? 1 : 0;
|
||
const currentLevel = splitCurrentCodec[3].substring(1);
|
||
const newTier = splitNewCodec[3].substring(0, 1) === 'H' ? 1 : 0;
|
||
const newLevel = splitNewCodec[3].substring(1);
|
||
return splitNewCodec[1] > splitCurrentCodec[1] || splitNewCodec[2] > splitCurrentCodec[2] || newTier > currentTier || newLevel > currentLevel;
|
||
case 'dvh1':
|
||
return splitNewCodec[1] > splitCurrentCodec[1] || splitNewCodec[2] > splitCurrentCodec[2];
|
||
}
|
||
},
|
||
};
|
||
|
||
class SilentAudio {
|
||
static getTrack(observer, id, codec, channelCount, logger) {
|
||
let track;
|
||
const segmentCodec = MediaUtil.getSegmentCodec(codec);
|
||
switch (segmentCodec) {
|
||
case 'aac':
|
||
{
|
||
let config;
|
||
if (channelCount === 1) {
|
||
config = ADTS.getAudioConfig(observer, new Uint8Array([255, 241, 92, 64, 1, 127, 252]), 0, codec, logger);
|
||
}
|
||
else {
|
||
config = ADTS.getAudioConfig(observer, new Uint8Array([255, 241, 92, 128, 1, 191, 252]), 0, codec, logger);
|
||
}
|
||
if (config) {
|
||
const info = { id, timescale: config.samplerate, duration: 0, encrypted: false, keyTagInfo: undefined };
|
||
track = { type: 'audio', info, config };
|
||
}
|
||
}
|
||
break;
|
||
case 'ac3':
|
||
case 'ec3':
|
||
{
|
||
const config = Dolby.getAudioConfig(observer, new Uint8Array([11, 119, 69, 17, 128, 64, 47, 132]), 0, logger);
|
||
if (config) {
|
||
const info = { id, timescale: config.samplerate, duration: 0, encrypted: false, keyTagInfo: undefined };
|
||
track = { type: 'audio', info, config };
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
return track;
|
||
}
|
||
static getSample(codec, channelCount) {
|
||
let sample;
|
||
switch (codec) {
|
||
case 'mp4a.40.2':
|
||
case 'mp4a.40.5':
|
||
if (channelCount === 1) {
|
||
sample = new Uint8Array([0, 208, 0, 7]);
|
||
}
|
||
else {
|
||
sample = new Uint8Array([33, 0, 3, 64, 104, 28]);
|
||
}
|
||
break;
|
||
case 'ac-3':
|
||
case 'ec-3':
|
||
sample = new Uint8Array([
|
||
11,
|
||
119,
|
||
69,
|
||
17,
|
||
128,
|
||
64,
|
||
47,
|
||
132,
|
||
41,
|
||
3,
|
||
253,
|
||
214,
|
||
124,
|
||
253,
|
||
243,
|
||
215,
|
||
233,
|
||
95,
|
||
185,
|
||
123,
|
||
78,
|
||
20,
|
||
40,
|
||
106,
|
||
97,
|
||
190,
|
||
74,
|
||
253,
|
||
43,
|
||
218,
|
||
208,
|
||
140,
|
||
191,
|
||
176,
|
||
144,
|
||
120,
|
||
214,
|
||
181,
|
||
44,
|
||
124,
|
||
129,
|
||
251,
|
||
91,
|
||
109,
|
||
187,
|
||
109,
|
||
198,
|
||
225,
|
||
43,
|
||
172,
|
||
116,
|
||
140,
|
||
176,
|
||
123,
|
||
38,
|
||
144,
|
||
211,
|
||
247,
|
||
225,
|
||
64,
|
||
29,
|
||
53,
|
||
175,
|
||
96,
|
||
16,
|
||
57,
|
||
121,
|
||
87,
|
||
78,
|
||
203,
|
||
81,
|
||
37,
|
||
7,
|
||
72,
|
||
228,
|
||
132,
|
||
37,
|
||
169,
|
||
38,
|
||
231,
|
||
97,
|
||
229,
|
||
247,
|
||
194,
|
||
208,
|
||
8,
|
||
12,
|
||
83,
|
||
74,
|
||
139,
|
||
137,
|
||
17,
|
||
22,
|
||
26,
|
||
221,
|
||
203,
|
||
107,
|
||
113,
|
||
94,
|
||
93,
|
||
75,
|
||
33,
|
||
208,
|
||
247,
|
||
146,
|
||
105,
|
||
39,
|
||
143,
|
||
6,
|
||
36,
|
||
1,
|
||
227,
|
||
108,
|
||
70,
|
||
11,
|
||
180,
|
||
152,
|
||
218,
|
||
182,
|
||
218,
|
||
209,
|
||
59,
|
||
85,
|
||
104,
|
||
201,
|
||
70,
|
||
37,
|
||
82,
|
||
219,
|
||
68,
|
||
55,
|
||
225,
|
||
144,
|
||
99,
|
||
149,
|
||
0,
|
||
119,
|
||
26,
|
||
14,
|
||
69,
|
||
164,
|
||
241,
|
||
204,
|
||
222,
|
||
81,
|
||
177,
|
||
142,
|
||
80,
|
||
20,
|
||
100,
|
||
97,
|
||
143,
|
||
101,
|
||
221,
|
||
140,
|
||
113,
|
||
31,
|
||
208,
|
||
124,
|
||
25,
|
||
64,
|
||
29,
|
||
49,
|
||
77,
|
||
140,
|
||
30,
|
||
155,
|
||
74,
|
||
214,
|
||
204,
|
||
138,
|
||
229,
|
||
109,
|
||
172,
|
||
95,
|
||
130,
|
||
70,
|
||
230,
|
||
134,
|
||
88,
|
||
59,
|
||
179,
|
||
212,
|
||
155,
|
||
232,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
173,
|
||
234,
|
||
]);
|
||
break;
|
||
}
|
||
return sample;
|
||
}
|
||
static getSegment(silentTrack, sequenceNumber, startDtsTimescale, segmentDuration) {
|
||
if (!silentTrack) {
|
||
return;
|
||
}
|
||
const { timescale } = silentTrack.info;
|
||
const { segmentCodec } = silentTrack.config;
|
||
const silentFrame = SilentAudio.getSample(silentTrack.config.codec, silentTrack.config.channelCount);
|
||
if (!silentFrame) {
|
||
return;
|
||
}
|
||
const samples = [];
|
||
const track = {
|
||
id: silentTrack.info.id,
|
||
sequenceNumber,
|
||
type: 'audio',
|
||
encrypted: false,
|
||
samples,
|
||
defaultPerSampleIVSize: 0,
|
||
};
|
||
const sampleDuration = SampleDurationMap[segmentCodec];
|
||
const nbSamples = Math.ceil((segmentDuration * timescale) / sampleDuration);
|
||
const baseTime = Math.round(nbSamples * sampleDuration + startDtsTimescale);
|
||
const endTs = { baseTime, timescale };
|
||
let offset = 0;
|
||
const mdatSize = nbSamples * silentFrame.byteLength + 8;
|
||
const mdat = new Uint8Array(mdatSize);
|
||
mdat[0] = (mdatSize >> 24) & 255;
|
||
mdat[1] = (mdatSize >> 16) & 255;
|
||
mdat[2] = (mdatSize >> 8) & 255;
|
||
mdat[3] = mdatSize & 255;
|
||
if (!MP4$1.types) {
|
||
MP4$1.init();
|
||
}
|
||
mdat.set(MP4$1.types.mdat, 4);
|
||
offset += 8;
|
||
for (let i = 0; i < nbSamples; i++) {
|
||
samples.push({
|
||
duration: sampleDuration,
|
||
size: silentFrame.byteLength,
|
||
cts: 0,
|
||
flags: {
|
||
isLeading: 0,
|
||
isDependedOn: 0,
|
||
hasRedundancy: 0,
|
||
degradPrio: 0,
|
||
dependsOn: 1,
|
||
isNonSync: 0,
|
||
paddingValue: 0,
|
||
},
|
||
});
|
||
mdat.set(silentFrame, offset);
|
||
offset += silentFrame.byteLength;
|
||
}
|
||
const moof = MP4$1.moof(startDtsTimescale, track);
|
||
const silentFragData = new Uint8Array(moof.byteLength + mdat.byteLength);
|
||
silentFragData.set(moof);
|
||
silentFragData.set(mdat, moof.byteLength);
|
||
return { silentFragData, endTs };
|
||
}
|
||
}
|
||
|
||
// 10 seconds
|
||
const MAX_SILENT_FRAME_DURATION = 10000;
|
||
class EsRemuxer extends RemuxerBase {
|
||
constructor(observer, config, typeSupported, vendor, logger) {
|
||
super(observer, config, logger);
|
||
this.typeSupported = typeSupported;
|
||
this.isVideoContiguous = false;
|
||
this.logger = logger.child({ name: 'EsRemuxer' });
|
||
const userAgent = navigator.userAgent;
|
||
this.isSafari = vendor && vendor.indexOf('Apple') > -1 && userAgent && !userAgent.match('CriOS');
|
||
}
|
||
resetTimeStamp(defaultTimeStamp) {
|
||
this._initPTS = this._initDTS = defaultTimeStamp;
|
||
}
|
||
resetInitSegment() {
|
||
this.currentInitTrack = undefined;
|
||
this._silentAudioTrack = undefined;
|
||
}
|
||
/**
|
||
* @param timeOffset The position in the media element corresponding to this fragment when playback rate = 1
|
||
* @param contiguous Whether this fragment is contiguous with previously appended fragment
|
||
* @param accurateTimeOffset Whether timeOffset is reliable
|
||
* @param keyTagInfo Information from EXT-X-KEY tag
|
||
* @param iframeMediaStart in iframe mode, the desired start DTS of the remuxed fragment
|
||
* @param iframeDuration in iframe mode, the desired duration of the remuxed fragment
|
||
*/
|
||
remuxEsTracks(audioTrack, videoTrack, id3Track, textTrack, timeOffset, contiguous, accurateTimeOffset, keyTagInfo, iframeMediaStart, iframeDuration) {
|
||
var _a;
|
||
if (!contiguous) {
|
||
this.isVideoContiguous = false;
|
||
}
|
||
// generate Init Segment if needed
|
||
let silentAudioSegment;
|
||
const timelineOffset = typeof iframeMediaStart === 'undefined' ? timeOffset : iframeMediaStart;
|
||
if (!this.currentInitTrack) {
|
||
if (audioTrack && audioTrack.config.codec) {
|
||
// when we start out in non-iframe mode we need to store the audio parameters
|
||
// we won't get this info from the iframe playlist since the demuxer will skip audio samples
|
||
this._audioTrackInfo = { id: audioTrack.info.id, codec: audioTrack.config.codec, channelCount: audioTrack.config.channelCount };
|
||
}
|
||
if (videoTrack && iframeDuration && this._audioTrackInfo) {
|
||
const silentAudioBase = SilentAudio.getTrack(this.observer, this._audioTrackInfo.id, this._audioTrackInfo.codec, this._audioTrackInfo.channelCount, this.logger);
|
||
if (silentAudioBase) {
|
||
this._silentAudioTrack = Object.assign(Object.assign({}, silentAudioBase), { info: Object.assign(Object.assign({}, silentAudioBase.info), { inputTimescale: 90000 }), parsingData: { len: 0, sequenceNumber: 0, esSamples: [] } });
|
||
// need to populate samples for generateIS
|
||
const segmentStartPTS = this._initPTS + Math.round(timelineOffset * this._silentAudioTrack.info.timescale);
|
||
silentAudioSegment = SilentAudio.getSegment(this._silentAudioTrack, videoTrack.parsingData.sequenceNumber, segmentStartPTS, iframeDuration);
|
||
videoTrack.parsingData.sequenceNumber++;
|
||
}
|
||
}
|
||
else {
|
||
this._silentAudioTrack = undefined;
|
||
}
|
||
this.updateInitPTSDTS(videoTrack, audioTrack, timeOffset); // Use real audioTrack info to update PTSDTS
|
||
this.generateIS(this._silentAudioTrack ? this._silentAudioTrack : audioTrack, videoTrack);
|
||
}
|
||
if (this.currentInitTrack) {
|
||
const remuxVideo = videoTrack && videoTrack.parsingData.esSamples.length;
|
||
const isVideoContiguous = this.isVideoContiguous;
|
||
let audioData, videoData;
|
||
// Purposefully remuxing audio before video, so that remuxVideo can use nextAudioPts, which is
|
||
// calculated in remuxAudio.
|
||
if (videoTrack && iframeDuration && this._silentAudioTrack && !silentAudioSegment) {
|
||
// samples could have been generated before init segment generation
|
||
const segmentStartPTS = this._initPTS + Math.round((timelineOffset + this.config.audioPrimingDelay) * this._silentAudioTrack.info.timescale);
|
||
silentAudioSegment = SilentAudio.getSegment(this._silentAudioTrack, videoTrack.parsingData.sequenceNumber, segmentStartPTS, iframeDuration);
|
||
}
|
||
if (audioTrack && audioTrack.parsingData.esSamples.length) {
|
||
// if initSegment was generated without video samples, regenerate it again
|
||
if (!isFiniteNumber(audioTrack.info.timescale)) {
|
||
this.logger.warn('regenerate InitSegment as audio detected');
|
||
this.updateInitPTSDTS(videoTrack, audioTrack, timeOffset); // Use real audioTrack info to update PTSDTS
|
||
this.generateIS(audioTrack, videoTrack);
|
||
}
|
||
audioData = this.remuxAudio(audioTrack, timelineOffset, contiguous, accurateTimeOffset, keyTagInfo);
|
||
if (remuxVideo) {
|
||
let audioTrackLength;
|
||
if (audioData) {
|
||
audioTrackLength = convertTimestampToSeconds(audioData.endPTS) - convertTimestampToSeconds(audioData.startPTS);
|
||
}
|
||
// if initSegment was generated without video samples, regenerate it again
|
||
if (!isFiniteNumber(videoTrack.info.timescale)) {
|
||
this.logger.warn('regenerate InitSegment as video detected');
|
||
this.updateInitPTSDTS(videoTrack, audioTrack, timeOffset); // Use real audioTrack info to update PTSDTS
|
||
this.generateIS(audioTrack, videoTrack);
|
||
}
|
||
videoData = this.remuxVideo(videoTrack, timelineOffset, isVideoContiguous, audioTrackLength, iframeDuration);
|
||
}
|
||
}
|
||
else {
|
||
if (remuxVideo) {
|
||
videoData = this.remuxVideo(videoTrack, timelineOffset, isVideoContiguous, undefined, iframeDuration);
|
||
}
|
||
if (videoData && audioTrack && audioTrack.config.codec) {
|
||
audioData = this.remuxEmptyAudio(audioTrack, timelineOffset, contiguous, accurateTimeOffset, videoData, keyTagInfo);
|
||
}
|
||
}
|
||
let parsingData;
|
||
if (silentAudioSegment) {
|
||
parsingData = {
|
||
data1: videoData.data1,
|
||
data2: silentAudioSegment.silentFragData,
|
||
startDTS: videoData.startDTS,
|
||
startPTS: videoData.startPTS,
|
||
endDTS: determineMaxTimestamp(videoData.endDTS, silentAudioSegment.endTs),
|
||
endPTS: determineMaxTimestamp(videoData.endPTS, silentAudioSegment.endTs),
|
||
type: 'audiovideo',
|
||
track: this.currentInitTrack,
|
||
};
|
||
}
|
||
else if (videoData && audioData) {
|
||
parsingData = {
|
||
data1: videoData.data1,
|
||
data2: audioData.data1,
|
||
startDTS: determineMinTimestamp(videoData.startDTS, audioData.startDTS),
|
||
startPTS: determineMinTimestamp(videoData.startPTS, audioData.startPTS),
|
||
endDTS: determineMaxTimestamp(videoData.endDTS, audioData.endDTS),
|
||
endPTS: determineMaxTimestamp(videoData.endPTS, audioData.endPTS),
|
||
type: 'audiovideo',
|
||
track: this.currentInitTrack,
|
||
dropped: videoData.dropped,
|
||
framesWithoutIDR: videoData.framesWithoutIDR,
|
||
firstKeyframePts: videoData.firstKeyframePts,
|
||
};
|
||
}
|
||
else if (videoData) {
|
||
parsingData = {
|
||
data1: videoData.data1,
|
||
startDTS: videoData.startDTS,
|
||
startPTS: videoData.startPTS,
|
||
endDTS: videoData.endDTS,
|
||
endPTS: videoData.endPTS,
|
||
type: 'video',
|
||
track: this.currentInitTrack,
|
||
dropped: videoData.dropped,
|
||
framesWithoutIDR: videoData.framesWithoutIDR,
|
||
firstKeyframePts: videoData.firstKeyframePts,
|
||
};
|
||
}
|
||
else if (audioData) {
|
||
parsingData = {
|
||
data1: audioData.data1,
|
||
startDTS: audioData.startDTS,
|
||
startPTS: audioData.startPTS,
|
||
endDTS: audioData.endDTS,
|
||
endPTS: audioData.endPTS,
|
||
type: 'audio',
|
||
track: this.currentInitTrack,
|
||
};
|
||
}
|
||
else {
|
||
this.logger.error('Missing video and audio data');
|
||
}
|
||
if (textTrack && textTrack.captionSamples.length) {
|
||
this.remuxText(textTrack, parsingData);
|
||
}
|
||
if ((_a = id3Track === null || id3Track === void 0 ? void 0 : id3Track.id3Samples) === null || _a === void 0 ? void 0 : _a.length) {
|
||
this.remuxID3(id3Track, parsingData);
|
||
}
|
||
this.observer.trigger(DemuxerEvent.FRAG_PARSING_DATA, parsingData);
|
||
}
|
||
else {
|
||
this.logger.error('failed to generate IS');
|
||
}
|
||
// notify end of parsing
|
||
this.observer.trigger(DemuxerEvent.FRAG_PARSED);
|
||
}
|
||
/**
|
||
* @param videoTrack
|
||
* @param audioTrack
|
||
* @param timeOffset Position in media element corresponding to the beginning of this segment
|
||
*/
|
||
updateInitPTSDTS(videoTrack, audioTrack, timeOffset) {
|
||
let initPTS = Infinity, initDTS = Infinity;
|
||
const videoSamples = videoTrack ? videoTrack.parsingData.esSamples : [];
|
||
const audioSamples = audioTrack ? audioTrack.parsingData.esSamples : [];
|
||
if (isFiniteNumber(this._initPTS)) {
|
||
return;
|
||
}
|
||
if (audioTrack && audioSamples.length) {
|
||
// remember first PTS of this demuxing context. for audio, PTS = DTS
|
||
initPTS = initDTS = audioSamples[0].pts - audioTrack.info.inputTimescale * timeOffset;
|
||
}
|
||
if (videoTrack && videoSamples.length) {
|
||
// let's use input time scale as MP4 video timescale
|
||
// we use input time scale straight away to avoid rounding issues on frame duration / cts computation
|
||
const inputTimeScale = videoTrack.info.inputTimescale;
|
||
videoTrack.info.timescale = inputTimeScale;
|
||
initPTS = Math.min(initPTS, getVideoStartPts(videoSamples) - inputTimeScale * timeOffset);
|
||
initDTS = Math.min(initDTS, videoSamples[0].dts - inputTimeScale * timeOffset);
|
||
this.observer.trigger(DemuxerEvent.INIT_PTS_FOUND, { initPTS: convertSecondsToTimestamp(initPTS, inputTimeScale) });
|
||
}
|
||
if (isFiniteNumber(initPTS) && isFiniteNumber(initDTS)) {
|
||
this._initPTS = initPTS;
|
||
this._initDTS = initDTS;
|
||
}
|
||
else {
|
||
const payload = new FragParsingError(false, 'invalid initPTS or initDTS', ErrorResponses.InvalidInitTimestamp);
|
||
this.observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
}
|
||
}
|
||
generateIS(audioTrack, videoTrack) {
|
||
// const observer = this.observer;
|
||
const videoSamples = videoTrack ? videoTrack.parsingData.esSamples : [];
|
||
const typeSupported = this.typeSupported;
|
||
let container = 'audio/mp4';
|
||
let track;
|
||
if (audioTrack && videoTrack && videoSamples.length) {
|
||
const inputTimeScale = videoTrack.info.inputTimescale;
|
||
videoTrack.info.timescale = inputTimeScale;
|
||
audioTrack.info.timescale = audioTrack.config.samplerate;
|
||
const initSegment = MP4.initSegment([videoTrack, audioTrack]);
|
||
track = {
|
||
type: 'audiovideo',
|
||
container: 'video/mp4',
|
||
codec: `${videoTrack.config.codec},${audioTrack.config.codec}`,
|
||
initSegment,
|
||
};
|
||
}
|
||
else if (audioTrack) {
|
||
// let's use audio sampling rate as MP4 time scale.
|
||
// rationale is that there is a integer nb of audio frames per audio sample (1024 for AAC)
|
||
// using audio sampling rate here helps having an integer MP4 frame duration
|
||
// this avoids potential rounding issue and AV sync issue
|
||
audioTrack.info.timescale = audioTrack.config.samplerate;
|
||
this.logger.info(`audio sampling rate : ${audioTrack.config.samplerate}`);
|
||
switch (audioTrack.config.segmentCodec) {
|
||
case 'mp3':
|
||
if (typeSupported.mpeg) {
|
||
// Chrome and Safari
|
||
container = 'audio/mpeg';
|
||
audioTrack.config.codec = '';
|
||
}
|
||
else if (typeSupported.mp3) {
|
||
// Firefox
|
||
audioTrack.config.codec = 'mp3';
|
||
}
|
||
break;
|
||
}
|
||
const initSegment = audioTrack.config.segmentCodec === 'mp3' && typeSupported.mpeg ? new Uint8Array() : MP4.initSegment([audioTrack]);
|
||
track = {
|
||
type: 'audio',
|
||
container: container,
|
||
codec: audioTrack.config.codec,
|
||
initSegment,
|
||
};
|
||
}
|
||
else if (videoTrack && videoSamples.length) {
|
||
// let's use input time scale as MP4 video timescale
|
||
// we use input time scale straight away to avoid rounding issues on frame duration / cts computation
|
||
const inputTimeScale = videoTrack.info.inputTimescale;
|
||
videoTrack.info.timescale = inputTimeScale;
|
||
const initSegment = MP4.initSegment([videoTrack]);
|
||
track = {
|
||
type: 'video',
|
||
container: 'video/mp4',
|
||
codec: videoTrack.config.codec,
|
||
initSegment,
|
||
};
|
||
}
|
||
if (track) {
|
||
this.currentInitTrack = track;
|
||
const data = { track };
|
||
this.observer.trigger(DemuxerEvent.FRAG_PARSING_INIT_SEGMENT, data);
|
||
}
|
||
else {
|
||
const payload = new FragParsingError(false, 'no audio/video samples found', ErrorResponses.NoAVSamplesFound);
|
||
this.observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
}
|
||
}
|
||
remuxVideo(track, timeOffset, contiguous, audioTrackLength, iframeDuration) {
|
||
let offset = 8;
|
||
let mp4SampleDuration;
|
||
let mdat;
|
||
let firstPTS;
|
||
let firstDTS;
|
||
let lastPTS;
|
||
let lastDTS;
|
||
let dropped = track.parsingData.dropped;
|
||
const dropFrames = !contiguous && this.config.forceKeyFrameOnDiscontinuity;
|
||
const inputSamples = track.parsingData.esSamples;
|
||
const timeScale = track.info.inputTimescale;
|
||
const outputSamples = [];
|
||
const encrypted = track.info.encrypted;
|
||
// PTS is coded on 33bits, and can loop from -2^32 to 2^32
|
||
// normalizePts will make PTS/DTS value monotonic, we use last known DTS value as reference value
|
||
let nextAvcDts;
|
||
// contiguous fragments are consecutive fragments from same quality level (same level, new SN = old SN + 1)
|
||
if (contiguous) {
|
||
// if parsed fragment is contiguous with last one, let's use last DTS value as reference
|
||
nextAvcDts = this.nextAvcDts;
|
||
}
|
||
else {
|
||
// if not contiguous, let's use sample
|
||
nextAvcDts = normalizePts(inputSamples[0].dts, inputSamples[0].pts);
|
||
}
|
||
inputSamples.forEach(function (sample) {
|
||
sample.pts = normalizePts(sample.pts, nextAvcDts);
|
||
sample.dts = normalizePts(sample.dts, nextAvcDts);
|
||
});
|
||
// sort video samples by DTS then PTS then demux id order
|
||
inputSamples.sort(function (a, b) {
|
||
const deltadts = a.dts - b.dts;
|
||
const deltapts = a.pts - b.pts;
|
||
return deltadts ? deltadts : deltapts ? deltapts : a.id - b.id;
|
||
});
|
||
let firstKeyframePts;
|
||
const firstKeyframeIndex = inputSamples.findIndex((sample) => sample.key);
|
||
if (inputSamples[firstKeyframeIndex]) {
|
||
firstKeyframePts = inputSamples[firstKeyframeIndex].pts;
|
||
}
|
||
if (dropFrames) {
|
||
if (firstKeyframeIndex > 0) {
|
||
this.logger.warn(`Dropped ${firstKeyframeIndex} out of ${inputSamples.length} video samples due to a missing keyframe`);
|
||
inputSamples.splice(0, firstKeyframeIndex);
|
||
dropped += firstKeyframeIndex;
|
||
}
|
||
else if (firstKeyframeIndex === -1) {
|
||
this.logger.warn(`No keyframe found out of ${inputSamples.length} video samples`);
|
||
dropped += inputSamples.length;
|
||
}
|
||
}
|
||
const firstSample = inputSamples[0];
|
||
const lastSample = inputSamples[inputSamples.length - 1];
|
||
// handle broken streams with PTS < DTS, tolerance up 200ms (18000 in 90kHz timescale)
|
||
const PTSDTSshift = inputSamples.reduce((prev, curr) => Math.max(Math.min(prev, curr.pts - curr.dts), -18000), 0);
|
||
if (PTSDTSshift < 0) {
|
||
this.logger.warn(`PTS < DTS detected in video samples, shifting DTS by ${Math.round(PTSDTSshift / 90)} ms to overcome this issue`);
|
||
for (let i = 0; i < inputSamples.length; i++) {
|
||
inputSamples[i].dts += PTSDTSshift;
|
||
}
|
||
}
|
||
const isSafari = this.isSafari;
|
||
// on Safari let's signal the same sample duration for all samples
|
||
// sample duration (as expected by trun MP4 boxes), should be the delta between sample DTS
|
||
// set this constant duration as being the avg delta between consecutive DTS.
|
||
mp4SampleDuration = Math.round((lastSample.dts - firstSample.dts) / (inputSamples.length - 1));
|
||
// compute first DTS/PTS, normalize them against reference value
|
||
firstDTS = Math.max(firstSample.dts, 0);
|
||
firstPTS = Math.max(firstSample.pts, 0);
|
||
if (isFiniteNumber(iframeDuration)) {
|
||
firstDTS = timeOffset * timeScale;
|
||
firstPTS = timeOffset * timeScale;
|
||
}
|
||
// if fragment are contiguous, detect hole/overlapping between fragments
|
||
if (contiguous) {
|
||
// check timestamp continuity across consecutive fragments (this is to remove inter-fragment gap/hole)
|
||
const delta = firstDTS - nextAvcDts;
|
||
const foundHole = delta > mp4SampleDuration;
|
||
const foundOverlap = delta < -1;
|
||
if (foundHole || foundOverlap) {
|
||
if (foundHole) {
|
||
this.logger.warn(`AVC: ${delta}/90000 hole between fragments detected`);
|
||
}
|
||
else {
|
||
this.logger.warn(`AVC: ${delta}/90000 overlapping between fragments detected`);
|
||
}
|
||
}
|
||
}
|
||
let nbNalu = 0;
|
||
let naluLen = 0;
|
||
const nbSamples = inputSamples.length;
|
||
for (let i = 0; i < nbSamples; i++) {
|
||
// compute total/avc sample length and nb of NAL units
|
||
const sample = inputSamples[i], units = sample.units, nbUnits = units.length;
|
||
let sampleLen = 0;
|
||
for (let j = 0; j < nbUnits; j++) {
|
||
sampleLen += units[j].data.length;
|
||
}
|
||
naluLen += sampleLen;
|
||
nbNalu += nbUnits;
|
||
sample.length = sampleLen;
|
||
// normalize PTS/DTS
|
||
if (isSafari) {
|
||
// sample DTS is computed using a constant decoding offset (mp4SampleDuration) between samples
|
||
sample.dts = firstDTS + i * mp4SampleDuration;
|
||
}
|
||
else {
|
||
// ensure sample monotonic DTS
|
||
sample.dts = Math.max(sample.dts, firstDTS);
|
||
}
|
||
// ensure that computed value is greater or equal than sample DTS
|
||
sample.pts = Math.max(sample.pts, sample.dts);
|
||
}
|
||
// compute lastPTS/lastDTS
|
||
lastDTS = Math.max(lastSample.dts, 0);
|
||
lastPTS = Math.max(lastSample.pts, 0, lastDTS);
|
||
if (isFiniteNumber(iframeDuration)) {
|
||
lastDTS = timeOffset * timeScale;
|
||
lastPTS = timeOffset * timeScale;
|
||
}
|
||
/* concatenate the video data and construct the mdat in place
|
||
(need 8 more bytes to fill length and mpdat type) */
|
||
const mdatSize = naluLen + 4 * nbNalu + 8;
|
||
try {
|
||
mdat = new Uint8Array(mdatSize);
|
||
}
|
||
catch (err) {
|
||
const payload = new RemuxAllocError(false, `fail allocating video mdat ${mdatSize}`, ErrorResponses.FailedToAllocateVideoMdat, mdatSize);
|
||
this.observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return;
|
||
}
|
||
const view = new DataView(mdat.buffer);
|
||
view.setUint32(0, mdatSize);
|
||
mdat.set(MP4.types.mdat, 4);
|
||
for (let i = 0; i < nbSamples; i++) {
|
||
const avcSample = inputSamples[i], avcSampleUnits = avcSample.units;
|
||
let mp4SampleLength = 0, compositionTimeOffset;
|
||
// convert NALU bitstream to MP4 format (prepend NALU with size field)
|
||
const subsamples = [];
|
||
let bytesClear = 0;
|
||
for (let j = 0, nbUnits = avcSampleUnits.length; j < nbUnits; j++) {
|
||
const unit = avcSampleUnits[j], unitData = unit.data, unitDataLen = unit.data.byteLength;
|
||
view.setUint32(offset, unitDataLen);
|
||
offset += 4;
|
||
mdat.set(unitData, offset);
|
||
offset += unitDataLen;
|
||
mp4SampleLength += 4 + unitDataLen;
|
||
if (encrypted) {
|
||
if (unitDataLen <= 48 || (unit.type !== 1 && unit.type !== 5)) {
|
||
bytesClear += 4 + unitDataLen;
|
||
}
|
||
else {
|
||
let encryptedDataLen = unitDataLen - 32;
|
||
if (encryptedDataLen % 16 === 0) {
|
||
encryptedDataLen -= 16; // different than fMP4 encryption - TS always requires at least 1 clear byte at the end
|
||
}
|
||
subsamples.push([bytesClear + 36, encryptedDataLen]);
|
||
bytesClear = unitDataLen - 32 - encryptedDataLen;
|
||
}
|
||
}
|
||
}
|
||
if (bytesClear > 0) {
|
||
subsamples.push([bytesClear, 0]);
|
||
}
|
||
if (!isSafari) {
|
||
// expected sample duration is the Decoding Timestamp diff of consecutive samples
|
||
if (i < nbSamples - 1) {
|
||
mp4SampleDuration = inputSamples[i + 1].dts - avcSample.dts;
|
||
}
|
||
else {
|
||
const config = this.config, lastFrameDuration = avcSample.dts - inputSamples[i > 0 ? i - 1 : i].dts;
|
||
if (config.stretchShortVideoTrack) {
|
||
// In some cases, a segment's audio track duration may exceed the video track duration.
|
||
// Since we've already remuxed audio, and we know how long the audio track is, we look to
|
||
// see if the delta to the next segment is longer than the minimum of maxBufferHole and
|
||
// maxSeekHole. If so, playback would potentially get stuck, so we artificially inflate
|
||
// the duration of the last frame to minimize any potential gap between segments.
|
||
const maxBufferHole = config.maxBufferHole, maxSeekHole = config.maxSeekHole, gapTolerance = Math.floor(Math.min(maxBufferHole, maxSeekHole) * timeScale), deltaToFrameEnd = (audioTrackLength ? firstPTS + audioTrackLength * timeScale : this.nextAudioPts) - avcSample.pts;
|
||
if (deltaToFrameEnd > gapTolerance) {
|
||
// We subtract lastFrameDuration from deltaToFrameEnd to try to prevent any video
|
||
// frame overlap. maxBufferHole/maxSeekHole should be >> lastFrameDuration anyway.
|
||
mp4SampleDuration = deltaToFrameEnd - lastFrameDuration;
|
||
if (mp4SampleDuration < 0) {
|
||
mp4SampleDuration = lastFrameDuration;
|
||
}
|
||
this.logger.info(`It is approximately ${deltaToFrameEnd / 90} ms to the next segment; using duration ${mp4SampleDuration / 90} ms for the last video frame.`);
|
||
}
|
||
else {
|
||
mp4SampleDuration = lastFrameDuration;
|
||
}
|
||
}
|
||
else {
|
||
mp4SampleDuration = lastFrameDuration;
|
||
}
|
||
}
|
||
compositionTimeOffset = Math.round(avcSample.pts - avcSample.dts);
|
||
}
|
||
else {
|
||
compositionTimeOffset = Math.max(0, mp4SampleDuration * Math.round((avcSample.pts - avcSample.dts) / mp4SampleDuration));
|
||
}
|
||
if (isFiniteNumber(iframeDuration)) {
|
||
compositionTimeOffset = 0;
|
||
mp4SampleDuration = iframeDuration * timeScale;
|
||
}
|
||
outputSamples.push({
|
||
size: mp4SampleLength,
|
||
// constant duration
|
||
duration: mp4SampleDuration,
|
||
cts: compositionTimeOffset,
|
||
flags: {
|
||
isLeading: 0,
|
||
isDependedOn: 0,
|
||
hasRedundancy: 0,
|
||
degradPrio: 0,
|
||
dependsOn: avcSample.key ? 2 : 1,
|
||
isNonSync: avcSample.key ? 0 : 1,
|
||
paddingValue: 0,
|
||
},
|
||
keyTagInfo: avcSample.keyTagInfo,
|
||
subsamples: subsamples,
|
||
});
|
||
}
|
||
// next AVC sample DTS should be equal to last sample DTS + last sample duration (in PES timescale)
|
||
this.nextAvcDts = lastDTS + mp4SampleDuration;
|
||
this.isVideoContiguous = true;
|
||
if (outputSamples.length && navigator.userAgent.toLowerCase().indexOf('chrome') > -1) {
|
||
const flags = outputSamples[0].flags;
|
||
// chrome workaround, mark first sample as being a Random Access Point to avoid sourcebuffer append issue
|
||
// https://code.google.com/p/chromium/issues/detail?id=229412
|
||
flags.dependsOn = 2;
|
||
flags.isNonSync = 0;
|
||
}
|
||
const moofSegment = {
|
||
sequenceNumber: track.parsingData.sequenceNumber++,
|
||
id: track.info.id,
|
||
type: track.type,
|
||
encrypted: track.info.encrypted,
|
||
samples: outputSamples,
|
||
defaultPerSampleIVSize: 0,
|
||
};
|
||
const moof = MP4.moof(firstDTS + this.config.audioPrimingDelay * timeScale, moofSegment);
|
||
track.parsingData.esSamples = [];
|
||
const fullSegment = new Uint8Array(moof.byteLength + mdat.byteLength);
|
||
fullSegment.set(moof);
|
||
fullSegment.set(mdat, moof.byteLength);
|
||
const type = 'video';
|
||
const data = {
|
||
data1: fullSegment,
|
||
startPTS: convertSecondsToTimestamp(firstPTS / timeScale, timeScale),
|
||
endPTS: convertSecondsToTimestamp((lastPTS + mp4SampleDuration) / timeScale, timeScale),
|
||
startDTS: convertSecondsToTimestamp(firstDTS / timeScale, timeScale),
|
||
endDTS: convertSecondsToTimestamp(this.nextAvcDts / timeScale, timeScale),
|
||
type,
|
||
dropped,
|
||
framesWithoutIDR: firstKeyframeIndex,
|
||
firstKeyframePts: convertSecondsToTimestamp(firstKeyframePts / timeScale, timeScale),
|
||
};
|
||
return data;
|
||
}
|
||
remuxAudio(track, timeOffset, contiguous, accurateTimeOffset, keyTagInfo) {
|
||
const inputTimeScale = track.info.inputTimescale, mp4timeScale = track.info.timescale, scaleFactor = inputTimeScale / mp4timeScale, mp4SampleDuration = track.config.segmentCodec === 'aac' ? 1024 : track.config.segmentCodec === 'mp3' ? 1152 : 1536, inputSampleDuration = mp4SampleDuration * scaleFactor, rawMPEG = track.config.segmentCodec === 'mp3' && this.typeSupported.mpeg, outputSamples = [], encrypted = track.info.encrypted, timeOffsetMpegTS = this._initPTS + timeOffset * inputTimeScale;
|
||
let view, offset = rawMPEG ? 0 : 8, audioSample, mp4Sample, unit, mdat, moof, firstPTS, firstDTS, lastDTS, pts, dts, fillFrame, newStamp, nextAudioPts;
|
||
const bu = new BitstreamUtils();
|
||
const inputSamples = track.parsingData.esSamples;
|
||
// for audio samples, also consider consecutive fragments as being contiguous (even if a level switch occurs),
|
||
// for sake of clarity:
|
||
// consecutive fragments are frags with
|
||
// - less than 100ms gaps between new time offset (if accurate) and next expected PTS OR
|
||
// - less than 20 audio frames distance
|
||
// contiguous fragments are consecutive fragments from same quality level (same level, new SN = old SN + 1)
|
||
// this helps ensuring audio continuity
|
||
// and this also avoids audio glitches/cut when switching quality, or reporting wrong duration on first audio frame
|
||
nextAudioPts = this.nextAudioPts;
|
||
contiguous =
|
||
contiguous ||
|
||
(inputSamples.length &&
|
||
nextAudioPts &&
|
||
((accurateTimeOffset && Math.abs(timeOffsetMpegTS - nextAudioPts) < 9000) || Math.abs(normalizePts(inputSamples[0].pts - nextAudioPts, timeOffsetMpegTS)) < 20 * inputSampleDuration));
|
||
if (!contiguous) {
|
||
// if fragments are not contiguous, let's use sample
|
||
nextAudioPts = normalizePts(inputSamples[0].pts, this._initPTS);
|
||
}
|
||
// compute normalized PTS
|
||
inputSamples.forEach(function (sample) {
|
||
sample.pts = sample.dts = normalizePts(sample.pts, nextAudioPts);
|
||
});
|
||
// If the audio track is missing samples, the frames seem to get "left-shifted" within the
|
||
// resulting mp4 segment, causing sync issues and leaving gaps at the end of the audio segment.
|
||
// In an effort to prevent this from happening, we inject frames here where there are gaps.
|
||
// When possible, we inject a silent frame; when that's not possible, we duplicate the last
|
||
// frame.
|
||
// only inject/drop audio frames in case time offset is accurate
|
||
if (accurateTimeOffset && track.config.segmentCodec === 'aac') {
|
||
for (let i = 0, nextPts = nextAudioPts; i < inputSamples.length;) {
|
||
// First, let's see how far off this frame is from where we expect it to be
|
||
const sample = inputSamples[i];
|
||
pts = sample.pts;
|
||
const delta = pts - nextPts;
|
||
const duration = Math.abs((1000 * delta) / inputTimeScale);
|
||
// If we're overlapping by more than a duration, drop this sample
|
||
if (delta <= -inputSampleDuration) {
|
||
this.logger.warn(`Dropping 1 audio frame @ ${(nextPts / inputTimeScale).toFixed(3)}s due to ${duration} ms overlap.`);
|
||
inputSamples.splice(i, 1);
|
||
track.parsingData.len -= sample.unit.length;
|
||
// Don't touch nextPtsNorm or i
|
||
// Insert missing frames if:
|
||
// 1: We're more than one frame away
|
||
// 2: Not more than MAX_SILENT_FRAME_DURATION away
|
||
// 3: currentTime (aka nextPtsNorm) is not 0
|
||
}
|
||
else if (delta >= inputSampleDuration && duration < MAX_SILENT_FRAME_DURATION && nextPts) {
|
||
const missing = Math.round(delta / inputSampleDuration);
|
||
this.logger.warn(`Injecting ${missing} audio frame @ ${(nextPts / inputTimeScale).toFixed(3)}s due to ${Math.round((1000 * delta) / inputTimeScale)} ms gap.`);
|
||
for (let j = 0; j < missing; j++) {
|
||
newStamp = Math.max(nextPts, 0);
|
||
fillFrame = getSilentFrame(track.config.codec, track.config.channelCount);
|
||
if (!fillFrame) {
|
||
this.logger.warn('Unable to get silent frame for given audio codec; duplicating last frame instead.');
|
||
fillFrame = sample.unit.subarray(0);
|
||
}
|
||
inputSamples.splice(i, 0, { unit: fillFrame, pts: newStamp, dts: newStamp, keyTagInfo: keyTagInfo });
|
||
track.parsingData.len += fillFrame.length;
|
||
nextPts += inputSampleDuration;
|
||
i += 1;
|
||
}
|
||
// Adjust sample to next expected pts
|
||
sample.pts = sample.dts = nextPts;
|
||
nextPts += inputSampleDuration;
|
||
i += 1;
|
||
// Otherwise, just adjust pts
|
||
}
|
||
else {
|
||
nextPts += inputSampleDuration;
|
||
if (i === 0) {
|
||
sample.pts = sample.dts = nextAudioPts;
|
||
}
|
||
else {
|
||
sample.pts = sample.dts = inputSamples[i - 1].pts + inputSampleDuration;
|
||
}
|
||
i += 1;
|
||
}
|
||
}
|
||
}
|
||
for (let j = 0, nbSamples = inputSamples.length; j < nbSamples; j++) {
|
||
audioSample = inputSamples[j];
|
||
unit = audioSample.unit;
|
||
pts = audioSample.pts;
|
||
dts = audioSample.dts;
|
||
// if not first sample
|
||
if (lastDTS !== undefined) {
|
||
mp4Sample.duration = Math.round((dts - lastDTS) / scaleFactor);
|
||
}
|
||
else {
|
||
const delta = Math.round((1000 * (pts - nextAudioPts)) / inputTimeScale);
|
||
let numMissingFrames = 0;
|
||
// if fragment are contiguous, detect hole/overlapping between fragments
|
||
// contiguous fragments are consecutive fragments from same quality level (same level, new SN = old SN + 1)
|
||
if (contiguous && track.config.segmentCodec === 'aac') {
|
||
// log delta
|
||
if (delta) {
|
||
if (delta > 0 && delta < MAX_SILENT_FRAME_DURATION) {
|
||
numMissingFrames = Math.round((pts - nextAudioPts) / inputSampleDuration);
|
||
this.logger.info(`${delta} ms hole between AAC samples detected,filling it`);
|
||
if (numMissingFrames > 0) {
|
||
fillFrame = getSilentFrame(track.config.codec, track.config.channelCount);
|
||
if (!fillFrame) {
|
||
fillFrame = unit.subarray(0);
|
||
}
|
||
track.parsingData.len += numMissingFrames * fillFrame.length;
|
||
}
|
||
// if we have frame overlap, overlapping for more than half a frame duraion
|
||
}
|
||
else if (delta < -12) {
|
||
// drop overlapping audio frames... browser will deal with it
|
||
this.logger.info(`drop overlapping AAC sample, expected/parsed/delta:${(nextAudioPts / inputTimeScale).toFixed(3)}s/${(pts / inputTimeScale).toFixed(3)}s/${-delta}ms`);
|
||
track.parsingData.len -= unit.byteLength;
|
||
continue;
|
||
}
|
||
// set PTS/DTS to expected PTS/DTS
|
||
pts = dts = nextAudioPts;
|
||
}
|
||
}
|
||
// remember first PTS of our audioSamples, ensure value is positive
|
||
firstPTS = Math.max(0, pts);
|
||
firstDTS = Math.max(0, dts);
|
||
if (track.parsingData.len > 0) {
|
||
/* concatenate the audio data and construct the mdat in place
|
||
(need 8 more bytes to fill length and mdat type) */
|
||
const mdatSize = rawMPEG ? track.parsingData.len : track.parsingData.len + 8;
|
||
try {
|
||
mdat = new Uint8Array(mdatSize);
|
||
}
|
||
catch (err) {
|
||
const payload = new RemuxAllocError(false, `fail allocating audio mdat ${mdatSize}`, ErrorResponses.FailedToAllocateAudioMdat, mdatSize);
|
||
this.observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return;
|
||
}
|
||
if (!rawMPEG) {
|
||
view = new DataView(mdat.buffer);
|
||
view.setUint32(0, mdatSize);
|
||
mdat.set(MP4.types.mdat, 4);
|
||
}
|
||
}
|
||
else {
|
||
// no audio samples
|
||
return;
|
||
}
|
||
for (let i = 0; i < numMissingFrames; i++) {
|
||
newStamp = pts - (numMissingFrames - i) * inputSampleDuration;
|
||
fillFrame = getSilentFrame(track.config.codec, track.config.channelCount);
|
||
if (!fillFrame) {
|
||
this.logger.warn('Unable to get silent frame for given audio codec; duplicating this frame instead.');
|
||
fillFrame = unit.subarray(0);
|
||
}
|
||
mdat.set(fillFrame, offset);
|
||
offset += fillFrame.byteLength;
|
||
mp4Sample = {
|
||
size: fillFrame.byteLength,
|
||
cts: 0,
|
||
duration: 1024,
|
||
flags: {
|
||
isLeading: 0,
|
||
isDependedOn: 0,
|
||
hasRedundancy: 0,
|
||
degradPrio: 0,
|
||
dependsOn: 1,
|
||
paddingValue: 0,
|
||
isNonSync: 0,
|
||
},
|
||
keyTagInfo: audioSample.keyTagInfo,
|
||
subsamples: encrypted ? [[fillFrame.byteLength, 0]] : [],
|
||
};
|
||
outputSamples.push(mp4Sample);
|
||
}
|
||
}
|
||
mdat.set(unit, offset);
|
||
const unitLen = unit.byteLength;
|
||
offset += unitLen;
|
||
const subsamples = [];
|
||
if (encrypted) {
|
||
if (track.config.segmentCodec === 'ec3') {
|
||
let bytesSeen = 0;
|
||
// for ec3, a protected block is a single sync frame; the sample may have one or more sync frames
|
||
while (bytesSeen < unit.byteLength) {
|
||
const frameLength = (bu.bsReadAndUpdate(unit, { byteOffset: bytesSeen + 2, usedBits: 5 }, 11) + 1) * 2;
|
||
bytesSeen += frameLength;
|
||
const clearBytes = Math.min(frameLength, 16); // 16 bytes in the clear
|
||
subsamples.push([clearBytes, frameLength - clearBytes]);
|
||
}
|
||
}
|
||
else {
|
||
const clearBytes = Math.min(unitLen, 16); // both aac and ac3 have 16 bytes in the clear
|
||
subsamples.push([clearBytes, unitLen - clearBytes]);
|
||
}
|
||
}
|
||
mp4Sample = {
|
||
size: unitLen,
|
||
cts: 0,
|
||
duration: 0,
|
||
flags: {
|
||
isLeading: 0,
|
||
isDependedOn: 0,
|
||
hasRedundancy: 0,
|
||
degradPrio: 0,
|
||
dependsOn: 1,
|
||
paddingValue: 0,
|
||
isNonSync: 0,
|
||
},
|
||
keyTagInfo: audioSample.keyTagInfo,
|
||
subsamples: subsamples,
|
||
};
|
||
outputSamples.push(mp4Sample);
|
||
lastDTS = dts;
|
||
}
|
||
let lastSampleDuration = 0;
|
||
const nbSamples = outputSamples.length;
|
||
// set last sample duration as being identical to previous sample
|
||
if (nbSamples >= 2) {
|
||
lastSampleDuration = outputSamples[nbSamples - 2].duration;
|
||
mp4Sample.duration = lastSampleDuration;
|
||
}
|
||
if (nbSamples) {
|
||
// next audio sample PTS should be equal to last sample PTS + duration
|
||
this.nextAudioPts = pts + scaleFactor * lastSampleDuration;
|
||
track.parsingData.len = 0;
|
||
if (rawMPEG) {
|
||
moof = new Uint8Array();
|
||
}
|
||
else {
|
||
const moofSegment = {
|
||
sequenceNumber: track.parsingData.sequenceNumber++,
|
||
id: track.info.id,
|
||
type: track.type,
|
||
encrypted: track.info.encrypted,
|
||
samples: outputSamples,
|
||
defaultPerSampleIVSize: 0,
|
||
};
|
||
moof = MP4.moof((firstDTS + this.config.audioPrimingDelay * inputTimeScale) / scaleFactor, moofSegment);
|
||
}
|
||
const fullSegment = new Uint8Array(moof.byteLength + mdat.byteLength);
|
||
fullSegment.set(moof);
|
||
fullSegment.set(mdat, moof.byteLength);
|
||
track.parsingData.esSamples = [];
|
||
const audioData = {
|
||
data1: fullSegment,
|
||
startPTS: convertSecondsToTimestamp(firstPTS / inputTimeScale, inputTimeScale),
|
||
endPTS: convertSecondsToTimestamp(this.nextAudioPts / inputTimeScale, inputTimeScale),
|
||
startDTS: convertSecondsToTimestamp(firstDTS / inputTimeScale, inputTimeScale),
|
||
endDTS: convertSecondsToTimestamp((dts + scaleFactor * lastSampleDuration) / inputTimeScale, inputTimeScale),
|
||
type: 'audio',
|
||
};
|
||
return audioData;
|
||
}
|
||
return null;
|
||
}
|
||
remuxEmptyAudio(track, timeOffset, contiguous, accurateTimeOffset, videoData, keyTagInfo) {
|
||
const inputTimeScale = track.info.inputTimescale, mp4timeScale = track.config.samplerate ? track.config.samplerate : inputTimeScale, scaleFactor = inputTimeScale / mp4timeScale, nextAudioPts = this.nextAudioPts,
|
||
// sync with video's timestamp
|
||
startDTS = (nextAudioPts !== undefined ? nextAudioPts : convertTimestampToSeconds(videoData.startDTS) * inputTimeScale) + this._initDTS, endDTS = convertTimestampToSeconds(videoData.endDTS) * inputTimeScale + this._initDTS,
|
||
// one sample's duration value
|
||
sampleDuration = 1024, frameDuration = scaleFactor * sampleDuration,
|
||
// samples count of this segment's duration
|
||
nbSamples = Math.ceil((endDTS - startDTS) / frameDuration),
|
||
// silent frame
|
||
silentFrame = getSilentFrame(track.config.codec, track.config.channelCount);
|
||
this.logger.warn('remux empty Audio');
|
||
// Can't remux if we can't generate a silent frame...
|
||
if (!silentFrame) {
|
||
this.logger.error('Unable to remuxEmptyAudio since we were unable to get a silent frame for given audio codec!');
|
||
return null;
|
||
}
|
||
const samples = [];
|
||
for (let i = 0; i < nbSamples; i++) {
|
||
const stamp = startDTS + i * frameDuration;
|
||
samples.push({ unit: silentFrame, pts: stamp, dts: stamp, keyTagInfo: keyTagInfo });
|
||
track.parsingData.len += silentFrame.length;
|
||
}
|
||
track.parsingData.esSamples = samples;
|
||
return this.remuxAudio(track, timeOffset, contiguous, accurateTimeOffset, keyTagInfo);
|
||
}
|
||
remuxID3(track, parsingData) {
|
||
const length = track.id3Samples.length;
|
||
let sample;
|
||
const inputTimeScale = track.inputTimescale;
|
||
// consume samples
|
||
if (length) {
|
||
for (let index = 0; index < length; index++) {
|
||
sample = track.id3Samples[index];
|
||
// setting id3 pts, dts to relative time
|
||
sample.pts = sample.pts / inputTimeScale;
|
||
sample.dts = sample.dts / inputTimeScale;
|
||
}
|
||
parsingData.id3Samples = track.id3Samples;
|
||
}
|
||
track.id3Samples = [];
|
||
}
|
||
remuxText(track, parsingData) {
|
||
track.captionSamples.sort(function (a, b) {
|
||
return a.pts - b.pts;
|
||
});
|
||
const length = track.captionSamples.length;
|
||
let sample;
|
||
const inputTimeScale = track.inputTimescale;
|
||
// consume samples
|
||
if (length) {
|
||
for (let index = 0; index < length; index++) {
|
||
sample = track.captionSamples[index];
|
||
// setting text pts, dts to relative time
|
||
sample.pts = sample.pts / inputTimeScale;
|
||
}
|
||
if (!parsingData.captionData) {
|
||
parsingData.captionData = {};
|
||
}
|
||
parsingData.captionData.ts = track.captionSamples;
|
||
}
|
||
track.captionSamples = [];
|
||
}
|
||
}
|
||
function normalizePts(value, reference) {
|
||
let offset;
|
||
if (reference === undefined) {
|
||
return value;
|
||
}
|
||
if (reference < value) {
|
||
// - 2^33
|
||
offset = -8589934592;
|
||
}
|
||
else {
|
||
// + 2^33
|
||
offset = 8589934592;
|
||
}
|
||
/* PTS is 33bit (from 0 to 2^33 -1)
|
||
if diff between value and reference is bigger than half of the amplitude (2^32) then it means that
|
||
PTS looping occured. fill the gap */
|
||
while (Math.abs(value - reference) > 4294967296) {
|
||
value += offset;
|
||
}
|
||
return value;
|
||
}
|
||
function getVideoStartPts(videoSamples) {
|
||
const startPTS = videoSamples.reduce((minPTS, sample) => {
|
||
const delta = sample.pts - minPTS;
|
||
if (delta < -4294967296) {
|
||
// 2^32, see normalizePts for reasoning, but we're hitting a rollover here, and we don't want that to impact the timeOffset calculation
|
||
return normalizePts(minPTS, sample.pts);
|
||
}
|
||
else if (delta > 0) {
|
||
return minPTS;
|
||
}
|
||
else {
|
||
return sample.pts;
|
||
}
|
||
}, videoSamples[0].pts);
|
||
return startPTS;
|
||
}
|
||
|
||
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
||
|
||
function tryStringify (o) {
|
||
try { return JSON.stringify(o) } catch(e) { return '"[Circular]"' }
|
||
}
|
||
|
||
var quickFormatUnescaped = format$1;
|
||
|
||
function format$1(f, args, opts) {
|
||
var ss = (opts && opts.stringify) || tryStringify;
|
||
var offset = 1;
|
||
if (typeof f === 'object' && f !== null) {
|
||
var len = args.length + offset;
|
||
if (len === 1) return f
|
||
var objects = new Array(len);
|
||
objects[0] = ss(f);
|
||
for (var index = 1; index < len; index++) {
|
||
objects[index] = ss(args[index]);
|
||
}
|
||
return objects.join(' ')
|
||
}
|
||
if (typeof f !== 'string') {
|
||
return f
|
||
}
|
||
var argLen = args.length;
|
||
if (argLen === 0) return f
|
||
var str = '';
|
||
var a = 1 - offset;
|
||
var lastPos = -1;
|
||
var flen = (f && f.length) || 0;
|
||
for (var i = 0; i < flen;) {
|
||
if (f.charCodeAt(i) === 37 && i + 1 < flen) {
|
||
lastPos = lastPos > -1 ? lastPos : 0;
|
||
switch (f.charCodeAt(i + 1)) {
|
||
case 100: // 'd'
|
||
if (a >= argLen)
|
||
break
|
||
if (lastPos < i)
|
||
str += f.slice(lastPos, i);
|
||
if (args[a] == null) break
|
||
str += Number(args[a]);
|
||
lastPos = i = i + 2;
|
||
break
|
||
case 79: // 'O'
|
||
case 111: // 'o'
|
||
case 106: // 'j'
|
||
if (a >= argLen)
|
||
break
|
||
if (lastPos < i)
|
||
str += f.slice(lastPos, i);
|
||
if (args[a] === undefined) break
|
||
var type = typeof args[a];
|
||
if (type === 'string') {
|
||
str += '\'' + args[a] + '\'';
|
||
lastPos = i + 2;
|
||
i++;
|
||
break
|
||
}
|
||
if (type === 'function') {
|
||
str += args[a].name || '<anonymous>';
|
||
lastPos = i + 2;
|
||
i++;
|
||
break
|
||
}
|
||
str += ss(args[a]);
|
||
lastPos = i + 2;
|
||
i++;
|
||
break
|
||
case 115: // 's'
|
||
if (a >= argLen)
|
||
break
|
||
if (lastPos < i)
|
||
str += f.slice(lastPos, i);
|
||
str += String(args[a]);
|
||
lastPos = i + 2;
|
||
i++;
|
||
break
|
||
case 37: // '%'
|
||
if (lastPos < i)
|
||
str += f.slice(lastPos, i);
|
||
str += '%';
|
||
lastPos = i + 2;
|
||
i++;
|
||
break
|
||
}
|
||
++a;
|
||
}
|
||
++i;
|
||
}
|
||
if (lastPos === -1)
|
||
return f
|
||
else if (lastPos < flen) {
|
||
str += f.slice(lastPos);
|
||
}
|
||
|
||
return str
|
||
}
|
||
|
||
const format = quickFormatUnescaped;
|
||
|
||
var browser = pino;
|
||
|
||
const _console = pfGlobalThisOrFallback().console || {};
|
||
const stdSerializers = {
|
||
mapHttpRequest: mock,
|
||
mapHttpResponse: mock,
|
||
wrapRequestSerializer: passthrough,
|
||
wrapResponseSerializer: passthrough,
|
||
wrapErrorSerializer: passthrough,
|
||
req: mock,
|
||
res: mock,
|
||
err: asErrValue
|
||
};
|
||
|
||
function shouldSerialize (serialize, serializers) {
|
||
if (Array.isArray(serialize)) {
|
||
const hasToFilter = serialize.filter(function (k) {
|
||
return k !== '!stdSerializers.err'
|
||
});
|
||
return hasToFilter
|
||
} else if (serialize === true) {
|
||
return Object.keys(serializers)
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
function pino (opts) {
|
||
opts = opts || {};
|
||
opts.browser = opts.browser || {};
|
||
|
||
const transmit = opts.browser.transmit;
|
||
if (transmit && typeof transmit.send !== 'function') { throw Error('pino: transmit option must have a send function') }
|
||
|
||
const proto = opts.browser.write || _console;
|
||
if (opts.browser.write) opts.browser.asObject = true;
|
||
const serializers = opts.serializers || {};
|
||
const serialize = shouldSerialize(opts.browser.serialize, serializers);
|
||
let stdErrSerialize = opts.browser.serialize;
|
||
|
||
if (
|
||
Array.isArray(opts.browser.serialize) &&
|
||
opts.browser.serialize.indexOf('!stdSerializers.err') > -1
|
||
) stdErrSerialize = false;
|
||
|
||
const levels = ['error', 'fatal', 'warn', 'info', 'debug', 'trace'];
|
||
|
||
if (typeof proto === 'function') {
|
||
proto.error = proto.fatal = proto.warn =
|
||
proto.info = proto.debug = proto.trace = proto;
|
||
}
|
||
if (opts.enabled === false) opts.level = 'silent';
|
||
const level = opts.level || 'info';
|
||
const logger = Object.create(proto);
|
||
if (!logger.log) logger.log = noop$2;
|
||
|
||
Object.defineProperty(logger, 'levelVal', {
|
||
get: getLevelVal
|
||
});
|
||
Object.defineProperty(logger, 'level', {
|
||
get: getLevel,
|
||
set: setLevel
|
||
});
|
||
|
||
const setOpts = {
|
||
transmit,
|
||
serialize,
|
||
asObject: opts.browser.asObject,
|
||
levels,
|
||
timestamp: getTimeFunction(opts)
|
||
};
|
||
logger.levels = pino.levels;
|
||
logger.level = level;
|
||
|
||
logger.setMaxListeners = logger.getMaxListeners =
|
||
logger.emit = logger.addListener = logger.on =
|
||
logger.prependListener = logger.once =
|
||
logger.prependOnceListener = logger.removeListener =
|
||
logger.removeAllListeners = logger.listeners =
|
||
logger.listenerCount = logger.eventNames =
|
||
logger.write = logger.flush = noop$2;
|
||
logger.serializers = serializers;
|
||
logger._serialize = serialize;
|
||
logger._stdErrSerialize = stdErrSerialize;
|
||
logger.child = child;
|
||
|
||
if (transmit) logger._logEvent = createLogEventShape();
|
||
|
||
function getLevelVal () {
|
||
return this.level === 'silent'
|
||
? Infinity
|
||
: this.levels.values[this.level]
|
||
}
|
||
|
||
function getLevel () {
|
||
return this._level
|
||
}
|
||
function setLevel (level) {
|
||
if (level !== 'silent' && !this.levels.values[level]) {
|
||
throw Error('unknown level ' + level)
|
||
}
|
||
this._level = level;
|
||
|
||
set$1(setOpts, logger, 'error', 'log'); // <-- must stay first
|
||
set$1(setOpts, logger, 'fatal', 'error');
|
||
set$1(setOpts, logger, 'warn', 'error');
|
||
set$1(setOpts, logger, 'info', 'log');
|
||
set$1(setOpts, logger, 'debug', 'log');
|
||
set$1(setOpts, logger, 'trace', 'log');
|
||
}
|
||
|
||
function child (bindings) {
|
||
if (!bindings) {
|
||
throw new Error('missing bindings for child Pino')
|
||
}
|
||
const bindingsSerializers = bindings.serializers;
|
||
if (serialize && bindingsSerializers) {
|
||
var childSerializers = Object.assign({}, serializers, bindingsSerializers);
|
||
var childSerialize = opts.browser.serialize === true
|
||
? Object.keys(childSerializers)
|
||
: serialize;
|
||
delete bindings.serializers;
|
||
applySerializers([bindings], childSerialize, childSerializers, this._stdErrSerialize);
|
||
}
|
||
function Child (parent) {
|
||
this._childLevel = (parent._childLevel | 0) + 1;
|
||
this.error = bind(parent, bindings, 'error');
|
||
this.fatal = bind(parent, bindings, 'fatal');
|
||
this.warn = bind(parent, bindings, 'warn');
|
||
this.info = bind(parent, bindings, 'info');
|
||
this.debug = bind(parent, bindings, 'debug');
|
||
this.trace = bind(parent, bindings, 'trace');
|
||
if (childSerializers) {
|
||
this.serializers = childSerializers;
|
||
this._serialize = childSerialize;
|
||
}
|
||
if (transmit) {
|
||
this._logEvent = createLogEventShape(
|
||
[].concat(parent._logEvent.bindings, bindings)
|
||
);
|
||
}
|
||
}
|
||
Child.prototype = this;
|
||
return new Child(this)
|
||
}
|
||
return logger
|
||
}
|
||
|
||
pino.levels = {
|
||
values: {
|
||
fatal: 60,
|
||
error: 50,
|
||
warn: 40,
|
||
info: 30,
|
||
debug: 20,
|
||
trace: 10
|
||
},
|
||
labels: {
|
||
10: 'trace',
|
||
20: 'debug',
|
||
30: 'info',
|
||
40: 'warn',
|
||
50: 'error',
|
||
60: 'fatal'
|
||
}
|
||
};
|
||
|
||
pino.stdSerializers = stdSerializers;
|
||
pino.stdTimeFunctions = Object.assign({}, { nullTime, epochTime, unixTime, isoTime });
|
||
|
||
function set$1 (opts, logger, level, fallback) {
|
||
const proto = Object.getPrototypeOf(logger);
|
||
logger[level] = logger.levelVal > logger.levels.values[level]
|
||
? noop$2
|
||
: (proto[level] ? proto[level] : (_console[level] || _console[fallback] || noop$2));
|
||
|
||
wrap(opts, logger, level);
|
||
}
|
||
|
||
function wrap (opts, logger, level) {
|
||
if (!opts.transmit && logger[level] === noop$2) return
|
||
|
||
logger[level] = (function (write) {
|
||
return function LOG () {
|
||
const ts = opts.timestamp();
|
||
const args = new Array(arguments.length);
|
||
const proto = (Object.getPrototypeOf && Object.getPrototypeOf(this) === _console) ? _console : this;
|
||
for (var i = 0; i < args.length; i++) args[i] = arguments[i];
|
||
|
||
if (opts.serialize && !opts.asObject) {
|
||
applySerializers(args, this._serialize, this.serializers, this._stdErrSerialize);
|
||
}
|
||
if (opts.asObject) write.call(proto, asObject(this, level, args, ts));
|
||
else write.apply(proto, args);
|
||
|
||
if (opts.transmit) {
|
||
const transmitLevel = opts.transmit.level || logger.level;
|
||
const transmitValue = pino.levels.values[transmitLevel];
|
||
const methodValue = pino.levels.values[level];
|
||
if (methodValue < transmitValue) return
|
||
transmit(this, {
|
||
ts,
|
||
methodLevel: level,
|
||
methodValue,
|
||
transmitLevel,
|
||
transmitValue: pino.levels.values[opts.transmit.level || logger.level],
|
||
send: opts.transmit.send,
|
||
val: logger.levelVal
|
||
}, args);
|
||
}
|
||
}
|
||
})(logger[level]);
|
||
}
|
||
|
||
function asObject (logger, level, args, ts) {
|
||
if (logger._serialize) applySerializers(args, logger._serialize, logger.serializers, logger._stdErrSerialize);
|
||
const argsCloned = args.slice();
|
||
let msg = argsCloned[0];
|
||
const o = {};
|
||
if (ts) {
|
||
o.time = ts;
|
||
}
|
||
o.level = pino.levels.values[level];
|
||
let lvl = (logger._childLevel | 0) + 1;
|
||
if (lvl < 1) lvl = 1;
|
||
// deliberate, catching objects, arrays
|
||
if (msg !== null && typeof msg === 'object') {
|
||
while (lvl-- && typeof argsCloned[0] === 'object') {
|
||
Object.assign(o, argsCloned.shift());
|
||
}
|
||
msg = argsCloned.length ? format(argsCloned.shift(), argsCloned) : undefined;
|
||
} else if (typeof msg === 'string') msg = format(argsCloned.shift(), argsCloned);
|
||
if (msg !== undefined) o.msg = msg;
|
||
return o
|
||
}
|
||
|
||
function applySerializers (args, serialize, serializers, stdErrSerialize) {
|
||
for (const i in args) {
|
||
if (stdErrSerialize && args[i] instanceof Error) {
|
||
args[i] = pino.stdSerializers.err(args[i]);
|
||
} else if (typeof args[i] === 'object' && !Array.isArray(args[i])) {
|
||
for (const k in args[i]) {
|
||
if (serialize && serialize.indexOf(k) > -1 && k in serializers) {
|
||
args[i][k] = serializers[k](args[i][k]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function bind (parent, bindings, level) {
|
||
return function () {
|
||
try{
|
||
const args = new Array(1 + arguments.length);
|
||
args[0] = bindings;
|
||
for (var i = 1; i < args.length; i++) {
|
||
args[i] = arguments[i - 1];
|
||
}
|
||
return parent[level].apply(this, args)}
|
||
catch(e){
|
||
|
||
}
|
||
}
|
||
}
|
||
|
||
function transmit (logger, opts, args) {
|
||
const send = opts.send;
|
||
const ts = opts.ts;
|
||
const methodLevel = opts.methodLevel;
|
||
const methodValue = opts.methodValue;
|
||
const val = opts.val;
|
||
const bindings = logger._logEvent.bindings;
|
||
|
||
applySerializers(
|
||
args,
|
||
logger._serialize || Object.keys(logger.serializers),
|
||
logger.serializers,
|
||
logger._stdErrSerialize === undefined ? true : logger._stdErrSerialize
|
||
);
|
||
logger._logEvent.ts = ts;
|
||
logger._logEvent.messages = args.filter(function (arg) {
|
||
// bindings can only be objects, so reference equality check via indexOf is fine
|
||
return bindings.indexOf(arg) === -1
|
||
});
|
||
|
||
logger._logEvent.level.label = methodLevel;
|
||
logger._logEvent.level.value = methodValue;
|
||
|
||
send(methodLevel, logger._logEvent, val);
|
||
|
||
logger._logEvent = createLogEventShape(bindings);
|
||
}
|
||
|
||
function createLogEventShape (bindings) {
|
||
return {
|
||
ts: 0,
|
||
messages: [],
|
||
bindings: bindings || [],
|
||
level: { label: '', value: 0 }
|
||
}
|
||
}
|
||
|
||
function asErrValue (err) {
|
||
const obj = {
|
||
type: err.constructor.name,
|
||
msg: err.message,
|
||
stack: err.stack
|
||
};
|
||
for (const key in err) {
|
||
if (obj[key] === undefined) {
|
||
obj[key] = err[key];
|
||
}
|
||
}
|
||
return obj
|
||
}
|
||
|
||
function getTimeFunction (opts) {
|
||
if (typeof opts.timestamp === 'function') {
|
||
return opts.timestamp
|
||
}
|
||
if (opts.timestamp === false) {
|
||
return nullTime
|
||
}
|
||
return epochTime
|
||
}
|
||
|
||
function mock () { return {} }
|
||
function passthrough (a) { return a }
|
||
function noop$2 () {}
|
||
|
||
function nullTime () { return false }
|
||
function epochTime () { return Date.now() }
|
||
function unixTime () { return Math.round(Date.now() / 1000.0) }
|
||
function isoTime () { return new Date(Date.now()).toISOString() } // using Date.now() for testability
|
||
|
||
/* eslint-disable */
|
||
/* istanbul ignore next */
|
||
function pfGlobalThisOrFallback () {
|
||
function defd (o) { return typeof o !== 'undefined' && o }
|
||
try {
|
||
if (typeof globalThis !== 'undefined') return globalThis
|
||
Object.defineProperty(Object.prototype, 'globalThis', {
|
||
get: function () {
|
||
delete Object.prototype.globalThis;
|
||
return (this.globalThis = this)
|
||
},
|
||
configurable: true
|
||
});
|
||
return globalThis
|
||
} catch (e) {
|
||
return defd(self) || defd(window) || defd(this) || {}
|
||
}
|
||
}
|
||
|
||
let LoggerSingleton;
|
||
function addQELevel(options = {}) {
|
||
return Object.assign(Object.assign({}, options), { customLevels: Object.assign(Object.assign({}, (options.customLevels || {})), { qe: 35 }) });
|
||
}
|
||
const setupLoggerSingleton = (sessionId, name, options = {}, stream) => {
|
||
if (!LoggerSingleton || LoggerSingleton.sessionId !== sessionId) {
|
||
LoggerSingleton = browser(addQELevel(options)).child({ sessionId, name: name });
|
||
LoggerSingleton.qe = (msg) => LoggerSingleton.info(msg);
|
||
LoggerSingleton.sessionId = sessionId;
|
||
}
|
||
else {
|
||
LoggerSingleton.warn('Logger Singleton already setup, returning existing singleton');
|
||
}
|
||
return LoggerSingleton;
|
||
};
|
||
/**
|
||
* Get the configuration for the Pino logger
|
||
*
|
||
* @param {object} config - Configuration that overrides/extends the defaults set by this function
|
||
* @return {object}
|
||
*/
|
||
function getLoggerConfig(config = {}) {
|
||
const { consoleOverride } = config;
|
||
return Object.assign({ name: 'hls', timestamp: !config.sendLogs ? browser.stdTimeFunctions.isoTime : browser.stdTimeFunctions.epochTime, browser: {
|
||
asObject: true,
|
||
serialize: true,
|
||
transmit: {
|
||
send: (level, logEvent) => {
|
||
if (config.sendLogs && logEvent) {
|
||
config.sendLogs(level, logEvent); // Custom send function
|
||
}
|
||
},
|
||
},
|
||
write: {
|
||
debug: consoleWriteFn.bind(null, logFn(consoleOverride || console, 'debug'), 'debug'),
|
||
info: consoleWriteFn.bind(null, logFn(consoleOverride || console, 'info'), 'info'),
|
||
warn: consoleWriteFn.bind(null, logFn(consoleOverride || console, 'warn'), 'warn'),
|
||
error: consoleWriteFn.bind(null, logFn(consoleOverride || console, 'error'), 'error'),
|
||
fatal: consoleWriteFn.bind(null, logFn(consoleOverride || console, 'error'), 'fatal'),
|
||
},
|
||
} }, config);
|
||
}
|
||
const noop$1 = () => { };
|
||
function logFn(consoleObj, consoleLevel) {
|
||
consoleObj = consoleLevel in consoleObj ? consoleObj : console;
|
||
const logFn = consoleObj[consoleLevel] || consoleObj.log;
|
||
if (logFn) {
|
||
return logFn.bind(consoleObj);
|
||
}
|
||
return noop$1;
|
||
}
|
||
function consoleWriteFn(method, logLevel, o) {
|
||
const { time, sessionId, critical, name, msg } = o;
|
||
let logData = '';
|
||
if ('data' in o) {
|
||
try {
|
||
const keys = [];
|
||
const refs = [];
|
||
logData = JSON.stringify(o.data, (key, value) => {
|
||
if (typeof value === 'object' && value !== null) {
|
||
const circularRefIndex = refs.indexOf(value);
|
||
if (circularRefIndex !== -1) {
|
||
return `[Circular object reference: '${keys[circularRefIndex]}']`;
|
||
}
|
||
keys.push(key);
|
||
refs.push(value);
|
||
}
|
||
return value;
|
||
});
|
||
}
|
||
catch (e) {
|
||
logData = `Log serialization error: "${e}"`;
|
||
}
|
||
}
|
||
method(`${getCustomLocaleTimeString(time)}| [SessionID: ${sessionId}] | [${logLevel}] >${critical ? ' [QE Critical]' : ''} [${name}] ${msg ? msg : ''} ${logData}`);
|
||
}
|
||
/**
|
||
* Returns local date and time with millisecond precision and timezone identifier
|
||
*/
|
||
function getCustomLocaleTimeString(isoDateTime) {
|
||
const date = new Date(isoDateTime);
|
||
const off = date.getTimezoneOffset();
|
||
const hrs = pad(Math.floor(Math.abs(off) / 60));
|
||
const min = pad(Math.abs(off) % 60);
|
||
let tz = off <= 0 ? 'UTC+' + hrs : 'UTC-' + hrs;
|
||
tz = min ? tz + ':' + min : tz;
|
||
return (date.getFullYear() + '-' +
|
||
pad(date.getMonth() + 1) + '-' +
|
||
pad(date.getDate()) + ' ' +
|
||
pad(date.getHours()) + ':' +
|
||
pad(date.getMinutes()) + ':' +
|
||
pad(date.getSeconds()) + '.' +
|
||
(date.getMilliseconds() / 1000).toFixed(3).slice(2, 5) + ' ' +
|
||
tz);
|
||
}
|
||
/** Print Helpers **/
|
||
function pad(number) {
|
||
if (number < 10) {
|
||
return '0' + number;
|
||
}
|
||
return number.toString();
|
||
}
|
||
/**
|
||
* getLogger get the logger singleton defined in hls.ts
|
||
*
|
||
* Usage:
|
||
* getLogger().info({ name: "yourModuleName" }, 'msg to log');
|
||
* if you do not provide { name: "yourModuleName" }, log line will have 'name: "hls"'. A default name
|
||
*/
|
||
const getLogger = () => {
|
||
if (!LoggerSingleton) {
|
||
LoggerSingleton = browser(addQELevel()).child({ name: 'hls' });
|
||
LoggerSingleton.qe = (msg) => LoggerSingleton.info(msg);
|
||
LoggerSingleton.warn('getLogger called without hls object instantiated, returning a logger that is not configured');
|
||
}
|
||
return LoggerSingleton;
|
||
};
|
||
|
||
const MuxerHelper = {
|
||
bin2str(buffer) {
|
||
return String.fromCharCode.apply(null, Array.from(buffer));
|
||
},
|
||
readUint16(buffer, offset) {
|
||
const val = (buffer[offset] << 8) | buffer[offset + 1];
|
||
return val < 0 ? 65536 + val : val;
|
||
},
|
||
readSint32(buffer, offset) {
|
||
const val = (buffer[offset] << 24) | (buffer[offset + 1] << 16) | (buffer[offset + 2] << 8) | buffer[offset + 3];
|
||
return val;
|
||
},
|
||
readUint32(buffer, offset) {
|
||
const val = MuxerHelper.readSint32(buffer, offset);
|
||
return val < 0 ? 4294967296 + val : val;
|
||
},
|
||
writeUint32(buffer, offset, value) {
|
||
buffer[offset] = value >> 24;
|
||
buffer[offset + 1] = (value >> 16) & 255;
|
||
buffer[offset + 2] = (value >> 8) & 255;
|
||
buffer[offset + 3] = value & 255;
|
||
},
|
||
readUint64(buffer, offset) {
|
||
let result = MuxerHelper.readUint32(buffer, offset);
|
||
result *= Math.pow(2, 32);
|
||
result += MuxerHelper.readUint32(buffer, offset + 4);
|
||
return result;
|
||
},
|
||
writeUint64(buffer, offset, value) {
|
||
const UINT32_MAX = Math.pow(2, 32) - 1;
|
||
const upper = Math.floor(value / (UINT32_MAX + 1));
|
||
const lower = Math.floor(value % (UINT32_MAX + 1));
|
||
MuxerHelper.writeUint32(buffer, offset, upper);
|
||
MuxerHelper.writeUint32(buffer, offset + 4, lower);
|
||
},
|
||
// Find the data for a box specified by its path
|
||
findBox(data, path) {
|
||
let results = [];
|
||
let ix;
|
||
let size;
|
||
let type;
|
||
let end;
|
||
let subresults;
|
||
if (!path.length) {
|
||
// short-circuit the search for empty paths
|
||
return [];
|
||
}
|
||
for (ix = 0; ix < data.byteLength;) {
|
||
size = MuxerHelper.readUint32(data, ix);
|
||
type = MuxerHelper.bin2str(data.subarray(ix + 4, ix + 8));
|
||
end = size > 1 ? ix + size : data.byteLength;
|
||
if (type === path[0]) {
|
||
if (path.length === 1) {
|
||
// this is the end of the path and we've found the box we were
|
||
// looking for
|
||
results.push(data.subarray(ix + 8, end));
|
||
}
|
||
else {
|
||
// recursively search for the next box along the path
|
||
subresults = MuxerHelper.findBox(data.subarray(ix + 8, end), path.slice(1));
|
||
if (subresults.length) {
|
||
results = results.concat(subresults);
|
||
}
|
||
}
|
||
}
|
||
ix = end;
|
||
}
|
||
// we've finished searching all of data
|
||
return results;
|
||
},
|
||
// Find the offset and data for a box specified by its path
|
||
findBoxWithOffset(data, offset, path, walkedPath) {
|
||
let results = [];
|
||
let ix;
|
||
let size;
|
||
let type;
|
||
let end;
|
||
let subresults;
|
||
if (!path.length) {
|
||
// short-circuit the search for empty paths
|
||
return [];
|
||
}
|
||
for (ix = 0; ix < data.byteLength;) {
|
||
size = MuxerHelper.readUint32(data, ix);
|
||
type = MuxerHelper.bin2str(data.subarray(ix + 4, ix + 8));
|
||
end = size > 1 ? ix + size : data.byteLength;
|
||
if (type === path[0]) {
|
||
if (walkedPath) {
|
||
walkedPath.push({ offset: ix + offset, type: type, size: size });
|
||
}
|
||
if (path.length === 1) {
|
||
// this is the end of the path and we've found the box we were
|
||
// looking for
|
||
results.push({ offset: ix + offset, type: type, data: data.subarray(ix + 8, end), boxSize: size, walkedPath: walkedPath ? walkedPath.slice(0) : undefined });
|
||
}
|
||
else {
|
||
// recursively search for the next box along the path
|
||
subresults = MuxerHelper.findBoxWithOffset(data.subarray(ix + 8, end), ix + offset + 8, path.slice(1), walkedPath ? walkedPath.slice(0) : undefined);
|
||
if (subresults.length) {
|
||
results = results.concat(subresults);
|
||
walkedPath = walkedPath ? walkedPath.slice(0, -1) : undefined;
|
||
}
|
||
}
|
||
}
|
||
ix = end;
|
||
}
|
||
// we've finished searching all of data
|
||
return results;
|
||
},
|
||
};
|
||
|
||
/**
|
||
* Hex dumper class
|
||
*
|
||
*
|
||
*
|
||
*
|
||
*/
|
||
const Hex = {
|
||
hexDump: function (array, limit = Infinity) {
|
||
if (!array) {
|
||
return '';
|
||
}
|
||
const buffer = new Uint8Array(array);
|
||
let i, str = '';
|
||
for (i = 0; i < buffer.length && i < limit; i++) {
|
||
let h = buffer[i].toString(16);
|
||
if (h.length < 2) {
|
||
h = '0' + h;
|
||
}
|
||
str += h;
|
||
}
|
||
return str;
|
||
},
|
||
};
|
||
var Hex$1 = Hex;
|
||
|
||
const loggerName$d = { name: 'MP4EncryptionRemuxer' };
|
||
class MP4EncryptionRemuxer extends RemuxerBase {
|
||
constructor(observer, config, typeSupported, vendor, logger) {
|
||
super(observer, config, logger);
|
||
}
|
||
static _isCommonEncryptionInternal(decryptMethod) {
|
||
return Boolean(decryptMethod && !(decryptMethod === 'NONE' || decryptMethod === 'AES-128'));
|
||
}
|
||
static remuxInitSegment(initSegment, logger, keyTagInfo, sinfArray) {
|
||
if (!keyTagInfo) {
|
||
return initSegment;
|
||
}
|
||
let remuxedInitSegment = initSegment;
|
||
if (MP4EncryptionRemuxer._isCommonEncryptionInternal(keyTagInfo.method)) {
|
||
const keyId = keyTagInfo.keyId;
|
||
let isCbc2 = false;
|
||
const newTraks = []; // list of trak boxes
|
||
const traks = MuxerHelper.findBoxWithOffset(initSegment, 0, ['moov', 'trak']);
|
||
logger.info(loggerName$d, 'trying to patch map');
|
||
traks.forEach((trakTuple) => {
|
||
const trak = trakTuple.data;
|
||
let mediaType;
|
||
let encBoxChildrenOffset = 0;
|
||
const stsdTuple = MuxerHelper.findBoxWithOffset(trak, 0, ['mdia', 'minf', 'stbl', 'stsd'], [])[0];
|
||
// skip the sample entry count
|
||
const sampleEntries = stsdTuple.data.subarray(8);
|
||
let isAudio = true;
|
||
let encBoxes = MuxerHelper.findBoxWithOffset(sampleEntries, stsdTuple.offset + 16, ['enca']);
|
||
if (encBoxes.length === 0) {
|
||
isAudio = false;
|
||
encBoxes = MuxerHelper.findBoxWithOffset(sampleEntries, stsdTuple.offset + 16, ['encv']);
|
||
}
|
||
encBoxes.forEach((encBoxTuple) => {
|
||
let encBoxChildren = null;
|
||
let newTrak = null;
|
||
if (isAudio) {
|
||
// encrypted AudioSampleEntry - skip 28 bytes
|
||
encBoxChildren = encBoxTuple.data.subarray(28);
|
||
mediaType = 'audio';
|
||
encBoxChildrenOffset = stsdTuple.offset + 16 + 8 + 28;
|
||
}
|
||
else {
|
||
// encrypted VisualSampleEntry - skip 78 bytes
|
||
encBoxChildren = encBoxTuple.data.subarray(78);
|
||
mediaType = 'video';
|
||
encBoxChildrenOffset = stsdTuple.offset + 16 + 8 + 78;
|
||
}
|
||
if (encBoxChildren) {
|
||
const sinfTuples = MuxerHelper.findBoxWithOffset(encBoxChildren, encBoxChildrenOffset, ['sinf']);
|
||
sinfTuples.forEach((sinfTuple) => {
|
||
const sinf = sinfTuple.data;
|
||
const frma = MuxerHelper.findBox(sinf, ['frma'])[0];
|
||
const schm = MuxerHelper.findBox(sinf, ['schm'])[0];
|
||
if (!frma) {
|
||
logger.error(loggerName$d, 'missing frma box');
|
||
return;
|
||
}
|
||
else if (!schm) {
|
||
logger.error(loggerName$d, 'missing schm box');
|
||
return;
|
||
}
|
||
const scheme = MuxerHelper.bin2str(schm.subarray(4, 8));
|
||
const format = MuxerHelper.bin2str(frma.subarray(0, 4));
|
||
if (format === 'aac ') {
|
||
if (!MP4$1.types) {
|
||
MP4$1.init();
|
||
}
|
||
logger.info(loggerName$d, 'found frma with type \'aac \', patching to \'mp4a\'');
|
||
frma.set(MP4$1.types.mp4a, 0);
|
||
}
|
||
if (scheme === 'cbcs' || scheme === 'cenc') {
|
||
if (sinfArray) {
|
||
sinfArray.push(sinf); // used for unit tests until we get a more generic parser
|
||
}
|
||
const tenc = MuxerHelper.findBox(sinf, ['schi', 'tenc'])[0];
|
||
if (tenc) {
|
||
const kKeyIdOffset = 8; // keyID offset is always 8 within the tenc box
|
||
// Look for default key id:
|
||
const tencKeyId = tenc.subarray(kKeyIdOffset, kKeyIdOffset + 16);
|
||
logger.info(loggerName$d, `found 'tenc' patching map with keyId default:${Hex$1.hexDump(tencKeyId)}->${Hex$1.hexDump(keyId)}`);
|
||
tenc.set(keyId, kKeyIdOffset);
|
||
// we may not need to do this, but at some point we were authoring content with zeros for the IV
|
||
// chrome does use this value, so set it to be safe
|
||
const defaultIsProtected = tenc[6];
|
||
const defaultPerSampleIVSize = tenc[7];
|
||
if (defaultIsProtected === 1 && defaultPerSampleIVSize === 0) {
|
||
const defaultConstantIVSize = tenc[24];
|
||
if (defaultConstantIVSize > 0 && keyTagInfo.iv && defaultConstantIVSize === keyTagInfo.iv.length) {
|
||
logger.info(loggerName$d, 'patching iv for good measure');
|
||
tenc.set(keyTagInfo.iv, 25);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else if (scheme === 'cbc2') {
|
||
logger.info(loggerName$d, 'found \'cbc2\' scheme, converting to \'cbcs\'');
|
||
isCbc2 = true;
|
||
if (!MP4$1.types) {
|
||
MP4$1.init();
|
||
}
|
||
// create a sinf from the existing format and new scheme and scheme info boxes
|
||
const frmaTuple = MuxerHelper.findBoxWithOffset(sinf, 0, ['frma'])[0];
|
||
const newSchi = MP4$1.box(MP4$1.types.schi, MP4$1.tenc(keyTagInfo, mediaType));
|
||
const newSinf = MP4$1.box(MP4$1.types.sinf, sinf.subarray(frmaTuple.offset, frmaTuple.boxSize), MP4$1.schm(), newSchi);
|
||
// create a new track from the existing trak with the new sinf spliced in
|
||
newTrak = MP4$1.box(MP4$1.types.trak, trak.subarray(0, sinfTuple.offset), newSinf, trak.subarray(sinfTuple.offset + sinfTuple.boxSize));
|
||
const newTrackData = newTrak.subarray(8);
|
||
// need to update all the sizes down the box path in the new track
|
||
// we can re-use the offsets from the already parsed path since they're before the splice point
|
||
const sizeIncrease = newSinf.byteLength - sinfTuple.boxSize;
|
||
if (stsdTuple.walkedPath) {
|
||
stsdTuple.walkedPath.push({ type: 'stsd', offset: encBoxTuple.offset, size: encBoxTuple.boxSize }); // enc* offset is special
|
||
stsdTuple.walkedPath.forEach((step) => {
|
||
MuxerHelper.writeUint32(newTrackData, step.offset, step.size + sizeIncrease);
|
||
});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
if (!newTrak) {
|
||
newTrak = initSegment.subarray(trakTuple.offset, trakTuple.offset + trakTuple.boxSize);
|
||
}
|
||
newTraks.push(newTrak);
|
||
});
|
||
const edtsTuple = MuxerHelper.findBoxWithOffset(trak, 0, ['edts'])[0];
|
||
if (edtsTuple) {
|
||
if (!MP4$1.types) {
|
||
MP4$1.init();
|
||
}
|
||
logger.info(loggerName$d, 'overwriting edts box');
|
||
trak.set(MP4$1.types.free, edtsTuple.offset + 4);
|
||
}
|
||
});
|
||
if (isCbc2) {
|
||
const cbcsInitSegment = MP4EncryptionRemuxer.remuxCbc2InitSegment(initSegment, newTraks, logger);
|
||
remuxedInitSegment = cbcsInitSegment ? cbcsInitSegment : initSegment;
|
||
}
|
||
}
|
||
return remuxedInitSegment;
|
||
}
|
||
/**
|
||
* @param traksToAppend Array of trak boxes to replace traks in initSegment with.
|
||
*/
|
||
static remuxCbc2InitSegment(initSegment, traksToAppend, logger) {
|
||
const ftypTuple = MuxerHelper.findBoxWithOffset(initSegment, 0, ['ftyp'])[0];
|
||
if (!ftypTuple) {
|
||
logger.error(loggerName$d, 'no ftyp found');
|
||
return;
|
||
}
|
||
const moovTuple = MuxerHelper.findBoxWithOffset(initSegment, ftypTuple.boxSize, ['moov'])[0];
|
||
let moovChildren = [];
|
||
let ix = 0;
|
||
while (ix < moovTuple.data.byteLength) {
|
||
const size = MuxerHelper.readUint32(moovTuple.data, ix);
|
||
const type = MuxerHelper.bin2str(moovTuple.data.subarray(ix + 4, ix + 8));
|
||
const end = size > 1 ? ix + size : moovTuple.data.byteLength;
|
||
switch (type) {
|
||
case 'trak': {
|
||
if (traksToAppend) {
|
||
// Just append these here where the traks used to be
|
||
moovChildren = moovChildren.concat(traksToAppend);
|
||
traksToAppend = undefined;
|
||
}
|
||
break;
|
||
}
|
||
default:
|
||
moovChildren.push(moovTuple.data.subarray(ix, end));
|
||
break;
|
||
}
|
||
ix = end;
|
||
}
|
||
const newMoov = MP4$1.box(MP4$1.types.moov, ...moovChildren);
|
||
const newInitSegment = new Uint8Array(ftypTuple.boxSize + newMoov.byteLength);
|
||
newInitSegment.set(initSegment.subarray(0, ftypTuple.boxSize));
|
||
newInitSegment.set(newMoov, ftypTuple.boxSize);
|
||
return newInitSegment;
|
||
}
|
||
static remuxOverflowSegment(data, logger) {
|
||
if (!MP4$1.types) {
|
||
MP4$1.init();
|
||
}
|
||
const tfdts = MuxerHelper.findBoxWithOffset(data, 0, ['moof', 'traf', 'tfdt'], []);
|
||
let newDataSize = data.byteLength;
|
||
let newData;
|
||
// total all tfdts in segment so we know the size increase (can have multiple moofs in a segment)
|
||
tfdts.forEach((tfdt) => {
|
||
// increase the data size by 4 bytes for each 32 bit tfdt
|
||
if (tfdt.data[0] === 0) {
|
||
newDataSize += 4;
|
||
}
|
||
});
|
||
if (newDataSize > data.byteLength) {
|
||
newData = new Uint8Array(newDataSize);
|
||
let newOffset = 0;
|
||
let dataBoxIndex = 0;
|
||
while (dataBoxIndex < data.byteLength) {
|
||
const size = MuxerHelper.readUint32(data, dataBoxIndex);
|
||
const type = MuxerHelper.bin2str(data.subarray(dataBoxIndex + 4, dataBoxIndex + 8));
|
||
const dataBoxEnd = size > 1 ? dataBoxIndex + size : data.byteLength;
|
||
if (type === 'moof') {
|
||
const newMoof = MP4EncryptionRemuxer.remuxOverflowMoof(data.subarray(dataBoxIndex + 8, dataBoxEnd));
|
||
newData.set(newMoof, newOffset);
|
||
newOffset += newMoof.byteLength;
|
||
}
|
||
else {
|
||
newData.set(data.subarray(dataBoxIndex, dataBoxEnd), newOffset);
|
||
newOffset += size;
|
||
}
|
||
dataBoxIndex = dataBoxEnd;
|
||
}
|
||
}
|
||
else {
|
||
logger.warn(loggerName$d, 'no increase in size');
|
||
}
|
||
return newData ? newData : data;
|
||
}
|
||
static remuxOverflowMoof(moofData) {
|
||
let moofIndex = 0;
|
||
const moofChildren = [];
|
||
while (moofIndex < moofData.byteLength) {
|
||
const size = MuxerHelper.readUint32(moofData, moofIndex);
|
||
const type = MuxerHelper.bin2str(moofData.subarray(moofIndex + 4, moofIndex + 8));
|
||
if (type === 'traf') {
|
||
const newTraf = MP4EncryptionRemuxer.remuxOverflowTraf(moofData.subarray(moofIndex + 8, moofIndex + size));
|
||
moofChildren.push(newTraf);
|
||
}
|
||
else {
|
||
moofChildren.push(moofData.subarray(moofIndex, moofIndex + size));
|
||
}
|
||
moofIndex = size > 1 ? moofIndex + size : moofData.byteLength;
|
||
}
|
||
const newMoof = MP4$1.box(MP4$1.types.moof, ...moofChildren);
|
||
const sizeDiff = newMoof.byteLength - moofData.byteLength - 8; // moofData doesn't include header, so subtract 8
|
||
const truns = MuxerHelper.findBoxWithOffset(newMoof, 0, ['moof', 'traf', 'trun'], []);
|
||
truns.forEach((trun) => {
|
||
const dataOffsetPresent = (trun.data[3] & 1) !== 0;
|
||
if (dataOffsetPresent) {
|
||
const dataOffset = MuxerHelper.readUint32(trun.data, 8);
|
||
MuxerHelper.writeUint32(trun.data, 8, dataOffset + sizeDiff);
|
||
}
|
||
});
|
||
const saios = MuxerHelper.findBoxWithOffset(newMoof, 0, ['moof', 'traf', 'saio'], []);
|
||
saios.forEach((saio) => {
|
||
const version = saio.data[0] & 1;
|
||
let dataOffset = 4;
|
||
if (saio.data[3] & 1) {
|
||
// aux info type and parameter
|
||
dataOffset += 8;
|
||
}
|
||
const ec = MuxerHelper.readUint32(saio.data, dataOffset);
|
||
dataOffset += 4;
|
||
if (!version) {
|
||
for (let i = 0; i < ec; i++) {
|
||
const value = MuxerHelper.readUint32(saio.data, dataOffset);
|
||
MuxerHelper.writeUint32(saio.data, dataOffset, value + sizeDiff);
|
||
dataOffset += 4;
|
||
}
|
||
}
|
||
else {
|
||
for (let i = 0; i < ec; i++) {
|
||
const value = MuxerHelper.readUint64(saio.data, dataOffset);
|
||
MuxerHelper.writeUint64(saio.data, dataOffset, value + sizeDiff);
|
||
dataOffset += 8;
|
||
}
|
||
}
|
||
});
|
||
return newMoof;
|
||
}
|
||
static remuxOverflowTraf(trafData) {
|
||
let trafIndex = 0;
|
||
const trafChildren = [];
|
||
while (trafIndex < trafData.byteLength) {
|
||
const size = MuxerHelper.readUint32(trafData, trafIndex);
|
||
const type = MuxerHelper.bin2str(trafData.subarray(trafIndex + 4, trafIndex + 8));
|
||
if (type === 'tfdt' && trafData[trafIndex + 8] === 0) {
|
||
const tfdtValue = MuxerHelper.readUint32(trafData, trafIndex + 12);
|
||
const newTfdt = MP4$1.box(MP4$1.types.tfdt, new Uint8Array([
|
||
1,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
0,
|
||
tfdtValue >> 24,
|
||
(tfdtValue >> 16) & 255,
|
||
(tfdtValue >> 8) & 255,
|
||
tfdtValue & 255,
|
||
]));
|
||
trafChildren.push(newTfdt);
|
||
}
|
||
else {
|
||
trafChildren.push(trafData.subarray(trafIndex, trafIndex + size));
|
||
}
|
||
trafIndex = size > 1 ? trafIndex + size : trafData.byteLength;
|
||
}
|
||
const newTraf = MP4$1.box(MP4$1.types.traf, ...trafChildren);
|
||
return newTraf;
|
||
}
|
||
remuxText(track, parsingData) {
|
||
track.captionSamples.sort(function (a, b) {
|
||
return a.pts - b.pts;
|
||
});
|
||
const length = track.captionSamples.length;
|
||
if (length) {
|
||
if (!parsingData.captionData) {
|
||
parsingData.captionData = {};
|
||
}
|
||
parsingData.captionData.mp4 = track.captionSamples;
|
||
}
|
||
track.captionSamples = [];
|
||
}
|
||
remuxIFrame(startDTS, videoTrack, audioTrack, iframeDuration, track) {
|
||
if (!videoTrack.samples || !videoTrack.samples.length || !videoTrack.samples[0].data) {
|
||
return null;
|
||
}
|
||
let streamType;
|
||
const moof = MP4$1.moof(startDTS * videoTrack.timescale, videoTrack);
|
||
const iframeSegment = new Uint8Array(moof.byteLength + videoTrack.samples[0].data.byteLength + 8);
|
||
iframeSegment.set(moof);
|
||
MuxerHelper.writeUint32(iframeSegment, moof.byteLength, videoTrack.samples[0].data.byteLength + 8);
|
||
iframeSegment.set(MP4$1.types.mdat, moof.byteLength + 4);
|
||
iframeSegment.set(videoTrack.samples[0].data, moof.byteLength + 8);
|
||
videoTrack.sequenceNumber++;
|
||
const videoTimescale = videoTrack.timescale;
|
||
const videoEndTs = convertSecondsToTimestamp(startDTS + iframeDuration, videoTimescale);
|
||
const videoStartTs = convertSecondsToTimestamp(startDTS, videoTimescale);
|
||
let endTs;
|
||
let audioSegment;
|
||
if (audioTrack) {
|
||
streamType = 'audiovideo';
|
||
audioTrack.sequenceNumber = videoTrack.sequenceNumber;
|
||
videoTrack.sequenceNumber++;
|
||
audioSegment = SilentAudio.getSegment(audioTrack, audioTrack.sequenceNumber, startDTS * audioTrack.info.timescale, iframeDuration);
|
||
endTs = determineMaxTimestamp(audioSegment.endTs, videoEndTs);
|
||
}
|
||
else {
|
||
streamType = 'video';
|
||
endTs = videoEndTs;
|
||
}
|
||
const data = {
|
||
data1: iframeSegment,
|
||
data2: audioSegment === null || audioSegment === void 0 ? void 0 : audioSegment.silentFragData,
|
||
startPTS: videoStartTs,
|
||
startDTS: videoStartTs,
|
||
endPTS: endTs,
|
||
endDTS: endTs,
|
||
type: streamType,
|
||
dropped: 0,
|
||
track,
|
||
};
|
||
// send the data
|
||
this.observer.trigger(DemuxerEvent.FRAG_PARSING_DATA, data);
|
||
// notify end of parsing
|
||
this.observer.trigger(DemuxerEvent.FRAG_PARSED);
|
||
}
|
||
remuxEmsgAndRawData(audioDuration, hasAudio, videoDuration, hasVideo, textTrack, id3Track, timeOffset, timescale, rawFragment, track) {
|
||
let streamType;
|
||
if (hasAudio && hasVideo) {
|
||
streamType = 'audiovideo';
|
||
}
|
||
else if (hasAudio) {
|
||
streamType = 'audio';
|
||
}
|
||
else if (hasVideo) {
|
||
streamType = 'video';
|
||
}
|
||
const fragDuration = Math.max(videoDuration, audioDuration);
|
||
const data = {
|
||
data1: rawFragment,
|
||
track,
|
||
startPTS: convertSecondsToTimestamp(timeOffset, timescale),
|
||
startDTS: convertSecondsToTimestamp(timeOffset, timescale),
|
||
endPTS: undefined,
|
||
endDTS: undefined,
|
||
type: streamType,
|
||
dropped: 0,
|
||
};
|
||
if (fragDuration) {
|
||
data.endPTS = convertSecondsToTimestamp(timeOffset + fragDuration, timescale);
|
||
data.endDTS = convertSecondsToTimestamp(timeOffset + fragDuration, timescale);
|
||
}
|
||
if (textTrack && textTrack.captionSamples.length) {
|
||
this.remuxText(textTrack, data);
|
||
}
|
||
if (id3Track && id3Track.id3Samples.length > 0) {
|
||
this.remuxID3(id3Track, data);
|
||
}
|
||
this.observer.trigger(DemuxerEvent.FRAG_PARSING_DATA, data);
|
||
this.observer.trigger(DemuxerEvent.FRAG_PARSED);
|
||
return data;
|
||
}
|
||
remuxID3(track, parsingData) {
|
||
let sample;
|
||
const length = track.id3Samples.length;
|
||
const PTSOffset = 10;
|
||
const DTSOffset = 10;
|
||
if (length) {
|
||
for (let index = 0; index < length; index++) {
|
||
sample = track.id3Samples[index];
|
||
// emsg pts is aligned with media content and have 10s offset to avoid negative time in audio samples
|
||
sample.pts = sample.pts - PTSOffset;
|
||
sample.dts = sample.dts - DTSOffset;
|
||
}
|
||
parsingData.id3Samples = [...track.id3Samples];
|
||
}
|
||
track.id3Samples = [];
|
||
}
|
||
}
|
||
|
||
|
||
const UINT32_MAX = Math.pow(2, 32) - 1;
|
||
const MAX_UPPER_32_VALUE = Math.pow(2, 20) - 1; // 52 bit integer float precision
|
||
const LARGE_TIMESCALE = 1000000;
|
||
const loggerName$c = { name: 'MP4Demuxer' };
|
||
class MP4Demuxer extends DemuxerBase {
|
||
constructor(observer, remuxer, config, typeSupported = undefined, logger) {
|
||
super(observer, remuxer, config, {}, logger);
|
||
this.mp4Remuxer = remuxer;
|
||
// may need a little headroom for priming audio samples, so shift everything by the max value we expect
|
||
// that should be 2112 audio samples at 22050 hz
|
||
this.audioPrimingDelay = config.audioPrimingDelay;
|
||
if (this.audioPrimingDelay > 0) {
|
||
this.logger.info(loggerName$c, `using audioPrimingDelay of ${this.audioPrimingDelay}`);
|
||
}
|
||
}
|
||
resetTimeStamp(initPTS90k) {
|
||
if (isFiniteNumber(initPTS90k)) {
|
||
if (this.initData.audio && !this.initData.video) {
|
||
// if this is an audio only track go ahead and convert initPTS to the sample rate to avoid cumulative rounding errors
|
||
this.initPtsTs = { baseTime: Math.round((initPTS90k * this.initData.audio.timescale) / 90000), timescale: this.initData.audio.timescale };
|
||
}
|
||
else {
|
||
this.initPtsTs = { baseTime: initPTS90k, timescale: 90000 };
|
||
}
|
||
}
|
||
else {
|
||
this.initPtsTs = undefined;
|
||
}
|
||
}
|
||
static isHEVCFlavor(codec) {
|
||
if (!codec) {
|
||
return false;
|
||
}
|
||
const delimit = codec.indexOf('.');
|
||
const baseCodec = delimit < 0 ? codec : codec.substring(0, delimit);
|
||
return (baseCodec === 'hvc1' ||
|
||
baseCodec === 'hev1' /*
|
||
baseCodec === 'phvc' ||
|
||
baseCodec === 'ehvc' ||
|
||
baseCodec === 'zhvc' ||*/
|
||
||
|
||
baseCodec === 'chvc' ||
|
||
baseCodec === 'qhvc' ||
|
||
baseCodec === 'qhev' ||
|
||
baseCodec === 'muxa' ||
|
||
// Dolby Vision
|
||
baseCodec === 'dvh1' ||
|
||
baseCodec === 'dvhe' /*
|
||
baseCodec === 'ddh1' ||
|
||
baseCodec === 'zdh1' ||*/
|
||
||
|
||
baseCodec === 'cdh1' ||
|
||
baseCodec === 'qdh1' ||
|
||
baseCodec === 'qdhe');
|
||
}
|
||
resetInitSegment(initSegment, duration, keyTagInfo) {
|
||
// jshint unused:false
|
||
this._silentAudioTrack = undefined;
|
||
if (initSegment && initSegment.byteLength) {
|
||
const remuxedInitSegment = MP4EncryptionRemuxer.remuxInitSegment(initSegment, this.logger, keyTagInfo);
|
||
const initData = (this.initData = MP4Demuxer.parseInitSegment(remuxedInitSegment));
|
||
let track;
|
||
if (initData.foundLargeTimescale) {
|
||
this.logger.warn(loggerName$c, 'large timescale found, will check for 32 bit tfdts');
|
||
}
|
||
const parsedAudioCodec = initData.audioCodec;
|
||
const parsedVideoCodec = initData.videoCodec;
|
||
if (initData.audio && initData.video) {
|
||
track = { type: 'audiovideo', container: 'video/mp4', codec: parsedAudioCodec + ',' + parsedVideoCodec, initSegment: remuxedInitSegment };
|
||
}
|
||
else {
|
||
if (initData.audio && parsedAudioCodec) {
|
||
track = { type: 'audio', container: 'audio/mp4', codec: parsedAudioCodec, initSegment: remuxedInitSegment };
|
||
}
|
||
if (initData.video && parsedVideoCodec) {
|
||
track = { type: 'video', container: 'video/mp4', codec: parsedVideoCodec, initSegment: remuxedInitSegment };
|
||
}
|
||
}
|
||
if (initData.video) {
|
||
// needed to generate moofs for iframe mode frames
|
||
// we need the track data to create a new moof for muxed iframe mode
|
||
const parsedVideoTrack = initData.video;
|
||
const trakData = initSegment.subarray(parsedVideoTrack.trakOffset, parsedVideoTrack.trakOffset + parsedVideoTrack.trakSize);
|
||
this._videoTrack = Object.assign(Object.assign({}, parsedVideoTrack), { info: { id: parsedVideoTrack.id, timescale: parsedVideoTrack.timescale, duration }, trakData: trakData, sequenceNumber: 0, samples: [] });
|
||
this.trySEICaptions = !MediaUtil.isVP09(parsedVideoCodec);
|
||
this._captionTrack = Object.assign(Object.assign({}, initData.caption), { sequenceNumber: 0, captionSamples: [] });
|
||
this.logger.info(loggerName$c, 'assume SEI closed caption exists');
|
||
}
|
||
if (initData.audio && parsedAudioCodec) {
|
||
// only need id and codec for silent audio generation
|
||
this._audioTrack = Object.assign({}, initData.audio);
|
||
}
|
||
if (initData.caption) {
|
||
// use clcp track if present
|
||
this.trySEICaptions = false;
|
||
this.logger.info(loggerName$c, `use clcp track id ${initData.caption.id}`);
|
||
this._captionTrack = Object.assign(Object.assign({}, initData.caption), { sequenceNumber: 0, captionSamples: [] });
|
||
}
|
||
this.remuxedInitDataTrack = track;
|
||
const eventData = { track };
|
||
this.observer.trigger(DemuxerEvent.FRAG_PARSING_INIT_SEGMENT, eventData);
|
||
}
|
||
}
|
||
static probe(data, logger) {
|
||
// ensure we find a moof box in the first 128kB
|
||
return MuxerHelper.findBox(data.subarray(0, Math.min(data.length, 512000)), ['moof']).length > 0;
|
||
}
|
||
static parseHvcC(data) {
|
||
let codecConfig;
|
||
if (data) {
|
||
const version = data[0];
|
||
if (version === 1) {
|
||
const tmpByte = data[1];
|
||
codecConfig = {
|
||
profileSpace: tmpByte >> 6,
|
||
tierFlag: (tmpByte & 32) >> 5 ? 'H' : 'L',
|
||
profileIDC: tmpByte & 31,
|
||
profileCompat: MuxerHelper.readUint32(data, 2),
|
||
constraintIndicator: data.subarray(6, 12),
|
||
levelIDC: data[12],
|
||
};
|
||
}
|
||
else {
|
||
getLogger().warn(loggerName$c, `Unhandled version ${version} in hvcC box`);
|
||
}
|
||
}
|
||
else {
|
||
getLogger().warn(loggerName$c, 'No hvcC box');
|
||
}
|
||
return codecConfig;
|
||
}
|
||
static hvcCToCodecString(baseCodecType, codecConfig) {
|
||
const profileSpaceStr = codecConfig.profileSpace ? String.fromCharCode(codecConfig.profileSpace + 'A' - 1) : '';
|
||
const codecString = baseCodecType + '.' + profileSpaceStr + codecConfig.profileIDC + '.' + codecConfig.profileCompat.toString(16).toUpperCase() + '.' + codecConfig.tierFlag + codecConfig.levelIDC;
|
||
// append constraint indicators, ignoring trailing zero bytes
|
||
let constraintIndicatorStr = '';
|
||
for (let i = codecConfig.constraintIndicator.length - 1; i >= 0; --i) {
|
||
const byte = codecConfig.constraintIndicator[i];
|
||
if (!(byte === 0 && constraintIndicatorStr === '')) {
|
||
const encodedByte = byte.toString(16).toUpperCase();
|
||
constraintIndicatorStr = '.' + (constraintIndicatorStr === '' ? encodedByte : encodedByte + constraintIndicatorStr);
|
||
}
|
||
}
|
||
return codecString + constraintIndicatorStr;
|
||
}
|
||
static parseDvcC(data) {
|
||
let codecConfig;
|
||
if (data) {
|
||
codecConfig = {
|
||
versionMajor: data[0],
|
||
versionMinor: data[1],
|
||
profile: (data[2] >> 1) & 127,
|
||
level: ((data[2] << 5) & 32) | ((data[3] >> 3) & 31),
|
||
};
|
||
}
|
||
else {
|
||
getLogger().warn(loggerName$c, 'No dvcC box');
|
||
}
|
||
return codecConfig;
|
||
}
|
||
static dvcCToCodecString(baseCodecType, codecConfig) {
|
||
const profileStr = MP4Demuxer.checkAndAddLeadingZero(codecConfig.profile);
|
||
const levelStr = MP4Demuxer.checkAndAddLeadingZero(codecConfig.level);
|
||
return baseCodecType + '.' + profileStr + '.' + levelStr;
|
||
}
|
||
static parseVpcC(data) {
|
||
let codecConfig;
|
||
if (data) {
|
||
codecConfig = {
|
||
profile: data[4],
|
||
level: data[5],
|
||
bitDepth: (data[6] >> 4) & 15,
|
||
};
|
||
}
|
||
else {
|
||
getLogger().warn(loggerName$c, 'No vpcC box');
|
||
}
|
||
return codecConfig;
|
||
}
|
||
static vpcCToCodecString(baseCodecType, codecConfig) {
|
||
const profileStr = MP4Demuxer.checkAndAddLeadingZero(codecConfig.profile);
|
||
const levelStr = MP4Demuxer.checkAndAddLeadingZero(codecConfig.level);
|
||
const subsampling = MP4Demuxer.checkAndAddLeadingZero(codecConfig.bitDepth);
|
||
return baseCodecType + '.' + profileStr + '.' + levelStr + '.' + subsampling;
|
||
}
|
||
static checkAndAddLeadingZero(num) {
|
||
return (num < 10 ? '0' : '') + num;
|
||
}
|
||
/**
|
||
* Parses an MP4 initialization segment and extracts stream type and
|
||
* timescale values for any declared tracks. Timescale values indicate the
|
||
* number of clock ticks per second to assume for time-based values
|
||
* elsewhere in the MP4.
|
||
*
|
||
* To determine the start time of an MP4, you need two pieces of
|
||
* information: the timescale unit and the earliest base media decode
|
||
* time. Multiple timescales can be specified within an MP4 but the
|
||
* base media decode time is always expressed in the timescale from
|
||
* the media header box for the track:
|
||
* ```
|
||
* moov > trak > mdia > mdhd.timescale
|
||
* moov > trak > mdia > hdlr
|
||
* ```
|
||
* @param init {Uint8Array} the bytes of the init segment
|
||
* @return {object} a hash of track type to timescale values or null if
|
||
* the init segment is malformed.
|
||
*/
|
||
static parseInitSegment(initSegment) {
|
||
const result = { foundLargeTimescale: false, tracksById: {} };
|
||
const traksTuple = MuxerHelper.findBoxWithOffset(initSegment, 0, ['moov', 'trak']);
|
||
traksTuple.forEach((trakTuple) => {
|
||
const trak = trakTuple.data;
|
||
const tkhd = MuxerHelper.findBox(trak, ['tkhd'])[0];
|
||
if (tkhd) {
|
||
let version = tkhd[0];
|
||
const index = version === 0 ? 12 : 20;
|
||
const id = MuxerHelper.readUint32(tkhd, index);
|
||
const mdhd = MuxerHelper.findBox(trak, ['mdia', 'mdhd'])[0];
|
||
if (mdhd) {
|
||
version = mdhd[0];
|
||
let timescaleIndex = version === 0 ? 12 : 20;
|
||
const timescale = MuxerHelper.readUint32(mdhd, timescaleIndex);
|
||
timescaleIndex += 4;
|
||
if (timescale >= LARGE_TIMESCALE) {
|
||
result.foundLargeTimescale = true;
|
||
}
|
||
const duration = version === 0 ? MuxerHelper.readUint32(mdhd, timescaleIndex) : 0; // TODO: read 64 bit? Maybe we don't care, but TS sets this value and we're trying to share track data
|
||
const hdlr = MuxerHelper.findBox(trak, ['mdia', 'hdlr'])[0];
|
||
if (hdlr) {
|
||
const hdlrType = MuxerHelper.bin2str(hdlr.subarray(8, 12));
|
||
const hdlrToTypeMap = { soun: 'audio', vide: 'video', clcp: 'caption' };
|
||
const type = hdlrToTypeMap[hdlrType] || hdlrType;
|
||
if (type) {
|
||
// extract codec info. TODO : parse codec details to be able to build MIME type
|
||
const codecBoxes = MuxerHelper.findBox(trak, ['mdia', 'minf', 'stbl', 'stsd']);
|
||
let codecType;
|
||
if (codecBoxes.length) {
|
||
const codecBox = codecBoxes[0];
|
||
codecType = MuxerHelper.bin2str(codecBox.subarray(12, 16));
|
||
getLogger().info(loggerName$c, `MP4Demuxer:${type}:${codecType} found`);
|
||
const stsdData = MP4Demuxer.parseStsd(codecBox);
|
||
let newTrack;
|
||
if (type === 'caption') {
|
||
const captionTrack = Object.assign({ id, type, timescale, duration, isTimingTrack: false, sequenceNumber: 0, captionSamples: [] }, stsdData);
|
||
result.caption = captionTrack;
|
||
newTrack = captionTrack;
|
||
}
|
||
else {
|
||
const mediaTrack = Object.assign({ id,
|
||
type,
|
||
timescale,
|
||
duration, isTimingTrack: true, trakOffset: trakTuple.offset, trakSize: trakTuple.boxSize, sequenceNumber: 0, samples: [], fragmentDuration: 0 }, stsdData);
|
||
if (type === 'video') {
|
||
result.video = mediaTrack;
|
||
result.videoCodec = mediaTrack.codec;
|
||
}
|
||
else {
|
||
result.audio = mediaTrack;
|
||
result.audioCodec = mediaTrack.codec;
|
||
}
|
||
newTrack = mediaTrack;
|
||
}
|
||
result.tracksById[id] = newTrack;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
const trexTuple = MuxerHelper.findBoxWithOffset(initSegment, 0, ['moov', 'mvex', 'trex']);
|
||
trexTuple.forEach((trexTuple) => {
|
||
const trex = trexTuple.data;
|
||
const id = MuxerHelper.readUint32(trex, 4);
|
||
const defaultSampleSize = MuxerHelper.readUint32(trex, 16);
|
||
result.tracksById[id].defaultSampleSize = defaultSampleSize;
|
||
});
|
||
return result;
|
||
}
|
||
static parseStsd(stsd) {
|
||
let defaultPerSampleIVSize;
|
||
let codec;
|
||
// skip the sample entry count
|
||
const sampleEntries = stsd.subarray(8);
|
||
let baseCodecType = MuxerHelper.bin2str(sampleEntries.subarray(4, 8));
|
||
let encBox = null;
|
||
let encBoxChildren = null;
|
||
if (baseCodecType === 'enca') {
|
||
// audio
|
||
encBox = MuxerHelper.findBox(sampleEntries, ['enca'])[0];
|
||
encBoxChildren = encBox.subarray(28);
|
||
}
|
||
else if (baseCodecType === 'encv') {
|
||
// video
|
||
encBox = MuxerHelper.findBox(sampleEntries, ['encv'])[0];
|
||
// FIXME: May have optional clap and pasp atoms after 78 bytes
|
||
encBoxChildren = encBox.subarray(78);
|
||
}
|
||
const encrypted = !!encBoxChildren;
|
||
defaultPerSampleIVSize = 0;
|
||
if (encBoxChildren) {
|
||
const sinfs = MuxerHelper.findBox(encBoxChildren, ['sinf']);
|
||
sinfs.forEach((sinf) => {
|
||
const schm = MuxerHelper.findBox(sinf, ['schm'])[0];
|
||
if (schm) {
|
||
const scheme = MuxerHelper.bin2str(schm.subarray(4, 8));
|
||
if (scheme === 'cbcs' || scheme === 'cenc') {
|
||
const frma = MuxerHelper.findBox(sinf, ['frma'])[0];
|
||
if (frma) {
|
||
// for encrypted content baseCodecType will be in frma
|
||
baseCodecType = MuxerHelper.bin2str(frma);
|
||
}
|
||
const tenc = MuxerHelper.findBox(sinf, ['schi', 'tenc'])[0];
|
||
if (tenc) {
|
||
defaultPerSampleIVSize = tenc[7];
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
let codecConfig;
|
||
const sampleEntriesEnd = sampleEntries.subarray(86);
|
||
switch (baseCodecType) {
|
||
case 'mp4a':
|
||
codec = 'mp4a.40.5';
|
||
break;
|
||
case 'ac-3':
|
||
case 'ec-3':
|
||
case 'alac':
|
||
case 'fLaC':
|
||
codec = baseCodecType;
|
||
break;
|
||
case 'avc1':
|
||
case 'avc3':
|
||
codec = baseCodecType + '.640028';
|
||
break;
|
||
case 'hvc1':
|
||
case 'hev1':
|
||
const hvcCBox = MuxerHelper.findBox(sampleEntriesEnd, ['hvcC'])[0]; // There may be other atoms right after sampleEntries
|
||
codecConfig = MP4Demuxer.parseHvcC(hvcCBox);
|
||
codec = codecConfig ? MP4Demuxer.hvcCToCodecString(baseCodecType, codecConfig) : baseCodecType + '.2.4.H150.B0';
|
||
break;
|
||
case 'dvh1':
|
||
case 'dvhe':
|
||
const dvcCBox = MuxerHelper.findBox(sampleEntriesEnd, ['dvcC'])[0];
|
||
codecConfig = MP4Demuxer.parseDvcC(dvcCBox);
|
||
codec = codecConfig ? MP4Demuxer.dvcCToCodecString(baseCodecType, codecConfig) : baseCodecType + '.05.01';
|
||
break;
|
||
case 'c608':
|
||
codec = baseCodecType;
|
||
break;
|
||
case 'vp09':
|
||
const vpcCBox = MuxerHelper.findBox(sampleEntriesEnd, ['vpcC'])[0];
|
||
codecConfig = MP4Demuxer.parseVpcC(vpcCBox);
|
||
codec = MP4Demuxer.vpcCToCodecString(baseCodecType, codecConfig);
|
||
break;
|
||
default:
|
||
codec = baseCodecType;
|
||
break;
|
||
}
|
||
return { codec, encrypted, defaultPerSampleIVSize };
|
||
}
|
||
static has32BitTfdts(fragment) {
|
||
const tfdts = MuxerHelper.findBox(fragment, ['moof', 'traf', 'tfdt']);
|
||
let found = false;
|
||
tfdts.forEach((tfdt) => {
|
||
if (tfdt[0] === 0) {
|
||
// version 0 means 32 bit tfdt
|
||
found = true;
|
||
}
|
||
});
|
||
return found;
|
||
}
|
||
/**
|
||
* Determine the base media decode start time, in seconds, for an MP4
|
||
* fragment. If multiple fragments are specified, the earliest time is
|
||
* returned.
|
||
*
|
||
* The base media decode time can be parsed from track fragment
|
||
* metadata:
|
||
* ```
|
||
* moof > traf > tfdt.baseMediaDecodeTime
|
||
* ```
|
||
* It requires the timescale value from the mdhd to interpret.
|
||
*
|
||
* @param timescale {object} a hash of track ids to timescale values.
|
||
* @return {number} the earliest base media decode start time for the
|
||
* fragment, in seconds
|
||
*/
|
||
static getStartDtsTs(initData, fragment) {
|
||
// we need info from two childrend of each track fragment box
|
||
const trafs = MuxerHelper.findBox(fragment, ['moof', 'traf']);
|
||
let minTime = Number.MAX_SAFE_INTEGER;
|
||
let result;
|
||
// determine the start times for each track
|
||
trafs.map(function (traf) {
|
||
return MuxerHelper.findBox(traf, ['tfhd']).forEach((tfhd) => {
|
||
// get the track id from the tfhd
|
||
const id = MuxerHelper.readUint32(tfhd, 4);
|
||
const parsedTrak = initData.tracksById[id];
|
||
let timescale;
|
||
if (!parsedTrak) {
|
||
return;
|
||
}
|
||
if (parsedTrak.isTimingTrack) {
|
||
// assume a 90kHz clock if no timescale was specified
|
||
timescale = parsedTrak.timescale || 90000;
|
||
}
|
||
else {
|
||
return Infinity; // Should just not be considered for time calculation
|
||
}
|
||
// get the base media decode time from the tfdt
|
||
const tfdtTimes = MuxerHelper.findBox(traf, ['tfdt']).map(function (tfdt) {
|
||
let result;
|
||
const version = tfdt[0];
|
||
result = MuxerHelper.readUint32(tfdt, 4);
|
||
if (version === 1) {
|
||
if (result > MAX_UPPER_32_VALUE) {
|
||
getLogger().warn(loggerName$c, `Value larger than can be represented by float for upper 32 bits ${result}`);
|
||
}
|
||
result *= Math.pow(2, 32);
|
||
result += MuxerHelper.readUint32(tfdt, 8);
|
||
}
|
||
return result;
|
||
});
|
||
const baseTime = tfdtTimes.length > 0 ? tfdtTimes[0] : Infinity;
|
||
if (isFinite(baseTime) && baseTime / timescale < minTime) {
|
||
minTime = baseTime / timescale;
|
||
result = { baseTime, timescale };
|
||
}
|
||
});
|
||
});
|
||
return result;
|
||
}
|
||
static offsetStartDTS(initData, fragment, timeOffsetTs, audioPrimingDelaySec) {
|
||
MuxerHelper.findBox(fragment, ['moof', 'traf']).map(function (traf) {
|
||
return MuxerHelper.findBox(traf, ['tfhd']).map(function (tfhd) {
|
||
// get the track id from the tfhd
|
||
const id = MuxerHelper.readUint32(tfhd, 4);
|
||
const parsedTrak = initData.tracksById[id];
|
||
if (!parsedTrak) {
|
||
return;
|
||
}
|
||
// assume a 90kHz clock if no timescale was specified
|
||
const timescale = parsedTrak.timescale || 90000;
|
||
const audioPrimingOffset = parsedTrak.type === 'caption' ? 0 : audioPrimingDelaySec;
|
||
// get the base media decode time from the tfdt
|
||
MuxerHelper.findBox(traf, ['tfdt']).map(function (tfdt) {
|
||
const version = tfdt[0];
|
||
const mediaType = parsedTrak.type;
|
||
if (version === 0) {
|
||
const baseMediaDecodeTime = MuxerHelper.readUint32(tfdt, 4);
|
||
let offsetBaseMediaDecodeTime = baseMediaDecodeTime - Math.round((timeOffsetTs.baseTime * timescale) / timeOffsetTs.timescale);
|
||
// need to make sure video doesn't go negative after timestampOffset is applied
|
||
if (mediaType === 'video' && offsetBaseMediaDecodeTime < 0) {
|
||
getLogger().warn(loggerName$c, `video tdft would have gone negative by ${offsetBaseMediaDecodeTime / timescale} seconds`);
|
||
offsetBaseMediaDecodeTime = 0;
|
||
}
|
||
offsetBaseMediaDecodeTime = offsetBaseMediaDecodeTime + Math.round(audioPrimingOffset * timescale);
|
||
offsetBaseMediaDecodeTime = Math.max(offsetBaseMediaDecodeTime, 0);
|
||
MuxerHelper.writeUint32(tfdt, 4, offsetBaseMediaDecodeTime);
|
||
}
|
||
else {
|
||
const baseMediaDecodeTime = MuxerHelper.readUint32(tfdt, 4);
|
||
if (baseMediaDecodeTime > MAX_UPPER_32_VALUE) {
|
||
getLogger().error(loggerName$c, `baseMediaDecodeTime larger than can be represented by float for upper 32 bits ${baseMediaDecodeTime}`);
|
||
}
|
||
let offsetBaseMediaDecodeTime = baseMediaDecodeTime;
|
||
offsetBaseMediaDecodeTime *= Math.pow(2, 32);
|
||
offsetBaseMediaDecodeTime += MuxerHelper.readUint32(tfdt, 8);
|
||
offsetBaseMediaDecodeTime = offsetBaseMediaDecodeTime - Math.round((timeOffsetTs.baseTime * timescale) / timeOffsetTs.timescale);
|
||
// need to make sure video doesn't go negative after timestampOffset is applied
|
||
if (mediaType === 'video' && offsetBaseMediaDecodeTime < 0) {
|
||
getLogger().warn(loggerName$c, `video tdft would have gone negative by ${offsetBaseMediaDecodeTime / timescale} seconds`);
|
||
offsetBaseMediaDecodeTime = 0;
|
||
}
|
||
offsetBaseMediaDecodeTime = offsetBaseMediaDecodeTime + Math.round(audioPrimingOffset * timescale);
|
||
offsetBaseMediaDecodeTime = Math.max(offsetBaseMediaDecodeTime, 0);
|
||
const upper = Math.floor(offsetBaseMediaDecodeTime / (UINT32_MAX + 1));
|
||
const lower = Math.floor(offsetBaseMediaDecodeTime % (UINT32_MAX + 1));
|
||
MuxerHelper.writeUint32(tfdt, 4, upper);
|
||
MuxerHelper.writeUint32(tfdt, 8, lower);
|
||
}
|
||
});
|
||
});
|
||
});
|
||
}
|
||
static writeStartDTS(initData, fragmentData, startDTS) {
|
||
MuxerHelper.findBox(fragmentData, ['moof', 'traf']).map(function (traf) {
|
||
return MuxerHelper.findBox(traf, ['tfhd']).map(function (tfhd) {
|
||
// get the track id from the tfhd
|
||
const id = MuxerHelper.readUint32(tfhd, 4);
|
||
const parsedTrak = initData.tracksById[id];
|
||
if (!parsedTrak) {
|
||
return;
|
||
}
|
||
// assume a 90kHz clock if no timescale was specified
|
||
const timescale = parsedTrak.timescale || 90000;
|
||
// everything is in decimal seconds, so we need round timestamps or they'll get truncated and lose accuracy
|
||
const roundedStartDTS = Math.round(startDTS * timescale) / timescale;
|
||
if (Math.abs(roundedStartDTS - startDTS) > 0.01) {
|
||
// the rounding errors should be very small since we snap durations to the timescale
|
||
getLogger().warn(loggerName$c, `[iframes] large rounding error when adjusting timestamps, startDTS: ${startDTS}, roundedStartDTS: ${roundedStartDTS}`);
|
||
}
|
||
// get the base media decode time from the tfdt
|
||
MuxerHelper.findBox(traf, ['tfdt']).map(function (tfdt) {
|
||
const version = tfdt[0];
|
||
if (version === 0) {
|
||
MuxerHelper.writeUint32(tfdt, 4, roundedStartDTS * timescale);
|
||
}
|
||
else {
|
||
let baseMediaDecodeTime = roundedStartDTS * timescale;
|
||
baseMediaDecodeTime = Math.max(baseMediaDecodeTime, 0);
|
||
const upper = Math.floor(baseMediaDecodeTime / (UINT32_MAX + 1));
|
||
const lower = Math.floor(baseMediaDecodeTime % (UINT32_MAX + 1));
|
||
MuxerHelper.writeUint32(tfdt, 4, upper);
|
||
MuxerHelper.writeUint32(tfdt, 8, lower);
|
||
}
|
||
});
|
||
});
|
||
});
|
||
}
|
||
static parseSAIO(saio) {
|
||
let subsampleOffset = 0;
|
||
let offset = 0;
|
||
const version = saio[0];
|
||
const flags = MuxerHelper.readUint32(saio, 0) & 16777215;
|
||
offset += 4;
|
||
if ((flags & 1) !== 0) {
|
||
offset += 8;
|
||
}
|
||
const entryCount = MuxerHelper.readUint32(saio, offset) & 16777215;
|
||
if (entryCount === 1) {
|
||
offset += 4;
|
||
subsampleOffset = MuxerHelper.readUint32(saio, offset);
|
||
if (version === 1) {
|
||
offset += 4;
|
||
subsampleOffset *= Math.pow(2, 32);
|
||
subsampleOffset += MuxerHelper.readUint32(saio, offset);
|
||
}
|
||
}
|
||
else {
|
||
getLogger().error(loggerName$c, `saio entry count error, count is: ${entryCount}`);
|
||
}
|
||
return subsampleOffset;
|
||
}
|
||
static parseSAIZ(saiz) {
|
||
let sampleSize = 0;
|
||
let offset = 0;
|
||
const flags = MuxerHelper.readUint32(saiz, 0) & 16777215;
|
||
offset += 4;
|
||
if ((flags & 1) !== 0) {
|
||
offset += 8;
|
||
}
|
||
sampleSize = saiz[offset];
|
||
offset++;
|
||
// let sampleCount = MP4Demuxer.readUint32(saiz, offset);
|
||
offset += 4; // just get the first sample
|
||
if (sampleSize === 0) {
|
||
sampleSize = saiz[offset];
|
||
}
|
||
return sampleSize;
|
||
}
|
||
static parseSubsample(perSampleIVSize, entry) {
|
||
const sampleAuxInfo = { subsamples: [] };
|
||
let offset = 0;
|
||
if (perSampleIVSize) {
|
||
sampleAuxInfo.iv = entry.subarray(0, perSampleIVSize);
|
||
offset += perSampleIVSize;
|
||
}
|
||
offset += 2; // skip entry length
|
||
// need at least 6 bytes to read a subsample entry
|
||
while (offset + 6 <= entry.byteLength) {
|
||
const bytesClear = MuxerHelper.readUint16(entry, offset);
|
||
offset += 2;
|
||
const bytesEncrypted = MuxerHelper.readUint32(entry, offset);
|
||
offset += 4;
|
||
sampleAuxInfo.subsamples.push([bytesClear, bytesEncrypted]);
|
||
}
|
||
return sampleAuxInfo;
|
||
}
|
||
static isSEIMessage(isHEVCFlavor, naluType) {
|
||
return isHEVCFlavor ? naluType === 39 || naluType === 40 : naluType === 6;
|
||
}
|
||
static parseCLCPSample(fragment, startSampleOffset, sampleSize) {
|
||
let sampleIndex = 0;
|
||
const samples = [];
|
||
let totalSize = 0;
|
||
while (sampleIndex < sampleSize) {
|
||
let sampleOffset = startSampleOffset + sampleIndex;
|
||
const cdatSize = MuxerHelper.readUint32(fragment, sampleOffset); // get actual cdat size
|
||
sampleOffset += 4;
|
||
const cdatType = MuxerHelper.bin2str(fragment.subarray(sampleOffset, sampleOffset + 4));
|
||
sampleOffset += 4;
|
||
if (cdatType === 'cdat') {
|
||
if (cdatSize !== sampleSize) {
|
||
getLogger().debug(loggerName$c, `clcp track: cdatSize ${cdatSize} @ offset ${sampleOffset}; sampleSize ${sampleSize}`);
|
||
}
|
||
const actualDataSize = cdatSize - 8;
|
||
const rawData = fragment.subarray(sampleOffset, sampleOffset + actualDataSize);
|
||
totalSize += actualDataSize;
|
||
samples.push(rawData);
|
||
sampleIndex += cdatSize; // actualDataSize + 8
|
||
}
|
||
else {
|
||
getLogger().debug(loggerName$c, `clcp track: unknown atom type ${cdatType}`);
|
||
break;
|
||
}
|
||
}
|
||
return { cdatList: samples, cdatTotalSize: totalSize };
|
||
}
|
||
// TODOs:
|
||
// * Parse cdt2 data
|
||
// * Handle 64-bit length in tfhd > baseDataOffset
|
||
static parseSamples(startDTS, fragment, track, iframeDuration, useSEICaptions, maxSamples) {
|
||
const timescale = track.timescale;
|
||
const trackId = track.id;
|
||
let pts = startDTS;
|
||
let parsedSampleCount = 0;
|
||
let sampleAuxInfo;
|
||
let isHEVCFlavor = false;
|
||
const moofs = MuxerHelper.findBoxWithOffset(fragment, 0, ['moof']);
|
||
moofs.map(function (moofTuple) {
|
||
const moof = moofTuple.data;
|
||
const moofOffset = moofTuple.offset;
|
||
const trafs = MuxerHelper.findBox(moof, ['traf']);
|
||
trafs.map(function (traf) {
|
||
// get the base media decode time from the tfdt
|
||
const baseTime = MuxerHelper.findBox(traf, ['tfdt']).map(function (tfdt) {
|
||
let result;
|
||
const version = tfdt[0];
|
||
result = MuxerHelper.readUint32(tfdt, 4);
|
||
if (version === 1) {
|
||
result *= Math.pow(2, 32);
|
||
result += MuxerHelper.readUint32(tfdt, 8);
|
||
}
|
||
return result / timescale;
|
||
})[0];
|
||
if (baseTime !== undefined) {
|
||
pts = baseTime;
|
||
}
|
||
return MuxerHelper.findBox(traf, ['tfhd']).map(function (tfhd) {
|
||
// tfhd default values
|
||
const id = MuxerHelper.readUint32(tfhd, 4);
|
||
const tfhdFlags = MuxerHelper.readUint32(tfhd, 0) & 16777215;
|
||
const baseDataOffsetPresent = (tfhdFlags & 1) !== 0;
|
||
const sampleDescriptionIndexPresent = (tfhdFlags & 2) !== 0;
|
||
const defaultSampleDurationPresent = (tfhdFlags & 8) !== 0;
|
||
let defaultSampleDuration = 0;
|
||
const defaultSampleSizePresent = (tfhdFlags & 16) !== 0;
|
||
let defaultSampleSize = 0;
|
||
const defaultSampleFlagsPresent = (tfhdFlags & 32) !== 0;
|
||
// let durationIsEmpty = (tfhdFlags & 0x010000) !== 0;
|
||
// let defaultBaseIsMoof = (tfhdFlags & 0x020000) !== 0;
|
||
let tfhdOffset = 8;
|
||
if (isFiniteNumber(track.defaultSampleSize)) {
|
||
defaultSampleSize = track.defaultSampleSize;
|
||
}
|
||
if (id === trackId) {
|
||
if (baseDataOffsetPresent) {
|
||
MuxerHelper.readUint32(tfhd, tfhdOffset); // Should be 64-bit
|
||
tfhdOffset += 4;
|
||
const mustBeZeroes = MuxerHelper.readUint32(tfhd, tfhdOffset);
|
||
tfhdOffset += 4;
|
||
if (mustBeZeroes !== 0) {
|
||
getLogger().info(loggerName$c, 'tfhd baseDataOffset has 64-bit value. Caption should be turned off');
|
||
}
|
||
}
|
||
if (sampleDescriptionIndexPresent) {
|
||
MuxerHelper.readUint32(tfhd, tfhdOffset);
|
||
tfhdOffset += 4;
|
||
}
|
||
if (defaultSampleDurationPresent) {
|
||
defaultSampleDuration = MuxerHelper.readUint32(tfhd, tfhdOffset);
|
||
tfhdOffset += 4;
|
||
}
|
||
if (defaultSampleSizePresent) {
|
||
defaultSampleSize = MuxerHelper.readUint32(tfhd, tfhdOffset);
|
||
tfhdOffset += 4;
|
||
}
|
||
if (defaultSampleFlagsPresent) {
|
||
MuxerHelper.readUint32(tfhd, tfhdOffset);
|
||
tfhdOffset += 4;
|
||
}
|
||
if (track.type === 'video') {
|
||
let subsampleOffset = 0, subsampleSize = 0;
|
||
// only use the first one
|
||
MuxerHelper.findBox(traf, ['saio']).map(function (saio) {
|
||
subsampleOffset = MP4Demuxer.parseSAIO(saio);
|
||
});
|
||
MuxerHelper.findBox(traf, ['saiz']).map(function (saiz) {
|
||
subsampleSize = MP4Demuxer.parseSAIZ(saiz);
|
||
});
|
||
if (subsampleOffset && subsampleSize) {
|
||
sampleAuxInfo = MP4Demuxer.parseSubsample(track.defaultPerSampleIVSize, fragment.subarray(subsampleOffset, subsampleOffset + subsampleSize));
|
||
}
|
||
isHEVCFlavor = MP4Demuxer.isHEVCFlavor(track.codec);
|
||
}
|
||
MuxerHelper.findBox(traf, ['trun']).map(function (trun) {
|
||
// trun specific values
|
||
const version = trun[0];
|
||
const flags = MuxerHelper.readUint32(trun, 0) & 16777215;
|
||
const dataOffsetPresent = (flags & 1) !== 0;
|
||
let dataOffset = 0;
|
||
const firstSampleFlagsPresent = (flags & 4) !== 0;
|
||
const sampleDurationPresent = (flags & 256) !== 0;
|
||
let sampleDuration = 0;
|
||
const sampleSizePresent = (flags & 512) !== 0;
|
||
let sampleSize = 0;
|
||
const sampleFlagsPresent = (flags & 1024) !== 0;
|
||
const sampleCompositionOffsetsPresent = (flags & 2048) !== 0;
|
||
let compositionOffset = 0;
|
||
const sampleCount = MuxerHelper.readUint32(trun, 4);
|
||
let trunOffset = 8; // past version, flags, and sample count
|
||
if (dataOffsetPresent) {
|
||
dataOffset = MuxerHelper.readUint32(trun, trunOffset);
|
||
trunOffset += 4;
|
||
}
|
||
if (firstSampleFlagsPresent) {
|
||
trunOffset += 4;
|
||
}
|
||
let sampleOffset = dataOffset + moofOffset;
|
||
for (let ix = 0; ix < sampleCount && (maxSamples < 0 || parsedSampleCount < maxSamples); ix++) {
|
||
if (sampleDurationPresent) {
|
||
sampleDuration = MuxerHelper.readUint32(trun, trunOffset);
|
||
trunOffset += 4;
|
||
}
|
||
else {
|
||
sampleDuration = defaultSampleDuration;
|
||
}
|
||
if (sampleSizePresent) {
|
||
sampleSize = MuxerHelper.readUint32(trun, trunOffset);
|
||
trunOffset += 4;
|
||
}
|
||
else {
|
||
sampleSize = defaultSampleSize;
|
||
}
|
||
if (sampleFlagsPresent) {
|
||
trunOffset += 4;
|
||
}
|
||
if (sampleCompositionOffsetsPresent) {
|
||
if (version === 0) {
|
||
compositionOffset = MuxerHelper.readUint32(trun, trunOffset);
|
||
}
|
||
else {
|
||
compositionOffset = MuxerHelper.readSint32(trun, trunOffset);
|
||
}
|
||
trunOffset += 4;
|
||
}
|
||
// TODO: make this more generic
|
||
if (track.type === 'video') {
|
||
if (!isFiniteNumber(iframeDuration)) {
|
||
// non iframe
|
||
track.fragmentDuration += sampleDuration;
|
||
if (useSEICaptions) {
|
||
let naluTotalSize = 0;
|
||
// hijacking sei parsing to get video duration
|
||
while (naluTotalSize < sampleSize) {
|
||
const naluSize = MuxerHelper.readUint32(fragment, sampleOffset);
|
||
sampleOffset += 4;
|
||
const naluType = fragment[sampleOffset] & 31;
|
||
if (!track.seiSamples) {
|
||
track.seiSamples = [];
|
||
}
|
||
if (MP4Demuxer.isSEIMessage(isHEVCFlavor, naluType)) {
|
||
const sampleData = fragment.subarray(sampleOffset, sampleOffset + naluSize);
|
||
track.seiSamples.push({ pts: pts + compositionOffset / timescale, type: naluType, data: sampleData, sampleOffset: sampleOffset, naluSize: naluSize });
|
||
}
|
||
sampleOffset += naluSize;
|
||
naluTotalSize += naluSize + 4;
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
// iframe
|
||
track.samples.push({
|
||
data: fragment.subarray(sampleOffset, sampleOffset + sampleSize),
|
||
size: sampleSize,
|
||
duration: iframeDuration * timescale,
|
||
cts: 0,
|
||
flags: {
|
||
isLeading: 0,
|
||
isDependedOn: 0,
|
||
hasRedundancy: 0,
|
||
degradPrio: 0,
|
||
dependsOn: 2,
|
||
isNonSync: 0,
|
||
paddingValue: 0,
|
||
},
|
||
subsamples: sampleAuxInfo ? sampleAuxInfo.subsamples : [],
|
||
iv: sampleAuxInfo ? sampleAuxInfo.iv : undefined,
|
||
});
|
||
}
|
||
}
|
||
else if (track.type === 'audio') {
|
||
track.fragmentDuration += sampleDuration;
|
||
}
|
||
else if (track.type === 'caption') {
|
||
const { cdatList, cdatTotalSize } = MP4Demuxer.parseCLCPSample(fragment, sampleOffset, sampleSize);
|
||
sampleOffset += sampleSize;
|
||
if (cdatList.length) {
|
||
let cdatArray;
|
||
if (cdatList.length === 1) {
|
||
cdatArray = new Uint8Array(cdatList[0]);
|
||
}
|
||
else if (cdatList.length > 1) {
|
||
let offset = 0;
|
||
cdatArray = new Uint8Array(cdatTotalSize);
|
||
for (const arr of cdatList) {
|
||
cdatArray.set(arr, offset);
|
||
offset += arr.length;
|
||
}
|
||
}
|
||
track.captionSamples.push({ type: 3, pts: pts, bytes: cdatArray });
|
||
}
|
||
}
|
||
parsedSampleCount++;
|
||
pts += sampleDuration / timescale;
|
||
} // for ix
|
||
}); // trun map
|
||
} // if (id == trackId)
|
||
}); // tfhd map
|
||
}); // traf map
|
||
}); // moof map
|
||
}
|
||
static parseEmsg(data) {
|
||
const version = data[0];
|
||
let schemeIdUri = '';
|
||
let value = '';
|
||
let timeScale;
|
||
let presentationTimeDelta;
|
||
let presentationTime;
|
||
let eventDuration;
|
||
let id;
|
||
let offset = 0;
|
||
if (version === 0) {
|
||
while (MuxerHelper.bin2str(data.subarray(offset, offset + 1)) !== '\0') {
|
||
schemeIdUri += MuxerHelper.bin2str(data.subarray(offset, offset + 1));
|
||
offset += 1;
|
||
}
|
||
schemeIdUri += MuxerHelper.bin2str(data.subarray(offset, offset + 1));
|
||
offset += 1;
|
||
while (MuxerHelper.bin2str(data.subarray(offset, offset + 1)) !== '\0') {
|
||
value += MuxerHelper.bin2str(data.subarray(offset, offset + 1));
|
||
offset += 1;
|
||
}
|
||
value += MuxerHelper.bin2str(data.subarray(offset, offset + 1));
|
||
offset += 1;
|
||
timeScale = MuxerHelper.readUint32(data, 12);
|
||
presentationTimeDelta = MuxerHelper.readUint32(data, 16);
|
||
eventDuration = MuxerHelper.readUint32(data, 20);
|
||
id = MuxerHelper.readUint32(data, 24);
|
||
offset = 28;
|
||
}
|
||
else {
|
||
offset += 4;
|
||
timeScale = MuxerHelper.readUint32(data, offset);
|
||
offset += 4;
|
||
const leftPresentationTime = MuxerHelper.readUint32(data, offset);
|
||
offset += 4;
|
||
const rightPresentationTime = MuxerHelper.readUint32(data, offset);
|
||
offset += 4;
|
||
presentationTime = Math.pow(2, 32) * leftPresentationTime + rightPresentationTime;
|
||
if (!Number.isSafeInteger(presentationTime)) {
|
||
presentationTime = Number.MAX_SAFE_INTEGER;
|
||
getLogger().warn(loggerName$c, 'Presentation time exceeds safe integer limit and wrapped to max safe integer in parsing emsg box');
|
||
}
|
||
eventDuration = MuxerHelper.readUint32(data, offset);
|
||
offset += 4;
|
||
id = MuxerHelper.readUint32(data, offset);
|
||
offset += 4;
|
||
while (MuxerHelper.bin2str(data.subarray(offset, offset + 1)) !== '\0') {
|
||
schemeIdUri += MuxerHelper.bin2str(data.subarray(offset, offset + 1));
|
||
offset += 1;
|
||
}
|
||
schemeIdUri += MuxerHelper.bin2str(data.subarray(offset, offset + 1));
|
||
offset += 1;
|
||
while (MuxerHelper.bin2str(data.subarray(offset, offset + 1)) !== '\0') {
|
||
value += MuxerHelper.bin2str(data.subarray(offset, offset + 1));
|
||
offset += 1;
|
||
}
|
||
value += MuxerHelper.bin2str(data.subarray(offset, offset + 1));
|
||
offset += 1;
|
||
}
|
||
const payload = data.subarray(offset, data.byteLength);
|
||
return { schemeIdUri, value, timeScale, presentationTime, presentationTimeDelta, eventDuration, id, payload };
|
||
}
|
||
static extractID3PayloadCreateID3Track(emsgInfo, timeOffset, id3Track, logger) {
|
||
const id3 = new ID3$1(emsgInfo.payload, logger);
|
||
const data = new Uint8Array(emsgInfo.payload);
|
||
const length = data.byteLength;
|
||
let frameIndex = 0;
|
||
let offset = 0;
|
||
const pts = isFiniteNumber(emsgInfo.presentationTime) ? emsgInfo.presentationTime / emsgInfo.timeScale : timeOffset + emsgInfo.presentationTimeDelta / emsgInfo.timeScale;
|
||
if (!isFiniteNumber(pts)) {
|
||
getLogger().error(loggerName$c, 'No pts found in emsg info when extracting ID3 payload');
|
||
return;
|
||
}
|
||
const frameDuration = emsgInfo.eventDuration;
|
||
const header = data.subarray(0, 10);
|
||
const fileIdentifier = MuxerHelper.bin2str(header.subarray(offset, offset + 3));
|
||
offset += 3;
|
||
if (fileIdentifier !== 'ID3') {
|
||
getLogger().error(loggerName$c, 'No ID3 tag found when extracting ID3 payload');
|
||
}
|
||
// skip versions
|
||
offset += 2;
|
||
const flags = data.subarray(offset, offset + 1);
|
||
const extendedHeader = flags[0] & 64;
|
||
const footerPresent = flags[0] & 16;
|
||
offset += 1;
|
||
ID3$1.readSynchSafeUint32(data.subarray(offset, offset + 4));
|
||
offset += 4;
|
||
if (extendedHeader) {
|
||
const extHeaderSize = ID3$1.readSynchSafeUint32(data.subarray(offset, offset + 4));
|
||
offset += 4;
|
||
offset += extHeaderSize;
|
||
}
|
||
while (offset + 2 < length) {
|
||
let frameId = '';
|
||
frameId += MuxerHelper.bin2str(data.subarray(offset, offset + 4));
|
||
offset += 4;
|
||
const frameLength = ID3$1.readSynchSafeUint32(data.subarray(offset, offset + 4));
|
||
offset += 4;
|
||
const stamp = pts + frameIndex * frameDuration;
|
||
const id3Sample = { data: data, pts: stamp, dts: stamp, keyTagInfo: undefined, frames: id3.frames };
|
||
id3Track.id3Samples.push(id3Sample);
|
||
offset += frameLength;
|
||
frameIndex++;
|
||
if (footerPresent) {
|
||
const di3 = MuxerHelper.bin2str(data.subarray(offset, offset + 3));
|
||
if (di3 !== 'DI3') {
|
||
getLogger().error(loggerName$c, 'End should be DI3 if footer present in extracting ID3 payload');
|
||
}
|
||
offset += 3;
|
||
// skip remaining 7 bits
|
||
offset += 7;
|
||
}
|
||
}
|
||
if (offset + 2 === length) {
|
||
const padding = MuxerHelper.readUint16(data, offset);
|
||
if (padding !== 0) {
|
||
getLogger().warn(loggerName$c, 'Padding should be 0 when extracting ID3 payload');
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* @param data
|
||
* @param timeOffset The position in the media element corresponding to this fragment at rate 1
|
||
* @param contiguous Contiguous with previously appeneded fragment
|
||
* @param keyTagInfo Object describing information about the key that was used to encrypt data
|
||
* @param iframeMediaStart If remuxing for iframe, the time in media element to remux fragment to
|
||
* @param iframeDuration If remuxing for iframe, the duration to remux fragment to
|
||
*/
|
||
append(data, timeOffset, contiguous, accurateTimeOffset, keyTagInfo, iframeMediaStart, iframeDuration) {
|
||
let initData = this.initData;
|
||
let videoDuration = 0, audioDuration = 0;
|
||
let hasVideo = false, hasAudio = false;
|
||
if (typeof initData === 'undefined') {
|
||
this.resetInitSegment(data, 0);
|
||
initData = this.initData;
|
||
}
|
||
let initPtsTs = this.initPtsTs;
|
||
let startDtsTs;
|
||
if (!initPtsTs) {
|
||
startDtsTs = MP4Demuxer.getStartDtsTs(initData, data);
|
||
this.initPtsTs = initPtsTs = { baseTime: startDtsTs.baseTime - Math.round(timeOffset * startDtsTs.timescale), timescale: startDtsTs.timescale };
|
||
this.observer.trigger(DemuxerEvent.INIT_PTS_FOUND, { initPTS: initPtsTs });
|
||
}
|
||
if (initData.foundLargeTimescale && MP4Demuxer.has32BitTfdts(data) && !iframeDuration) {
|
||
data = MP4EncryptionRemuxer.remuxOverflowSegment(data, this.logger);
|
||
}
|
||
// Don't remux start DTS, will be adjusted in source buffer timestampOffset instead
|
||
startDtsTs = MP4Demuxer.getStartDtsTs(initData, data);
|
||
const emsgs = MuxerHelper.findBox(data, ['emsg']);
|
||
if (initData.video && initData.video.encrypted) {
|
||
// Check for 'senc' or 'saiz' && 'saio'
|
||
const trafs = MuxerHelper.findBox(data, ['moof', 'traf']);
|
||
const gotSubsamples = trafs.find(function (traf) {
|
||
return Boolean(MuxerHelper.findBox(traf, ['senc'])[0] || (MuxerHelper.findBox(traf, ['saiz'])[0] && MuxerHelper.findBox(traf, ['saio'])[0]));
|
||
});
|
||
if (!gotSubsamples) {
|
||
this.logger.warn(loggerName$c, `Missing subsample information for encrypted content codec=${initData.videoCodec}`);
|
||
}
|
||
}
|
||
if (iframeDuration) {
|
||
const timescale = this._videoTrack.timescale;
|
||
// snap the iframe durations so added the durations will result in timestamps that are always accurately representable when converted to uints
|
||
// we want to use ceil so durations aren't reduced, otherwise the same iframe could get chosen in the next round.
|
||
iframeDuration = Math.ceil(iframeDuration * timescale) / timescale;
|
||
// iframe timestamps are written directly, not offset, so we have to add the audioPrimingDelay since it'll
|
||
// still be shifted by the sourceBuffer.timestampOffset
|
||
const startDtsSec = (startDtsTs.baseTime + this.audioPrimingDelay * startDtsTs.timescale) / startDtsTs.timescale;
|
||
if (this._videoTrack && this._audioTrack && !this._silentAudioTrack) {
|
||
const silentTrackBase = SilentAudio.getTrack(this.observer, this._audioTrack.id, this._audioTrack.codec, 2, this.logger);
|
||
if (!silentTrackBase) {
|
||
throw `unable to create silent audio track for codec ${this._audioTrack.codec}`;
|
||
}
|
||
this._silentAudioTrack = Object.assign(Object.assign({}, silentTrackBase), { sequenceNumber: 0 });
|
||
const iframeInitSegment = MP4$1.initSegment([this._videoTrack, this._silentAudioTrack]);
|
||
this.remuxedInitDataTrack = { type: 'audiovideo', container: 'video/mp4', codec: this._silentAudioTrack.config.codec + ',' + this._videoTrack.codec, initSegment: iframeInitSegment };
|
||
const data = { track: this.remuxedInitDataTrack };
|
||
this.observer.trigger(DemuxerEvent.FRAG_PARSING_INIT_SEGMENT, data);
|
||
}
|
||
MP4Demuxer.parseSamples(startDtsSec, data, this._videoTrack, iframeDuration, false, 1);
|
||
this.mp4Remuxer.remuxIFrame(startDtsSec, this._videoTrack, this._silentAudioTrack, iframeDuration, this.remuxedInitDataTrack);
|
||
this._videoTrack.samples = [];
|
||
}
|
||
else {
|
||
let startDtsSec = (startDtsTs.baseTime - this.audioPrimingDelay * startDtsTs.timescale) / startDtsTs.timescale;
|
||
startDtsSec = Math.max(0, startDtsSec);
|
||
if (emsgs && emsgs.length > 0) {
|
||
const emsgsInfos = emsgs.map((emsg) => {
|
||
const emsgInfo = MP4Demuxer.parseEmsg(emsg);
|
||
// HLSJS only processes id3 tags in emsgs
|
||
if (emsgInfo.schemeIdUri === 'https://aomedia.org/emsg/ID3\0' && !this._id3Track) {
|
||
this._id3Track = { id3Samples: [], inputTimescale: 90000 };
|
||
}
|
||
return emsgInfo;
|
||
});
|
||
if (this._id3Track) {
|
||
emsgsInfos.map((emsgInfo) => {
|
||
MP4Demuxer.extractID3PayloadCreateID3Track(emsgInfo, timeOffset, this._id3Track, this.logger);
|
||
});
|
||
}
|
||
}
|
||
if (this._videoTrack) {
|
||
MP4Demuxer.parseSamples(startDtsSec, data, this._videoTrack, undefined, this.trySEICaptions, -1);
|
||
videoDuration = this._videoTrack.fragmentDuration / this._videoTrack.timescale;
|
||
hasVideo = true;
|
||
this._videoTrack.fragmentDuration = 0;
|
||
this.logger.info(loggerName$c, `frag video duration: ${videoDuration}`);
|
||
if (this.trySEICaptions) {
|
||
MP4Demuxer.extractSEICaptionsFromNALu(this._videoTrack.seiSamples, this._captionTrack);
|
||
this._videoTrack.seiSamples = [];
|
||
this.logger.info(loggerName$c, `sei samples: ${this._captionTrack.captionSamples.length}`);
|
||
}
|
||
else if (this._captionTrack) {
|
||
// clcp text track
|
||
MP4Demuxer.parseSamples(startDtsSec, data, this._captionTrack, undefined, false, Number.MAX_SAFE_INTEGER);
|
||
}
|
||
}
|
||
if (this._audioTrack) {
|
||
MP4Demuxer.parseSamples(startDtsSec, data, this._audioTrack, undefined, false, -1);
|
||
audioDuration = this._audioTrack.fragmentDuration / this._audioTrack.timescale;
|
||
hasAudio = true;
|
||
this._audioTrack.fragmentDuration = 0;
|
||
}
|
||
this.mp4Remuxer.remuxEmsgAndRawData(audioDuration, hasAudio, videoDuration, hasVideo, this._captionTrack, this._id3Track, startDtsSec, startDtsTs.timescale, data, this.remuxedInitDataTrack);
|
||
if (this._id3Track) {
|
||
this._id3Track.id3Samples = [];
|
||
}
|
||
}
|
||
}
|
||
static extractSEICaptionsFromNALu(units, textTrack) {
|
||
if (!units) {
|
||
return;
|
||
}
|
||
for (let u = 0; u < units.length; ++u) {
|
||
const unit = units[u];
|
||
const pts = unit.pts;
|
||
let seiPtr = 0;
|
||
seiPtr++;
|
||
let payloadType = 0;
|
||
let payloadSize = 0;
|
||
let endOfCaptions = false;
|
||
let b = 0;
|
||
while (!endOfCaptions && seiPtr < unit.data.length) {
|
||
payloadType = 0;
|
||
do {
|
||
if (seiPtr >= unit.data.length) {
|
||
break;
|
||
}
|
||
b = unit.data[seiPtr++];
|
||
payloadType += b;
|
||
} while (b === 255);
|
||
// Parse payload size.
|
||
payloadSize = 0;
|
||
do {
|
||
if (seiPtr >= unit.data.length) {
|
||
break;
|
||
}
|
||
b = unit.data[seiPtr++];
|
||
payloadSize += b;
|
||
} while (b === 255);
|
||
const leftOver = unit.data.length - seiPtr;
|
||
if (payloadType === 4 && seiPtr < unit.data.length) {
|
||
endOfCaptions = true;
|
||
const countryCode = unit.data[seiPtr++];
|
||
if (countryCode === 181) {
|
||
const providerCode = MuxerHelper.readUint16(unit.data, seiPtr);
|
||
seiPtr += 2;
|
||
if (providerCode === 49) {
|
||
const userStructure = MuxerHelper.readUint32(unit.data, seiPtr);
|
||
seiPtr += 4;
|
||
if (userStructure === 1195456820) {
|
||
const userDataType = unit.data[seiPtr++];
|
||
// Raw CEA-608 bytes wrapped in CEA-708 packet
|
||
if (userDataType === 3) {
|
||
const firstByte = unit.data[seiPtr++];
|
||
seiPtr++; // skip second byte
|
||
const totalCCs = 31 & firstByte;
|
||
const enabled = 64 & firstByte;
|
||
const byteArray = [];
|
||
if (enabled) {
|
||
for (let i = 0; i < totalCCs; i++) {
|
||
const ccData = unit.data[seiPtr++];
|
||
const ccDataChecked = ccData & 252;
|
||
if (ccData === ccDataChecked) {
|
||
// valid ccData
|
||
const ccType = ccData & 3;
|
||
if (0 === ccType || 1 === ccType) {
|
||
// Exclude CEA708 CC data.
|
||
const b1 = unit.data[seiPtr++];
|
||
const b2 = unit.data[seiPtr++];
|
||
byteArray.push(b1);
|
||
byteArray.push(b2);
|
||
}
|
||
}
|
||
else {
|
||
seiPtr += 2; // Skip byte pair too
|
||
}
|
||
}
|
||
}
|
||
if (byteArray.length > 0) {
|
||
textTrack.captionSamples.push({ type: 3, pts: pts, bytes: byteArray });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else if (payloadSize < leftOver) {
|
||
seiPtr += payloadSize;
|
||
}
|
||
else if (payloadSize > leftOver) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return textTrack;
|
||
}
|
||
}
|
||
|
||
|
||
const loggerName$b = { name: 'ExpGolomb' };
|
||
class ExpGolomb {
|
||
constructor(data, logger) {
|
||
this.data = data;
|
||
this.logger = logger;
|
||
// the number of bytes left to examine in this.data
|
||
this._bytesAvailable = data.byteLength;
|
||
// the current word being examined
|
||
this.word = 0; // :uint
|
||
// the number of bits left to examine in the current word
|
||
this.bitsAvailable = 0; // :uint
|
||
}
|
||
get bytesAvailable() {
|
||
return this._bytesAvailable;
|
||
}
|
||
loadWord() {
|
||
const data = this.data, bytesAvailable = this._bytesAvailable, position = data.byteLength - bytesAvailable, workingBytes = new Uint8Array(4), availableBytes = Math.min(4, bytesAvailable);
|
||
if (availableBytes === 0) {
|
||
throw new Error('no bytes available');
|
||
}
|
||
workingBytes.set(data.subarray(position, position + availableBytes));
|
||
this.word = new DataView(workingBytes.buffer).getUint32(0);
|
||
// track the amount of this.data that has been processed
|
||
this.bitsAvailable = availableBytes * 8;
|
||
this._bytesAvailable -= availableBytes;
|
||
}
|
||
skipBits(count) {
|
||
let skipBytes; // :int
|
||
if (this.bitsAvailable > count) {
|
||
this.word <<= count;
|
||
this.bitsAvailable -= count;
|
||
}
|
||
else {
|
||
count -= this.bitsAvailable;
|
||
skipBytes = count >> 3;
|
||
count -= skipBytes >> 3;
|
||
this._bytesAvailable -= skipBytes;
|
||
this.loadWord();
|
||
this.word <<= count;
|
||
this.bitsAvailable -= count;
|
||
}
|
||
}
|
||
readBits(size) {
|
||
let bits = Math.min(this.bitsAvailable, size); // :uint
|
||
const valu = this.word >>> (32 - bits); // :uint
|
||
if (size > 32) {
|
||
this.logger.error(loggerName$b, 'Cannot read more than 32 bits at a time');
|
||
}
|
||
this.bitsAvailable -= bits;
|
||
if (this.bitsAvailable > 0) {
|
||
this.word <<= bits;
|
||
}
|
||
else if (this._bytesAvailable > 0) {
|
||
this.loadWord();
|
||
}
|
||
bits = size - bits;
|
||
if (bits > 0 && this.bitsAvailable) {
|
||
return (valu << bits) | this.readBits(bits);
|
||
}
|
||
else {
|
||
return valu;
|
||
}
|
||
}
|
||
// ():uint
|
||
skipLZ() {
|
||
let leadingZeroCount; // :uint
|
||
for (leadingZeroCount = 0; leadingZeroCount < this.bitsAvailable; ++leadingZeroCount) {
|
||
if (0 !== (this.word & (2147483648 >>> leadingZeroCount))) {
|
||
// the first bit of working word is 1
|
||
this.word <<= leadingZeroCount;
|
||
this.bitsAvailable -= leadingZeroCount;
|
||
return leadingZeroCount;
|
||
}
|
||
}
|
||
// we exhausted word and still have not found a 1
|
||
this.loadWord();
|
||
return leadingZeroCount + this.skipLZ();
|
||
}
|
||
// ():void
|
||
skipUEG() {
|
||
this.skipBits(1 + this.skipLZ());
|
||
}
|
||
// ():void
|
||
skipEG() {
|
||
this.skipBits(1 + this.skipLZ());
|
||
}
|
||
// ():uint
|
||
readUEG() {
|
||
const clz = this.skipLZ(); // :uint
|
||
return this.readBits(clz + 1) - 1;
|
||
}
|
||
// ():int
|
||
readEG() {
|
||
const valu = this.readUEG(); // :int
|
||
if (1 & valu) {
|
||
// the number is odd if the low order bit is set
|
||
return (1 + valu) >>> 1; // add 1 to make it even, and divide by 2
|
||
}
|
||
else {
|
||
return -1 * (valu >>> 1); // divide by two then make it negative
|
||
}
|
||
}
|
||
// Some convenience functions
|
||
// :Boolean
|
||
readBoolean() {
|
||
return 1 === this.readBits(1);
|
||
}
|
||
// ():int
|
||
readUByte() {
|
||
return this.readBits(8);
|
||
}
|
||
// ():int
|
||
readUShort() {
|
||
return this.readBits(16);
|
||
}
|
||
// ():int
|
||
readUInt() {
|
||
return this.readBits(32);
|
||
}
|
||
/**
|
||
* Advance the ExpGolomb decoder past a scaling list. The scaling
|
||
* list is optionally transmitted as part of a sequence parameter
|
||
* set and is not relevant to transmuxing.
|
||
* @param count {number} the number of entries in this scaling list
|
||
* @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1
|
||
*/
|
||
skipScalingList(count) {
|
||
let lastScale = 8, nextScale = 8, j, deltaScale;
|
||
for (j = 0; j < count; j++) {
|
||
if (nextScale !== 0) {
|
||
deltaScale = this.readEG();
|
||
nextScale = (lastScale + deltaScale + 256) % 256;
|
||
}
|
||
lastScale = nextScale === 0 ? lastScale : nextScale;
|
||
}
|
||
}
|
||
/**
|
||
* Read a sequence parameter set and return some interesting video
|
||
* properties. A sequence parameter set is the H264 metadata that
|
||
* describes the properties of upcoming video frames.
|
||
* @param data {Uint8Array} the bytes of a sequence parameter set
|
||
* @return {object} an object with configuration parsed from the
|
||
* sequence parameter set, including the dimensions of the
|
||
* associated video frames.
|
||
*/
|
||
readSPS() {
|
||
let frameCropLeftOffset = 0, frameCropRightOffset = 0, frameCropTopOffset = 0, frameCropBottomOffset = 0, numRefFramesInPicOrderCntCycle, scalingListCount, i;
|
||
const readUByte = this.readUByte.bind(this), readBits = this.readBits.bind(this), readUEG = this.readUEG.bind(this), readBoolean = this.readBoolean.bind(this), skipBits = this.skipBits.bind(this), skipEG = this.skipEG.bind(this), skipUEG = this.skipUEG.bind(this), skipScalingList = this.skipScalingList.bind(this);
|
||
readUByte();
|
||
const profileIdc = readUByte(); // profile_idc
|
||
readBits(5); // constraint_set[0-4]_flag, u(5)
|
||
skipBits(3); // reserved_zero_3bits u(3),
|
||
readUByte(); // level_idc u(8)
|
||
skipUEG(); // seq_parameter_set_id
|
||
// some profiles have more optional data we don't need
|
||
if (profileIdc === 100 ||
|
||
profileIdc === 110 ||
|
||
profileIdc === 122 ||
|
||
profileIdc === 244 ||
|
||
profileIdc === 44 ||
|
||
profileIdc === 83 ||
|
||
profileIdc === 86 ||
|
||
profileIdc === 118 ||
|
||
profileIdc === 128) {
|
||
const chromaFormatIdc = readUEG();
|
||
if (chromaFormatIdc === 3) {
|
||
skipBits(1); // separate_colour_plane_flag
|
||
}
|
||
skipUEG(); // bit_depth_luma_minus8
|
||
skipUEG(); // bit_depth_chroma_minus8
|
||
skipBits(1); // qpprime_y_zero_transform_bypass_flag
|
||
if (readBoolean()) {
|
||
// seq_scaling_matrix_present_flag
|
||
scalingListCount = chromaFormatIdc !== 3 ? 8 : 12;
|
||
for (i = 0; i < scalingListCount; i++) {
|
||
if (readBoolean()) {
|
||
// seq_scaling_list_present_flag[ i ]
|
||
if (i < 6) {
|
||
skipScalingList(16);
|
||
}
|
||
else {
|
||
skipScalingList(64);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
skipUEG(); // log2_max_frame_num_minus4
|
||
const picOrderCntType = readUEG();
|
||
if (picOrderCntType === 0) {
|
||
readUEG(); // log2_max_pic_order_cnt_lsb_minus4
|
||
}
|
||
else if (picOrderCntType === 1) {
|
||
skipBits(1); // delta_pic_order_always_zero_flag
|
||
skipEG(); // offset_for_non_ref_pic
|
||
skipEG(); // offset_for_top_to_bottom_field
|
||
numRefFramesInPicOrderCntCycle = readUEG();
|
||
for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) {
|
||
skipEG(); // offset_for_ref_frame[ i ]
|
||
}
|
||
}
|
||
skipUEG(); // max_num_ref_frames
|
||
skipBits(1); // gaps_in_frame_num_value_allowed_flag
|
||
const picWidthInMbsMinus1 = readUEG();
|
||
const picHeightInMapUnitsMinus1 = readUEG();
|
||
const frameMbsOnlyFlag = readBits(1);
|
||
if (frameMbsOnlyFlag === 0) {
|
||
skipBits(1); // mb_adaptive_frame_field_flag
|
||
}
|
||
skipBits(1); // direct_8x8_inference_flag
|
||
if (readBoolean()) {
|
||
// frame_cropping_flag
|
||
frameCropLeftOffset = readUEG();
|
||
frameCropRightOffset = readUEG();
|
||
frameCropTopOffset = readUEG();
|
||
frameCropBottomOffset = readUEG();
|
||
}
|
||
let pixelRatio = [1, 1];
|
||
if (readBoolean()) {
|
||
// vui_parameters_present_flag
|
||
if (readBoolean()) {
|
||
// aspect_ratio_info_present_flag
|
||
const aspectRatioIdc = readUByte();
|
||
switch (aspectRatioIdc) {
|
||
case 1:
|
||
pixelRatio = [1, 1];
|
||
break;
|
||
case 2:
|
||
pixelRatio = [12, 11];
|
||
break;
|
||
case 3:
|
||
pixelRatio = [10, 11];
|
||
break;
|
||
case 4:
|
||
pixelRatio = [16, 11];
|
||
break;
|
||
case 5:
|
||
pixelRatio = [40, 33];
|
||
break;
|
||
case 6:
|
||
pixelRatio = [24, 11];
|
||
break;
|
||
case 7:
|
||
pixelRatio = [20, 11];
|
||
break;
|
||
case 8:
|
||
pixelRatio = [32, 11];
|
||
break;
|
||
case 9:
|
||
pixelRatio = [80, 33];
|
||
break;
|
||
case 10:
|
||
pixelRatio = [18, 11];
|
||
break;
|
||
case 11:
|
||
pixelRatio = [15, 11];
|
||
break;
|
||
case 12:
|
||
pixelRatio = [64, 33];
|
||
break;
|
||
case 13:
|
||
pixelRatio = [160, 99];
|
||
break;
|
||
case 14:
|
||
pixelRatio = [4, 3];
|
||
break;
|
||
case 15:
|
||
pixelRatio = [3, 2];
|
||
break;
|
||
case 16:
|
||
pixelRatio = [2, 1];
|
||
break;
|
||
case 255: {
|
||
pixelRatio = [(readUByte() << 8) | readUByte(), (readUByte() << 8) | readUByte()];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return {
|
||
width: Math.ceil((picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2),
|
||
height: (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - (frameMbsOnlyFlag ? 2 : 4) * (frameCropTopOffset + frameCropBottomOffset),
|
||
pixelRatio: pixelRatio,
|
||
};
|
||
}
|
||
readSliceType() {
|
||
// skip NALu type
|
||
this.readUByte();
|
||
// discard first_mb_in_slice
|
||
this.readUEG();
|
||
// return slice_type
|
||
return this.readUEG();
|
||
}
|
||
}
|
||
|
||
|
||
const TIMESCALE = 90000;
|
||
const loggerName$a = { name: 'TS Demuxer' };
|
||
function isCompleteVideoConfig(config) {
|
||
const is = typeof config.codec === 'string' &&
|
||
Array.isArray(config.sps) &&
|
||
Array.isArray(config.pps) &&
|
||
typeof config.width === 'number' &&
|
||
typeof config.height === 'number' &&
|
||
Array.isArray(config.pixelRatio);
|
||
return is;
|
||
}
|
||
class TsDemuxer extends EsDemuxer {
|
||
constructor(observer, remuxer, config, typeSupported, logger) {
|
||
super(observer, remuxer, config, typeSupported, logger);
|
||
}
|
||
static probe(data, logger) {
|
||
// a TS fragment should contain at least 3 TS packets, a PAT, a PMT, and one PID, each starting with 0x47
|
||
if (data.length >= 564 && data[0] === 71 && data[188] === 71 && data[376] === 71) {
|
||
return true;
|
||
}
|
||
else {
|
||
return false;
|
||
}
|
||
}
|
||
resetInitSegment(initSegment, duration, keyTagInfo) {
|
||
this.pmtParsed = false;
|
||
this._pmtId = -1;
|
||
const baseInfo = { id: -1, inputTimescale: TIMESCALE, timescale: NaN, duration: 0, encrypted: keyTagInfo && keyTagInfo.isEncrypted, keyTagInfo };
|
||
const baseParsingInfo = { len: 0, sequenceNumber: 0 };
|
||
this._avcContext = { info: Object.assign({}, baseInfo), parsingData: Object.assign(Object.assign({}, baseParsingInfo), { esSamples: new Array(), dropped: 0 }), config: {}, container: 'video/mp2t', type: 'video' };
|
||
this._audioContext = { info: Object.assign({}, baseInfo), parsingData: Object.assign(Object.assign({}, baseParsingInfo), { esSamples: new Array() }), container: 'video/mp2t', type: 'audio' };
|
||
this._id3Track = { id: -1, inputTimescale: TIMESCALE, id3Samples: [] };
|
||
this._txtTrack = { inputTimescale: TIMESCALE, captionSamples: [] };
|
||
this._duration = duration;
|
||
this._initSegment = initSegment;
|
||
}
|
||
// feed incoming data to the front of the parsing pipeline
|
||
append(data, timeOffset, contiguous, accurateTimeOffset, keyTagInfo, iframeMediaStart, iframeDuration) {
|
||
let start, stt, pid, atf, offset, pes, unknownPIDs = false;
|
||
this.contiguous = contiguous;
|
||
const avcContext = this._avcContext, audioContext = this._audioContext, id3Track = this._id3Track;
|
||
let pmtParsed = this.pmtParsed, avcId = avcContext.info.id, audioId = audioContext.info.id, id3Id = id3Track.id, pmtId = this._pmtId, avcData = avcContext.pesData, audioData = audioContext.pesData, id3Data = id3Track.pesData;
|
||
this.iframeMode = typeof iframeDuration !== 'undefined';
|
||
if (this._initSegment && this._initSegment.byteLength > 0) {
|
||
// if there's an init segment then it needs to be prepended to the first data
|
||
const newData = new Uint8Array(this._initSegment.byteLength + data.byteLength);
|
||
newData.set(this._initSegment);
|
||
newData.set(data, this._initSegment.byteLength);
|
||
this._initSegment = undefined;
|
||
// make sure its still valid TS
|
||
if (newData[0] === 71) {
|
||
data = newData;
|
||
}
|
||
}
|
||
let len = data.length;
|
||
// don't parse last TS packet if incomplete
|
||
len -= len % 188;
|
||
// loop through TS packets
|
||
for (start = 0; start < len; start += 188) {
|
||
if (data[start] === 71) {
|
||
// payload unit start indicator - pes immediately follows the ts header
|
||
stt = !!(data[start + 1] & 64);
|
||
// pid is a 13-bit field starting at the last bit of TS[1]
|
||
pid = ((data[start + 1] & 31) << 8) + data[start + 2];
|
||
atf = (data[start + 3] & 48) >> 4;
|
||
// if an adaption field is present, its length is specified by the fifth byte of the TS packet header.
|
||
if (atf > 1) {
|
||
offset = start + 5 + data[start + 4];
|
||
// continue if there is only adaptation field
|
||
if (offset === start + 188) {
|
||
continue;
|
||
}
|
||
}
|
||
else {
|
||
offset = start + 4;
|
||
}
|
||
switch (pid) {
|
||
case avcId:
|
||
if (stt) {
|
||
if (avcData && (pes = this._parsePES(avcData))) {
|
||
// new pes is starting and we have an current one, therefore the current pes is complete, so lets parse it
|
||
this._parseAVCPES(pes, false);
|
||
}
|
||
// reset the current pes data
|
||
avcData = { data: [], size: 0, keyTagInfo: keyTagInfo };
|
||
}
|
||
if (avcData) {
|
||
avcData.data.push(data.subarray(offset, start + 188));
|
||
avcData.size += start + 188 - offset;
|
||
}
|
||
break;
|
||
case audioId:
|
||
if (stt && !this.iframeMode) {
|
||
if (audioData && (pes = this._parsePES(audioData))) {
|
||
switch (audioContext.segmentCodec) {
|
||
case 'aac':
|
||
this._parseAACPES(pes);
|
||
break;
|
||
case 'mp3':
|
||
this._parseMPEGPES(pes);
|
||
break;
|
||
case 'ac3':
|
||
case 'ec3':
|
||
this._parseDolbyPES(pes);
|
||
break;
|
||
}
|
||
}
|
||
audioData = { data: [], size: 0, keyTagInfo: keyTagInfo };
|
||
}
|
||
if (audioData) {
|
||
audioData.data.push(data.subarray(offset, start + 188));
|
||
audioData.size += start + 188 - offset;
|
||
}
|
||
break;
|
||
case id3Id:
|
||
if (stt) {
|
||
if (id3Data && (pes = this._parsePES(id3Data))) {
|
||
id3Track.id3Samples.push(pes);
|
||
}
|
||
id3Data = { data: [], size: 0 };
|
||
}
|
||
if (id3Data) {
|
||
id3Data.data.push(data.subarray(offset, start + 188));
|
||
id3Data.size += start + 188 - offset;
|
||
}
|
||
break;
|
||
case 0:
|
||
if (stt) {
|
||
offset += data[offset] + 1;
|
||
}
|
||
pmtId = this._pmtId = this._parsePAT(data, offset);
|
||
break;
|
||
case pmtId:
|
||
if (stt) {
|
||
offset += data[offset] + 1;
|
||
}
|
||
const parsedPIDs = this._parsePMT(data, offset, this.typeSupported);
|
||
// only update track id if track PID found while parsing PMT
|
||
// this is to avoid resetting the PID to -1 in case
|
||
// track PID transiently disappears from the stream
|
||
// this could happen in case of transient missing audio samples for example
|
||
avcId = parsedPIDs.avcId;
|
||
if (avcId > 0) {
|
||
avcContext.info.id = avcId;
|
||
avcContext.info.encrypted = parsedPIDs.videoEncrypted;
|
||
}
|
||
audioId = parsedPIDs.audioId;
|
||
if (audioId > 0) {
|
||
audioContext.info.id = audioId;
|
||
audioContext.segmentCodec = parsedPIDs.audioSegmentCodec;
|
||
audioContext.info.encrypted = parsedPIDs.audioEncrypted;
|
||
}
|
||
id3Id = parsedPIDs.id3Id;
|
||
if (id3Id > 0) {
|
||
id3Track.id = id3Id;
|
||
}
|
||
if (unknownPIDs && !pmtParsed) {
|
||
this.logger.info(loggerName$a, 'reparse from beginning');
|
||
unknownPIDs = false;
|
||
// we set it to -188, the += 188 in the for loop will reset start to 0
|
||
start = -188;
|
||
}
|
||
pmtParsed = this.pmtParsed = true;
|
||
break;
|
||
case 17: // eslint-disable-line
|
||
case 8191:
|
||
break;
|
||
default:
|
||
unknownPIDs = true;
|
||
break;
|
||
}
|
||
}
|
||
else {
|
||
const payload = new FragParsingError(false, 'TS packet did not start with 0x47', ErrorResponses.NoTSSyncByteFound);
|
||
this.observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return;
|
||
}
|
||
}
|
||
// try to parse last PES packets
|
||
if (avcData && (pes = this._parsePES(avcData))) {
|
||
this._parseAVCPES(pes, true);
|
||
avcContext.pesData = undefined;
|
||
}
|
||
else {
|
||
// either avcData null or PES truncated, keep it for next frag parsing
|
||
avcContext.pesData = avcData;
|
||
}
|
||
if (audioData && (pes = this._parsePES(audioData))) {
|
||
switch (audioContext.segmentCodec) {
|
||
case 'aac':
|
||
this._parseAACPES(pes);
|
||
break;
|
||
case 'mp3':
|
||
this._parseMPEGPES(pes);
|
||
break;
|
||
case 'ac3':
|
||
case 'ec3':
|
||
this._parseDolbyPES(pes);
|
||
break;
|
||
}
|
||
audioContext.pesData = undefined;
|
||
}
|
||
else {
|
||
if (audioData && audioData.size) {
|
||
this.logger.warn(loggerName$a, 'last AAC PES packet truncated,might overlap between fragments');
|
||
}
|
||
// either audioData null or PES truncated, keep it for next frag parsing
|
||
audioContext.pesData = audioData;
|
||
}
|
||
if (id3Data && (pes = this._parsePES(id3Data))) {
|
||
id3Track.id3Samples.push(pes);
|
||
id3Track.pesData = undefined;
|
||
}
|
||
else {
|
||
// either id3Data null or PES truncated, keep it for next frag parsing
|
||
id3Track.pesData = id3Data;
|
||
}
|
||
let audioTrack;
|
||
if (audioContext.config && audioContext.segmentCodec) {
|
||
audioTrack = { type: 'audio', info: audioContext.info, config: audioContext.config, parsingData: audioContext.parsingData };
|
||
}
|
||
let videoTrack;
|
||
const pc = avcContext.config;
|
||
if (isCompleteVideoConfig(pc)) {
|
||
videoTrack = { type: 'video', info: avcContext.info, config: pc, parsingData: avcContext.parsingData };
|
||
}
|
||
this.esRemuxer.remuxEsTracks(audioTrack, videoTrack, id3Track, this._txtTrack, timeOffset, contiguous, accurateTimeOffset, keyTagInfo, iframeMediaStart, iframeDuration);
|
||
}
|
||
destroy() {
|
||
this._duration = 0;
|
||
}
|
||
_parsePAT(data, offset) {
|
||
// skip the PSI header and parse the first PMT entry
|
||
return ((data[offset + 10] & 31) << 8) | data[offset + 11];
|
||
}
|
||
_parsePMT(data, offset, typeSupported) {
|
||
let pid; // result = { audio : -1, avc : -1, id3 : -1, segmentCodec : 'aac', videoEncrypted : false, audioEncrypted :false };
|
||
const result = {
|
||
audioId: -1,
|
||
avcId: -1,
|
||
id3Id: -1,
|
||
audioEncrypted: false,
|
||
videoEncrypted: false,
|
||
};
|
||
const sectionLength = ((data[offset + 1] & 15) << 8) | data[offset + 2];
|
||
const tableEnd = offset + 3 + sectionLength - 4;
|
||
// to determine where the table is, we have to figure out how
|
||
// long the program info descriptors are
|
||
const programInfoLength = ((data[offset + 10] & 15) << 8) | data[offset + 11];
|
||
// advance the offset to the first entry in the mapping table
|
||
offset += 12 + programInfoLength;
|
||
while (offset < tableEnd) {
|
||
pid = ((data[offset + 1] & 31) << 8) | data[offset + 2];
|
||
switch (data[offset]) {
|
||
case 207:
|
||
result.audioEncrypted = true;
|
||
/* falls through */
|
||
// ISO/IEC 13818-7 ADTS AAC (MPEG-2 lower bit-rate audio)
|
||
case 15:
|
||
// logger.info('AAC PID:' + pid);
|
||
if (result.audioId === -1) {
|
||
result.audioId = pid;
|
||
result.audioSegmentCodec = 'aac';
|
||
}
|
||
break;
|
||
// Packetized metadata (ID3)
|
||
case 21:
|
||
// logger.info('ID3 PID:' + pid);
|
||
if (result.id3Id === -1) {
|
||
result.id3Id = pid;
|
||
}
|
||
break;
|
||
case 219:
|
||
result.videoEncrypted = true;
|
||
/* falls through */
|
||
// ITU-T Rec. H.264 and ISO/IEC 14496-10 (lower bit-rate video)
|
||
case 27:
|
||
// logger.info('AVC PID:' + pid);
|
||
if (result.avcId === -1) {
|
||
result.avcId = pid;
|
||
}
|
||
break;
|
||
// ISO/IEC 11172-3 (MPEG-1 audio)
|
||
// or ISO/IEC 13818-3 (MPEG-2 halved sample rate audio)
|
||
case 3:
|
||
case 4:
|
||
// logger.info('MPEG PID:' + pid);
|
||
if (typeSupported.mpeg !== true && typeSupported.mp3 !== true) {
|
||
this.logger.warn(loggerName$a, 'MPEG audio found, not supported in this browser for now');
|
||
}
|
||
else if (result.audioId === -1) {
|
||
result.audioId = pid;
|
||
result.audioSegmentCodec = 'mp3';
|
||
}
|
||
break;
|
||
case 193:
|
||
result.audioEncrypted = true;
|
||
this.logger.info(loggerName$a, 'TS encrypted AC-3 audio found');
|
||
/* falls through */
|
||
case 129:
|
||
if (typeSupported.ac3 !== true) {
|
||
this.logger.warn(loggerName$a, 'AC-3 audio found, not supported in this browser for now');
|
||
}
|
||
else if (result.audioId === -1) {
|
||
result.audioId = pid;
|
||
result.audioSegmentCodec = 'ac3';
|
||
}
|
||
break;
|
||
case 194:
|
||
result.audioEncrypted = true;
|
||
this.logger.info(loggerName$a, 'TS encrypted EC-3 audio found');
|
||
/* falls through */
|
||
case 135:
|
||
if (typeSupported.ec3 !== true) {
|
||
this.logger.warn(loggerName$a, 'EC-3 audio found, not supported in this browser for now');
|
||
}
|
||
else if (result.audioId === -1) {
|
||
result.audioId = pid;
|
||
result.audioSegmentCodec = 'ec3';
|
||
}
|
||
break;
|
||
case 36:
|
||
this.logger.warn(loggerName$a, 'HEVC stream type found, not supported for now');
|
||
break;
|
||
default:
|
||
this.logger.warn(loggerName$a, 'unkown stream type:' + data[offset]);
|
||
break;
|
||
}
|
||
// move to the next table entry
|
||
// skip past the elementary stream descriptors, if present
|
||
offset += (((data[offset + 3] & 15) << 8) | data[offset + 4]) + 5;
|
||
}
|
||
return result;
|
||
}
|
||
_parsePES(stream) {
|
||
let i = 0, frag, pesFlags, pesLen, pesHdrLen, pesData, payloadStartOffset;
|
||
const data = stream.data, keyTagInfo = stream.keyTagInfo;
|
||
let pesPts = NaN, pesDts = NaN;
|
||
// safety check
|
||
if (!stream || stream.size === 0) {
|
||
return undefined;
|
||
}
|
||
// we might need up to 19 bytes to read PES header
|
||
// if first chunk of data is less than 19 bytes, let's merge it with following ones until we get 19 bytes
|
||
// usually only one merge is needed (and this is rare ...)
|
||
while (data[0].length < 19 && data.length > 1) {
|
||
const newData = new Uint8Array(data[0].length + data[1].length);
|
||
newData.set(data[0]);
|
||
newData.set(data[1], data[0].length);
|
||
data[0] = newData;
|
||
data.splice(1, 1);
|
||
}
|
||
// retrieve PTS/DTS from first fragment
|
||
frag = data[0];
|
||
const pesPrefix = (frag[0] << 16) + (frag[1] << 8) + frag[2];
|
||
if (pesPrefix === 1) {
|
||
pesLen = (frag[4] << 8) + frag[5];
|
||
// if PES parsed length is not zero and greater than total received length, stop parsing. PES might be truncated
|
||
// minus 6 : PES header size
|
||
if (pesLen && pesLen > stream.size - 6) {
|
||
return undefined;
|
||
}
|
||
pesFlags = frag[7];
|
||
if (pesFlags & 192) {
|
||
/* PES header described here : http://dvd.sourceforge.net/dvdinfo/pes-hdr.html
|
||
as PTS / DTS is 33 bit we cannot use bitwise operator in JS,
|
||
as Bitwise operators treat their operands as a sequence of 32 bits */
|
||
pesPts =
|
||
(frag[9] & 14) * 536870912 + // 1 << 29
|
||
(frag[10] & 255) * 4194304 + // 1 << 22
|
||
(frag[11] & 254) * 16384 + // 1 << 14
|
||
(frag[12] & 255) * 128 + // 1 << 7
|
||
(frag[13] & 254) / 2;
|
||
if (pesFlags & 64) {
|
||
pesDts =
|
||
(frag[14] & 14) * 536870912 + // 1 << 29
|
||
(frag[15] & 255) * 4194304 + // 1 << 22
|
||
(frag[16] & 254) * 16384 + // 1 << 14
|
||
(frag[17] & 255) * 128 + // 1 << 7
|
||
(frag[18] & 254) / 2;
|
||
if (pesPts - pesDts > 5400000) {
|
||
this.logger.warn(loggerName$a, `${Math.round((pesPts - pesDts) / 90000)}s delta between PTS and DTS, align them`);
|
||
pesPts = pesDts;
|
||
}
|
||
}
|
||
else {
|
||
pesDts = pesPts;
|
||
}
|
||
}
|
||
pesHdrLen = frag[8];
|
||
// 9 bytes : 6 bytes for PES header + 3 bytes for PES extension
|
||
payloadStartOffset = pesHdrLen + 9;
|
||
stream.size -= payloadStartOffset;
|
||
// reassemble PES packet
|
||
pesData = new Uint8Array(stream.size);
|
||
for (let j = 0, dataLen = data.length; j < dataLen; j++) {
|
||
frag = data[j];
|
||
let len = frag.byteLength;
|
||
if (payloadStartOffset) {
|
||
if (payloadStartOffset > len) {
|
||
// trim full frag if PES header bigger than frag
|
||
payloadStartOffset -= len;
|
||
continue;
|
||
}
|
||
else {
|
||
// trim partial frag if PES header smaller than frag
|
||
frag = frag.subarray(payloadStartOffset);
|
||
len -= payloadStartOffset;
|
||
payloadStartOffset = 0;
|
||
}
|
||
}
|
||
pesData.set(frag, i);
|
||
i += len;
|
||
}
|
||
if (pesLen) {
|
||
// payload size : remove PES header + PES extension
|
||
pesLen -= pesHdrLen + 3;
|
||
}
|
||
return { data: pesData, pts: pesPts, dts: pesDts, len: pesLen, keyTagInfo: keyTagInfo };
|
||
}
|
||
else {
|
||
return undefined;
|
||
}
|
||
}
|
||
pushAccesUnit(avcContext, keyTagInfo) {
|
||
const avcSample = avcContext.avcSample;
|
||
if (avcSample && avcSample.units.length && avcSample.frame) {
|
||
const samples = avcContext.parsingData.esSamples;
|
||
const nbSamples = samples.length;
|
||
if (avcSample.key === true || (avcContext.config.sps && (nbSamples || this.contiguous))) {
|
||
avcSample.id = nbSamples;
|
||
avcSample.keyTagInfo = keyTagInfo;
|
||
if (avcContext.info.encrypted) {
|
||
const units = avcSample.units;
|
||
units.forEach((unit) => {
|
||
if (unit.data.byteLength > 48) {
|
||
switch (unit.type) {
|
||
case 1:
|
||
case 5:
|
||
// need to remove EPBs from encrypted NALs now that they're parsed into units
|
||
unit.data = this.discardEPB(unit.data);
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
if (!nbSamples && !isFinite(avcSample.pts)) {
|
||
// dropping samples, no timestamp found
|
||
avcContext.parsingData.dropped++;
|
||
return;
|
||
}
|
||
samples.push(avcSample);
|
||
}
|
||
if (avcSample && avcSample.debug.length) {
|
||
this.logger.info(loggerName$a, avcSample.pts + '/' + avcSample.dts + ':' + avcSample.debug);
|
||
}
|
||
}
|
||
_parseAVCPES(pes, last) {
|
||
if (!pes.data) {
|
||
throw 'invalid pes data';
|
||
}
|
||
const avcContext = this._avcContext, units = this._parseAVCNALu(pes.data);
|
||
let expGolombDecoder, avcSample = avcContext.avcSample, push, i;
|
||
const keyTagInfo = pes.keyTagInfo;
|
||
// free pes.data to save up some memory
|
||
pes.data = undefined;
|
||
units.forEach((unit) => {
|
||
switch (unit.type) {
|
||
// NDR
|
||
case 1:
|
||
if (avcSample && !this.iframeMode) {
|
||
// TODO: demuxer needs better support for partial nal units
|
||
push = true;
|
||
avcSample.frame = true;
|
||
// retrieve slice type by parsing beginning of NAL unit (follow H264 spec, slice_header definition) to detect keyframe embedded in NDR
|
||
const data = unit.data;
|
||
if (data.length > 4) {
|
||
const sliceType = new ExpGolomb(data, this.logger).readSliceType();
|
||
// 2 : I slice, 4 : SI slice, 7 : I slice, 9: SI slice
|
||
// SI slice : A slice that is coded using intra prediction only and using quantisation of the prediction samples.
|
||
// An SI slice can be coded such that its decoded samples can be constructed identically to an SP slice.
|
||
// I slice: A slice that is not an SI slice that is decoded using intra prediction only.
|
||
// if (sliceType === 2 || sliceType === 7) {
|
||
if (sliceType === 2 || sliceType === 4 || sliceType === 7 || sliceType === 9) {
|
||
avcSample.key = true;
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
// IDR
|
||
case 5:
|
||
push = true;
|
||
if (avcSample) {
|
||
// handle PES not starting with AUD
|
||
if (!avcSample) {
|
||
avcSample = avcContext.avcSample = this._createAVCSample(true, pes.pts, pes.dts, '');
|
||
}
|
||
avcSample.key = true;
|
||
avcSample.frame = true;
|
||
}
|
||
break;
|
||
// SEI
|
||
case 6:
|
||
push = true;
|
||
expGolombDecoder = new ExpGolomb(this.discardEPB(unit.data), this.logger);
|
||
// skip frameType
|
||
expGolombDecoder.readUByte();
|
||
let payloadType = 0;
|
||
let payloadSize = 0;
|
||
let endOfCaptions = false;
|
||
let b = 0;
|
||
while (!endOfCaptions && expGolombDecoder.bytesAvailable > 1) {
|
||
payloadType = 0;
|
||
do {
|
||
b = expGolombDecoder.readUByte();
|
||
payloadType += b;
|
||
} while (b === 255);
|
||
// Parse payload size.
|
||
payloadSize = 0;
|
||
do {
|
||
b = expGolombDecoder.readUByte();
|
||
payloadSize += b;
|
||
} while (b === 255);
|
||
// TODO: there can be more than one payload in an SEI packet...
|
||
// TODO: need to read type and size in a while loop to get them all
|
||
if (payloadType === 4 && expGolombDecoder.bytesAvailable !== 0) {
|
||
endOfCaptions = true;
|
||
const countryCode = expGolombDecoder.readUByte();
|
||
if (countryCode === 181) {
|
||
const providerCode = expGolombDecoder.readUShort();
|
||
if (providerCode === 49) {
|
||
const userStructure = expGolombDecoder.readUInt();
|
||
if (userStructure === 1195456820) {
|
||
const userDataType = expGolombDecoder.readUByte();
|
||
// Raw CEA-608 bytes wrapped in CEA-708 packet
|
||
if (userDataType === 3) {
|
||
const firstByte = expGolombDecoder.readUByte();
|
||
const secondByte = expGolombDecoder.readUByte();
|
||
const totalCCs = 31 & firstByte;
|
||
const byteArray = [firstByte, secondByte];
|
||
for (i = 0; i < totalCCs; i++) {
|
||
// 3 bytes per CC
|
||
byteArray.push(expGolombDecoder.readUByte());
|
||
byteArray.push(expGolombDecoder.readUByte());
|
||
byteArray.push(expGolombDecoder.readUByte());
|
||
}
|
||
this._insertSampleInOrder(this._txtTrack.captionSamples, { type: 3, pts: pes.pts, bytes: byteArray });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else if (payloadSize < expGolombDecoder.bytesAvailable) {
|
||
for (i = 0; i < payloadSize; i++) {
|
||
expGolombDecoder.readUByte();
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
// SPS
|
||
case 7:
|
||
push = true;
|
||
if (!avcContext.config.sps) {
|
||
expGolombDecoder = new ExpGolomb(unit.data, this.logger);
|
||
const config = expGolombDecoder.readSPS();
|
||
avcContext.config.width = config.width;
|
||
avcContext.config.height = config.height;
|
||
avcContext.config.pixelRatio = config.pixelRatio;
|
||
avcContext.config.sps = [unit.data];
|
||
avcContext.info.duration = this._duration;
|
||
const codecarray = unit.data.subarray(1, 4);
|
||
let codecstring = 'avc1.';
|
||
for (i = 0; i < 3; i++) {
|
||
let h = codecarray[i].toString(16);
|
||
if (h.length < 2) {
|
||
h = '0' + h;
|
||
}
|
||
codecstring += h;
|
||
}
|
||
avcContext.config.codec = codecstring;
|
||
}
|
||
break;
|
||
// PPS
|
||
case 8:
|
||
push = true;
|
||
if (!avcContext.config.pps) {
|
||
avcContext.config.pps = [unit.data];
|
||
}
|
||
break;
|
||
// AUD
|
||
case 9:
|
||
push = false;
|
||
if (avcSample) {
|
||
this.pushAccesUnit(avcContext, keyTagInfo);
|
||
}
|
||
avcSample = avcContext.avcSample = this._createAVCSample(false, pes.pts, pes.dts, '');
|
||
break;
|
||
// Filler Data
|
||
case 12:
|
||
push = false;
|
||
break;
|
||
default:
|
||
push = false;
|
||
if (avcSample) {
|
||
avcSample.debug += 'unknown NAL ' + unit.type + ' ';
|
||
}
|
||
break;
|
||
}
|
||
if (avcSample && push) {
|
||
const units = avcSample.units;
|
||
units.push(unit);
|
||
}
|
||
});
|
||
// if last PES packet, push samples
|
||
if (last && avcSample) {
|
||
this.pushAccesUnit(avcContext, keyTagInfo);
|
||
avcContext.avcSample = undefined;
|
||
}
|
||
}
|
||
_createAVCSample(key, pts, dts, debug) {
|
||
return { id: NaN, key: key, pts: pts, dts: dts, units: new Array(), debug: debug };
|
||
}
|
||
_insertSampleInOrder(arr, data) {
|
||
const len = arr.length;
|
||
if (len > 0) {
|
||
if (data.pts >= arr[len - 1].pts) {
|
||
arr.push(data);
|
||
}
|
||
else {
|
||
for (let pos = len - 1; pos >= 0; pos--) {
|
||
if (data.pts < arr[pos].pts) {
|
||
arr.splice(pos, 0, data);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
arr.push(data);
|
||
}
|
||
}
|
||
_getLastNalUnit() {
|
||
const avcContext = this._avcContext;
|
||
let avcSample = avcContext.avcSample, lastUnit;
|
||
// try to fallback to previous sample if current one is empty
|
||
if (!avcSample || avcSample.units.length === 0) {
|
||
const samples = avcContext.parsingData.esSamples;
|
||
avcSample = samples[samples.length - 1];
|
||
}
|
||
if (avcSample) {
|
||
const units = avcSample.units;
|
||
lastUnit = units[units.length - 1];
|
||
}
|
||
return lastUnit;
|
||
}
|
||
_parseAVCNALu(array) {
|
||
const len = array.byteLength;
|
||
let i = 0, value, overflow;
|
||
const avcContext = this._avcContext;
|
||
let state = avcContext.naluState || 0;
|
||
const lastState = state;
|
||
const units = [];
|
||
let unit, unitType, lastUnitStart = -1, lastUnitType;
|
||
if (state === -1) {
|
||
// special use case where we found 3 or 4-byte start codes exactly at the end of previous PES packet
|
||
lastUnitStart = 0;
|
||
// NALu type is value read from offset 0
|
||
lastUnitType = array[0] & 31;
|
||
state = 0;
|
||
i = 1;
|
||
}
|
||
while (i < len) {
|
||
value = array[i++];
|
||
// optimization. state 0 and 1 are the predominant case. let's handle them outside of the switch/case
|
||
if (!state) {
|
||
state = value ? 0 : 1;
|
||
continue;
|
||
}
|
||
if (state === 1) {
|
||
state = value ? 0 : 2;
|
||
continue;
|
||
}
|
||
// here we have state either equal to 2 or 3
|
||
if (!value) {
|
||
state = 3;
|
||
}
|
||
else if (value === 1) {
|
||
if (lastUnitStart >= 0) {
|
||
unit = { data: array.subarray(lastUnitStart, i - state - 1), type: lastUnitType };
|
||
units.push(unit);
|
||
}
|
||
else {
|
||
// lastUnitStart is -1 => this is the first start code found in this PES packet
|
||
// first check if start code delimiter is overlapping between 2 PES packets,
|
||
// ie it started in last packet (lastState not zero)
|
||
// and ended at the beginning of this PES packet (i <= 4 - lastState)
|
||
const lastUnit = this._getLastNalUnit();
|
||
if (lastUnit) {
|
||
if (lastState && i <= 4 - lastState) {
|
||
// start delimiter overlapping between PES packets
|
||
// strip start delimiter bytes from the end of last NAL unit
|
||
// check if lastUnit had a state different from zero
|
||
if (lastUnit.state) {
|
||
// strip last bytes
|
||
lastUnit.data = lastUnit.data.subarray(0, lastUnit.data.byteLength - lastState);
|
||
}
|
||
}
|
||
// If NAL units are not starting right at the beginning of the PES packet, push preceding data into previous NAL unit.
|
||
overflow = i - state - 1;
|
||
if (overflow > 0) {
|
||
const tmp = new Uint8Array(lastUnit.data.byteLength + overflow);
|
||
tmp.set(lastUnit.data, 0);
|
||
tmp.set(array.subarray(0, overflow), lastUnit.data.byteLength);
|
||
lastUnit.data = tmp;
|
||
lastUnit.state = 0;
|
||
}
|
||
}
|
||
}
|
||
// check if we can read unit type
|
||
if (i < len) {
|
||
unitType = array[i] & 31;
|
||
lastUnitStart = i;
|
||
lastUnitType = unitType;
|
||
state = 0;
|
||
}
|
||
else {
|
||
// not enough byte to read unit type. let's read it on next PES parsing
|
||
state = -1;
|
||
}
|
||
}
|
||
else {
|
||
state = 0;
|
||
}
|
||
}
|
||
if (lastUnitStart >= 0 && state >= 0) {
|
||
unit = { data: array.subarray(lastUnitStart, len), type: lastUnitType, state };
|
||
units.push(unit);
|
||
}
|
||
// no NALu found
|
||
if (units.length === 0) {
|
||
// append pes.data to previous NAL unit
|
||
const lastUnit = this._getLastNalUnit();
|
||
if (lastUnit) {
|
||
const tmp = new Uint8Array(lastUnit.data.byteLength + array.byteLength);
|
||
tmp.set(lastUnit.data, 0);
|
||
tmp.set(array, lastUnit.data.byteLength);
|
||
lastUnit.data = tmp;
|
||
}
|
||
}
|
||
avcContext.naluState = state;
|
||
return units;
|
||
}
|
||
/**
|
||
* remove Emulation Prevention bytes from a RBSP
|
||
*/
|
||
discardEPB(data) {
|
||
const length = data.byteLength, EPBPositions = [];
|
||
let i = 1;
|
||
// Find all `Emulation Prevention Bytes`
|
||
while (i < length - 2) {
|
||
if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 3) {
|
||
EPBPositions.push(i + 2);
|
||
i += 2;
|
||
}
|
||
else {
|
||
i++;
|
||
}
|
||
}
|
||
// If no Emulation Prevention Bytes were found just return the original
|
||
// array
|
||
if (EPBPositions.length === 0) {
|
||
return data;
|
||
}
|
||
// Create a new array to hold the NAL unit data
|
||
const newLength = length - EPBPositions.length;
|
||
const newData = new Uint8Array(newLength);
|
||
let sourceIndex = 0;
|
||
for (i = 0; i < newLength; sourceIndex++, i++) {
|
||
if (sourceIndex === EPBPositions[0]) {
|
||
// Skip this byte
|
||
sourceIndex++;
|
||
// Remove this position index
|
||
EPBPositions.shift();
|
||
}
|
||
newData[i] = data[sourceIndex];
|
||
}
|
||
return newData;
|
||
}
|
||
_parseAACPES(pes) {
|
||
const audioContext = this._audioContext, audioLastPTS = audioContext.audioLastPTS, startOffset = 0, keyTagInfo = pes.keyTagInfo;
|
||
let data = pes.data, pts = pes.pts, audioOverFlow = audioContext.audioOverFlow, frameLength, frameIndex, offset, headerLength, stamp, len, aacSample;
|
||
if (audioOverFlow) {
|
||
const tmp = new Uint8Array(audioOverFlow.byteLength + data.byteLength);
|
||
tmp.set(audioOverFlow, 0);
|
||
tmp.set(data, audioOverFlow.byteLength);
|
||
data = tmp;
|
||
}
|
||
// look for ADTS header (0xFFFx)
|
||
for (offset = startOffset, len = data.length; offset < len - 1; offset++) {
|
||
if (data[offset] === 255 && (data[offset + 1] & 240) === 240) {
|
||
break;
|
||
}
|
||
}
|
||
// if ADTS header does not start straight from the beginning of the PES payload, raise an error
|
||
if (offset) {
|
||
let reason, fatal, response;
|
||
if (offset < len - 1) {
|
||
reason = `AAC PES did not start with ADTS header,offset:${offset}`;
|
||
fatal = false;
|
||
response = ErrorResponses.PESDidNotStartWithADTS;
|
||
}
|
||
else {
|
||
reason = 'no ADTS header found in AAC PES';
|
||
fatal = true;
|
||
response = ErrorResponses.NoADTSHeaderInPES;
|
||
}
|
||
this.logger.warn(loggerName$a, `parsing error:${reason}`);
|
||
const payload = new FragParsingError(fatal, reason, response);
|
||
this.observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
if (fatal) {
|
||
return;
|
||
}
|
||
}
|
||
if (!audioContext.config) {
|
||
const config = ADTS.getAudioConfig(this.observer, data, offset, undefined, this.logger);
|
||
if (!config) {
|
||
throw 'unable to parse adts header';
|
||
}
|
||
audioContext.config = config;
|
||
this.logger.info(loggerName$a, `parsed codec:${config.codec},rate:${config.samplerate},nb channel:${config.channelCount}`);
|
||
}
|
||
frameIndex = 0;
|
||
const frameDuration = 92160000 / audioContext.config.samplerate;
|
||
// if last AAC frame is overflowing, we should ensure timestamps are contiguous:
|
||
// first sample PTS should be equal to last sample PTS + frameDuration
|
||
if (audioOverFlow && audioLastPTS) {
|
||
const newPTS = audioLastPTS + frameDuration;
|
||
if (Math.abs(newPTS - pts) > 1) {
|
||
this.logger.info(loggerName$a, `AAC: align PTS for overlapping frames by ${Math.round((newPTS - pts) / 90)}`);
|
||
pts = newPTS;
|
||
}
|
||
}
|
||
while (offset + 5 < len) {
|
||
// The protection skip bit tells us if we have 2 bytes of CRC data at the end of the ADTS header
|
||
headerLength = data[offset + 1] & 1 ? 7 : 9;
|
||
// retrieve frame size
|
||
frameLength = ((data[offset + 3] & 3) << 11) | (data[offset + 4] << 3) | ((data[offset + 5] & 224) >>> 5);
|
||
frameLength -= headerLength;
|
||
if (frameLength > 0 && offset + headerLength + frameLength <= len) {
|
||
stamp = pts + frameIndex * frameDuration;
|
||
aacSample = { unit: data.subarray(offset + headerLength, offset + headerLength + frameLength), pts: stamp, dts: stamp, keyTagInfo: keyTagInfo };
|
||
audioContext.parsingData.esSamples.push(aacSample);
|
||
audioContext.parsingData.len += frameLength;
|
||
offset += frameLength + headerLength;
|
||
frameIndex++;
|
||
// look for ADTS header (0xFFFx)
|
||
for (; offset < len - 1; offset++) {
|
||
if (data[offset] === 255 && (data[offset + 1] & 240) === 240) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
break;
|
||
}
|
||
}
|
||
if (offset < len) {
|
||
audioOverFlow = data.subarray(offset, len);
|
||
}
|
||
else {
|
||
audioOverFlow = undefined;
|
||
}
|
||
audioContext.audioOverFlow = audioOverFlow;
|
||
audioContext.audioLastPTS = stamp;
|
||
}
|
||
_parseMPEGPES(pes) {
|
||
if (this._audioContext.segmentCodec === 'mp3') {
|
||
MpegAudio$1.parse(this._audioContext.parsingData, pes.data, 0, pes.pts, this.logger);
|
||
}
|
||
}
|
||
_parseDolbyPES(pes) {
|
||
const audioContext = this._audioContext;
|
||
let data = pes.data;
|
||
let pts = pes.pts;
|
||
const keyTagInfo = pes.keyTagInfo;
|
||
let frameIndex = 0;
|
||
let offset = 0;
|
||
let audioOverFlow = audioContext.audioOverFlow;
|
||
const audioLastPTS = audioContext.audioLastPTS;
|
||
if (!audioContext.config) {
|
||
let config;
|
||
if (audioContext.segmentCodec === 'ac3') {
|
||
config = Dolby.getAudioConfig(this.observer, data, offset, this.logger);
|
||
}
|
||
else if (audioContext.segmentCodec === 'ec3') {
|
||
config = DDPlus$1.getAudioConfig(this.observer, data, offset, this.logger);
|
||
}
|
||
if (!config) {
|
||
throw 'unable to parse dolby header';
|
||
}
|
||
audioContext.config = config;
|
||
}
|
||
if (audioContext.config.segmentCodec !== 'ac3' && audioContext.config.segmentCodec !== 'ec3') {
|
||
throw 'unexpected config type';
|
||
}
|
||
const frameDuration = (1536 / audioContext.config.samplerate) * audioContext.info.inputTimescale;
|
||
if (audioOverFlow) {
|
||
const tmp = new Uint8Array(audioOverFlow.byteLength + data.byteLength);
|
||
tmp.set(audioOverFlow, 0);
|
||
tmp.set(data, audioOverFlow.byteLength);
|
||
data = tmp;
|
||
}
|
||
const length = data.length;
|
||
if (audioOverFlow && audioLastPTS) {
|
||
const newPTS = audioLastPTS + frameDuration;
|
||
if (Math.abs(newPTS - pts) > 1) {
|
||
pts = newPTS;
|
||
}
|
||
}
|
||
let frameLength = 0;
|
||
while (offset + frameLength <= length) {
|
||
if (data[offset] !== 11 || data[offset + 1] !== 119) {
|
||
const payload = new FragParsingError(true, 'invalid dolby audio magic', ErrorResponses.InvalidDolbyAudioMagic);
|
||
this.observer.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return;
|
||
}
|
||
if (audioContext.segmentCodec === 'ac3') {
|
||
frameLength = Dolby.getFrameLength(this.observer, data, offset);
|
||
}
|
||
else if (audioContext.segmentCodec === 'ec3') {
|
||
frameLength = DDPlus$1.getFrameLength(this.observer, data, offset, this.logger);
|
||
}
|
||
const stamp = pts + frameIndex * frameDuration;
|
||
audioContext.audioLastPTS = stamp;
|
||
const dolbySample = { unit: data.subarray(offset, offset + frameLength), pts: stamp, dts: stamp, keyTagInfo: keyTagInfo };
|
||
audioContext.parsingData.esSamples.push(dolbySample);
|
||
audioContext.info.duration = this._duration;
|
||
audioContext.parsingData.len += frameLength;
|
||
offset += frameLength;
|
||
frameIndex++;
|
||
}
|
||
if (offset < length) {
|
||
audioOverFlow = data.subarray(offset, length);
|
||
}
|
||
else {
|
||
audioOverFlow = undefined;
|
||
}
|
||
audioContext.audioOverFlow = audioOverFlow;
|
||
}
|
||
}
|
||
var TsDemuxer$1 = TsDemuxer;
|
||
|
||
|
||
class DemuxerInline extends Observer {
|
||
constructor(typeSupported, config, vendor, logger) {
|
||
super();
|
||
this.typeSupported = typeSupported;
|
||
this.config = config;
|
||
this.vendor = vendor;
|
||
this.logger = logger;
|
||
}
|
||
destroy() {
|
||
this.removeAllListeners();
|
||
const demuxer = this.demuxer;
|
||
const remuxer = this.remuxer;
|
||
if (demuxer) {
|
||
demuxer.destroy();
|
||
}
|
||
if (remuxer) {
|
||
remuxer.destroy();
|
||
}
|
||
}
|
||
push(data, keyTagInfo, initSegment, timeOffset, discontinuity, trackSwitch, contiguous, duration, accurateTimeOffset, defaultInitPTS, iframeMediaStart, iframeDuration) {
|
||
if (!data) {
|
||
// if data is undefined and this.probeFn exists,
|
||
// all the demuxers will fail probing. We will still end up with the old probeFn.
|
||
// We will push Uint8Array(undefined) to the demuxer that owns this.probeFn, which will be fatal
|
||
return;
|
||
}
|
||
let demuxer = this.demuxer;
|
||
const _data = new Uint8Array(data);
|
||
if (!demuxer ||
|
||
// in case of continuity change, we might switch from content type (AAC container to TS container for example). this is signaled through the discontinuity argument.
|
||
// also a variant switch can also change the content type (TS container to fMP4 contaner for example). this is signaled through the trackSwitch argument.
|
||
// so let's check that current demuxer is still valid
|
||
((discontinuity || trackSwitch) && !this.probeFn(_data, this.logger))) {
|
||
const { typeSupported, config } = this;
|
||
const muxConfig = [
|
||
{ demux: MP4Demuxer, remux: MP4EncryptionRemuxer },
|
||
{ demux: TsDemuxer$1, remux: EsRemuxer },
|
||
{ demux: EC3Demuxer, remux: EsRemuxer },
|
||
{ demux: AC3Demuxer, remux: EsRemuxer },
|
||
{ demux: AACDemuxer, remux: EsRemuxer },
|
||
{ demux: MP3Demuxer, remux: EsRemuxer },
|
||
]; // move MP3Demuxer to last to avoid false positive mp3 detection.
|
||
// probe for content type
|
||
for (const mux of muxConfig) {
|
||
const { probe } = mux.demux;
|
||
if (probe(_data, this.logger)) {
|
||
this.remuxer = new mux.remux(this, config, typeSupported, this.vendor, this.logger);
|
||
demuxer = new mux.demux(this, this.remuxer, config, typeSupported, this.logger);
|
||
this.probeFn = probe;
|
||
break;
|
||
}
|
||
}
|
||
if (!demuxer) {
|
||
const payload = new FragParsingError(true, 'no demux matching with content found', ErrorResponses.DemuxerNotFound);
|
||
this.trigger(HlsEvent$1.INTERNAL_ERROR, payload);
|
||
return;
|
||
}
|
||
this.demuxer = demuxer;
|
||
}
|
||
const remuxer = this.remuxer;
|
||
const keyChange = !this.lastKeyTagInfo || (keyTagInfo && keyTagInfo.method !== 'NONE' && this.lastKeyTagInfo.uri !== keyTagInfo.uri);
|
||
this.lastKeyTagInfo = keyTagInfo;
|
||
if (discontinuity || trackSwitch || keyChange) {
|
||
// resetInitSegment error handling may need to know if we are in a discontinuity or track switch
|
||
demuxer.resetInitSegment(new Uint8Array(initSegment), duration, keyTagInfo, discontinuity);
|
||
remuxer.resetInitSegment();
|
||
}
|
||
if (discontinuity) {
|
||
const pts = defaultInitPTS ? convertTimestampToSeconds(defaultInitPTS) : undefined;
|
||
demuxer.resetTimeStamp(pts);
|
||
remuxer.resetTimeStamp(pts);
|
||
}
|
||
demuxer.append(_data, timeOffset, contiguous, accurateTimeOffset, keyTagInfo, iframeMediaStart, iframeDuration);
|
||
}
|
||
}
|
||
|
||
function generateUniqueID() {
|
||
let id = `${Date.now()}-${Math.random()}`;
|
||
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
|
||
id += `-${performance.now()}`;
|
||
}
|
||
return id;
|
||
}
|
||
|
||
class DemuxRPCServer {
|
||
constructor(rpc, logger) {
|
||
this.rpc = rpc;
|
||
this.logger = logger;
|
||
this.init = (typeSupported, config, vendor) => callback => {
|
||
const demuxSessionID = generateUniqueID();
|
||
const demuxer = (this.demuxers[demuxSessionID] = new DemuxerInline(typeSupported, config, vendor, this.logger));
|
||
[
|
||
DemuxerEvent.INIT_PTS_FOUND,
|
||
DemuxerEvent.FRAG_PARSING_INIT_SEGMENT,
|
||
DemuxerEvent.FRAG_PARSING_DATA,
|
||
DemuxerEvent.FRAG_PARSED,
|
||
HlsEvent$1.INTERNAL_ERROR
|
||
].forEach(event => {
|
||
demuxer.on(event, (data) => this.rpc.invoke('demuxer.event', [demuxSessionID, event, data])(() => { }));
|
||
});
|
||
callback(demuxSessionID);
|
||
};
|
||
this.push = (id, data, keyTagInfo, initSegment, timeOffset, discontinuity, trackSwitch, contiguous, duration, accurateTimeOffset, defaultInitPTS, iframeMediaStart, iframeDuration) => callback => {
|
||
const demuxer = this.demuxers[id];
|
||
if (!demuxer) {
|
||
callback(undefined, `Demuxer with id "${id}" does not exist on push`);
|
||
return;
|
||
}
|
||
demuxer.push(data, keyTagInfo, initSegment, timeOffset, discontinuity, trackSwitch, contiguous, duration, accurateTimeOffset, defaultInitPTS, iframeMediaStart, iframeDuration);
|
||
callback();
|
||
};
|
||
this.destroy = (id) => callback => {
|
||
const demuxer = this.demuxers[id];
|
||
if (!demuxer) {
|
||
this.logger.error(`Demuxer with id "${id}" does not exist on destroy`);
|
||
return;
|
||
}
|
||
demuxer.destroy();
|
||
delete this.demuxers[id];
|
||
callback();
|
||
};
|
||
this.demuxers = {};
|
||
rpc.register('demuxer.init', this.init);
|
||
rpc.register('demuxer.push', this.push);
|
||
rpc.register('demuxer.destroy', this.destroy);
|
||
}
|
||
}
|
||
|
||
// RPCWorkerService has client and server counterparts, where the service server
|
||
// runs in a Web Worker process, while the client remains in the main process.
|
||
class RPCWorkerService {
|
||
constructor(worker) {
|
||
this.worker = worker;
|
||
this.handlers = {};
|
||
this.deferers = {};
|
||
this._messageHandler = (event) => {
|
||
const { type, id, command, args, result, error } = event.data;
|
||
if (type === RPCWorkerMessageType.Invoke) {
|
||
try {
|
||
if (this.handlers[command] == null) {
|
||
throw new Error(`command ${command} not found`);
|
||
}
|
||
this.handlers[command](...args)(this._respond.bind(this, id, command));
|
||
}
|
||
catch (error) {
|
||
this._respond(id, command, null, new Error(`command ${command} not found`));
|
||
}
|
||
}
|
||
else if (type === RPCWorkerMessageType.Result) {
|
||
if (this.deferers[id] == null) {
|
||
return;
|
||
}
|
||
this.deferers[id](result, error);
|
||
delete this.deferers[id];
|
||
}
|
||
};
|
||
worker.addEventListener('message', this._messageHandler);
|
||
}
|
||
register(command, handler) {
|
||
if (this.handlers[command] != null) {
|
||
return false;
|
||
}
|
||
this.handlers[command] = handler;
|
||
}
|
||
unregister(command) {
|
||
if (this.handlers[command] != null) {
|
||
return false;
|
||
}
|
||
delete this.handlers[command];
|
||
}
|
||
invoke(command, args, transfer) {
|
||
return (callback = RPCWorkerService._fallbackCallback) => {
|
||
const id = generateUniqueID();
|
||
this.deferers[id] = callback;
|
||
const message = {
|
||
type: RPCWorkerMessageType.Invoke,
|
||
id,
|
||
command,
|
||
args,
|
||
};
|
||
this._send(message, transfer);
|
||
};
|
||
}
|
||
teardown(done) {
|
||
this.worker.removeEventListener('message', this._messageHandler);
|
||
done();
|
||
}
|
||
_respond(id, command, result, error, transfer) {
|
||
if (error instanceof Error) {
|
||
error = `[${error.name}] ${error.message}\n${error.stack}`;
|
||
}
|
||
const message = {
|
||
type: RPCWorkerMessageType.Result,
|
||
id,
|
||
command,
|
||
result,
|
||
error,
|
||
};
|
||
this._send(message, transfer);
|
||
}
|
||
_send(message, transfer = []) {
|
||
this.worker.postMessage(message, transfer.map((value) => (ArrayBuffer.isView(value) ? value.buffer : value)).filter((value) => value !== undefined));
|
||
}
|
||
}
|
||
RPCWorkerService._fallbackCallback = (result, error) => {
|
||
if (error != null) {
|
||
throw error;
|
||
}
|
||
};
|
||
var RPCWorkerMessageType;
|
||
(function (RPCWorkerMessageType) {
|
||
RPCWorkerMessageType[RPCWorkerMessageType["Invoke"] = 0] = "Invoke";
|
||
RPCWorkerMessageType[RPCWorkerMessageType["Result"] = 1] = "Result";
|
||
})(RPCWorkerMessageType || (RPCWorkerMessageType = {}));
|
||
// Minimal polyfill
|
||
// ArrayBuffer.isView() is relatively new
|
||
// https://caniuse.com/?search=arraybuffer
|
||
if (!ArrayBuffer['isView']) {
|
||
ArrayBuffer.isView = function isView(a) {
|
||
return a !== null && typeof a === 'object' && a['buffer'] instanceof ArrayBuffer;
|
||
};
|
||
}
|
||
|
||
const startWorker = () => {
|
||
const ctx = global$1;
|
||
const rpcService = new RPCWorkerService(ctx);
|
||
const logger = LoggerRPCClient(rpcService);
|
||
new CryptoRPCServer(rpcService, logger);
|
||
new DemuxRPCServer(rpcService, logger);
|
||
logger.info('WebWorker RPCService has started');
|
||
};
|
||
|
||
if (hasUMDWorker() && typeof __IN_WORKER__ !== 'undefined' && __IN_WORKER__) {
|
||
startWorker();
|
||
}
|
||
|
||
/*! *****************************************************************************
|
||
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 */
|
||
|
||
var extendStatics = function(d, b) {
|
||
extendStatics = Object.setPrototypeOf ||
|
||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
|
||
return extendStatics(d, b);
|
||
};
|
||
|
||
function __extends(d, b) {
|
||
if (typeof b !== "function" && b !== null)
|
||
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
|
||
extendStatics(d, b);
|
||
function __() { this.constructor = d; }
|
||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||
}
|
||
|
||
var __assign = function() {
|
||
__assign = Object.assign || function __assign(t) {
|
||
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
||
s = arguments[i];
|
||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
|
||
}
|
||
return t;
|
||
};
|
||
return __assign.apply(this, arguments);
|
||
};
|
||
|
||
function __rest(s, e) {
|
||
var t = {};
|
||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
||
t[p] = s[p];
|
||
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
||
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
||
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
||
t[p[i]] = s[p[i]];
|
||
}
|
||
return t;
|
||
}
|
||
|
||
function __decorate$2(decorators, target, key, desc) {
|
||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||
}
|
||
|
||
function __metadata(metadataKey, metadataValue) {
|
||
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue);
|
||
}
|
||
|
||
function __values(o) {
|
||
var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
|
||
if (m) return m.call(o);
|
||
if (o && typeof o.length === "number") return {
|
||
next: function () {
|
||
if (o && i >= o.length) o = void 0;
|
||
return { value: o && o[i++], done: !o };
|
||
}
|
||
};
|
||
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
|
||
}
|
||
|
||
function __read(o, n) {
|
||
var m = typeof Symbol === "function" && o[Symbol.iterator];
|
||
if (!m) return o;
|
||
var i = m.call(o), r, ar = [], e;
|
||
try {
|
||
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
|
||
}
|
||
catch (error) { e = { error: error }; }
|
||
finally {
|
||
try {
|
||
if (r && !r.done && (m = i["return"])) m.call(i);
|
||
}
|
||
finally { if (e) throw e.error; }
|
||
}
|
||
return ar;
|
||
}
|
||
|
||
/** @deprecated */
|
||
function __spread() {
|
||
for (var ar = [], i = 0; i < arguments.length; i++)
|
||
ar = ar.concat(__read(arguments[i]));
|
||
return ar;
|
||
}
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
function isFunction$1(x) {
|
||
return typeof x === 'function';
|
||
}
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
var _enable_super_gross_mode_that_will_cause_bad_things = false;
|
||
var config = {
|
||
Promise: undefined,
|
||
set useDeprecatedSynchronousErrorHandling(value) {
|
||
_enable_super_gross_mode_that_will_cause_bad_things = value;
|
||
},
|
||
get useDeprecatedSynchronousErrorHandling() {
|
||
return _enable_super_gross_mode_that_will_cause_bad_things;
|
||
},
|
||
};
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
function hostReportError(err) {
|
||
setTimeout(function () { throw err; }, 0);
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _config,_util_hostReportError PURE_IMPORTS_END */
|
||
var empty$1 = {
|
||
closed: true,
|
||
next: function (value) { },
|
||
error: function (err) {
|
||
if (config.useDeprecatedSynchronousErrorHandling) {
|
||
throw err;
|
||
}
|
||
else {
|
||
hostReportError(err);
|
||
}
|
||
},
|
||
complete: function () { }
|
||
};
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
var isArray$1 = /*@__PURE__*/ (function () { return Array.isArray || (function (x) { return x && typeof x.length === 'number'; }); })();
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
function isObject$1(x) {
|
||
return x !== null && typeof x === 'object';
|
||
}
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
var UnsubscriptionErrorImpl = /*@__PURE__*/ (function () {
|
||
function UnsubscriptionErrorImpl(errors) {
|
||
Error.call(this);
|
||
this.message = errors ?
|
||
errors.length + " errors occurred during unsubscription:\n" + errors.map(function (err, i) { return i + 1 + ") " + err.toString(); }).join('\n ') : '';
|
||
this.name = 'UnsubscriptionError';
|
||
this.errors = errors;
|
||
return this;
|
||
}
|
||
UnsubscriptionErrorImpl.prototype = /*@__PURE__*/ Object.create(Error.prototype);
|
||
return UnsubscriptionErrorImpl;
|
||
})();
|
||
var UnsubscriptionError = UnsubscriptionErrorImpl;
|
||
|
||
/** PURE_IMPORTS_START _util_isArray,_util_isObject,_util_isFunction,_util_UnsubscriptionError PURE_IMPORTS_END */
|
||
var Subscription = /*@__PURE__*/ (function () {
|
||
function Subscription(unsubscribe) {
|
||
this.closed = false;
|
||
this._parentOrParents = null;
|
||
this._subscriptions = null;
|
||
if (unsubscribe) {
|
||
this._ctorUnsubscribe = true;
|
||
this._unsubscribe = unsubscribe;
|
||
}
|
||
}
|
||
Subscription.prototype.unsubscribe = function () {
|
||
var errors;
|
||
if (this.closed) {
|
||
return;
|
||
}
|
||
var _a = this, _parentOrParents = _a._parentOrParents, _ctorUnsubscribe = _a._ctorUnsubscribe, _unsubscribe = _a._unsubscribe, _subscriptions = _a._subscriptions;
|
||
this.closed = true;
|
||
this._parentOrParents = null;
|
||
this._subscriptions = null;
|
||
if (_parentOrParents instanceof Subscription) {
|
||
_parentOrParents.remove(this);
|
||
}
|
||
else if (_parentOrParents !== null) {
|
||
for (var index = 0; index < _parentOrParents.length; ++index) {
|
||
var parent_1 = _parentOrParents[index];
|
||
parent_1.remove(this);
|
||
}
|
||
}
|
||
if (isFunction$1(_unsubscribe)) {
|
||
if (_ctorUnsubscribe) {
|
||
this._unsubscribe = undefined;
|
||
}
|
||
try {
|
||
_unsubscribe.call(this);
|
||
}
|
||
catch (e) {
|
||
errors = e instanceof UnsubscriptionError ? flattenUnsubscriptionErrors(e.errors) : [e];
|
||
}
|
||
}
|
||
if (isArray$1(_subscriptions)) {
|
||
var index = -1;
|
||
var len = _subscriptions.length;
|
||
while (++index < len) {
|
||
var sub = _subscriptions[index];
|
||
if (isObject$1(sub)) {
|
||
try {
|
||
sub.unsubscribe();
|
||
}
|
||
catch (e) {
|
||
errors = errors || [];
|
||
if (e instanceof UnsubscriptionError) {
|
||
errors = errors.concat(flattenUnsubscriptionErrors(e.errors));
|
||
}
|
||
else {
|
||
errors.push(e);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (errors) {
|
||
throw new UnsubscriptionError(errors);
|
||
}
|
||
};
|
||
Subscription.prototype.add = function (teardown) {
|
||
var subscription = teardown;
|
||
if (!teardown) {
|
||
return Subscription.EMPTY;
|
||
}
|
||
switch (typeof teardown) {
|
||
case 'function':
|
||
subscription = new Subscription(teardown);
|
||
case 'object':
|
||
if (subscription === this || subscription.closed || typeof subscription.unsubscribe !== 'function') {
|
||
return subscription;
|
||
}
|
||
else if (this.closed) {
|
||
subscription.unsubscribe();
|
||
return subscription;
|
||
}
|
||
else if (!(subscription instanceof Subscription)) {
|
||
var tmp = subscription;
|
||
subscription = new Subscription();
|
||
subscription._subscriptions = [tmp];
|
||
}
|
||
break;
|
||
default: {
|
||
throw new Error('unrecognized teardown ' + teardown + ' added to Subscription.');
|
||
}
|
||
}
|
||
var _parentOrParents = subscription._parentOrParents;
|
||
if (_parentOrParents === null) {
|
||
subscription._parentOrParents = this;
|
||
}
|
||
else if (_parentOrParents instanceof Subscription) {
|
||
if (_parentOrParents === this) {
|
||
return subscription;
|
||
}
|
||
subscription._parentOrParents = [_parentOrParents, this];
|
||
}
|
||
else if (_parentOrParents.indexOf(this) === -1) {
|
||
_parentOrParents.push(this);
|
||
}
|
||
else {
|
||
return subscription;
|
||
}
|
||
var subscriptions = this._subscriptions;
|
||
if (subscriptions === null) {
|
||
this._subscriptions = [subscription];
|
||
}
|
||
else {
|
||
subscriptions.push(subscription);
|
||
}
|
||
return subscription;
|
||
};
|
||
Subscription.prototype.remove = function (subscription) {
|
||
var subscriptions = this._subscriptions;
|
||
if (subscriptions) {
|
||
var subscriptionIndex = subscriptions.indexOf(subscription);
|
||
if (subscriptionIndex !== -1) {
|
||
subscriptions.splice(subscriptionIndex, 1);
|
||
}
|
||
}
|
||
};
|
||
Subscription.EMPTY = (function (empty) {
|
||
empty.closed = true;
|
||
return empty;
|
||
}(new Subscription()));
|
||
return Subscription;
|
||
}());
|
||
function flattenUnsubscriptionErrors(errors) {
|
||
return errors.reduce(function (errs, err) { return errs.concat((err instanceof UnsubscriptionError) ? err.errors : err); }, []);
|
||
}
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
var rxSubscriber = /*@__PURE__*/ (function () {
|
||
return typeof Symbol === 'function'
|
||
? /*@__PURE__*/ Symbol('rxSubscriber')
|
||
: '@@rxSubscriber_' + /*@__PURE__*/ Math.random();
|
||
})();
|
||
|
||
/** PURE_IMPORTS_START tslib,_util_isFunction,_Observer,_Subscription,_internal_symbol_rxSubscriber,_config,_util_hostReportError PURE_IMPORTS_END */
|
||
var Subscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(Subscriber, _super);
|
||
function Subscriber(destinationOrNext, error, complete) {
|
||
var _this = _super.call(this) || this;
|
||
_this.syncErrorValue = null;
|
||
_this.syncErrorThrown = false;
|
||
_this.syncErrorThrowable = false;
|
||
_this.isStopped = false;
|
||
switch (arguments.length) {
|
||
case 0:
|
||
_this.destination = empty$1;
|
||
break;
|
||
case 1:
|
||
if (!destinationOrNext) {
|
||
_this.destination = empty$1;
|
||
break;
|
||
}
|
||
if (typeof destinationOrNext === 'object') {
|
||
if (destinationOrNext instanceof Subscriber) {
|
||
_this.syncErrorThrowable = destinationOrNext.syncErrorThrowable;
|
||
_this.destination = destinationOrNext;
|
||
destinationOrNext.add(_this);
|
||
}
|
||
else {
|
||
_this.syncErrorThrowable = true;
|
||
_this.destination = new SafeSubscriber(_this, destinationOrNext);
|
||
}
|
||
break;
|
||
}
|
||
default:
|
||
_this.syncErrorThrowable = true;
|
||
_this.destination = new SafeSubscriber(_this, destinationOrNext, error, complete);
|
||
break;
|
||
}
|
||
return _this;
|
||
}
|
||
Subscriber.prototype[rxSubscriber] = function () { return this; };
|
||
Subscriber.create = function (next, error, complete) {
|
||
var subscriber = new Subscriber(next, error, complete);
|
||
subscriber.syncErrorThrowable = false;
|
||
return subscriber;
|
||
};
|
||
Subscriber.prototype.next = function (value) {
|
||
if (!this.isStopped) {
|
||
this._next(value);
|
||
}
|
||
};
|
||
Subscriber.prototype.error = function (err) {
|
||
if (!this.isStopped) {
|
||
this.isStopped = true;
|
||
this._error(err);
|
||
}
|
||
};
|
||
Subscriber.prototype.complete = function () {
|
||
if (!this.isStopped) {
|
||
this.isStopped = true;
|
||
this._complete();
|
||
}
|
||
};
|
||
Subscriber.prototype.unsubscribe = function () {
|
||
if (this.closed) {
|
||
return;
|
||
}
|
||
this.isStopped = true;
|
||
_super.prototype.unsubscribe.call(this);
|
||
};
|
||
Subscriber.prototype._next = function (value) {
|
||
this.destination.next(value);
|
||
};
|
||
Subscriber.prototype._error = function (err) {
|
||
this.destination.error(err);
|
||
this.unsubscribe();
|
||
};
|
||
Subscriber.prototype._complete = function () {
|
||
this.destination.complete();
|
||
this.unsubscribe();
|
||
};
|
||
Subscriber.prototype._unsubscribeAndRecycle = function () {
|
||
var _parentOrParents = this._parentOrParents;
|
||
this._parentOrParents = null;
|
||
this.unsubscribe();
|
||
this.closed = false;
|
||
this.isStopped = false;
|
||
this._parentOrParents = _parentOrParents;
|
||
return this;
|
||
};
|
||
return Subscriber;
|
||
}(Subscription));
|
||
var SafeSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(SafeSubscriber, _super);
|
||
function SafeSubscriber(_parentSubscriber, observerOrNext, error, complete) {
|
||
var _this = _super.call(this) || this;
|
||
_this._parentSubscriber = _parentSubscriber;
|
||
var next;
|
||
var context = _this;
|
||
if (isFunction$1(observerOrNext)) {
|
||
next = observerOrNext;
|
||
}
|
||
else if (observerOrNext) {
|
||
next = observerOrNext.next;
|
||
error = observerOrNext.error;
|
||
complete = observerOrNext.complete;
|
||
if (observerOrNext !== empty$1) {
|
||
context = Object.create(observerOrNext);
|
||
if (isFunction$1(context.unsubscribe)) {
|
||
_this.add(context.unsubscribe.bind(context));
|
||
}
|
||
context.unsubscribe = _this.unsubscribe.bind(_this);
|
||
}
|
||
}
|
||
_this._context = context;
|
||
_this._next = next;
|
||
_this._error = error;
|
||
_this._complete = complete;
|
||
return _this;
|
||
}
|
||
SafeSubscriber.prototype.next = function (value) {
|
||
if (!this.isStopped && this._next) {
|
||
var _parentSubscriber = this._parentSubscriber;
|
||
if (!config.useDeprecatedSynchronousErrorHandling || !_parentSubscriber.syncErrorThrowable) {
|
||
this.__tryOrUnsub(this._next, value);
|
||
}
|
||
else if (this.__tryOrSetError(_parentSubscriber, this._next, value)) {
|
||
this.unsubscribe();
|
||
}
|
||
}
|
||
};
|
||
SafeSubscriber.prototype.error = function (err) {
|
||
if (!this.isStopped) {
|
||
var _parentSubscriber = this._parentSubscriber;
|
||
var useDeprecatedSynchronousErrorHandling = config.useDeprecatedSynchronousErrorHandling;
|
||
if (this._error) {
|
||
if (!useDeprecatedSynchronousErrorHandling || !_parentSubscriber.syncErrorThrowable) {
|
||
this.__tryOrUnsub(this._error, err);
|
||
this.unsubscribe();
|
||
}
|
||
else {
|
||
this.__tryOrSetError(_parentSubscriber, this._error, err);
|
||
this.unsubscribe();
|
||
}
|
||
}
|
||
else if (!_parentSubscriber.syncErrorThrowable) {
|
||
this.unsubscribe();
|
||
if (useDeprecatedSynchronousErrorHandling) {
|
||
throw err;
|
||
}
|
||
hostReportError(err);
|
||
}
|
||
else {
|
||
if (useDeprecatedSynchronousErrorHandling) {
|
||
_parentSubscriber.syncErrorValue = err;
|
||
_parentSubscriber.syncErrorThrown = true;
|
||
}
|
||
else {
|
||
hostReportError(err);
|
||
}
|
||
this.unsubscribe();
|
||
}
|
||
}
|
||
};
|
||
SafeSubscriber.prototype.complete = function () {
|
||
var _this = this;
|
||
if (!this.isStopped) {
|
||
var _parentSubscriber = this._parentSubscriber;
|
||
if (this._complete) {
|
||
var wrappedComplete = function () { return _this._complete.call(_this._context); };
|
||
if (!config.useDeprecatedSynchronousErrorHandling || !_parentSubscriber.syncErrorThrowable) {
|
||
this.__tryOrUnsub(wrappedComplete);
|
||
this.unsubscribe();
|
||
}
|
||
else {
|
||
this.__tryOrSetError(_parentSubscriber, wrappedComplete);
|
||
this.unsubscribe();
|
||
}
|
||
}
|
||
else {
|
||
this.unsubscribe();
|
||
}
|
||
}
|
||
};
|
||
SafeSubscriber.prototype.__tryOrUnsub = function (fn, value) {
|
||
try {
|
||
fn.call(this._context, value);
|
||
}
|
||
catch (err) {
|
||
this.unsubscribe();
|
||
if (config.useDeprecatedSynchronousErrorHandling) {
|
||
throw err;
|
||
}
|
||
else {
|
||
hostReportError(err);
|
||
}
|
||
}
|
||
};
|
||
SafeSubscriber.prototype.__tryOrSetError = function (parent, fn, value) {
|
||
if (!config.useDeprecatedSynchronousErrorHandling) {
|
||
throw new Error('bad call');
|
||
}
|
||
try {
|
||
fn.call(this._context, value);
|
||
}
|
||
catch (err) {
|
||
if (config.useDeprecatedSynchronousErrorHandling) {
|
||
parent.syncErrorValue = err;
|
||
parent.syncErrorThrown = true;
|
||
return true;
|
||
}
|
||
else {
|
||
hostReportError(err);
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
};
|
||
SafeSubscriber.prototype._unsubscribe = function () {
|
||
var _parentSubscriber = this._parentSubscriber;
|
||
this._context = null;
|
||
this._parentSubscriber = null;
|
||
_parentSubscriber.unsubscribe();
|
||
};
|
||
return SafeSubscriber;
|
||
}(Subscriber));
|
||
|
||
/** PURE_IMPORTS_START _Subscriber PURE_IMPORTS_END */
|
||
function canReportError(observer) {
|
||
while (observer) {
|
||
var _a = observer, closed_1 = _a.closed, destination = _a.destination, isStopped = _a.isStopped;
|
||
if (closed_1 || isStopped) {
|
||
return false;
|
||
}
|
||
else if (destination && destination instanceof Subscriber) {
|
||
observer = destination;
|
||
}
|
||
else {
|
||
observer = null;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _Subscriber,_symbol_rxSubscriber,_Observer PURE_IMPORTS_END */
|
||
function toSubscriber(nextOrObserver, error, complete) {
|
||
if (nextOrObserver) {
|
||
if (nextOrObserver instanceof Subscriber) {
|
||
return nextOrObserver;
|
||
}
|
||
if (nextOrObserver[rxSubscriber]) {
|
||
return nextOrObserver[rxSubscriber]();
|
||
}
|
||
}
|
||
if (!nextOrObserver && !error && !complete) {
|
||
return new Subscriber(empty$1);
|
||
}
|
||
return new Subscriber(nextOrObserver, error, complete);
|
||
}
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
var observable = /*@__PURE__*/ (function () { return typeof Symbol === 'function' && Symbol.observable || '@@observable'; })();
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
function identity(x) {
|
||
return x;
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _identity PURE_IMPORTS_END */
|
||
function pipe() {
|
||
var fns = [];
|
||
for (var _i = 0; _i < arguments.length; _i++) {
|
||
fns[_i] = arguments[_i];
|
||
}
|
||
return pipeFromArray(fns);
|
||
}
|
||
function pipeFromArray(fns) {
|
||
if (fns.length === 0) {
|
||
return identity;
|
||
}
|
||
if (fns.length === 1) {
|
||
return fns[0];
|
||
}
|
||
return function piped(input) {
|
||
return fns.reduce(function (prev, fn) { return fn(prev); }, input);
|
||
};
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _util_canReportError,_util_toSubscriber,_symbol_observable,_util_pipe,_config PURE_IMPORTS_END */
|
||
var Observable = /*@__PURE__*/ (function () {
|
||
function Observable(subscribe) {
|
||
this._isScalar = false;
|
||
if (subscribe) {
|
||
this._subscribe = subscribe;
|
||
}
|
||
}
|
||
Observable.prototype.lift = function (operator) {
|
||
var observable = new Observable();
|
||
observable.source = this;
|
||
observable.operator = operator;
|
||
return observable;
|
||
};
|
||
Observable.prototype.subscribe = function (observerOrNext, error, complete) {
|
||
var operator = this.operator;
|
||
var sink = toSubscriber(observerOrNext, error, complete);
|
||
if (operator) {
|
||
sink.add(operator.call(sink, this.source));
|
||
}
|
||
else {
|
||
sink.add(this.source || (config.useDeprecatedSynchronousErrorHandling && !sink.syncErrorThrowable) ?
|
||
this._subscribe(sink) :
|
||
this._trySubscribe(sink));
|
||
}
|
||
if (config.useDeprecatedSynchronousErrorHandling) {
|
||
if (sink.syncErrorThrowable) {
|
||
sink.syncErrorThrowable = false;
|
||
if (sink.syncErrorThrown) {
|
||
throw sink.syncErrorValue;
|
||
}
|
||
}
|
||
}
|
||
return sink;
|
||
};
|
||
Observable.prototype._trySubscribe = function (sink) {
|
||
try {
|
||
return this._subscribe(sink);
|
||
}
|
||
catch (err) {
|
||
if (config.useDeprecatedSynchronousErrorHandling) {
|
||
sink.syncErrorThrown = true;
|
||
sink.syncErrorValue = err;
|
||
}
|
||
if (canReportError(sink)) {
|
||
sink.error(err);
|
||
}
|
||
else {
|
||
console.warn(err);
|
||
}
|
||
}
|
||
};
|
||
Observable.prototype.forEach = function (next, promiseCtor) {
|
||
var _this = this;
|
||
promiseCtor = getPromiseCtor(promiseCtor);
|
||
return new promiseCtor(function (resolve, reject) {
|
||
var subscription;
|
||
subscription = _this.subscribe(function (value) {
|
||
try {
|
||
next(value);
|
||
}
|
||
catch (err) {
|
||
reject(err);
|
||
if (subscription) {
|
||
subscription.unsubscribe();
|
||
}
|
||
}
|
||
}, reject, resolve);
|
||
});
|
||
};
|
||
Observable.prototype._subscribe = function (subscriber) {
|
||
var source = this.source;
|
||
return source && source.subscribe(subscriber);
|
||
};
|
||
Observable.prototype[observable] = function () {
|
||
return this;
|
||
};
|
||
Observable.prototype.pipe = function () {
|
||
var operations = [];
|
||
for (var _i = 0; _i < arguments.length; _i++) {
|
||
operations[_i] = arguments[_i];
|
||
}
|
||
if (operations.length === 0) {
|
||
return this;
|
||
}
|
||
return pipeFromArray(operations)(this);
|
||
};
|
||
Observable.prototype.toPromise = function (promiseCtor) {
|
||
var _this = this;
|
||
promiseCtor = getPromiseCtor(promiseCtor);
|
||
return new promiseCtor(function (resolve, reject) {
|
||
var value;
|
||
_this.subscribe(function (x) { return value = x; }, function (err) { return reject(err); }, function () { return resolve(value); });
|
||
});
|
||
};
|
||
Observable.create = function (subscribe) {
|
||
return new Observable(subscribe);
|
||
};
|
||
return Observable;
|
||
}());
|
||
function getPromiseCtor(promiseCtor) {
|
||
if (!promiseCtor) {
|
||
promiseCtor = config.Promise || Promise;
|
||
}
|
||
if (!promiseCtor) {
|
||
throw new Error('no Promise impl found');
|
||
}
|
||
return promiseCtor;
|
||
}
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
var ObjectUnsubscribedErrorImpl = /*@__PURE__*/ (function () {
|
||
function ObjectUnsubscribedErrorImpl() {
|
||
Error.call(this);
|
||
this.message = 'object unsubscribed';
|
||
this.name = 'ObjectUnsubscribedError';
|
||
return this;
|
||
}
|
||
ObjectUnsubscribedErrorImpl.prototype = /*@__PURE__*/ Object.create(Error.prototype);
|
||
return ObjectUnsubscribedErrorImpl;
|
||
})();
|
||
var ObjectUnsubscribedError = ObjectUnsubscribedErrorImpl;
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscription PURE_IMPORTS_END */
|
||
var SubjectSubscription = /*@__PURE__*/ (function (_super) {
|
||
__extends(SubjectSubscription, _super);
|
||
function SubjectSubscription(subject, subscriber) {
|
||
var _this = _super.call(this) || this;
|
||
_this.subject = subject;
|
||
_this.subscriber = subscriber;
|
||
_this.closed = false;
|
||
return _this;
|
||
}
|
||
SubjectSubscription.prototype.unsubscribe = function () {
|
||
if (this.closed) {
|
||
return;
|
||
}
|
||
this.closed = true;
|
||
var subject = this.subject;
|
||
var observers = subject.observers;
|
||
this.subject = null;
|
||
if (!observers || observers.length === 0 || subject.isStopped || subject.closed) {
|
||
return;
|
||
}
|
||
var subscriberIndex = observers.indexOf(this.subscriber);
|
||
if (subscriberIndex !== -1) {
|
||
observers.splice(subscriberIndex, 1);
|
||
}
|
||
};
|
||
return SubjectSubscription;
|
||
}(Subscription));
|
||
|
||
/** PURE_IMPORTS_START tslib,_Observable,_Subscriber,_Subscription,_util_ObjectUnsubscribedError,_SubjectSubscription,_internal_symbol_rxSubscriber PURE_IMPORTS_END */
|
||
var SubjectSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(SubjectSubscriber, _super);
|
||
function SubjectSubscriber(destination) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.destination = destination;
|
||
return _this;
|
||
}
|
||
return SubjectSubscriber;
|
||
}(Subscriber));
|
||
var Subject = /*@__PURE__*/ (function (_super) {
|
||
__extends(Subject, _super);
|
||
function Subject() {
|
||
var _this = _super.call(this) || this;
|
||
_this.observers = [];
|
||
_this.closed = false;
|
||
_this.isStopped = false;
|
||
_this.hasError = false;
|
||
_this.thrownError = null;
|
||
return _this;
|
||
}
|
||
Subject.prototype[rxSubscriber] = function () {
|
||
return new SubjectSubscriber(this);
|
||
};
|
||
Subject.prototype.lift = function (operator) {
|
||
var subject = new AnonymousSubject(this, this);
|
||
subject.operator = operator;
|
||
return subject;
|
||
};
|
||
Subject.prototype.next = function (value) {
|
||
if (this.closed) {
|
||
throw new ObjectUnsubscribedError();
|
||
}
|
||
if (!this.isStopped) {
|
||
var observers = this.observers;
|
||
var len = observers.length;
|
||
var copy = observers.slice();
|
||
for (var i = 0; i < len; i++) {
|
||
copy[i].next(value);
|
||
}
|
||
}
|
||
};
|
||
Subject.prototype.error = function (err) {
|
||
if (this.closed) {
|
||
throw new ObjectUnsubscribedError();
|
||
}
|
||
this.hasError = true;
|
||
this.thrownError = err;
|
||
this.isStopped = true;
|
||
var observers = this.observers;
|
||
var len = observers.length;
|
||
var copy = observers.slice();
|
||
for (var i = 0; i < len; i++) {
|
||
copy[i].error(err);
|
||
}
|
||
this.observers.length = 0;
|
||
};
|
||
Subject.prototype.complete = function () {
|
||
if (this.closed) {
|
||
throw new ObjectUnsubscribedError();
|
||
}
|
||
this.isStopped = true;
|
||
var observers = this.observers;
|
||
var len = observers.length;
|
||
var copy = observers.slice();
|
||
for (var i = 0; i < len; i++) {
|
||
copy[i].complete();
|
||
}
|
||
this.observers.length = 0;
|
||
};
|
||
Subject.prototype.unsubscribe = function () {
|
||
this.isStopped = true;
|
||
this.closed = true;
|
||
this.observers = null;
|
||
};
|
||
Subject.prototype._trySubscribe = function (subscriber) {
|
||
if (this.closed) {
|
||
throw new ObjectUnsubscribedError();
|
||
}
|
||
else {
|
||
return _super.prototype._trySubscribe.call(this, subscriber);
|
||
}
|
||
};
|
||
Subject.prototype._subscribe = function (subscriber) {
|
||
if (this.closed) {
|
||
throw new ObjectUnsubscribedError();
|
||
}
|
||
else if (this.hasError) {
|
||
subscriber.error(this.thrownError);
|
||
return Subscription.EMPTY;
|
||
}
|
||
else if (this.isStopped) {
|
||
subscriber.complete();
|
||
return Subscription.EMPTY;
|
||
}
|
||
else {
|
||
this.observers.push(subscriber);
|
||
return new SubjectSubscription(this, subscriber);
|
||
}
|
||
};
|
||
Subject.prototype.asObservable = function () {
|
||
var observable = new Observable();
|
||
observable.source = this;
|
||
return observable;
|
||
};
|
||
Subject.create = function (destination, source) {
|
||
return new AnonymousSubject(destination, source);
|
||
};
|
||
return Subject;
|
||
}(Observable));
|
||
var AnonymousSubject = /*@__PURE__*/ (function (_super) {
|
||
__extends(AnonymousSubject, _super);
|
||
function AnonymousSubject(destination, source) {
|
||
var _this = _super.call(this) || this;
|
||
_this.destination = destination;
|
||
_this.source = source;
|
||
return _this;
|
||
}
|
||
AnonymousSubject.prototype.next = function (value) {
|
||
var destination = this.destination;
|
||
if (destination && destination.next) {
|
||
destination.next(value);
|
||
}
|
||
};
|
||
AnonymousSubject.prototype.error = function (err) {
|
||
var destination = this.destination;
|
||
if (destination && destination.error) {
|
||
this.destination.error(err);
|
||
}
|
||
};
|
||
AnonymousSubject.prototype.complete = function () {
|
||
var destination = this.destination;
|
||
if (destination && destination.complete) {
|
||
this.destination.complete();
|
||
}
|
||
};
|
||
AnonymousSubject.prototype._subscribe = function (subscriber) {
|
||
var source = this.source;
|
||
if (source) {
|
||
return this.source.subscribe(subscriber);
|
||
}
|
||
else {
|
||
return Subscription.EMPTY;
|
||
}
|
||
};
|
||
return AnonymousSubject;
|
||
}(Subject));
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */
|
||
function refCount() {
|
||
return function refCountOperatorFunction(source) {
|
||
return source.lift(new RefCountOperator(source));
|
||
};
|
||
}
|
||
var RefCountOperator = /*@__PURE__*/ (function () {
|
||
function RefCountOperator(connectable) {
|
||
this.connectable = connectable;
|
||
}
|
||
RefCountOperator.prototype.call = function (subscriber, source) {
|
||
var connectable = this.connectable;
|
||
connectable._refCount++;
|
||
var refCounter = new RefCountSubscriber(subscriber, connectable);
|
||
var subscription = source.subscribe(refCounter);
|
||
if (!refCounter.closed) {
|
||
refCounter.connection = connectable.connect();
|
||
}
|
||
return subscription;
|
||
};
|
||
return RefCountOperator;
|
||
}());
|
||
var RefCountSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(RefCountSubscriber, _super);
|
||
function RefCountSubscriber(destination, connectable) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.connectable = connectable;
|
||
return _this;
|
||
}
|
||
RefCountSubscriber.prototype._unsubscribe = function () {
|
||
var connectable = this.connectable;
|
||
if (!connectable) {
|
||
this.connection = null;
|
||
return;
|
||
}
|
||
this.connectable = null;
|
||
var refCount = connectable._refCount;
|
||
if (refCount <= 0) {
|
||
this.connection = null;
|
||
return;
|
||
}
|
||
connectable._refCount = refCount - 1;
|
||
if (refCount > 1) {
|
||
this.connection = null;
|
||
return;
|
||
}
|
||
var connection = this.connection;
|
||
var sharedConnection = connectable._connection;
|
||
this.connection = null;
|
||
if (sharedConnection && (!connection || sharedConnection === connection)) {
|
||
sharedConnection.unsubscribe();
|
||
}
|
||
};
|
||
return RefCountSubscriber;
|
||
}(Subscriber));
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subject,_Observable,_Subscriber,_Subscription,_operators_refCount PURE_IMPORTS_END */
|
||
var ConnectableObservable = /*@__PURE__*/ (function (_super) {
|
||
__extends(ConnectableObservable, _super);
|
||
function ConnectableObservable(source, subjectFactory) {
|
||
var _this = _super.call(this) || this;
|
||
_this.source = source;
|
||
_this.subjectFactory = subjectFactory;
|
||
_this._refCount = 0;
|
||
_this._isComplete = false;
|
||
return _this;
|
||
}
|
||
ConnectableObservable.prototype._subscribe = function (subscriber) {
|
||
return this.getSubject().subscribe(subscriber);
|
||
};
|
||
ConnectableObservable.prototype.getSubject = function () {
|
||
var subject = this._subject;
|
||
if (!subject || subject.isStopped) {
|
||
this._subject = this.subjectFactory();
|
||
}
|
||
return this._subject;
|
||
};
|
||
ConnectableObservable.prototype.connect = function () {
|
||
var connection = this._connection;
|
||
if (!connection) {
|
||
this._isComplete = false;
|
||
connection = this._connection = new Subscription();
|
||
connection.add(this.source
|
||
.subscribe(new ConnectableSubscriber(this.getSubject(), this)));
|
||
if (connection.closed) {
|
||
this._connection = null;
|
||
connection = Subscription.EMPTY;
|
||
}
|
||
}
|
||
return connection;
|
||
};
|
||
ConnectableObservable.prototype.refCount = function () {
|
||
return refCount()(this);
|
||
};
|
||
return ConnectableObservable;
|
||
}(Observable));
|
||
var connectableObservableDescriptor = /*@__PURE__*/ (function () {
|
||
var connectableProto = ConnectableObservable.prototype;
|
||
return {
|
||
operator: { value: null },
|
||
_refCount: { value: 0, writable: true },
|
||
_subject: { value: null, writable: true },
|
||
_connection: { value: null, writable: true },
|
||
_subscribe: { value: connectableProto._subscribe },
|
||
_isComplete: { value: connectableProto._isComplete, writable: true },
|
||
getSubject: { value: connectableProto.getSubject },
|
||
connect: { value: connectableProto.connect },
|
||
refCount: { value: connectableProto.refCount }
|
||
};
|
||
})();
|
||
var ConnectableSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(ConnectableSubscriber, _super);
|
||
function ConnectableSubscriber(destination, connectable) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.connectable = connectable;
|
||
return _this;
|
||
}
|
||
ConnectableSubscriber.prototype._error = function (err) {
|
||
this._unsubscribe();
|
||
_super.prototype._error.call(this, err);
|
||
};
|
||
ConnectableSubscriber.prototype._complete = function () {
|
||
this.connectable._isComplete = true;
|
||
this._unsubscribe();
|
||
_super.prototype._complete.call(this);
|
||
};
|
||
ConnectableSubscriber.prototype._unsubscribe = function () {
|
||
var connectable = this.connectable;
|
||
if (connectable) {
|
||
this.connectable = null;
|
||
var connection = connectable._connection;
|
||
connectable._refCount = 0;
|
||
connectable._subject = null;
|
||
connectable._connection = null;
|
||
if (connection) {
|
||
connection.unsubscribe();
|
||
}
|
||
}
|
||
};
|
||
return ConnectableSubscriber;
|
||
}(SubjectSubscriber));
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subject,_util_ObjectUnsubscribedError PURE_IMPORTS_END */
|
||
var BehaviorSubject = /*@__PURE__*/ (function (_super) {
|
||
__extends(BehaviorSubject, _super);
|
||
function BehaviorSubject(_value) {
|
||
var _this = _super.call(this) || this;
|
||
_this._value = _value;
|
||
return _this;
|
||
}
|
||
Object.defineProperty(BehaviorSubject.prototype, "value", {
|
||
get: function () {
|
||
return this.getValue();
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
BehaviorSubject.prototype._subscribe = function (subscriber) {
|
||
var subscription = _super.prototype._subscribe.call(this, subscriber);
|
||
if (subscription && !subscription.closed) {
|
||
subscriber.next(this._value);
|
||
}
|
||
return subscription;
|
||
};
|
||
BehaviorSubject.prototype.getValue = function () {
|
||
if (this.hasError) {
|
||
throw this.thrownError;
|
||
}
|
||
else if (this.closed) {
|
||
throw new ObjectUnsubscribedError();
|
||
}
|
||
else {
|
||
return this._value;
|
||
}
|
||
};
|
||
BehaviorSubject.prototype.next = function (value) {
|
||
_super.prototype.next.call(this, this._value = value);
|
||
};
|
||
return BehaviorSubject;
|
||
}(Subject));
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscription PURE_IMPORTS_END */
|
||
var Action = /*@__PURE__*/ (function (_super) {
|
||
__extends(Action, _super);
|
||
function Action(scheduler, work) {
|
||
return _super.call(this) || this;
|
||
}
|
||
Action.prototype.schedule = function (state, delay) {
|
||
return this;
|
||
};
|
||
return Action;
|
||
}(Subscription));
|
||
|
||
/** PURE_IMPORTS_START tslib,_Action PURE_IMPORTS_END */
|
||
var AsyncAction = /*@__PURE__*/ (function (_super) {
|
||
__extends(AsyncAction, _super);
|
||
function AsyncAction(scheduler, work) {
|
||
var _this = _super.call(this, scheduler, work) || this;
|
||
_this.scheduler = scheduler;
|
||
_this.work = work;
|
||
_this.pending = false;
|
||
return _this;
|
||
}
|
||
AsyncAction.prototype.schedule = function (state, delay) {
|
||
if (delay === void 0) {
|
||
delay = 0;
|
||
}
|
||
if (this.closed) {
|
||
return this;
|
||
}
|
||
this.state = state;
|
||
var id = this.id;
|
||
var scheduler = this.scheduler;
|
||
if (id != null) {
|
||
this.id = this.recycleAsyncId(scheduler, id, delay);
|
||
}
|
||
this.pending = true;
|
||
this.delay = delay;
|
||
this.id = this.id || this.requestAsyncId(scheduler, this.id, delay);
|
||
return this;
|
||
};
|
||
AsyncAction.prototype.requestAsyncId = function (scheduler, id, delay) {
|
||
if (delay === void 0) {
|
||
delay = 0;
|
||
}
|
||
return setInterval(scheduler.flush.bind(scheduler, this), delay);
|
||
};
|
||
AsyncAction.prototype.recycleAsyncId = function (scheduler, id, delay) {
|
||
if (delay === void 0) {
|
||
delay = 0;
|
||
}
|
||
if (delay !== null && this.delay === delay && this.pending === false) {
|
||
return id;
|
||
}
|
||
clearInterval(id);
|
||
return undefined;
|
||
};
|
||
AsyncAction.prototype.execute = function (state, delay) {
|
||
if (this.closed) {
|
||
return new Error('executing a cancelled action');
|
||
}
|
||
this.pending = false;
|
||
var error = this._execute(state, delay);
|
||
if (error) {
|
||
return error;
|
||
}
|
||
else if (this.pending === false && this.id != null) {
|
||
this.id = this.recycleAsyncId(this.scheduler, this.id, null);
|
||
}
|
||
};
|
||
AsyncAction.prototype._execute = function (state, delay) {
|
||
var errored = false;
|
||
var errorValue = undefined;
|
||
try {
|
||
this.work(state);
|
||
}
|
||
catch (e) {
|
||
errored = true;
|
||
errorValue = !!e && e || new Error(e);
|
||
}
|
||
if (errored) {
|
||
this.unsubscribe();
|
||
return errorValue;
|
||
}
|
||
};
|
||
AsyncAction.prototype._unsubscribe = function () {
|
||
var id = this.id;
|
||
var scheduler = this.scheduler;
|
||
var actions = scheduler.actions;
|
||
var index = actions.indexOf(this);
|
||
this.work = null;
|
||
this.state = null;
|
||
this.pending = false;
|
||
this.scheduler = null;
|
||
if (index !== -1) {
|
||
actions.splice(index, 1);
|
||
}
|
||
if (id != null) {
|
||
this.id = this.recycleAsyncId(scheduler, id, null);
|
||
}
|
||
this.delay = null;
|
||
};
|
||
return AsyncAction;
|
||
}(Action));
|
||
|
||
/** PURE_IMPORTS_START tslib,_AsyncAction PURE_IMPORTS_END */
|
||
var QueueAction = /*@__PURE__*/ (function (_super) {
|
||
__extends(QueueAction, _super);
|
||
function QueueAction(scheduler, work) {
|
||
var _this = _super.call(this, scheduler, work) || this;
|
||
_this.scheduler = scheduler;
|
||
_this.work = work;
|
||
return _this;
|
||
}
|
||
QueueAction.prototype.schedule = function (state, delay) {
|
||
if (delay === void 0) {
|
||
delay = 0;
|
||
}
|
||
if (delay > 0) {
|
||
return _super.prototype.schedule.call(this, state, delay);
|
||
}
|
||
this.delay = delay;
|
||
this.state = state;
|
||
this.scheduler.flush(this);
|
||
return this;
|
||
};
|
||
QueueAction.prototype.execute = function (state, delay) {
|
||
return (delay > 0 || this.closed) ?
|
||
_super.prototype.execute.call(this, state, delay) :
|
||
this._execute(state, delay);
|
||
};
|
||
QueueAction.prototype.requestAsyncId = function (scheduler, id, delay) {
|
||
if (delay === void 0) {
|
||
delay = 0;
|
||
}
|
||
if ((delay !== null && delay > 0) || (delay === null && this.delay > 0)) {
|
||
return _super.prototype.requestAsyncId.call(this, scheduler, id, delay);
|
||
}
|
||
return scheduler.flush(this);
|
||
};
|
||
return QueueAction;
|
||
}(AsyncAction));
|
||
|
||
var Scheduler = /*@__PURE__*/ (function () {
|
||
function Scheduler(SchedulerAction, now) {
|
||
if (now === void 0) {
|
||
now = Scheduler.now;
|
||
}
|
||
this.SchedulerAction = SchedulerAction;
|
||
this.now = now;
|
||
}
|
||
Scheduler.prototype.schedule = function (work, delay, state) {
|
||
if (delay === void 0) {
|
||
delay = 0;
|
||
}
|
||
return new this.SchedulerAction(this, work).schedule(state, delay);
|
||
};
|
||
Scheduler.now = function () { return Date.now(); };
|
||
return Scheduler;
|
||
}());
|
||
|
||
/** PURE_IMPORTS_START tslib,_Scheduler PURE_IMPORTS_END */
|
||
var AsyncScheduler = /*@__PURE__*/ (function (_super) {
|
||
__extends(AsyncScheduler, _super);
|
||
function AsyncScheduler(SchedulerAction, now) {
|
||
if (now === void 0) {
|
||
now = Scheduler.now;
|
||
}
|
||
var _this = _super.call(this, SchedulerAction, function () {
|
||
if (AsyncScheduler.delegate && AsyncScheduler.delegate !== _this) {
|
||
return AsyncScheduler.delegate.now();
|
||
}
|
||
else {
|
||
return now();
|
||
}
|
||
}) || this;
|
||
_this.actions = [];
|
||
_this.active = false;
|
||
_this.scheduled = undefined;
|
||
return _this;
|
||
}
|
||
AsyncScheduler.prototype.schedule = function (work, delay, state) {
|
||
if (delay === void 0) {
|
||
delay = 0;
|
||
}
|
||
if (AsyncScheduler.delegate && AsyncScheduler.delegate !== this) {
|
||
return AsyncScheduler.delegate.schedule(work, delay, state);
|
||
}
|
||
else {
|
||
return _super.prototype.schedule.call(this, work, delay, state);
|
||
}
|
||
};
|
||
AsyncScheduler.prototype.flush = function (action) {
|
||
var actions = this.actions;
|
||
if (this.active) {
|
||
actions.push(action);
|
||
return;
|
||
}
|
||
var error;
|
||
this.active = true;
|
||
do {
|
||
if (error = action.execute(action.state, action.delay)) {
|
||
break;
|
||
}
|
||
} while (action = actions.shift());
|
||
this.active = false;
|
||
if (error) {
|
||
while (action = actions.shift()) {
|
||
action.unsubscribe();
|
||
}
|
||
throw error;
|
||
}
|
||
};
|
||
return AsyncScheduler;
|
||
}(Scheduler));
|
||
|
||
/** PURE_IMPORTS_START tslib,_AsyncScheduler PURE_IMPORTS_END */
|
||
var QueueScheduler = /*@__PURE__*/ (function (_super) {
|
||
__extends(QueueScheduler, _super);
|
||
function QueueScheduler() {
|
||
return _super !== null && _super.apply(this, arguments) || this;
|
||
}
|
||
return QueueScheduler;
|
||
}(AsyncScheduler));
|
||
|
||
/** PURE_IMPORTS_START _QueueAction,_QueueScheduler PURE_IMPORTS_END */
|
||
var queueScheduler = /*@__PURE__*/ new QueueScheduler(QueueAction);
|
||
var queue = queueScheduler;
|
||
|
||
/** PURE_IMPORTS_START _Observable PURE_IMPORTS_END */
|
||
var EMPTY = /*@__PURE__*/ new Observable(function (subscriber) { return subscriber.complete(); });
|
||
function empty(scheduler) {
|
||
return scheduler ? emptyScheduled(scheduler) : EMPTY;
|
||
}
|
||
function emptyScheduled(scheduler) {
|
||
return new Observable(function (subscriber) { return scheduler.schedule(function () { return subscriber.complete(); }); });
|
||
}
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
function isScheduler(value) {
|
||
return value && typeof value.schedule === 'function';
|
||
}
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
var subscribeToArray = function (array) {
|
||
return function (subscriber) {
|
||
for (var i = 0, len = array.length; i < len && !subscriber.closed; i++) {
|
||
subscriber.next(array[i]);
|
||
}
|
||
subscriber.complete();
|
||
};
|
||
};
|
||
|
||
/** PURE_IMPORTS_START _Observable,_Subscription PURE_IMPORTS_END */
|
||
function scheduleArray(input, scheduler) {
|
||
return new Observable(function (subscriber) {
|
||
var sub = new Subscription();
|
||
var i = 0;
|
||
sub.add(scheduler.schedule(function () {
|
||
if (i === input.length) {
|
||
subscriber.complete();
|
||
return;
|
||
}
|
||
subscriber.next(input[i++]);
|
||
if (!subscriber.closed) {
|
||
sub.add(this.schedule());
|
||
}
|
||
}));
|
||
return sub;
|
||
});
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _Observable,_util_subscribeToArray,_scheduled_scheduleArray PURE_IMPORTS_END */
|
||
function fromArray(input, scheduler) {
|
||
if (!scheduler) {
|
||
return new Observable(subscribeToArray(input));
|
||
}
|
||
else {
|
||
return scheduleArray(input, scheduler);
|
||
}
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _util_isScheduler,_fromArray,_scheduled_scheduleArray PURE_IMPORTS_END */
|
||
function of() {
|
||
var args = [];
|
||
for (var _i = 0; _i < arguments.length; _i++) {
|
||
args[_i] = arguments[_i];
|
||
}
|
||
var scheduler = args[args.length - 1];
|
||
if (isScheduler(scheduler)) {
|
||
args.pop();
|
||
return scheduleArray(args, scheduler);
|
||
}
|
||
else {
|
||
return fromArray(args);
|
||
}
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _Observable PURE_IMPORTS_END */
|
||
function throwError(error, scheduler) {
|
||
if (!scheduler) {
|
||
return new Observable(function (subscriber) { return subscriber.error(error); });
|
||
}
|
||
else {
|
||
return new Observable(function (subscriber) { return scheduler.schedule(dispatch$1, 0, { error: error, subscriber: subscriber }); });
|
||
}
|
||
}
|
||
function dispatch$1(_a) {
|
||
var error = _a.error, subscriber = _a.subscriber;
|
||
subscriber.error(error);
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _observable_empty,_observable_of,_observable_throwError PURE_IMPORTS_END */
|
||
var Notification = /*@__PURE__*/ (function () {
|
||
function Notification(kind, value, error) {
|
||
this.kind = kind;
|
||
this.value = value;
|
||
this.error = error;
|
||
this.hasValue = kind === 'N';
|
||
}
|
||
Notification.prototype.observe = function (observer) {
|
||
switch (this.kind) {
|
||
case 'N':
|
||
return observer.next && observer.next(this.value);
|
||
case 'E':
|
||
return observer.error && observer.error(this.error);
|
||
case 'C':
|
||
return observer.complete && observer.complete();
|
||
}
|
||
};
|
||
Notification.prototype.do = function (next, error, complete) {
|
||
var kind = this.kind;
|
||
switch (kind) {
|
||
case 'N':
|
||
return next && next(this.value);
|
||
case 'E':
|
||
return error && error(this.error);
|
||
case 'C':
|
||
return complete && complete();
|
||
}
|
||
};
|
||
Notification.prototype.accept = function (nextOrObserver, error, complete) {
|
||
if (nextOrObserver && typeof nextOrObserver.next === 'function') {
|
||
return this.observe(nextOrObserver);
|
||
}
|
||
else {
|
||
return this.do(nextOrObserver, error, complete);
|
||
}
|
||
};
|
||
Notification.prototype.toObservable = function () {
|
||
var kind = this.kind;
|
||
switch (kind) {
|
||
case 'N':
|
||
return of(this.value);
|
||
case 'E':
|
||
return throwError(this.error);
|
||
case 'C':
|
||
return empty();
|
||
}
|
||
throw new Error('unexpected notification kind value');
|
||
};
|
||
Notification.createNext = function (value) {
|
||
if (typeof value !== 'undefined') {
|
||
return new Notification('N', value);
|
||
}
|
||
return Notification.undefinedValueNotification;
|
||
};
|
||
Notification.createError = function (err) {
|
||
return new Notification('E', undefined, err);
|
||
};
|
||
Notification.createComplete = function () {
|
||
return Notification.completeNotification;
|
||
};
|
||
Notification.completeNotification = new Notification('C');
|
||
Notification.undefinedValueNotification = new Notification('N', undefined);
|
||
return Notification;
|
||
}());
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber,_Notification PURE_IMPORTS_END */
|
||
function observeOn(scheduler, delay) {
|
||
if (delay === void 0) {
|
||
delay = 0;
|
||
}
|
||
return function observeOnOperatorFunction(source) {
|
||
return source.lift(new ObserveOnOperator(scheduler, delay));
|
||
};
|
||
}
|
||
var ObserveOnOperator = /*@__PURE__*/ (function () {
|
||
function ObserveOnOperator(scheduler, delay) {
|
||
if (delay === void 0) {
|
||
delay = 0;
|
||
}
|
||
this.scheduler = scheduler;
|
||
this.delay = delay;
|
||
}
|
||
ObserveOnOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new ObserveOnSubscriber(subscriber, this.scheduler, this.delay));
|
||
};
|
||
return ObserveOnOperator;
|
||
}());
|
||
var ObserveOnSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(ObserveOnSubscriber, _super);
|
||
function ObserveOnSubscriber(destination, scheduler, delay) {
|
||
if (delay === void 0) {
|
||
delay = 0;
|
||
}
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.scheduler = scheduler;
|
||
_this.delay = delay;
|
||
return _this;
|
||
}
|
||
ObserveOnSubscriber.dispatch = function (arg) {
|
||
var notification = arg.notification, destination = arg.destination;
|
||
notification.observe(destination);
|
||
this.unsubscribe();
|
||
};
|
||
ObserveOnSubscriber.prototype.scheduleMessage = function (notification) {
|
||
var destination = this.destination;
|
||
destination.add(this.scheduler.schedule(ObserveOnSubscriber.dispatch, this.delay, new ObserveOnMessage(notification, this.destination)));
|
||
};
|
||
ObserveOnSubscriber.prototype._next = function (value) {
|
||
this.scheduleMessage(Notification.createNext(value));
|
||
};
|
||
ObserveOnSubscriber.prototype._error = function (err) {
|
||
this.scheduleMessage(Notification.createError(err));
|
||
this.unsubscribe();
|
||
};
|
||
ObserveOnSubscriber.prototype._complete = function () {
|
||
this.scheduleMessage(Notification.createComplete());
|
||
this.unsubscribe();
|
||
};
|
||
return ObserveOnSubscriber;
|
||
}(Subscriber));
|
||
var ObserveOnMessage = /*@__PURE__*/ (function () {
|
||
function ObserveOnMessage(notification, destination) {
|
||
this.notification = notification;
|
||
this.destination = destination;
|
||
}
|
||
return ObserveOnMessage;
|
||
}());
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subject,_scheduler_queue,_Subscription,_operators_observeOn,_util_ObjectUnsubscribedError,_SubjectSubscription PURE_IMPORTS_END */
|
||
var ReplaySubject = /*@__PURE__*/ (function (_super) {
|
||
__extends(ReplaySubject, _super);
|
||
function ReplaySubject(bufferSize, windowTime, scheduler) {
|
||
if (bufferSize === void 0) {
|
||
bufferSize = Number.POSITIVE_INFINITY;
|
||
}
|
||
if (windowTime === void 0) {
|
||
windowTime = Number.POSITIVE_INFINITY;
|
||
}
|
||
var _this = _super.call(this) || this;
|
||
_this.scheduler = scheduler;
|
||
_this._events = [];
|
||
_this._infiniteTimeWindow = false;
|
||
_this._bufferSize = bufferSize < 1 ? 1 : bufferSize;
|
||
_this._windowTime = windowTime < 1 ? 1 : windowTime;
|
||
if (windowTime === Number.POSITIVE_INFINITY) {
|
||
_this._infiniteTimeWindow = true;
|
||
_this.next = _this.nextInfiniteTimeWindow;
|
||
}
|
||
else {
|
||
_this.next = _this.nextTimeWindow;
|
||
}
|
||
return _this;
|
||
}
|
||
ReplaySubject.prototype.nextInfiniteTimeWindow = function (value) {
|
||
if (!this.isStopped) {
|
||
var _events = this._events;
|
||
_events.push(value);
|
||
if (_events.length > this._bufferSize) {
|
||
_events.shift();
|
||
}
|
||
}
|
||
_super.prototype.next.call(this, value);
|
||
};
|
||
ReplaySubject.prototype.nextTimeWindow = function (value) {
|
||
if (!this.isStopped) {
|
||
this._events.push(new ReplayEvent(this._getNow(), value));
|
||
this._trimBufferThenGetEvents();
|
||
}
|
||
_super.prototype.next.call(this, value);
|
||
};
|
||
ReplaySubject.prototype._subscribe = function (subscriber) {
|
||
var _infiniteTimeWindow = this._infiniteTimeWindow;
|
||
var _events = _infiniteTimeWindow ? this._events : this._trimBufferThenGetEvents();
|
||
var scheduler = this.scheduler;
|
||
var len = _events.length;
|
||
var subscription;
|
||
if (this.closed) {
|
||
throw new ObjectUnsubscribedError();
|
||
}
|
||
else if (this.isStopped || this.hasError) {
|
||
subscription = Subscription.EMPTY;
|
||
}
|
||
else {
|
||
this.observers.push(subscriber);
|
||
subscription = new SubjectSubscription(this, subscriber);
|
||
}
|
||
if (scheduler) {
|
||
subscriber.add(subscriber = new ObserveOnSubscriber(subscriber, scheduler));
|
||
}
|
||
if (_infiniteTimeWindow) {
|
||
for (var i = 0; i < len && !subscriber.closed; i++) {
|
||
subscriber.next(_events[i]);
|
||
}
|
||
}
|
||
else {
|
||
for (var i = 0; i < len && !subscriber.closed; i++) {
|
||
subscriber.next(_events[i].value);
|
||
}
|
||
}
|
||
if (this.hasError) {
|
||
subscriber.error(this.thrownError);
|
||
}
|
||
else if (this.isStopped) {
|
||
subscriber.complete();
|
||
}
|
||
return subscription;
|
||
};
|
||
ReplaySubject.prototype._getNow = function () {
|
||
return (this.scheduler || queue).now();
|
||
};
|
||
ReplaySubject.prototype._trimBufferThenGetEvents = function () {
|
||
var now = this._getNow();
|
||
var _bufferSize = this._bufferSize;
|
||
var _windowTime = this._windowTime;
|
||
var _events = this._events;
|
||
var eventsCount = _events.length;
|
||
var spliceCount = 0;
|
||
while (spliceCount < eventsCount) {
|
||
if ((now - _events[spliceCount].time) < _windowTime) {
|
||
break;
|
||
}
|
||
spliceCount++;
|
||
}
|
||
if (eventsCount > _bufferSize) {
|
||
spliceCount = Math.max(spliceCount, eventsCount - _bufferSize);
|
||
}
|
||
if (spliceCount > 0) {
|
||
_events.splice(0, spliceCount);
|
||
}
|
||
return _events;
|
||
};
|
||
return ReplaySubject;
|
||
}(Subject));
|
||
var ReplayEvent = /*@__PURE__*/ (function () {
|
||
function ReplayEvent(time, value) {
|
||
this.time = time;
|
||
this.value = value;
|
||
}
|
||
return ReplayEvent;
|
||
}());
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subject,_Subscription PURE_IMPORTS_END */
|
||
var AsyncSubject = /*@__PURE__*/ (function (_super) {
|
||
__extends(AsyncSubject, _super);
|
||
function AsyncSubject() {
|
||
var _this = _super !== null && _super.apply(this, arguments) || this;
|
||
_this.value = null;
|
||
_this.hasNext = false;
|
||
_this.hasCompleted = false;
|
||
return _this;
|
||
}
|
||
AsyncSubject.prototype._subscribe = function (subscriber) {
|
||
if (this.hasError) {
|
||
subscriber.error(this.thrownError);
|
||
return Subscription.EMPTY;
|
||
}
|
||
else if (this.hasCompleted && this.hasNext) {
|
||
subscriber.next(this.value);
|
||
subscriber.complete();
|
||
return Subscription.EMPTY;
|
||
}
|
||
return _super.prototype._subscribe.call(this, subscriber);
|
||
};
|
||
AsyncSubject.prototype.next = function (value) {
|
||
if (!this.hasCompleted) {
|
||
this.value = value;
|
||
this.hasNext = true;
|
||
}
|
||
};
|
||
AsyncSubject.prototype.error = function (error) {
|
||
if (!this.hasCompleted) {
|
||
_super.prototype.error.call(this, error);
|
||
}
|
||
};
|
||
AsyncSubject.prototype.complete = function () {
|
||
this.hasCompleted = true;
|
||
if (this.hasNext) {
|
||
_super.prototype.next.call(this, this.value);
|
||
}
|
||
_super.prototype.complete.call(this);
|
||
};
|
||
return AsyncSubject;
|
||
}(Subject));
|
||
|
||
/** PURE_IMPORTS_START _AsyncAction,_AsyncScheduler PURE_IMPORTS_END */
|
||
var asyncScheduler = /*@__PURE__*/ new AsyncScheduler(AsyncAction);
|
||
var async = asyncScheduler;
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
function noop() { }
|
||
|
||
/** PURE_IMPORTS_START _Observable PURE_IMPORTS_END */
|
||
function isObservable(obj) {
|
||
return !!obj && (obj instanceof Observable || (typeof obj.lift === 'function' && typeof obj.subscribe === 'function'));
|
||
}
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
var ArgumentOutOfRangeErrorImpl = /*@__PURE__*/ (function () {
|
||
function ArgumentOutOfRangeErrorImpl() {
|
||
Error.call(this);
|
||
this.message = 'argument out of range';
|
||
this.name = 'ArgumentOutOfRangeError';
|
||
return this;
|
||
}
|
||
ArgumentOutOfRangeErrorImpl.prototype = /*@__PURE__*/ Object.create(Error.prototype);
|
||
return ArgumentOutOfRangeErrorImpl;
|
||
})();
|
||
var ArgumentOutOfRangeError = ArgumentOutOfRangeErrorImpl;
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
var TimeoutErrorImpl = /*@__PURE__*/ (function () {
|
||
function TimeoutErrorImpl() {
|
||
Error.call(this);
|
||
this.message = 'Timeout has occurred';
|
||
this.name = 'TimeoutError';
|
||
return this;
|
||
}
|
||
TimeoutErrorImpl.prototype = /*@__PURE__*/ Object.create(Error.prototype);
|
||
return TimeoutErrorImpl;
|
||
})();
|
||
var TimeoutError = TimeoutErrorImpl;
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */
|
||
function map(project, thisArg) {
|
||
return function mapOperation(source) {
|
||
if (typeof project !== 'function') {
|
||
throw new TypeError('argument is not a function. Are you looking for `mapTo()`?');
|
||
}
|
||
return source.lift(new MapOperator(project, thisArg));
|
||
};
|
||
}
|
||
var MapOperator = /*@__PURE__*/ (function () {
|
||
function MapOperator(project, thisArg) {
|
||
this.project = project;
|
||
this.thisArg = thisArg;
|
||
}
|
||
MapOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new MapSubscriber(subscriber, this.project, this.thisArg));
|
||
};
|
||
return MapOperator;
|
||
}());
|
||
var MapSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(MapSubscriber, _super);
|
||
function MapSubscriber(destination, project, thisArg) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.project = project;
|
||
_this.count = 0;
|
||
_this.thisArg = thisArg || _this;
|
||
return _this;
|
||
}
|
||
MapSubscriber.prototype._next = function (value) {
|
||
var result;
|
||
try {
|
||
result = this.project.call(this.thisArg, value, this.count++);
|
||
}
|
||
catch (err) {
|
||
this.destination.error(err);
|
||
return;
|
||
}
|
||
this.destination.next(result);
|
||
};
|
||
return MapSubscriber;
|
||
}(Subscriber));
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */
|
||
var OuterSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(OuterSubscriber, _super);
|
||
function OuterSubscriber() {
|
||
return _super !== null && _super.apply(this, arguments) || this;
|
||
}
|
||
OuterSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) {
|
||
this.destination.next(innerValue);
|
||
};
|
||
OuterSubscriber.prototype.notifyError = function (error, innerSub) {
|
||
this.destination.error(error);
|
||
};
|
||
OuterSubscriber.prototype.notifyComplete = function (innerSub) {
|
||
this.destination.complete();
|
||
};
|
||
return OuterSubscriber;
|
||
}(Subscriber));
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */
|
||
var InnerSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(InnerSubscriber, _super);
|
||
function InnerSubscriber(parent, outerValue, outerIndex) {
|
||
var _this = _super.call(this) || this;
|
||
_this.parent = parent;
|
||
_this.outerValue = outerValue;
|
||
_this.outerIndex = outerIndex;
|
||
_this.index = 0;
|
||
return _this;
|
||
}
|
||
InnerSubscriber.prototype._next = function (value) {
|
||
this.parent.notifyNext(this.outerValue, value, this.outerIndex, this.index++, this);
|
||
};
|
||
InnerSubscriber.prototype._error = function (error) {
|
||
this.parent.notifyError(error, this);
|
||
this.unsubscribe();
|
||
};
|
||
InnerSubscriber.prototype._complete = function () {
|
||
this.parent.notifyComplete(this);
|
||
this.unsubscribe();
|
||
};
|
||
return InnerSubscriber;
|
||
}(Subscriber));
|
||
|
||
/** PURE_IMPORTS_START _hostReportError PURE_IMPORTS_END */
|
||
var subscribeToPromise = function (promise) {
|
||
return function (subscriber) {
|
||
promise.then(function (value) {
|
||
if (!subscriber.closed) {
|
||
subscriber.next(value);
|
||
subscriber.complete();
|
||
}
|
||
}, function (err) { return subscriber.error(err); })
|
||
.then(null, hostReportError);
|
||
return subscriber;
|
||
};
|
||
};
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
function getSymbolIterator() {
|
||
if (typeof Symbol !== 'function' || !Symbol.iterator) {
|
||
return '@@iterator';
|
||
}
|
||
return Symbol.iterator;
|
||
}
|
||
var iterator = /*@__PURE__*/ getSymbolIterator();
|
||
|
||
/** PURE_IMPORTS_START _symbol_iterator PURE_IMPORTS_END */
|
||
var subscribeToIterable = function (iterable) {
|
||
return function (subscriber) {
|
||
var iterator$1 = iterable[iterator]();
|
||
do {
|
||
var item = void 0;
|
||
try {
|
||
item = iterator$1.next();
|
||
}
|
||
catch (err) {
|
||
subscriber.error(err);
|
||
return subscriber;
|
||
}
|
||
if (item.done) {
|
||
subscriber.complete();
|
||
break;
|
||
}
|
||
subscriber.next(item.value);
|
||
if (subscriber.closed) {
|
||
break;
|
||
}
|
||
} while (true);
|
||
if (typeof iterator$1.return === 'function') {
|
||
subscriber.add(function () {
|
||
if (iterator$1.return) {
|
||
iterator$1.return();
|
||
}
|
||
});
|
||
}
|
||
return subscriber;
|
||
};
|
||
};
|
||
|
||
/** PURE_IMPORTS_START _symbol_observable PURE_IMPORTS_END */
|
||
var subscribeToObservable = function (obj) {
|
||
return function (subscriber) {
|
||
var obs = obj[observable]();
|
||
if (typeof obs.subscribe !== 'function') {
|
||
throw new TypeError('Provided object does not correctly implement Symbol.observable');
|
||
}
|
||
else {
|
||
return obs.subscribe(subscriber);
|
||
}
|
||
};
|
||
};
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
var isArrayLike = (function (x) { return x && typeof x.length === 'number' && typeof x !== 'function'; });
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
function isPromise(value) {
|
||
return !!value && typeof value.subscribe !== 'function' && typeof value.then === 'function';
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _subscribeToArray,_subscribeToPromise,_subscribeToIterable,_subscribeToObservable,_isArrayLike,_isPromise,_isObject,_symbol_iterator,_symbol_observable PURE_IMPORTS_END */
|
||
var subscribeTo = function (result) {
|
||
if (!!result && typeof result[observable] === 'function') {
|
||
return subscribeToObservable(result);
|
||
}
|
||
else if (isArrayLike(result)) {
|
||
return subscribeToArray(result);
|
||
}
|
||
else if (isPromise(result)) {
|
||
return subscribeToPromise(result);
|
||
}
|
||
else if (!!result && typeof result[iterator] === 'function') {
|
||
return subscribeToIterable(result);
|
||
}
|
||
else {
|
||
var value = isObject$1(result) ? 'an invalid object' : "'" + result + "'";
|
||
var msg = "You provided " + value + " where a stream was expected."
|
||
+ ' You can provide an Observable, Promise, Array, or Iterable.';
|
||
throw new TypeError(msg);
|
||
}
|
||
};
|
||
|
||
/** PURE_IMPORTS_START _InnerSubscriber,_subscribeTo,_Observable PURE_IMPORTS_END */
|
||
function subscribeToResult(outerSubscriber, result, outerValue, outerIndex, innerSubscriber) {
|
||
if (innerSubscriber === void 0) {
|
||
innerSubscriber = new InnerSubscriber(outerSubscriber, outerValue, outerIndex);
|
||
}
|
||
if (innerSubscriber.closed) {
|
||
return undefined;
|
||
}
|
||
if (result instanceof Observable) {
|
||
return result.subscribe(innerSubscriber);
|
||
}
|
||
return subscribeTo(result)(innerSubscriber);
|
||
}
|
||
|
||
/** PURE_IMPORTS_START tslib,_util_isScheduler,_util_isArray,_OuterSubscriber,_util_subscribeToResult,_fromArray PURE_IMPORTS_END */
|
||
var NONE = {};
|
||
function combineLatest() {
|
||
var observables = [];
|
||
for (var _i = 0; _i < arguments.length; _i++) {
|
||
observables[_i] = arguments[_i];
|
||
}
|
||
var resultSelector = undefined;
|
||
var scheduler = undefined;
|
||
if (isScheduler(observables[observables.length - 1])) {
|
||
scheduler = observables.pop();
|
||
}
|
||
if (typeof observables[observables.length - 1] === 'function') {
|
||
resultSelector = observables.pop();
|
||
}
|
||
if (observables.length === 1 && isArray$1(observables[0])) {
|
||
observables = observables[0];
|
||
}
|
||
return fromArray(observables, scheduler).lift(new CombineLatestOperator(resultSelector));
|
||
}
|
||
var CombineLatestOperator = /*@__PURE__*/ (function () {
|
||
function CombineLatestOperator(resultSelector) {
|
||
this.resultSelector = resultSelector;
|
||
}
|
||
CombineLatestOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new CombineLatestSubscriber(subscriber, this.resultSelector));
|
||
};
|
||
return CombineLatestOperator;
|
||
}());
|
||
var CombineLatestSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(CombineLatestSubscriber, _super);
|
||
function CombineLatestSubscriber(destination, resultSelector) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.resultSelector = resultSelector;
|
||
_this.active = 0;
|
||
_this.values = [];
|
||
_this.observables = [];
|
||
return _this;
|
||
}
|
||
CombineLatestSubscriber.prototype._next = function (observable) {
|
||
this.values.push(NONE);
|
||
this.observables.push(observable);
|
||
};
|
||
CombineLatestSubscriber.prototype._complete = function () {
|
||
var observables = this.observables;
|
||
var len = observables.length;
|
||
if (len === 0) {
|
||
this.destination.complete();
|
||
}
|
||
else {
|
||
this.active = len;
|
||
this.toRespond = len;
|
||
for (var i = 0; i < len; i++) {
|
||
var observable = observables[i];
|
||
this.add(subscribeToResult(this, observable, undefined, i));
|
||
}
|
||
}
|
||
};
|
||
CombineLatestSubscriber.prototype.notifyComplete = function (unused) {
|
||
if ((this.active -= 1) === 0) {
|
||
this.destination.complete();
|
||
}
|
||
};
|
||
CombineLatestSubscriber.prototype.notifyNext = function (_outerValue, innerValue, outerIndex) {
|
||
var values = this.values;
|
||
var oldVal = values[outerIndex];
|
||
var toRespond = !this.toRespond
|
||
? 0
|
||
: oldVal === NONE ? --this.toRespond : this.toRespond;
|
||
values[outerIndex] = innerValue;
|
||
if (toRespond === 0) {
|
||
if (this.resultSelector) {
|
||
this._tryResultSelector(values);
|
||
}
|
||
else {
|
||
this.destination.next(values.slice());
|
||
}
|
||
}
|
||
};
|
||
CombineLatestSubscriber.prototype._tryResultSelector = function (values) {
|
||
var result;
|
||
try {
|
||
result = this.resultSelector.apply(this, values);
|
||
}
|
||
catch (err) {
|
||
this.destination.error(err);
|
||
return;
|
||
}
|
||
this.destination.next(result);
|
||
};
|
||
return CombineLatestSubscriber;
|
||
}(OuterSubscriber));
|
||
|
||
/** PURE_IMPORTS_START _Observable,_Subscription,_symbol_observable PURE_IMPORTS_END */
|
||
function scheduleObservable(input, scheduler) {
|
||
return new Observable(function (subscriber) {
|
||
var sub = new Subscription();
|
||
sub.add(scheduler.schedule(function () {
|
||
var observable$1 = input[observable]();
|
||
sub.add(observable$1.subscribe({
|
||
next: function (value) { sub.add(scheduler.schedule(function () { return subscriber.next(value); })); },
|
||
error: function (err) { sub.add(scheduler.schedule(function () { return subscriber.error(err); })); },
|
||
complete: function () { sub.add(scheduler.schedule(function () { return subscriber.complete(); })); },
|
||
}));
|
||
}));
|
||
return sub;
|
||
});
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _Observable,_Subscription PURE_IMPORTS_END */
|
||
function schedulePromise(input, scheduler) {
|
||
return new Observable(function (subscriber) {
|
||
var sub = new Subscription();
|
||
sub.add(scheduler.schedule(function () {
|
||
return input.then(function (value) {
|
||
sub.add(scheduler.schedule(function () {
|
||
subscriber.next(value);
|
||
sub.add(scheduler.schedule(function () { return subscriber.complete(); }));
|
||
}));
|
||
}, function (err) {
|
||
sub.add(scheduler.schedule(function () { return subscriber.error(err); }));
|
||
});
|
||
}));
|
||
return sub;
|
||
});
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _Observable,_Subscription,_symbol_iterator PURE_IMPORTS_END */
|
||
function scheduleIterable(input, scheduler) {
|
||
if (!input) {
|
||
throw new Error('Iterable cannot be null');
|
||
}
|
||
return new Observable(function (subscriber) {
|
||
var sub = new Subscription();
|
||
var iterator$1;
|
||
sub.add(function () {
|
||
if (iterator$1 && typeof iterator$1.return === 'function') {
|
||
iterator$1.return();
|
||
}
|
||
});
|
||
sub.add(scheduler.schedule(function () {
|
||
iterator$1 = input[iterator]();
|
||
sub.add(scheduler.schedule(function () {
|
||
if (subscriber.closed) {
|
||
return;
|
||
}
|
||
var value;
|
||
var done;
|
||
try {
|
||
var result = iterator$1.next();
|
||
value = result.value;
|
||
done = result.done;
|
||
}
|
||
catch (err) {
|
||
subscriber.error(err);
|
||
return;
|
||
}
|
||
if (done) {
|
||
subscriber.complete();
|
||
}
|
||
else {
|
||
subscriber.next(value);
|
||
this.schedule();
|
||
}
|
||
}));
|
||
}));
|
||
return sub;
|
||
});
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _symbol_observable PURE_IMPORTS_END */
|
||
function isInteropObservable(input) {
|
||
return input && typeof input[observable] === 'function';
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _symbol_iterator PURE_IMPORTS_END */
|
||
function isIterable(input) {
|
||
return input && typeof input[iterator] === 'function';
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _scheduleObservable,_schedulePromise,_scheduleArray,_scheduleIterable,_util_isInteropObservable,_util_isPromise,_util_isArrayLike,_util_isIterable PURE_IMPORTS_END */
|
||
function scheduled(input, scheduler) {
|
||
if (input != null) {
|
||
if (isInteropObservable(input)) {
|
||
return scheduleObservable(input, scheduler);
|
||
}
|
||
else if (isPromise(input)) {
|
||
return schedulePromise(input, scheduler);
|
||
}
|
||
else if (isArrayLike(input)) {
|
||
return scheduleArray(input, scheduler);
|
||
}
|
||
else if (isIterable(input) || typeof input === 'string') {
|
||
return scheduleIterable(input, scheduler);
|
||
}
|
||
}
|
||
throw new TypeError((input !== null && typeof input || input) + ' is not observable');
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _Observable,_util_subscribeTo,_scheduled_scheduled PURE_IMPORTS_END */
|
||
function from(input, scheduler) {
|
||
if (!scheduler) {
|
||
if (input instanceof Observable) {
|
||
return input;
|
||
}
|
||
return new Observable(subscribeTo(input));
|
||
}
|
||
else {
|
||
return scheduled(input, scheduler);
|
||
}
|
||
}
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber,_Observable,_util_subscribeTo PURE_IMPORTS_END */
|
||
var SimpleInnerSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(SimpleInnerSubscriber, _super);
|
||
function SimpleInnerSubscriber(parent) {
|
||
var _this = _super.call(this) || this;
|
||
_this.parent = parent;
|
||
return _this;
|
||
}
|
||
SimpleInnerSubscriber.prototype._next = function (value) {
|
||
this.parent.notifyNext(value);
|
||
};
|
||
SimpleInnerSubscriber.prototype._error = function (error) {
|
||
this.parent.notifyError(error);
|
||
this.unsubscribe();
|
||
};
|
||
SimpleInnerSubscriber.prototype._complete = function () {
|
||
this.parent.notifyComplete();
|
||
this.unsubscribe();
|
||
};
|
||
return SimpleInnerSubscriber;
|
||
}(Subscriber));
|
||
var SimpleOuterSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(SimpleOuterSubscriber, _super);
|
||
function SimpleOuterSubscriber() {
|
||
return _super !== null && _super.apply(this, arguments) || this;
|
||
}
|
||
SimpleOuterSubscriber.prototype.notifyNext = function (innerValue) {
|
||
this.destination.next(innerValue);
|
||
};
|
||
SimpleOuterSubscriber.prototype.notifyError = function (err) {
|
||
this.destination.error(err);
|
||
};
|
||
SimpleOuterSubscriber.prototype.notifyComplete = function () {
|
||
this.destination.complete();
|
||
};
|
||
return SimpleOuterSubscriber;
|
||
}(Subscriber));
|
||
function innerSubscribe(result, innerSubscriber) {
|
||
if (innerSubscriber.closed) {
|
||
return undefined;
|
||
}
|
||
if (result instanceof Observable) {
|
||
return result.subscribe(innerSubscriber);
|
||
}
|
||
return subscribeTo(result)(innerSubscriber);
|
||
}
|
||
|
||
/** PURE_IMPORTS_START tslib,_map,_observable_from,_innerSubscribe PURE_IMPORTS_END */
|
||
function mergeMap(project, resultSelector, concurrent) {
|
||
if (concurrent === void 0) {
|
||
concurrent = Number.POSITIVE_INFINITY;
|
||
}
|
||
if (typeof resultSelector === 'function') {
|
||
return function (source) { return source.pipe(mergeMap(function (a, i) { return from(project(a, i)).pipe(map(function (b, ii) { return resultSelector(a, b, i, ii); })); }, concurrent)); };
|
||
}
|
||
else if (typeof resultSelector === 'number') {
|
||
concurrent = resultSelector;
|
||
}
|
||
return function (source) { return source.lift(new MergeMapOperator(project, concurrent)); };
|
||
}
|
||
var MergeMapOperator = /*@__PURE__*/ (function () {
|
||
function MergeMapOperator(project, concurrent) {
|
||
if (concurrent === void 0) {
|
||
concurrent = Number.POSITIVE_INFINITY;
|
||
}
|
||
this.project = project;
|
||
this.concurrent = concurrent;
|
||
}
|
||
MergeMapOperator.prototype.call = function (observer, source) {
|
||
return source.subscribe(new MergeMapSubscriber(observer, this.project, this.concurrent));
|
||
};
|
||
return MergeMapOperator;
|
||
}());
|
||
var MergeMapSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(MergeMapSubscriber, _super);
|
||
function MergeMapSubscriber(destination, project, concurrent) {
|
||
if (concurrent === void 0) {
|
||
concurrent = Number.POSITIVE_INFINITY;
|
||
}
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.project = project;
|
||
_this.concurrent = concurrent;
|
||
_this.hasCompleted = false;
|
||
_this.buffer = [];
|
||
_this.active = 0;
|
||
_this.index = 0;
|
||
return _this;
|
||
}
|
||
MergeMapSubscriber.prototype._next = function (value) {
|
||
if (this.active < this.concurrent) {
|
||
this._tryNext(value);
|
||
}
|
||
else {
|
||
this.buffer.push(value);
|
||
}
|
||
};
|
||
MergeMapSubscriber.prototype._tryNext = function (value) {
|
||
var result;
|
||
var index = this.index++;
|
||
try {
|
||
result = this.project(value, index);
|
||
}
|
||
catch (err) {
|
||
this.destination.error(err);
|
||
return;
|
||
}
|
||
this.active++;
|
||
this._innerSub(result);
|
||
};
|
||
MergeMapSubscriber.prototype._innerSub = function (ish) {
|
||
var innerSubscriber = new SimpleInnerSubscriber(this);
|
||
var destination = this.destination;
|
||
destination.add(innerSubscriber);
|
||
var innerSubscription = innerSubscribe(ish, innerSubscriber);
|
||
if (innerSubscription !== innerSubscriber) {
|
||
destination.add(innerSubscription);
|
||
}
|
||
};
|
||
MergeMapSubscriber.prototype._complete = function () {
|
||
this.hasCompleted = true;
|
||
if (this.active === 0 && this.buffer.length === 0) {
|
||
this.destination.complete();
|
||
}
|
||
this.unsubscribe();
|
||
};
|
||
MergeMapSubscriber.prototype.notifyNext = function (innerValue) {
|
||
this.destination.next(innerValue);
|
||
};
|
||
MergeMapSubscriber.prototype.notifyComplete = function () {
|
||
var buffer = this.buffer;
|
||
this.active--;
|
||
if (buffer.length > 0) {
|
||
this._next(buffer.shift());
|
||
}
|
||
else if (this.active === 0 && this.hasCompleted) {
|
||
this.destination.complete();
|
||
}
|
||
};
|
||
return MergeMapSubscriber;
|
||
}(SimpleOuterSubscriber));
|
||
|
||
/** PURE_IMPORTS_START _mergeMap,_util_identity PURE_IMPORTS_END */
|
||
function mergeAll(concurrent) {
|
||
if (concurrent === void 0) {
|
||
concurrent = Number.POSITIVE_INFINITY;
|
||
}
|
||
return mergeMap(identity, concurrent);
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _mergeAll PURE_IMPORTS_END */
|
||
function concatAll() {
|
||
return mergeAll(1);
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _of,_operators_concatAll PURE_IMPORTS_END */
|
||
function concat() {
|
||
var observables = [];
|
||
for (var _i = 0; _i < arguments.length; _i++) {
|
||
observables[_i] = arguments[_i];
|
||
}
|
||
return concatAll()(of.apply(void 0, observables));
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _Observable,_from,_empty PURE_IMPORTS_END */
|
||
function defer(observableFactory) {
|
||
return new Observable(function (subscriber) {
|
||
var input;
|
||
try {
|
||
input = observableFactory();
|
||
}
|
||
catch (err) {
|
||
subscriber.error(err);
|
||
return undefined;
|
||
}
|
||
var source = input ? from(input) : empty();
|
||
return source.subscribe(subscriber);
|
||
});
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _Observable,_util_isArray,_operators_map,_util_isObject,_from PURE_IMPORTS_END */
|
||
function forkJoin() {
|
||
var sources = [];
|
||
for (var _i = 0; _i < arguments.length; _i++) {
|
||
sources[_i] = arguments[_i];
|
||
}
|
||
if (sources.length === 1) {
|
||
var first_1 = sources[0];
|
||
if (isArray$1(first_1)) {
|
||
return forkJoinInternal(first_1, null);
|
||
}
|
||
if (isObject$1(first_1) && Object.getPrototypeOf(first_1) === Object.prototype) {
|
||
var keys = Object.keys(first_1);
|
||
return forkJoinInternal(keys.map(function (key) { return first_1[key]; }), keys);
|
||
}
|
||
}
|
||
if (typeof sources[sources.length - 1] === 'function') {
|
||
var resultSelector_1 = sources.pop();
|
||
sources = (sources.length === 1 && isArray$1(sources[0])) ? sources[0] : sources;
|
||
return forkJoinInternal(sources, null).pipe(map(function (args) { return resultSelector_1.apply(void 0, args); }));
|
||
}
|
||
return forkJoinInternal(sources, null);
|
||
}
|
||
function forkJoinInternal(sources, keys) {
|
||
return new Observable(function (subscriber) {
|
||
var len = sources.length;
|
||
if (len === 0) {
|
||
subscriber.complete();
|
||
return;
|
||
}
|
||
var values = new Array(len);
|
||
var completed = 0;
|
||
var emitted = 0;
|
||
var _loop_1 = function (i) {
|
||
var source = from(sources[i]);
|
||
var hasValue = false;
|
||
subscriber.add(source.subscribe({
|
||
next: function (value) {
|
||
if (!hasValue) {
|
||
hasValue = true;
|
||
emitted++;
|
||
}
|
||
values[i] = value;
|
||
},
|
||
error: function (err) { return subscriber.error(err); },
|
||
complete: function () {
|
||
completed++;
|
||
if (completed === len || !hasValue) {
|
||
if (emitted === len) {
|
||
subscriber.next(keys ?
|
||
keys.reduce(function (result, key, i) { return (result[key] = values[i], result); }, {}) :
|
||
values);
|
||
}
|
||
subscriber.complete();
|
||
}
|
||
}
|
||
}));
|
||
};
|
||
for (var i = 0; i < len; i++) {
|
||
_loop_1(i);
|
||
}
|
||
});
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _Observable,_util_isArray,_util_isFunction,_operators_map PURE_IMPORTS_END */
|
||
function fromEvent(target, eventName, options, resultSelector) {
|
||
if (isFunction$1(options)) {
|
||
resultSelector = options;
|
||
options = undefined;
|
||
}
|
||
if (resultSelector) {
|
||
return fromEvent(target, eventName, options).pipe(map(function (args) { return isArray$1(args) ? resultSelector.apply(void 0, args) : resultSelector(args); }));
|
||
}
|
||
return new Observable(function (subscriber) {
|
||
function handler(e) {
|
||
if (arguments.length > 1) {
|
||
subscriber.next(Array.prototype.slice.call(arguments));
|
||
}
|
||
else {
|
||
subscriber.next(e);
|
||
}
|
||
}
|
||
setupSubscription(target, eventName, handler, subscriber, options);
|
||
});
|
||
}
|
||
function setupSubscription(sourceObj, eventName, handler, subscriber, options) {
|
||
var unsubscribe;
|
||
if (isEventTarget(sourceObj)) {
|
||
var source_1 = sourceObj;
|
||
sourceObj.addEventListener(eventName, handler, options);
|
||
unsubscribe = function () { return source_1.removeEventListener(eventName, handler, options); };
|
||
}
|
||
else if (isJQueryStyleEventEmitter(sourceObj)) {
|
||
var source_2 = sourceObj;
|
||
sourceObj.on(eventName, handler);
|
||
unsubscribe = function () { return source_2.off(eventName, handler); };
|
||
}
|
||
else if (isNodeStyleEventEmitter(sourceObj)) {
|
||
var source_3 = sourceObj;
|
||
sourceObj.addListener(eventName, handler);
|
||
unsubscribe = function () { return source_3.removeListener(eventName, handler); };
|
||
}
|
||
else if (sourceObj && sourceObj.length) {
|
||
for (var i = 0, len = sourceObj.length; i < len; i++) {
|
||
setupSubscription(sourceObj[i], eventName, handler, subscriber, options);
|
||
}
|
||
}
|
||
else {
|
||
throw new TypeError('Invalid event target');
|
||
}
|
||
subscriber.add(unsubscribe);
|
||
}
|
||
function isNodeStyleEventEmitter(sourceObj) {
|
||
return sourceObj && typeof sourceObj.addListener === 'function' && typeof sourceObj.removeListener === 'function';
|
||
}
|
||
function isJQueryStyleEventEmitter(sourceObj) {
|
||
return sourceObj && typeof sourceObj.on === 'function' && typeof sourceObj.off === 'function';
|
||
}
|
||
function isEventTarget(sourceObj) {
|
||
return sourceObj && typeof sourceObj.addEventListener === 'function' && typeof sourceObj.removeEventListener === 'function';
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _defer,_empty PURE_IMPORTS_END */
|
||
function iif(condition, trueResult, falseResult) {
|
||
if (trueResult === void 0) {
|
||
trueResult = EMPTY;
|
||
}
|
||
if (falseResult === void 0) {
|
||
falseResult = EMPTY;
|
||
}
|
||
return defer(function () { return condition() ? trueResult : falseResult; });
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _isArray PURE_IMPORTS_END */
|
||
function isNumeric(val) {
|
||
return !isArray$1(val) && (val - parseFloat(val) + 1) >= 0;
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _Observable,_util_isScheduler,_operators_mergeAll,_fromArray PURE_IMPORTS_END */
|
||
function merge() {
|
||
var observables = [];
|
||
for (var _i = 0; _i < arguments.length; _i++) {
|
||
observables[_i] = arguments[_i];
|
||
}
|
||
var concurrent = Number.POSITIVE_INFINITY;
|
||
var scheduler = null;
|
||
var last = observables[observables.length - 1];
|
||
if (isScheduler(last)) {
|
||
scheduler = observables.pop();
|
||
if (observables.length > 1 && typeof observables[observables.length - 1] === 'number') {
|
||
concurrent = observables.pop();
|
||
}
|
||
}
|
||
else if (typeof last === 'number') {
|
||
concurrent = observables.pop();
|
||
}
|
||
if (scheduler === null && observables.length === 1 && observables[0] instanceof Observable) {
|
||
return observables[0];
|
||
}
|
||
return mergeAll(concurrent)(fromArray(observables, scheduler));
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _Observable,_util_noop PURE_IMPORTS_END */
|
||
var NEVER = /*@__PURE__*/ new Observable(noop);
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */
|
||
function filter(predicate, thisArg) {
|
||
return function filterOperatorFunction(source) {
|
||
return source.lift(new FilterOperator(predicate, thisArg));
|
||
};
|
||
}
|
||
var FilterOperator = /*@__PURE__*/ (function () {
|
||
function FilterOperator(predicate, thisArg) {
|
||
this.predicate = predicate;
|
||
this.thisArg = thisArg;
|
||
}
|
||
FilterOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new FilterSubscriber(subscriber, this.predicate, this.thisArg));
|
||
};
|
||
return FilterOperator;
|
||
}());
|
||
var FilterSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(FilterSubscriber, _super);
|
||
function FilterSubscriber(destination, predicate, thisArg) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.predicate = predicate;
|
||
_this.thisArg = thisArg;
|
||
_this.count = 0;
|
||
return _this;
|
||
}
|
||
FilterSubscriber.prototype._next = function (value) {
|
||
var result;
|
||
try {
|
||
result = this.predicate.call(this.thisArg, value, this.count++);
|
||
}
|
||
catch (err) {
|
||
this.destination.error(err);
|
||
return;
|
||
}
|
||
if (result) {
|
||
this.destination.next(value);
|
||
}
|
||
};
|
||
return FilterSubscriber;
|
||
}(Subscriber));
|
||
|
||
/** PURE_IMPORTS_START tslib,_util_isArray,_fromArray,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */
|
||
function race() {
|
||
var observables = [];
|
||
for (var _i = 0; _i < arguments.length; _i++) {
|
||
observables[_i] = arguments[_i];
|
||
}
|
||
if (observables.length === 1) {
|
||
if (isArray$1(observables[0])) {
|
||
observables = observables[0];
|
||
}
|
||
else {
|
||
return observables[0];
|
||
}
|
||
}
|
||
return fromArray(observables, undefined).lift(new RaceOperator());
|
||
}
|
||
var RaceOperator = /*@__PURE__*/ (function () {
|
||
function RaceOperator() {
|
||
}
|
||
RaceOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new RaceSubscriber(subscriber));
|
||
};
|
||
return RaceOperator;
|
||
}());
|
||
var RaceSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(RaceSubscriber, _super);
|
||
function RaceSubscriber(destination) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.hasFirst = false;
|
||
_this.observables = [];
|
||
_this.subscriptions = [];
|
||
return _this;
|
||
}
|
||
RaceSubscriber.prototype._next = function (observable) {
|
||
this.observables.push(observable);
|
||
};
|
||
RaceSubscriber.prototype._complete = function () {
|
||
var observables = this.observables;
|
||
var len = observables.length;
|
||
if (len === 0) {
|
||
this.destination.complete();
|
||
}
|
||
else {
|
||
for (var i = 0; i < len && !this.hasFirst; i++) {
|
||
var observable = observables[i];
|
||
var subscription = subscribeToResult(this, observable, undefined, i);
|
||
if (this.subscriptions) {
|
||
this.subscriptions.push(subscription);
|
||
}
|
||
this.add(subscription);
|
||
}
|
||
this.observables = null;
|
||
}
|
||
};
|
||
RaceSubscriber.prototype.notifyNext = function (_outerValue, innerValue, outerIndex) {
|
||
if (!this.hasFirst) {
|
||
this.hasFirst = true;
|
||
for (var i = 0; i < this.subscriptions.length; i++) {
|
||
if (i !== outerIndex) {
|
||
var subscription = this.subscriptions[i];
|
||
subscription.unsubscribe();
|
||
this.remove(subscription);
|
||
}
|
||
}
|
||
this.subscriptions = null;
|
||
}
|
||
this.destination.next(innerValue);
|
||
};
|
||
return RaceSubscriber;
|
||
}(OuterSubscriber));
|
||
|
||
/** PURE_IMPORTS_START _Observable,_scheduler_async,_util_isNumeric,_util_isScheduler PURE_IMPORTS_END */
|
||
function timer(dueTime, periodOrScheduler, scheduler) {
|
||
if (dueTime === void 0) {
|
||
dueTime = 0;
|
||
}
|
||
var period = -1;
|
||
if (isNumeric(periodOrScheduler)) {
|
||
period = Number(periodOrScheduler) < 1 && 1 || Number(periodOrScheduler);
|
||
}
|
||
else if (isScheduler(periodOrScheduler)) {
|
||
scheduler = periodOrScheduler;
|
||
}
|
||
if (!isScheduler(scheduler)) {
|
||
scheduler = async;
|
||
}
|
||
return new Observable(function (subscriber) {
|
||
var due = isNumeric(dueTime)
|
||
? dueTime
|
||
: (+dueTime - scheduler.now());
|
||
return scheduler.schedule(dispatch, due, {
|
||
index: 0, period: period, subscriber: subscriber
|
||
});
|
||
});
|
||
}
|
||
function dispatch(state) {
|
||
var index = state.index, period = state.period, subscriber = state.subscriber;
|
||
subscriber.next(index);
|
||
if (subscriber.closed) {
|
||
return;
|
||
}
|
||
else if (period === -1) {
|
||
return subscriber.complete();
|
||
}
|
||
state.index = index + 1;
|
||
this.schedule(state, period);
|
||
}
|
||
|
||
/** PURE_IMPORTS_START tslib,_fromArray,_util_isArray,_Subscriber,_.._internal_symbol_iterator,_innerSubscribe PURE_IMPORTS_END */
|
||
function zip() {
|
||
var observables = [];
|
||
for (var _i = 0; _i < arguments.length; _i++) {
|
||
observables[_i] = arguments[_i];
|
||
}
|
||
var resultSelector = observables[observables.length - 1];
|
||
if (typeof resultSelector === 'function') {
|
||
observables.pop();
|
||
}
|
||
return fromArray(observables, undefined).lift(new ZipOperator(resultSelector));
|
||
}
|
||
var ZipOperator = /*@__PURE__*/ (function () {
|
||
function ZipOperator(resultSelector) {
|
||
this.resultSelector = resultSelector;
|
||
}
|
||
ZipOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new ZipSubscriber(subscriber, this.resultSelector));
|
||
};
|
||
return ZipOperator;
|
||
}());
|
||
var ZipSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(ZipSubscriber, _super);
|
||
function ZipSubscriber(destination, resultSelector, values) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.resultSelector = resultSelector;
|
||
_this.iterators = [];
|
||
_this.active = 0;
|
||
_this.resultSelector = (typeof resultSelector === 'function') ? resultSelector : undefined;
|
||
return _this;
|
||
}
|
||
ZipSubscriber.prototype._next = function (value) {
|
||
var iterators = this.iterators;
|
||
if (isArray$1(value)) {
|
||
iterators.push(new StaticArrayIterator(value));
|
||
}
|
||
else if (typeof value[iterator] === 'function') {
|
||
iterators.push(new StaticIterator(value[iterator]()));
|
||
}
|
||
else {
|
||
iterators.push(new ZipBufferIterator(this.destination, this, value));
|
||
}
|
||
};
|
||
ZipSubscriber.prototype._complete = function () {
|
||
var iterators = this.iterators;
|
||
var len = iterators.length;
|
||
this.unsubscribe();
|
||
if (len === 0) {
|
||
this.destination.complete();
|
||
return;
|
||
}
|
||
this.active = len;
|
||
for (var i = 0; i < len; i++) {
|
||
var iterator = iterators[i];
|
||
if (iterator.stillUnsubscribed) {
|
||
var destination = this.destination;
|
||
destination.add(iterator.subscribe());
|
||
}
|
||
else {
|
||
this.active--;
|
||
}
|
||
}
|
||
};
|
||
ZipSubscriber.prototype.notifyInactive = function () {
|
||
this.active--;
|
||
if (this.active === 0) {
|
||
this.destination.complete();
|
||
}
|
||
};
|
||
ZipSubscriber.prototype.checkIterators = function () {
|
||
var iterators = this.iterators;
|
||
var len = iterators.length;
|
||
var destination = this.destination;
|
||
for (var i = 0; i < len; i++) {
|
||
var iterator = iterators[i];
|
||
if (typeof iterator.hasValue === 'function' && !iterator.hasValue()) {
|
||
return;
|
||
}
|
||
}
|
||
var shouldComplete = false;
|
||
var args = [];
|
||
for (var i = 0; i < len; i++) {
|
||
var iterator = iterators[i];
|
||
var result = iterator.next();
|
||
if (iterator.hasCompleted()) {
|
||
shouldComplete = true;
|
||
}
|
||
if (result.done) {
|
||
destination.complete();
|
||
return;
|
||
}
|
||
args.push(result.value);
|
||
}
|
||
if (this.resultSelector) {
|
||
this._tryresultSelector(args);
|
||
}
|
||
else {
|
||
destination.next(args);
|
||
}
|
||
if (shouldComplete) {
|
||
destination.complete();
|
||
}
|
||
};
|
||
ZipSubscriber.prototype._tryresultSelector = function (args) {
|
||
var result;
|
||
try {
|
||
result = this.resultSelector.apply(this, args);
|
||
}
|
||
catch (err) {
|
||
this.destination.error(err);
|
||
return;
|
||
}
|
||
this.destination.next(result);
|
||
};
|
||
return ZipSubscriber;
|
||
}(Subscriber));
|
||
var StaticIterator = /*@__PURE__*/ (function () {
|
||
function StaticIterator(iterator) {
|
||
this.iterator = iterator;
|
||
this.nextResult = iterator.next();
|
||
}
|
||
StaticIterator.prototype.hasValue = function () {
|
||
return true;
|
||
};
|
||
StaticIterator.prototype.next = function () {
|
||
var result = this.nextResult;
|
||
this.nextResult = this.iterator.next();
|
||
return result;
|
||
};
|
||
StaticIterator.prototype.hasCompleted = function () {
|
||
var nextResult = this.nextResult;
|
||
return Boolean(nextResult && nextResult.done);
|
||
};
|
||
return StaticIterator;
|
||
}());
|
||
var StaticArrayIterator = /*@__PURE__*/ (function () {
|
||
function StaticArrayIterator(array) {
|
||
this.array = array;
|
||
this.index = 0;
|
||
this.length = 0;
|
||
this.length = array.length;
|
||
}
|
||
StaticArrayIterator.prototype[iterator] = function () {
|
||
return this;
|
||
};
|
||
StaticArrayIterator.prototype.next = function (value) {
|
||
var i = this.index++;
|
||
var array = this.array;
|
||
return i < this.length ? { value: array[i], done: false } : { value: null, done: true };
|
||
};
|
||
StaticArrayIterator.prototype.hasValue = function () {
|
||
return this.array.length > this.index;
|
||
};
|
||
StaticArrayIterator.prototype.hasCompleted = function () {
|
||
return this.array.length === this.index;
|
||
};
|
||
return StaticArrayIterator;
|
||
}());
|
||
var ZipBufferIterator = /*@__PURE__*/ (function (_super) {
|
||
__extends(ZipBufferIterator, _super);
|
||
function ZipBufferIterator(destination, parent, observable) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.parent = parent;
|
||
_this.observable = observable;
|
||
_this.stillUnsubscribed = true;
|
||
_this.buffer = [];
|
||
_this.isComplete = false;
|
||
return _this;
|
||
}
|
||
ZipBufferIterator.prototype[iterator] = function () {
|
||
return this;
|
||
};
|
||
ZipBufferIterator.prototype.next = function () {
|
||
var buffer = this.buffer;
|
||
if (buffer.length === 0 && this.isComplete) {
|
||
return { value: null, done: true };
|
||
}
|
||
else {
|
||
return { value: buffer.shift(), done: false };
|
||
}
|
||
};
|
||
ZipBufferIterator.prototype.hasValue = function () {
|
||
return this.buffer.length > 0;
|
||
};
|
||
ZipBufferIterator.prototype.hasCompleted = function () {
|
||
return this.buffer.length === 0 && this.isComplete;
|
||
};
|
||
ZipBufferIterator.prototype.notifyComplete = function () {
|
||
if (this.buffer.length > 0) {
|
||
this.isComplete = true;
|
||
this.parent.notifyInactive();
|
||
}
|
||
else {
|
||
this.destination.complete();
|
||
}
|
||
};
|
||
ZipBufferIterator.prototype.notifyNext = function (innerValue) {
|
||
this.buffer.push(innerValue);
|
||
this.parent.checkIterators();
|
||
};
|
||
ZipBufferIterator.prototype.subscribe = function () {
|
||
return innerSubscribe(this.observable, new SimpleInnerSubscriber(this));
|
||
};
|
||
return ZipBufferIterator;
|
||
}(SimpleOuterSubscriber));
|
||
|
||
/** PURE_IMPORTS_START tslib,_innerSubscribe PURE_IMPORTS_END */
|
||
function audit(durationSelector) {
|
||
return function auditOperatorFunction(source) {
|
||
return source.lift(new AuditOperator(durationSelector));
|
||
};
|
||
}
|
||
var AuditOperator = /*@__PURE__*/ (function () {
|
||
function AuditOperator(durationSelector) {
|
||
this.durationSelector = durationSelector;
|
||
}
|
||
AuditOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new AuditSubscriber(subscriber, this.durationSelector));
|
||
};
|
||
return AuditOperator;
|
||
}());
|
||
var AuditSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(AuditSubscriber, _super);
|
||
function AuditSubscriber(destination, durationSelector) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.durationSelector = durationSelector;
|
||
_this.hasValue = false;
|
||
return _this;
|
||
}
|
||
AuditSubscriber.prototype._next = function (value) {
|
||
this.value = value;
|
||
this.hasValue = true;
|
||
if (!this.throttled) {
|
||
var duration = void 0;
|
||
try {
|
||
var durationSelector = this.durationSelector;
|
||
duration = durationSelector(value);
|
||
}
|
||
catch (err) {
|
||
return this.destination.error(err);
|
||
}
|
||
var innerSubscription = innerSubscribe(duration, new SimpleInnerSubscriber(this));
|
||
if (!innerSubscription || innerSubscription.closed) {
|
||
this.clearThrottle();
|
||
}
|
||
else {
|
||
this.add(this.throttled = innerSubscription);
|
||
}
|
||
}
|
||
};
|
||
AuditSubscriber.prototype.clearThrottle = function () {
|
||
var _a = this, value = _a.value, hasValue = _a.hasValue, throttled = _a.throttled;
|
||
if (throttled) {
|
||
this.remove(throttled);
|
||
this.throttled = undefined;
|
||
throttled.unsubscribe();
|
||
}
|
||
if (hasValue) {
|
||
this.value = undefined;
|
||
this.hasValue = false;
|
||
this.destination.next(value);
|
||
}
|
||
};
|
||
AuditSubscriber.prototype.notifyNext = function () {
|
||
this.clearThrottle();
|
||
};
|
||
AuditSubscriber.prototype.notifyComplete = function () {
|
||
this.clearThrottle();
|
||
};
|
||
return AuditSubscriber;
|
||
}(SimpleOuterSubscriber));
|
||
|
||
/** PURE_IMPORTS_START _scheduler_async,_audit,_observable_timer PURE_IMPORTS_END */
|
||
function auditTime(duration, scheduler) {
|
||
if (scheduler === void 0) {
|
||
scheduler = async;
|
||
}
|
||
return audit(function () { return timer(duration, scheduler); });
|
||
}
|
||
|
||
/** PURE_IMPORTS_START tslib,_innerSubscribe PURE_IMPORTS_END */
|
||
function catchError(selector) {
|
||
return function catchErrorOperatorFunction(source) {
|
||
var operator = new CatchOperator(selector);
|
||
var caught = source.lift(operator);
|
||
return (operator.caught = caught);
|
||
};
|
||
}
|
||
var CatchOperator = /*@__PURE__*/ (function () {
|
||
function CatchOperator(selector) {
|
||
this.selector = selector;
|
||
}
|
||
CatchOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new CatchSubscriber(subscriber, this.selector, this.caught));
|
||
};
|
||
return CatchOperator;
|
||
}());
|
||
var CatchSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(CatchSubscriber, _super);
|
||
function CatchSubscriber(destination, selector, caught) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.selector = selector;
|
||
_this.caught = caught;
|
||
return _this;
|
||
}
|
||
CatchSubscriber.prototype.error = function (err) {
|
||
if (!this.isStopped) {
|
||
var result = void 0;
|
||
try {
|
||
result = this.selector(err, this.caught);
|
||
}
|
||
catch (err2) {
|
||
_super.prototype.error.call(this, err2);
|
||
return;
|
||
}
|
||
this._unsubscribeAndRecycle();
|
||
var innerSubscriber = new SimpleInnerSubscriber(this);
|
||
this.add(innerSubscriber);
|
||
var innerSubscription = innerSubscribe(result, innerSubscriber);
|
||
if (innerSubscription !== innerSubscriber) {
|
||
this.add(innerSubscription);
|
||
}
|
||
}
|
||
};
|
||
return CatchSubscriber;
|
||
}(SimpleOuterSubscriber));
|
||
|
||
/** PURE_IMPORTS_START _mergeMap PURE_IMPORTS_END */
|
||
function concatMap(project, resultSelector) {
|
||
return mergeMap(project, resultSelector, 1);
|
||
}
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async PURE_IMPORTS_END */
|
||
function debounceTime(dueTime, scheduler) {
|
||
if (scheduler === void 0) {
|
||
scheduler = async;
|
||
}
|
||
return function (source) { return source.lift(new DebounceTimeOperator(dueTime, scheduler)); };
|
||
}
|
||
var DebounceTimeOperator = /*@__PURE__*/ (function () {
|
||
function DebounceTimeOperator(dueTime, scheduler) {
|
||
this.dueTime = dueTime;
|
||
this.scheduler = scheduler;
|
||
}
|
||
DebounceTimeOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new DebounceTimeSubscriber(subscriber, this.dueTime, this.scheduler));
|
||
};
|
||
return DebounceTimeOperator;
|
||
}());
|
||
var DebounceTimeSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(DebounceTimeSubscriber, _super);
|
||
function DebounceTimeSubscriber(destination, dueTime, scheduler) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.dueTime = dueTime;
|
||
_this.scheduler = scheduler;
|
||
_this.debouncedSubscription = null;
|
||
_this.lastValue = null;
|
||
_this.hasValue = false;
|
||
return _this;
|
||
}
|
||
DebounceTimeSubscriber.prototype._next = function (value) {
|
||
this.clearDebounce();
|
||
this.lastValue = value;
|
||
this.hasValue = true;
|
||
this.add(this.debouncedSubscription = this.scheduler.schedule(dispatchNext$1, this.dueTime, this));
|
||
};
|
||
DebounceTimeSubscriber.prototype._complete = function () {
|
||
this.debouncedNext();
|
||
this.destination.complete();
|
||
};
|
||
DebounceTimeSubscriber.prototype.debouncedNext = function () {
|
||
this.clearDebounce();
|
||
if (this.hasValue) {
|
||
var lastValue = this.lastValue;
|
||
this.lastValue = null;
|
||
this.hasValue = false;
|
||
this.destination.next(lastValue);
|
||
}
|
||
};
|
||
DebounceTimeSubscriber.prototype.clearDebounce = function () {
|
||
var debouncedSubscription = this.debouncedSubscription;
|
||
if (debouncedSubscription !== null) {
|
||
this.remove(debouncedSubscription);
|
||
debouncedSubscription.unsubscribe();
|
||
this.debouncedSubscription = null;
|
||
}
|
||
};
|
||
return DebounceTimeSubscriber;
|
||
}(Subscriber));
|
||
function dispatchNext$1(subscriber) {
|
||
subscriber.debouncedNext();
|
||
}
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */
|
||
function defaultIfEmpty(defaultValue) {
|
||
if (defaultValue === void 0) {
|
||
defaultValue = null;
|
||
}
|
||
return function (source) { return source.lift(new DefaultIfEmptyOperator(defaultValue)); };
|
||
}
|
||
var DefaultIfEmptyOperator = /*@__PURE__*/ (function () {
|
||
function DefaultIfEmptyOperator(defaultValue) {
|
||
this.defaultValue = defaultValue;
|
||
}
|
||
DefaultIfEmptyOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new DefaultIfEmptySubscriber(subscriber, this.defaultValue));
|
||
};
|
||
return DefaultIfEmptyOperator;
|
||
}());
|
||
var DefaultIfEmptySubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(DefaultIfEmptySubscriber, _super);
|
||
function DefaultIfEmptySubscriber(destination, defaultValue) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.defaultValue = defaultValue;
|
||
_this.isEmpty = true;
|
||
return _this;
|
||
}
|
||
DefaultIfEmptySubscriber.prototype._next = function (value) {
|
||
this.isEmpty = false;
|
||
this.destination.next(value);
|
||
};
|
||
DefaultIfEmptySubscriber.prototype._complete = function () {
|
||
if (this.isEmpty) {
|
||
this.destination.next(this.defaultValue);
|
||
}
|
||
this.destination.complete();
|
||
};
|
||
return DefaultIfEmptySubscriber;
|
||
}(Subscriber));
|
||
|
||
/** PURE_IMPORTS_START PURE_IMPORTS_END */
|
||
function isDate(value) {
|
||
return value instanceof Date && !isNaN(+value);
|
||
}
|
||
|
||
/** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_Subscriber,_Notification PURE_IMPORTS_END */
|
||
function delay(delay, scheduler) {
|
||
if (scheduler === void 0) {
|
||
scheduler = async;
|
||
}
|
||
var absoluteDelay = isDate(delay);
|
||
var delayFor = absoluteDelay ? (+delay - scheduler.now()) : Math.abs(delay);
|
||
return function (source) { return source.lift(new DelayOperator(delayFor, scheduler)); };
|
||
}
|
||
var DelayOperator = /*@__PURE__*/ (function () {
|
||
function DelayOperator(delay, scheduler) {
|
||
this.delay = delay;
|
||
this.scheduler = scheduler;
|
||
}
|
||
DelayOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new DelaySubscriber(subscriber, this.delay, this.scheduler));
|
||
};
|
||
return DelayOperator;
|
||
}());
|
||
var DelaySubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(DelaySubscriber, _super);
|
||
function DelaySubscriber(destination, delay, scheduler) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.delay = delay;
|
||
_this.scheduler = scheduler;
|
||
_this.queue = [];
|
||
_this.active = false;
|
||
_this.errored = false;
|
||
return _this;
|
||
}
|
||
DelaySubscriber.dispatch = function (state) {
|
||
var source = state.source;
|
||
var queue = source.queue;
|
||
var scheduler = state.scheduler;
|
||
var destination = state.destination;
|
||
while (queue.length > 0 && (queue[0].time - scheduler.now()) <= 0) {
|
||
queue.shift().notification.observe(destination);
|
||
}
|
||
if (queue.length > 0) {
|
||
var delay_1 = Math.max(0, queue[0].time - scheduler.now());
|
||
this.schedule(state, delay_1);
|
||
}
|
||
else {
|
||
this.unsubscribe();
|
||
source.active = false;
|
||
}
|
||
};
|
||
DelaySubscriber.prototype._schedule = function (scheduler) {
|
||
this.active = true;
|
||
var destination = this.destination;
|
||
destination.add(scheduler.schedule(DelaySubscriber.dispatch, this.delay, {
|
||
source: this, destination: this.destination, scheduler: scheduler
|
||
}));
|
||
};
|
||
DelaySubscriber.prototype.scheduleNotification = function (notification) {
|
||
if (this.errored === true) {
|
||
return;
|
||
}
|
||
var scheduler = this.scheduler;
|
||
var message = new DelayMessage(scheduler.now() + this.delay, notification);
|
||
this.queue.push(message);
|
||
if (this.active === false) {
|
||
this._schedule(scheduler);
|
||
}
|
||
};
|
||
DelaySubscriber.prototype._next = function (value) {
|
||
this.scheduleNotification(Notification.createNext(value));
|
||
};
|
||
DelaySubscriber.prototype._error = function (err) {
|
||
this.errored = true;
|
||
this.queue = [];
|
||
this.destination.error(err);
|
||
this.unsubscribe();
|
||
};
|
||
DelaySubscriber.prototype._complete = function () {
|
||
this.scheduleNotification(Notification.createComplete());
|
||
this.unsubscribe();
|
||
};
|
||
return DelaySubscriber;
|
||
}(Subscriber));
|
||
var DelayMessage = /*@__PURE__*/ (function () {
|
||
function DelayMessage(time, notification) {
|
||
this.time = time;
|
||
this.notification = notification;
|
||
}
|
||
return DelayMessage;
|
||
}());
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber,_Observable,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */
|
||
function delayWhen(delayDurationSelector, subscriptionDelay) {
|
||
if (subscriptionDelay) {
|
||
return function (source) {
|
||
return new SubscriptionDelayObservable(source, subscriptionDelay)
|
||
.lift(new DelayWhenOperator(delayDurationSelector));
|
||
};
|
||
}
|
||
return function (source) { return source.lift(new DelayWhenOperator(delayDurationSelector)); };
|
||
}
|
||
var DelayWhenOperator = /*@__PURE__*/ (function () {
|
||
function DelayWhenOperator(delayDurationSelector) {
|
||
this.delayDurationSelector = delayDurationSelector;
|
||
}
|
||
DelayWhenOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new DelayWhenSubscriber(subscriber, this.delayDurationSelector));
|
||
};
|
||
return DelayWhenOperator;
|
||
}());
|
||
var DelayWhenSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(DelayWhenSubscriber, _super);
|
||
function DelayWhenSubscriber(destination, delayDurationSelector) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.delayDurationSelector = delayDurationSelector;
|
||
_this.completed = false;
|
||
_this.delayNotifierSubscriptions = [];
|
||
_this.index = 0;
|
||
return _this;
|
||
}
|
||
DelayWhenSubscriber.prototype.notifyNext = function (outerValue, _innerValue, _outerIndex, _innerIndex, innerSub) {
|
||
this.destination.next(outerValue);
|
||
this.removeSubscription(innerSub);
|
||
this.tryComplete();
|
||
};
|
||
DelayWhenSubscriber.prototype.notifyError = function (error, innerSub) {
|
||
this._error(error);
|
||
};
|
||
DelayWhenSubscriber.prototype.notifyComplete = function (innerSub) {
|
||
var value = this.removeSubscription(innerSub);
|
||
if (value) {
|
||
this.destination.next(value);
|
||
}
|
||
this.tryComplete();
|
||
};
|
||
DelayWhenSubscriber.prototype._next = function (value) {
|
||
var index = this.index++;
|
||
try {
|
||
var delayNotifier = this.delayDurationSelector(value, index);
|
||
if (delayNotifier) {
|
||
this.tryDelay(delayNotifier, value);
|
||
}
|
||
}
|
||
catch (err) {
|
||
this.destination.error(err);
|
||
}
|
||
};
|
||
DelayWhenSubscriber.prototype._complete = function () {
|
||
this.completed = true;
|
||
this.tryComplete();
|
||
this.unsubscribe();
|
||
};
|
||
DelayWhenSubscriber.prototype.removeSubscription = function (subscription) {
|
||
subscription.unsubscribe();
|
||
var subscriptionIdx = this.delayNotifierSubscriptions.indexOf(subscription);
|
||
if (subscriptionIdx !== -1) {
|
||
this.delayNotifierSubscriptions.splice(subscriptionIdx, 1);
|
||
}
|
||
return subscription.outerValue;
|
||
};
|
||
DelayWhenSubscriber.prototype.tryDelay = function (delayNotifier, value) {
|
||
var notifierSubscription = subscribeToResult(this, delayNotifier, value);
|
||
if (notifierSubscription && !notifierSubscription.closed) {
|
||
var destination = this.destination;
|
||
destination.add(notifierSubscription);
|
||
this.delayNotifierSubscriptions.push(notifierSubscription);
|
||
}
|
||
};
|
||
DelayWhenSubscriber.prototype.tryComplete = function () {
|
||
if (this.completed && this.delayNotifierSubscriptions.length === 0) {
|
||
this.destination.complete();
|
||
}
|
||
};
|
||
return DelayWhenSubscriber;
|
||
}(OuterSubscriber));
|
||
var SubscriptionDelayObservable = /*@__PURE__*/ (function (_super) {
|
||
__extends(SubscriptionDelayObservable, _super);
|
||
function SubscriptionDelayObservable(source, subscriptionDelay) {
|
||
var _this = _super.call(this) || this;
|
||
_this.source = source;
|
||
_this.subscriptionDelay = subscriptionDelay;
|
||
return _this;
|
||
}
|
||
SubscriptionDelayObservable.prototype._subscribe = function (subscriber) {
|
||
this.subscriptionDelay.subscribe(new SubscriptionDelaySubscriber(subscriber, this.source));
|
||
};
|
||
return SubscriptionDelayObservable;
|
||
}(Observable));
|
||
var SubscriptionDelaySubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(SubscriptionDelaySubscriber, _super);
|
||
function SubscriptionDelaySubscriber(parent, source) {
|
||
var _this = _super.call(this) || this;
|
||
_this.parent = parent;
|
||
_this.source = source;
|
||
_this.sourceSubscribed = false;
|
||
return _this;
|
||
}
|
||
SubscriptionDelaySubscriber.prototype._next = function (unused) {
|
||
this.subscribeToSource();
|
||
};
|
||
SubscriptionDelaySubscriber.prototype._error = function (err) {
|
||
this.unsubscribe();
|
||
this.parent.error(err);
|
||
};
|
||
SubscriptionDelaySubscriber.prototype._complete = function () {
|
||
this.unsubscribe();
|
||
this.subscribeToSource();
|
||
};
|
||
SubscriptionDelaySubscriber.prototype.subscribeToSource = function () {
|
||
if (!this.sourceSubscribed) {
|
||
this.sourceSubscribed = true;
|
||
this.unsubscribe();
|
||
this.source.subscribe(this.parent);
|
||
}
|
||
};
|
||
return SubscriptionDelaySubscriber;
|
||
}(Subscriber));
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */
|
||
function distinctUntilChanged(compare, keySelector) {
|
||
return function (source) { return source.lift(new DistinctUntilChangedOperator(compare, keySelector)); };
|
||
}
|
||
var DistinctUntilChangedOperator = /*@__PURE__*/ (function () {
|
||
function DistinctUntilChangedOperator(compare, keySelector) {
|
||
this.compare = compare;
|
||
this.keySelector = keySelector;
|
||
}
|
||
DistinctUntilChangedOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new DistinctUntilChangedSubscriber(subscriber, this.compare, this.keySelector));
|
||
};
|
||
return DistinctUntilChangedOperator;
|
||
}());
|
||
var DistinctUntilChangedSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(DistinctUntilChangedSubscriber, _super);
|
||
function DistinctUntilChangedSubscriber(destination, compare, keySelector) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.keySelector = keySelector;
|
||
_this.hasKey = false;
|
||
if (typeof compare === 'function') {
|
||
_this.compare = compare;
|
||
}
|
||
return _this;
|
||
}
|
||
DistinctUntilChangedSubscriber.prototype.compare = function (x, y) {
|
||
return x === y;
|
||
};
|
||
DistinctUntilChangedSubscriber.prototype._next = function (value) {
|
||
var key;
|
||
try {
|
||
var keySelector = this.keySelector;
|
||
key = keySelector ? keySelector(value) : value;
|
||
}
|
||
catch (err) {
|
||
return this.destination.error(err);
|
||
}
|
||
var result = false;
|
||
if (this.hasKey) {
|
||
try {
|
||
var compare = this.compare;
|
||
result = compare(this.key, key);
|
||
}
|
||
catch (err) {
|
||
return this.destination.error(err);
|
||
}
|
||
}
|
||
else {
|
||
this.hasKey = true;
|
||
}
|
||
if (!result) {
|
||
this.key = key;
|
||
this.destination.next(value);
|
||
}
|
||
};
|
||
return DistinctUntilChangedSubscriber;
|
||
}(Subscriber));
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber,_util_ArgumentOutOfRangeError,_observable_empty PURE_IMPORTS_END */
|
||
function take(count) {
|
||
return function (source) {
|
||
if (count === 0) {
|
||
return empty();
|
||
}
|
||
else {
|
||
return source.lift(new TakeOperator(count));
|
||
}
|
||
};
|
||
}
|
||
var TakeOperator = /*@__PURE__*/ (function () {
|
||
function TakeOperator(total) {
|
||
this.total = total;
|
||
if (this.total < 0) {
|
||
throw new ArgumentOutOfRangeError;
|
||
}
|
||
}
|
||
TakeOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new TakeSubscriber(subscriber, this.total));
|
||
};
|
||
return TakeOperator;
|
||
}());
|
||
var TakeSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(TakeSubscriber, _super);
|
||
function TakeSubscriber(destination, total) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.total = total;
|
||
_this.count = 0;
|
||
return _this;
|
||
}
|
||
TakeSubscriber.prototype._next = function (value) {
|
||
var total = this.total;
|
||
var count = ++this.count;
|
||
if (count <= total) {
|
||
this.destination.next(value);
|
||
if (count === total) {
|
||
this.destination.complete();
|
||
this.unsubscribe();
|
||
}
|
||
}
|
||
};
|
||
return TakeSubscriber;
|
||
}(Subscriber));
|
||
|
||
/** PURE_IMPORTS_START tslib,_map,_observable_from,_innerSubscribe PURE_IMPORTS_END */
|
||
function exhaustMap(project, resultSelector) {
|
||
if (resultSelector) {
|
||
return function (source) { return source.pipe(exhaustMap(function (a, i) { return from(project(a, i)).pipe(map(function (b, ii) { return resultSelector(a, b, i, ii); })); })); };
|
||
}
|
||
return function (source) {
|
||
return source.lift(new ExhaustMapOperator(project));
|
||
};
|
||
}
|
||
var ExhaustMapOperator = /*@__PURE__*/ (function () {
|
||
function ExhaustMapOperator(project) {
|
||
this.project = project;
|
||
}
|
||
ExhaustMapOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new ExhaustMapSubscriber(subscriber, this.project));
|
||
};
|
||
return ExhaustMapOperator;
|
||
}());
|
||
var ExhaustMapSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(ExhaustMapSubscriber, _super);
|
||
function ExhaustMapSubscriber(destination, project) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.project = project;
|
||
_this.hasSubscription = false;
|
||
_this.hasCompleted = false;
|
||
_this.index = 0;
|
||
return _this;
|
||
}
|
||
ExhaustMapSubscriber.prototype._next = function (value) {
|
||
if (!this.hasSubscription) {
|
||
this.tryNext(value);
|
||
}
|
||
};
|
||
ExhaustMapSubscriber.prototype.tryNext = function (value) {
|
||
var result;
|
||
var index = this.index++;
|
||
try {
|
||
result = this.project(value, index);
|
||
}
|
||
catch (err) {
|
||
this.destination.error(err);
|
||
return;
|
||
}
|
||
this.hasSubscription = true;
|
||
this._innerSub(result);
|
||
};
|
||
ExhaustMapSubscriber.prototype._innerSub = function (result) {
|
||
var innerSubscriber = new SimpleInnerSubscriber(this);
|
||
var destination = this.destination;
|
||
destination.add(innerSubscriber);
|
||
var innerSubscription = innerSubscribe(result, innerSubscriber);
|
||
if (innerSubscription !== innerSubscriber) {
|
||
destination.add(innerSubscription);
|
||
}
|
||
};
|
||
ExhaustMapSubscriber.prototype._complete = function () {
|
||
this.hasCompleted = true;
|
||
if (!this.hasSubscription) {
|
||
this.destination.complete();
|
||
}
|
||
this.unsubscribe();
|
||
};
|
||
ExhaustMapSubscriber.prototype.notifyNext = function (innerValue) {
|
||
this.destination.next(innerValue);
|
||
};
|
||
ExhaustMapSubscriber.prototype.notifyError = function (err) {
|
||
this.destination.error(err);
|
||
};
|
||
ExhaustMapSubscriber.prototype.notifyComplete = function () {
|
||
this.hasSubscription = false;
|
||
if (this.hasCompleted) {
|
||
this.destination.complete();
|
||
}
|
||
};
|
||
return ExhaustMapSubscriber;
|
||
}(SimpleOuterSubscriber));
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber,_Subscription PURE_IMPORTS_END */
|
||
function finalize$1(callback) {
|
||
return function (source) { return source.lift(new FinallyOperator(callback)); };
|
||
}
|
||
var FinallyOperator = /*@__PURE__*/ (function () {
|
||
function FinallyOperator(callback) {
|
||
this.callback = callback;
|
||
}
|
||
FinallyOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new FinallySubscriber(subscriber, this.callback));
|
||
};
|
||
return FinallyOperator;
|
||
}());
|
||
var FinallySubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(FinallySubscriber, _super);
|
||
function FinallySubscriber(destination, callback) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.add(new Subscription(callback));
|
||
return _this;
|
||
}
|
||
return FinallySubscriber;
|
||
}(Subscriber));
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber,_util_ArgumentOutOfRangeError,_observable_empty PURE_IMPORTS_END */
|
||
function takeLast(count) {
|
||
return function takeLastOperatorFunction(source) {
|
||
if (count === 0) {
|
||
return empty();
|
||
}
|
||
else {
|
||
return source.lift(new TakeLastOperator(count));
|
||
}
|
||
};
|
||
}
|
||
var TakeLastOperator = /*@__PURE__*/ (function () {
|
||
function TakeLastOperator(total) {
|
||
this.total = total;
|
||
if (this.total < 0) {
|
||
throw new ArgumentOutOfRangeError;
|
||
}
|
||
}
|
||
TakeLastOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new TakeLastSubscriber(subscriber, this.total));
|
||
};
|
||
return TakeLastOperator;
|
||
}());
|
||
var TakeLastSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(TakeLastSubscriber, _super);
|
||
function TakeLastSubscriber(destination, total) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.total = total;
|
||
_this.ring = new Array();
|
||
_this.count = 0;
|
||
return _this;
|
||
}
|
||
TakeLastSubscriber.prototype._next = function (value) {
|
||
var ring = this.ring;
|
||
var total = this.total;
|
||
var count = this.count++;
|
||
if (ring.length < total) {
|
||
ring.push(value);
|
||
}
|
||
else {
|
||
var index = count % total;
|
||
ring[index] = value;
|
||
}
|
||
};
|
||
TakeLastSubscriber.prototype._complete = function () {
|
||
var destination = this.destination;
|
||
var count = this.count;
|
||
if (count > 0) {
|
||
var total = this.count >= this.total ? this.total : this.count;
|
||
var ring = this.ring;
|
||
for (var i = 0; i < total; i++) {
|
||
var idx = (count++) % total;
|
||
destination.next(ring[idx]);
|
||
}
|
||
}
|
||
destination.complete();
|
||
};
|
||
return TakeLastSubscriber;
|
||
}(Subscriber));
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */
|
||
function mapTo(value) {
|
||
return function (source) { return source.lift(new MapToOperator(value)); };
|
||
}
|
||
var MapToOperator = /*@__PURE__*/ (function () {
|
||
function MapToOperator(value) {
|
||
this.value = value;
|
||
}
|
||
MapToOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new MapToSubscriber(subscriber, this.value));
|
||
};
|
||
return MapToOperator;
|
||
}());
|
||
var MapToSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(MapToSubscriber, _super);
|
||
function MapToSubscriber(destination, value) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.value = value;
|
||
return _this;
|
||
}
|
||
MapToSubscriber.prototype._next = function (x) {
|
||
this.destination.next(this.value);
|
||
};
|
||
return MapToSubscriber;
|
||
}(Subscriber));
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */
|
||
function scan(accumulator, seed) {
|
||
var hasSeed = false;
|
||
if (arguments.length >= 2) {
|
||
hasSeed = true;
|
||
}
|
||
return function scanOperatorFunction(source) {
|
||
return source.lift(new ScanOperator(accumulator, seed, hasSeed));
|
||
};
|
||
}
|
||
var ScanOperator = /*@__PURE__*/ (function () {
|
||
function ScanOperator(accumulator, seed, hasSeed) {
|
||
if (hasSeed === void 0) {
|
||
hasSeed = false;
|
||
}
|
||
this.accumulator = accumulator;
|
||
this.seed = seed;
|
||
this.hasSeed = hasSeed;
|
||
}
|
||
ScanOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new ScanSubscriber(subscriber, this.accumulator, this.seed, this.hasSeed));
|
||
};
|
||
return ScanOperator;
|
||
}());
|
||
var ScanSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(ScanSubscriber, _super);
|
||
function ScanSubscriber(destination, accumulator, _seed, hasSeed) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.accumulator = accumulator;
|
||
_this._seed = _seed;
|
||
_this.hasSeed = hasSeed;
|
||
_this.index = 0;
|
||
return _this;
|
||
}
|
||
Object.defineProperty(ScanSubscriber.prototype, "seed", {
|
||
get: function () {
|
||
return this._seed;
|
||
},
|
||
set: function (value) {
|
||
this.hasSeed = true;
|
||
this._seed = value;
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
ScanSubscriber.prototype._next = function (value) {
|
||
if (!this.hasSeed) {
|
||
this.seed = value;
|
||
this.destination.next(value);
|
||
}
|
||
else {
|
||
return this._tryNext(value);
|
||
}
|
||
};
|
||
ScanSubscriber.prototype._tryNext = function (value) {
|
||
var index = this.index++;
|
||
var result;
|
||
try {
|
||
result = this.accumulator(this.seed, value, index);
|
||
}
|
||
catch (err) {
|
||
this.destination.error(err);
|
||
}
|
||
this.seed = result;
|
||
this.destination.next(result);
|
||
};
|
||
return ScanSubscriber;
|
||
}(Subscriber));
|
||
|
||
/** PURE_IMPORTS_START _scan,_takeLast,_defaultIfEmpty,_util_pipe PURE_IMPORTS_END */
|
||
function reduce(accumulator, seed) {
|
||
if (arguments.length >= 2) {
|
||
return function reduceOperatorFunctionWithSeed(source) {
|
||
return pipe(scan(accumulator, seed), takeLast(1), defaultIfEmpty(seed))(source);
|
||
};
|
||
}
|
||
return function reduceOperatorFunction(source) {
|
||
return pipe(scan(function (acc, value, index) { return accumulator(acc, value, index + 1); }), takeLast(1))(source);
|
||
};
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _observable_ConnectableObservable PURE_IMPORTS_END */
|
||
function multicast(subjectOrSubjectFactory, selector) {
|
||
return function multicastOperatorFunction(source) {
|
||
var subjectFactory;
|
||
if (typeof subjectOrSubjectFactory === 'function') {
|
||
subjectFactory = subjectOrSubjectFactory;
|
||
}
|
||
else {
|
||
subjectFactory = function subjectFactory() {
|
||
return subjectOrSubjectFactory;
|
||
};
|
||
}
|
||
if (typeof selector === 'function') {
|
||
return source.lift(new MulticastOperator(subjectFactory, selector));
|
||
}
|
||
var connectable = Object.create(source, connectableObservableDescriptor);
|
||
connectable.source = source;
|
||
connectable.subjectFactory = subjectFactory;
|
||
return connectable;
|
||
};
|
||
}
|
||
var MulticastOperator = /*@__PURE__*/ (function () {
|
||
function MulticastOperator(subjectFactory, selector) {
|
||
this.subjectFactory = subjectFactory;
|
||
this.selector = selector;
|
||
}
|
||
MulticastOperator.prototype.call = function (subscriber, source) {
|
||
var selector = this.selector;
|
||
var subject = this.subjectFactory();
|
||
var subscription = selector(subject).subscribe(subscriber);
|
||
subscription.add(source.subscribe(subject));
|
||
return subscription;
|
||
};
|
||
return MulticastOperator;
|
||
}());
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */
|
||
function pairwise() {
|
||
return function (source) { return source.lift(new PairwiseOperator()); };
|
||
}
|
||
var PairwiseOperator = /*@__PURE__*/ (function () {
|
||
function PairwiseOperator() {
|
||
}
|
||
PairwiseOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new PairwiseSubscriber(subscriber));
|
||
};
|
||
return PairwiseOperator;
|
||
}());
|
||
var PairwiseSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(PairwiseSubscriber, _super);
|
||
function PairwiseSubscriber(destination) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.hasPrev = false;
|
||
return _this;
|
||
}
|
||
PairwiseSubscriber.prototype._next = function (value) {
|
||
var pair;
|
||
if (this.hasPrev) {
|
||
pair = [this.prev, value];
|
||
}
|
||
else {
|
||
this.hasPrev = true;
|
||
}
|
||
this.prev = value;
|
||
if (pair) {
|
||
this.destination.next(pair);
|
||
}
|
||
};
|
||
return PairwiseSubscriber;
|
||
}(Subscriber));
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subject,_innerSubscribe PURE_IMPORTS_END */
|
||
function retryWhen(notifier) {
|
||
return function (source) { return source.lift(new RetryWhenOperator(notifier, source)); };
|
||
}
|
||
var RetryWhenOperator = /*@__PURE__*/ (function () {
|
||
function RetryWhenOperator(notifier, source) {
|
||
this.notifier = notifier;
|
||
this.source = source;
|
||
}
|
||
RetryWhenOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new RetryWhenSubscriber(subscriber, this.notifier, this.source));
|
||
};
|
||
return RetryWhenOperator;
|
||
}());
|
||
var RetryWhenSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(RetryWhenSubscriber, _super);
|
||
function RetryWhenSubscriber(destination, notifier, source) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.notifier = notifier;
|
||
_this.source = source;
|
||
return _this;
|
||
}
|
||
RetryWhenSubscriber.prototype.error = function (err) {
|
||
if (!this.isStopped) {
|
||
var errors = this.errors;
|
||
var retries = this.retries;
|
||
var retriesSubscription = this.retriesSubscription;
|
||
if (!retries) {
|
||
errors = new Subject();
|
||
try {
|
||
var notifier = this.notifier;
|
||
retries = notifier(errors);
|
||
}
|
||
catch (e) {
|
||
return _super.prototype.error.call(this, e);
|
||
}
|
||
retriesSubscription = innerSubscribe(retries, new SimpleInnerSubscriber(this));
|
||
}
|
||
else {
|
||
this.errors = undefined;
|
||
this.retriesSubscription = undefined;
|
||
}
|
||
this._unsubscribeAndRecycle();
|
||
this.errors = errors;
|
||
this.retries = retries;
|
||
this.retriesSubscription = retriesSubscription;
|
||
errors.next(err);
|
||
}
|
||
};
|
||
RetryWhenSubscriber.prototype._unsubscribe = function () {
|
||
var _a = this, errors = _a.errors, retriesSubscription = _a.retriesSubscription;
|
||
if (errors) {
|
||
errors.unsubscribe();
|
||
this.errors = undefined;
|
||
}
|
||
if (retriesSubscription) {
|
||
retriesSubscription.unsubscribe();
|
||
this.retriesSubscription = undefined;
|
||
}
|
||
this.retries = undefined;
|
||
};
|
||
RetryWhenSubscriber.prototype.notifyNext = function () {
|
||
var _unsubscribe = this._unsubscribe;
|
||
this._unsubscribe = null;
|
||
this._unsubscribeAndRecycle();
|
||
this._unsubscribe = _unsubscribe;
|
||
this.source.subscribe(this);
|
||
};
|
||
return RetryWhenSubscriber;
|
||
}(SimpleOuterSubscriber));
|
||
|
||
/** PURE_IMPORTS_START _multicast,_refCount,_Subject PURE_IMPORTS_END */
|
||
function shareSubjectFactory() {
|
||
return new Subject();
|
||
}
|
||
function share() {
|
||
return function (source) { return refCount()(multicast(shareSubjectFactory)(source)); };
|
||
}
|
||
|
||
/** PURE_IMPORTS_START _ReplaySubject PURE_IMPORTS_END */
|
||
function shareReplay(configOrBufferSize, windowTime, scheduler) {
|
||
var config;
|
||
if (configOrBufferSize && typeof configOrBufferSize === 'object') {
|
||
config = configOrBufferSize;
|
||
}
|
||
else {
|
||
config = {
|
||
bufferSize: configOrBufferSize,
|
||
windowTime: windowTime,
|
||
refCount: false,
|
||
scheduler: scheduler,
|
||
};
|
||
}
|
||
return function (source) { return source.lift(shareReplayOperator(config)); };
|
||
}
|
||
function shareReplayOperator(_a) {
|
||
var _b = _a.bufferSize, bufferSize = _b === void 0 ? Number.POSITIVE_INFINITY : _b, _c = _a.windowTime, windowTime = _c === void 0 ? Number.POSITIVE_INFINITY : _c, useRefCount = _a.refCount, scheduler = _a.scheduler;
|
||
var subject;
|
||
var refCount = 0;
|
||
var subscription;
|
||
var hasError = false;
|
||
var isComplete = false;
|
||
return function shareReplayOperation(source) {
|
||
refCount++;
|
||
var innerSub;
|
||
if (!subject || hasError) {
|
||
hasError = false;
|
||
subject = new ReplaySubject(bufferSize, windowTime, scheduler);
|
||
innerSub = subject.subscribe(this);
|
||
subscription = source.subscribe({
|
||
next: function (value) {
|
||
subject.next(value);
|
||
},
|
||
error: function (err) {
|
||
hasError = true;
|
||
subject.error(err);
|
||
},
|
||
complete: function () {
|
||
isComplete = true;
|
||
subscription = undefined;
|
||
subject.complete();
|
||
},
|
||
});
|
||
if (isComplete) {
|
||
subscription = undefined;
|
||
}
|
||
}
|
||
else {
|
||
innerSub = subject.subscribe(this);
|
||
}
|
||
this.add(function () {
|
||
refCount--;
|
||
innerSub.unsubscribe();
|
||
innerSub = undefined;
|
||
if (subscription && !isComplete && useRefCount && refCount === 0) {
|
||
subscription.unsubscribe();
|
||
subscription = undefined;
|
||
subject = undefined;
|
||
}
|
||
});
|
||
};
|
||
}
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */
|
||
function skip(count) {
|
||
return function (source) { return source.lift(new SkipOperator(count)); };
|
||
}
|
||
var SkipOperator = /*@__PURE__*/ (function () {
|
||
function SkipOperator(total) {
|
||
this.total = total;
|
||
}
|
||
SkipOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new SkipSubscriber(subscriber, this.total));
|
||
};
|
||
return SkipOperator;
|
||
}());
|
||
var SkipSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(SkipSubscriber, _super);
|
||
function SkipSubscriber(destination, total) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.total = total;
|
||
_this.count = 0;
|
||
return _this;
|
||
}
|
||
SkipSubscriber.prototype._next = function (x) {
|
||
if (++this.count > this.total) {
|
||
this.destination.next(x);
|
||
}
|
||
};
|
||
return SkipSubscriber;
|
||
}(Subscriber));
|
||
|
||
/** PURE_IMPORTS_START _observable_concat,_util_isScheduler PURE_IMPORTS_END */
|
||
function startWith() {
|
||
var array = [];
|
||
for (var _i = 0; _i < arguments.length; _i++) {
|
||
array[_i] = arguments[_i];
|
||
}
|
||
var scheduler = array[array.length - 1];
|
||
if (isScheduler(scheduler)) {
|
||
array.pop();
|
||
return function (source) { return concat(array, source, scheduler); };
|
||
}
|
||
else {
|
||
return function (source) { return concat(array, source); };
|
||
}
|
||
}
|
||
|
||
/** PURE_IMPORTS_START tslib,_map,_observable_from,_innerSubscribe PURE_IMPORTS_END */
|
||
function switchMap(project, resultSelector) {
|
||
if (typeof resultSelector === 'function') {
|
||
return function (source) { return source.pipe(switchMap(function (a, i) { return from(project(a, i)).pipe(map(function (b, ii) { return resultSelector(a, b, i, ii); })); })); };
|
||
}
|
||
return function (source) { return source.lift(new SwitchMapOperator(project)); };
|
||
}
|
||
var SwitchMapOperator = /*@__PURE__*/ (function () {
|
||
function SwitchMapOperator(project) {
|
||
this.project = project;
|
||
}
|
||
SwitchMapOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new SwitchMapSubscriber(subscriber, this.project));
|
||
};
|
||
return SwitchMapOperator;
|
||
}());
|
||
var SwitchMapSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(SwitchMapSubscriber, _super);
|
||
function SwitchMapSubscriber(destination, project) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.project = project;
|
||
_this.index = 0;
|
||
return _this;
|
||
}
|
||
SwitchMapSubscriber.prototype._next = function (value) {
|
||
var result;
|
||
var index = this.index++;
|
||
try {
|
||
result = this.project(value, index);
|
||
}
|
||
catch (error) {
|
||
this.destination.error(error);
|
||
return;
|
||
}
|
||
this._innerSub(result);
|
||
};
|
||
SwitchMapSubscriber.prototype._innerSub = function (result) {
|
||
var innerSubscription = this.innerSubscription;
|
||
if (innerSubscription) {
|
||
innerSubscription.unsubscribe();
|
||
}
|
||
var innerSubscriber = new SimpleInnerSubscriber(this);
|
||
var destination = this.destination;
|
||
destination.add(innerSubscriber);
|
||
this.innerSubscription = innerSubscribe(result, innerSubscriber);
|
||
if (this.innerSubscription !== innerSubscriber) {
|
||
destination.add(this.innerSubscription);
|
||
}
|
||
};
|
||
SwitchMapSubscriber.prototype._complete = function () {
|
||
var innerSubscription = this.innerSubscription;
|
||
if (!innerSubscription || innerSubscription.closed) {
|
||
_super.prototype._complete.call(this);
|
||
}
|
||
this.unsubscribe();
|
||
};
|
||
SwitchMapSubscriber.prototype._unsubscribe = function () {
|
||
this.innerSubscription = undefined;
|
||
};
|
||
SwitchMapSubscriber.prototype.notifyComplete = function () {
|
||
this.innerSubscription = undefined;
|
||
if (this.isStopped) {
|
||
_super.prototype._complete.call(this);
|
||
}
|
||
};
|
||
SwitchMapSubscriber.prototype.notifyNext = function (innerValue) {
|
||
this.destination.next(innerValue);
|
||
};
|
||
return SwitchMapSubscriber;
|
||
}(SimpleOuterSubscriber));
|
||
|
||
/** PURE_IMPORTS_START _switchMap PURE_IMPORTS_END */
|
||
function switchMapTo(innerObservable, resultSelector) {
|
||
return resultSelector ? switchMap(function () { return innerObservable; }, resultSelector) : switchMap(function () { return innerObservable; });
|
||
}
|
||
|
||
/** PURE_IMPORTS_START tslib,_innerSubscribe PURE_IMPORTS_END */
|
||
function takeUntil(notifier) {
|
||
return function (source) { return source.lift(new TakeUntilOperator(notifier)); };
|
||
}
|
||
var TakeUntilOperator = /*@__PURE__*/ (function () {
|
||
function TakeUntilOperator(notifier) {
|
||
this.notifier = notifier;
|
||
}
|
||
TakeUntilOperator.prototype.call = function (subscriber, source) {
|
||
var takeUntilSubscriber = new TakeUntilSubscriber(subscriber);
|
||
var notifierSubscription = innerSubscribe(this.notifier, new SimpleInnerSubscriber(takeUntilSubscriber));
|
||
if (notifierSubscription && !takeUntilSubscriber.seenValue) {
|
||
takeUntilSubscriber.add(notifierSubscription);
|
||
return source.subscribe(takeUntilSubscriber);
|
||
}
|
||
return takeUntilSubscriber;
|
||
};
|
||
return TakeUntilOperator;
|
||
}());
|
||
var TakeUntilSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(TakeUntilSubscriber, _super);
|
||
function TakeUntilSubscriber(destination) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.seenValue = false;
|
||
return _this;
|
||
}
|
||
TakeUntilSubscriber.prototype.notifyNext = function () {
|
||
this.seenValue = true;
|
||
this.complete();
|
||
};
|
||
TakeUntilSubscriber.prototype.notifyComplete = function () {
|
||
};
|
||
return TakeUntilSubscriber;
|
||
}(SimpleOuterSubscriber));
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */
|
||
function takeWhile(predicate, inclusive) {
|
||
if (inclusive === void 0) {
|
||
inclusive = false;
|
||
}
|
||
return function (source) {
|
||
return source.lift(new TakeWhileOperator(predicate, inclusive));
|
||
};
|
||
}
|
||
var TakeWhileOperator = /*@__PURE__*/ (function () {
|
||
function TakeWhileOperator(predicate, inclusive) {
|
||
this.predicate = predicate;
|
||
this.inclusive = inclusive;
|
||
}
|
||
TakeWhileOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new TakeWhileSubscriber(subscriber, this.predicate, this.inclusive));
|
||
};
|
||
return TakeWhileOperator;
|
||
}());
|
||
var TakeWhileSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(TakeWhileSubscriber, _super);
|
||
function TakeWhileSubscriber(destination, predicate, inclusive) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.predicate = predicate;
|
||
_this.inclusive = inclusive;
|
||
_this.index = 0;
|
||
return _this;
|
||
}
|
||
TakeWhileSubscriber.prototype._next = function (value) {
|
||
var destination = this.destination;
|
||
var result;
|
||
try {
|
||
result = this.predicate(value, this.index++);
|
||
}
|
||
catch (err) {
|
||
destination.error(err);
|
||
return;
|
||
}
|
||
this.nextOrComplete(value, result);
|
||
};
|
||
TakeWhileSubscriber.prototype.nextOrComplete = function (value, predicateResult) {
|
||
var destination = this.destination;
|
||
if (Boolean(predicateResult)) {
|
||
destination.next(value);
|
||
}
|
||
else {
|
||
if (this.inclusive) {
|
||
destination.next(value);
|
||
}
|
||
destination.complete();
|
||
}
|
||
};
|
||
return TakeWhileSubscriber;
|
||
}(Subscriber));
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber,_util_noop,_util_isFunction PURE_IMPORTS_END */
|
||
function tap(nextOrObserver, error, complete) {
|
||
return function tapOperatorFunction(source) {
|
||
return source.lift(new DoOperator(nextOrObserver, error, complete));
|
||
};
|
||
}
|
||
var DoOperator = /*@__PURE__*/ (function () {
|
||
function DoOperator(nextOrObserver, error, complete) {
|
||
this.nextOrObserver = nextOrObserver;
|
||
this.error = error;
|
||
this.complete = complete;
|
||
}
|
||
DoOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new TapSubscriber(subscriber, this.nextOrObserver, this.error, this.complete));
|
||
};
|
||
return DoOperator;
|
||
}());
|
||
var TapSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(TapSubscriber, _super);
|
||
function TapSubscriber(destination, observerOrNext, error, complete) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this._tapNext = noop;
|
||
_this._tapError = noop;
|
||
_this._tapComplete = noop;
|
||
_this._tapError = error || noop;
|
||
_this._tapComplete = complete || noop;
|
||
if (isFunction$1(observerOrNext)) {
|
||
_this._context = _this;
|
||
_this._tapNext = observerOrNext;
|
||
}
|
||
else if (observerOrNext) {
|
||
_this._context = observerOrNext;
|
||
_this._tapNext = observerOrNext.next || noop;
|
||
_this._tapError = observerOrNext.error || noop;
|
||
_this._tapComplete = observerOrNext.complete || noop;
|
||
}
|
||
return _this;
|
||
}
|
||
TapSubscriber.prototype._next = function (value) {
|
||
try {
|
||
this._tapNext.call(this._context, value);
|
||
}
|
||
catch (err) {
|
||
this.destination.error(err);
|
||
return;
|
||
}
|
||
this.destination.next(value);
|
||
};
|
||
TapSubscriber.prototype._error = function (err) {
|
||
try {
|
||
this._tapError.call(this._context, err);
|
||
}
|
||
catch (err) {
|
||
this.destination.error(err);
|
||
return;
|
||
}
|
||
this.destination.error(err);
|
||
};
|
||
TapSubscriber.prototype._complete = function () {
|
||
try {
|
||
this._tapComplete.call(this._context);
|
||
}
|
||
catch (err) {
|
||
this.destination.error(err);
|
||
return;
|
||
}
|
||
return this.destination.complete();
|
||
};
|
||
return TapSubscriber;
|
||
}(Subscriber));
|
||
|
||
/** PURE_IMPORTS_START tslib,_innerSubscribe PURE_IMPORTS_END */
|
||
var defaultThrottleConfig = {
|
||
leading: true,
|
||
trailing: false
|
||
};
|
||
|
||
/** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async,_throttle PURE_IMPORTS_END */
|
||
function throttleTime(duration, scheduler, config) {
|
||
if (scheduler === void 0) {
|
||
scheduler = async;
|
||
}
|
||
if (config === void 0) {
|
||
config = defaultThrottleConfig;
|
||
}
|
||
return function (source) { return source.lift(new ThrottleTimeOperator(duration, scheduler, config.leading, config.trailing)); };
|
||
}
|
||
var ThrottleTimeOperator = /*@__PURE__*/ (function () {
|
||
function ThrottleTimeOperator(duration, scheduler, leading, trailing) {
|
||
this.duration = duration;
|
||
this.scheduler = scheduler;
|
||
this.leading = leading;
|
||
this.trailing = trailing;
|
||
}
|
||
ThrottleTimeOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new ThrottleTimeSubscriber(subscriber, this.duration, this.scheduler, this.leading, this.trailing));
|
||
};
|
||
return ThrottleTimeOperator;
|
||
}());
|
||
var ThrottleTimeSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(ThrottleTimeSubscriber, _super);
|
||
function ThrottleTimeSubscriber(destination, duration, scheduler, leading, trailing) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.duration = duration;
|
||
_this.scheduler = scheduler;
|
||
_this.leading = leading;
|
||
_this.trailing = trailing;
|
||
_this._hasTrailingValue = false;
|
||
_this._trailingValue = null;
|
||
return _this;
|
||
}
|
||
ThrottleTimeSubscriber.prototype._next = function (value) {
|
||
if (this.throttled) {
|
||
if (this.trailing) {
|
||
this._trailingValue = value;
|
||
this._hasTrailingValue = true;
|
||
}
|
||
}
|
||
else {
|
||
this.add(this.throttled = this.scheduler.schedule(dispatchNext, this.duration, { subscriber: this }));
|
||
if (this.leading) {
|
||
this.destination.next(value);
|
||
}
|
||
else if (this.trailing) {
|
||
this._trailingValue = value;
|
||
this._hasTrailingValue = true;
|
||
}
|
||
}
|
||
};
|
||
ThrottleTimeSubscriber.prototype._complete = function () {
|
||
if (this._hasTrailingValue) {
|
||
this.destination.next(this._trailingValue);
|
||
this.destination.complete();
|
||
}
|
||
else {
|
||
this.destination.complete();
|
||
}
|
||
};
|
||
ThrottleTimeSubscriber.prototype.clearThrottle = function () {
|
||
var throttled = this.throttled;
|
||
if (throttled) {
|
||
if (this.trailing && this._hasTrailingValue) {
|
||
this.destination.next(this._trailingValue);
|
||
this._trailingValue = null;
|
||
this._hasTrailingValue = false;
|
||
}
|
||
throttled.unsubscribe();
|
||
this.remove(throttled);
|
||
this.throttled = null;
|
||
}
|
||
};
|
||
return ThrottleTimeSubscriber;
|
||
}(Subscriber));
|
||
function dispatchNext(arg) {
|
||
var subscriber = arg.subscriber;
|
||
subscriber.clearThrottle();
|
||
}
|
||
|
||
/** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_innerSubscribe PURE_IMPORTS_END */
|
||
function timeoutWith(due, withObservable, scheduler) {
|
||
if (scheduler === void 0) {
|
||
scheduler = async;
|
||
}
|
||
return function (source) {
|
||
var absoluteTimeout = isDate(due);
|
||
var waitFor = absoluteTimeout ? (+due - scheduler.now()) : Math.abs(due);
|
||
return source.lift(new TimeoutWithOperator(waitFor, absoluteTimeout, withObservable, scheduler));
|
||
};
|
||
}
|
||
var TimeoutWithOperator = /*@__PURE__*/ (function () {
|
||
function TimeoutWithOperator(waitFor, absoluteTimeout, withObservable, scheduler) {
|
||
this.waitFor = waitFor;
|
||
this.absoluteTimeout = absoluteTimeout;
|
||
this.withObservable = withObservable;
|
||
this.scheduler = scheduler;
|
||
}
|
||
TimeoutWithOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new TimeoutWithSubscriber(subscriber, this.absoluteTimeout, this.waitFor, this.withObservable, this.scheduler));
|
||
};
|
||
return TimeoutWithOperator;
|
||
}());
|
||
var TimeoutWithSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(TimeoutWithSubscriber, _super);
|
||
function TimeoutWithSubscriber(destination, absoluteTimeout, waitFor, withObservable, scheduler) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.absoluteTimeout = absoluteTimeout;
|
||
_this.waitFor = waitFor;
|
||
_this.withObservable = withObservable;
|
||
_this.scheduler = scheduler;
|
||
_this.scheduleTimeout();
|
||
return _this;
|
||
}
|
||
TimeoutWithSubscriber.dispatchTimeout = function (subscriber) {
|
||
var withObservable = subscriber.withObservable;
|
||
subscriber._unsubscribeAndRecycle();
|
||
subscriber.add(innerSubscribe(withObservable, new SimpleInnerSubscriber(subscriber)));
|
||
};
|
||
TimeoutWithSubscriber.prototype.scheduleTimeout = function () {
|
||
var action = this.action;
|
||
if (action) {
|
||
this.action = action.schedule(this, this.waitFor);
|
||
}
|
||
else {
|
||
this.add(this.action = this.scheduler.schedule(TimeoutWithSubscriber.dispatchTimeout, this.waitFor, this));
|
||
}
|
||
};
|
||
TimeoutWithSubscriber.prototype._next = function (value) {
|
||
if (!this.absoluteTimeout) {
|
||
this.scheduleTimeout();
|
||
}
|
||
_super.prototype._next.call(this, value);
|
||
};
|
||
TimeoutWithSubscriber.prototype._unsubscribe = function () {
|
||
this.action = undefined;
|
||
this.scheduler = null;
|
||
this.withObservable = null;
|
||
};
|
||
return TimeoutWithSubscriber;
|
||
}(SimpleOuterSubscriber));
|
||
|
||
/** PURE_IMPORTS_START _scheduler_async,_util_TimeoutError,_timeoutWith,_observable_throwError PURE_IMPORTS_END */
|
||
function timeout(due, scheduler) {
|
||
if (scheduler === void 0) {
|
||
scheduler = async;
|
||
}
|
||
return timeoutWith(due, throwError(new TimeoutError()), scheduler);
|
||
}
|
||
|
||
/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */
|
||
function withLatestFrom() {
|
||
var args = [];
|
||
for (var _i = 0; _i < arguments.length; _i++) {
|
||
args[_i] = arguments[_i];
|
||
}
|
||
return function (source) {
|
||
var project;
|
||
if (typeof args[args.length - 1] === 'function') {
|
||
project = args.pop();
|
||
}
|
||
var observables = args;
|
||
return source.lift(new WithLatestFromOperator(observables, project));
|
||
};
|
||
}
|
||
var WithLatestFromOperator = /*@__PURE__*/ (function () {
|
||
function WithLatestFromOperator(observables, project) {
|
||
this.observables = observables;
|
||
this.project = project;
|
||
}
|
||
WithLatestFromOperator.prototype.call = function (subscriber, source) {
|
||
return source.subscribe(new WithLatestFromSubscriber(subscriber, this.observables, this.project));
|
||
};
|
||
return WithLatestFromOperator;
|
||
}());
|
||
var WithLatestFromSubscriber = /*@__PURE__*/ (function (_super) {
|
||
__extends(WithLatestFromSubscriber, _super);
|
||
function WithLatestFromSubscriber(destination, observables, project) {
|
||
var _this = _super.call(this, destination) || this;
|
||
_this.observables = observables;
|
||
_this.project = project;
|
||
_this.toRespond = [];
|
||
var len = observables.length;
|
||
_this.values = new Array(len);
|
||
for (var i = 0; i < len; i++) {
|
||
_this.toRespond.push(i);
|
||
}
|
||
for (var i = 0; i < len; i++) {
|
||
var observable = observables[i];
|
||
_this.add(subscribeToResult(_this, observable, undefined, i));
|
||
}
|
||
return _this;
|
||
}
|
||
WithLatestFromSubscriber.prototype.notifyNext = function (_outerValue, innerValue, outerIndex) {
|
||
this.values[outerIndex] = innerValue;
|
||
var toRespond = this.toRespond;
|
||
if (toRespond.length > 0) {
|
||
var found = toRespond.indexOf(outerIndex);
|
||
if (found !== -1) {
|
||
toRespond.splice(found, 1);
|
||
}
|
||
}
|
||
};
|
||
WithLatestFromSubscriber.prototype.notifyComplete = function () {
|
||
};
|
||
WithLatestFromSubscriber.prototype._next = function (value) {
|
||
if (this.toRespond.length === 0) {
|
||
var args = [value].concat(this.values);
|
||
if (this.project) {
|
||
this._tryProject(args);
|
||
}
|
||
else {
|
||
this.destination.next(args);
|
||
}
|
||
}
|
||
};
|
||
WithLatestFromSubscriber.prototype._tryProject = function (args) {
|
||
var result;
|
||
try {
|
||
result = this.project.apply(this, args);
|
||
}
|
||
catch (err) {
|
||
this.destination.error(err);
|
||
return;
|
||
}
|
||
this.destination.next(result);
|
||
};
|
||
return WithLatestFromSubscriber;
|
||
}(OuterSubscriber));
|
||
|
||
var currentAction = {
|
||
type: null,
|
||
entityIds: null,
|
||
skip: false,
|
||
};
|
||
var customActionActive = false;
|
||
function resetCustomAction() {
|
||
customActionActive = false;
|
||
}
|
||
// public API for custom actions. Custom action always wins
|
||
function logAction(type, entityIds) {
|
||
setAction(type, entityIds);
|
||
customActionActive = true;
|
||
}
|
||
function setAction(type, entityIds) {
|
||
if (customActionActive === false) {
|
||
currentAction.type = type;
|
||
currentAction.entityIds = entityIds;
|
||
}
|
||
}
|
||
function action(action, entityIds) {
|
||
return function (target, propertyKey, descriptor) {
|
||
var originalMethod = descriptor.value;
|
||
descriptor.value = function () {
|
||
var args = [];
|
||
for (var _i = 0; _i < arguments.length; _i++) {
|
||
args[_i] = arguments[_i];
|
||
}
|
||
logAction(action, entityIds);
|
||
return originalMethod.apply(this, args);
|
||
};
|
||
return descriptor;
|
||
};
|
||
}
|
||
|
||
// @internal
|
||
function hasEntity(entities, id) {
|
||
return entities.hasOwnProperty(id);
|
||
}
|
||
|
||
// @internal
|
||
function addEntities(_a) {
|
||
var state = _a.state, entities = _a.entities, idKey = _a.idKey, _b = _a.options, options = _b === void 0 ? {} : _b, preAddEntity = _a.preAddEntity;
|
||
var e_1, _c;
|
||
var newEntities = {};
|
||
var newIds = [];
|
||
var hasNewEntities = false;
|
||
try {
|
||
for (var entities_1 = __values(entities), entities_1_1 = entities_1.next(); !entities_1_1.done; entities_1_1 = entities_1.next()) {
|
||
var entity = entities_1_1.value;
|
||
if (hasEntity(state.entities, entity[idKey]) === false) {
|
||
// evaluate the middleware first to support dynamic ids
|
||
var current = preAddEntity(entity);
|
||
var entityId = current[idKey];
|
||
newEntities[entityId] = current;
|
||
if (options.prepend)
|
||
newIds.unshift(entityId);
|
||
else
|
||
newIds.push(entityId);
|
||
hasNewEntities = true;
|
||
}
|
||
}
|
||
}
|
||
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
||
finally {
|
||
try {
|
||
if (entities_1_1 && !entities_1_1.done && (_c = entities_1.return)) _c.call(entities_1);
|
||
}
|
||
finally { if (e_1) throw e_1.error; }
|
||
}
|
||
return hasNewEntities
|
||
? {
|
||
newState: __assign({}, state, { entities: __assign({}, state.entities, newEntities), ids: options.prepend ? __spread(newIds, state.ids) : __spread(state.ids, newIds) }),
|
||
newIds: newIds
|
||
}
|
||
: null;
|
||
}
|
||
|
||
// @internal
|
||
function isNil(v) {
|
||
return v === null || v === undefined;
|
||
}
|
||
|
||
// @internal
|
||
function coerceArray(value) {
|
||
if (isNil(value)) {
|
||
return [];
|
||
}
|
||
return Array.isArray(value) ? value : [value];
|
||
}
|
||
|
||
var DEFAULT_ID_KEY = 'id';
|
||
|
||
var EntityActions;
|
||
(function (EntityActions) {
|
||
EntityActions["Set"] = "Set";
|
||
EntityActions["Add"] = "Add";
|
||
EntityActions["Update"] = "Update";
|
||
EntityActions["Remove"] = "Remove";
|
||
})(EntityActions || (EntityActions = {}));
|
||
|
||
var isBrowser = typeof window !== 'undefined';
|
||
|
||
// @internal
|
||
function isObject(value) {
|
||
var type = typeof value;
|
||
return value != null && (type == 'object' || type == 'function');
|
||
}
|
||
|
||
// @internal
|
||
function isArray(value) {
|
||
return Array.isArray(value);
|
||
}
|
||
|
||
// @internal
|
||
function getActiveEntities(idOrOptions, ids, currentActive) {
|
||
var result;
|
||
if (isArray(idOrOptions)) {
|
||
result = idOrOptions;
|
||
}
|
||
else {
|
||
if (isObject(idOrOptions)) {
|
||
if (isNil(currentActive))
|
||
return;
|
||
idOrOptions = Object.assign({ wrap: true }, idOrOptions);
|
||
var currentIdIndex = ids.indexOf(currentActive);
|
||
if (idOrOptions.prev) {
|
||
var isFirst = currentIdIndex === 0;
|
||
if (isFirst && !idOrOptions.wrap)
|
||
return;
|
||
result = isFirst ? ids[ids.length - 1] : ids[currentIdIndex - 1];
|
||
}
|
||
else if (idOrOptions.next) {
|
||
var isLast = ids.length === currentIdIndex + 1;
|
||
if (isLast && !idOrOptions.wrap)
|
||
return;
|
||
result = isLast ? ids[0] : ids[currentIdIndex + 1];
|
||
}
|
||
}
|
||
else {
|
||
if (idOrOptions === currentActive)
|
||
return;
|
||
result = idOrOptions;
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// @internal
|
||
var getInitialEntitiesState = function () {
|
||
return ({
|
||
entities: {},
|
||
ids: [],
|
||
loading: true,
|
||
error: null
|
||
});
|
||
};
|
||
|
||
// @internal
|
||
function isDefined(val) {
|
||
return isNil(val) === false;
|
||
}
|
||
|
||
// @internal
|
||
function isEmpty(arr) {
|
||
if (isArray(arr)) {
|
||
return arr.length === 0;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// @internal
|
||
function isFunction(value) {
|
||
return typeof value === 'function';
|
||
}
|
||
|
||
// @internal
|
||
function isUndefined(value) {
|
||
return value === undefined;
|
||
}
|
||
|
||
// @internal
|
||
function hasActiveState(state) {
|
||
return state.hasOwnProperty('active');
|
||
}
|
||
// @internal
|
||
function isMultiActiveState(active) {
|
||
return isArray(active);
|
||
}
|
||
// @internal
|
||
function resolveActiveEntity(_a) {
|
||
var active = _a.active, ids = _a.ids, entities = _a.entities;
|
||
if (isMultiActiveState(active)) {
|
||
return getExitingActives(active, ids);
|
||
}
|
||
if (hasEntity(entities, active) === false) {
|
||
return null;
|
||
}
|
||
return active;
|
||
}
|
||
// @internal
|
||
function getExitingActives(currentActivesIds, newIds) {
|
||
var filtered = currentActivesIds.filter(function (id) { return newIds.indexOf(id) > -1; });
|
||
/** Return the same reference if nothing has changed */
|
||
if (filtered.length === currentActivesIds.length) {
|
||
return currentActivesIds;
|
||
}
|
||
return filtered;
|
||
}
|
||
|
||
// @internal
|
||
function removeEntities(_a) {
|
||
var state = _a.state, ids = _a.ids;
|
||
var e_1, _b;
|
||
if (isNil(ids))
|
||
return removeAllEntities(state);
|
||
var entities = state.entities;
|
||
var newEntities = {};
|
||
try {
|
||
for (var _c = __values(state.ids), _d = _c.next(); !_d.done; _d = _c.next()) {
|
||
var id = _d.value;
|
||
if (ids.includes(id) === false) {
|
||
newEntities[id] = entities[id];
|
||
}
|
||
}
|
||
}
|
||
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
||
finally {
|
||
try {
|
||
if (_d && !_d.done && (_b = _c.return)) _b.call(_c);
|
||
}
|
||
finally { if (e_1) throw e_1.error; }
|
||
}
|
||
var newState = __assign({}, state, { entities: newEntities, ids: state.ids.filter(function (current) { return ids.includes(current) === false; }) });
|
||
if (hasActiveState(state)) {
|
||
newState.active = resolveActiveEntity(newState);
|
||
}
|
||
return newState;
|
||
}
|
||
// @internal
|
||
function removeAllEntities(state) {
|
||
return __assign({}, state, { entities: {}, ids: [], active: isMultiActiveState(state.active) ? [] : null });
|
||
}
|
||
|
||
// @internal
|
||
function toEntitiesObject(entities, idKey, preAddEntity) {
|
||
var e_1, _a;
|
||
var acc = {
|
||
entities: {},
|
||
ids: []
|
||
};
|
||
try {
|
||
for (var entities_1 = __values(entities), entities_1_1 = entities_1.next(); !entities_1_1.done; entities_1_1 = entities_1.next()) {
|
||
var entity = entities_1_1.value;
|
||
// evaluate the middleware first to support dynamic ids
|
||
var current = preAddEntity(entity);
|
||
acc.entities[current[idKey]] = current;
|
||
acc.ids.push(current[idKey]);
|
||
}
|
||
}
|
||
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
||
finally {
|
||
try {
|
||
if (entities_1_1 && !entities_1_1.done && (_a = entities_1.return)) _a.call(entities_1);
|
||
}
|
||
finally { if (e_1) throw e_1.error; }
|
||
}
|
||
return acc;
|
||
}
|
||
|
||
// @internal
|
||
function isEntityState(state) {
|
||
return state.entities && state.ids;
|
||
}
|
||
// @internal
|
||
function applyMiddleware(entities, preAddEntity) {
|
||
var e_1, _a;
|
||
var mapped = {};
|
||
try {
|
||
for (var _b = __values(Object.keys(entities)), _c = _b.next(); !_c.done; _c = _b.next()) {
|
||
var id = _c.value;
|
||
mapped[id] = preAddEntity(entities[id]);
|
||
}
|
||
}
|
||
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
||
finally {
|
||
try {
|
||
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
|
||
}
|
||
finally { if (e_1) throw e_1.error; }
|
||
}
|
||
return mapped;
|
||
}
|
||
// @internal
|
||
function setEntities(_a) {
|
||
var state = _a.state, entities = _a.entities, idKey = _a.idKey, preAddEntity = _a.preAddEntity, isNativePreAdd = _a.isNativePreAdd;
|
||
var newEntities;
|
||
var newIds;
|
||
if (isArray(entities)) {
|
||
var resolve = toEntitiesObject(entities, idKey, preAddEntity);
|
||
newEntities = resolve.entities;
|
||
newIds = resolve.ids;
|
||
}
|
||
else if (isEntityState(entities)) {
|
||
newEntities = isNativePreAdd ? entities.entities : applyMiddleware(entities.entities, preAddEntity);
|
||
newIds = entities.ids;
|
||
}
|
||
else {
|
||
// it's an object
|
||
newEntities = isNativePreAdd ? entities : applyMiddleware(entities, preAddEntity);
|
||
newIds = Object.keys(newEntities).map(function (id) { return (isNaN(id) ? id : Number(id)); });
|
||
}
|
||
var newState = __assign({}, state, { entities: newEntities, ids: newIds, loading: false });
|
||
if (hasActiveState(state)) {
|
||
newState.active = resolveActiveEntity(newState);
|
||
}
|
||
return newState;
|
||
}
|
||
|
||
var CONFIG = {
|
||
resettable: false,
|
||
ttl: null,
|
||
producerFn: undefined
|
||
};
|
||
// @internal
|
||
function getAkitaConfig() {
|
||
return CONFIG;
|
||
}
|
||
function getGlobalProducerFn() {
|
||
return CONFIG.producerFn;
|
||
}
|
||
|
||
// @internal
|
||
function deepFreeze(o) {
|
||
Object.freeze(o);
|
||
var oIsFunction = typeof o === 'function';
|
||
var hasOwnProp = Object.prototype.hasOwnProperty;
|
||
Object.getOwnPropertyNames(o).forEach(function (prop) {
|
||
if (hasOwnProp.call(o, prop) &&
|
||
(oIsFunction ? prop !== 'caller' && prop !== 'callee' && prop !== 'arguments' : true) &&
|
||
o[prop] !== null &&
|
||
(typeof o[prop] === 'object' || typeof o[prop] === 'function') &&
|
||
!Object.isFrozen(o[prop])) {
|
||
deepFreeze(o[prop]);
|
||
}
|
||
});
|
||
return o;
|
||
}
|
||
|
||
// @internal
|
||
var $$deleteStore = new Subject();
|
||
// @internal
|
||
var $$addStore = new ReplaySubject(50, 5000);
|
||
// @internal
|
||
var $$updateStore = new Subject();
|
||
// @internal
|
||
function dispatchDeleted(storeName) {
|
||
$$deleteStore.next(storeName);
|
||
}
|
||
// @internal
|
||
function dispatchAdded(storeName) {
|
||
$$addStore.next(storeName);
|
||
}
|
||
// @internal
|
||
function dispatchUpdate(storeName, action) {
|
||
$$updateStore.next({ storeName: storeName, action: action });
|
||
}
|
||
|
||
// @internal
|
||
/** @class */ ((function (_super) {
|
||
__extends(AkitaError, _super);
|
||
function AkitaError(message) {
|
||
return _super.call(this, message) || this;
|
||
}
|
||
return AkitaError;
|
||
})(Error));
|
||
// @internal
|
||
function assertStoreHasName(name, className) {
|
||
if (!name) {
|
||
console.error("@StoreConfig({ name }) is missing in " + className);
|
||
}
|
||
}
|
||
|
||
// @internal
|
||
function toBoolean(value) {
|
||
return value != null && "" + value !== 'false';
|
||
}
|
||
|
||
// @internal
|
||
function isPlainObject$1(value) {
|
||
return toBoolean(value) && value.constructor.name === 'Object';
|
||
}
|
||
|
||
var configKey = 'akitaConfig';
|
||
|
||
// @internal
|
||
var __stores__ = {};
|
||
// @internal
|
||
var __queries__ = {};
|
||
if (isBrowser) {
|
||
window.$$stores = __stores__;
|
||
window.$$queries = __queries__;
|
||
}
|
||
|
||
// @internal
|
||
var transactionFinished = new Subject();
|
||
// @internal
|
||
var transactionInProcess = new BehaviorSubject(false);
|
||
// @internal
|
||
var transactionManager = {
|
||
activeTransactions: 0,
|
||
batchTransaction: null
|
||
};
|
||
// @internal
|
||
function startBatch() {
|
||
if (!isTransactionInProcess()) {
|
||
transactionManager.batchTransaction = new Subject();
|
||
}
|
||
transactionManager.activeTransactions++;
|
||
transactionInProcess.next(true);
|
||
}
|
||
// @internal
|
||
function endBatch() {
|
||
if (--transactionManager.activeTransactions === 0) {
|
||
transactionManager.batchTransaction.next(true);
|
||
transactionManager.batchTransaction.complete();
|
||
transactionInProcess.next(false);
|
||
transactionFinished.next(true);
|
||
}
|
||
}
|
||
// @internal
|
||
function isTransactionInProcess() {
|
||
return transactionManager.activeTransactions > 0;
|
||
}
|
||
// @internal
|
||
function commit() {
|
||
return transactionManager.batchTransaction ? transactionManager.batchTransaction.asObservable() : of(true);
|
||
}
|
||
/**
|
||
* A logical transaction.
|
||
* Use this transaction to optimize the dispatch of all the stores.
|
||
* The following code will update the store, BUT emits only once
|
||
*
|
||
* @example
|
||
* applyTransaction(() => {
|
||
* this.todosStore.add(new Todo(1, title));
|
||
* this.todosStore.add(new Todo(2, title));
|
||
* });
|
||
*
|
||
*/
|
||
function applyTransaction(action, thisArg) {
|
||
if (thisArg === void 0) { thisArg = undefined; }
|
||
startBatch();
|
||
try {
|
||
return action.apply(thisArg);
|
||
}
|
||
finally {
|
||
logAction('@Transaction');
|
||
endBatch();
|
||
}
|
||
}
|
||
/**
|
||
* A logical transaction.
|
||
* Use this transaction to optimize the dispatch of all the stores.
|
||
*
|
||
* The following code will update the store, BUT emits only once.
|
||
*
|
||
* @example
|
||
* @transaction
|
||
* addTodos() {
|
||
* this.todosStore.add(new Todo(1, title));
|
||
* this.todosStore.add(new Todo(2, title));
|
||
* }
|
||
*
|
||
*
|
||
*/
|
||
function transaction() {
|
||
return function (target, propertyKey, descriptor) {
|
||
var originalMethod = descriptor.value;
|
||
descriptor.value = function () {
|
||
var _this = this;
|
||
var args = [];
|
||
for (var _i = 0; _i < arguments.length; _i++) {
|
||
args[_i] = arguments[_i];
|
||
}
|
||
return applyTransaction(function () {
|
||
return originalMethod.apply(_this, args);
|
||
}, this);
|
||
};
|
||
return descriptor;
|
||
};
|
||
}
|
||
/**
|
||
*
|
||
* RxJS custom operator that wraps the callback inside transaction
|
||
*
|
||
* @example
|
||
*
|
||
* return http.get().pipe(
|
||
* withTransaction(response > {
|
||
* store.setActive(1);
|
||
* store.update();
|
||
* store.updateEntity(1, {});
|
||
* })
|
||
* )
|
||
*
|
||
*/
|
||
function withTransaction(next) {
|
||
return function (source) {
|
||
return source.pipe(tap(function (value) { return applyTransaction(function () { return next(value); }); }));
|
||
};
|
||
}
|
||
|
||
/**
|
||
*
|
||
* Store for managing any type of data
|
||
*
|
||
* @example
|
||
*
|
||
* export interface SessionState {
|
||
* token: string;
|
||
* userDetails: UserDetails
|
||
* }
|
||
*
|
||
* export function createInitialState(): SessionState {
|
||
* return {
|
||
* token: '',
|
||
* userDetails: null
|
||
* };
|
||
* }
|
||
*
|
||
* @StoreConfig({ name: 'session' })
|
||
* export class SessionStore extends Store<SessionState> {
|
||
* constructor() {
|
||
* super(createInitialState());
|
||
* }
|
||
* }
|
||
*/
|
||
var Store = /** @class */ (function () {
|
||
function Store(initialState, options) {
|
||
if (options === void 0) { options = {}; }
|
||
this.options = options;
|
||
this.inTransaction = false;
|
||
this.cache = {
|
||
active: new BehaviorSubject(false),
|
||
ttl: null,
|
||
};
|
||
this.onInit(initialState);
|
||
}
|
||
/**
|
||
* Set the loading state
|
||
*
|
||
* @example
|
||
*
|
||
* store.setLoading(true)
|
||
*
|
||
*/
|
||
Store.prototype.setLoading = function (loading) {
|
||
if (loading === void 0) { loading = false; }
|
||
if (loading !== this._value().loading) {
|
||
setAction('Set Loading');
|
||
this._setState(function (state) { return (__assign({}, state, { loading: loading })); });
|
||
}
|
||
};
|
||
/**
|
||
*
|
||
* Set whether the data is cached
|
||
*
|
||
* @example
|
||
*
|
||
* store.setHasCache(true)
|
||
* store.setHasCache(false)
|
||
* store.setHasCache(true, { restartTTL: true })
|
||
*
|
||
*/
|
||
Store.prototype.setHasCache = function (hasCache, options) {
|
||
var _this = this;
|
||
if (options === void 0) { options = { restartTTL: false }; }
|
||
if (hasCache !== this.cache.active.value) {
|
||
this.cache.active.next(hasCache);
|
||
}
|
||
if (options.restartTTL) {
|
||
var ttlConfig = this.getCacheTTL();
|
||
if (ttlConfig) {
|
||
if (this.cache.ttl !== null) {
|
||
clearTimeout(this.cache.ttl);
|
||
}
|
||
this.cache.ttl = setTimeout(function () { return _this.setHasCache(false); }, ttlConfig);
|
||
}
|
||
}
|
||
};
|
||
/**
|
||
*
|
||
* Sometimes we need to access the store value from a store
|
||
*
|
||
* @example middleware
|
||
*
|
||
*/
|
||
Store.prototype.getValue = function () {
|
||
return this.storeValue;
|
||
};
|
||
/**
|
||
* Set the error state
|
||
*
|
||
* @example
|
||
*
|
||
* store.setError({text: 'unable to load data' })
|
||
*
|
||
*/
|
||
Store.prototype.setError = function (error) {
|
||
if (error !== this._value().error) {
|
||
setAction('Set Error');
|
||
this._setState(function (state) { return (__assign({}, state, { error: error })); });
|
||
}
|
||
};
|
||
// @internal
|
||
Store.prototype._select = function (project) {
|
||
return this.store.asObservable().pipe(map(function (snapshot) { return project(snapshot.state); }), distinctUntilChanged());
|
||
};
|
||
// @internal
|
||
Store.prototype._value = function () {
|
||
return this.storeValue;
|
||
};
|
||
// @internal
|
||
Store.prototype._cache = function () {
|
||
return this.cache.active;
|
||
};
|
||
Object.defineProperty(Store.prototype, "config", {
|
||
// @internal
|
||
get: function () {
|
||
return this.constructor[configKey] || {};
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
Object.defineProperty(Store.prototype, "storeName", {
|
||
// @internal
|
||
get: function () {
|
||
return this.config.storeName || this.options.storeName || this.options.name;
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
Object.defineProperty(Store.prototype, "deepFreeze", {
|
||
// @internal
|
||
get: function () {
|
||
return this.config.deepFreezeFn || this.options.deepFreezeFn || deepFreeze;
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
Object.defineProperty(Store.prototype, "cacheConfig", {
|
||
// @internal
|
||
get: function () {
|
||
return this.config.cache || this.options.cache;
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
Object.defineProperty(Store.prototype, "_producerFn", {
|
||
get: function () {
|
||
return this.config.producerFn || this.options.producerFn || getGlobalProducerFn();
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
Object.defineProperty(Store.prototype, "resettable", {
|
||
// @internal
|
||
get: function () {
|
||
return isDefined(this.config.resettable) ? this.config.resettable : this.options.resettable;
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
// @internal
|
||
Store.prototype._setState = function (newState, _dispatchAction) {
|
||
var _this = this;
|
||
if (_dispatchAction === void 0) { _dispatchAction = true; }
|
||
if (isFunction(newState)) {
|
||
var _newState = newState(this._value());
|
||
this.storeValue = this.deepFreeze(_newState) ;
|
||
}
|
||
else {
|
||
this.storeValue = newState;
|
||
}
|
||
if (!this.store) {
|
||
this.store = new BehaviorSubject({ state: this.storeValue });
|
||
{
|
||
this.store.subscribe(function (_a) {
|
||
var action = _a.action;
|
||
if (action) {
|
||
dispatchUpdate(_this.storeName, action);
|
||
}
|
||
});
|
||
}
|
||
return;
|
||
}
|
||
if (isTransactionInProcess()) {
|
||
this.handleTransaction();
|
||
return;
|
||
}
|
||
this.dispatch(this.storeValue, _dispatchAction);
|
||
};
|
||
/**
|
||
*
|
||
* Reset the current store back to the initial value
|
||
*
|
||
* @example
|
||
*
|
||
* store.reset()
|
||
*
|
||
*/
|
||
Store.prototype.reset = function () {
|
||
var _this = this;
|
||
if (this.isResettable()) {
|
||
setAction('Reset');
|
||
this._setState(function () { return Object.assign({}, _this._initialState); });
|
||
this.setHasCache(false);
|
||
}
|
||
else {
|
||
console.warn("You need to enable the reset functionality");
|
||
}
|
||
};
|
||
Store.prototype.update = function (stateOrCallback) {
|
||
setAction('Update');
|
||
var newState;
|
||
var currentState = this._value();
|
||
if (isFunction(stateOrCallback)) {
|
||
newState = isFunction(this._producerFn) ? this._producerFn(currentState, stateOrCallback) : stateOrCallback(currentState);
|
||
}
|
||
else {
|
||
newState = stateOrCallback;
|
||
}
|
||
var withHook = this.akitaPreUpdate(currentState, __assign({}, currentState, newState));
|
||
var resolved = isPlainObject$1(currentState) ? withHook : new currentState.constructor(withHook);
|
||
this._setState(resolved);
|
||
};
|
||
Store.prototype.updateStoreConfig = function (newOptions) {
|
||
this.options = __assign({}, this.options, newOptions);
|
||
};
|
||
// @internal
|
||
Store.prototype.akitaPreUpdate = function (_, nextState) {
|
||
return nextState;
|
||
};
|
||
Store.prototype.ngOnDestroy = function () {
|
||
this.destroy();
|
||
};
|
||
/**
|
||
*
|
||
* Destroy the store
|
||
*
|
||
* @example
|
||
*
|
||
* store.destroy()
|
||
*
|
||
*/
|
||
Store.prototype.destroy = function () {
|
||
var hmrEnabled = isBrowser ? window.hmrEnabled : false;
|
||
if (!hmrEnabled && this === __stores__[this.storeName]) {
|
||
delete __stores__[this.storeName];
|
||
dispatchDeleted(this.storeName);
|
||
this.setHasCache(false);
|
||
this.cache.active.complete();
|
||
this.store.complete();
|
||
}
|
||
};
|
||
Store.prototype.onInit = function (initialState) {
|
||
__stores__[this.storeName] = this;
|
||
this._setState(function () { return initialState; });
|
||
dispatchAdded(this.storeName);
|
||
if (this.isResettable()) {
|
||
this._initialState = initialState;
|
||
}
|
||
assertStoreHasName(this.storeName, this.constructor.name);
|
||
};
|
||
Store.prototype.dispatch = function (state, _dispatchAction) {
|
||
if (_dispatchAction === void 0) { _dispatchAction = true; }
|
||
var action = undefined;
|
||
if (_dispatchAction) {
|
||
action = currentAction;
|
||
resetCustomAction();
|
||
}
|
||
this.store.next({ state: state, action: action });
|
||
};
|
||
Store.prototype.watchTransaction = function () {
|
||
var _this = this;
|
||
commit().subscribe(function () {
|
||
_this.inTransaction = false;
|
||
_this.dispatch(_this._value());
|
||
});
|
||
};
|
||
Store.prototype.isResettable = function () {
|
||
if (this.resettable === false) {
|
||
return false;
|
||
}
|
||
return this.resettable || getAkitaConfig().resettable;
|
||
};
|
||
Store.prototype.handleTransaction = function () {
|
||
if (!this.inTransaction) {
|
||
this.watchTransaction();
|
||
this.inTransaction = true;
|
||
}
|
||
};
|
||
Store.prototype.getCacheTTL = function () {
|
||
return (this.cacheConfig && this.cacheConfig.ttl) || getAkitaConfig().ttl;
|
||
};
|
||
return Store;
|
||
}());
|
||
|
||
// @internal
|
||
function updateEntities(_a) {
|
||
var state = _a.state, ids = _a.ids, idKey = _a.idKey, newStateOrFn = _a.newStateOrFn, preUpdateEntity = _a.preUpdateEntity, producerFn = _a.producerFn, onEntityIdChanges = _a.onEntityIdChanges;
|
||
var e_1, _b;
|
||
var updatedEntities = {};
|
||
var isUpdatingIdKey = false;
|
||
var idToUpdate;
|
||
try {
|
||
for (var ids_1 = __values(ids), ids_1_1 = ids_1.next(); !ids_1_1.done; ids_1_1 = ids_1.next()) {
|
||
var id = ids_1_1.value;
|
||
// if the entity doesn't exist don't do anything
|
||
if (hasEntity(state.entities, id) === false) {
|
||
continue;
|
||
}
|
||
var oldEntity = state.entities[id];
|
||
var newState = void 0;
|
||
if (isFunction(newStateOrFn)) {
|
||
newState = isFunction(producerFn) ? producerFn(oldEntity, newStateOrFn) : newStateOrFn(oldEntity);
|
||
}
|
||
else {
|
||
newState = newStateOrFn;
|
||
}
|
||
var isIdChanged = newState.hasOwnProperty(idKey) && newState[idKey] !== oldEntity[idKey];
|
||
var newEntity = void 0;
|
||
idToUpdate = id;
|
||
if (isIdChanged) {
|
||
isUpdatingIdKey = true;
|
||
idToUpdate = newState[idKey];
|
||
}
|
||
var merged = __assign({}, oldEntity, newState);
|
||
if (isPlainObject$1(oldEntity)) {
|
||
newEntity = merged;
|
||
}
|
||
else {
|
||
/**
|
||
* In case that new state is class of it's own, there's
|
||
* a possibility that it will be different than the old
|
||
* class.
|
||
* For example, Old state is an instance of animal class
|
||
* and new state is instance of person class.
|
||
* To avoid run over new person class with the old animal
|
||
* class we check if the new state is a class of it's own.
|
||
* If so, use it. Otherwise, use the old state class
|
||
*/
|
||
if (isPlainObject$1(newState)) {
|
||
newEntity = new oldEntity.constructor(merged);
|
||
}
|
||
else {
|
||
newEntity = new newState.constructor(merged);
|
||
}
|
||
}
|
||
updatedEntities[idToUpdate] = preUpdateEntity(oldEntity, newEntity);
|
||
}
|
||
}
|
||
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
||
finally {
|
||
try {
|
||
if (ids_1_1 && !ids_1_1.done && (_b = ids_1.return)) _b.call(ids_1);
|
||
}
|
||
finally { if (e_1) throw e_1.error; }
|
||
}
|
||
var updatedIds = state.ids;
|
||
var stateEntities = state.entities;
|
||
if (isUpdatingIdKey) {
|
||
var _c = __read(ids, 1), id_1 = _c[0];
|
||
var _d = state.entities, _e = id_1, rest = __rest(_d, [typeof _e === "symbol" ? _e : _e + ""]);
|
||
stateEntities = rest;
|
||
updatedIds = state.ids.map(function (current) { return (current === id_1 ? idToUpdate : current); });
|
||
onEntityIdChanges(id_1, idToUpdate);
|
||
}
|
||
return __assign({}, state, { entities: __assign({}, stateEntities, updatedEntities), ids: updatedIds });
|
||
}
|
||
|
||
/**
|
||
*
|
||
* Store for managing a collection of entities
|
||
*
|
||
* @example
|
||
*
|
||
* export interface WidgetsState extends EntityState<Widget> { }
|
||
*
|
||
* @StoreConfig({ name: 'widgets' })
|
||
* export class WidgetsStore extends EntityStore<WidgetsState> {
|
||
* constructor() {
|
||
* super();
|
||
* }
|
||
* }
|
||
*
|
||
*
|
||
*/
|
||
var EntityStore = /** @class */ (function (_super) {
|
||
__extends(EntityStore, _super);
|
||
function EntityStore(initialState, options) {
|
||
if (initialState === void 0) { initialState = {}; }
|
||
if (options === void 0) { options = {}; }
|
||
var _this = _super.call(this, __assign({}, getInitialEntitiesState(), initialState), options) || this;
|
||
_this.options = options;
|
||
_this.entityActions = new Subject();
|
||
_this.entityIdChanges = new Subject();
|
||
return _this;
|
||
}
|
||
Object.defineProperty(EntityStore.prototype, "selectEntityAction$", {
|
||
// @internal
|
||
get: function () {
|
||
return this.entityActions.asObservable();
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
Object.defineProperty(EntityStore.prototype, "selectEntityIdChanges$", {
|
||
// @internal
|
||
get: function () {
|
||
return this.entityIdChanges.asObservable();
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
Object.defineProperty(EntityStore.prototype, "idKey", {
|
||
// @internal
|
||
get: function () {
|
||
return this.config.idKey || this.options.idKey || DEFAULT_ID_KEY;
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
/**
|
||
*
|
||
* Replace current collection with provided collection
|
||
*
|
||
* @example
|
||
*
|
||
* this.store.set([Entity, Entity])
|
||
* this.store.set({ids: [], entities: {}})
|
||
* this.store.set({ 1: {}, 2: {}})
|
||
*
|
||
*/
|
||
EntityStore.prototype.set = function (entities, options) {
|
||
var _this = this;
|
||
if (options === void 0) { options = {}; }
|
||
if (isNil(entities))
|
||
return;
|
||
setAction('Set Entity');
|
||
var isNativePreAdd = this.akitaPreAddEntity === EntityStore.prototype.akitaPreAddEntity;
|
||
this.setHasCache(true, { restartTTL: true });
|
||
this._setState(function (state) {
|
||
var newState = setEntities({
|
||
state: state,
|
||
entities: entities,
|
||
idKey: _this.idKey,
|
||
preAddEntity: _this.akitaPreAddEntity,
|
||
isNativePreAdd: isNativePreAdd,
|
||
});
|
||
if (isUndefined(options.activeId) === false) {
|
||
newState.active = options.activeId;
|
||
}
|
||
return newState;
|
||
});
|
||
if (this.hasInitialUIState()) {
|
||
this.handleUICreation();
|
||
}
|
||
this.entityActions.next({ type: EntityActions.Set, ids: this.ids });
|
||
};
|
||
/**
|
||
* Add entities
|
||
*
|
||
* @example
|
||
*
|
||
* this.store.add([Entity, Entity])
|
||
* this.store.add(Entity)
|
||
* this.store.add(Entity, { prepend: true })
|
||
*
|
||
* this.store.add(Entity, { loading: false })
|
||
*/
|
||
EntityStore.prototype.add = function (entities, options) {
|
||
if (options === void 0) { options = { loading: false }; }
|
||
var collection = coerceArray(entities);
|
||
if (isEmpty(collection))
|
||
return;
|
||
var data = addEntities({
|
||
state: this._value(),
|
||
preAddEntity: this.akitaPreAddEntity,
|
||
entities: collection,
|
||
idKey: this.idKey,
|
||
options: options,
|
||
});
|
||
if (data) {
|
||
setAction('Add Entity');
|
||
data.newState.loading = options.loading;
|
||
this._setState(function () { return data.newState; });
|
||
if (this.hasInitialUIState()) {
|
||
this.handleUICreation(true);
|
||
}
|
||
this.entityActions.next({ type: EntityActions.Add, ids: data.newIds });
|
||
}
|
||
};
|
||
EntityStore.prototype.update = function (idsOrFnOrState, newStateOrFn) {
|
||
var _this = this;
|
||
if (isUndefined(newStateOrFn)) {
|
||
_super.prototype.update.call(this, idsOrFnOrState);
|
||
return;
|
||
}
|
||
var ids = [];
|
||
if (isFunction(idsOrFnOrState)) {
|
||
// We need to filter according the predicate function
|
||
ids = this.ids.filter(function (id) { return idsOrFnOrState(_this.entities[id]); });
|
||
}
|
||
else {
|
||
// If it's nil we want all of them
|
||
ids = isNil(idsOrFnOrState) ? this.ids : coerceArray(idsOrFnOrState);
|
||
}
|
||
if (isEmpty(ids))
|
||
return;
|
||
setAction('Update Entity', ids);
|
||
var entityIdChanged;
|
||
this._setState(function (state) {
|
||
return updateEntities({
|
||
idKey: _this.idKey,
|
||
ids: ids,
|
||
preUpdateEntity: _this.akitaPreUpdateEntity,
|
||
state: state,
|
||
newStateOrFn: newStateOrFn,
|
||
producerFn: _this._producerFn,
|
||
onEntityIdChanges: function (oldId, newId) {
|
||
entityIdChanged = { oldId: oldId, newId: newId };
|
||
_this.entityIdChanges.next(__assign({}, entityIdChanged, { pending: true }));
|
||
},
|
||
});
|
||
});
|
||
if (entityIdChanged) {
|
||
this.entityIdChanges.next(__assign({}, entityIdChanged, { pending: false }));
|
||
}
|
||
this.entityActions.next({ type: EntityActions.Update, ids: ids });
|
||
};
|
||
EntityStore.prototype.upsert = function (ids, newState, onCreate, options) {
|
||
var _this = this;
|
||
if (options === void 0) { options = {}; }
|
||
var toArray = coerceArray(ids);
|
||
var predicate = function (isUpdate) { return function (id) { return hasEntity(_this.entities, id) === isUpdate; }; };
|
||
var baseClass = isFunction(onCreate) ? options.baseClass : onCreate ? onCreate.baseClass : undefined;
|
||
var isClassBased = isFunction(baseClass);
|
||
var updateIds = toArray.filter(predicate(true));
|
||
var newEntities = toArray.filter(predicate(false)).map(function (id) {
|
||
var _a;
|
||
var newStateObj = typeof newState === 'function' ? newState({}) : newState;
|
||
var entity = isFunction(onCreate) ? onCreate(id, newStateObj) : newStateObj;
|
||
var withId = __assign({}, entity, (_a = {}, _a[_this.idKey] = id, _a));
|
||
if (isClassBased) {
|
||
return new baseClass(withId);
|
||
}
|
||
return withId;
|
||
});
|
||
// it can be any of the three types
|
||
this.update(updateIds, newState);
|
||
this.add(newEntities);
|
||
logAction('Upsert Entity');
|
||
};
|
||
/**
|
||
*
|
||
* Upsert entity collection (idKey must be present)
|
||
*
|
||
* @example
|
||
*
|
||
* store.upsertMany([ { id: 1 }, { id: 2 }]);
|
||
*
|
||
* store.upsertMany([ { id: 1 }, { id: 2 }], { loading: true });
|
||
* store.upsertMany([ { id: 1 }, { id: 2 }], { baseClass: Todo });
|
||
*
|
||
*/
|
||
EntityStore.prototype.upsertMany = function (entities, options) {
|
||
if (options === void 0) { options = {}; }
|
||
var e_1, _a;
|
||
var addedIds = [];
|
||
var updatedIds = [];
|
||
var updatedEntities = {};
|
||
try {
|
||
// Update the state directly to optimize performance
|
||
for (var entities_1 = __values(entities), entities_1_1 = entities_1.next(); !entities_1_1.done; entities_1_1 = entities_1.next()) {
|
||
var entity = entities_1_1.value;
|
||
var withPreCheckHook = this.akitaPreCheckEntity(entity);
|
||
var id = withPreCheckHook[this.idKey];
|
||
if (hasEntity(this.entities, id)) {
|
||
var prev = this._value().entities[id];
|
||
var merged = __assign({}, this._value().entities[id], withPreCheckHook);
|
||
var next = options.baseClass ? new options.baseClass(merged) : merged;
|
||
var withHook = this.akitaPreUpdateEntity(prev, next);
|
||
var nextId = withHook[this.idKey];
|
||
updatedEntities[nextId] = withHook;
|
||
updatedIds.push(nextId);
|
||
}
|
||
else {
|
||
var newEntity = options.baseClass ? new options.baseClass(withPreCheckHook) : withPreCheckHook;
|
||
var withHook = this.akitaPreAddEntity(newEntity);
|
||
var nextId = withHook[this.idKey];
|
||
addedIds.push(nextId);
|
||
updatedEntities[nextId] = withHook;
|
||
}
|
||
}
|
||
}
|
||
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
||
finally {
|
||
try {
|
||
if (entities_1_1 && !entities_1_1.done && (_a = entities_1.return)) _a.call(entities_1);
|
||
}
|
||
finally { if (e_1) throw e_1.error; }
|
||
}
|
||
logAction('Upsert Many');
|
||
this._setState(function (state) { return (__assign({}, state, { ids: addedIds.length ? __spread(state.ids, addedIds) : state.ids, entities: __assign({}, state.entities, updatedEntities), loading: !!options.loading })); });
|
||
updatedIds.length && this.entityActions.next({ type: EntityActions.Update, ids: updatedIds });
|
||
addedIds.length && this.entityActions.next({ type: EntityActions.Add, ids: addedIds });
|
||
if (addedIds.length && this.hasUIStore()) {
|
||
this.handleUICreation(true);
|
||
}
|
||
};
|
||
/**
|
||
*
|
||
* Replace one or more entities (except the id property)
|
||
*
|
||
*
|
||
* @example
|
||
*
|
||
* this.store.replace(5, newEntity)
|
||
* this.store.replace([1,2,3], newEntity)
|
||
*/
|
||
EntityStore.prototype.replace = function (ids, newState) {
|
||
var e_2, _a;
|
||
var toArray = coerceArray(ids);
|
||
if (isEmpty(toArray))
|
||
return;
|
||
var replaced = {};
|
||
try {
|
||
for (var toArray_1 = __values(toArray), toArray_1_1 = toArray_1.next(); !toArray_1_1.done; toArray_1_1 = toArray_1.next()) {
|
||
var id = toArray_1_1.value;
|
||
newState[this.idKey] = id;
|
||
replaced[id] = newState;
|
||
}
|
||
}
|
||
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
||
finally {
|
||
try {
|
||
if (toArray_1_1 && !toArray_1_1.done && (_a = toArray_1.return)) _a.call(toArray_1);
|
||
}
|
||
finally { if (e_2) throw e_2.error; }
|
||
}
|
||
setAction('Replace Entity', ids);
|
||
this._setState(function (state) { return (__assign({}, state, { entities: __assign({}, state.entities, replaced) })); });
|
||
};
|
||
/**
|
||
*
|
||
* Move entity inside the collection
|
||
*
|
||
*
|
||
* @example
|
||
*
|
||
* this.store.move(fromIndex, toIndex)
|
||
*/
|
||
EntityStore.prototype.move = function (from, to) {
|
||
var ids = this.ids.slice();
|
||
ids.splice(to < 0 ? ids.length + to : to, 0, ids.splice(from, 1)[0]);
|
||
setAction('Move Entity');
|
||
this._setState(function (state) { return (__assign({}, state, {
|
||
// Change the entities reference so that selectAll emit
|
||
entities: __assign({}, state.entities), ids: ids })); });
|
||
};
|
||
EntityStore.prototype.remove = function (idsOrFn) {
|
||
var _this = this;
|
||
if (isEmpty(this.ids))
|
||
return;
|
||
var idPassed = isDefined(idsOrFn);
|
||
// null means remove all
|
||
var ids = [];
|
||
if (isFunction(idsOrFn)) {
|
||
ids = this.ids.filter(function (entityId) { return idsOrFn(_this.entities[entityId]); });
|
||
}
|
||
else {
|
||
ids = idPassed ? coerceArray(idsOrFn) : this.ids;
|
||
}
|
||
if (isEmpty(ids))
|
||
return;
|
||
setAction('Remove Entity', ids);
|
||
this._setState(function (state) { return removeEntities({ state: state, ids: ids }); });
|
||
if (!idPassed) {
|
||
this.setHasCache(false);
|
||
}
|
||
this.handleUIRemove(ids);
|
||
this.entityActions.next({ type: EntityActions.Remove, ids: ids });
|
||
};
|
||
/**
|
||
*
|
||
* Update the active entity
|
||
*
|
||
* @example
|
||
*
|
||
* this.store.updateActive({ completed: true })
|
||
* this.store.updateActive(active => {
|
||
* return {
|
||
* config: {
|
||
* ..active.config,
|
||
* date
|
||
* }
|
||
* }
|
||
* })
|
||
*/
|
||
EntityStore.prototype.updateActive = function (newStateOrCallback) {
|
||
var ids = coerceArray(this.active);
|
||
setAction('Update Active', ids);
|
||
this.update(ids, newStateOrCallback);
|
||
};
|
||
EntityStore.prototype.setActive = function (idOrOptions) {
|
||
var active = getActiveEntities(idOrOptions, this.ids, this.active);
|
||
if (active === undefined) {
|
||
return;
|
||
}
|
||
setAction('Set Active', active);
|
||
this._setActive(active);
|
||
};
|
||
/**
|
||
* Add active entities
|
||
*
|
||
* @example
|
||
*
|
||
* store.addActive(2);
|
||
* store.addActive([3, 4, 5]);
|
||
*/
|
||
EntityStore.prototype.addActive = function (ids) {
|
||
var _this = this;
|
||
var toArray = coerceArray(ids);
|
||
if (isEmpty(toArray))
|
||
return;
|
||
var everyExist = toArray.every(function (id) { return _this.active.indexOf(id) > -1; });
|
||
if (everyExist)
|
||
return;
|
||
setAction('Add Active', ids);
|
||
this._setState(function (state) {
|
||
/** Protect against case that one of the items in the array exist */
|
||
var uniques = Array.from(new Set(__spread(state.active, toArray)));
|
||
return __assign({}, state, { active: uniques });
|
||
});
|
||
};
|
||
/**
|
||
* Remove active entities
|
||
*
|
||
* @example
|
||
*
|
||
* store.removeActive(2)
|
||
* store.removeActive([3, 4, 5])
|
||
*/
|
||
EntityStore.prototype.removeActive = function (ids) {
|
||
var _this = this;
|
||
var toArray = coerceArray(ids);
|
||
if (isEmpty(toArray))
|
||
return;
|
||
var someExist = toArray.some(function (id) { return _this.active.indexOf(id) > -1; });
|
||
if (!someExist)
|
||
return;
|
||
setAction('Remove Active', ids);
|
||
this._setState(function (state) {
|
||
return __assign({}, state, { active: Array.isArray(state.active) ? state.active.filter(function (currentId) { return toArray.indexOf(currentId) === -1; }) : null });
|
||
});
|
||
};
|
||
/**
|
||
* Toggle active entities
|
||
*
|
||
* @example
|
||
*
|
||
* store.toggle(2)
|
||
* store.toggle([3, 4, 5])
|
||
*/
|
||
EntityStore.prototype.toggleActive = function (ids) {
|
||
var _this = this;
|
||
var toArray = coerceArray(ids);
|
||
var filterExists = function (remove) { return function (id) { return _this.active.includes(id) === remove; }; };
|
||
var remove = toArray.filter(filterExists(true));
|
||
var add = toArray.filter(filterExists(false));
|
||
this.removeActive(remove);
|
||
this.addActive(add);
|
||
logAction('Toggle Active');
|
||
};
|
||
/**
|
||
*
|
||
* Create sub UI store for managing Entity's UI state
|
||
*
|
||
* @example
|
||
*
|
||
* export type ProductUI = {
|
||
* isLoading: boolean;
|
||
* isOpen: boolean
|
||
* }
|
||
*
|
||
* interface ProductsUIState extends EntityState<ProductUI> {}
|
||
*
|
||
* export class ProductsStore EntityStore<ProductsState, Product> {
|
||
* ui: EntityUIStore<ProductsUIState, ProductUI>;
|
||
*
|
||
* constructor() {
|
||
* super();
|
||
* this.createUIStore();
|
||
* }
|
||
*
|
||
* }
|
||
*/
|
||
EntityStore.prototype.createUIStore = function (initialState, storeConfig) {
|
||
if (initialState === void 0) { initialState = {}; }
|
||
if (storeConfig === void 0) { storeConfig = {}; }
|
||
var defaults = { name: "UI/" + this.storeName, idKey: this.idKey };
|
||
this.ui = new EntityUIStore(initialState, __assign({}, defaults, storeConfig));
|
||
return this.ui;
|
||
};
|
||
// @internal
|
||
EntityStore.prototype.destroy = function () {
|
||
_super.prototype.destroy.call(this);
|
||
if (this.ui instanceof EntityStore) {
|
||
this.ui.destroy();
|
||
}
|
||
this.entityActions.complete();
|
||
};
|
||
// @internal
|
||
EntityStore.prototype.akitaPreUpdateEntity = function (_, nextEntity) {
|
||
return nextEntity;
|
||
};
|
||
// @internal
|
||
EntityStore.prototype.akitaPreAddEntity = function (newEntity) {
|
||
return newEntity;
|
||
};
|
||
// @internal
|
||
EntityStore.prototype.akitaPreCheckEntity = function (newEntity) {
|
||
return newEntity;
|
||
};
|
||
Object.defineProperty(EntityStore.prototype, "ids", {
|
||
get: function () {
|
||
return this._value().ids;
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
Object.defineProperty(EntityStore.prototype, "entities", {
|
||
get: function () {
|
||
return this._value().entities;
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
Object.defineProperty(EntityStore.prototype, "active", {
|
||
get: function () {
|
||
return this._value().active;
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
EntityStore.prototype._setActive = function (ids) {
|
||
this._setState(function (state) {
|
||
return __assign({}, state, { active: ids });
|
||
});
|
||
};
|
||
EntityStore.prototype.handleUICreation = function (add) {
|
||
var _this = this;
|
||
if (add === void 0) { add = false; }
|
||
var ids = this.ids;
|
||
var isFunc = isFunction(this.ui._akitaCreateEntityFn);
|
||
var uiEntities;
|
||
var createFn = function (id) {
|
||
var _a;
|
||
var current = _this.entities[id];
|
||
var ui = isFunc ? _this.ui._akitaCreateEntityFn(current) : _this.ui._akitaCreateEntityFn;
|
||
return __assign((_a = {}, _a[_this.idKey] = current[_this.idKey], _a), ui);
|
||
};
|
||
if (add) {
|
||
uiEntities = this.ids.filter(function (id) { return isUndefined(_this.ui.entities[id]); }).map(createFn);
|
||
}
|
||
else {
|
||
uiEntities = ids.map(createFn);
|
||
}
|
||
add ? this.ui.add(uiEntities) : this.ui.set(uiEntities);
|
||
};
|
||
EntityStore.prototype.hasInitialUIState = function () {
|
||
return this.hasUIStore() && isUndefined(this.ui._akitaCreateEntityFn) === false;
|
||
};
|
||
EntityStore.prototype.handleUIRemove = function (ids) {
|
||
if (this.hasUIStore()) {
|
||
this.ui.remove(ids);
|
||
}
|
||
};
|
||
EntityStore.prototype.hasUIStore = function () {
|
||
return this.ui instanceof EntityUIStore;
|
||
};
|
||
var _b;
|
||
__decorate$2([
|
||
transaction(),
|
||
__metadata("design:type", Function),
|
||
__metadata("design:paramtypes", [Object, Object, Object, Object]),
|
||
__metadata("design:returntype", void 0)
|
||
], EntityStore.prototype, "upsert", null);
|
||
__decorate$2([
|
||
transaction(),
|
||
__metadata("design:type", Function),
|
||
__metadata("design:paramtypes", [typeof (_b = typeof T !== "undefined" && T) === "function" ? _b : Object]),
|
||
__metadata("design:returntype", void 0)
|
||
], EntityStore.prototype, "toggleActive", null);
|
||
return EntityStore;
|
||
}(Store));
|
||
// @internal
|
||
var EntityUIStore = /** @class */ (function (_super) {
|
||
__extends(EntityUIStore, _super);
|
||
function EntityUIStore(initialState, storeConfig) {
|
||
if (initialState === void 0) { initialState = {}; }
|
||
if (storeConfig === void 0) { storeConfig = {}; }
|
||
return _super.call(this, initialState, storeConfig) || this;
|
||
}
|
||
/**
|
||
*
|
||
* Set the initial UI entity state. This function will determine the entity's
|
||
* initial state when we call `set()` or `add()`.
|
||
*
|
||
* @example
|
||
*
|
||
* constructor() {
|
||
* super();
|
||
* this.createUIStore().setInitialEntityState(entity => ({ isLoading: false, isOpen: true }));
|
||
* this.createUIStore().setInitialEntityState({ isLoading: false, isOpen: true });
|
||
* }
|
||
*
|
||
*/
|
||
EntityUIStore.prototype.setInitialEntityState = function (createFn) {
|
||
this._akitaCreateEntityFn = createFn;
|
||
};
|
||
return EntityUIStore;
|
||
}(EntityStore));
|
||
// @internal
|
||
function distinctUntilArrayItemChanged() {
|
||
return distinctUntilChanged(function (prevCollection, currentCollection) {
|
||
if (prevCollection === currentCollection) {
|
||
return true;
|
||
}
|
||
if (isArray(prevCollection) === false || isArray(currentCollection) === false) {
|
||
return false;
|
||
}
|
||
if (isEmpty(prevCollection) && isEmpty(currentCollection)) {
|
||
return true;
|
||
}
|
||
// if item is new in the current collection but not exist in the prev collection
|
||
var hasNewItem = hasChange(currentCollection, prevCollection);
|
||
if (hasNewItem) {
|
||
return false;
|
||
}
|
||
var isOneOfItemReferenceChanged = hasChange(prevCollection, currentCollection);
|
||
// return false means there is a change and we want to call next()
|
||
return isOneOfItemReferenceChanged === false;
|
||
});
|
||
}
|
||
// @internal
|
||
function hasChange(first, second) {
|
||
var hasChange = second.some(function (currentItem) {
|
||
var oldItem = first.find(function (prevItem) { return prevItem === currentItem; });
|
||
return oldItem === undefined;
|
||
});
|
||
return hasChange;
|
||
}
|
||
|
||
var Order;
|
||
(function (Order) {
|
||
Order["ASC"] = "asc";
|
||
Order["DESC"] = "desc";
|
||
})(Order || (Order = {}));
|
||
// @internal
|
||
function compareValues(key, order) {
|
||
if (order === void 0) { order = Order.ASC; }
|
||
return function (a, b) {
|
||
if (!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) {
|
||
return 0;
|
||
}
|
||
var varA = typeof a[key] === 'string' ? a[key].toUpperCase() : a[key];
|
||
var varB = typeof b[key] === 'string' ? b[key].toUpperCase() : b[key];
|
||
var comparison = 0;
|
||
if (varA > varB) {
|
||
comparison = 1;
|
||
}
|
||
else if (varA < varB) {
|
||
comparison = -1;
|
||
}
|
||
return order == Order.DESC ? comparison * -1 : comparison;
|
||
};
|
||
}
|
||
|
||
// @internal
|
||
function entitiesToArray(state, options) {
|
||
var arr = [];
|
||
var ids = state.ids, entities = state.entities;
|
||
var filterBy = options.filterBy, limitTo = options.limitTo, sortBy = options.sortBy, sortByOrder = options.sortByOrder;
|
||
var _loop_1 = function (i) {
|
||
var entity = entities[ids[i]];
|
||
if (!filterBy) {
|
||
arr.push(entity);
|
||
return "continue";
|
||
}
|
||
var toArray = coerceArray(filterBy);
|
||
var allPass = toArray.every(function (fn) { return fn(entity, i); });
|
||
if (allPass) {
|
||
arr.push(entity);
|
||
}
|
||
};
|
||
for (var i = 0; i < ids.length; i++) {
|
||
_loop_1(i);
|
||
}
|
||
if (sortBy) {
|
||
var _sortBy_1 = isFunction(sortBy) ? sortBy : compareValues(sortBy, sortByOrder);
|
||
arr = arr.sort(function (a, b) { return _sortBy_1(a, b, state); });
|
||
}
|
||
var length = Math.min(limitTo || arr.length, arr.length);
|
||
return length === arr.length ? arr : arr.slice(0, length);
|
||
}
|
||
|
||
// @internal
|
||
function entitiesToMap(state, options) {
|
||
var map = {};
|
||
var filterBy = options.filterBy, limitTo = options.limitTo;
|
||
var ids = state.ids, entities = state.entities;
|
||
if (!filterBy && !limitTo) {
|
||
return entities;
|
||
}
|
||
var hasLimit = isNil(limitTo) === false;
|
||
if (filterBy && hasLimit) {
|
||
var count = 0;
|
||
var _loop_1 = function (i, length_1) {
|
||
if (count === limitTo)
|
||
return "break";
|
||
var id = ids[i];
|
||
var entity = entities[id];
|
||
var allPass = coerceArray(filterBy).every(function (fn) { return fn(entity, i); });
|
||
if (allPass) {
|
||
map[id] = entity;
|
||
count++;
|
||
}
|
||
};
|
||
for (var i = 0, length_1 = ids.length; i < length_1; i++) {
|
||
var state_1 = _loop_1(i, length_1);
|
||
if (state_1 === "break")
|
||
break;
|
||
}
|
||
}
|
||
else {
|
||
var finalLength = Math.min(limitTo || ids.length, ids.length);
|
||
var _loop_2 = function (i) {
|
||
var id = ids[i];
|
||
var entity = entities[id];
|
||
if (!filterBy) {
|
||
map[id] = entity;
|
||
return "continue";
|
||
}
|
||
var allPass = coerceArray(filterBy).every(function (fn) { return fn(entity, i); });
|
||
if (allPass) {
|
||
map[id] = entity;
|
||
}
|
||
};
|
||
for (var i = 0; i < finalLength; i++) {
|
||
_loop_2(i);
|
||
}
|
||
}
|
||
return map;
|
||
}
|
||
|
||
// @internal
|
||
function isString(value) {
|
||
return typeof value === 'string';
|
||
}
|
||
|
||
// @internal
|
||
function findEntityByPredicate(predicate, entities) {
|
||
var e_1, _a;
|
||
try {
|
||
for (var _b = __values(Object.keys(entities)), _c = _b.next(); !_c.done; _c = _b.next()) {
|
||
var entityId = _c.value;
|
||
if (predicate(entities[entityId]) === true) {
|
||
return entityId;
|
||
}
|
||
}
|
||
}
|
||
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
||
finally {
|
||
try {
|
||
if (_c && !_c.done && (_a = _b.return)) _a.call(_b);
|
||
}
|
||
finally { if (e_1) throw e_1.error; }
|
||
}
|
||
return undefined;
|
||
}
|
||
// @internal
|
||
function getEntity(id, project) {
|
||
return function (entities) {
|
||
var entity = entities[id];
|
||
if (isUndefined(entity)) {
|
||
return undefined;
|
||
}
|
||
if (!project) {
|
||
return entity;
|
||
}
|
||
if (isString(project)) {
|
||
return entity[project];
|
||
}
|
||
return project(entity);
|
||
};
|
||
}
|
||
|
||
// @internal
|
||
function mapSkipUndefined(arr, callbackFn) {
|
||
return arr.reduce(function (result, value, index, array) {
|
||
var val = callbackFn(value, index, array);
|
||
if (val !== undefined) {
|
||
result.push(val);
|
||
}
|
||
return result;
|
||
}, []);
|
||
}
|
||
|
||
var queryConfigKey = 'akitaQueryConfig';
|
||
|
||
function compareKeys(keysOrFuncs) {
|
||
return function (prevState, currState) {
|
||
var isFns = isFunction(keysOrFuncs[0]);
|
||
// Return when they are NOT changed
|
||
return keysOrFuncs.some(function (keyOrFunc) {
|
||
if (isFns) {
|
||
return keyOrFunc(prevState) !== keyOrFunc(currState);
|
||
}
|
||
return prevState[keyOrFunc] !== currState[keyOrFunc];
|
||
}) === false;
|
||
};
|
||
}
|
||
|
||
var Query = /** @class */ (function () {
|
||
function Query(store) {
|
||
this.store = store;
|
||
this.__store__ = store;
|
||
{
|
||
// @internal
|
||
__queries__[store.storeName] = this;
|
||
}
|
||
}
|
||
Query.prototype.select = function (project) {
|
||
var mapFn;
|
||
if (isFunction(project)) {
|
||
mapFn = project;
|
||
}
|
||
else if (isString(project)) {
|
||
mapFn = function (state) { return state[project]; };
|
||
}
|
||
else if (Array.isArray(project)) {
|
||
return this.store
|
||
._select(function (state) { return state; })
|
||
.pipe(distinctUntilChanged(compareKeys(project)), map(function (state) {
|
||
if (isFunction(project[0])) {
|
||
return project.map(function (func) { return func(state); });
|
||
}
|
||
return project.reduce(function (acc, k) {
|
||
acc[k] = state[k];
|
||
return acc;
|
||
}, {});
|
||
}));
|
||
}
|
||
else {
|
||
mapFn = function (state) { return state; };
|
||
}
|
||
return this.store._select(mapFn);
|
||
};
|
||
/**
|
||
* Select the loading state
|
||
*
|
||
* @example
|
||
*
|
||
* this.query.selectLoading().subscribe(isLoading => {})
|
||
*/
|
||
Query.prototype.selectLoading = function () {
|
||
return this.select(function (state) { return state.loading; });
|
||
};
|
||
/**
|
||
* Select the error state
|
||
*
|
||
* @example
|
||
*
|
||
* this.query.selectError().subscribe(error => {})
|
||
*/
|
||
Query.prototype.selectError = function () {
|
||
return this.select(function (state) { return state.error; });
|
||
};
|
||
/**
|
||
* Get the store's value
|
||
*
|
||
* @example
|
||
*
|
||
* this.query.getValue()
|
||
*
|
||
*/
|
||
Query.prototype.getValue = function () {
|
||
return this.store._value();
|
||
};
|
||
/**
|
||
* Select the cache state
|
||
*
|
||
* @example
|
||
*
|
||
* this.query.selectHasCache().pipe(
|
||
* switchMap(hasCache => {
|
||
* return hasCache ? of() : http().pipe(res => store.set(res))
|
||
* })
|
||
* )
|
||
*/
|
||
Query.prototype.selectHasCache = function () {
|
||
return this.store._cache().asObservable();
|
||
};
|
||
/**
|
||
* Whether we've cached data
|
||
*
|
||
* @example
|
||
*
|
||
* this.query.getHasCache()
|
||
*
|
||
*/
|
||
Query.prototype.getHasCache = function () {
|
||
return this.store._cache().value;
|
||
};
|
||
Object.defineProperty(Query.prototype, "config", {
|
||
// @internal
|
||
get: function () {
|
||
return this.constructor[queryConfigKey];
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
return Query;
|
||
}());
|
||
|
||
// @internal
|
||
function sortByOptions(options, config) {
|
||
options.sortBy = options.sortBy || (config && config.sortBy);
|
||
options.sortByOrder = options.sortByOrder || (config && config.sortByOrder);
|
||
}
|
||
|
||
/**
|
||
*
|
||
* The Entity Query is similar to the general Query, with additional functionality tailored for EntityStores.
|
||
*
|
||
* class WidgetsQuery extends QueryEntity<WidgetsState> {
|
||
* constructor(protected store: WidgetsStore) {
|
||
* super(store);
|
||
* }
|
||
* }
|
||
*
|
||
*
|
||
*
|
||
*/
|
||
var QueryEntity = /** @class */ (function (_super) {
|
||
__extends(QueryEntity, _super);
|
||
function QueryEntity(store, options) {
|
||
if (options === void 0) { options = {}; }
|
||
var _this = _super.call(this, store) || this;
|
||
_this.options = options;
|
||
_this.__store__ = store;
|
||
return _this;
|
||
}
|
||
QueryEntity.prototype.selectAll = function (options) {
|
||
var _this = this;
|
||
if (options === void 0) { options = {
|
||
asObject: false,
|
||
}; }
|
||
return this.select(function (state) { return state.entities; }).pipe(map(function () { return _this.getAll(options); }));
|
||
};
|
||
QueryEntity.prototype.getAll = function (options) {
|
||
if (options === void 0) { options = { asObject: false, filterBy: undefined, limitTo: undefined }; }
|
||
if (options.asObject) {
|
||
return entitiesToMap(this.getValue(), options);
|
||
}
|
||
sortByOptions(options, this.config || this.options);
|
||
return entitiesToArray(this.getValue(), options);
|
||
};
|
||
QueryEntity.prototype.selectMany = function (ids, project) {
|
||
if (!ids || !ids.length)
|
||
return of([]);
|
||
return this.select(function (state) { return state.entities; }).pipe(map(function (entities) { return mapSkipUndefined(ids, function (id) { return getEntity(id, project)(entities); }); }), distinctUntilArrayItemChanged());
|
||
};
|
||
QueryEntity.prototype.selectEntity = function (idOrPredicate, project) {
|
||
var id = idOrPredicate;
|
||
if (isFunction(idOrPredicate)) {
|
||
// For performance reason we expect the entity to be in the store
|
||
id = findEntityByPredicate(idOrPredicate, this.getValue().entities);
|
||
}
|
||
return this.select(function (state) { return state.entities; }).pipe(map(getEntity(id, project)), distinctUntilChanged());
|
||
};
|
||
/**
|
||
* Get an entity by id
|
||
*
|
||
* @example
|
||
*
|
||
* this.query.getEntity(1);
|
||
*/
|
||
QueryEntity.prototype.getEntity = function (id) {
|
||
return this.getValue().entities[id];
|
||
};
|
||
/**
|
||
* Select the active entity's id
|
||
*
|
||
* @example
|
||
*
|
||
* this.query.selectActiveId()
|
||
*/
|
||
QueryEntity.prototype.selectActiveId = function () {
|
||
return this.select(function (state) { return state.active; });
|
||
};
|
||
/**
|
||
* Get the active id
|
||
*
|
||
* @example
|
||
*
|
||
* this.query.getActiveId()
|
||
*/
|
||
QueryEntity.prototype.getActiveId = function () {
|
||
return this.getValue().active;
|
||
};
|
||
QueryEntity.prototype.selectActive = function (project) {
|
||
var _this = this;
|
||
if (isArray(this.getActive())) {
|
||
return this.selectActiveId().pipe(switchMap(function (ids) { return _this.selectMany(ids, project); }));
|
||
}
|
||
return this.selectActiveId().pipe(switchMap(function (ids) { return _this.selectEntity(ids, project); }));
|
||
};
|
||
QueryEntity.prototype.getActive = function () {
|
||
var _this = this;
|
||
var activeId = this.getActiveId();
|
||
if (isArray(activeId)) {
|
||
return activeId.map(function (id) { return _this.getValue().entities[id]; });
|
||
}
|
||
return toBoolean(activeId) ? this.getEntity(activeId) : undefined;
|
||
};
|
||
/**
|
||
* Select the store's entity collection length
|
||
*
|
||
* @example
|
||
*
|
||
* this.query.selectCount()
|
||
* this.query.selectCount(entity => entity.completed)
|
||
*/
|
||
QueryEntity.prototype.selectCount = function (predicate) {
|
||
var _this = this;
|
||
return this.select(function (state) { return state.entities; }).pipe(map(function () { return _this.getCount(predicate); }));
|
||
};
|
||
/**
|
||
* Get the store's entity collection length
|
||
*
|
||
* @example
|
||
*
|
||
* this.query.getCount()
|
||
* this.query.getCount(entity => entity.completed)
|
||
*/
|
||
QueryEntity.prototype.getCount = function (predicate) {
|
||
if (isFunction(predicate)) {
|
||
return this.getAll().filter(predicate).length;
|
||
}
|
||
return this.getValue().ids.length;
|
||
};
|
||
QueryEntity.prototype.selectLast = function (project) {
|
||
return this.selectAt(function (ids) { return ids[ids.length - 1]; }, project);
|
||
};
|
||
QueryEntity.prototype.selectFirst = function (project) {
|
||
return this.selectAt(function (ids) { return ids[0]; }, project);
|
||
};
|
||
QueryEntity.prototype.selectEntityAction = function (actionOrActions) {
|
||
if (isNil(actionOrActions)) {
|
||
return this.store.selectEntityAction$;
|
||
}
|
||
var project = isArray(actionOrActions) ? function (action) { return action; } : function (_a) {
|
||
var ids = _a.ids;
|
||
return ids;
|
||
};
|
||
var actions = coerceArray(actionOrActions);
|
||
return this.store.selectEntityAction$.pipe(filter(function (_a) {
|
||
var type = _a.type;
|
||
return actions.includes(type);
|
||
}), map(function (action) { return project(action); }));
|
||
};
|
||
QueryEntity.prototype.hasEntity = function (projectOrIds) {
|
||
var _this = this;
|
||
if (isNil(projectOrIds)) {
|
||
return this.getValue().ids.length > 0;
|
||
}
|
||
if (isFunction(projectOrIds)) {
|
||
return this.getAll().some(projectOrIds);
|
||
}
|
||
if (isArray(projectOrIds)) {
|
||
return projectOrIds.every(function (id) { return id in _this.getValue().entities; });
|
||
}
|
||
return projectOrIds in this.getValue().entities;
|
||
};
|
||
/**
|
||
* Returns whether entity store has an active entity
|
||
*
|
||
* @example
|
||
*
|
||
* this.query.hasActive()
|
||
* this.query.hasActive(3)
|
||
*
|
||
*/
|
||
QueryEntity.prototype.hasActive = function (id) {
|
||
var active = this.getValue().active;
|
||
var isIdProvided = isDefined(id);
|
||
if (Array.isArray(active)) {
|
||
if (isIdProvided) {
|
||
return active.includes(id);
|
||
}
|
||
return active.length > 0;
|
||
}
|
||
return isIdProvided ? active === id : isDefined(active);
|
||
};
|
||
/**
|
||
*
|
||
* Create sub UI query for querying Entity's UI state
|
||
*
|
||
* @example
|
||
*
|
||
*
|
||
* export class ProductsQuery extends QueryEntity<ProductsState> {
|
||
* ui: EntityUIQuery<ProductsUIState>;
|
||
*
|
||
* constructor(protected store: ProductsStore) {
|
||
* super(store);
|
||
* this.createUIQuery();
|
||
* }
|
||
*
|
||
* }
|
||
*/
|
||
QueryEntity.prototype.createUIQuery = function () {
|
||
this.ui = new EntityUIQuery(this.__store__.ui);
|
||
};
|
||
QueryEntity.prototype.selectAt = function (mapFn, project) {
|
||
var _this = this;
|
||
return this.select(function (state) { return state.ids; }).pipe(map(mapFn), distinctUntilChanged(), switchMap(function (id) { return _this.selectEntity(id, project); }));
|
||
};
|
||
return QueryEntity;
|
||
}(Query));
|
||
// @internal
|
||
var EntityUIQuery = /** @class */ (function (_super) {
|
||
__extends(EntityUIQuery, _super);
|
||
function EntityUIQuery(store) {
|
||
return _super.call(this, store) || this;
|
||
}
|
||
return EntityUIQuery;
|
||
}(QueryEntity));
|
||
|
||
/**
|
||
* @example
|
||
*
|
||
* query.selectEntity(2).pipe(filterNil)
|
||
*/
|
||
var filterNil = function (source) { return source.pipe(filter(function (value) { return value !== null && typeof value !== 'undefined'; })); };
|
||
|
||
/**
|
||
* @internal
|
||
*
|
||
* @example
|
||
*
|
||
* getValue(state, 'todos.ui')
|
||
*
|
||
*/
|
||
function getValue(obj, prop) {
|
||
/** return the whole state */
|
||
if (prop.split('.').length === 1) {
|
||
return obj;
|
||
}
|
||
var removeStoreName = prop
|
||
.split('.')
|
||
.slice(1)
|
||
.join('.');
|
||
return removeStoreName.split('.').reduce(function (acc, part) { return acc && acc[part]; }, obj);
|
||
}
|
||
|
||
/**
|
||
* @internal
|
||
*
|
||
* @example
|
||
* setValue(state, 'todos.ui', { filter: {} })
|
||
*/
|
||
function setValue(obj, prop, val) {
|
||
var split = prop.split('.');
|
||
if (split.length === 1) {
|
||
return __assign({}, obj, val);
|
||
}
|
||
obj = __assign({}, obj);
|
||
var lastIndex = split.length - 2;
|
||
var removeStoreName = prop.split('.').slice(1);
|
||
removeStoreName.reduce(function (acc, part, index) {
|
||
if (index !== lastIndex) {
|
||
acc[part] = __assign({}, acc[part]);
|
||
return acc && acc[part];
|
||
}
|
||
acc[part] = Array.isArray(acc[part]) || !isObject(acc[part]) ? val : __assign({}, acc[part], val);
|
||
return acc && acc[part];
|
||
}, obj);
|
||
return obj;
|
||
}
|
||
new ReplaySubject(1);
|
||
|
||
var AkitaPlugin = /** @class */ (function () {
|
||
function AkitaPlugin(query, config) {
|
||
this.query = query;
|
||
}
|
||
/** This method is responsible for getting access to the query. */
|
||
AkitaPlugin.prototype.getQuery = function () {
|
||
return this.query;
|
||
};
|
||
/** This method is responsible for getting access to the store. */
|
||
AkitaPlugin.prototype.getStore = function () {
|
||
return this.getQuery().__store__;
|
||
};
|
||
/** This method is responsible tells whether the plugin is entityBased or not. */
|
||
AkitaPlugin.prototype.isEntityBased = function (entityId) {
|
||
return toBoolean(entityId);
|
||
};
|
||
/** This method is responsible for selecting the source; it can be the whole store or one entity. */
|
||
AkitaPlugin.prototype.selectSource = function (entityId, property) {
|
||
var _this = this;
|
||
if (this.isEntityBased(entityId)) {
|
||
return this.getQuery().selectEntity(entityId).pipe(filterNil);
|
||
}
|
||
if (property) {
|
||
return this.getQuery().select(function (state) { return getValue(state, _this.withStoreName(property)); });
|
||
}
|
||
return this.getQuery().select();
|
||
};
|
||
AkitaPlugin.prototype.getSource = function (entityId, property) {
|
||
if (this.isEntityBased(entityId)) {
|
||
return this.getQuery().getEntity(entityId);
|
||
}
|
||
var state = this.getQuery().getValue();
|
||
if (property) {
|
||
return getValue(state, this.withStoreName(property));
|
||
}
|
||
return state;
|
||
};
|
||
AkitaPlugin.prototype.withStoreName = function (prop) {
|
||
return this.storeName + "." + prop;
|
||
};
|
||
Object.defineProperty(AkitaPlugin.prototype, "storeName", {
|
||
get: function () {
|
||
return this.getStore().storeName;
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
/** This method is responsible for updating the store or one entity; it can be the whole store or one entity. */
|
||
AkitaPlugin.prototype.updateStore = function (newState, entityId, property) {
|
||
var _this = this;
|
||
if (this.isEntityBased(entityId)) {
|
||
this.getStore().update(entityId, newState);
|
||
}
|
||
else {
|
||
if (property) {
|
||
this.getStore()._setState(function (state) {
|
||
return setValue(state, _this.withStoreName(property), newState);
|
||
});
|
||
return;
|
||
}
|
||
this.getStore()._setState(function (state) { return (__assign({}, state, newState)); });
|
||
}
|
||
};
|
||
/**
|
||
* Function to invoke upon reset
|
||
*/
|
||
AkitaPlugin.prototype.onReset = function (fn) {
|
||
var _this = this;
|
||
var original = this.getStore().reset;
|
||
this.getStore().reset = function () {
|
||
var params = [];
|
||
for (var _i = 0; _i < arguments.length; _i++) {
|
||
params[_i] = arguments[_i];
|
||
}
|
||
/** It should run after the plugin destroy method */
|
||
setTimeout(function () {
|
||
original.apply(_this.getStore(), params);
|
||
fn();
|
||
});
|
||
};
|
||
};
|
||
return AkitaPlugin;
|
||
}());
|
||
|
||
var paginatorDefaults = {
|
||
pagesControls: false,
|
||
range: false,
|
||
startWith: 1,
|
||
cacheTimeout: undefined,
|
||
clearStoreWithCache: true
|
||
};
|
||
/** @class */ ((function (_super) {
|
||
__extends(PaginatorPlugin, _super);
|
||
function PaginatorPlugin(query, config) {
|
||
if (config === void 0) { config = {}; }
|
||
var _this = _super.call(this, query, {
|
||
resetFn: function () {
|
||
_this.initial = false;
|
||
_this.destroy({ clearCache: true, currentPage: 1 });
|
||
}
|
||
}) || this;
|
||
_this.query = query;
|
||
_this.config = config;
|
||
/** Save current filters, sorting, etc. in cache */
|
||
_this.metadata = new Map();
|
||
_this.pages = new Map();
|
||
_this.pagination = {
|
||
currentPage: 1,
|
||
perPage: 0,
|
||
total: 0,
|
||
lastPage: 0,
|
||
data: []
|
||
};
|
||
/**
|
||
* When the user navigates to a different page and return
|
||
* we don't want to call `clearCache` on first time.
|
||
*/
|
||
_this.initial = true;
|
||
/**
|
||
* Proxy to the query loading
|
||
*/
|
||
_this.isLoading$ = _this.query.selectLoading().pipe(delay(0));
|
||
_this.config = Object.assign(paginatorDefaults, config);
|
||
var _a = _this.config, startWith = _a.startWith, cacheTimeout = _a.cacheTimeout;
|
||
_this.page = new BehaviorSubject(startWith);
|
||
if (isObservable(cacheTimeout)) {
|
||
_this.clearCacheSubscription = cacheTimeout.subscribe(function () { return _this.clearCache(); });
|
||
}
|
||
return _this;
|
||
}
|
||
Object.defineProperty(PaginatorPlugin.prototype, "pageChanges", {
|
||
/**
|
||
* Listen to page changes
|
||
*/
|
||
get: function () {
|
||
return this.page.asObservable();
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
Object.defineProperty(PaginatorPlugin.prototype, "currentPage", {
|
||
/**
|
||
* Get the current page number
|
||
*/
|
||
get: function () {
|
||
return this.pagination.currentPage;
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
Object.defineProperty(PaginatorPlugin.prototype, "isFirst", {
|
||
/**
|
||
* Check if current page is the first one
|
||
*/
|
||
get: function () {
|
||
return this.currentPage === 1;
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
Object.defineProperty(PaginatorPlugin.prototype, "isLast", {
|
||
/**
|
||
* Check if current page is the last one
|
||
*/
|
||
get: function () {
|
||
return this.currentPage === this.pagination.lastPage;
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
/**
|
||
* Whether to generate an array of pages for *ngFor
|
||
* [1, 2, 3, 4]
|
||
*/
|
||
PaginatorPlugin.prototype.withControls = function () {
|
||
this.config.pagesControls = true;
|
||
return this;
|
||
};
|
||
/**
|
||
* Whether to generate the `from` and `to` keys
|
||
* [1, 2, 3, 4]
|
||
*/
|
||
PaginatorPlugin.prototype.withRange = function () {
|
||
this.config.range = true;
|
||
return this;
|
||
};
|
||
/**
|
||
* Set the loading state
|
||
*/
|
||
PaginatorPlugin.prototype.setLoading = function (value) {
|
||
if (value === void 0) { value = true; }
|
||
this.getStore().setLoading(value);
|
||
};
|
||
/**
|
||
* Update the pagination object and add the page
|
||
*/
|
||
PaginatorPlugin.prototype.update = function (response) {
|
||
this.pagination = response;
|
||
this.addPage(response.data);
|
||
};
|
||
/**
|
||
*
|
||
* Set the ids and add the page to store
|
||
*/
|
||
PaginatorPlugin.prototype.addPage = function (data) {
|
||
var _this = this;
|
||
this.pages.set(this.currentPage, { ids: data.map(function (entity) { return entity[_this.getStore().idKey]; }) });
|
||
this.getStore().upsertMany(data);
|
||
};
|
||
/**
|
||
* Clear the cache.
|
||
*/
|
||
PaginatorPlugin.prototype.clearCache = function (options) {
|
||
if (options === void 0) { options = {}; }
|
||
if (!this.initial) {
|
||
logAction('@Pagination - Clear Cache');
|
||
if (options.clearStore !== false && (this.config.clearStoreWithCache || options.clearStore)) {
|
||
this.getStore().remove();
|
||
}
|
||
this.pages = new Map();
|
||
this.metadata = new Map();
|
||
}
|
||
this.initial = false;
|
||
};
|
||
PaginatorPlugin.prototype.clearPage = function (page) {
|
||
this.pages.delete(page);
|
||
};
|
||
/**
|
||
* Clear the cache timeout and optionally the pages
|
||
*/
|
||
PaginatorPlugin.prototype.destroy = function (_a) {
|
||
var _b = _a === void 0 ? {} : _a, clearCache = _b.clearCache, currentPage = _b.currentPage;
|
||
if (this.clearCacheSubscription) {
|
||
this.clearCacheSubscription.unsubscribe();
|
||
}
|
||
if (clearCache) {
|
||
this.clearCache();
|
||
}
|
||
if (!isUndefined(currentPage)) {
|
||
this.setPage(currentPage);
|
||
}
|
||
this.initial = true;
|
||
};
|
||
/**
|
||
* Whether the provided page is active
|
||
*/
|
||
PaginatorPlugin.prototype.isPageActive = function (page) {
|
||
return this.currentPage === page;
|
||
};
|
||
/**
|
||
* Set the current page
|
||
*/
|
||
PaginatorPlugin.prototype.setPage = function (page) {
|
||
if (page !== this.currentPage || !this.hasPage(page)) {
|
||
this.page.next((this.pagination.currentPage = page));
|
||
}
|
||
};
|
||
/**
|
||
* Increment current page
|
||
*/
|
||
PaginatorPlugin.prototype.nextPage = function () {
|
||
if (this.currentPage !== this.pagination.lastPage) {
|
||
this.setPage(this.pagination.currentPage + 1);
|
||
}
|
||
};
|
||
/**
|
||
* Decrement current page
|
||
*/
|
||
PaginatorPlugin.prototype.prevPage = function () {
|
||
if (this.pagination.currentPage > 1) {
|
||
this.setPage(this.pagination.currentPage - 1);
|
||
}
|
||
};
|
||
/**
|
||
* Set current page to last
|
||
*/
|
||
PaginatorPlugin.prototype.setLastPage = function () {
|
||
this.setPage(this.pagination.lastPage);
|
||
};
|
||
/**
|
||
* Set current page to first
|
||
*/
|
||
PaginatorPlugin.prototype.setFirstPage = function () {
|
||
this.setPage(1);
|
||
};
|
||
/**
|
||
* Check if page exists in cache
|
||
*/
|
||
PaginatorPlugin.prototype.hasPage = function (page) {
|
||
return this.pages.has(page);
|
||
};
|
||
/**
|
||
* Get the current page if it's in cache, otherwise invoke the request
|
||
*/
|
||
PaginatorPlugin.prototype.getPage = function (req) {
|
||
var _this = this;
|
||
var page = this.pagination.currentPage;
|
||
if (this.hasPage(page)) {
|
||
return this.selectPage(page);
|
||
}
|
||
else {
|
||
this.setLoading(true);
|
||
return from(req()).pipe(switchMap(function (config) {
|
||
page = config.currentPage;
|
||
applyTransaction(function () {
|
||
_this.setLoading(false);
|
||
_this.update(config);
|
||
});
|
||
return _this.selectPage(page);
|
||
}));
|
||
}
|
||
};
|
||
PaginatorPlugin.prototype.getQuery = function () {
|
||
return this.query;
|
||
};
|
||
PaginatorPlugin.prototype.refreshCurrentPage = function () {
|
||
if (isNil(this.currentPage) === false) {
|
||
this.clearPage(this.currentPage);
|
||
this.setPage(this.currentPage);
|
||
}
|
||
};
|
||
PaginatorPlugin.prototype.getFrom = function () {
|
||
if (this.isFirst) {
|
||
return 1;
|
||
}
|
||
return (this.currentPage - 1) * this.pagination.perPage + 1;
|
||
};
|
||
PaginatorPlugin.prototype.getTo = function () {
|
||
if (this.isLast) {
|
||
return this.pagination.total;
|
||
}
|
||
return this.currentPage * this.pagination.perPage;
|
||
};
|
||
/**
|
||
* Select the page
|
||
*/
|
||
PaginatorPlugin.prototype.selectPage = function (page) {
|
||
var _this = this;
|
||
return this.query.selectAll({ asObject: true }).pipe(take(1), map(function (entities) {
|
||
var response = __assign({}, _this.pagination, { data: _this.pages.get(page).ids.map(function (id) { return entities[id]; }) });
|
||
var _a = _this.config, range = _a.range, pagesControls = _a.pagesControls;
|
||
/** If no total - calc it */
|
||
if (isNaN(_this.pagination.total)) {
|
||
if (response.lastPage === 1) {
|
||
response.total = response.data ? response.data.length : 0;
|
||
}
|
||
else {
|
||
response.total = response.perPage * response.lastPage;
|
||
}
|
||
_this.pagination.total = response.total;
|
||
}
|
||
if (range) {
|
||
response.from = _this.getFrom();
|
||
response.to = _this.getTo();
|
||
}
|
||
if (pagesControls) {
|
||
response.pageControls = generatePages(_this.pagination.total, _this.pagination.perPage);
|
||
}
|
||
return response;
|
||
}));
|
||
};
|
||
__decorate$2([
|
||
action('@Pagination - New Page'),
|
||
__metadata("design:type", Function),
|
||
__metadata("design:paramtypes", [Object]),
|
||
__metadata("design:returntype", void 0)
|
||
], PaginatorPlugin.prototype, "update", null);
|
||
return PaginatorPlugin;
|
||
})(AkitaPlugin));
|
||
/**
|
||
* Generate an array so we can ngFor them to navigate between pages
|
||
*/
|
||
function generatePages(total, perPage) {
|
||
var len = Math.ceil(total / perPage);
|
||
var arr = [];
|
||
for (var i = 0; i < len; i++) {
|
||
arr.push(i + 1);
|
||
}
|
||
return arr;
|
||
}
|
||
|
||
/** @class */ ((function (_super) {
|
||
__extends(PersistNgFormPlugin, _super);
|
||
function PersistNgFormPlugin(query, factoryFnOrPath, params) {
|
||
if (params === void 0) { params = {}; }
|
||
var _this = _super.call(this, query) || this;
|
||
_this.query = query;
|
||
_this.factoryFnOrPath = factoryFnOrPath;
|
||
_this.params = params;
|
||
_this.params = __assign({ debounceTime: 300, formKey: 'akitaForm', emitEvent: false, arrControlFactory: function (v) { return _this.builder.control(v); } }, params);
|
||
_this.isRootKeys = toBoolean(factoryFnOrPath) === false;
|
||
_this.isKeyBased = isString(factoryFnOrPath) || _this.isRootKeys;
|
||
return _this;
|
||
}
|
||
PersistNgFormPlugin.prototype.setForm = function (form, builder) {
|
||
this.form = form;
|
||
this.builder = builder;
|
||
this.activate();
|
||
return this;
|
||
};
|
||
PersistNgFormPlugin.prototype.reset = function (initialState) {
|
||
var _this = this;
|
||
var _a;
|
||
var value;
|
||
if (initialState) {
|
||
value = initialState;
|
||
}
|
||
else {
|
||
value = this.isKeyBased ? this.initialValue : this.factoryFnOrPath();
|
||
}
|
||
if (this.isKeyBased) {
|
||
Object.keys(this.initialValue).forEach(function (stateKey) {
|
||
var value = _this.initialValue[stateKey];
|
||
if (Array.isArray(value) && _this.builder) {
|
||
var formArray = _this.form.controls[stateKey];
|
||
_this.cleanArray(formArray);
|
||
value.forEach(function (v, i) {
|
||
_this.form.get(stateKey).insert(i, _this.params.arrControlFactory(v));
|
||
});
|
||
}
|
||
});
|
||
}
|
||
this.form.patchValue(value, { emitEvent: this.params.emitEvent });
|
||
var storeValue = this.isKeyBased ? setValue(this.getQuery().getValue(), this.getStore().storeName + "." + this.factoryFnOrPath, value) : (_a = {}, _a[this.params.formKey] = value, _a);
|
||
this.updateStore(storeValue);
|
||
};
|
||
PersistNgFormPlugin.prototype.cleanArray = function (control) {
|
||
while (control.length !== 0) {
|
||
control.removeAt(0);
|
||
}
|
||
};
|
||
PersistNgFormPlugin.prototype.resolveInitialValue = function (formValue, root) {
|
||
var _this = this;
|
||
if (!formValue)
|
||
return;
|
||
return Object.keys(formValue).reduce(function (acc, stateKey) {
|
||
var value = root[stateKey];
|
||
if (Array.isArray(value) && _this.builder) {
|
||
var factory_1 = _this.params.arrControlFactory;
|
||
_this.cleanArray(_this.form.get(stateKey));
|
||
value.forEach(function (v, i) {
|
||
_this.form.get(stateKey).insert(i, factory_1(v));
|
||
});
|
||
}
|
||
acc[stateKey] = root[stateKey];
|
||
return acc;
|
||
}, {});
|
||
};
|
||
PersistNgFormPlugin.prototype.activate = function () {
|
||
var _this = this;
|
||
var _a;
|
||
var path;
|
||
if (this.isKeyBased) {
|
||
if (this.isRootKeys) {
|
||
this.initialValue = this.resolveInitialValue(this.form.value, this.getQuery().getValue());
|
||
this.form.patchValue(this.initialValue, { emitEvent: this.params.emitEvent });
|
||
}
|
||
else {
|
||
path = this.getStore().storeName + "." + this.factoryFnOrPath;
|
||
var root = getValue(this.getQuery().getValue(), path);
|
||
this.initialValue = this.resolveInitialValue(root, root);
|
||
this.form.patchValue(this.initialValue, { emitEvent: this.params.emitEvent });
|
||
}
|
||
}
|
||
else {
|
||
if (!this.getQuery().getValue()[this.params.formKey]) {
|
||
logAction('@PersistNgFormPlugin activate');
|
||
this.updateStore((_a = {}, _a[this.params.formKey] = this.factoryFnOrPath(), _a));
|
||
}
|
||
var value = this.getQuery().getValue()[this.params.formKey];
|
||
this.form.patchValue(value);
|
||
}
|
||
this.formChanges = this.form.valueChanges.pipe(debounceTime(this.params.debounceTime)).subscribe(function (value) {
|
||
logAction('@PersistForm - Update');
|
||
var newState;
|
||
if (_this.isKeyBased) {
|
||
if (_this.isRootKeys) {
|
||
newState = function (state) { return (__assign({}, state, value)); };
|
||
}
|
||
else {
|
||
newState = function (state) { return setValue(state, path, value); };
|
||
}
|
||
}
|
||
else {
|
||
newState = function () {
|
||
var _a;
|
||
return (_a = {}, _a[_this.params.formKey] = value, _a);
|
||
};
|
||
}
|
||
_this.updateStore(newState(_this.getQuery().getValue()));
|
||
});
|
||
};
|
||
PersistNgFormPlugin.prototype.destroy = function () {
|
||
this.formChanges && this.formChanges.unsubscribe();
|
||
this.form = null;
|
||
this.builder = null;
|
||
};
|
||
return PersistNgFormPlugin;
|
||
})(AkitaPlugin));
|
||
|
||
/**
|
||
* Each plugin that wants to add support for entities should extend this interface.
|
||
*/
|
||
var EntityCollectionPlugin = /** @class */ (function () {
|
||
function EntityCollectionPlugin(query, entityIds) {
|
||
this.query = query;
|
||
this.entityIds = entityIds;
|
||
this.entities = new Map();
|
||
}
|
||
/**
|
||
* Get the entity plugin instance.
|
||
*/
|
||
EntityCollectionPlugin.prototype.getEntity = function (id) {
|
||
return this.entities.get(id);
|
||
};
|
||
/**
|
||
* Whether the entity plugin exist.
|
||
*/
|
||
EntityCollectionPlugin.prototype.hasEntity = function (id) {
|
||
return this.entities.has(id);
|
||
};
|
||
/**
|
||
* Remove the entity plugin instance.
|
||
*/
|
||
EntityCollectionPlugin.prototype.removeEntity = function (id) {
|
||
this.destroy(id);
|
||
return this.entities.delete(id);
|
||
};
|
||
/**
|
||
* Set the entity plugin instance.
|
||
*/
|
||
EntityCollectionPlugin.prototype.createEntity = function (id, plugin) {
|
||
return this.entities.set(id, plugin);
|
||
};
|
||
/**
|
||
* If the user passes `entityIds` we take them; otherwise, we take all.
|
||
*/
|
||
EntityCollectionPlugin.prototype.getIds = function () {
|
||
return isUndefined(this.entityIds) ? this.query.getValue().ids : coerceArray(this.entityIds);
|
||
};
|
||
/**
|
||
* When you call one of the plugin methods, you can pass id/ids or undefined which means all.
|
||
*/
|
||
EntityCollectionPlugin.prototype.resolvedIds = function (ids) {
|
||
return isUndefined(ids) ? this.getIds() : coerceArray(ids);
|
||
};
|
||
/**
|
||
* Call this method when you want to activate the plugin on init or when you need to listen to add/remove of entities dynamically.
|
||
*
|
||
* For example in your plugin you may do the following:
|
||
*
|
||
* this.query.select(state => state.ids).pipe(skip(1)).subscribe(ids => this.activate(ids));
|
||
*/
|
||
EntityCollectionPlugin.prototype.rebase = function (ids, actions) {
|
||
var _this = this;
|
||
if (actions === void 0) { actions = {}; }
|
||
/**
|
||
*
|
||
* If the user passes `entityIds` & we have new ids check if we need to add/remove instances.
|
||
*
|
||
* This phase will be called only upon update.
|
||
*/
|
||
if (toBoolean(ids)) {
|
||
/**
|
||
* Which means all
|
||
*/
|
||
if (isUndefined(this.entityIds)) {
|
||
for (var i = 0, len = ids.length; i < len; i++) {
|
||
var entityId = ids[i];
|
||
if (this.hasEntity(entityId) === false) {
|
||
isFunction(actions.beforeAdd) && actions.beforeAdd(entityId);
|
||
var plugin = this.instantiatePlugin(entityId);
|
||
this.entities.set(entityId, plugin);
|
||
isFunction(actions.afterAdd) && actions.afterAdd(plugin);
|
||
}
|
||
}
|
||
this.entities.forEach(function (plugin, entityId) {
|
||
if (ids.indexOf(entityId) === -1) {
|
||
isFunction(actions.beforeRemove) && actions.beforeRemove(plugin);
|
||
_this.removeEntity(entityId);
|
||
}
|
||
});
|
||
}
|
||
else {
|
||
/**
|
||
* Which means the user passes specific ids
|
||
*/
|
||
var _ids = coerceArray(this.entityIds);
|
||
for (var i = 0, len = _ids.length; i < len; i++) {
|
||
var entityId = _ids[i];
|
||
/** The Entity in current ids and doesn't exist, add it. */
|
||
if (ids.indexOf(entityId) > -1 && this.hasEntity(entityId) === false) {
|
||
isFunction(actions.beforeAdd) && actions.beforeAdd(entityId);
|
||
var plugin = this.instantiatePlugin(entityId);
|
||
this.entities.set(entityId, plugin);
|
||
isFunction(actions.afterAdd) && actions.afterAdd(plugin);
|
||
}
|
||
else {
|
||
this.entities.forEach(function (plugin, entityId) {
|
||
/** The Entity not in current ids and exists, remove it. */
|
||
if (ids.indexOf(entityId) === -1 && _this.hasEntity(entityId) === true) {
|
||
isFunction(actions.beforeRemove) && actions.beforeRemove(plugin);
|
||
_this.removeEntity(entityId);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
/**
|
||
* Otherwise, start with the provided ids or all.
|
||
*/
|
||
this.getIds().forEach(function (id) {
|
||
if (!_this.hasEntity(id))
|
||
_this.createEntity(id, _this.instantiatePlugin(id));
|
||
});
|
||
}
|
||
};
|
||
/**
|
||
* Listen for add/remove entities.
|
||
*/
|
||
EntityCollectionPlugin.prototype.selectIds = function () {
|
||
return this.query.select(function (state) { return state.ids; });
|
||
};
|
||
/**
|
||
* Base method for activation, you can override it if you need to.
|
||
*/
|
||
EntityCollectionPlugin.prototype.activate = function (ids) {
|
||
this.rebase(ids);
|
||
};
|
||
/**
|
||
* Loop over each id and invoke the plugin method.
|
||
*/
|
||
EntityCollectionPlugin.prototype.forEachId = function (ids, cb) {
|
||
var _ids = this.resolvedIds(ids);
|
||
for (var i = 0, len = _ids.length; i < len; i++) {
|
||
var id = _ids[i];
|
||
if (this.hasEntity(id)) {
|
||
cb(this.getEntity(id));
|
||
}
|
||
}
|
||
};
|
||
return EntityCollectionPlugin;
|
||
}());
|
||
|
||
var StateHistoryPlugin = /** @class */ (function (_super) {
|
||
__extends(StateHistoryPlugin, _super);
|
||
function StateHistoryPlugin(query, params, _entityId) {
|
||
if (params === void 0) { params = {}; }
|
||
var _this = _super.call(this, query, {
|
||
resetFn: function () { return _this.clear(); }
|
||
}) || this;
|
||
_this.query = query;
|
||
_this.params = params;
|
||
_this._entityId = _entityId;
|
||
/** Allow skipping an update from outside */
|
||
_this.skip = false;
|
||
_this.history = {
|
||
past: [],
|
||
present: null,
|
||
future: []
|
||
};
|
||
/** Skip the update when redo/undo */
|
||
_this.skipUpdate = false;
|
||
params.maxAge = !!params.maxAge ? params.maxAge : 10;
|
||
params.comparator = params.comparator || (function () { return true; });
|
||
_this.activate();
|
||
return _this;
|
||
}
|
||
Object.defineProperty(StateHistoryPlugin.prototype, "hasPast$", {
|
||
/**
|
||
* Observable stream representing whether the history plugin has an available past
|
||
*
|
||
*/
|
||
get: function () {
|
||
return this._hasPast$;
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
Object.defineProperty(StateHistoryPlugin.prototype, "hasFuture$", {
|
||
/**
|
||
* Observable stream representing whether the history plugin has an available future
|
||
*
|
||
*/
|
||
get: function () {
|
||
return this._hasFuture$;
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
Object.defineProperty(StateHistoryPlugin.prototype, "hasPast", {
|
||
get: function () {
|
||
return this.history.past.length > 0;
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
Object.defineProperty(StateHistoryPlugin.prototype, "hasFuture", {
|
||
get: function () {
|
||
return this.history.future.length > 0;
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
Object.defineProperty(StateHistoryPlugin.prototype, "property", {
|
||
get: function () {
|
||
return this.params.watchProperty;
|
||
},
|
||
enumerable: true,
|
||
configurable: true
|
||
});
|
||
/* Updates the hasPast$ hasFuture$ observables*/
|
||
StateHistoryPlugin.prototype.updateHasHistory = function () {
|
||
this.hasFutureSubject.next(this.hasFuture);
|
||
this.hasPastSubject.next(this.hasPast);
|
||
};
|
||
StateHistoryPlugin.prototype.activate = function () {
|
||
var _this = this;
|
||
this.hasPastSubject = new BehaviorSubject(false);
|
||
this._hasPast$ = this.hasPastSubject.asObservable().pipe(distinctUntilChanged());
|
||
this.hasFutureSubject = new BehaviorSubject(false);
|
||
this._hasFuture$ = this.hasFutureSubject.asObservable().pipe(distinctUntilChanged());
|
||
this.history.present = this.getSource(this._entityId, this.property);
|
||
this.subscription = this
|
||
.selectSource(this._entityId, this.property)
|
||
.pipe(pairwise())
|
||
.subscribe(function (_a) {
|
||
var _b = __read(_a, 2), past = _b[0], present = _b[1];
|
||
if (_this.skip) {
|
||
_this.skip = false;
|
||
return;
|
||
}
|
||
/**
|
||
* comparator: (prev, current) => isEqual(prev, current) === false
|
||
*/
|
||
var shouldUpdate = _this.params.comparator(past, present);
|
||
if (!_this.skipUpdate && shouldUpdate) {
|
||
if (_this.history.past.length === _this.params.maxAge) {
|
||
_this.history.past = _this.history.past.slice(1);
|
||
}
|
||
_this.history.past = __spread(_this.history.past, [past]);
|
||
_this.history.present = present;
|
||
_this.updateHasHistory();
|
||
}
|
||
});
|
||
};
|
||
StateHistoryPlugin.prototype.undo = function () {
|
||
if (this.history.past.length > 0) {
|
||
var _a = this.history, past = _a.past, present = _a.present;
|
||
var previous = past[past.length - 1];
|
||
this.history.past = past.slice(0, past.length - 1);
|
||
this.history.present = previous;
|
||
this.history.future = __spread([present], this.history.future);
|
||
this.update();
|
||
}
|
||
};
|
||
StateHistoryPlugin.prototype.redo = function () {
|
||
if (this.history.future.length > 0) {
|
||
var _a = this.history, past = _a.past, present = _a.present;
|
||
var next = this.history.future[0];
|
||
var newFuture = this.history.future.slice(1);
|
||
this.history.past = __spread(past, [present]);
|
||
this.history.present = next;
|
||
this.history.future = newFuture;
|
||
this.update('Redo');
|
||
}
|
||
};
|
||
StateHistoryPlugin.prototype.jumpToPast = function (index) {
|
||
if (index < 0 || index >= this.history.past.length)
|
||
return;
|
||
var _a = this.history, past = _a.past, future = _a.future, present = _a.present;
|
||
/**
|
||
*
|
||
* const past = [1, 2, 3, 4, 5];
|
||
* const present = 6;
|
||
* const future = [7, 8, 9];
|
||
* const index = 2;
|
||
*
|
||
* newPast = past.slice(0, index) = [1, 2];
|
||
* newPresent = past[index] = 3;
|
||
* newFuture = [...past.slice(index + 1),present, ...future] = [4, 5, 6, 7, 8, 9];
|
||
*
|
||
*/
|
||
var newPast = past.slice(0, index);
|
||
var newFuture = __spread(past.slice(index + 1), [present], future);
|
||
var newPresent = past[index];
|
||
this.history.past = newPast;
|
||
this.history.present = newPresent;
|
||
this.history.future = newFuture;
|
||
this.update();
|
||
};
|
||
StateHistoryPlugin.prototype.jumpToFuture = function (index) {
|
||
if (index < 0 || index >= this.history.future.length)
|
||
return;
|
||
var _a = this.history, past = _a.past, future = _a.future, present = _a.present;
|
||
/**
|
||
*
|
||
* const past = [1, 2, 3, 4, 5];
|
||
* const present = 6;
|
||
* const future = [7, 8, 9, 10]
|
||
* const index = 1
|
||
*
|
||
* newPast = [...past, present, ...future.slice(0, index) = [1, 2, 3, 4, 5, 6, 7];
|
||
* newPresent = future[index] = 8;
|
||
* newFuture = futrue.slice(index+1) = [9, 10];
|
||
*
|
||
*/
|
||
var newPast = __spread(past, [present], future.slice(0, index));
|
||
var newPresent = future[index];
|
||
var newFuture = future.slice(index + 1);
|
||
this.history.past = newPast;
|
||
this.history.present = newPresent;
|
||
this.history.future = newFuture;
|
||
this.update('Redo');
|
||
};
|
||
/**
|
||
*
|
||
* jump n steps in the past or forward
|
||
*
|
||
*/
|
||
StateHistoryPlugin.prototype.jump = function (n) {
|
||
if (n > 0)
|
||
return this.jumpToFuture(n - 1);
|
||
if (n < 0)
|
||
return this.jumpToPast(this.history.past.length + n);
|
||
};
|
||
/**
|
||
* Clear the history
|
||
*
|
||
* @param customUpdateFn Callback function for only clearing part of the history
|
||
*
|
||
* @example
|
||
*
|
||
* stateHistory.clear((history) => {
|
||
* return {
|
||
* past: history.past,
|
||
* present: history.present,
|
||
* future: []
|
||
* };
|
||
* });
|
||
*/
|
||
StateHistoryPlugin.prototype.clear = function (customUpdateFn) {
|
||
this.history = isFunction(customUpdateFn)
|
||
? customUpdateFn(this.history)
|
||
: {
|
||
past: [],
|
||
present: null,
|
||
future: []
|
||
};
|
||
this.updateHasHistory();
|
||
};
|
||
StateHistoryPlugin.prototype.destroy = function (clearHistory) {
|
||
if (clearHistory === void 0) { clearHistory = false; }
|
||
if (clearHistory) {
|
||
this.clear();
|
||
}
|
||
this.subscription.unsubscribe();
|
||
};
|
||
StateHistoryPlugin.prototype.ignoreNext = function () {
|
||
this.skip = true;
|
||
};
|
||
StateHistoryPlugin.prototype.update = function (action) {
|
||
if (action === void 0) { action = 'Undo'; }
|
||
this.skipUpdate = true;
|
||
logAction("@StateHistory - " + action);
|
||
this.updateStore(this.history.present, this._entityId, this.property);
|
||
this.updateHasHistory();
|
||
this.skipUpdate = false;
|
||
};
|
||
return StateHistoryPlugin;
|
||
}(AkitaPlugin));
|
||
|
||
/** @class */ ((function (_super) {
|
||
__extends(EntityStateHistoryPlugin, _super);
|
||
function EntityStateHistoryPlugin(query, params) {
|
||
if (params === void 0) { params = {}; }
|
||
var _this = _super.call(this, query, params.entityIds) || this;
|
||
_this.query = query;
|
||
_this.params = params;
|
||
params.maxAge = toBoolean(params.maxAge) ? params.maxAge : 10;
|
||
_this.activate();
|
||
_this.selectIds()
|
||
.pipe(skip(1))
|
||
.subscribe(function (ids) { return _this.activate(ids); });
|
||
return _this;
|
||
}
|
||
EntityStateHistoryPlugin.prototype.redo = function (ids) {
|
||
this.forEachId(ids, function (e) { return e.redo(); });
|
||
};
|
||
EntityStateHistoryPlugin.prototype.undo = function (ids) {
|
||
this.forEachId(ids, function (e) { return e.undo(); });
|
||
};
|
||
EntityStateHistoryPlugin.prototype.hasPast = function (id) {
|
||
if (this.hasEntity(id)) {
|
||
return this.getEntity(id).hasPast;
|
||
}
|
||
};
|
||
EntityStateHistoryPlugin.prototype.hasFuture = function (id) {
|
||
if (this.hasEntity(id)) {
|
||
return this.getEntity(id).hasFuture;
|
||
}
|
||
};
|
||
EntityStateHistoryPlugin.prototype.jumpToFuture = function (ids, index) {
|
||
this.forEachId(ids, function (e) { return e.jumpToFuture(index); });
|
||
};
|
||
EntityStateHistoryPlugin.prototype.jumpToPast = function (ids, index) {
|
||
this.forEachId(ids, function (e) { return e.jumpToPast(index); });
|
||
};
|
||
EntityStateHistoryPlugin.prototype.clear = function (ids) {
|
||
this.forEachId(ids, function (e) { return e.clear(); });
|
||
};
|
||
EntityStateHistoryPlugin.prototype.destroy = function (ids, clearHistory) {
|
||
if (clearHistory === void 0) { clearHistory = false; }
|
||
this.forEachId(ids, function (e) { return e.destroy(clearHistory); });
|
||
};
|
||
EntityStateHistoryPlugin.prototype.ignoreNext = function (ids) {
|
||
this.forEachId(ids, function (e) { return e.ignoreNext(); });
|
||
};
|
||
EntityStateHistoryPlugin.prototype.instantiatePlugin = function (id) {
|
||
return new StateHistoryPlugin(this.query, this.params, id);
|
||
};
|
||
return EntityStateHistoryPlugin;
|
||
})(EntityCollectionPlugin));
|
||
|
||
var ɵ0 = function (head, current) { return JSON.stringify(head) !== JSON.stringify(current); };
|
||
var dirtyCheckDefaultParams = {
|
||
comparator: ɵ0
|
||
};
|
||
function getNestedPath(nestedObj, path) {
|
||
var pathAsArray = path.split('.');
|
||
return pathAsArray.reduce(function (obj, key) { return (obj && obj[key] !== 'undefined' ? obj[key] : undefined); }, nestedObj);
|
||
}
|
||
var DirtyCheckPlugin = /** @class */ (function (_super) {
|
||
__extends(DirtyCheckPlugin, _super);
|
||
function DirtyCheckPlugin(query, params, _entityId) {
|
||
var _this = _super.call(this, query) || this;
|
||
_this.query = query;
|
||
_this.params = params;
|
||
_this._entityId = _entityId;
|
||
_this.dirty = new BehaviorSubject(false);
|
||
_this.active = false;
|
||
_this._reset = new Subject();
|
||
_this.isDirty$ = _this.dirty.asObservable().pipe(distinctUntilChanged());
|
||
_this.reset$ = _this._reset.asObservable();
|
||
_this.params = __assign({}, dirtyCheckDefaultParams, params);
|
||
if (_this.params.watchProperty) {
|
||
var watchProp = coerceArray(_this.params.watchProperty);
|
||
if (query instanceof QueryEntity && watchProp.includes('entities') && !watchProp.includes('ids')) {
|
||
watchProp.push('ids');
|
||
}
|
||
_this.params.watchProperty = watchProp;
|
||
}
|
||
return _this;
|
||
}
|
||
DirtyCheckPlugin.prototype.reset = function (params) {
|
||
if (params === void 0) { params = {}; }
|
||
var currentValue = this.head;
|
||
if (isFunction(params.updateFn)) {
|
||
if (this.isEntityBased(this._entityId)) {
|
||
currentValue = params.updateFn(this.head, this.getQuery().getEntity(this._entityId));
|
||
}
|
||
else {
|
||
currentValue = params.updateFn(this.head, this.getQuery().getValue());
|
||
}
|
||
}
|
||
logAction("@DirtyCheck - Revert");
|
||
this.updateStore(currentValue, this._entityId);
|
||
this._reset.next();
|
||
};
|
||
DirtyCheckPlugin.prototype.setHead = function () {
|
||
if (!this.active) {
|
||
this.activate();
|
||
this.active = true;
|
||
}
|
||
else {
|
||
this.head = this._getHead();
|
||
}
|
||
this.updateDirtiness(false);
|
||
return this;
|
||
};
|
||
DirtyCheckPlugin.prototype.isDirty = function () {
|
||
return !!this.dirty.value;
|
||
};
|
||
DirtyCheckPlugin.prototype.hasHead = function () {
|
||
return !!this.getHead();
|
||
};
|
||
DirtyCheckPlugin.prototype.destroy = function () {
|
||
this.head = null;
|
||
this.subscription && this.subscription.unsubscribe();
|
||
this._reset && this._reset.complete();
|
||
};
|
||
DirtyCheckPlugin.prototype.isPathDirty = function (path) {
|
||
var head = this.getHead();
|
||
var current = this.getQuery().getValue();
|
||
var currentPathValue = getNestedPath(current, path);
|
||
var headPathValue = getNestedPath(head, path);
|
||
return this.params.comparator(currentPathValue, headPathValue);
|
||
};
|
||
DirtyCheckPlugin.prototype.getHead = function () {
|
||
return this.head;
|
||
};
|
||
DirtyCheckPlugin.prototype.activate = function () {
|
||
var _this = this;
|
||
this.head = this._getHead();
|
||
/** if we are tracking specific properties select only the relevant ones */
|
||
var source = this.params.watchProperty
|
||
? this.params.watchProperty.map(function (prop) {
|
||
return _this.query
|
||
.select(function (state) { return state[prop]; })
|
||
.pipe(map(function (val) { return ({
|
||
val: val,
|
||
__akitaKey: prop
|
||
}); }));
|
||
})
|
||
: [this.selectSource(this._entityId)];
|
||
this.subscription = combineLatest.apply(void 0, __spread(source)).pipe(skip(1))
|
||
.subscribe(function (currentState) {
|
||
if (isUndefined(_this.head))
|
||
return;
|
||
/** __akitaKey is used to determine if we are tracking a specific property or a store change */
|
||
var isChange = currentState.some(function (state) {
|
||
var head = state.__akitaKey ? _this.head[state.__akitaKey] : _this.head;
|
||
var compareTo = state.__akitaKey ? state.val : state;
|
||
return _this.params.comparator(head, compareTo);
|
||
});
|
||
_this.updateDirtiness(isChange);
|
||
});
|
||
};
|
||
DirtyCheckPlugin.prototype.updateDirtiness = function (isDirty) {
|
||
this.dirty.next(isDirty);
|
||
};
|
||
DirtyCheckPlugin.prototype._getHead = function () {
|
||
var head = this.getSource(this._entityId);
|
||
if (this.params.watchProperty) {
|
||
head = this.getWatchedValues(head);
|
||
}
|
||
return head;
|
||
};
|
||
DirtyCheckPlugin.prototype.getWatchedValues = function (source) {
|
||
return this.params.watchProperty.reduce(function (watched, prop) {
|
||
watched[prop] = source[prop];
|
||
return watched;
|
||
}, {});
|
||
};
|
||
return DirtyCheckPlugin;
|
||
}(AkitaPlugin));
|
||
|
||
/** @class */ ((function (_super) {
|
||
__extends(EntityDirtyCheckPlugin, _super);
|
||
function EntityDirtyCheckPlugin(query, params) {
|
||
if (params === void 0) { params = {}; }
|
||
var _this = _super.call(this, query, params.entityIds) || this;
|
||
_this.query = query;
|
||
_this.params = params;
|
||
_this._someDirty = new Subject();
|
||
_this.someDirty$ = merge(_this.query.select(function (state) { return state.entities; }), _this._someDirty.asObservable()).pipe(auditTime(0), map(function () { return _this.checkSomeDirty(); }));
|
||
_this.params = __assign({}, dirtyCheckDefaultParams, params);
|
||
// TODO lazy activate?
|
||
_this.activate();
|
||
_this.selectIds()
|
||
.pipe(skip(1))
|
||
.subscribe(function (ids) {
|
||
_super.prototype.rebase.call(_this, ids, { afterAdd: function (plugin) { return plugin.setHead(); } });
|
||
});
|
||
return _this;
|
||
}
|
||
EntityDirtyCheckPlugin.prototype.setHead = function (ids) {
|
||
if (this.params.entityIds && ids) {
|
||
var toArray_1 = coerceArray(ids);
|
||
var someAreWatched = coerceArray(this.params.entityIds).some(function (id) { return toArray_1.indexOf(id) > -1; });
|
||
if (someAreWatched === false) {
|
||
return this;
|
||
}
|
||
}
|
||
this.forEachId(ids, function (e) { return e.setHead(); });
|
||
this._someDirty.next();
|
||
return this;
|
||
};
|
||
EntityDirtyCheckPlugin.prototype.hasHead = function (id) {
|
||
if (this.entities.has(id)) {
|
||
var entity = this.getEntity(id);
|
||
return entity.hasHead();
|
||
}
|
||
return false;
|
||
};
|
||
EntityDirtyCheckPlugin.prototype.reset = function (ids, params) {
|
||
if (params === void 0) { params = {}; }
|
||
this.forEachId(ids, function (e) { return e.reset(params); });
|
||
};
|
||
EntityDirtyCheckPlugin.prototype.isDirty = function (id, asObservable) {
|
||
if (asObservable === void 0) { asObservable = true; }
|
||
if (this.entities.has(id)) {
|
||
var entity = this.getEntity(id);
|
||
return asObservable ? entity.isDirty$ : entity.isDirty();
|
||
}
|
||
return false;
|
||
};
|
||
EntityDirtyCheckPlugin.prototype.someDirty = function () {
|
||
return this.checkSomeDirty();
|
||
};
|
||
EntityDirtyCheckPlugin.prototype.isPathDirty = function (id, path) {
|
||
if (this.entities.has(id)) {
|
||
var head = this.getEntity(id).getHead();
|
||
var current = this.query.getEntity(id);
|
||
var currentPathValue = getNestedPath(current, path);
|
||
var headPathValue = getNestedPath(head, path);
|
||
return this.params.comparator(currentPathValue, headPathValue);
|
||
}
|
||
return null;
|
||
};
|
||
EntityDirtyCheckPlugin.prototype.destroy = function (ids) {
|
||
this.forEachId(ids, function (e) { return e.destroy(); });
|
||
/** complete only when the plugin destroys */
|
||
if (!ids) {
|
||
this._someDirty.complete();
|
||
}
|
||
};
|
||
EntityDirtyCheckPlugin.prototype.instantiatePlugin = function (id) {
|
||
return new DirtyCheckPlugin(this.query, this.params, id);
|
||
};
|
||
EntityDirtyCheckPlugin.prototype.checkSomeDirty = function () {
|
||
var e_1, _a;
|
||
var entitiesIds = this.resolvedIds();
|
||
try {
|
||
for (var entitiesIds_1 = __values(entitiesIds), entitiesIds_1_1 = entitiesIds_1.next(); !entitiesIds_1_1.done; entitiesIds_1_1 = entitiesIds_1.next()) {
|
||
var id = entitiesIds_1_1.value;
|
||
if (this.getEntity(id).isDirty()) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
||
finally {
|
||
try {
|
||
if (entitiesIds_1_1 && !entitiesIds_1_1.done && (_a = entitiesIds_1.return)) _a.call(entitiesIds_1);
|
||
}
|
||
finally { if (e_1) throw e_1.error; }
|
||
}
|
||
return false;
|
||
};
|
||
return EntityDirtyCheckPlugin;
|
||
})(EntityCollectionPlugin));
|
||
|
||
/**
|
||
* Generate random guid
|
||
*
|
||
* @example
|
||
*
|
||
* {
|
||
* id: guid()
|
||
* }
|
||
*
|
||
* @remarks this isn't a GUID, but a 10 char random alpha-num
|
||
*/
|
||
function guid() {
|
||
return Math.random()
|
||
.toString(36)
|
||
.slice(2);
|
||
}
|
||
|
||
var _a, _b;
|
||
var StoreAction;
|
||
(function (StoreAction) {
|
||
StoreAction["Update"] = "UPDATE";
|
||
})(StoreAction || (StoreAction = {}));
|
||
(_a = {},
|
||
_a[StoreAction.Update] = 'update',
|
||
_a);
|
||
var EntityStoreAction;
|
||
(function (EntityStoreAction) {
|
||
EntityStoreAction["Update"] = "UPDATE";
|
||
EntityStoreAction["AddEntities"] = "ADD_ENTITIES";
|
||
EntityStoreAction["SetEntities"] = "SET_ENTITIES";
|
||
EntityStoreAction["UpdateEntities"] = "UPDATE_ENTITIES";
|
||
EntityStoreAction["RemoveEntities"] = "REMOVE_ENTITIES";
|
||
EntityStoreAction["UpsertEntities"] = "UPSERT_ENTITIES";
|
||
EntityStoreAction["UpsertManyEntities"] = "UPSERT_MANY_ENTITIES";
|
||
})(EntityStoreAction || (EntityStoreAction = {}));
|
||
(_b = {},
|
||
_b[EntityStoreAction.Update] = 'update',
|
||
_b[EntityStoreAction.AddEntities] = 'add',
|
||
_b[EntityStoreAction.SetEntities] = 'set',
|
||
_b[EntityStoreAction.UpdateEntities] = 'update',
|
||
_b[EntityStoreAction.RemoveEntities] = 'remove',
|
||
_b[EntityStoreAction.UpsertEntities] = 'upsert',
|
||
_b[EntityStoreAction.UpsertManyEntities] = 'upsertMany',
|
||
_b);
|
||
|
||
function combineQueries(observables) {
|
||
return combineLatest(observables).pipe(auditTime(0));
|
||
}
|
||
|
||
var immer_cjs_development = {};
|
||
|
||
Object.defineProperty(immer_cjs_development, '__esModule', { value: true });
|
||
|
||
var _ref;
|
||
|
||
// Should be no imports here!
|
||
// Some things that should be evaluated before all else...
|
||
// We only want to know if non-polyfilled symbols are available
|
||
var hasSymbol = typeof Symbol !== "undefined" && typeof
|
||
/*#__PURE__*/
|
||
Symbol("x") === "symbol";
|
||
var hasMap = typeof Map !== "undefined";
|
||
var hasSet = typeof Set !== "undefined";
|
||
var hasProxies = typeof Proxy !== "undefined" && typeof Proxy.revocable !== "undefined" && typeof Reflect !== "undefined";
|
||
/**
|
||
* The sentinel value returned by producers to replace the draft with undefined.
|
||
*/
|
||
|
||
var NOTHING = hasSymbol ?
|
||
/*#__PURE__*/
|
||
Symbol.for("immer-nothing") : (_ref = {}, _ref["immer-nothing"] = true, _ref);
|
||
/**
|
||
* To let Immer treat your class instances as plain immutable objects
|
||
* (albeit with a custom prototype), you must define either an instance property
|
||
* or a static property on each of your custom classes.
|
||
*
|
||
* Otherwise, your class instance will never be drafted, which means it won't be
|
||
* safe to mutate in a produce callback.
|
||
*/
|
||
|
||
var DRAFTABLE = hasSymbol ?
|
||
/*#__PURE__*/
|
||
Symbol.for("immer-draftable") : "__$immer_draftable";
|
||
var DRAFT_STATE = hasSymbol ?
|
||
/*#__PURE__*/
|
||
Symbol.for("immer-state") : "__$immer_state"; // Even a polyfilled Symbol might provide Symbol.iterator
|
||
|
||
var iteratorSymbol = typeof Symbol != "undefined" && Symbol.iterator || "@@iterator";
|
||
|
||
var errors = {
|
||
0: "Illegal state",
|
||
1: "Immer drafts cannot have computed properties",
|
||
2: "This object has been frozen and should not be mutated",
|
||
3: function _(data) {
|
||
return "Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? " + data;
|
||
},
|
||
4: "An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft.",
|
||
5: "Immer forbids circular references",
|
||
6: "The first or second argument to `produce` must be a function",
|
||
7: "The third argument to `produce` must be a function or undefined",
|
||
8: "First argument to `createDraft` must be a plain object, an array, or an immerable object",
|
||
9: "First argument to `finishDraft` must be a draft returned by `createDraft`",
|
||
10: "The given draft is already finalized",
|
||
11: "Object.defineProperty() cannot be used on an Immer draft",
|
||
12: "Object.setPrototypeOf() cannot be used on an Immer draft",
|
||
13: "Immer only supports deleting array indices",
|
||
14: "Immer only supports setting array indices and the 'length' property",
|
||
15: function _(path) {
|
||
return "Cannot apply patch, path doesn't resolve: " + path;
|
||
},
|
||
16: 'Sets cannot have "replace" patches.',
|
||
17: function _(op) {
|
||
return "Unsupported patch operation: " + op;
|
||
},
|
||
18: function _(plugin) {
|
||
return "The plugin for '" + plugin + "' has not been loaded into Immer. To enable the plugin, import and call `enable" + plugin + "()` when initializing your application.";
|
||
},
|
||
20: "Cannot use proxies if Proxy, Proxy.revocable or Reflect are not available",
|
||
21: function _(thing) {
|
||
return "produce can only be called on things that are draftable: plain objects, arrays, Map, Set or classes that are marked with '[immerable]: true'. Got '" + thing + "'";
|
||
},
|
||
22: function _(thing) {
|
||
return "'current' expects a draft, got: " + thing;
|
||
},
|
||
23: function _(thing) {
|
||
return "'original' expects a draft, got: " + thing;
|
||
}
|
||
};
|
||
function die(error) {
|
||
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
||
args[_key - 1] = arguments[_key];
|
||
}
|
||
|
||
{
|
||
var e = errors[error];
|
||
var msg = !e ? "unknown error nr: " + error : typeof e === "function" ? e.apply(null, args) : e;
|
||
throw new Error("[Immer] " + msg);
|
||
}
|
||
}
|
||
|
||
var ArchtypeObject = 0;
|
||
var ArchtypeArray = 1;
|
||
var ArchtypeMap = 2;
|
||
var ArchtypeSet = 3;
|
||
var ProxyTypeProxyObject = 0;
|
||
var ProxyTypeProxyArray = 1;
|
||
var ProxyTypeES5Object = 4;
|
||
var ProxyTypeES5Array = 5;
|
||
var ProxyTypeMap = 2;
|
||
var ProxyTypeSet = 3;
|
||
|
||
/** Returns true if the given value is an Immer draft */
|
||
|
||
|
||
|
||
function isDraft(value) {
|
||
return !!value && !!value[DRAFT_STATE];
|
||
}
|
||
/** Returns true if the given value can be drafted by Immer */
|
||
|
||
|
||
|
||
function isDraftable(value) {
|
||
if (!value) return false;
|
||
return isPlainObject(value) || Array.isArray(value) || !!value[DRAFTABLE] || !!value.constructor[DRAFTABLE] || isMap(value) || isSet(value);
|
||
}
|
||
|
||
|
||
function isPlainObject(value) {
|
||
if (!value || typeof value !== "object") return false;
|
||
var proto = Object.getPrototypeOf(value);
|
||
return !proto || proto === Object.prototype;
|
||
}
|
||
function original(value) {
|
||
if (!isDraft(value)) die(23, value);
|
||
return value[DRAFT_STATE].base_;
|
||
}
|
||
|
||
|
||
var ownKeys = typeof Reflect !== "undefined" && Reflect.ownKeys ? Reflect.ownKeys : typeof Object.getOwnPropertySymbols !== "undefined" ? function (obj) {
|
||
return Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj));
|
||
} :
|
||
/* istanbul ignore next */
|
||
Object.getOwnPropertyNames;
|
||
var getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors || function getOwnPropertyDescriptors(target) {
|
||
// Polyfill needed for Hermes and IE, see https://github.com/facebook/hermes/issues/274
|
||
var res = {};
|
||
ownKeys(target).forEach(function (key) {
|
||
res[key] = Object.getOwnPropertyDescriptor(target, key);
|
||
});
|
||
return res;
|
||
};
|
||
function each(obj, iter, enumerableOnly) {
|
||
if (enumerableOnly === void 0) {
|
||
enumerableOnly = false;
|
||
}
|
||
|
||
if (getArchtype(obj) === ArchtypeObject) {
|
||
(enumerableOnly ? Object.keys : ownKeys)(obj).forEach(function (key) {
|
||
if (!enumerableOnly || typeof key !== "symbol") iter(key, obj[key], obj);
|
||
});
|
||
} else {
|
||
obj.forEach(function (entry, index) {
|
||
return iter(index, entry, obj);
|
||
});
|
||
}
|
||
}
|
||
|
||
|
||
function getArchtype(thing) {
|
||
/* istanbul ignore next */
|
||
var state = thing[DRAFT_STATE];
|
||
return state ? state.type_ > 3 ? state.type_ - 4 // cause Object and Array map back from 4 and 5
|
||
: state.type_ // others are the same
|
||
: Array.isArray(thing) ? ArchtypeArray : isMap(thing) ? ArchtypeMap : isSet(thing) ? ArchtypeSet : ArchtypeObject;
|
||
}
|
||
|
||
|
||
function has(thing, prop) {
|
||
return getArchtype(thing) === ArchtypeMap ? thing.has(prop) : Object.prototype.hasOwnProperty.call(thing, prop);
|
||
}
|
||
|
||
|
||
function get(thing, prop) {
|
||
// @ts-ignore
|
||
return getArchtype(thing) === ArchtypeMap ? thing.get(prop) : thing[prop];
|
||
}
|
||
|
||
|
||
function set(thing, propOrOldValue, value) {
|
||
var t = getArchtype(thing);
|
||
if (t === ArchtypeMap) thing.set(propOrOldValue, value);else if (t === ArchtypeSet) {
|
||
thing.delete(propOrOldValue);
|
||
thing.add(value);
|
||
} else thing[propOrOldValue] = value;
|
||
}
|
||
|
||
|
||
function is(x, y) {
|
||
// From: https://github.com/facebook/fbjs/blob/c69904a511b900266935168223063dd8772dfc40/packages/fbjs/src/core/shallowEqual.js
|
||
if (x === y) {
|
||
return x !== 0 || 1 / x === 1 / y;
|
||
} else {
|
||
return x !== x && y !== y;
|
||
}
|
||
}
|
||
|
||
|
||
function isMap(target) {
|
||
return hasMap && target instanceof Map;
|
||
}
|
||
|
||
|
||
function isSet(target) {
|
||
return hasSet && target instanceof Set;
|
||
}
|
||
|
||
|
||
function latest(state) {
|
||
return state.copy_ || state.base_;
|
||
}
|
||
|
||
|
||
function shallowCopy(base) {
|
||
if (Array.isArray(base)) return Array.prototype.slice.call(base);
|
||
var descriptors = getOwnPropertyDescriptors(base);
|
||
delete descriptors[DRAFT_STATE];
|
||
var keys = ownKeys(descriptors);
|
||
|
||
for (var i = 0; i < keys.length; i++) {
|
||
var key = keys[i];
|
||
var desc = descriptors[key];
|
||
|
||
if (desc.writable === false) {
|
||
desc.writable = true;
|
||
desc.configurable = true;
|
||
} // like object.assign, we will read any _own_, get/set accessors. This helps in dealing
|
||
// with libraries that trap values, like mobx or vue
|
||
// unlike object.assign, non-enumerables will be copied as well
|
||
|
||
|
||
if (desc.get || desc.set) descriptors[key] = {
|
||
configurable: true,
|
||
writable: true,
|
||
enumerable: desc.enumerable,
|
||
value: base[key]
|
||
};
|
||
}
|
||
|
||
return Object.create(Object.getPrototypeOf(base), descriptors);
|
||
}
|
||
function freeze(obj, deep) {
|
||
if (isFrozen(obj) || isDraft(obj) || !isDraftable(obj)) return;
|
||
|
||
if (getArchtype(obj) > 1
|
||
/* Map or Set */
|
||
) {
|
||
obj.set = obj.add = obj.clear = obj.delete = dontMutateFrozenCollections;
|
||
}
|
||
|
||
Object.freeze(obj);
|
||
if (deep) each(obj, function (key, value) {
|
||
return freeze(value, true);
|
||
}, true);
|
||
}
|
||
|
||
function dontMutateFrozenCollections() {
|
||
die(2);
|
||
}
|
||
|
||
function isFrozen(obj) {
|
||
if (obj == null || typeof obj !== "object") return true; // See #600, IE dies on non-objects in Object.isFrozen
|
||
|
||
return Object.isFrozen(obj);
|
||
}
|
||
|
||
/** Plugin utilities */
|
||
|
||
var plugins = {};
|
||
function getPlugin(pluginKey) {
|
||
var plugin = plugins[pluginKey];
|
||
|
||
if (!plugin) {
|
||
die(18, pluginKey);
|
||
} // @ts-ignore
|
||
|
||
|
||
return plugin;
|
||
}
|
||
function loadPlugin(pluginKey, implementation) {
|
||
if (!plugins[pluginKey]) plugins[pluginKey] = implementation;
|
||
}
|
||
|
||
var currentScope;
|
||
function getCurrentScope() {
|
||
if ( !currentScope) die(0);
|
||
return currentScope;
|
||
}
|
||
|
||
function createScope(parent_, immer_) {
|
||
return {
|
||
drafts_: [],
|
||
parent_: parent_,
|
||
immer_: immer_,
|
||
// Whenever the modified draft contains a draft from another scope, we
|
||
// need to prevent auto-freezing so the unowned draft can be finalized.
|
||
canAutoFreeze_: true,
|
||
unfinalizedDrafts_: 0
|
||
};
|
||
}
|
||
|
||
function usePatchesInScope(scope, patchListener) {
|
||
if (patchListener) {
|
||
getPlugin("Patches"); // assert we have the plugin
|
||
|
||
scope.patches_ = [];
|
||
scope.inversePatches_ = [];
|
||
scope.patchListener_ = patchListener;
|
||
}
|
||
}
|
||
function revokeScope(scope) {
|
||
leaveScope(scope);
|
||
scope.drafts_.forEach(revokeDraft); // @ts-ignore
|
||
|
||
scope.drafts_ = null;
|
||
}
|
||
function leaveScope(scope) {
|
||
if (scope === currentScope) {
|
||
currentScope = scope.parent_;
|
||
}
|
||
}
|
||
function enterScope(immer) {
|
||
return currentScope = createScope(currentScope, immer);
|
||
}
|
||
|
||
function revokeDraft(draft) {
|
||
var state = draft[DRAFT_STATE];
|
||
if (state.type_ === ProxyTypeProxyObject || state.type_ === ProxyTypeProxyArray) state.revoke_();else state.revoked_ = true;
|
||
}
|
||
|
||
function processResult(result, scope) {
|
||
scope.unfinalizedDrafts_ = scope.drafts_.length;
|
||
var baseDraft = scope.drafts_[0];
|
||
var isReplaced = result !== undefined && result !== baseDraft;
|
||
if (!scope.immer_.useProxies_) getPlugin("ES5").willFinalizeES5_(scope, result, isReplaced);
|
||
|
||
if (isReplaced) {
|
||
if (baseDraft[DRAFT_STATE].modified_) {
|
||
revokeScope(scope);
|
||
die(4);
|
||
}
|
||
|
||
if (isDraftable(result)) {
|
||
// Finalize the result in case it contains (or is) a subset of the draft.
|
||
result = finalize(scope, result);
|
||
if (!scope.parent_) maybeFreeze(scope, result);
|
||
}
|
||
|
||
if (scope.patches_) {
|
||
getPlugin("Patches").generateReplacementPatches_(baseDraft[DRAFT_STATE], result, scope.patches_, scope.inversePatches_);
|
||
}
|
||
} else {
|
||
// Finalize the base draft.
|
||
result = finalize(scope, baseDraft, []);
|
||
}
|
||
|
||
revokeScope(scope);
|
||
|
||
if (scope.patches_) {
|
||
scope.patchListener_(scope.patches_, scope.inversePatches_);
|
||
}
|
||
|
||
return result !== NOTHING ? result : undefined;
|
||
}
|
||
|
||
function finalize(rootScope, value, path) {
|
||
// Don't recurse in tho recursive data structures
|
||
if (isFrozen(value)) return value;
|
||
var state = value[DRAFT_STATE]; // A plain object, might need freezing, might contain drafts
|
||
|
||
if (!state) {
|
||
each(value, function (key, childValue) {
|
||
return finalizeProperty(rootScope, state, value, key, childValue, path);
|
||
}, true // See #590, don't recurse into non-enumarable of non drafted objects
|
||
);
|
||
return value;
|
||
} // Never finalize drafts owned by another scope.
|
||
|
||
|
||
if (state.scope_ !== rootScope) return value; // Unmodified draft, return the (frozen) original
|
||
|
||
if (!state.modified_) {
|
||
maybeFreeze(rootScope, state.base_, true);
|
||
return state.base_;
|
||
} // Not finalized yet, let's do that now
|
||
|
||
|
||
if (!state.finalized_) {
|
||
state.finalized_ = true;
|
||
state.scope_.unfinalizedDrafts_--;
|
||
var result = // For ES5, create a good copy from the draft first, with added keys and without deleted keys.
|
||
state.type_ === ProxyTypeES5Object || state.type_ === ProxyTypeES5Array ? state.copy_ = shallowCopy(state.draft_) : state.copy_; // Finalize all children of the copy
|
||
// For sets we clone before iterating, otherwise we can get in endless loop due to modifying during iteration, see #628
|
||
// Although the original test case doesn't seem valid anyway, so if this in the way we can turn the next line
|
||
// back to each(result, ....)
|
||
|
||
each(state.type_ === ProxyTypeSet ? new Set(result) : result, function (key, childValue) {
|
||
return finalizeProperty(rootScope, state, result, key, childValue, path);
|
||
}); // everything inside is frozen, we can freeze here
|
||
|
||
maybeFreeze(rootScope, result, false); // first time finalizing, let's create those patches
|
||
|
||
if (path && rootScope.patches_) {
|
||
getPlugin("Patches").generatePatches_(state, path, rootScope.patches_, rootScope.inversePatches_);
|
||
}
|
||
}
|
||
|
||
return state.copy_;
|
||
}
|
||
|
||
function finalizeProperty(rootScope, parentState, targetObject, prop, childValue, rootPath) {
|
||
if ( childValue === targetObject) die(5);
|
||
|
||
if (isDraft(childValue)) {
|
||
var path = rootPath && parentState && parentState.type_ !== ProxyTypeSet && // Set objects are atomic since they have no keys.
|
||
!has(parentState.assigned_, prop) // Skip deep patches for assigned keys.
|
||
? rootPath.concat(prop) : undefined; // Drafts owned by `scope` are finalized here.
|
||
|
||
var res = finalize(rootScope, childValue, path);
|
||
set(targetObject, prop, res); // Drafts from another scope must prevented to be frozen
|
||
// if we got a draft back from finalize, we're in a nested produce and shouldn't freeze
|
||
|
||
if (isDraft(res)) {
|
||
rootScope.canAutoFreeze_ = false;
|
||
} else return;
|
||
} // Search new objects for unfinalized drafts. Frozen objects should never contain drafts.
|
||
|
||
|
||
if (isDraftable(childValue) && !isFrozen(childValue)) {
|
||
if (!rootScope.immer_.autoFreeze_ && rootScope.unfinalizedDrafts_ < 1) {
|
||
// optimization: if an object is not a draft, and we don't have to
|
||
// deepfreeze everything, and we are sure that no drafts are left in the remaining object
|
||
// cause we saw and finalized all drafts already; we can stop visiting the rest of the tree.
|
||
// This benefits especially adding large data tree's without further processing.
|
||
// See add-data.js perf test
|
||
return;
|
||
}
|
||
|
||
finalize(rootScope, childValue); // immer deep freezes plain objects, so if there is no parent state, we freeze as well
|
||
|
||
if (!parentState || !parentState.scope_.parent_) maybeFreeze(rootScope, childValue);
|
||
}
|
||
}
|
||
|
||
function maybeFreeze(scope, value, deep) {
|
||
if (deep === void 0) {
|
||
deep = false;
|
||
}
|
||
|
||
if (scope.immer_.autoFreeze_ && scope.canAutoFreeze_) {
|
||
freeze(value, deep);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Returns a new draft of the `base` object.
|
||
*
|
||
* The second argument is the parent draft-state (used internally).
|
||
*/
|
||
|
||
function createProxyProxy(base, parent) {
|
||
var isArray = Array.isArray(base);
|
||
var state = {
|
||
type_: isArray ? ProxyTypeProxyArray : ProxyTypeProxyObject,
|
||
// Track which produce call this is associated with.
|
||
scope_: parent ? parent.scope_ : getCurrentScope(),
|
||
// True for both shallow and deep changes.
|
||
modified_: false,
|
||
// Used during finalization.
|
||
finalized_: false,
|
||
// Track which properties have been assigned (true) or deleted (false).
|
||
assigned_: {},
|
||
// The parent draft state.
|
||
parent_: parent,
|
||
// The base state.
|
||
base_: base,
|
||
// The base proxy.
|
||
draft_: null,
|
||
// The base copy with any updated values.
|
||
copy_: null,
|
||
// Called by the `produce` function.
|
||
revoke_: null,
|
||
isManual_: false
|
||
}; // the traps must target something, a bit like the 'real' base.
|
||
// but also, we need to be able to determine from the target what the relevant state is
|
||
// (to avoid creating traps per instance to capture the state in closure,
|
||
// and to avoid creating weird hidden properties as well)
|
||
// So the trick is to use 'state' as the actual 'target'! (and make sure we intercept everything)
|
||
// Note that in the case of an array, we put the state in an array to have better Reflect defaults ootb
|
||
|
||
var target = state;
|
||
var traps = objectTraps;
|
||
|
||
if (isArray) {
|
||
target = [state];
|
||
traps = arrayTraps;
|
||
}
|
||
|
||
var _Proxy$revocable = Proxy.revocable(target, traps),
|
||
revoke = _Proxy$revocable.revoke,
|
||
proxy = _Proxy$revocable.proxy;
|
||
|
||
state.draft_ = proxy;
|
||
state.revoke_ = revoke;
|
||
return proxy;
|
||
}
|
||
/**
|
||
* Object drafts
|
||
*/
|
||
|
||
var objectTraps = {
|
||
get: function get(state, prop) {
|
||
if (prop === DRAFT_STATE) return state;
|
||
var source = latest(state);
|
||
|
||
if (!has(source, prop)) {
|
||
// non-existing or non-own property...
|
||
return readPropFromProto(state, source, prop);
|
||
}
|
||
|
||
var value = source[prop];
|
||
|
||
if (state.finalized_ || !isDraftable(value)) {
|
||
return value;
|
||
} // Check for existing draft in modified state.
|
||
// Assigned values are never drafted. This catches any drafts we created, too.
|
||
|
||
|
||
if (value === peek(state.base_, prop)) {
|
||
prepareCopy(state);
|
||
return state.copy_[prop] = createProxy(state.scope_.immer_, value, state);
|
||
}
|
||
|
||
return value;
|
||
},
|
||
has: function has(state, prop) {
|
||
return prop in latest(state);
|
||
},
|
||
ownKeys: function ownKeys(state) {
|
||
return Reflect.ownKeys(latest(state));
|
||
},
|
||
set: function set(state, prop
|
||
/* strictly not, but helps TS */
|
||
, value) {
|
||
var desc = getDescriptorFromProto(latest(state), prop);
|
||
|
||
if (desc === null || desc === void 0 ? void 0 : desc.set) {
|
||
// special case: if this write is captured by a setter, we have
|
||
// to trigger it with the correct context
|
||
desc.set.call(state.draft_, value);
|
||
return true;
|
||
}
|
||
|
||
if (!state.modified_) {
|
||
// the last check is because we need to be able to distinguish setting a non-existig to undefined (which is a change)
|
||
// from setting an existing property with value undefined to undefined (which is not a change)
|
||
var current = peek(latest(state), prop); // special case, if we assigning the original value to a draft, we can ignore the assignment
|
||
|
||
var currentState = current === null || current === void 0 ? void 0 : current[DRAFT_STATE];
|
||
|
||
if (currentState && currentState.base_ === value) {
|
||
state.copy_[prop] = value;
|
||
state.assigned_[prop] = false;
|
||
return true;
|
||
}
|
||
|
||
if (is(value, current) && (value !== undefined || has(state.base_, prop))) return true;
|
||
prepareCopy(state);
|
||
markChanged(state);
|
||
} // @ts-ignore
|
||
|
||
|
||
state.copy_[prop] = value;
|
||
state.assigned_[prop] = true;
|
||
return true;
|
||
},
|
||
deleteProperty: function deleteProperty(state, prop) {
|
||
// The `undefined` check is a fast path for pre-existing keys.
|
||
if (peek(state.base_, prop) !== undefined || prop in state.base_) {
|
||
state.assigned_[prop] = false;
|
||
prepareCopy(state);
|
||
markChanged(state);
|
||
} else {
|
||
// if an originally not assigned property was deleted
|
||
delete state.assigned_[prop];
|
||
} // @ts-ignore
|
||
|
||
|
||
if (state.copy_) delete state.copy_[prop];
|
||
return true;
|
||
},
|
||
// Note: We never coerce `desc.value` into an Immer draft, because we can't make
|
||
// the same guarantee in ES5 mode.
|
||
getOwnPropertyDescriptor: function getOwnPropertyDescriptor(state, prop) {
|
||
var owner = latest(state);
|
||
var desc = Reflect.getOwnPropertyDescriptor(owner, prop);
|
||
if (!desc) return desc;
|
||
return {
|
||
writable: true,
|
||
configurable: state.type_ !== ProxyTypeProxyArray || prop !== "length",
|
||
enumerable: desc.enumerable,
|
||
value: owner[prop]
|
||
};
|
||
},
|
||
defineProperty: function defineProperty() {
|
||
die(11);
|
||
},
|
||
getPrototypeOf: function getPrototypeOf(state) {
|
||
return Object.getPrototypeOf(state.base_);
|
||
},
|
||
setPrototypeOf: function setPrototypeOf() {
|
||
die(12);
|
||
}
|
||
};
|
||
/**
|
||
* Array drafts
|
||
*/
|
||
|
||
var arrayTraps = {};
|
||
each(objectTraps, function (key, fn) {
|
||
// @ts-ignore
|
||
arrayTraps[key] = function () {
|
||
arguments[0] = arguments[0][0];
|
||
return fn.apply(this, arguments);
|
||
};
|
||
});
|
||
|
||
arrayTraps.deleteProperty = function (state, prop) {
|
||
if ( isNaN(parseInt(prop))) die(13);
|
||
return objectTraps.deleteProperty.call(this, state[0], prop);
|
||
};
|
||
|
||
arrayTraps.set = function (state, prop, value) {
|
||
if ( prop !== "length" && isNaN(parseInt(prop))) die(14);
|
||
return objectTraps.set.call(this, state[0], prop, value, state[0]);
|
||
}; // Access a property without creating an Immer draft.
|
||
|
||
|
||
function peek(draft, prop) {
|
||
var state = draft[DRAFT_STATE];
|
||
var source = state ? latest(state) : draft;
|
||
return source[prop];
|
||
}
|
||
|
||
function readPropFromProto(state, source, prop) {
|
||
var _desc$get;
|
||
|
||
var desc = getDescriptorFromProto(source, prop);
|
||
return desc ? "value" in desc ? desc.value : // This is a very special case, if the prop is a getter defined by the
|
||
// prototype, we should invoke it with the draft as context!
|
||
(_desc$get = desc.get) === null || _desc$get === void 0 ? void 0 : _desc$get.call(state.draft_) : undefined;
|
||
}
|
||
|
||
function getDescriptorFromProto(source, prop) {
|
||
// 'in' checks proto!
|
||
if (!(prop in source)) return undefined;
|
||
var proto = Object.getPrototypeOf(source);
|
||
|
||
while (proto) {
|
||
var desc = Object.getOwnPropertyDescriptor(proto, prop);
|
||
if (desc) return desc;
|
||
proto = Object.getPrototypeOf(proto);
|
||
}
|
||
|
||
return undefined;
|
||
}
|
||
|
||
function markChanged(state) {
|
||
if (!state.modified_) {
|
||
state.modified_ = true;
|
||
|
||
if (state.parent_) {
|
||
markChanged(state.parent_);
|
||
}
|
||
}
|
||
}
|
||
function prepareCopy(state) {
|
||
if (!state.copy_) {
|
||
state.copy_ = shallowCopy(state.base_);
|
||
}
|
||
}
|
||
|
||
var Immer =
|
||
/*#__PURE__*/
|
||
function () {
|
||
function Immer(config) {
|
||
this.useProxies_ = hasProxies;
|
||
this.autoFreeze_ = true
|
||
/* istanbul ignore next */
|
||
;
|
||
if (typeof (config === null || config === void 0 ? void 0 : config.useProxies) === "boolean") this.setUseProxies(config.useProxies);
|
||
if (typeof (config === null || config === void 0 ? void 0 : config.autoFreeze) === "boolean") this.setAutoFreeze(config.autoFreeze);
|
||
this.produce = this.produce.bind(this);
|
||
this.produceWithPatches = this.produceWithPatches.bind(this);
|
||
}
|
||
/**
|
||
* The `produce` function takes a value and a "recipe function" (whose
|
||
* return value often depends on the base state). The recipe function is
|
||
* free to mutate its first argument however it wants. All mutations are
|
||
* only ever applied to a __copy__ of the base state.
|
||
*
|
||
* Pass only a function to create a "curried producer" which relieves you
|
||
* from passing the recipe function every time.
|
||
*
|
||
* Only plain objects and arrays are made mutable. All other objects are
|
||
* considered uncopyable.
|
||
*
|
||
* Note: This function is __bound__ to its `Immer` instance.
|
||
*
|
||
* @param {any} base - the initial state
|
||
* @param {Function} producer - function that receives a proxy of the base state as first argument and which can be freely modified
|
||
* @param {Function} patchListener - optional function that will be called with all the patches produced here
|
||
* @returns {any} a new state, or the initial state if nothing was modified
|
||
*/
|
||
|
||
|
||
var _proto = Immer.prototype;
|
||
|
||
_proto.produce = function produce(base, recipe, patchListener) {
|
||
// curried invocation
|
||
if (typeof base === "function" && typeof recipe !== "function") {
|
||
var defaultBase = recipe;
|
||
recipe = base;
|
||
var self = this;
|
||
return function curriedProduce(base) {
|
||
var _this = this;
|
||
|
||
if (base === void 0) {
|
||
base = defaultBase;
|
||
}
|
||
|
||
for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
||
args[_key - 1] = arguments[_key];
|
||
}
|
||
|
||
return self.produce(base, function (draft) {
|
||
var _recipe;
|
||
|
||
return (_recipe = recipe).call.apply(_recipe, [_this, draft].concat(args));
|
||
}); // prettier-ignore
|
||
};
|
||
}
|
||
|
||
if (typeof recipe !== "function") die(6);
|
||
if (patchListener !== undefined && typeof patchListener !== "function") die(7);
|
||
var result; // Only plain objects, arrays, and "immerable classes" are drafted.
|
||
|
||
if (isDraftable(base)) {
|
||
var scope = enterScope(this);
|
||
var proxy = createProxy(this, base, undefined);
|
||
var hasError = true;
|
||
|
||
try {
|
||
result = recipe(proxy);
|
||
hasError = false;
|
||
} finally {
|
||
// finally instead of catch + rethrow better preserves original stack
|
||
if (hasError) revokeScope(scope);else leaveScope(scope);
|
||
}
|
||
|
||
if (typeof Promise !== "undefined" && result instanceof Promise) {
|
||
return result.then(function (result) {
|
||
usePatchesInScope(scope, patchListener);
|
||
return processResult(result, scope);
|
||
}, function (error) {
|
||
revokeScope(scope);
|
||
throw error;
|
||
});
|
||
}
|
||
|
||
usePatchesInScope(scope, patchListener);
|
||
return processResult(result, scope);
|
||
} else if (!base || typeof base !== "object") {
|
||
result = recipe(base);
|
||
if (result === NOTHING) return undefined;
|
||
if (result === undefined) result = base;
|
||
if (this.autoFreeze_) freeze(result, true);
|
||
return result;
|
||
} else die(21, base);
|
||
};
|
||
|
||
_proto.produceWithPatches = function produceWithPatches(arg1, arg2, arg3) {
|
||
var _this2 = this;
|
||
|
||
if (typeof arg1 === "function") {
|
||
return function (state) {
|
||
for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
|
||
args[_key2 - 1] = arguments[_key2];
|
||
}
|
||
|
||
return _this2.produceWithPatches(state, function (draft) {
|
||
return arg1.apply(void 0, [draft].concat(args));
|
||
});
|
||
};
|
||
}
|
||
|
||
var patches, inversePatches;
|
||
var nextState = this.produce(arg1, arg2, function (p, ip) {
|
||
patches = p;
|
||
inversePatches = ip;
|
||
});
|
||
return [nextState, patches, inversePatches];
|
||
};
|
||
|
||
_proto.createDraft = function createDraft(base) {
|
||
if (!isDraftable(base)) die(8);
|
||
if (isDraft(base)) base = current(base);
|
||
var scope = enterScope(this);
|
||
var proxy = createProxy(this, base, undefined);
|
||
proxy[DRAFT_STATE].isManual_ = true;
|
||
leaveScope(scope);
|
||
return proxy;
|
||
};
|
||
|
||
_proto.finishDraft = function finishDraft(draft, patchListener) {
|
||
var state = draft && draft[DRAFT_STATE];
|
||
|
||
{
|
||
if (!state || !state.isManual_) die(9);
|
||
if (state.finalized_) die(10);
|
||
}
|
||
|
||
var scope = state.scope_;
|
||
usePatchesInScope(scope, patchListener);
|
||
return processResult(undefined, scope);
|
||
}
|
||
/**
|
||
* Pass true to automatically freeze all copies created by Immer.
|
||
*
|
||
* By default, auto-freezing is disabled in production.
|
||
*/
|
||
;
|
||
|
||
_proto.setAutoFreeze = function setAutoFreeze(value) {
|
||
this.autoFreeze_ = value;
|
||
}
|
||
/**
|
||
* Pass true to use the ES2015 `Proxy` class when creating drafts, which is
|
||
* always faster than using ES5 proxies.
|
||
*
|
||
* By default, feature detection is used, so calling this is rarely necessary.
|
||
*/
|
||
;
|
||
|
||
_proto.setUseProxies = function setUseProxies(value) {
|
||
if (value && !hasProxies) {
|
||
die(20);
|
||
}
|
||
|
||
this.useProxies_ = value;
|
||
};
|
||
|
||
_proto.applyPatches = function applyPatches(base, patches) {
|
||
// If a patch replaces the entire state, take that replacement as base
|
||
// before applying patches
|
||
var i;
|
||
|
||
for (i = patches.length - 1; i >= 0; i--) {
|
||
var patch = patches[i];
|
||
|
||
if (patch.path.length === 0 && patch.op === "replace") {
|
||
base = patch.value;
|
||
break;
|
||
}
|
||
}
|
||
|
||
var applyPatchesImpl = getPlugin("Patches").applyPatches_;
|
||
|
||
if (isDraft(base)) {
|
||
// N.B: never hits if some patch a replacement, patches are never drafts
|
||
return applyPatchesImpl(base, patches);
|
||
} // Otherwise, produce a copy of the base state.
|
||
|
||
|
||
return this.produce(base, function (draft) {
|
||
return applyPatchesImpl(draft, patches.slice(i + 1));
|
||
});
|
||
};
|
||
|
||
return Immer;
|
||
}();
|
||
function createProxy(immer, value, parent) {
|
||
// precondition: createProxy should be guarded by isDraftable, so we know we can safely draft
|
||
var draft = isMap(value) ? getPlugin("MapSet").proxyMap_(value, parent) : isSet(value) ? getPlugin("MapSet").proxySet_(value, parent) : immer.useProxies_ ? createProxyProxy(value, parent) : getPlugin("ES5").createES5Proxy_(value, parent);
|
||
var scope = parent ? parent.scope_ : getCurrentScope();
|
||
scope.drafts_.push(draft);
|
||
return draft;
|
||
}
|
||
|
||
function current(value) {
|
||
if (!isDraft(value)) die(22, value);
|
||
return currentImpl(value);
|
||
}
|
||
|
||
function currentImpl(value) {
|
||
if (!isDraftable(value)) return value;
|
||
var state = value[DRAFT_STATE];
|
||
var copy;
|
||
var archType = getArchtype(value);
|
||
|
||
if (state) {
|
||
if (!state.modified_ && (state.type_ < 4 || !getPlugin("ES5").hasChanges_(state))) return state.base_; // Optimization: avoid generating new drafts during copying
|
||
|
||
state.finalized_ = true;
|
||
copy = copyHelper(value, archType);
|
||
state.finalized_ = false;
|
||
} else {
|
||
copy = copyHelper(value, archType);
|
||
}
|
||
|
||
each(copy, function (key, childValue) {
|
||
if (state && get(state.base_, key) === childValue) return; // no need to copy or search in something that didn't change
|
||
|
||
set(copy, key, currentImpl(childValue));
|
||
}); // In the future, we might consider freezing here, based on the current settings
|
||
|
||
return archType === ArchtypeSet ? new Set(copy) : copy;
|
||
}
|
||
|
||
function copyHelper(value, archType) {
|
||
// creates a shallow copy, even if it is a map or set
|
||
switch (archType) {
|
||
case ArchtypeMap:
|
||
return new Map(value);
|
||
|
||
case ArchtypeSet:
|
||
// Set will be cloned as array temporarily, so that we can replace individual items
|
||
return Array.from(value);
|
||
}
|
||
|
||
return shallowCopy(value);
|
||
}
|
||
|
||
function enableES5() {
|
||
function willFinalizeES5_(scope, result, isReplaced) {
|
||
if (!isReplaced) {
|
||
if (scope.patches_) {
|
||
markChangesRecursively(scope.drafts_[0]);
|
||
} // This is faster when we don't care about which attributes changed.
|
||
|
||
|
||
markChangesSweep(scope.drafts_);
|
||
} // When a child draft is returned, look for changes.
|
||
else if (isDraft(result) && result[DRAFT_STATE].scope_ === scope) {
|
||
markChangesSweep(scope.drafts_);
|
||
}
|
||
}
|
||
|
||
function createES5Draft(isArray, base) {
|
||
if (isArray) {
|
||
var draft = new Array(base.length);
|
||
|
||
for (var i = 0; i < base.length; i++) {
|
||
Object.defineProperty(draft, "" + i, proxyProperty(i, true));
|
||
}
|
||
|
||
return draft;
|
||
} else {
|
||
var _descriptors = getOwnPropertyDescriptors(base);
|
||
|
||
delete _descriptors[DRAFT_STATE];
|
||
var keys = ownKeys(_descriptors);
|
||
|
||
for (var _i = 0; _i < keys.length; _i++) {
|
||
var key = keys[_i];
|
||
_descriptors[key] = proxyProperty(key, isArray || !!_descriptors[key].enumerable);
|
||
}
|
||
|
||
return Object.create(Object.getPrototypeOf(base), _descriptors);
|
||
}
|
||
}
|
||
|
||
function createES5Proxy_(base, parent) {
|
||
var isArray = Array.isArray(base);
|
||
var draft = createES5Draft(isArray, base);
|
||
var state = {
|
||
type_: isArray ? ProxyTypeES5Array : ProxyTypeES5Object,
|
||
scope_: parent ? parent.scope_ : getCurrentScope(),
|
||
modified_: false,
|
||
finalized_: false,
|
||
assigned_: {},
|
||
parent_: parent,
|
||
// base is the object we are drafting
|
||
base_: base,
|
||
// draft is the draft object itself, that traps all reads and reads from either the base (if unmodified) or copy (if modified)
|
||
draft_: draft,
|
||
copy_: null,
|
||
revoked_: false,
|
||
isManual_: false
|
||
};
|
||
Object.defineProperty(draft, DRAFT_STATE, {
|
||
value: state,
|
||
// enumerable: false <- the default
|
||
writable: true
|
||
});
|
||
return draft;
|
||
} // property descriptors are recycled to make sure we don't create a get and set closure per property,
|
||
// but share them all instead
|
||
|
||
|
||
var descriptors = {};
|
||
|
||
function proxyProperty(prop, enumerable) {
|
||
var desc = descriptors[prop];
|
||
|
||
if (desc) {
|
||
desc.enumerable = enumerable;
|
||
} else {
|
||
descriptors[prop] = desc = {
|
||
configurable: true,
|
||
enumerable: enumerable,
|
||
get: function get() {
|
||
var state = this[DRAFT_STATE];
|
||
assertUnrevoked(state); // @ts-ignore
|
||
|
||
return objectTraps.get(state, prop);
|
||
},
|
||
set: function set(value) {
|
||
var state = this[DRAFT_STATE];
|
||
assertUnrevoked(state); // @ts-ignore
|
||
|
||
objectTraps.set(state, prop, value);
|
||
}
|
||
};
|
||
}
|
||
|
||
return desc;
|
||
} // This looks expensive, but only proxies are visited, and only objects without known changes are scanned.
|
||
|
||
|
||
function markChangesSweep(drafts) {
|
||
// The natural order of drafts in the `scope` array is based on when they
|
||
// were accessed. By processing drafts in reverse natural order, we have a
|
||
// better chance of processing leaf nodes first. When a leaf node is known to
|
||
// have changed, we can avoid any traversal of its ancestor nodes.
|
||
for (var i = drafts.length - 1; i >= 0; i--) {
|
||
var state = drafts[i][DRAFT_STATE];
|
||
|
||
if (!state.modified_) {
|
||
switch (state.type_) {
|
||
case ProxyTypeES5Array:
|
||
if (hasArrayChanges(state)) markChanged(state);
|
||
break;
|
||
|
||
case ProxyTypeES5Object:
|
||
if (hasObjectChanges(state)) markChanged(state);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function markChangesRecursively(object) {
|
||
if (!object || typeof object !== "object") return;
|
||
var state = object[DRAFT_STATE];
|
||
if (!state) return;
|
||
var base_ = state.base_,
|
||
draft_ = state.draft_,
|
||
assigned_ = state.assigned_,
|
||
type_ = state.type_;
|
||
|
||
if (type_ === ProxyTypeES5Object) {
|
||
// Look for added keys.
|
||
// probably there is a faster way to detect changes, as sweep + recurse seems to do some
|
||
// unnecessary work.
|
||
// also: probably we can store the information we detect here, to speed up tree finalization!
|
||
each(draft_, function (key) {
|
||
if (key === DRAFT_STATE) return; // The `undefined` check is a fast path for pre-existing keys.
|
||
|
||
if (base_[key] === undefined && !has(base_, key)) {
|
||
assigned_[key] = true;
|
||
markChanged(state);
|
||
} else if (!assigned_[key]) {
|
||
// Only untouched properties trigger recursion.
|
||
markChangesRecursively(draft_[key]);
|
||
}
|
||
}); // Look for removed keys.
|
||
|
||
each(base_, function (key) {
|
||
// The `undefined` check is a fast path for pre-existing keys.
|
||
if (draft_[key] === undefined && !has(draft_, key)) {
|
||
assigned_[key] = false;
|
||
markChanged(state);
|
||
}
|
||
});
|
||
} else if (type_ === ProxyTypeES5Array) {
|
||
if (hasArrayChanges(state)) {
|
||
markChanged(state);
|
||
assigned_.length = true;
|
||
}
|
||
|
||
if (draft_.length < base_.length) {
|
||
for (var i = draft_.length; i < base_.length; i++) {
|
||
assigned_[i] = false;
|
||
}
|
||
} else {
|
||
for (var _i2 = base_.length; _i2 < draft_.length; _i2++) {
|
||
assigned_[_i2] = true;
|
||
}
|
||
} // Minimum count is enough, the other parts has been processed.
|
||
|
||
|
||
var min = Math.min(draft_.length, base_.length);
|
||
|
||
for (var _i3 = 0; _i3 < min; _i3++) {
|
||
// Only untouched indices trigger recursion.
|
||
if (assigned_[_i3] === undefined) markChangesRecursively(draft_[_i3]);
|
||
}
|
||
}
|
||
}
|
||
|
||
function hasObjectChanges(state) {
|
||
var base_ = state.base_,
|
||
draft_ = state.draft_; // Search for added keys and changed keys. Start at the back, because
|
||
// non-numeric keys are ordered by time of definition on the object.
|
||
|
||
var keys = ownKeys(draft_);
|
||
|
||
for (var i = keys.length - 1; i >= 0; i--) {
|
||
var key = keys[i];
|
||
if (key === DRAFT_STATE) continue;
|
||
var baseValue = base_[key]; // The `undefined` check is a fast path for pre-existing keys.
|
||
|
||
if (baseValue === undefined && !has(base_, key)) {
|
||
return true;
|
||
} // Once a base key is deleted, future changes go undetected, because its
|
||
// descriptor is erased. This branch detects any missed changes.
|
||
else {
|
||
var value = draft_[key];
|
||
|
||
var _state = value && value[DRAFT_STATE];
|
||
|
||
if (_state ? _state.base_ !== baseValue : !is(value, baseValue)) {
|
||
return true;
|
||
}
|
||
}
|
||
} // At this point, no keys were added or changed.
|
||
// Compare key count to determine if keys were deleted.
|
||
|
||
|
||
var baseIsDraft = !!base_[DRAFT_STATE];
|
||
return keys.length !== ownKeys(base_).length + (baseIsDraft ? 0 : 1); // + 1 to correct for DRAFT_STATE
|
||
}
|
||
|
||
function hasArrayChanges(state) {
|
||
var draft_ = state.draft_;
|
||
if (draft_.length !== state.base_.length) return true; // See #116
|
||
// If we first shorten the length, our array interceptors will be removed.
|
||
// If after that new items are added, result in the same original length,
|
||
// those last items will have no intercepting property.
|
||
// So if there is no own descriptor on the last position, we know that items were removed and added
|
||
// N.B.: splice, unshift, etc only shift values around, but not prop descriptors, so we only have to check
|
||
// the last one
|
||
|
||
var descriptor = Object.getOwnPropertyDescriptor(draft_, draft_.length - 1); // descriptor can be null, but only for newly created sparse arrays, eg. new Array(10)
|
||
|
||
if (descriptor && !descriptor.get) return true; // For all other cases, we don't have to compare, as they would have been picked up by the index setters
|
||
|
||
return false;
|
||
}
|
||
|
||
function hasChanges_(state) {
|
||
return state.type_ === ProxyTypeES5Object ? hasObjectChanges(state) : hasArrayChanges(state);
|
||
}
|
||
|
||
function assertUnrevoked(state
|
||
/*ES5State | MapState | SetState*/
|
||
) {
|
||
if (state.revoked_) die(3, JSON.stringify(latest(state)));
|
||
}
|
||
|
||
loadPlugin("ES5", {
|
||
createES5Proxy_: createES5Proxy_,
|
||
willFinalizeES5_: willFinalizeES5_,
|
||
hasChanges_: hasChanges_
|
||
});
|
||
}
|
||
|
||
function enablePatches() {
|
||
var REPLACE = "replace";
|
||
var ADD = "add";
|
||
var REMOVE = "remove";
|
||
|
||
function generatePatches_(state, basePath, patches, inversePatches) {
|
||
switch (state.type_) {
|
||
case ProxyTypeProxyObject:
|
||
case ProxyTypeES5Object:
|
||
case ProxyTypeMap:
|
||
return generatePatchesFromAssigned(state, basePath, patches, inversePatches);
|
||
|
||
case ProxyTypeES5Array:
|
||
case ProxyTypeProxyArray:
|
||
return generateArrayPatches(state, basePath, patches, inversePatches);
|
||
|
||
case ProxyTypeSet:
|
||
return generateSetPatches(state, basePath, patches, inversePatches);
|
||
}
|
||
}
|
||
|
||
function generateArrayPatches(state, basePath, patches, inversePatches) {
|
||
var base_ = state.base_,
|
||
assigned_ = state.assigned_;
|
||
var copy_ = state.copy_; // Reduce complexity by ensuring `base` is never longer.
|
||
|
||
if (copy_.length < base_.length) {
|
||
var _ref = [copy_, base_];
|
||
base_ = _ref[0];
|
||
copy_ = _ref[1];
|
||
var _ref2 = [inversePatches, patches];
|
||
patches = _ref2[0];
|
||
inversePatches = _ref2[1];
|
||
} // Process replaced indices.
|
||
|
||
|
||
for (var i = 0; i < base_.length; i++) {
|
||
if (assigned_[i] && copy_[i] !== base_[i]) {
|
||
var path = basePath.concat([i]);
|
||
patches.push({
|
||
op: REPLACE,
|
||
path: path,
|
||
// Need to maybe clone it, as it can in fact be the original value
|
||
// due to the base/copy inversion at the start of this function
|
||
value: clonePatchValueIfNeeded(copy_[i])
|
||
});
|
||
inversePatches.push({
|
||
op: REPLACE,
|
||
path: path,
|
||
value: clonePatchValueIfNeeded(base_[i])
|
||
});
|
||
}
|
||
} // Process added indices.
|
||
|
||
|
||
for (var _i = base_.length; _i < copy_.length; _i++) {
|
||
var _path = basePath.concat([_i]);
|
||
|
||
patches.push({
|
||
op: ADD,
|
||
path: _path,
|
||
// Need to maybe clone it, as it can in fact be the original value
|
||
// due to the base/copy inversion at the start of this function
|
||
value: clonePatchValueIfNeeded(copy_[_i])
|
||
});
|
||
}
|
||
|
||
if (base_.length < copy_.length) {
|
||
inversePatches.push({
|
||
op: REPLACE,
|
||
path: basePath.concat(["length"]),
|
||
value: base_.length
|
||
});
|
||
}
|
||
} // This is used for both Map objects and normal objects.
|
||
|
||
|
||
function generatePatchesFromAssigned(state, basePath, patches, inversePatches) {
|
||
var base_ = state.base_,
|
||
copy_ = state.copy_;
|
||
each(state.assigned_, function (key, assignedValue) {
|
||
var origValue = get(base_, key);
|
||
var value = get(copy_, key);
|
||
var op = !assignedValue ? REMOVE : has(base_, key) ? REPLACE : ADD;
|
||
if (origValue === value && op === REPLACE) return;
|
||
var path = basePath.concat(key);
|
||
patches.push(op === REMOVE ? {
|
||
op: op,
|
||
path: path
|
||
} : {
|
||
op: op,
|
||
path: path,
|
||
value: value
|
||
});
|
||
inversePatches.push(op === ADD ? {
|
||
op: REMOVE,
|
||
path: path
|
||
} : op === REMOVE ? {
|
||
op: ADD,
|
||
path: path,
|
||
value: clonePatchValueIfNeeded(origValue)
|
||
} : {
|
||
op: REPLACE,
|
||
path: path,
|
||
value: clonePatchValueIfNeeded(origValue)
|
||
});
|
||
});
|
||
}
|
||
|
||
function generateSetPatches(state, basePath, patches, inversePatches) {
|
||
var base_ = state.base_,
|
||
copy_ = state.copy_;
|
||
var i = 0;
|
||
base_.forEach(function (value) {
|
||
if (!copy_.has(value)) {
|
||
var path = basePath.concat([i]);
|
||
patches.push({
|
||
op: REMOVE,
|
||
path: path,
|
||
value: value
|
||
});
|
||
inversePatches.unshift({
|
||
op: ADD,
|
||
path: path,
|
||
value: value
|
||
});
|
||
}
|
||
|
||
i++;
|
||
});
|
||
i = 0;
|
||
copy_.forEach(function (value) {
|
||
if (!base_.has(value)) {
|
||
var path = basePath.concat([i]);
|
||
patches.push({
|
||
op: ADD,
|
||
path: path,
|
||
value: value
|
||
});
|
||
inversePatches.unshift({
|
||
op: REMOVE,
|
||
path: path,
|
||
value: value
|
||
});
|
||
}
|
||
|
||
i++;
|
||
});
|
||
}
|
||
|
||
function generateReplacementPatches_(rootState, replacement, patches, inversePatches) {
|
||
patches.push({
|
||
op: REPLACE,
|
||
path: [],
|
||
value: replacement
|
||
});
|
||
inversePatches.push({
|
||
op: REPLACE,
|
||
path: [],
|
||
value: rootState.base_
|
||
});
|
||
}
|
||
|
||
function applyPatches_(draft, patches) {
|
||
patches.forEach(function (patch) {
|
||
var path = patch.path,
|
||
op = patch.op;
|
||
var base = draft;
|
||
|
||
for (var i = 0; i < path.length - 1; i++) {
|
||
base = get(base, path[i]);
|
||
if (typeof base !== "object") die(15, path.join("/"));
|
||
}
|
||
|
||
var type = getArchtype(base);
|
||
var value = deepClonePatchValue(patch.value); // used to clone patch to ensure original patch is not modified, see #411
|
||
|
||
var key = path[path.length - 1];
|
||
|
||
switch (op) {
|
||
case REPLACE:
|
||
switch (type) {
|
||
case ArchtypeMap:
|
||
return base.set(key, value);
|
||
|
||
/* istanbul ignore next */
|
||
|
||
case ArchtypeSet:
|
||
die(16);
|
||
|
||
default:
|
||
// if value is an object, then it's assigned by reference
|
||
// in the following add or remove ops, the value field inside the patch will also be modifyed
|
||
// so we use value from the cloned patch
|
||
// @ts-ignore
|
||
return base[key] = value;
|
||
}
|
||
|
||
case ADD:
|
||
switch (type) {
|
||
case ArchtypeArray:
|
||
return base.splice(key, 0, value);
|
||
|
||
case ArchtypeMap:
|
||
return base.set(key, value);
|
||
|
||
case ArchtypeSet:
|
||
return base.add(value);
|
||
|
||
default:
|
||
return base[key] = value;
|
||
}
|
||
|
||
case REMOVE:
|
||
switch (type) {
|
||
case ArchtypeArray:
|
||
return base.splice(key, 1);
|
||
|
||
case ArchtypeMap:
|
||
return base.delete(key);
|
||
|
||
case ArchtypeSet:
|
||
return base.delete(patch.value);
|
||
|
||
default:
|
||
return delete base[key];
|
||
}
|
||
|
||
default:
|
||
die(17, op);
|
||
}
|
||
});
|
||
return draft;
|
||
}
|
||
|
||
function deepClonePatchValue(obj) {
|
||
if (!isDraftable(obj)) return obj;
|
||
if (Array.isArray(obj)) return obj.map(deepClonePatchValue);
|
||
if (isMap(obj)) return new Map(Array.from(obj.entries()).map(function (_ref3) {
|
||
var k = _ref3[0],
|
||
v = _ref3[1];
|
||
return [k, deepClonePatchValue(v)];
|
||
}));
|
||
if (isSet(obj)) return new Set(Array.from(obj).map(deepClonePatchValue));
|
||
var cloned = Object.create(Object.getPrototypeOf(obj));
|
||
|
||
for (var key in obj) {
|
||
cloned[key] = deepClonePatchValue(obj[key]);
|
||
}
|
||
|
||
return cloned;
|
||
}
|
||
|
||
function clonePatchValueIfNeeded(obj) {
|
||
if (isDraft(obj)) {
|
||
return deepClonePatchValue(obj);
|
||
} else return obj;
|
||
}
|
||
|
||
loadPlugin("Patches", {
|
||
applyPatches_: applyPatches_,
|
||
generatePatches_: generatePatches_,
|
||
generateReplacementPatches_: generateReplacementPatches_
|
||
});
|
||
}
|
||
|
||
// types only!
|
||
function enableMapSet() {
|
||
/* istanbul ignore next */
|
||
var _extendStatics = function extendStatics(d, b) {
|
||
_extendStatics = Object.setPrototypeOf || {
|
||
__proto__: []
|
||
} instanceof Array && function (d, b) {
|
||
d.__proto__ = b;
|
||
} || function (d, b) {
|
||
for (var p in b) {
|
||
if (b.hasOwnProperty(p)) d[p] = b[p];
|
||
}
|
||
};
|
||
|
||
return _extendStatics(d, b);
|
||
}; // Ugly hack to resolve #502 and inherit built in Map / Set
|
||
|
||
|
||
function __extends(d, b) {
|
||
_extendStatics(d, b);
|
||
|
||
function __() {
|
||
this.constructor = d;
|
||
}
|
||
|
||
d.prototype = ( // @ts-ignore
|
||
__.prototype = b.prototype, new __());
|
||
}
|
||
|
||
var DraftMap = function (_super) {
|
||
__extends(DraftMap, _super); // Create class manually, cause #502
|
||
|
||
|
||
function DraftMap(target, parent) {
|
||
this[DRAFT_STATE] = {
|
||
type_: ProxyTypeMap,
|
||
parent_: parent,
|
||
scope_: parent ? parent.scope_ : getCurrentScope(),
|
||
modified_: false,
|
||
finalized_: false,
|
||
copy_: undefined,
|
||
assigned_: undefined,
|
||
base_: target,
|
||
draft_: this,
|
||
isManual_: false,
|
||
revoked_: false
|
||
};
|
||
return this;
|
||
}
|
||
|
||
var p = DraftMap.prototype;
|
||
Object.defineProperty(p, "size", {
|
||
get: function get() {
|
||
return latest(this[DRAFT_STATE]).size;
|
||
} // enumerable: false,
|
||
// configurable: true
|
||
|
||
});
|
||
|
||
p.has = function (key) {
|
||
return latest(this[DRAFT_STATE]).has(key);
|
||
};
|
||
|
||
p.set = function (key, value) {
|
||
var state = this[DRAFT_STATE];
|
||
assertUnrevoked(state);
|
||
|
||
if (!latest(state).has(key) || latest(state).get(key) !== value) {
|
||
prepareMapCopy(state);
|
||
markChanged(state);
|
||
state.assigned_.set(key, true);
|
||
state.copy_.set(key, value);
|
||
state.assigned_.set(key, true);
|
||
}
|
||
|
||
return this;
|
||
};
|
||
|
||
p.delete = function (key) {
|
||
if (!this.has(key)) {
|
||
return false;
|
||
}
|
||
|
||
var state = this[DRAFT_STATE];
|
||
assertUnrevoked(state);
|
||
prepareMapCopy(state);
|
||
markChanged(state);
|
||
state.assigned_.set(key, false);
|
||
state.copy_.delete(key);
|
||
return true;
|
||
};
|
||
|
||
p.clear = function () {
|
||
var state = this[DRAFT_STATE];
|
||
assertUnrevoked(state);
|
||
|
||
if (latest(state).size) {
|
||
prepareMapCopy(state);
|
||
markChanged(state);
|
||
state.assigned_ = new Map();
|
||
each(state.base_, function (key) {
|
||
state.assigned_.set(key, false);
|
||
});
|
||
state.copy_.clear();
|
||
}
|
||
};
|
||
|
||
p.forEach = function (cb, thisArg) {
|
||
var _this = this;
|
||
|
||
var state = this[DRAFT_STATE];
|
||
latest(state).forEach(function (_value, key, _map) {
|
||
cb.call(thisArg, _this.get(key), key, _this);
|
||
});
|
||
};
|
||
|
||
p.get = function (key) {
|
||
var state = this[DRAFT_STATE];
|
||
assertUnrevoked(state);
|
||
var value = latest(state).get(key);
|
||
|
||
if (state.finalized_ || !isDraftable(value)) {
|
||
return value;
|
||
}
|
||
|
||
if (value !== state.base_.get(key)) {
|
||
return value; // either already drafted or reassigned
|
||
} // despite what it looks, this creates a draft only once, see above condition
|
||
|
||
|
||
var draft = createProxy(state.scope_.immer_, value, state);
|
||
prepareMapCopy(state);
|
||
state.copy_.set(key, draft);
|
||
return draft;
|
||
};
|
||
|
||
p.keys = function () {
|
||
return latest(this[DRAFT_STATE]).keys();
|
||
};
|
||
|
||
p.values = function () {
|
||
var _this2 = this,
|
||
_ref;
|
||
|
||
var iterator = this.keys();
|
||
return _ref = {}, _ref[iteratorSymbol] = function () {
|
||
return _this2.values();
|
||
}, _ref.next = function next() {
|
||
var r = iterator.next();
|
||
/* istanbul ignore next */
|
||
|
||
if (r.done) return r;
|
||
|
||
var value = _this2.get(r.value);
|
||
|
||
return {
|
||
done: false,
|
||
value: value
|
||
};
|
||
}, _ref;
|
||
};
|
||
|
||
p.entries = function () {
|
||
var _this3 = this,
|
||
_ref2;
|
||
|
||
var iterator = this.keys();
|
||
return _ref2 = {}, _ref2[iteratorSymbol] = function () {
|
||
return _this3.entries();
|
||
}, _ref2.next = function next() {
|
||
var r = iterator.next();
|
||
/* istanbul ignore next */
|
||
|
||
if (r.done) return r;
|
||
|
||
var value = _this3.get(r.value);
|
||
|
||
return {
|
||
done: false,
|
||
value: [r.value, value]
|
||
};
|
||
}, _ref2;
|
||
};
|
||
|
||
p[iteratorSymbol] = function () {
|
||
return this.entries();
|
||
};
|
||
|
||
return DraftMap;
|
||
}(Map);
|
||
|
||
function proxyMap_(target, parent) {
|
||
// @ts-ignore
|
||
return new DraftMap(target, parent);
|
||
}
|
||
|
||
function prepareMapCopy(state) {
|
||
if (!state.copy_) {
|
||
state.assigned_ = new Map();
|
||
state.copy_ = new Map(state.base_);
|
||
}
|
||
}
|
||
|
||
var DraftSet = function (_super) {
|
||
__extends(DraftSet, _super); // Create class manually, cause #502
|
||
|
||
|
||
function DraftSet(target, parent) {
|
||
this[DRAFT_STATE] = {
|
||
type_: ProxyTypeSet,
|
||
parent_: parent,
|
||
scope_: parent ? parent.scope_ : getCurrentScope(),
|
||
modified_: false,
|
||
finalized_: false,
|
||
copy_: undefined,
|
||
base_: target,
|
||
draft_: this,
|
||
drafts_: new Map(),
|
||
revoked_: false,
|
||
isManual_: false
|
||
};
|
||
return this;
|
||
}
|
||
|
||
var p = DraftSet.prototype;
|
||
Object.defineProperty(p, "size", {
|
||
get: function get() {
|
||
return latest(this[DRAFT_STATE]).size;
|
||
} // enumerable: true,
|
||
|
||
});
|
||
|
||
p.has = function (value) {
|
||
var state = this[DRAFT_STATE];
|
||
assertUnrevoked(state); // bit of trickery here, to be able to recognize both the value, and the draft of its value
|
||
|
||
if (!state.copy_) {
|
||
return state.base_.has(value);
|
||
}
|
||
|
||
if (state.copy_.has(value)) return true;
|
||
if (state.drafts_.has(value) && state.copy_.has(state.drafts_.get(value))) return true;
|
||
return false;
|
||
};
|
||
|
||
p.add = function (value) {
|
||
var state = this[DRAFT_STATE];
|
||
assertUnrevoked(state);
|
||
|
||
if (!this.has(value)) {
|
||
prepareSetCopy(state);
|
||
markChanged(state);
|
||
state.copy_.add(value);
|
||
}
|
||
|
||
return this;
|
||
};
|
||
|
||
p.delete = function (value) {
|
||
if (!this.has(value)) {
|
||
return false;
|
||
}
|
||
|
||
var state = this[DRAFT_STATE];
|
||
assertUnrevoked(state);
|
||
prepareSetCopy(state);
|
||
markChanged(state);
|
||
return state.copy_.delete(value) || (state.drafts_.has(value) ? state.copy_.delete(state.drafts_.get(value)) :
|
||
/* istanbul ignore next */
|
||
false);
|
||
};
|
||
|
||
p.clear = function () {
|
||
var state = this[DRAFT_STATE];
|
||
assertUnrevoked(state);
|
||
|
||
if (latest(state).size) {
|
||
prepareSetCopy(state);
|
||
markChanged(state);
|
||
state.copy_.clear();
|
||
}
|
||
};
|
||
|
||
p.values = function () {
|
||
var state = this[DRAFT_STATE];
|
||
assertUnrevoked(state);
|
||
prepareSetCopy(state);
|
||
return state.copy_.values();
|
||
};
|
||
|
||
p.entries = function entries() {
|
||
var state = this[DRAFT_STATE];
|
||
assertUnrevoked(state);
|
||
prepareSetCopy(state);
|
||
return state.copy_.entries();
|
||
};
|
||
|
||
p.keys = function () {
|
||
return this.values();
|
||
};
|
||
|
||
p[iteratorSymbol] = function () {
|
||
return this.values();
|
||
};
|
||
|
||
p.forEach = function forEach(cb, thisArg) {
|
||
var iterator = this.values();
|
||
var result = iterator.next();
|
||
|
||
while (!result.done) {
|
||
cb.call(thisArg, result.value, result.value, this);
|
||
result = iterator.next();
|
||
}
|
||
};
|
||
|
||
return DraftSet;
|
||
}(Set);
|
||
|
||
function proxySet_(target, parent) {
|
||
// @ts-ignore
|
||
return new DraftSet(target, parent);
|
||
}
|
||
|
||
function prepareSetCopy(state) {
|
||
if (!state.copy_) {
|
||
// create drafts for all entries to preserve insertion order
|
||
state.copy_ = new Set();
|
||
state.base_.forEach(function (value) {
|
||
if (isDraftable(value)) {
|
||
var draft = createProxy(state.scope_.immer_, value, state);
|
||
state.drafts_.set(value, draft);
|
||
state.copy_.add(draft);
|
||
} else {
|
||
state.copy_.add(value);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function assertUnrevoked(state
|
||
/*ES5State | MapState | SetState*/
|
||
) {
|
||
if (state.revoked_) die(3, JSON.stringify(latest(state)));
|
||
}
|
||
|
||
loadPlugin("MapSet", {
|
||
proxyMap_: proxyMap_,
|
||
proxySet_: proxySet_
|
||
});
|
||
}
|
||
|
||
function enableAllPlugins() {
|
||
enableES5();
|
||
enableMapSet();
|
||
enablePatches();
|
||
}
|
||
|
||
var immer =
|
||
/*#__PURE__*/
|
||
new Immer();
|
||
/**
|
||
* The `produce` function takes a value and a "recipe function" (whose
|
||
* return value often depends on the base state). The recipe function is
|
||
* free to mutate its first argument however it wants. All mutations are
|
||
* only ever applied to a __copy__ of the base state.
|
||
*
|
||
* Pass only a function to create a "curried producer" which relieves you
|
||
* from passing the recipe function every time.
|
||
*
|
||
* Only plain objects and arrays are made mutable. All other objects are
|
||
* considered uncopyable.
|
||
*
|
||
* Note: This function is __bound__ to its `Immer` instance.
|
||
*
|
||
* @param {any} base - the initial state
|
||
* @param {Function} producer - function that receives a proxy of the base state as first argument and which can be freely modified
|
||
* @param {Function} patchListener - optional function that will be called with all the patches produced here
|
||
* @returns {any} a new state, or the initial state if nothing was modified
|
||
*/
|
||
|
||
var produce = immer.produce;
|
||
/**
|
||
* Like `produce`, but `produceWithPatches` always returns a tuple
|
||
* [nextState, patches, inversePatches] (instead of just the next state)
|
||
*/
|
||
|
||
var produceWithPatches =
|
||
/*#__PURE__*/
|
||
immer.produceWithPatches.bind(immer);
|
||
/**
|
||
* Pass true to automatically freeze all copies created by Immer.
|
||
*
|
||
* By default, auto-freezing is disabled in production.
|
||
*/
|
||
|
||
var setAutoFreeze =
|
||
/*#__PURE__*/
|
||
immer.setAutoFreeze.bind(immer);
|
||
/**
|
||
* Pass true to use the ES2015 `Proxy` class when creating drafts, which is
|
||
* always faster than using ES5 proxies.
|
||
*
|
||
* By default, feature detection is used, so calling this is rarely necessary.
|
||
*/
|
||
|
||
var setUseProxies =
|
||
/*#__PURE__*/
|
||
immer.setUseProxies.bind(immer);
|
||
/**
|
||
* Apply an array of Immer patches to the first argument.
|
||
*
|
||
* This function is a producer, which means copy-on-write is in effect.
|
||
*/
|
||
|
||
var applyPatches =
|
||
/*#__PURE__*/
|
||
immer.applyPatches.bind(immer);
|
||
/**
|
||
* Create an Immer draft from the given base state, which may be a draft itself.
|
||
* The draft can be modified until you finalize it with the `finishDraft` function.
|
||
*/
|
||
|
||
var createDraft =
|
||
/*#__PURE__*/
|
||
immer.createDraft.bind(immer);
|
||
/**
|
||
* Finalize an Immer draft from a `createDraft` call, returning the base state
|
||
* (if no changes were made) or a modified copy. The draft must *not* be
|
||
* mutated afterwards.
|
||
*
|
||
* Pass a function as the 2nd argument to generate Immer patches based on the
|
||
* changes that were made.
|
||
*/
|
||
|
||
var finishDraft =
|
||
/*#__PURE__*/
|
||
immer.finishDraft.bind(immer);
|
||
/**
|
||
* This function is actually a no-op, but can be used to cast an immutable type
|
||
* to an draft type and make TypeScript happy
|
||
*
|
||
* @param value
|
||
*/
|
||
|
||
function castDraft(value) {
|
||
return value;
|
||
}
|
||
/**
|
||
* This function is actually a no-op, but can be used to cast a mutable type
|
||
* to an immutable type and make TypeScript happy
|
||
* @param value
|
||
*/
|
||
|
||
function castImmutable(value) {
|
||
return value;
|
||
}
|
||
|
||
immer_cjs_development.Immer = Immer;
|
||
immer_cjs_development.applyPatches = applyPatches;
|
||
immer_cjs_development.castDraft = castDraft;
|
||
immer_cjs_development.castImmutable = castImmutable;
|
||
immer_cjs_development.createDraft = createDraft;
|
||
immer_cjs_development.current = current;
|
||
immer_cjs_development.default = produce;
|
||
immer_cjs_development.enableAllPlugins = enableAllPlugins;
|
||
immer_cjs_development.enableES5 = enableES5;
|
||
var enableMapSet_1 = immer_cjs_development.enableMapSet = enableMapSet;
|
||
immer_cjs_development.enablePatches = enablePatches;
|
||
immer_cjs_development.finishDraft = finishDraft;
|
||
immer_cjs_development.immerable = DRAFTABLE;
|
||
immer_cjs_development.isDraft = isDraft;
|
||
immer_cjs_development.isDraftable = isDraftable;
|
||
immer_cjs_development.nothing = NOTHING;
|
||
immer_cjs_development.original = original;
|
||
var produce_1 = immer_cjs_development.produce = produce;
|
||
immer_cjs_development.produceWithPatches = produceWithPatches;
|
||
var setAutoFreeze_1 = immer_cjs_development.setAutoFreeze = setAutoFreeze;
|
||
immer_cjs_development.setUseProxies = setUseProxies;
|
||
|
||
var rtc = {exports: {}};
|
||
|
||
/**
|
||
*
|
||
* Copyright (c) 2018 Apple (http://www.apple.com)
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*
|
||
*
|
||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||
* THE SOFTWARE.
|
||
*
|
||
*/
|
||
|
||
(function (module, exports) {
|
||
/*! Modules included in this bundle:
|
||
* name: process
|
||
* license: MIT
|
||
* version: 0.11.10
|
||
* text:
|
||
* (The MIT License)
|
||
*
|
||
* Copyright (c) 2013 Roman Shtylman <shtylman@gmail.com>
|
||
*
|
||
* Permission is hereby granted, free of charge, to any person obtaining
|
||
* a copy of this software and associated documentation files (the
|
||
* 'Software'), to deal in the Software without restriction, including
|
||
* without limitation the rights to use, copy, modify, merge, publish,
|
||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||
* permit persons to whom the Software is furnished to do so, subject to
|
||
* the following conditions:
|
||
*
|
||
* The above copyright notice and this permission notice shall be
|
||
* included in all copies or substantial portions of the Software.
|
||
*
|
||
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||
*
|
||
*
|
||
* name: semver
|
||
* license: ISC
|
||
* version: 5.5.0
|
||
* text:
|
||
* The ISC License
|
||
*
|
||
* Copyright (c) Isaac Z. Schlueter and Contributors
|
||
*
|
||
* Permission to use, copy, modify, and/or distribute this software for any
|
||
* purpose with or without fee is hereby granted, provided that the above
|
||
* copyright notice and this permission notice appear in all copies.
|
||
*
|
||
* 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.
|
||
*
|
||
*
|
||
* name: webpack
|
||
* license: MIT
|
||
* version: 4.41.2
|
||
* text:
|
||
* Copyright JS Foundation and other contributors
|
||
*
|
||
* Permission is hereby granted, free of charge, to any person obtaining
|
||
* a copy of this software and associated documentation files (the
|
||
* 'Software'), to deal in the Software without restriction, including
|
||
* without limitation the rights to use, copy, modify, merge, publish,
|
||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||
* permit persons to whom the Software is furnished to do so, subject to
|
||
* the following conditions:
|
||
*
|
||
* The above copyright notice and this permission notice shall be
|
||
* included in all copies or substantial portions of the Software.
|
||
*
|
||
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||
*
|
||
*
|
||
*/
|
||
(function webpackUniversalModuleDefinition(root, factory) {
|
||
module.exports = factory();
|
||
})(commonjsGlobal, function() {
|
||
return /******/ (function(modules) { // webpackBootstrap
|
||
/******/ // The module cache
|
||
/******/ var installedModules = {};
|
||
/******/
|
||
/******/ // The require function
|
||
/******/ function __webpack_require__(moduleId) {
|
||
/******/
|
||
/******/ // Check if module is in cache
|
||
/******/ if(installedModules[moduleId]) {
|
||
/******/ return installedModules[moduleId].exports;
|
||
/******/ }
|
||
/******/ // Create a new module (and put it into the cache)
|
||
/******/ var module = installedModules[moduleId] = {
|
||
/******/ i: moduleId,
|
||
/******/ l: false,
|
||
/******/ exports: {}
|
||
/******/ };
|
||
/******/
|
||
/******/ // Execute the module function
|
||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||
/******/
|
||
/******/ // Flag the module as loaded
|
||
/******/ module.l = true;
|
||
/******/
|
||
/******/ // Return the exports of the module
|
||
/******/ return module.exports;
|
||
/******/ }
|
||
/******/
|
||
/******/
|
||
/******/ // expose the modules object (__webpack_modules__)
|
||
/******/ __webpack_require__.m = modules;
|
||
/******/
|
||
/******/ // expose the module cache
|
||
/******/ __webpack_require__.c = installedModules;
|
||
/******/
|
||
/******/ // define getter function for harmony exports
|
||
/******/ __webpack_require__.d = function(exports, name, getter) {
|
||
/******/ if(!__webpack_require__.o(exports, name)) {
|
||
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
|
||
/******/ }
|
||
/******/ };
|
||
/******/
|
||
/******/ // define __esModule on exports
|
||
/******/ __webpack_require__.r = function(exports) {
|
||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||
/******/ }
|
||
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
||
/******/ };
|
||
/******/
|
||
/******/ // create a fake namespace object
|
||
/******/ // mode & 1: value is a module id, require it
|
||
/******/ // mode & 2: merge all properties of value into the ns
|
||
/******/ // mode & 4: return value when already ns object
|
||
/******/ // mode & 8|1: behave like require
|
||
/******/ __webpack_require__.t = function(value, mode) {
|
||
/******/ if(mode & 1) value = __webpack_require__(value);
|
||
/******/ if(mode & 8) return value;
|
||
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
|
||
/******/ var ns = Object.create(null);
|
||
/******/ __webpack_require__.r(ns);
|
||
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
|
||
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
|
||
/******/ return ns;
|
||
/******/ };
|
||
/******/
|
||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||
/******/ __webpack_require__.n = function(module) {
|
||
/******/ var getter = module && module.__esModule ?
|
||
/******/ function getDefault() { return module['default']; } :
|
||
/******/ function getModuleExports() { return module; };
|
||
/******/ __webpack_require__.d(getter, 'a', getter);
|
||
/******/ return getter;
|
||
/******/ };
|
||
/******/
|
||
/******/ // Object.prototype.hasOwnProperty.call
|
||
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
|
||
/******/
|
||
/******/ // __webpack_public_path__
|
||
/******/ __webpack_require__.p = "";
|
||
/******/
|
||
/******/
|
||
/******/ // Load entry module and return exports
|
||
/******/ return __webpack_require__(__webpack_require__.s = "./src/rtc_reporting/reporting_agent.ts");
|
||
/******/ })
|
||
/************************************************************************/
|
||
/******/ ({
|
||
|
||
/***/ "./node_modules/process/browser.js":
|
||
/*!*****************************************!*\
|
||
!*** ./node_modules/process/browser.js ***!
|
||
\*****************************************/
|
||
/*! no static exports found */
|
||
/***/ (function(module, exports) {
|
||
|
||
// shim for using process in browser
|
||
var process = module.exports = {};
|
||
|
||
// cached from whatever global is present so that test runners that stub it
|
||
// don't break things. But we need to wrap it in a try catch in case it is
|
||
// wrapped in strict mode code which doesn't define any globals. It's inside a
|
||
// function because try/catches deoptimize in certain engines.
|
||
|
||
var cachedSetTimeout;
|
||
var cachedClearTimeout;
|
||
|
||
function defaultSetTimout() {
|
||
throw new Error('setTimeout has not been defined');
|
||
}
|
||
function defaultClearTimeout () {
|
||
throw new Error('clearTimeout has not been defined');
|
||
}
|
||
(function () {
|
||
try {
|
||
if (typeof setTimeout === 'function') {
|
||
cachedSetTimeout = setTimeout;
|
||
} else {
|
||
cachedSetTimeout = defaultSetTimout;
|
||
}
|
||
} catch (e) {
|
||
cachedSetTimeout = defaultSetTimout;
|
||
}
|
||
try {
|
||
if (typeof clearTimeout === 'function') {
|
||
cachedClearTimeout = clearTimeout;
|
||
} else {
|
||
cachedClearTimeout = defaultClearTimeout;
|
||
}
|
||
} catch (e) {
|
||
cachedClearTimeout = defaultClearTimeout;
|
||
}
|
||
} ());
|
||
function runTimeout(fun) {
|
||
if (cachedSetTimeout === setTimeout) {
|
||
//normal enviroments in sane situations
|
||
return setTimeout(fun, 0);
|
||
}
|
||
// if setTimeout wasn't available but was latter defined
|
||
if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) {
|
||
cachedSetTimeout = setTimeout;
|
||
return setTimeout(fun, 0);
|
||
}
|
||
try {
|
||
// when when somebody has screwed with setTimeout but no I.E. maddness
|
||
return cachedSetTimeout(fun, 0);
|
||
} catch(e){
|
||
try {
|
||
// When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
|
||
return cachedSetTimeout.call(null, fun, 0);
|
||
} catch(e){
|
||
// same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error
|
||
return cachedSetTimeout.call(this, fun, 0);
|
||
}
|
||
}
|
||
|
||
|
||
}
|
||
function runClearTimeout(marker) {
|
||
if (cachedClearTimeout === clearTimeout) {
|
||
//normal enviroments in sane situations
|
||
return clearTimeout(marker);
|
||
}
|
||
// if clearTimeout wasn't available but was latter defined
|
||
if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) {
|
||
cachedClearTimeout = clearTimeout;
|
||
return clearTimeout(marker);
|
||
}
|
||
try {
|
||
// when when somebody has screwed with setTimeout but no I.E. maddness
|
||
return cachedClearTimeout(marker);
|
||
} catch (e){
|
||
try {
|
||
// When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
|
||
return cachedClearTimeout.call(null, marker);
|
||
} catch (e){
|
||
// same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error.
|
||
// Some versions of I.E. have different rules for clearTimeout vs setTimeout
|
||
return cachedClearTimeout.call(this, marker);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
}
|
||
var queue = [];
|
||
var draining = false;
|
||
var currentQueue;
|
||
var queueIndex = -1;
|
||
|
||
function cleanUpNextTick() {
|
||
if (!draining || !currentQueue) {
|
||
return;
|
||
}
|
||
draining = false;
|
||
if (currentQueue.length) {
|
||
queue = currentQueue.concat(queue);
|
||
} else {
|
||
queueIndex = -1;
|
||
}
|
||
if (queue.length) {
|
||
drainQueue();
|
||
}
|
||
}
|
||
|
||
function drainQueue() {
|
||
if (draining) {
|
||
return;
|
||
}
|
||
var timeout = runTimeout(cleanUpNextTick);
|
||
draining = true;
|
||
|
||
var len = queue.length;
|
||
while(len) {
|
||
currentQueue = queue;
|
||
queue = [];
|
||
while (++queueIndex < len) {
|
||
if (currentQueue) {
|
||
currentQueue[queueIndex].run();
|
||
}
|
||
}
|
||
queueIndex = -1;
|
||
len = queue.length;
|
||
}
|
||
currentQueue = null;
|
||
draining = false;
|
||
runClearTimeout(timeout);
|
||
}
|
||
|
||
process.nextTick = function (fun) {
|
||
var args = new Array(arguments.length - 1);
|
||
if (arguments.length > 1) {
|
||
for (var i = 1; i < arguments.length; i++) {
|
||
args[i - 1] = arguments[i];
|
||
}
|
||
}
|
||
queue.push(new Item(fun, args));
|
||
if (queue.length === 1 && !draining) {
|
||
runTimeout(drainQueue);
|
||
}
|
||
};
|
||
|
||
// v8 likes predictible objects
|
||
function Item(fun, array) {
|
||
this.fun = fun;
|
||
this.array = array;
|
||
}
|
||
Item.prototype.run = function () {
|
||
this.fun.apply(null, this.array);
|
||
};
|
||
process.title = 'browser';
|
||
process.browser = true;
|
||
process.env = {};
|
||
process.argv = [];
|
||
process.version = ''; // empty string to avoid regexp issues
|
||
process.versions = {};
|
||
|
||
function noop() {}
|
||
|
||
process.on = noop;
|
||
process.addListener = noop;
|
||
process.once = noop;
|
||
process.off = noop;
|
||
process.removeListener = noop;
|
||
process.removeAllListeners = noop;
|
||
process.emit = noop;
|
||
process.prependListener = noop;
|
||
process.prependOnceListener = noop;
|
||
|
||
process.listeners = function (name) { return [] };
|
||
|
||
process.binding = function (name) {
|
||
throw new Error('process.binding is not supported');
|
||
};
|
||
|
||
process.cwd = function () { return '/' };
|
||
process.chdir = function (dir) {
|
||
throw new Error('process.chdir is not supported');
|
||
};
|
||
process.umask = function() { return 0; };
|
||
|
||
|
||
/***/ }),
|
||
|
||
/***/ "./node_modules/semver/semver.js":
|
||
/*!***************************************!*\
|
||
!*** ./node_modules/semver/semver.js ***!
|
||
\***************************************/
|
||
/*! no static exports found */
|
||
/***/ (function(module, exports, __webpack_require__) {
|
||
|
||
/* WEBPACK VAR INJECTION */(function(process) {exports = module.exports = SemVer;
|
||
|
||
// The debug function is excluded entirely from the minified version.
|
||
/* nomin */ var debug;
|
||
/* nomin */ if (typeof process === 'object' &&
|
||
/* nomin */ process.env &&
|
||
/* nomin */ process.env.NODE_DEBUG &&
|
||
/* nomin */ /\bsemver\b/i.test(process.env.NODE_DEBUG))
|
||
/* nomin */ debug = function() {
|
||
/* nomin */ var args = Array.prototype.slice.call(arguments, 0);
|
||
/* nomin */ args.unshift('SEMVER');
|
||
/* nomin */ console.log.apply(console, args);
|
||
/* nomin */ };
|
||
/* nomin */ else
|
||
/* nomin */ debug = function() {};
|
||
|
||
// Note: this is the semver.org version of the spec that it implements
|
||
// Not necessarily the package version of this code.
|
||
exports.SEMVER_SPEC_VERSION = '2.0.0';
|
||
|
||
var MAX_LENGTH = 256;
|
||
var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991;
|
||
|
||
// Max safe segment length for coercion.
|
||
var MAX_SAFE_COMPONENT_LENGTH = 16;
|
||
|
||
// The actual regexps go on exports.re
|
||
var re = exports.re = [];
|
||
var src = exports.src = [];
|
||
var R = 0;
|
||
|
||
// The following Regular Expressions can be used for tokenizing,
|
||
// validating, and parsing SemVer version strings.
|
||
|
||
// ## Numeric Identifier
|
||
// A single `0`, or a non-zero digit followed by zero or more digits.
|
||
|
||
var NUMERICIDENTIFIER = R++;
|
||
src[NUMERICIDENTIFIER] = '0|[1-9]\\d*';
|
||
var NUMERICIDENTIFIERLOOSE = R++;
|
||
src[NUMERICIDENTIFIERLOOSE] = '[0-9]+';
|
||
|
||
|
||
// ## Non-numeric Identifier
|
||
// Zero or more digits, followed by a letter or hyphen, and then zero or
|
||
// more letters, digits, or hyphens.
|
||
|
||
var NONNUMERICIDENTIFIER = R++;
|
||
src[NONNUMERICIDENTIFIER] = '\\d*[a-zA-Z-][a-zA-Z0-9-]*';
|
||
|
||
|
||
// ## Main Version
|
||
// Three dot-separated numeric identifiers.
|
||
|
||
var MAINVERSION = R++;
|
||
src[MAINVERSION] = '(' + src[NUMERICIDENTIFIER] + ')\\.' +
|
||
'(' + src[NUMERICIDENTIFIER] + ')\\.' +
|
||
'(' + src[NUMERICIDENTIFIER] + ')';
|
||
|
||
var MAINVERSIONLOOSE = R++;
|
||
src[MAINVERSIONLOOSE] = '(' + src[NUMERICIDENTIFIERLOOSE] + ')\\.' +
|
||
'(' + src[NUMERICIDENTIFIERLOOSE] + ')\\.' +
|
||
'(' + src[NUMERICIDENTIFIERLOOSE] + ')';
|
||
|
||
// ## Pre-release Version Identifier
|
||
// A numeric identifier, or a non-numeric identifier.
|
||
|
||
var PRERELEASEIDENTIFIER = R++;
|
||
src[PRERELEASEIDENTIFIER] = '(?:' + src[NUMERICIDENTIFIER] +
|
||
'|' + src[NONNUMERICIDENTIFIER] + ')';
|
||
|
||
var PRERELEASEIDENTIFIERLOOSE = R++;
|
||
src[PRERELEASEIDENTIFIERLOOSE] = '(?:' + src[NUMERICIDENTIFIERLOOSE] +
|
||
'|' + src[NONNUMERICIDENTIFIER] + ')';
|
||
|
||
|
||
// ## Pre-release Version
|
||
// Hyphen, followed by one or more dot-separated pre-release version
|
||
// identifiers.
|
||
|
||
var PRERELEASE = R++;
|
||
src[PRERELEASE] = '(?:-(' + src[PRERELEASEIDENTIFIER] +
|
||
'(?:\\.' + src[PRERELEASEIDENTIFIER] + ')*))';
|
||
|
||
var PRERELEASELOOSE = R++;
|
||
src[PRERELEASELOOSE] = '(?:-?(' + src[PRERELEASEIDENTIFIERLOOSE] +
|
||
'(?:\\.' + src[PRERELEASEIDENTIFIERLOOSE] + ')*))';
|
||
|
||
// ## Build Metadata Identifier
|
||
// Any combination of digits, letters, or hyphens.
|
||
|
||
var BUILDIDENTIFIER = R++;
|
||
src[BUILDIDENTIFIER] = '[0-9A-Za-z-]+';
|
||
|
||
// ## Build Metadata
|
||
// Plus sign, followed by one or more period-separated build metadata
|
||
// identifiers.
|
||
|
||
var BUILD = R++;
|
||
src[BUILD] = '(?:\\+(' + src[BUILDIDENTIFIER] +
|
||
'(?:\\.' + src[BUILDIDENTIFIER] + ')*))';
|
||
|
||
|
||
// ## Full Version String
|
||
// A main version, followed optionally by a pre-release version and
|
||
// build metadata.
|
||
|
||
// Note that the only major, minor, patch, and pre-release sections of
|
||
// the version string are capturing groups. The build metadata is not a
|
||
// capturing group, because it should not ever be used in version
|
||
// comparison.
|
||
|
||
var FULL = R++;
|
||
var FULLPLAIN = 'v?' + src[MAINVERSION] +
|
||
src[PRERELEASE] + '?' +
|
||
src[BUILD] + '?';
|
||
|
||
src[FULL] = '^' + FULLPLAIN + '$';
|
||
|
||
// like full, but allows v1.2.3 and =1.2.3, which people do sometimes.
|
||
// also, 1.0.0alpha1 (prerelease without the hyphen) which is pretty
|
||
// common in the npm registry.
|
||
var LOOSEPLAIN = '[v=\\s]*' + src[MAINVERSIONLOOSE] +
|
||
src[PRERELEASELOOSE] + '?' +
|
||
src[BUILD] + '?';
|
||
|
||
var LOOSE = R++;
|
||
src[LOOSE] = '^' + LOOSEPLAIN + '$';
|
||
|
||
var GTLT = R++;
|
||
src[GTLT] = '((?:<|>)?=?)';
|
||
|
||
// Something like "2.*" or "1.2.x".
|
||
// Note that "x.x" is a valid xRange identifer, meaning "any version"
|
||
// Only the first item is strictly required.
|
||
var XRANGEIDENTIFIERLOOSE = R++;
|
||
src[XRANGEIDENTIFIERLOOSE] = src[NUMERICIDENTIFIERLOOSE] + '|x|X|\\*';
|
||
var XRANGEIDENTIFIER = R++;
|
||
src[XRANGEIDENTIFIER] = src[NUMERICIDENTIFIER] + '|x|X|\\*';
|
||
|
||
var XRANGEPLAIN = R++;
|
||
src[XRANGEPLAIN] = '[v=\\s]*(' + src[XRANGEIDENTIFIER] + ')' +
|
||
'(?:\\.(' + src[XRANGEIDENTIFIER] + ')' +
|
||
'(?:\\.(' + src[XRANGEIDENTIFIER] + ')' +
|
||
'(?:' + src[PRERELEASE] + ')?' +
|
||
src[BUILD] + '?' +
|
||
')?)?';
|
||
|
||
var XRANGEPLAINLOOSE = R++;
|
||
src[XRANGEPLAINLOOSE] = '[v=\\s]*(' + src[XRANGEIDENTIFIERLOOSE] + ')' +
|
||
'(?:\\.(' + src[XRANGEIDENTIFIERLOOSE] + ')' +
|
||
'(?:\\.(' + src[XRANGEIDENTIFIERLOOSE] + ')' +
|
||
'(?:' + src[PRERELEASELOOSE] + ')?' +
|
||
src[BUILD] + '?' +
|
||
')?)?';
|
||
|
||
var XRANGE = R++;
|
||
src[XRANGE] = '^' + src[GTLT] + '\\s*' + src[XRANGEPLAIN] + '$';
|
||
var XRANGELOOSE = R++;
|
||
src[XRANGELOOSE] = '^' + src[GTLT] + '\\s*' + src[XRANGEPLAINLOOSE] + '$';
|
||
|
||
// Coercion.
|
||
// Extract anything that could conceivably be a part of a valid semver
|
||
var COERCE = R++;
|
||
src[COERCE] = '(?:^|[^\\d])' +
|
||
'(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '})' +
|
||
'(?:\\.(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '}))?' +
|
||
'(?:\\.(\\d{1,' + MAX_SAFE_COMPONENT_LENGTH + '}))?' +
|
||
'(?:$|[^\\d])';
|
||
|
||
// Tilde ranges.
|
||
// Meaning is "reasonably at or greater than"
|
||
var LONETILDE = R++;
|
||
src[LONETILDE] = '(?:~>?)';
|
||
|
||
var TILDETRIM = R++;
|
||
src[TILDETRIM] = '(\\s*)' + src[LONETILDE] + '\\s+';
|
||
re[TILDETRIM] = new RegExp(src[TILDETRIM], 'g');
|
||
var tildeTrimReplace = '$1~';
|
||
|
||
var TILDE = R++;
|
||
src[TILDE] = '^' + src[LONETILDE] + src[XRANGEPLAIN] + '$';
|
||
var TILDELOOSE = R++;
|
||
src[TILDELOOSE] = '^' + src[LONETILDE] + src[XRANGEPLAINLOOSE] + '$';
|
||
|
||
// Caret ranges.
|
||
// Meaning is "at least and backwards compatible with"
|
||
var LONECARET = R++;
|
||
src[LONECARET] = '(?:\\^)';
|
||
|
||
var CARETTRIM = R++;
|
||
src[CARETTRIM] = '(\\s*)' + src[LONECARET] + '\\s+';
|
||
re[CARETTRIM] = new RegExp(src[CARETTRIM], 'g');
|
||
var caretTrimReplace = '$1^';
|
||
|
||
var CARET = R++;
|
||
src[CARET] = '^' + src[LONECARET] + src[XRANGEPLAIN] + '$';
|
||
var CARETLOOSE = R++;
|
||
src[CARETLOOSE] = '^' + src[LONECARET] + src[XRANGEPLAINLOOSE] + '$';
|
||
|
||
// A simple gt/lt/eq thing, or just "" to indicate "any version"
|
||
var COMPARATORLOOSE = R++;
|
||
src[COMPARATORLOOSE] = '^' + src[GTLT] + '\\s*(' + LOOSEPLAIN + ')$|^$';
|
||
var COMPARATOR = R++;
|
||
src[COMPARATOR] = '^' + src[GTLT] + '\\s*(' + FULLPLAIN + ')$|^$';
|
||
|
||
|
||
// An expression to strip any whitespace between the gtlt and the thing
|
||
// it modifies, so that `> 1.2.3` ==> `>1.2.3`
|
||
var COMPARATORTRIM = R++;
|
||
src[COMPARATORTRIM] = '(\\s*)' + src[GTLT] +
|
||
'\\s*(' + LOOSEPLAIN + '|' + src[XRANGEPLAIN] + ')';
|
||
|
||
// this one has to use the /g flag
|
||
re[COMPARATORTRIM] = new RegExp(src[COMPARATORTRIM], 'g');
|
||
var comparatorTrimReplace = '$1$2$3';
|
||
|
||
|
||
// Something like `1.2.3 - 1.2.4`
|
||
// Note that these all use the loose form, because they'll be
|
||
// checked against either the strict or loose comparator form
|
||
// later.
|
||
var HYPHENRANGE = R++;
|
||
src[HYPHENRANGE] = '^\\s*(' + src[XRANGEPLAIN] + ')' +
|
||
'\\s+-\\s+' +
|
||
'(' + src[XRANGEPLAIN] + ')' +
|
||
'\\s*$';
|
||
|
||
var HYPHENRANGELOOSE = R++;
|
||
src[HYPHENRANGELOOSE] = '^\\s*(' + src[XRANGEPLAINLOOSE] + ')' +
|
||
'\\s+-\\s+' +
|
||
'(' + src[XRANGEPLAINLOOSE] + ')' +
|
||
'\\s*$';
|
||
|
||
// Star ranges basically just allow anything at all.
|
||
var STAR = R++;
|
||
src[STAR] = '(<|>)?=?\\s*\\*';
|
||
|
||
// Compile to actual regexp objects.
|
||
// All are flag-free, unless they were created above with a flag.
|
||
for (var i = 0; i < R; i++) {
|
||
debug(i, src[i]);
|
||
if (!re[i])
|
||
re[i] = new RegExp(src[i]);
|
||
}
|
||
|
||
exports.parse = parse;
|
||
function parse(version, loose) {
|
||
if (version instanceof SemVer)
|
||
return version;
|
||
|
||
if (typeof version !== 'string')
|
||
return null;
|
||
|
||
if (version.length > MAX_LENGTH)
|
||
return null;
|
||
|
||
var r = loose ? re[LOOSE] : re[FULL];
|
||
if (!r.test(version))
|
||
return null;
|
||
|
||
try {
|
||
return new SemVer(version, loose);
|
||
} catch (er) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
exports.valid = valid;
|
||
function valid(version, loose) {
|
||
var v = parse(version, loose);
|
||
return v ? v.version : null;
|
||
}
|
||
|
||
|
||
exports.clean = clean;
|
||
function clean(version, loose) {
|
||
var s = parse(version.trim().replace(/^[=v]+/, ''), loose);
|
||
return s ? s.version : null;
|
||
}
|
||
|
||
exports.SemVer = SemVer;
|
||
|
||
function SemVer(version, loose) {
|
||
if (version instanceof SemVer) {
|
||
if (version.loose === loose)
|
||
return version;
|
||
else
|
||
version = version.version;
|
||
} else if (typeof version !== 'string') {
|
||
throw new TypeError('Invalid Version: ' + version);
|
||
}
|
||
|
||
if (version.length > MAX_LENGTH)
|
||
throw new TypeError('version is longer than ' + MAX_LENGTH + ' characters')
|
||
|
||
if (!(this instanceof SemVer))
|
||
return new SemVer(version, loose);
|
||
|
||
debug('SemVer', version, loose);
|
||
this.loose = loose;
|
||
var m = version.trim().match(loose ? re[LOOSE] : re[FULL]);
|
||
|
||
if (!m)
|
||
throw new TypeError('Invalid Version: ' + version);
|
||
|
||
this.raw = version;
|
||
|
||
// these are actually numbers
|
||
this.major = +m[1];
|
||
this.minor = +m[2];
|
||
this.patch = +m[3];
|
||
|
||
if (this.major > MAX_SAFE_INTEGER || this.major < 0)
|
||
throw new TypeError('Invalid major version')
|
||
|
||
if (this.minor > MAX_SAFE_INTEGER || this.minor < 0)
|
||
throw new TypeError('Invalid minor version')
|
||
|
||
if (this.patch > MAX_SAFE_INTEGER || this.patch < 0)
|
||
throw new TypeError('Invalid patch version')
|
||
|
||
// numberify any prerelease numeric ids
|
||
if (!m[4])
|
||
this.prerelease = [];
|
||
else
|
||
this.prerelease = m[4].split('.').map(function(id) {
|
||
if (/^[0-9]+$/.test(id)) {
|
||
var num = +id;
|
||
if (num >= 0 && num < MAX_SAFE_INTEGER)
|
||
return num;
|
||
}
|
||
return id;
|
||
});
|
||
|
||
this.build = m[5] ? m[5].split('.') : [];
|
||
this.format();
|
||
}
|
||
|
||
SemVer.prototype.format = function() {
|
||
this.version = this.major + '.' + this.minor + '.' + this.patch;
|
||
if (this.prerelease.length)
|
||
this.version += '-' + this.prerelease.join('.');
|
||
return this.version;
|
||
};
|
||
|
||
SemVer.prototype.toString = function() {
|
||
return this.version;
|
||
};
|
||
|
||
SemVer.prototype.compare = function(other) {
|
||
debug('SemVer.compare', this.version, this.loose, other);
|
||
if (!(other instanceof SemVer))
|
||
other = new SemVer(other, this.loose);
|
||
|
||
return this.compareMain(other) || this.comparePre(other);
|
||
};
|
||
|
||
SemVer.prototype.compareMain = function(other) {
|
||
if (!(other instanceof SemVer))
|
||
other = new SemVer(other, this.loose);
|
||
|
||
return compareIdentifiers(this.major, other.major) ||
|
||
compareIdentifiers(this.minor, other.minor) ||
|
||
compareIdentifiers(this.patch, other.patch);
|
||
};
|
||
|
||
SemVer.prototype.comparePre = function(other) {
|
||
if (!(other instanceof SemVer))
|
||
other = new SemVer(other, this.loose);
|
||
|
||
// NOT having a prerelease is > having one
|
||
if (this.prerelease.length && !other.prerelease.length)
|
||
return -1;
|
||
else if (!this.prerelease.length && other.prerelease.length)
|
||
return 1;
|
||
else if (!this.prerelease.length && !other.prerelease.length)
|
||
return 0;
|
||
|
||
var i = 0;
|
||
do {
|
||
var a = this.prerelease[i];
|
||
var b = other.prerelease[i];
|
||
debug('prerelease compare', i, a, b);
|
||
if (a === undefined && b === undefined)
|
||
return 0;
|
||
else if (b === undefined)
|
||
return 1;
|
||
else if (a === undefined)
|
||
return -1;
|
||
else if (a === b)
|
||
continue;
|
||
else
|
||
return compareIdentifiers(a, b);
|
||
} while (++i);
|
||
};
|
||
|
||
// preminor will bump the version up to the next minor release, and immediately
|
||
// down to pre-release. premajor and prepatch work the same way.
|
||
SemVer.prototype.inc = function(release, identifier) {
|
||
switch (release) {
|
||
case 'premajor':
|
||
this.prerelease.length = 0;
|
||
this.patch = 0;
|
||
this.minor = 0;
|
||
this.major++;
|
||
this.inc('pre', identifier);
|
||
break;
|
||
case 'preminor':
|
||
this.prerelease.length = 0;
|
||
this.patch = 0;
|
||
this.minor++;
|
||
this.inc('pre', identifier);
|
||
break;
|
||
case 'prepatch':
|
||
// If this is already a prerelease, it will bump to the next version
|
||
// drop any prereleases that might already exist, since they are not
|
||
// relevant at this point.
|
||
this.prerelease.length = 0;
|
||
this.inc('patch', identifier);
|
||
this.inc('pre', identifier);
|
||
break;
|
||
// If the input is a non-prerelease version, this acts the same as
|
||
// prepatch.
|
||
case 'prerelease':
|
||
if (this.prerelease.length === 0)
|
||
this.inc('patch', identifier);
|
||
this.inc('pre', identifier);
|
||
break;
|
||
|
||
case 'major':
|
||
// If this is a pre-major version, bump up to the same major version.
|
||
// Otherwise increment major.
|
||
// 1.0.0-5 bumps to 1.0.0
|
||
// 1.1.0 bumps to 2.0.0
|
||
if (this.minor !== 0 || this.patch !== 0 || this.prerelease.length === 0)
|
||
this.major++;
|
||
this.minor = 0;
|
||
this.patch = 0;
|
||
this.prerelease = [];
|
||
break;
|
||
case 'minor':
|
||
// If this is a pre-minor version, bump up to the same minor version.
|
||
// Otherwise increment minor.
|
||
// 1.2.0-5 bumps to 1.2.0
|
||
// 1.2.1 bumps to 1.3.0
|
||
if (this.patch !== 0 || this.prerelease.length === 0)
|
||
this.minor++;
|
||
this.patch = 0;
|
||
this.prerelease = [];
|
||
break;
|
||
case 'patch':
|
||
// If this is not a pre-release version, it will increment the patch.
|
||
// If it is a pre-release it will bump up to the same patch version.
|
||
// 1.2.0-5 patches to 1.2.0
|
||
// 1.2.0 patches to 1.2.1
|
||
if (this.prerelease.length === 0)
|
||
this.patch++;
|
||
this.prerelease = [];
|
||
break;
|
||
// This probably shouldn't be used publicly.
|
||
// 1.0.0 "pre" would become 1.0.0-0 which is the wrong direction.
|
||
case 'pre':
|
||
if (this.prerelease.length === 0)
|
||
this.prerelease = [0];
|
||
else {
|
||
var i = this.prerelease.length;
|
||
while (--i >= 0) {
|
||
if (typeof this.prerelease[i] === 'number') {
|
||
this.prerelease[i]++;
|
||
i = -2;
|
||
}
|
||
}
|
||
if (i === -1) // didn't increment anything
|
||
this.prerelease.push(0);
|
||
}
|
||
if (identifier) {
|
||
// 1.2.0-beta.1 bumps to 1.2.0-beta.2,
|
||
// 1.2.0-beta.fooblz or 1.2.0-beta bumps to 1.2.0-beta.0
|
||
if (this.prerelease[0] === identifier) {
|
||
if (isNaN(this.prerelease[1]))
|
||
this.prerelease = [identifier, 0];
|
||
} else
|
||
this.prerelease = [identifier, 0];
|
||
}
|
||
break;
|
||
|
||
default:
|
||
throw new Error('invalid increment argument: ' + release);
|
||
}
|
||
this.format();
|
||
this.raw = this.version;
|
||
return this;
|
||
};
|
||
|
||
exports.inc = inc;
|
||
function inc(version, release, loose, identifier) {
|
||
if (typeof(loose) === 'string') {
|
||
identifier = loose;
|
||
loose = undefined;
|
||
}
|
||
|
||
try {
|
||
return new SemVer(version, loose).inc(release, identifier).version;
|
||
} catch (er) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
exports.diff = diff;
|
||
function diff(version1, version2) {
|
||
if (eq(version1, version2)) {
|
||
return null;
|
||
} else {
|
||
var v1 = parse(version1);
|
||
var v2 = parse(version2);
|
||
if (v1.prerelease.length || v2.prerelease.length) {
|
||
for (var key in v1) {
|
||
if (key === 'major' || key === 'minor' || key === 'patch') {
|
||
if (v1[key] !== v2[key]) {
|
||
return 'pre'+key;
|
||
}
|
||
}
|
||
}
|
||
return 'prerelease';
|
||
}
|
||
for (var key in v1) {
|
||
if (key === 'major' || key === 'minor' || key === 'patch') {
|
||
if (v1[key] !== v2[key]) {
|
||
return key;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
exports.compareIdentifiers = compareIdentifiers;
|
||
|
||
var numeric = /^[0-9]+$/;
|
||
function compareIdentifiers(a, b) {
|
||
var anum = numeric.test(a);
|
||
var bnum = numeric.test(b);
|
||
|
||
if (anum && bnum) {
|
||
a = +a;
|
||
b = +b;
|
||
}
|
||
|
||
return (anum && !bnum) ? -1 :
|
||
(bnum && !anum) ? 1 :
|
||
a < b ? -1 :
|
||
a > b ? 1 :
|
||
0;
|
||
}
|
||
|
||
exports.rcompareIdentifiers = rcompareIdentifiers;
|
||
function rcompareIdentifiers(a, b) {
|
||
return compareIdentifiers(b, a);
|
||
}
|
||
|
||
exports.major = major;
|
||
function major(a, loose) {
|
||
return new SemVer(a, loose).major;
|
||
}
|
||
|
||
exports.minor = minor;
|
||
function minor(a, loose) {
|
||
return new SemVer(a, loose).minor;
|
||
}
|
||
|
||
exports.patch = patch;
|
||
function patch(a, loose) {
|
||
return new SemVer(a, loose).patch;
|
||
}
|
||
|
||
exports.compare = compare;
|
||
function compare(a, b, loose) {
|
||
return new SemVer(a, loose).compare(new SemVer(b, loose));
|
||
}
|
||
|
||
exports.compareLoose = compareLoose;
|
||
function compareLoose(a, b) {
|
||
return compare(a, b, true);
|
||
}
|
||
|
||
exports.rcompare = rcompare;
|
||
function rcompare(a, b, loose) {
|
||
return compare(b, a, loose);
|
||
}
|
||
|
||
exports.sort = sort;
|
||
function sort(list, loose) {
|
||
return list.sort(function(a, b) {
|
||
return exports.compare(a, b, loose);
|
||
});
|
||
}
|
||
|
||
exports.rsort = rsort;
|
||
function rsort(list, loose) {
|
||
return list.sort(function(a, b) {
|
||
return exports.rcompare(a, b, loose);
|
||
});
|
||
}
|
||
|
||
exports.gt = gt;
|
||
function gt(a, b, loose) {
|
||
return compare(a, b, loose) > 0;
|
||
}
|
||
|
||
exports.lt = lt;
|
||
function lt(a, b, loose) {
|
||
return compare(a, b, loose) < 0;
|
||
}
|
||
|
||
exports.eq = eq;
|
||
function eq(a, b, loose) {
|
||
return compare(a, b, loose) === 0;
|
||
}
|
||
|
||
exports.neq = neq;
|
||
function neq(a, b, loose) {
|
||
return compare(a, b, loose) !== 0;
|
||
}
|
||
|
||
exports.gte = gte;
|
||
function gte(a, b, loose) {
|
||
return compare(a, b, loose) >= 0;
|
||
}
|
||
|
||
exports.lte = lte;
|
||
function lte(a, b, loose) {
|
||
return compare(a, b, loose) <= 0;
|
||
}
|
||
|
||
exports.cmp = cmp;
|
||
function cmp(a, op, b, loose) {
|
||
var ret;
|
||
switch (op) {
|
||
case '===':
|
||
if (typeof a === 'object') a = a.version;
|
||
if (typeof b === 'object') b = b.version;
|
||
ret = a === b;
|
||
break;
|
||
case '!==':
|
||
if (typeof a === 'object') a = a.version;
|
||
if (typeof b === 'object') b = b.version;
|
||
ret = a !== b;
|
||
break;
|
||
case '': case '=': case '==': ret = eq(a, b, loose); break;
|
||
case '!=': ret = neq(a, b, loose); break;
|
||
case '>': ret = gt(a, b, loose); break;
|
||
case '>=': ret = gte(a, b, loose); break;
|
||
case '<': ret = lt(a, b, loose); break;
|
||
case '<=': ret = lte(a, b, loose); break;
|
||
default: throw new TypeError('Invalid operator: ' + op);
|
||
}
|
||
return ret;
|
||
}
|
||
|
||
exports.Comparator = Comparator;
|
||
function Comparator(comp, loose) {
|
||
if (comp instanceof Comparator) {
|
||
if (comp.loose === loose)
|
||
return comp;
|
||
else
|
||
comp = comp.value;
|
||
}
|
||
|
||
if (!(this instanceof Comparator))
|
||
return new Comparator(comp, loose);
|
||
|
||
debug('comparator', comp, loose);
|
||
this.loose = loose;
|
||
this.parse(comp);
|
||
|
||
if (this.semver === ANY)
|
||
this.value = '';
|
||
else
|
||
this.value = this.operator + this.semver.version;
|
||
|
||
debug('comp', this);
|
||
}
|
||
|
||
var ANY = {};
|
||
Comparator.prototype.parse = function(comp) {
|
||
var r = this.loose ? re[COMPARATORLOOSE] : re[COMPARATOR];
|
||
var m = comp.match(r);
|
||
|
||
if (!m)
|
||
throw new TypeError('Invalid comparator: ' + comp);
|
||
|
||
this.operator = m[1];
|
||
if (this.operator === '=')
|
||
this.operator = '';
|
||
|
||
// if it literally is just '>' or '' then allow anything.
|
||
if (!m[2])
|
||
this.semver = ANY;
|
||
else
|
||
this.semver = new SemVer(m[2], this.loose);
|
||
};
|
||
|
||
Comparator.prototype.toString = function() {
|
||
return this.value;
|
||
};
|
||
|
||
Comparator.prototype.test = function(version) {
|
||
debug('Comparator.test', version, this.loose);
|
||
|
||
if (this.semver === ANY)
|
||
return true;
|
||
|
||
if (typeof version === 'string')
|
||
version = new SemVer(version, this.loose);
|
||
|
||
return cmp(version, this.operator, this.semver, this.loose);
|
||
};
|
||
|
||
Comparator.prototype.intersects = function(comp, loose) {
|
||
if (!(comp instanceof Comparator)) {
|
||
throw new TypeError('a Comparator is required');
|
||
}
|
||
|
||
var rangeTmp;
|
||
|
||
if (this.operator === '') {
|
||
rangeTmp = new Range(comp.value, loose);
|
||
return satisfies(this.value, rangeTmp, loose);
|
||
} else if (comp.operator === '') {
|
||
rangeTmp = new Range(this.value, loose);
|
||
return satisfies(comp.semver, rangeTmp, loose);
|
||
}
|
||
|
||
var sameDirectionIncreasing =
|
||
(this.operator === '>=' || this.operator === '>') &&
|
||
(comp.operator === '>=' || comp.operator === '>');
|
||
var sameDirectionDecreasing =
|
||
(this.operator === '<=' || this.operator === '<') &&
|
||
(comp.operator === '<=' || comp.operator === '<');
|
||
var sameSemVer = this.semver.version === comp.semver.version;
|
||
var differentDirectionsInclusive =
|
||
(this.operator === '>=' || this.operator === '<=') &&
|
||
(comp.operator === '>=' || comp.operator === '<=');
|
||
var oppositeDirectionsLessThan =
|
||
cmp(this.semver, '<', comp.semver, loose) &&
|
||
((this.operator === '>=' || this.operator === '>') &&
|
||
(comp.operator === '<=' || comp.operator === '<'));
|
||
var oppositeDirectionsGreaterThan =
|
||
cmp(this.semver, '>', comp.semver, loose) &&
|
||
((this.operator === '<=' || this.operator === '<') &&
|
||
(comp.operator === '>=' || comp.operator === '>'));
|
||
|
||
return sameDirectionIncreasing || sameDirectionDecreasing ||
|
||
(sameSemVer && differentDirectionsInclusive) ||
|
||
oppositeDirectionsLessThan || oppositeDirectionsGreaterThan;
|
||
};
|
||
|
||
|
||
exports.Range = Range;
|
||
function Range(range, loose) {
|
||
if (range instanceof Range) {
|
||
if (range.loose === loose) {
|
||
return range;
|
||
} else {
|
||
return new Range(range.raw, loose);
|
||
}
|
||
}
|
||
|
||
if (range instanceof Comparator) {
|
||
return new Range(range.value, loose);
|
||
}
|
||
|
||
if (!(this instanceof Range))
|
||
return new Range(range, loose);
|
||
|
||
this.loose = loose;
|
||
|
||
// First, split based on boolean or ||
|
||
this.raw = range;
|
||
this.set = range.split(/\s*\|\|\s*/).map(function(range) {
|
||
return this.parseRange(range.trim());
|
||
}, this).filter(function(c) {
|
||
// throw out any that are not relevant for whatever reason
|
||
return c.length;
|
||
});
|
||
|
||
if (!this.set.length) {
|
||
throw new TypeError('Invalid SemVer Range: ' + range);
|
||
}
|
||
|
||
this.format();
|
||
}
|
||
|
||
Range.prototype.format = function() {
|
||
this.range = this.set.map(function(comps) {
|
||
return comps.join(' ').trim();
|
||
}).join('||').trim();
|
||
return this.range;
|
||
};
|
||
|
||
Range.prototype.toString = function() {
|
||
return this.range;
|
||
};
|
||
|
||
Range.prototype.parseRange = function(range) {
|
||
var loose = this.loose;
|
||
range = range.trim();
|
||
debug('range', range, loose);
|
||
// `1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4`
|
||
var hr = loose ? re[HYPHENRANGELOOSE] : re[HYPHENRANGE];
|
||
range = range.replace(hr, hyphenReplace);
|
||
debug('hyphen replace', range);
|
||
// `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5`
|
||
range = range.replace(re[COMPARATORTRIM], comparatorTrimReplace);
|
||
debug('comparator trim', range, re[COMPARATORTRIM]);
|
||
|
||
// `~ 1.2.3` => `~1.2.3`
|
||
range = range.replace(re[TILDETRIM], tildeTrimReplace);
|
||
|
||
// `^ 1.2.3` => `^1.2.3`
|
||
range = range.replace(re[CARETTRIM], caretTrimReplace);
|
||
|
||
// normalize spaces
|
||
range = range.split(/\s+/).join(' ');
|
||
|
||
// At this point, the range is completely trimmed and
|
||
// ready to be split into comparators.
|
||
|
||
var compRe = loose ? re[COMPARATORLOOSE] : re[COMPARATOR];
|
||
var set = range.split(' ').map(function(comp) {
|
||
return parseComparator(comp, loose);
|
||
}).join(' ').split(/\s+/);
|
||
if (this.loose) {
|
||
// in loose mode, throw out any that are not valid comparators
|
||
set = set.filter(function(comp) {
|
||
return !!comp.match(compRe);
|
||
});
|
||
}
|
||
set = set.map(function(comp) {
|
||
return new Comparator(comp, loose);
|
||
});
|
||
|
||
return set;
|
||
};
|
||
|
||
Range.prototype.intersects = function(range, loose) {
|
||
if (!(range instanceof Range)) {
|
||
throw new TypeError('a Range is required');
|
||
}
|
||
|
||
return this.set.some(function(thisComparators) {
|
||
return thisComparators.every(function(thisComparator) {
|
||
return range.set.some(function(rangeComparators) {
|
||
return rangeComparators.every(function(rangeComparator) {
|
||
return thisComparator.intersects(rangeComparator, loose);
|
||
});
|
||
});
|
||
});
|
||
});
|
||
};
|
||
|
||
// Mostly just for testing and legacy API reasons
|
||
exports.toComparators = toComparators;
|
||
function toComparators(range, loose) {
|
||
return new Range(range, loose).set.map(function(comp) {
|
||
return comp.map(function(c) {
|
||
return c.value;
|
||
}).join(' ').trim().split(' ');
|
||
});
|
||
}
|
||
|
||
// comprised of xranges, tildes, stars, and gtlt's at this point.
|
||
// already replaced the hyphen ranges
|
||
// turn into a set of JUST comparators.
|
||
function parseComparator(comp, loose) {
|
||
debug('comp', comp);
|
||
comp = replaceCarets(comp, loose);
|
||
debug('caret', comp);
|
||
comp = replaceTildes(comp, loose);
|
||
debug('tildes', comp);
|
||
comp = replaceXRanges(comp, loose);
|
||
debug('xrange', comp);
|
||
comp = replaceStars(comp, loose);
|
||
debug('stars', comp);
|
||
return comp;
|
||
}
|
||
|
||
function isX(id) {
|
||
return !id || id.toLowerCase() === 'x' || id === '*';
|
||
}
|
||
|
||
// ~, ~> --> * (any, kinda silly)
|
||
// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0
|
||
// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0
|
||
// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0 <1.3.0
|
||
// ~1.2.3, ~>1.2.3 --> >=1.2.3 <1.3.0
|
||
// ~1.2.0, ~>1.2.0 --> >=1.2.0 <1.3.0
|
||
function replaceTildes(comp, loose) {
|
||
return comp.trim().split(/\s+/).map(function(comp) {
|
||
return replaceTilde(comp, loose);
|
||
}).join(' ');
|
||
}
|
||
|
||
function replaceTilde(comp, loose) {
|
||
var r = loose ? re[TILDELOOSE] : re[TILDE];
|
||
return comp.replace(r, function(_, M, m, p, pr) {
|
||
debug('tilde', comp, _, M, m, p, pr);
|
||
var ret;
|
||
|
||
if (isX(M))
|
||
ret = '';
|
||
else if (isX(m))
|
||
ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0';
|
||
else if (isX(p))
|
||
// ~1.2 == >=1.2.0 <1.3.0
|
||
ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0';
|
||
else if (pr) {
|
||
debug('replaceTilde pr', pr);
|
||
if (pr.charAt(0) !== '-')
|
||
pr = '-' + pr;
|
||
ret = '>=' + M + '.' + m + '.' + p + pr +
|
||
' <' + M + '.' + (+m + 1) + '.0';
|
||
} else
|
||
// ~1.2.3 == >=1.2.3 <1.3.0
|
||
ret = '>=' + M + '.' + m + '.' + p +
|
||
' <' + M + '.' + (+m + 1) + '.0';
|
||
|
||
debug('tilde return', ret);
|
||
return ret;
|
||
});
|
||
}
|
||
|
||
// ^ --> * (any, kinda silly)
|
||
// ^2, ^2.x, ^2.x.x --> >=2.0.0 <3.0.0
|
||
// ^2.0, ^2.0.x --> >=2.0.0 <3.0.0
|
||
// ^1.2, ^1.2.x --> >=1.2.0 <2.0.0
|
||
// ^1.2.3 --> >=1.2.3 <2.0.0
|
||
// ^1.2.0 --> >=1.2.0 <2.0.0
|
||
function replaceCarets(comp, loose) {
|
||
return comp.trim().split(/\s+/).map(function(comp) {
|
||
return replaceCaret(comp, loose);
|
||
}).join(' ');
|
||
}
|
||
|
||
function replaceCaret(comp, loose) {
|
||
debug('caret', comp, loose);
|
||
var r = loose ? re[CARETLOOSE] : re[CARET];
|
||
return comp.replace(r, function(_, M, m, p, pr) {
|
||
debug('caret', comp, _, M, m, p, pr);
|
||
var ret;
|
||
|
||
if (isX(M))
|
||
ret = '';
|
||
else if (isX(m))
|
||
ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0';
|
||
else if (isX(p)) {
|
||
if (M === '0')
|
||
ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0';
|
||
else
|
||
ret = '>=' + M + '.' + m + '.0 <' + (+M + 1) + '.0.0';
|
||
} else if (pr) {
|
||
debug('replaceCaret pr', pr);
|
||
if (pr.charAt(0) !== '-')
|
||
pr = '-' + pr;
|
||
if (M === '0') {
|
||
if (m === '0')
|
||
ret = '>=' + M + '.' + m + '.' + p + pr +
|
||
' <' + M + '.' + m + '.' + (+p + 1);
|
||
else
|
||
ret = '>=' + M + '.' + m + '.' + p + pr +
|
||
' <' + M + '.' + (+m + 1) + '.0';
|
||
} else
|
||
ret = '>=' + M + '.' + m + '.' + p + pr +
|
||
' <' + (+M + 1) + '.0.0';
|
||
} else {
|
||
debug('no pr');
|
||
if (M === '0') {
|
||
if (m === '0')
|
||
ret = '>=' + M + '.' + m + '.' + p +
|
||
' <' + M + '.' + m + '.' + (+p + 1);
|
||
else
|
||
ret = '>=' + M + '.' + m + '.' + p +
|
||
' <' + M + '.' + (+m + 1) + '.0';
|
||
} else
|
||
ret = '>=' + M + '.' + m + '.' + p +
|
||
' <' + (+M + 1) + '.0.0';
|
||
}
|
||
|
||
debug('caret return', ret);
|
||
return ret;
|
||
});
|
||
}
|
||
|
||
function replaceXRanges(comp, loose) {
|
||
debug('replaceXRanges', comp, loose);
|
||
return comp.split(/\s+/).map(function(comp) {
|
||
return replaceXRange(comp, loose);
|
||
}).join(' ');
|
||
}
|
||
|
||
function replaceXRange(comp, loose) {
|
||
comp = comp.trim();
|
||
var r = loose ? re[XRANGELOOSE] : re[XRANGE];
|
||
return comp.replace(r, function(ret, gtlt, M, m, p, pr) {
|
||
debug('xRange', comp, ret, gtlt, M, m, p, pr);
|
||
var xM = isX(M);
|
||
var xm = xM || isX(m);
|
||
var xp = xm || isX(p);
|
||
var anyX = xp;
|
||
|
||
if (gtlt === '=' && anyX)
|
||
gtlt = '';
|
||
|
||
if (xM) {
|
||
if (gtlt === '>' || gtlt === '<') {
|
||
// nothing is allowed
|
||
ret = '<0.0.0';
|
||
} else {
|
||
// nothing is forbidden
|
||
ret = '*';
|
||
}
|
||
} else if (gtlt && anyX) {
|
||
// replace X with 0
|
||
if (xm)
|
||
m = 0;
|
||
if (xp)
|
||
p = 0;
|
||
|
||
if (gtlt === '>') {
|
||
// >1 => >=2.0.0
|
||
// >1.2 => >=1.3.0
|
||
// >1.2.3 => >= 1.2.4
|
||
gtlt = '>=';
|
||
if (xm) {
|
||
M = +M + 1;
|
||
m = 0;
|
||
p = 0;
|
||
} else if (xp) {
|
||
m = +m + 1;
|
||
p = 0;
|
||
}
|
||
} else if (gtlt === '<=') {
|
||
// <=0.7.x is actually <0.8.0, since any 0.7.x should
|
||
// pass. Similarly, <=7.x is actually <8.0.0, etc.
|
||
gtlt = '<';
|
||
if (xm)
|
||
M = +M + 1;
|
||
else
|
||
m = +m + 1;
|
||
}
|
||
|
||
ret = gtlt + M + '.' + m + '.' + p;
|
||
} else if (xm) {
|
||
ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0';
|
||
} else if (xp) {
|
||
ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0';
|
||
}
|
||
|
||
debug('xRange return', ret);
|
||
|
||
return ret;
|
||
});
|
||
}
|
||
|
||
// Because * is AND-ed with everything else in the comparator,
|
||
// and '' means "any version", just remove the *s entirely.
|
||
function replaceStars(comp, loose) {
|
||
debug('replaceStars', comp, loose);
|
||
// Looseness is ignored here. star is always as loose as it gets!
|
||
return comp.trim().replace(re[STAR], '');
|
||
}
|
||
|
||
// This function is passed to string.replace(re[HYPHENRANGE])
|
||
// M, m, patch, prerelease, build
|
||
// 1.2 - 3.4.5 => >=1.2.0 <=3.4.5
|
||
// 1.2.3 - 3.4 => >=1.2.0 <3.5.0 Any 3.4.x will do
|
||
// 1.2 - 3.4 => >=1.2.0 <3.5.0
|
||
function hyphenReplace($0,
|
||
from, fM, fm, fp, fpr, fb,
|
||
to, tM, tm, tp, tpr, tb) {
|
||
|
||
if (isX(fM))
|
||
from = '';
|
||
else if (isX(fm))
|
||
from = '>=' + fM + '.0.0';
|
||
else if (isX(fp))
|
||
from = '>=' + fM + '.' + fm + '.0';
|
||
else
|
||
from = '>=' + from;
|
||
|
||
if (isX(tM))
|
||
to = '';
|
||
else if (isX(tm))
|
||
to = '<' + (+tM + 1) + '.0.0';
|
||
else if (isX(tp))
|
||
to = '<' + tM + '.' + (+tm + 1) + '.0';
|
||
else if (tpr)
|
||
to = '<=' + tM + '.' + tm + '.' + tp + '-' + tpr;
|
||
else
|
||
to = '<=' + to;
|
||
|
||
return (from + ' ' + to).trim();
|
||
}
|
||
|
||
|
||
// if ANY of the sets match ALL of its comparators, then pass
|
||
Range.prototype.test = function(version) {
|
||
if (!version)
|
||
return false;
|
||
|
||
if (typeof version === 'string')
|
||
version = new SemVer(version, this.loose);
|
||
|
||
for (var i = 0; i < this.set.length; i++) {
|
||
if (testSet(this.set[i], version))
|
||
return true;
|
||
}
|
||
return false;
|
||
};
|
||
|
||
function testSet(set, version) {
|
||
for (var i = 0; i < set.length; i++) {
|
||
if (!set[i].test(version))
|
||
return false;
|
||
}
|
||
|
||
if (version.prerelease.length) {
|
||
// Find the set of versions that are allowed to have prereleases
|
||
// For example, ^1.2.3-pr.1 desugars to >=1.2.3-pr.1 <2.0.0
|
||
// That should allow `1.2.3-pr.2` to pass.
|
||
// However, `1.2.4-alpha.notready` should NOT be allowed,
|
||
// even though it's within the range set by the comparators.
|
||
for (var i = 0; i < set.length; i++) {
|
||
debug(set[i].semver);
|
||
if (set[i].semver === ANY)
|
||
continue;
|
||
|
||
if (set[i].semver.prerelease.length > 0) {
|
||
var allowed = set[i].semver;
|
||
if (allowed.major === version.major &&
|
||
allowed.minor === version.minor &&
|
||
allowed.patch === version.patch)
|
||
return true;
|
||
}
|
||
}
|
||
|
||
// Version has a -pre, but it's not one of the ones we like.
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
exports.satisfies = satisfies;
|
||
function satisfies(version, range, loose) {
|
||
try {
|
||
range = new Range(range, loose);
|
||
} catch (er) {
|
||
return false;
|
||
}
|
||
return range.test(version);
|
||
}
|
||
|
||
exports.maxSatisfying = maxSatisfying;
|
||
function maxSatisfying(versions, range, loose) {
|
||
var max = null;
|
||
var maxSV = null;
|
||
try {
|
||
var rangeObj = new Range(range, loose);
|
||
} catch (er) {
|
||
return null;
|
||
}
|
||
versions.forEach(function (v) {
|
||
if (rangeObj.test(v)) { // satisfies(v, range, loose)
|
||
if (!max || maxSV.compare(v) === -1) { // compare(max, v, true)
|
||
max = v;
|
||
maxSV = new SemVer(max, loose);
|
||
}
|
||
}
|
||
});
|
||
return max;
|
||
}
|
||
|
||
exports.minSatisfying = minSatisfying;
|
||
function minSatisfying(versions, range, loose) {
|
||
var min = null;
|
||
var minSV = null;
|
||
try {
|
||
var rangeObj = new Range(range, loose);
|
||
} catch (er) {
|
||
return null;
|
||
}
|
||
versions.forEach(function (v) {
|
||
if (rangeObj.test(v)) { // satisfies(v, range, loose)
|
||
if (!min || minSV.compare(v) === 1) { // compare(min, v, true)
|
||
min = v;
|
||
minSV = new SemVer(min, loose);
|
||
}
|
||
}
|
||
});
|
||
return min;
|
||
}
|
||
|
||
exports.validRange = validRange;
|
||
function validRange(range, loose) {
|
||
try {
|
||
// Return '*' instead of '' so that truthiness works.
|
||
// This will throw if it's invalid anyway
|
||
return new Range(range, loose).range || '*';
|
||
} catch (er) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// Determine if version is less than all the versions possible in the range
|
||
exports.ltr = ltr;
|
||
function ltr(version, range, loose) {
|
||
return outside(version, range, '<', loose);
|
||
}
|
||
|
||
// Determine if version is greater than all the versions possible in the range.
|
||
exports.gtr = gtr;
|
||
function gtr(version, range, loose) {
|
||
return outside(version, range, '>', loose);
|
||
}
|
||
|
||
exports.outside = outside;
|
||
function outside(version, range, hilo, loose) {
|
||
version = new SemVer(version, loose);
|
||
range = new Range(range, loose);
|
||
|
||
var gtfn, ltefn, ltfn, comp, ecomp;
|
||
switch (hilo) {
|
||
case '>':
|
||
gtfn = gt;
|
||
ltefn = lte;
|
||
ltfn = lt;
|
||
comp = '>';
|
||
ecomp = '>=';
|
||
break;
|
||
case '<':
|
||
gtfn = lt;
|
||
ltefn = gte;
|
||
ltfn = gt;
|
||
comp = '<';
|
||
ecomp = '<=';
|
||
break;
|
||
default:
|
||
throw new TypeError('Must provide a hilo val of "<" or ">"');
|
||
}
|
||
|
||
// If it satisifes the range it is not outside
|
||
if (satisfies(version, range, loose)) {
|
||
return false;
|
||
}
|
||
|
||
// From now on, variable terms are as if we're in "gtr" mode.
|
||
// but note that everything is flipped for the "ltr" function.
|
||
|
||
for (var i = 0; i < range.set.length; ++i) {
|
||
var comparators = range.set[i];
|
||
|
||
var high = null;
|
||
var low = null;
|
||
|
||
comparators.forEach(function(comparator) {
|
||
if (comparator.semver === ANY) {
|
||
comparator = new Comparator('>=0.0.0');
|
||
}
|
||
high = high || comparator;
|
||
low = low || comparator;
|
||
if (gtfn(comparator.semver, high.semver, loose)) {
|
||
high = comparator;
|
||
} else if (ltfn(comparator.semver, low.semver, loose)) {
|
||
low = comparator;
|
||
}
|
||
});
|
||
|
||
// If the edge version comparator has a operator then our version
|
||
// isn't outside it
|
||
if (high.operator === comp || high.operator === ecomp) {
|
||
return false;
|
||
}
|
||
|
||
// If the lowest version comparator has an operator and our version
|
||
// is less than it then it isn't higher than the range
|
||
if ((!low.operator || low.operator === comp) &&
|
||
ltefn(version, low.semver)) {
|
||
return false;
|
||
} else if (low.operator === ecomp && ltfn(version, low.semver)) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
exports.prerelease = prerelease;
|
||
function prerelease(version, loose) {
|
||
var parsed = parse(version, loose);
|
||
return (parsed && parsed.prerelease.length) ? parsed.prerelease : null;
|
||
}
|
||
|
||
exports.intersects = intersects;
|
||
function intersects(r1, r2, loose) {
|
||
r1 = new Range(r1, loose);
|
||
r2 = new Range(r2, loose);
|
||
return r1.intersects(r2)
|
||
}
|
||
|
||
exports.coerce = coerce;
|
||
function coerce(version) {
|
||
if (version instanceof SemVer)
|
||
return version;
|
||
|
||
if (typeof version !== 'string')
|
||
return null;
|
||
|
||
var match = version.match(re[COERCE]);
|
||
|
||
if (match == null)
|
||
return null;
|
||
|
||
return parse((match[1] || '0') + '.' + (match[2] || '0') + '.' + (match[3] || '0'));
|
||
}
|
||
|
||
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../process/browser.js */ "./node_modules/process/browser.js")));
|
||
|
||
/***/ }),
|
||
|
||
/***/ "./node_modules/webpack/buildin/harmony-module.js":
|
||
/*!*******************************************!*\
|
||
!*** (webpack)/buildin/harmony-module.js ***!
|
||
\*******************************************/
|
||
/*! no static exports found */
|
||
/***/ (function(module, exports) {
|
||
|
||
module.exports = function(originalModule) {
|
||
if (!originalModule.webpackPolyfill) {
|
||
var module = Object.create(originalModule);
|
||
// module.parent = undefined by default
|
||
if (!module.children) module.children = [];
|
||
Object.defineProperty(module, "loaded", {
|
||
enumerable: true,
|
||
get: function() {
|
||
return module.l;
|
||
}
|
||
});
|
||
Object.defineProperty(module, "id", {
|
||
enumerable: true,
|
||
get: function() {
|
||
return module.i;
|
||
}
|
||
});
|
||
Object.defineProperty(module, "exports", {
|
||
enumerable: true
|
||
});
|
||
module.webpackPolyfill = 1;
|
||
}
|
||
return module;
|
||
};
|
||
|
||
|
||
/***/ }),
|
||
|
||
/***/ "./src/rtc_reporting/reporting_agent.ts":
|
||
/*!**********************************************!*\
|
||
!*** ./src/rtc_reporting/reporting_agent.ts ***!
|
||
\**********************************************/
|
||
/*! exports provided: default */
|
||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||
__webpack_require__.r(__webpack_exports__);
|
||
/* harmony import */ var _reporting_reporter__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./reporting_reporter */ "./src/rtc_reporting/reporting_reporter.ts");
|
||
/* harmony import */ var _reporting_event__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./reporting_event */ "./src/rtc_reporting/reporting_event.ts");
|
||
/* harmony import */ var _reporting_storebag__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./reporting_storebag */ "./src/rtc_reporting/reporting_storebag.ts");
|
||
/* harmony import */ var _reporting_config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./reporting_config */ "./src/rtc_reporting/reporting_config.ts");
|
||
|
||
|
||
|
||
const { RTCReportingStoreBag } = _reporting_storebag__WEBPACK_IMPORTED_MODULE_2__;
|
||
const ReportingVersion = '1';
|
||
|
||
class RTCObjectRef {
|
||
constructor(objectNotToDestroy = null) {
|
||
this._objectNotToDestroy = objectNotToDestroy;
|
||
this._objectToDestroy = null;
|
||
}
|
||
addInstantiatedObject(objectToDestroy) {
|
||
this._objectNotToDestroy = this._objectToDestroy = objectToDestroy;
|
||
}
|
||
get instance() {
|
||
return this._objectNotToDestroy;
|
||
}
|
||
destroy() {
|
||
if (this._objectToDestroy) {
|
||
this._objectToDestroy.destroy();
|
||
}
|
||
this._objectNotToDestroy = this._objectNotToDestroy = null;
|
||
}
|
||
}
|
||
class RTCReportingAgent {
|
||
constructor(reportingConfig = {}) {
|
||
this._sessionInfo = {};
|
||
this._reportingDisabled = false;
|
||
this.storebag = new RTCObjectRef();
|
||
this.reporter = null;
|
||
this.reportingVersion = ReportingVersion;
|
||
this.fuzzFactor = 0;
|
||
this.sessionStartTime = 0;
|
||
this._initializeReportingAgent(reportingConfig);
|
||
}
|
||
destroy() {
|
||
this.endSession();
|
||
}
|
||
get ReportingAllowed() {
|
||
return !(this._reportingDisabled);
|
||
}
|
||
static get version() {
|
||
return "0.1.36";
|
||
}
|
||
issueReportingEvent(eventID, eventPayload, eventType = _reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentEventTypes"].BatchEvent) {
|
||
let error = null;
|
||
if (this.ReportingAllowed && this.reporter) {
|
||
let event = new _reporting_event__WEBPACK_IMPORTED_MODULE_1__["RTCReportingEvent"](eventID, _reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingSessionEventStatus"].EventStatusSessionBegin, eventPayload, this._FuzzFactor, eventType);
|
||
error = this.reporter.issueReportingEvent(event, this._sessionInfo);
|
||
}
|
||
else {
|
||
error = new _reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingError"]('Reporting not allowed.');
|
||
}
|
||
return error;
|
||
}
|
||
endSession() {
|
||
if (this.ReportingAllowed && this.reporter) {
|
||
this._issueSessionEndEvent();
|
||
this.reporter.flushAllEvents();
|
||
}
|
||
this._sessionInfo = null;
|
||
this.reporter = null;
|
||
this.storebag.destroy();
|
||
this.storebag = null;
|
||
}
|
||
set SessionInfo(session_info) {
|
||
this._sessionInfo = session_info;
|
||
}
|
||
get SessionInfo() {
|
||
return this._sessionInfo;
|
||
}
|
||
get SessionID() {
|
||
if (_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingEventMetadataKeys"].SessionID in this._sessionInfo) {
|
||
return this._sessionInfo[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingEventMetadataKeys"].SessionID];
|
||
}
|
||
return '';
|
||
}
|
||
get ClientName() {
|
||
if (_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ClientName in this._sessionInfo) {
|
||
return this._sessionInfo[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ClientName];
|
||
}
|
||
return '';
|
||
}
|
||
get ServiceName() {
|
||
if (_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ServiceName in this._sessionInfo) {
|
||
return this._sessionInfo[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ServiceName];
|
||
}
|
||
return '';
|
||
}
|
||
get _FuzzFactor() {
|
||
return this.fuzzFactor;
|
||
}
|
||
_initializeReportingAgent(reportingConfig) {
|
||
let clientName = null;
|
||
let serviceName = null;
|
||
let applicationName = '';
|
||
let sender = null;
|
||
let deviceName = null;
|
||
let osVersion = null;
|
||
let storebagURL = null;
|
||
let reportingDisabled = false;
|
||
let startTime = (new Date()).getTime();
|
||
let hlsEventEmitter = null;
|
||
let sessionID = null;
|
||
let userInfoDict = null;
|
||
let senderOSVersion = null;
|
||
let storeBag = null;
|
||
let senderServiceID = null;
|
||
let senderAltConfig = null;
|
||
let useHTTPHeaders = null;
|
||
if (!this._sessionInfo) {
|
||
this._sessionInfo = {};
|
||
}
|
||
if (reportingConfig) {
|
||
if (_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ClientName in reportingConfig) {
|
||
clientName = reportingConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ClientName];
|
||
}
|
||
if (_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ServiceName in reportingConfig) {
|
||
serviceName = reportingConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ServiceName];
|
||
}
|
||
if (_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ApplicationName in reportingConfig) {
|
||
applicationName = reportingConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ApplicationName];
|
||
}
|
||
if (_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].Sender in reportingConfig) {
|
||
sender = reportingConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].Sender];
|
||
}
|
||
if (_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingEventMetadataKeys"].OSVersion in reportingConfig) {
|
||
osVersion = reportingConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingEventMetadataKeys"].OSVersion];
|
||
}
|
||
if (_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].StorebagURL in reportingConfig) {
|
||
storebagURL = reportingConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].StorebagURL];
|
||
}
|
||
if (_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].DeviceName in reportingConfig) {
|
||
deviceName = reportingConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].DeviceName];
|
||
}
|
||
if (_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ReportingDisabled in reportingConfig) {
|
||
reportingDisabled = reportingConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ReportingDisabled];
|
||
}
|
||
if (_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].HLSEventEmitter in reportingConfig) {
|
||
hlsEventEmitter = reportingConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].HLSEventEmitter];
|
||
}
|
||
if (_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingEventMetadataKeys"].SessionID in reportingConfig) {
|
||
sessionID = reportingConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingEventMetadataKeys"].SessionID];
|
||
}
|
||
if (_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].UserInfoDict in reportingConfig) {
|
||
userInfoDict = reportingConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].UserInfoDict];
|
||
if (userInfoDict && _reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].SenderAltConfig in userInfoDict) {
|
||
senderAltConfig = userInfoDict[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].SenderAltConfig];
|
||
delete userInfoDict[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].SenderAltConfig];
|
||
if (senderAltConfig) {
|
||
if (_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ServiceIdentifier in senderAltConfig) {
|
||
senderServiceID = senderAltConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ServiceIdentifier];
|
||
}
|
||
if (Object.keys(senderAltConfig)) {
|
||
Object.assign(userInfoDict, senderAltConfig);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingEventMetadataKeys"].SenderOSVersion in reportingConfig) {
|
||
senderOSVersion = reportingConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingEventMetadataKeys"].SenderOSVersion];
|
||
}
|
||
if (_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ReportingStoreBag in reportingConfig) {
|
||
storeBag = reportingConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ReportingStoreBag];
|
||
}
|
||
if (_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].UseHTTPHeaders in reportingConfig) {
|
||
useHTTPHeaders = reportingConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].UseHTTPHeaders];
|
||
}
|
||
}
|
||
if (!sessionID) {
|
||
sessionID = RTCReportingAgent._createSessionID();
|
||
}
|
||
this._sessionInfo[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ClientName] = clientName;
|
||
this._sessionInfo[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ServiceName] = serviceName;
|
||
this._sessionInfo[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ApplicationName] = applicationName;
|
||
this._sessionInfo[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].DeviceName] = deviceName;
|
||
this._sessionInfo[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].OSVersion] = osVersion;
|
||
this._sessionInfo[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingEventMetadataKeys"].Sender] = sender;
|
||
this._sessionInfo[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingEventMetadataKeys"].RTCSchemeVersion] = this.reportingVersion;
|
||
this._sessionInfo[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingEventMetadataKeys"].RTCReportingVersion] = RTCReportingAgent.version;
|
||
this._reportingDisabled = reportingDisabled;
|
||
this._sessionInfo[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingEventMetadataKeys"].SessionID] = sessionID;
|
||
this._sessionInfo[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingEventMetadataKeys"].SenderOSVersion] = senderOSVersion;
|
||
if (applicationName && serviceName) {
|
||
serviceName = serviceName + '.' + applicationName;
|
||
}
|
||
if (senderServiceID && serviceName) {
|
||
serviceName = serviceName + '.' + senderServiceID;
|
||
}
|
||
this._sessionInfo[_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"].ServiceName] = serviceName;
|
||
this.fuzzFactor = startTime - Math.floor(startTime / 60000) * 60000;
|
||
this.sessionStartTime = startTime - this.fuzzFactor;
|
||
Object.keys(this._sessionInfo).forEach((key) => (this._sessionInfo[key] == null) && delete this._sessionInfo[key]);
|
||
if (userInfoDict && Object.keys(userInfoDict)) {
|
||
Object.assign(this._sessionInfo, userInfoDict);
|
||
}
|
||
if (this._reportingDisabled) {
|
||
return;
|
||
}
|
||
if (storeBag) {
|
||
this.storebag = new RTCObjectRef(storeBag);
|
||
}
|
||
else {
|
||
this.storebag.addInstantiatedObject(new RTCReportingStoreBag(storebagURL, clientName, serviceName, applicationName, deviceName, osVersion));
|
||
}
|
||
this.reporter = new _reporting_reporter__WEBPACK_IMPORTED_MODULE_0__["RTCReporter"](clientName, serviceName, sender, this.storebag.instance, this.reportingVersion, hlsEventEmitter, applicationName, senderServiceID, useHTTPHeaders, sessionID);
|
||
console.log(_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentDefaultConfig"].LogMsgPrefix + 'Created reporting agent using rtc.js version ' + RTCReportingAgent.version);
|
||
this._issueSessionBeginEvent();
|
||
}
|
||
_issueSessionBeginEvent() {
|
||
let event = new _reporting_event__WEBPACK_IMPORTED_MODULE_1__["RTCReportingEvent"](_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentMethods"].ReportingSessionMethod, _reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingSessionEventStatus"].EventStatusSessionBegin, null, this._FuzzFactor, _reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentEventTypes"].BatchEvent);
|
||
if (this.reporter && event) {
|
||
this.reporter.issueReportingEvent(event, this.SessionInfo);
|
||
}
|
||
}
|
||
_issueSessionEndEvent() {
|
||
let event = new _reporting_event__WEBPACK_IMPORTED_MODULE_1__["RTCReportingEvent"](_reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentMethods"].ReportingSessionMethod, _reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingSessionEventStatus"].EventStatusSessionEnd, null, this._FuzzFactor, _reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentEventTypes"].RealTimeEvent);
|
||
if (this.reporter) {
|
||
this.reporter.issueReportingEvent(event, this.SessionInfo);
|
||
}
|
||
}
|
||
static _createSessionID() {
|
||
let word0 = Math.random() * 0xffffffff | 0;
|
||
let word1 = Math.random() * 0xffffffff | 0;
|
||
let word2 = Math.random() * 0xffffffff | 0;
|
||
let word3 = Math.random() * 0xffffffff | 0;
|
||
const lastNibble = 0x0f;
|
||
const lastByte = 0xff;
|
||
const secondByteShift = 8;
|
||
const thirdByteShift = 16;
|
||
const fourthByteShift = 24;
|
||
const uuidVersion = 0x40;
|
||
const uuidVariant = 0x80;
|
||
const nByteMask = 0x3f;
|
||
let guid = [];
|
||
let radix = 16;
|
||
let guidLength = 256;
|
||
for (let i = 0; i < guidLength; ++i) {
|
||
guid[i] = (i < radix ? '0' : '') + (i).toString(radix);
|
||
}
|
||
return guid[word0 & lastByte] + guid[word0 >> secondByteShift & lastByte] + guid[word0 >> thirdByteShift & lastByte] + guid[word0 >> fourthByteShift & lastByte] + '-' +
|
||
guid[word1 & lastByte] + guid[word1 >> secondByteShift & lastByte] + '-' +
|
||
guid[word1 >> secondByteShift & lastNibble | uuidVersion] + guid[word1 >> thirdByteShift & lastByte] + '-' +
|
||
guid[word2 & nByteMask | uuidVariant] + guid[word2 >> secondByteShift & lastByte] + '-' +
|
||
guid[word2 >> secondByteShift & lastByte] + guid[word2 >> thirdByteShift & lastByte] +
|
||
guid[word3 & lastByte] + guid[word3 >> secondByteShift & lastByte] + guid[word3 >> thirdByteShift & lastByte] + guid[word3 >> fourthByteShift & lastByte];
|
||
}
|
||
}
|
||
/* harmony default export */ __webpack_exports__["default"] = ({
|
||
RTCReportingAgent,
|
||
RTCStorebag: _reporting_storebag__WEBPACK_IMPORTED_MODULE_2__,
|
||
RTCReportingAgentDefaultConfig: _reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentDefaultConfig"],
|
||
RTCReportingAgentConfigKeys: _reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentConfigKeys"],
|
||
RTCReportingAgentEventTypes: _reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingAgentEventTypes"],
|
||
RTCReportingHTTPHeaderDefault: _reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingHTTPHeaderDefault"],
|
||
RTCReportingHTTPHeaderKeys: _reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingHTTPHeaderKeys"],
|
||
RTCReportingEventMetadataKeys: _reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingEventMetadataKeys"],
|
||
RTC_REPORTING_EVENTS_DISPATCHED: _reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTC_REPORTING_EVENTS_DISPATCHED"],
|
||
RTCObjectRef,
|
||
RTCReportingStoreBagConfig: _reporting_config__WEBPACK_IMPORTED_MODULE_3__["RTCReportingStoreBagConfig"],
|
||
});
|
||
|
||
|
||
/***/ }),
|
||
|
||
/***/ "./src/rtc_reporting/reporting_config.ts":
|
||
/*!***********************************************!*\
|
||
!*** ./src/rtc_reporting/reporting_config.ts ***!
|
||
\***********************************************/
|
||
/*! exports provided: RTCReportingAgentDefaultConfig, RTCReportingAgentConfigKeys, RTCReportingAgentEventTypes, RTCReportingHTTPHeaderKeys, RTCReportingHTTPHeaderDefault, RTCReportingEventMetadataKeys, RTCReportingStoreBagConfig, RTCReportingReporterConfig, RTCReportingAgentMethods, RTCReportingSessionEventStatus, RTCReportingStoreBagKeys, RTCReportingError, RTCReportingBuildTypes, RTC_REPORTING_EVENTS_DISPATCHED */
|
||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||
__webpack_require__.r(__webpack_exports__);
|
||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RTCReportingAgentDefaultConfig", function() { return RTCReportingAgentDefaultConfig; });
|
||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RTCReportingAgentConfigKeys", function() { return RTCReportingAgentConfigKeys; });
|
||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RTCReportingAgentEventTypes", function() { return RTCReportingAgentEventTypes; });
|
||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RTCReportingHTTPHeaderKeys", function() { return RTCReportingHTTPHeaderKeys; });
|
||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RTCReportingHTTPHeaderDefault", function() { return RTCReportingHTTPHeaderDefault; });
|
||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RTCReportingEventMetadataKeys", function() { return RTCReportingEventMetadataKeys; });
|
||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RTCReportingStoreBagConfig", function() { return RTCReportingStoreBagConfig; });
|
||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RTCReportingReporterConfig", function() { return RTCReportingReporterConfig; });
|
||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RTCReportingAgentMethods", function() { return RTCReportingAgentMethods; });
|
||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RTCReportingSessionEventStatus", function() { return RTCReportingSessionEventStatus; });
|
||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RTCReportingStoreBagKeys", function() { return RTCReportingStoreBagKeys; });
|
||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RTCReportingError", function() { return RTCReportingError; });
|
||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RTCReportingBuildTypes", function() { return RTCReportingBuildTypes; });
|
||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RTC_REPORTING_EVENTS_DISPATCHED", function() { return RTC_REPORTING_EVENTS_DISPATCHED; });
|
||
const RTCReportingHTTPHeaderKeys = {
|
||
ContentType: 'ContentType',
|
||
InternalBuild: 'X-RTC-Internal-Build',
|
||
ClientName: 'X-RTC-Client-Name',
|
||
ServiceName: 'X-RTC-Service-Name',
|
||
ClientVersion: 'X-RTC-Client-Version',
|
||
Sender: 'X-RTC-Sender',
|
||
UserAgent: 'User-Agent',
|
||
SessionID: 'X-RTC-Session-ID',
|
||
};
|
||
const RTCReportingHTTPHeaderDefault = {
|
||
ContentTypekey: 'application/json;charset=UTF-8',
|
||
};
|
||
const RTCReportingEventMetadataKeys = {
|
||
EventID: '_method',
|
||
EventType: '_eventType',
|
||
EventNumber: '_eventNumber',
|
||
EventTime: 'eventTime',
|
||
Sender: '_sender',
|
||
Status: '_status',
|
||
ClientTS: '_clientTS',
|
||
TimeZoneOffset: '_timezoneOffset',
|
||
PostTime: 'postTime',
|
||
Events: 'events',
|
||
SessionID: '_sessionID',
|
||
OSVersion: 'osVersion',
|
||
RTCReportingVersion: '_reportVers',
|
||
StoreBagVersion: 'StorebagVersion',
|
||
RTCSchemeVersion: '_reportScheme',
|
||
BagSchemeVersion: 'StorebagScheme',
|
||
StoreBagName: 'StorebagName',
|
||
SessionTag: 'SessionTag',
|
||
SenderOSVersion: 'SenderOSVersion',
|
||
};
|
||
const RTCReportingAgentDefaultConfig = {
|
||
RunInlineIfNoWebWorker: true,
|
||
BatchEvent: true,
|
||
LogMsgPrefix: '[RTC_RA]: ',
|
||
};
|
||
const RTCReportingAgentConfigKeys = {
|
||
ClientName: '_clientName',
|
||
ServiceName: '_serviceName',
|
||
StorebagURL: 'storebagURL',
|
||
ApplicationName: 'applicationName',
|
||
Sender: 'sender',
|
||
DeviceName: 'deviceName',
|
||
OSVersion: 'osVersion',
|
||
ReportingDisabled: 'reportingDisabled',
|
||
HLSEventEmitter: 'hlsEventEmitter',
|
||
BuildType: 'internal',
|
||
UserInfoDict: 'userInfoDict',
|
||
ReportingStoreBag: 'reportingStoreBag',
|
||
SenderAltConfig: 'senderAltConfig',
|
||
ServiceIdentifier: 'ServiceIdentifier',
|
||
UseHTTPHeaders: 'UseHTTPHeaders',
|
||
};
|
||
const RTCReportingAgentEventTypes = {
|
||
BatchEvent: 0,
|
||
RealTimeEvent: 1,
|
||
};
|
||
const RTCReportingAgentMethods = {
|
||
ReportingSessionMethod: 0,
|
||
};
|
||
const RTCReportingStoreBagConfig = {
|
||
StoreBagFetchSyncMode: 0,
|
||
StoreBagFetchAsyncMode: 1,
|
||
StoreBagVersionSupported: 1,
|
||
};
|
||
const RTCReportingReporterConfig = {
|
||
BatchThreshold: 50,
|
||
};
|
||
const RTCReportingSessionEventStatus = {
|
||
EventStatusSessionBegin: 0,
|
||
EventStatusSessionEnd: 1,
|
||
EventStatusSessionEndWithError: 2,
|
||
};
|
||
const RTCReportingStoreBagKeys = {
|
||
Version: 'Version',
|
||
Scheme: 'Scheme',
|
||
BagVersion: 'BagVersion',
|
||
Configurations: 'Configurations',
|
||
ConfigurationList: 'ConfigurationList',
|
||
PostURLs: 'PostURLs',
|
||
Filters: 'Filters',
|
||
BagList: 'BagList',
|
||
MinOSVersionDict: 'MinOSVersionDict',
|
||
ClientName: 'ClientName',
|
||
ServiceName: 'ServiceName',
|
||
ServiceNameList: 'ServiceNameList',
|
||
SenderServiceIDList: 'SenderServiceIDList',
|
||
ApplicationNameList: 'ApplicationNameList',
|
||
DeviceList: 'DeviceList',
|
||
Remove: 'Remove',
|
||
EventThreshold: 'EventThreshold',
|
||
SamplingThreshold: 'SamplingThreshold',
|
||
DeviceWhiteList: 'DeviceWhiteList',
|
||
FlushEventsInterval: 'FlushEventsInterval',
|
||
};
|
||
const RTCReportingBuildTypes = {
|
||
BuildTypePublic: 0,
|
||
BuildTypeInternal: 1,
|
||
BuildTypeSeed: 2,
|
||
BuildTypeInternalSeed: 3
|
||
};
|
||
class RTCReportingError extends Error {
|
||
constructor(...args) {
|
||
super(...args);
|
||
Error.captureStackTrace(this, RTCReportingError);
|
||
}
|
||
}
|
||
const RTC_REPORTING_EVENTS_DISPATCHED = 'rtcReportingEventsDispatched';
|
||
|
||
|
||
|
||
/***/ }),
|
||
|
||
/***/ "./src/rtc_reporting/reporting_event.ts":
|
||
/*!**********************************************!*\
|
||
!*** ./src/rtc_reporting/reporting_event.ts ***!
|
||
\**********************************************/
|
||
/*! exports provided: RTCReportingEvent */
|
||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||
__webpack_require__.r(__webpack_exports__);
|
||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RTCReportingEvent", function() { return RTCReportingEvent; });
|
||
/* harmony import */ var _reporting_config__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./reporting_config */ "./src/rtc_reporting/reporting_config.ts");
|
||
|
||
class RTCReportingEvent {
|
||
constructor(eventID, status = 0, eventPayload = {}, fuzzFactor = 0, eventType = _reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingAgentEventTypes"].BatchEvent) {
|
||
this.payload = eventPayload ? eventPayload : {};
|
||
this.payload[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingEventMetadataKeys"].EventID] = eventID;
|
||
this.payload[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingEventMetadataKeys"].Status] = status;
|
||
this.payload[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingEventMetadataKeys"].EventType] = eventType;
|
||
this.fuzzFactor = fuzzFactor;
|
||
this.isEventRealTime = (eventType === _reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingAgentEventTypes"].RealTimeEvent);
|
||
this._addEventMetadata();
|
||
}
|
||
prepareEventPayload(sessionInfo, sizeTillLastBatch, sizeOfLastBatch) {
|
||
Object.keys(sessionInfo).forEach(function (sessionInfoKey) {
|
||
this.payload[sessionInfoKey] = sessionInfo[sessionInfoKey];
|
||
}.bind(this));
|
||
if (this.isEventRealTime && sizeOfLastBatch && sizeTillLastBatch) {
|
||
this.payload.sizeOfLastBatch = sizeOfLastBatch;
|
||
this.payload.sizeTillLastBatch = sizeTillLastBatch;
|
||
}
|
||
}
|
||
get EventPayload() {
|
||
return this.payload;
|
||
}
|
||
get IsEventRealTime() {
|
||
return this.isEventRealTime;
|
||
}
|
||
_addEventMetadata() {
|
||
let date = new Date();
|
||
this.payload[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingEventMetadataKeys"].EventTime] = (date.getTime()) / 1000;
|
||
this.payload[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingEventMetadataKeys"].TimeZoneOffset] = date.getTimezoneOffset() * 60;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/***/ }),
|
||
|
||
/***/ "./src/rtc_reporting/reporting_reporter.ts":
|
||
/*!*************************************************!*\
|
||
!*** ./src/rtc_reporting/reporting_reporter.ts ***!
|
||
\*************************************************/
|
||
/*! exports provided: RTCReporter */
|
||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||
__webpack_require__.r(__webpack_exports__);
|
||
/* WEBPACK VAR INJECTION */(function(module) {/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RTCReporter", function() { return RTCReporter; });
|
||
/* harmony import */ var _reporting_config__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./reporting_config */ "./src/rtc_reporting/reporting_config.ts");
|
||
|
||
const ReportingContentType = 'application/json;charset=UTF-8';
|
||
const ReportingVersion = 'RTCReportingJS/0.314';
|
||
class EventDispatcher {
|
||
constructor(eventEmitter, sessionID, useHTTPHeaders = false) {
|
||
this._eventWriter = null;
|
||
this._sessionID = sessionID;
|
||
this._eventDispatcher = eventEmitter;
|
||
if (typeof navigator !== 'undefined' && navigator && (typeof navigator.sendBeacon !== 'undefined') && navigator.sendBeacon) {
|
||
this._eventWriter = this._sendEventDataWithHTTPPost;
|
||
}
|
||
else if ( this.module !== module) {
|
||
this._eventWriter = this._sendEventDataWithHTTPHeaders;
|
||
}
|
||
else {
|
||
this._eventWriter = this._sendEventDataWithHTTPPost;
|
||
}
|
||
if (useHTTPHeaders) {
|
||
this._eventWriter = this._sendEventDataWithHTTPHeaders;
|
||
}
|
||
}
|
||
sendEventData(url, eventDataAsJSON, clientName, serviceName, clientVersion, sender) {
|
||
if (this._eventWriter) {
|
||
this._eventWriter(url, eventDataAsJSON, clientName, serviceName, clientVersion, sender);
|
||
if (this._eventDispatcher) {
|
||
this._eventDispatcher.emit(_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTC_REPORTING_EVENTS_DISPATCHED"], eventDataAsJSON);
|
||
}
|
||
}
|
||
}
|
||
_sendEventDataUsingBeacon(url, eventDataAsJSON, clientName, serviceName, clientVersion, sender) {
|
||
let headerJSON = this._createEventPayloadAsJSON(clientName, serviceName, clientVersion, sender);
|
||
let formData = new FormData();
|
||
formData.append('headers', headerJSON);
|
||
formData.append('payload', eventDataAsJSON);
|
||
navigator.sendBeacon(url, formData);
|
||
}
|
||
_sendEventDataWithHTTPPost(url, eventDataAsJSON, clientName, serviceName, clientVersion, sender) {
|
||
let headerJSON = this._createEventPayloadAsJSON(clientName, serviceName, clientVersion, sender);
|
||
let formData = new FormData();
|
||
formData.append('headers', headerJSON);
|
||
formData.append('payload', eventDataAsJSON);
|
||
let request = new XMLHttpRequest();
|
||
const method = 'POST';
|
||
const async = true;
|
||
request.open(method, url, async);
|
||
request.send(formData);
|
||
}
|
||
_createEventPayloadAsJSON(clientName, serviceName, clientVersion, sender) {
|
||
let header = {};
|
||
header[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingHTTPHeaderKeys"].InternalBuild] = 0;
|
||
header[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingHTTPHeaderKeys"].ClientName] = clientName;
|
||
header[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingHTTPHeaderKeys"].ServiceName] = serviceName;
|
||
header[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingHTTPHeaderKeys"].ClientVersion] = clientVersion;
|
||
header[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingHTTPHeaderKeys"].Sender] = sender;
|
||
header[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingHTTPHeaderKeys"].ContentType] = 'application/json;charset=UTF-8';
|
||
let headerJSON = JSON.stringify(header);
|
||
return headerJSON;
|
||
}
|
||
_sendEventDataWithHTTPHeaders(url, eventDataAsJSON, clientName, serviceName, clientVersion, sender) {
|
||
let request = new XMLHttpRequest();
|
||
const method = 'POST';
|
||
const async = true;
|
||
request.onload = function () {
|
||
};
|
||
request.open(method, url, async);
|
||
request.setRequestHeader(_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingHTTPHeaderKeys"].ContentType, ReportingContentType);
|
||
request.setRequestHeader(_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingHTTPHeaderKeys"].InternalBuild, '0');
|
||
request.setRequestHeader(_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingHTTPHeaderKeys"].ClientName, clientName);
|
||
request.setRequestHeader(_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingHTTPHeaderKeys"].ServiceName, serviceName);
|
||
request.setRequestHeader(_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingHTTPHeaderKeys"].ClientVersion, clientVersion);
|
||
request.setRequestHeader(_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingHTTPHeaderKeys"].Sender, sender);
|
||
request.setRequestHeader(_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingHTTPHeaderKeys"].ClientVersion, ReportingVersion);
|
||
request.setRequestHeader(_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingHTTPHeaderKeys"].SessionID, this._sessionID);
|
||
request.send(eventDataAsJSON);
|
||
}
|
||
}
|
||
class RTCReporter {
|
||
constructor(clientName, serviceName, sender, reportingStoreBag, clientVersion, hlsEventEmitter, applicationName = null, senderServiceID = null, useHTTPHeaders = false, sessionID = null) {
|
||
this.clientName = clientName;
|
||
this.serviceName = serviceName;
|
||
this.reportingStoreBag = reportingStoreBag;
|
||
this.sender = sender;
|
||
this.clientVersion = clientVersion;
|
||
this.eventList = [];
|
||
this._count = 0;
|
||
this._eventDispatcher = new EventDispatcher(hlsEventEmitter, sessionID, useHTTPHeaders);
|
||
this.sampleFactor = Math.random();
|
||
this._storebagStats = {};
|
||
this._canReport = undefined;
|
||
this._sizeTillLastBatch = this._sizeOfLastBatch = 0;
|
||
this.storeBag = null;
|
||
this.applicationName = applicationName;
|
||
this.senderServiceID = senderServiceID;
|
||
this.checkAndConfigureEventFlushTimeoutOnce = false;
|
||
}
|
||
issueReportingEvent(event, sessionInfo = {}) {
|
||
if (!event) {
|
||
return new _reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingError"]('not a valid event to report');
|
||
}
|
||
this._updateEventMetadata(event);
|
||
event.prepareEventPayload(sessionInfo, this._sizeTillLastBatch, this._sizeOfLastBatch);
|
||
this.eventList.push(event);
|
||
if (!this.checkAndConfigureEventFlushTimeoutOnce) {
|
||
if (this.canReportEvents()) {
|
||
this._configureEventFlushTimer();
|
||
}
|
||
}
|
||
if ((this.eventList.length > _reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingReporterConfig"].BatchThreshold) || event.IsEventRealTime) {
|
||
return this.flushAllEvents();
|
||
}
|
||
return null;
|
||
}
|
||
canReportEvents() {
|
||
let sampleRate;
|
||
if (this._canReport !== undefined) {
|
||
return this._canReport;
|
||
}
|
||
if (this.reportingStoreBag && !this.storeBag) {
|
||
this.storeBag = this.reportingStoreBag.createParsedStoreBag(this.applicationName, this.senderServiceID);
|
||
if (!this.storeBag) {
|
||
return false;
|
||
}
|
||
}
|
||
if (this.storeBag && this.storeBag.IsValid) {
|
||
this._canReport = true;
|
||
sampleRate = this.storeBag.getStoreBagProperty(_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagKeys"].SamplingThreshold);
|
||
if (sampleRate && (parseFloat(sampleRate) < this.sampleFactor)) {
|
||
this._canReport = false;
|
||
}
|
||
}
|
||
else if (this.storeBag) {
|
||
console.log(`StoreBag for client ${this.clientName}, service ${this.serviceName}, app ${this.applicationName} senderService ${this.senderServiceID} not valid ${this.storeBag.IsValid} `);
|
||
}
|
||
if (this._canReport) {
|
||
this._storebagStats[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagKeys"].SamplingThreshold] = sampleRate;
|
||
this._storebagStats[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingEventMetadataKeys"].BagSchemeVersion] = this.storeBag.StoreBagSchemeVersion;
|
||
this._storebagStats[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingEventMetadataKeys"].StoreBagVersion] = this.storeBag.StoreBagVersion;
|
||
this._storebagStats[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingEventMetadataKeys"].StoreBagName] = this.storeBag.StoreBagName;
|
||
let bagMinVersionDict = this.storeBag.BagMinVersionDict;
|
||
if (typeof bagMinVersionDict === 'object' && bagMinVersionDict) {
|
||
for (let key in bagMinVersionDict) {
|
||
if (bagMinVersionDict.hasOwnProperty(key)) {
|
||
this._storebagStats[key] = bagMinVersionDict[key];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
console.log(`${_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingAgentDefaultConfig"].LogMsgPrefix} Event reporting is ${this._canReport} for client ${this.clientName} service ${this.serviceName}, app ${this.applicationName}, senderService ${this.senderServiceID}`);
|
||
return this._canReport;
|
||
}
|
||
flushEventTimeout() {
|
||
this.flushAllEvents();
|
||
}
|
||
flushAllEvents() {
|
||
let error = null;
|
||
if (this.canReportEvents()) {
|
||
this._sendAllEvents();
|
||
}
|
||
else {
|
||
error = new _reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingError"]('no valid storebag configured or blocked by sampling threshold, ignoring events.');
|
||
}
|
||
this.eventList = [];
|
||
return error;
|
||
}
|
||
batchedEventsCount() {
|
||
return this.eventList.length;
|
||
}
|
||
get _EventList() {
|
||
return this.eventList;
|
||
}
|
||
_updateEventMetadata(event) {
|
||
event.EventPayload[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingEventMetadataKeys"].EventNumber] = this._eventCounter();
|
||
event.EventPayload[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingEventMetadataKeys"].ClientTS] = ((new Date()).getTime()) / 1000;
|
||
event.EventPayload[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingEventMetadataKeys"].Sender] = this.sender;
|
||
event.EventPayload[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingAgentConfigKeys"].ClientName] = this.clientName;
|
||
event.EventPayload[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingAgentConfigKeys"].ServiceName] = this.serviceName;
|
||
}
|
||
_sendAllEvents() {
|
||
if (this.eventList.length === 0) {
|
||
return;
|
||
}
|
||
let backEnds = this.storeBag.listOfSupportedBackEnds(this.clientName, this.serviceName);
|
||
if (!backEnds || backEnds.length <= 0) {
|
||
return;
|
||
}
|
||
let blackListKeys = this.storeBag.listOfKeysToBlock();
|
||
if (blackListKeys) {
|
||
this.eventList.forEach(function (event) {
|
||
event.prepareEventPayload(this._storebagStats, this._sizeTillLastBatch, this._sizeOfLastBatch);
|
||
blackListKeys.forEach(function (key) {
|
||
delete event.EventPayload[key];
|
||
});
|
||
}.bind(this));
|
||
}
|
||
let eventData = this._marshallAsJSONString();
|
||
this._sizeTillLastBatch += eventData.length;
|
||
this._sizeOfLastBatch = eventData.length;
|
||
backEnds.forEach(function (backEndURL) {
|
||
this._eventDispatcher.sendEventData(backEndURL, eventData, this.clientName, this.serviceName, this.clientVersion, this.sender);
|
||
}.bind(this));
|
||
console.log(`${_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingAgentDefaultConfig"].LogMsgPrefix} events reported.`);
|
||
}
|
||
_marshallAsJSONString() {
|
||
let time = ((new Date()).getTime()) / 1000;
|
||
let postData = {};
|
||
let events = [];
|
||
this.eventList.forEach(function (event) {
|
||
events.push(event.EventPayload);
|
||
});
|
||
postData[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingEventMetadataKeys"].PostTime] = time;
|
||
postData[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingEventMetadataKeys"].Events] = events;
|
||
let postBody = JSON.stringify(postData);
|
||
return postBody;
|
||
}
|
||
_eventCounter() {
|
||
return this._count++;
|
||
}
|
||
_configureEventFlushTimer() {
|
||
let flushTimeout = null;
|
||
this.checkAndConfigureEventFlushTimeoutOnce = true;
|
||
if (this.reportingStoreBag && !this.storeBag) {
|
||
this.storeBag = this.reportingStoreBag.createParsedStoreBag(this.applicationName, this.senderServiceID);
|
||
}
|
||
if (this.storeBag && this.storeBag.IsValid) {
|
||
flushTimeout = this.storeBag.getStoreBagProperty(_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagKeys"].FlushEventsInterval);
|
||
}
|
||
if (flushTimeout) {
|
||
flushTimeout = parseInt(flushTimeout, 10) * 1000;
|
||
if (flushTimeout >= 10000) {
|
||
setInterval(this.flushEventTimeout.bind(this), flushTimeout);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../../node_modules/webpack/buildin/harmony-module.js */ "./node_modules/webpack/buildin/harmony-module.js")(module)));
|
||
|
||
/***/ }),
|
||
|
||
/***/ "./src/rtc_reporting/reporting_storebag.ts":
|
||
/*!*************************************************!*\
|
||
!*** ./src/rtc_reporting/reporting_storebag.ts ***!
|
||
\*************************************************/
|
||
/*! exports provided: RTCReportingStoreBag */
|
||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||
__webpack_require__.r(__webpack_exports__);
|
||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RTCReportingStoreBag", function() { return RTCReportingStoreBag; });
|
||
/* harmony import */ var _reporting_config__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./reporting_config */ "./src/rtc_reporting/reporting_config.ts");
|
||
/* harmony import */ var semver__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! semver */ "./node_modules/semver/semver.js");
|
||
|
||
|
||
class RTCReportingParsedStoreBag {
|
||
constructor(storeBagConfig, selectedBagName, bagVersion, bagMinVersionDict) {
|
||
this.storeBagConfig = storeBagConfig;
|
||
this.selectedBagName = selectedBagName;
|
||
this.bagVersion = bagVersion;
|
||
this.bagMinVersionDict = bagMinVersionDict;
|
||
}
|
||
listOfSupportedBackEnds() {
|
||
return this.getStoreBagProperty(_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagKeys"].PostURLs);
|
||
}
|
||
listOfKeysToBlock() {
|
||
if (!this.storeBagConfig) {
|
||
return null;
|
||
}
|
||
let filters = this.storeBagConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagKeys"].Filters];
|
||
if (!filters) {
|
||
return null;
|
||
}
|
||
return filters[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagKeys"].Remove];
|
||
}
|
||
get IsValid() {
|
||
return Boolean(this.storeBagConfig);
|
||
}
|
||
get StoreBagName() {
|
||
return this.IsValid ? this.selectedBagName : null;
|
||
}
|
||
get StorebagVersion() {
|
||
return this.IsValid ? this.bagVersion : null;
|
||
}
|
||
get StoreBagSchemeVersion() {
|
||
return _reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagConfig"].StoreBagVersionSupported;
|
||
}
|
||
get BagMinVersionDict() {
|
||
return this.bagMinVersionDict;
|
||
}
|
||
getStoreBagProperty(propertyKey) {
|
||
let propertyValue = null;
|
||
if (this.IsValid && propertyKey && propertyKey in this.storeBagConfig) {
|
||
propertyValue = this.storeBagConfig[propertyKey];
|
||
}
|
||
return propertyValue;
|
||
}
|
||
}
|
||
class RTCReportingStoreBag {
|
||
constructor(storebagURL, clientName = null, serviceName = null, applicationName = null, deviceName = null, bagMinVersionDict = null, mode = _reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagConfig"].StoreBagFetchAsyncMode, storebagAsJSON = null) {
|
||
this.storebagURL = storebagURL;
|
||
this.mode = mode;
|
||
this.storeBagConfig = null;
|
||
this.clientName = clientName;
|
||
this.serviceName = serviceName;
|
||
this.applicationName = applicationName;
|
||
this.deviceName = deviceName;
|
||
this.storebagLoader = null;
|
||
this.selectedBagName = null;
|
||
this.storebagAsJSON = storebagAsJSON;
|
||
this.bagVersion = null;
|
||
this.bagMinVersionDict = null;
|
||
if (typeof bagMinVersionDict === 'object') {
|
||
this.bagMinVersionDict = bagMinVersionDict;
|
||
}
|
||
this.fetchAndParseStoreBag();
|
||
}
|
||
destroy() {
|
||
if (this.storebagLoader) {
|
||
this.storebagLoader.abort();
|
||
this.storebagLoader = null;
|
||
}
|
||
}
|
||
fetchAndParseStoreBag() {
|
||
this._fetchStoreBag();
|
||
}
|
||
createParsedStoreBag(applicationName = this.applicationName, senderServiceID = null) {
|
||
if (!this.storebagAsJSON) {
|
||
console.log('no bag available!');
|
||
return null;
|
||
}
|
||
return this._parseAndReturnStoreBag(this.storebagAsJSON, applicationName, senderServiceID);
|
||
}
|
||
_checkbagMinVersion(bagMinVersionDict, currentVersionDict) {
|
||
if (bagMinVersionDict && currentVersionDict) {
|
||
for (let key in bagMinVersionDict) {
|
||
if (bagMinVersionDict.hasOwnProperty(key)) {
|
||
let bagMinVersion = bagMinVersionDict[key];
|
||
if (currentVersionDict.hasOwnProperty(key)) {
|
||
let currentVersion = currentVersionDict[key];
|
||
if (Object(semver__WEBPACK_IMPORTED_MODULE_1__["gte"])(currentVersion, bagMinVersion)) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
_parseAndReturnStoreBag(storebag, applicationName, senderServiceID) {
|
||
let selectedBag = null;
|
||
let selectedBagConfig = null;
|
||
let selectedBagConfigKey = null;
|
||
let selectedBagVersion = null;
|
||
storebag.forEach(function (bag) {
|
||
selectedBag = null;
|
||
if (bag[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagKeys"].Scheme] === _reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagConfig"].StoreBagVersionSupported) {
|
||
let bagList = bag[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagKeys"].BagList];
|
||
Object.keys(bagList).some(function (bagConfigKey) {
|
||
let bagConfig = bagList[bagConfigKey];
|
||
let bagMinVersionDict = bagConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagKeys"].MinOSVersionDict];
|
||
if (!this._checkbagMinVersion(bagMinVersionDict, this.bagMinVersionDict)) {
|
||
console.log(`failed to match the version # `);
|
||
return false;
|
||
}
|
||
if (!this._canUseBagConfig(bagConfig, applicationName, senderServiceID)) {
|
||
return false;
|
||
}
|
||
selectedBag = bagConfig;
|
||
selectedBagConfigKey = bagConfigKey;
|
||
selectedBagVersion = bag[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagKeys"].BagVersion];
|
||
return false;
|
||
}.bind(this));
|
||
}
|
||
if (selectedBag) {
|
||
selectedBagConfig = this._fillSelectedBagConfig(selectedBag, bag);
|
||
}
|
||
}.bind(this));
|
||
if (!selectedBagConfig) {
|
||
console.log(`${_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingAgentDefaultConfig"].LogMsgPrefix} Failed to select a storebag.`);
|
||
}
|
||
return new RTCReportingParsedStoreBag(selectedBagConfig, selectedBagConfigKey, selectedBagVersion, this.bagMinVersionDict);
|
||
}
|
||
_fetchStoreBag() {
|
||
if (this.storebagURL && !this.storebagAsJSON) {
|
||
this._requestStoreBagAsync();
|
||
}
|
||
}
|
||
_requestStoreBagAsync() {
|
||
this.storebagLoader = new XMLHttpRequest();
|
||
this.storebagLoader.onreadystatechange = function () {
|
||
if (this.storebagLoader && this.storebagLoader.readyState === 4 && this.storebagLoader.status === 200 && this.storebagLoader.responseText) {
|
||
this.storebagAsJSON = JSON.parse(this.storebagLoader.responseText);
|
||
}
|
||
}.bind(this);
|
||
this.storebagLoader.open('GET', this.storebagURL, true);
|
||
this.storebagLoader.send(null);
|
||
}
|
||
_canUseBagConfig(bagConfig, applicationName, senderServiceID) {
|
||
let bagClientName = bagConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagKeys"].ClientName];
|
||
if (bagClientName && bagClientName !== this.clientName) {
|
||
return false;
|
||
}
|
||
let bagServiceNameList = bagConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagKeys"].ServiceNameList];
|
||
let bagServiceName = bagConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagKeys"].ServiceName];
|
||
if (bagServiceNameList && bagServiceNameList.length > 0 && -1 === bagServiceNameList.indexOf(this.serviceName)) {
|
||
return false;
|
||
}
|
||
if (!bagServiceNameList && bagServiceName && bagServiceName !== this.serviceName) {
|
||
return false;
|
||
}
|
||
let bagApplicationNameList = bagConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagKeys"].ApplicationNameList];
|
||
if (bagApplicationNameList && bagApplicationNameList.length > 0 && -1 === bagApplicationNameList.indexOf(applicationName)) {
|
||
return false;
|
||
}
|
||
let bagDeviceList = bagConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagKeys"].DeviceList];
|
||
if (bagDeviceList && bagDeviceList.length > 0 && -1 === bagDeviceList.indexOf(this.deviceName)) {
|
||
return false;
|
||
}
|
||
let bagSenderServiceIDList = bagConfig[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagKeys"].SenderServiceIDList];
|
||
if (bagSenderServiceIDList && bagSenderServiceIDList.length > 0 && -1 === bagSenderServiceIDList.indexOf(senderServiceID)) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
_fillSelectedBagConfig(selectedBag, bag) {
|
||
let selectedBagConfig = null;
|
||
if (!selectedBag || !bag) {
|
||
return selectedBagConfig;
|
||
}
|
||
let configurationList = bag[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagKeys"].ConfigurationList];
|
||
let listOfConfigs = selectedBag[_reporting_config__WEBPACK_IMPORTED_MODULE_0__["RTCReportingStoreBagKeys"].Configurations];
|
||
if (!listOfConfigs) {
|
||
return selectedBagConfig;
|
||
}
|
||
listOfConfigs.forEach(function (configName) {
|
||
let config = configurationList[configName];
|
||
selectedBagConfig = selectedBagConfig ? selectedBagConfig : {};
|
||
Object.assign(selectedBagConfig, config);
|
||
});
|
||
return selectedBagConfig;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/***/ })
|
||
|
||
/******/ })["default"];
|
||
});
|
||
|
||
}(rtc));
|
||
|
||
var socket_io_min = {exports: {}};
|
||
|
||
/*!
|
||
* Socket.IO v4.0.2
|
||
* (c) 2014-2021 Guillermo Rauch
|
||
* Released under the MIT License.
|
||
*/
|
||
|
||
(function (module, exports) {
|
||
!function(t,e){module.exports=e();}(self,(function(){return function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r});},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0});},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=18)}([function(t,e,n){var r=n(24),o=n(25),i=String.fromCharCode(30);t.exports={protocol:4,encodePacket:r,encodePayload:function(t,e){var n=t.length,o=new Array(n),s=0;t.forEach((function(t,c){r(t,!1,(function(t){o[c]=t,++s===n&&e(o.join(i));}));}));},decodePacket:o,decodePayload:function(t,e){for(var n=t.split(i),r=[],s=0;s<n.length;s++){var c=o(n[s],e);if(r.push(c),"error"===c.type)break}return r}};},function(t,e,n){function r(t){if(t)return function(t){for(var e in r.prototype)t[e]=r.prototype[e];return t}(t)}t.exports=r,r.prototype.on=r.prototype.addEventListener=function(t,e){return this._callbacks=this._callbacks||{},(this._callbacks["$"+t]=this._callbacks["$"+t]||[]).push(e),this},r.prototype.once=function(t,e){function n(){this.off(t,n),e.apply(this,arguments);}return n.fn=e,this.on(t,n),this},r.prototype.off=r.prototype.removeListener=r.prototype.removeAllListeners=r.prototype.removeEventListener=function(t,e){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var n,r=this._callbacks["$"+t];if(!r)return this;if(1==arguments.length)return delete this._callbacks["$"+t],this;for(var o=0;o<r.length;o++)if((n=r[o])===e||n.fn===e){r.splice(o,1);break}return 0===r.length&&delete this._callbacks["$"+t],this},r.prototype.emit=function(t){this._callbacks=this._callbacks||{};for(var e=new Array(arguments.length-1),n=this._callbacks["$"+t],r=1;r<arguments.length;r++)e[r-1]=arguments[r];if(n){r=0;for(var o=(n=n.slice(0)).length;r<o;++r)n[r].apply(this,e);}return this},r.prototype.listeners=function(t){return this._callbacks=this._callbacks||{},this._callbacks["$"+t]||[]},r.prototype.hasListeners=function(t){return !!this.listeners(t).length};},function(t,e){t.exports="undefined"!=typeof self?self:"undefined"!=typeof window?window:Function("return this")();},function(t,e,n){function r(t){return (r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function o(t,e){for(var n=0;n<e.length;n++){var r=e[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r);}}function i(t,e){return (i=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function s(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return !1;if(Reflect.construct.sham)return !1;if("function"==typeof Proxy)return !0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(t){return !1}}();return function(){var n,r=a(t);if(e){var o=a(this).constructor;n=Reflect.construct(r,arguments,o);}else n=r.apply(this,arguments);return c(this,n)}}function c(t,e){return !e||"object"!==r(e)&&"function"!=typeof e?function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t):e}function a(t){return (a=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}var u=n(0),f=function(t){!function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),e&&i(t,e);}(a,t);var e,n,c=s(a);function a(t){var e;return function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,a),(e=c.call(this)).opts=t,e.query=t.query,e.readyState="",e.socket=t.socket,e}return e=a,(n=[{key:"onError",value:function(t,e){var n=new Error(t);return n.type="TransportError",n.description=e,this.emit("error",n),this}},{key:"open",value:function(){return "closed"!==this.readyState&&""!==this.readyState||(this.readyState="opening",this.doOpen()),this}},{key:"close",value:function(){return "opening"!==this.readyState&&"open"!==this.readyState||(this.doClose(),this.onClose()),this}},{key:"send",value:function(t){"open"===this.readyState&&this.write(t);}},{key:"onOpen",value:function(){this.readyState="open",this.writable=!0,this.emit("open");}},{key:"onData",value:function(t){var e=u.decodePacket(t,this.socket.binaryType);this.onPacket(e);}},{key:"onPacket",value:function(t){this.emit("packet",t);}},{key:"onClose",value:function(){this.readyState="closed",this.emit("close");}}])&&o(e.prototype,n),a}(n(1));t.exports=f;},function(t,e){e.encode=function(t){var e="";for(var n in t)t.hasOwnProperty(n)&&(e.length&&(e+="&"),e+=encodeURIComponent(n)+"="+encodeURIComponent(t[n]));return e},e.decode=function(t){for(var e={},n=t.split("&"),r=0,o=n.length;r<o;r++){var i=n[r].split("=");e[decodeURIComponent(i[0])]=decodeURIComponent(i[1]);}return e};},function(t,e,n){function r(t){return (r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function o(t,e,n){return (o="undefined"!=typeof Reflect&&Reflect.get?Reflect.get:function(t,e,n){var r=function(t,e){for(;!Object.prototype.hasOwnProperty.call(t,e)&&null!==(t=a(t)););return t}(t,e);if(r){var o=Object.getOwnPropertyDescriptor(r,e);return o.get?o.get.call(n):o.value}})(t,e,n||t)}function i(t,e){return (i=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function s(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return !1;if(Reflect.construct.sham)return !1;if("function"==typeof Proxy)return !0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(t){return !1}}();return function(){var n,r=a(t);if(e){var o=a(this).constructor;n=Reflect.construct(r,arguments,o);}else n=r.apply(this,arguments);return c(this,n)}}function c(t,e){return !e||"object"!==r(e)&&"function"!=typeof e?function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t):e}function a(t){return (a=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}function u(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function f(t,e){for(var n=0;n<e.length;n++){var r=e[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r);}}function p(t,e,n){return e&&f(t.prototype,e),n&&f(t,n),t}Object.defineProperty(e,"__esModule",{value:!0}),e.Decoder=e.Encoder=e.PacketType=e.protocol=void 0;var l,h=n(1),y=n(30),d=n(15);e.protocol=5,function(t){t[t.CONNECT=0]="CONNECT",t[t.DISCONNECT=1]="DISCONNECT",t[t.EVENT=2]="EVENT",t[t.ACK=3]="ACK",t[t.CONNECT_ERROR=4]="CONNECT_ERROR",t[t.BINARY_EVENT=5]="BINARY_EVENT",t[t.BINARY_ACK=6]="BINARY_ACK";}(l=e.PacketType||(e.PacketType={}));var v=function(){function t(){u(this,t);}return p(t,[{key:"encode",value:function(t){return t.type!==l.EVENT&&t.type!==l.ACK||!d.hasBinary(t)?[this.encodeAsString(t)]:(t.type=t.type===l.EVENT?l.BINARY_EVENT:l.BINARY_ACK,this.encodeAsBinary(t))}},{key:"encodeAsString",value:function(t){var e=""+t.type;return t.type!==l.BINARY_EVENT&&t.type!==l.BINARY_ACK||(e+=t.attachments+"-"),t.nsp&&"/"!==t.nsp&&(e+=t.nsp+","),null!=t.id&&(e+=t.id),null!=t.data&&(e+=JSON.stringify(t.data)),e}},{key:"encodeAsBinary",value:function(t){var e=y.deconstructPacket(t),n=this.encodeAsString(e.packet),r=e.buffers;return r.unshift(n),r}}]),t}();e.Encoder=v;var b=function(t){!function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),e&&i(t,e);}(n,t);var e=s(n);function n(){return u(this,n),e.call(this)}return p(n,[{key:"add",value:function(t){var e;if("string"==typeof t)(e=this.decodeString(t)).type===l.BINARY_EVENT||e.type===l.BINARY_ACK?(this.reconstructor=new m(e),0===e.attachments&&o(a(n.prototype),"emit",this).call(this,"decoded",e)):o(a(n.prototype),"emit",this).call(this,"decoded",e);else {if(!d.isBinary(t)&&!t.base64)throw new Error("Unknown type: "+t);if(!this.reconstructor)throw new Error("got binary data when not reconstructing a packet");(e=this.reconstructor.takeBinaryData(t))&&(this.reconstructor=null,o(a(n.prototype),"emit",this).call(this,"decoded",e));}}},{key:"decodeString",value:function(t){var e=0,r={type:Number(t.charAt(0))};if(void 0===l[r.type])throw new Error("unknown packet type "+r.type);if(r.type===l.BINARY_EVENT||r.type===l.BINARY_ACK){for(var o=e+1;"-"!==t.charAt(++e)&&e!=t.length;);var i=t.substring(o,e);if(i!=Number(i)||"-"!==t.charAt(e))throw new Error("Illegal attachments");r.attachments=Number(i);}if("/"===t.charAt(e+1)){for(var s=e+1;++e;){if(","===t.charAt(e))break;if(e===t.length)break}r.nsp=t.substring(s,e);}else r.nsp="/";var c=t.charAt(e+1);if(""!==c&&Number(c)==c){for(var a=e+1;++e;){var u=t.charAt(e);if(null==u||Number(u)!=u){--e;break}if(e===t.length)break}r.id=Number(t.substring(a,e+1));}if(t.charAt(++e)){var f=function(t){try{return JSON.parse(t)}catch(t){return !1}}(t.substr(e));if(!n.isPayloadValid(r.type,f))throw new Error("invalid payload");r.data=f;}return r}},{key:"destroy",value:function(){this.reconstructor&&this.reconstructor.finishedReconstruction();}}],[{key:"isPayloadValid",value:function(t,e){switch(t){case l.CONNECT:return "object"===r(e);case l.DISCONNECT:return void 0===e;case l.CONNECT_ERROR:return "string"==typeof e||"object"===r(e);case l.EVENT:case l.BINARY_EVENT:return Array.isArray(e)&&e.length>0;case l.ACK:case l.BINARY_ACK:return Array.isArray(e)}}}]),n}(h);e.Decoder=b;var m=function(){function t(e){u(this,t),this.packet=e,this.buffers=[],this.reconPack=e;}return p(t,[{key:"takeBinaryData",value:function(t){if(this.buffers.push(t),this.buffers.length===this.reconPack.attachments){var e=y.reconstructPacket(this.reconPack,this.buffers);return this.finishedReconstruction(),e}return null}},{key:"finishedReconstruction",value:function(){this.reconPack=null,this.buffers=[];}}]),t}();},function(t,e){var n=/^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,r=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"];t.exports=function(t){var e=t,o=t.indexOf("["),i=t.indexOf("]");-1!=o&&-1!=i&&(t=t.substring(0,o)+t.substring(o,i).replace(/:/g,";")+t.substring(i,t.length));for(var s,c,a=n.exec(t||""),u={},f=14;f--;)u[r[f]]=a[f]||"";return -1!=o&&-1!=i&&(u.source=e,u.host=u.host.substring(1,u.host.length-1).replace(/;/g,":"),u.authority=u.authority.replace("[","").replace("]","").replace(/;/g,":"),u.ipv6uri=!0),u.pathNames=function(t,e){var n=e.replace(/\/{2,9}/g,"/").split("/");"/"!=e.substr(0,1)&&0!==e.length||n.splice(0,1);"/"==e.substr(e.length-1,1)&&n.splice(n.length-1,1);return n}(0,u.path),u.queryKey=(s=u.query,c={},s.replace(/(?:^|&)([^&=]*)=?([^&]*)/g,(function(t,e,n){e&&(c[e]=n);})),c),u};},function(t,e,n){function r(t){return (r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function o(t,e){for(var n=0;n<e.length;n++){var r=e[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r);}}function i(t,e){return (i=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function s(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return !1;if(Reflect.construct.sham)return !1;if("function"==typeof Proxy)return !0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(t){return !1}}();return function(){var n,r=a(t);if(e){var o=a(this).constructor;n=Reflect.construct(r,arguments,o);}else n=r.apply(this,arguments);return c(this,n)}}function c(t,e){return !e||"object"!==r(e)&&"function"!=typeof e?function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t):e}function a(t){return (a=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}Object.defineProperty(e,"__esModule",{value:!0}),e.Manager=void 0;var u=n(20),f=n(14),p=n(5),l=n(16),h=n(31),y=function(t){!function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),e&&i(t,e);}(y,t);var e,n,a=s(y);function y(t,e){var n;!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,y),(n=a.call(this)).nsps={},n.subs=[],t&&"object"===r(t)&&(e=t,t=void 0),(e=e||{}).path=e.path||"/socket.io",n.opts=e,n.reconnection(!1!==e.reconnection),n.reconnectionAttempts(e.reconnectionAttempts||1/0),n.reconnectionDelay(e.reconnectionDelay||1e3),n.reconnectionDelayMax(e.reconnectionDelayMax||5e3),n.randomizationFactor(e.randomizationFactor||.5),n.backoff=new h({min:n.reconnectionDelay(),max:n.reconnectionDelayMax(),jitter:n.randomizationFactor()}),n.timeout(null==e.timeout?2e4:e.timeout),n._readyState="closed",n.uri=t;var o=e.parser||p;return n.encoder=new o.Encoder,n.decoder=new o.Decoder,n._autoConnect=!1!==e.autoConnect,n._autoConnect&&n.open(),n}return e=y,(n=[{key:"reconnection",value:function(t){return arguments.length?(this._reconnection=!!t,this):this._reconnection}},{key:"reconnectionAttempts",value:function(t){return void 0===t?this._reconnectionAttempts:(this._reconnectionAttempts=t,this)}},{key:"reconnectionDelay",value:function(t){var e;return void 0===t?this._reconnectionDelay:(this._reconnectionDelay=t,null===(e=this.backoff)||void 0===e||e.setMin(t),this)}},{key:"randomizationFactor",value:function(t){var e;return void 0===t?this._randomizationFactor:(this._randomizationFactor=t,null===(e=this.backoff)||void 0===e||e.setJitter(t),this)}},{key:"reconnectionDelayMax",value:function(t){var e;return void 0===t?this._reconnectionDelayMax:(this._reconnectionDelayMax=t,null===(e=this.backoff)||void 0===e||e.setMax(t),this)}},{key:"timeout",value:function(t){return arguments.length?(this._timeout=t,this):this._timeout}},{key:"maybeReconnectOnOpen",value:function(){!this._reconnecting&&this._reconnection&&0===this.backoff.attempts&&this.reconnect();}},{key:"open",value:function(t){var e=this;if(~this._readyState.indexOf("open"))return this;this.engine=u(this.uri,this.opts);var n=this.engine,r=this;this._readyState="opening",this.skipReconnect=!1;var o=l.on(n,"open",(function(){r.onopen(),t&&t();})),i=l.on(n,"error",(function(n){r.cleanup(),r._readyState="closed",e.emitReserved("error",n),t?t(n):r.maybeReconnectOnOpen();}));if(!1!==this._timeout){var s=this._timeout;0===s&&o();var c=setTimeout((function(){o(),n.close(),n.emit("error",new Error("timeout"));}),s);this.opts.autoUnref&&c.unref(),this.subs.push((function(){clearTimeout(c);}));}return this.subs.push(o),this.subs.push(i),this}},{key:"connect",value:function(t){return this.open(t)}},{key:"onopen",value:function(){this.cleanup(),this._readyState="open",this.emitReserved("open");var t=this.engine;this.subs.push(l.on(t,"ping",this.onping.bind(this)),l.on(t,"data",this.ondata.bind(this)),l.on(t,"error",this.onerror.bind(this)),l.on(t,"close",this.onclose.bind(this)),l.on(this.decoder,"decoded",this.ondecoded.bind(this)));}},{key:"onping",value:function(){this.emitReserved("ping");}},{key:"ondata",value:function(t){this.decoder.add(t);}},{key:"ondecoded",value:function(t){this.emitReserved("packet",t);}},{key:"onerror",value:function(t){this.emitReserved("error",t);}},{key:"socket",value:function(t,e){var n=this.nsps[t];return n||(n=new f.Socket(this,t,e),this.nsps[t]=n),n}},{key:"_destroy",value:function(t){for(var e=0,n=Object.keys(this.nsps);e<n.length;e++){var r=n[e];if(this.nsps[r].active)return}this._close();}},{key:"_packet",value:function(t){for(var e=this.encoder.encode(t),n=0;n<e.length;n++)this.engine.write(e[n],t.options);}},{key:"cleanup",value:function(){this.subs.forEach((function(t){return t()})),this.subs.length=0,this.decoder.destroy();}},{key:"_close",value:function(){this.skipReconnect=!0,this._reconnecting=!1,"opening"===this._readyState&&this.cleanup(),this.backoff.reset(),this._readyState="closed",this.engine&&this.engine.close();}},{key:"disconnect",value:function(){return this._close()}},{key:"onclose",value:function(t){this.cleanup(),this.backoff.reset(),this._readyState="closed",this.emitReserved("close",t),this._reconnection&&!this.skipReconnect&&this.reconnect();}},{key:"reconnect",value:function(){var t=this;if(this._reconnecting||this.skipReconnect)return this;var e=this;if(this.backoff.attempts>=this._reconnectionAttempts)this.backoff.reset(),this.emitReserved("reconnect_failed"),this._reconnecting=!1;else {var n=this.backoff.duration();this._reconnecting=!0;var r=setTimeout((function(){e.skipReconnect||(t.emitReserved("reconnect_attempt",e.backoff.attempts),e.skipReconnect||e.open((function(n){n?(e._reconnecting=!1,e.reconnect(),t.emitReserved("reconnect_error",n)):e.onreconnect();})));}),n);this.opts.autoUnref&&r.unref(),this.subs.push((function(){clearTimeout(r);}));}}},{key:"onreconnect",value:function(){var t=this.backoff.attempts;this._reconnecting=!1,this.backoff.reset(),this.emitReserved("reconnect",t);}}])&&o(e.prototype,n),y}(n(17).StrictEventEmitter);e.Manager=y;},function(t,e,n){var r=n(9),o=n(23),i=n(27),s=n(28);e.polling=function(t){var e=!1,n=!1,s=!1!==t.jsonp;if("undefined"!=typeof location){var c="https:"===location.protocol,a=location.port;a||(a=c?443:80),e=t.hostname!==location.hostname||a!==t.port,n=t.secure!==c;}if(t.xdomain=e,t.xscheme=n,"open"in new r(t)&&!t.forceJSONP)return new o(t);if(!s)throw new Error("JSONP disabled");return new i(t)},e.websocket=s;},function(t,e,n){var r=n(22),o=n(2);t.exports=function(t){var e=t.xdomain,n=t.xscheme,i=t.enablesXDR;try{if("undefined"!=typeof XMLHttpRequest&&(!e||r))return new XMLHttpRequest}catch(t){}try{if("undefined"!=typeof XDomainRequest&&!n&&i)return new XDomainRequest}catch(t){}if(!e)try{return new(o[["Active"].concat("Object").join("X")])("Microsoft.XMLHTTP")}catch(t){}};},function(t,e,n){function r(t){return (r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){for(var n=0;n<e.length;n++){var r=e[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r);}}function s(t,e){return (s=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function c(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return !1;if(Reflect.construct.sham)return !1;if("function"==typeof Proxy)return !0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(t){return !1}}();return function(){var n,r=u(t);if(e){var o=u(this).constructor;n=Reflect.construct(r,arguments,o);}else n=r.apply(this,arguments);return a(this,n)}}function a(t,e){return !e||"object"!==r(e)&&"function"!=typeof e?function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t):e}function u(t){return (u=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}var f=n(3),p=n(4),l=n(0),h=n(12),y=function(t){!function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),e&&s(t,e);}(u,t);var e,n,a=c(u);function u(){return o(this,u),a.apply(this,arguments)}return e=u,(n=[{key:"doOpen",value:function(){this.poll();}},{key:"pause",value:function(t){var e=this;function n(){e.readyState="paused",t();}if(this.readyState="pausing",this.polling||!this.writable){var r=0;this.polling&&(r++,this.once("pollComplete",(function(){--r||n();}))),this.writable||(r++,this.once("drain",(function(){--r||n();})));}else n();}},{key:"poll",value:function(){this.polling=!0,this.doPoll(),this.emit("poll");}},{key:"onData",value:function(t){var e=this;l.decodePayload(t,this.socket.binaryType).forEach((function(t,n,r){if("opening"===e.readyState&&"open"===t.type&&e.onOpen(),"close"===t.type)return e.onClose(),!1;e.onPacket(t);})),"closed"!==this.readyState&&(this.polling=!1,this.emit("pollComplete"),"open"===this.readyState&&this.poll());}},{key:"doClose",value:function(){var t=this;function e(){t.write([{type:"close"}]);}"open"===this.readyState?e():this.once("open",e);}},{key:"write",value:function(t){var e=this;this.writable=!1,l.encodePayload(t,(function(t){e.doWrite(t,(function(){e.writable=!0,e.emit("drain");}));}));}},{key:"uri",value:function(){var t=this.query||{},e=this.opts.secure?"https":"http",n="";return !1!==this.opts.timestampRequests&&(t[this.opts.timestampParam]=h()),this.supportsBinary||t.sid||(t.b64=1),t=p.encode(t),this.opts.port&&("https"===e&&443!==Number(this.opts.port)||"http"===e&&80!==Number(this.opts.port))&&(n=":"+this.opts.port),t.length&&(t="?"+t),e+"://"+(-1!==this.opts.hostname.indexOf(":")?"["+this.opts.hostname+"]":this.opts.hostname)+n+this.opts.path+t}},{key:"name",get:function(){return "polling"}}])&&i(e.prototype,n),u}(f);t.exports=y;},function(t,e){var n=Object.create(null);n.open="0",n.close="1",n.ping="2",n.pong="3",n.message="4",n.upgrade="5",n.noop="6";var r=Object.create(null);Object.keys(n).forEach((function(t){r[n[t]]=t;}));t.exports={PACKET_TYPES:n,PACKET_TYPES_REVERSE:r,ERROR_PACKET:{type:"error",data:"parser error"}};},function(t,e,n){var r,o="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_".split(""),i={},s=0,c=0;function a(t){var e="";do{e=o[t%64]+e,t=Math.floor(t/64);}while(t>0);return e}function u(){var t=a(+new Date);return t!==r?(s=0,r=t):t+"."+a(s++)}for(;c<64;c++)i[o[c]]=c;u.encode=a,u.decode=function(t){var e=0;for(c=0;c<t.length;c++)e=64*e+i[t.charAt(c)];return e},t.exports=u;},function(t,e){t.exports.pick=function(t){for(var e=arguments.length,n=new Array(e>1?e-1:0),r=1;r<e;r++)n[r-1]=arguments[r];return n.reduce((function(e,n){return t.hasOwnProperty(n)&&(e[n]=t[n]),e}),{})};},function(t,e,n){function r(t){return (r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function o(t,e){var n;if("undefined"==typeof Symbol||null==t[Symbol.iterator]){if(Array.isArray(t)||(n=function(t,e){if(!t)return;if("string"==typeof t)return i(t,e);var n=Object.prototype.toString.call(t).slice(8,-1);"Object"===n&&t.constructor&&(n=t.constructor.name);if("Map"===n||"Set"===n)return Array.from(t);if("Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n))return i(t,e)}(t))||e&&t&&"number"==typeof t.length){n&&(t=n);var r=0,o=function(){};return {s:o,n:function(){return r>=t.length?{done:!0}:{done:!1,value:t[r++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var s,c=!0,a=!1;return {s:function(){n=t[Symbol.iterator]();},n:function(){var t=n.next();return c=t.done,t},e:function(t){a=!0,s=t;},f:function(){try{c||null==n.return||n.return();}finally{if(a)throw s}}}}function i(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,r=new Array(e);n<e;n++)r[n]=t[n];return r}function s(t,e){for(var n=0;n<e.length;n++){var r=e[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r);}}function c(t,e,n){return (c="undefined"!=typeof Reflect&&Reflect.get?Reflect.get:function(t,e,n){var r=function(t,e){for(;!Object.prototype.hasOwnProperty.call(t,e)&&null!==(t=p(t)););return t}(t,e);if(r){var o=Object.getOwnPropertyDescriptor(r,e);return o.get?o.get.call(n):o.value}})(t,e,n||t)}function a(t,e){return (a=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function u(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return !1;if(Reflect.construct.sham)return !1;if("function"==typeof Proxy)return !0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(t){return !1}}();return function(){var n,r=p(t);if(e){var o=p(this).constructor;n=Reflect.construct(r,arguments,o);}else n=r.apply(this,arguments);return f(this,n)}}function f(t,e){return !e||"object"!==r(e)&&"function"!=typeof e?function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t):e}function p(t){return (p=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}Object.defineProperty(e,"__esModule",{value:!0}),e.Socket=void 0;var l=n(5),h=n(16),y=n(17),d=Object.freeze({connect:1,connect_error:1,disconnect:1,disconnecting:1,newListener:1,removeListener:1}),v=function(t){!function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),e&&a(t,e);}(f,t);var e,n,i=u(f);function f(t,e,n){var r;return function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,f),(r=i.call(this)).receiveBuffer=[],r.sendBuffer=[],r.ids=0,r.acks={},r.flags={},r.io=t,r.nsp=e,r.ids=0,r.acks={},r.receiveBuffer=[],r.sendBuffer=[],r.connected=!1,r.disconnected=!0,r.flags={},n&&n.auth&&(r.auth=n.auth),r.io._autoConnect&&r.open(),r}return e=f,(n=[{key:"subEvents",value:function(){if(!this.subs){var t=this.io;this.subs=[h.on(t,"open",this.onopen.bind(this)),h.on(t,"packet",this.onpacket.bind(this)),h.on(t,"error",this.onerror.bind(this)),h.on(t,"close",this.onclose.bind(this))];}}},{key:"connect",value:function(){return this.connected||(this.subEvents(),this.io._reconnecting||this.io.open(),"open"===this.io._readyState&&this.onopen()),this}},{key:"open",value:function(){return this.connect()}},{key:"send",value:function(){for(var t=arguments.length,e=new Array(t),n=0;n<t;n++)e[n]=arguments[n];return e.unshift("message"),this.emit.apply(this,e),this}},{key:"emit",value:function(t){if(d.hasOwnProperty(t))throw new Error('"'+t+'" is a reserved event name');for(var e=arguments.length,n=new Array(e>1?e-1:0),r=1;r<e;r++)n[r-1]=arguments[r];n.unshift(t);var o={type:l.PacketType.EVENT,data:n,options:{}};o.options.compress=!1!==this.flags.compress,"function"==typeof n[n.length-1]&&(this.acks[this.ids]=n.pop(),o.id=this.ids++);var i=this.io.engine&&this.io.engine.transport&&this.io.engine.transport.writable,s=this.flags.volatile&&(!i||!this.connected);return s||(this.connected?this.packet(o):this.sendBuffer.push(o)),this.flags={},this}},{key:"packet",value:function(t){t.nsp=this.nsp,this.io._packet(t);}},{key:"onopen",value:function(){var t=this;"function"==typeof this.auth?this.auth((function(e){t.packet({type:l.PacketType.CONNECT,data:e});})):this.packet({type:l.PacketType.CONNECT,data:this.auth});}},{key:"onerror",value:function(t){this.connected||this.emitReserved("connect_error",t);}},{key:"onclose",value:function(t){this.connected=!1,this.disconnected=!0,delete this.id,this.emitReserved("disconnect",t);}},{key:"onpacket",value:function(t){if(t.nsp===this.nsp)switch(t.type){case l.PacketType.CONNECT:if(t.data&&t.data.sid){var e=t.data.sid;this.onconnect(e);}else this.emitReserved("connect_error",new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));break;case l.PacketType.EVENT:case l.PacketType.BINARY_EVENT:this.onevent(t);break;case l.PacketType.ACK:case l.PacketType.BINARY_ACK:this.onack(t);break;case l.PacketType.DISCONNECT:this.ondisconnect();break;case l.PacketType.CONNECT_ERROR:var n=new Error(t.data.message);n.data=t.data.data,this.emitReserved("connect_error",n);}}},{key:"onevent",value:function(t){var e=t.data||[];null!=t.id&&e.push(this.ack(t.id)),this.connected?this.emitEvent(e):this.receiveBuffer.push(Object.freeze(e));}},{key:"emitEvent",value:function(t){if(this._anyListeners&&this._anyListeners.length){var e,n=o(this._anyListeners.slice());try{for(n.s();!(e=n.n()).done;)e.value.apply(this,t);}catch(t){n.e(t);}finally{n.f();}}c(p(f.prototype),"emit",this).apply(this,t);}},{key:"ack",value:function(t){var e=this,n=!1;return function(){if(!n){n=!0;for(var r=arguments.length,o=new Array(r),i=0;i<r;i++)o[i]=arguments[i];e.packet({type:l.PacketType.ACK,id:t,data:o});}}}},{key:"onack",value:function(t){var e=this.acks[t.id];"function"==typeof e&&(e.apply(this,t.data),delete this.acks[t.id]);}},{key:"onconnect",value:function(t){this.id=t,this.connected=!0,this.disconnected=!1,this.emitBuffered(),this.emitReserved("connect");}},{key:"emitBuffered",value:function(){var t=this;this.receiveBuffer.forEach((function(e){return t.emitEvent(e)})),this.receiveBuffer=[],this.sendBuffer.forEach((function(e){return t.packet(e)})),this.sendBuffer=[];}},{key:"ondisconnect",value:function(){this.destroy(),this.onclose("io server disconnect");}},{key:"destroy",value:function(){this.subs&&(this.subs.forEach((function(t){return t()})),this.subs=void 0),this.io._destroy(this);}},{key:"disconnect",value:function(){return this.connected&&this.packet({type:l.PacketType.DISCONNECT}),this.destroy(),this.connected&&this.onclose("io client disconnect"),this}},{key:"close",value:function(){return this.disconnect()}},{key:"compress",value:function(t){return this.flags.compress=t,this}},{key:"onAny",value:function(t){return this._anyListeners=this._anyListeners||[],this._anyListeners.push(t),this}},{key:"prependAny",value:function(t){return this._anyListeners=this._anyListeners||[],this._anyListeners.unshift(t),this}},{key:"offAny",value:function(t){if(!this._anyListeners)return this;if(t){for(var e=this._anyListeners,n=0;n<e.length;n++)if(t===e[n])return e.splice(n,1),this}else this._anyListeners=[];return this}},{key:"listenersAny",value:function(){return this._anyListeners||[]}},{key:"active",get:function(){return !!this.subs}},{key:"volatile",get:function(){return this.flags.volatile=!0,this}}])&&s(e.prototype,n),f}(y.StrictEventEmitter);e.Socket=v;},function(t,e,n){function r(t){return (r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}Object.defineProperty(e,"__esModule",{value:!0}),e.hasBinary=e.isBinary=void 0;var o="function"==typeof ArrayBuffer,i=Object.prototype.toString,s="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===i.call(Blob),c="function"==typeof File||"undefined"!=typeof File&&"[object FileConstructor]"===i.call(File);function a(t){return o&&(t instanceof ArrayBuffer||function(t){return "function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(t):t.buffer instanceof ArrayBuffer}(t))||s&&t instanceof Blob||c&&t instanceof File}e.isBinary=a,e.hasBinary=function t(e,n){if(!e||"object"!==r(e))return !1;if(Array.isArray(e)){for(var o=0,i=e.length;o<i;o++)if(t(e[o]))return !0;return !1}if(a(e))return !0;if(e.toJSON&&"function"==typeof e.toJSON&&1===arguments.length)return t(e.toJSON(),!0);for(var s in e)if(Object.prototype.hasOwnProperty.call(e,s)&&t(e[s]))return !0;return !1};},function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0}),e.on=void 0,e.on=function(t,e,n){return t.on(e,n),function(){t.off(e,n);}};},function(t,e,n){function r(t){return (r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){for(var n=0;n<e.length;n++){var r=e[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r);}}function s(t,e,n){return (s="undefined"!=typeof Reflect&&Reflect.get?Reflect.get:function(t,e,n){var r=function(t,e){for(;!Object.prototype.hasOwnProperty.call(t,e)&&null!==(t=f(t)););return t}(t,e);if(r){var o=Object.getOwnPropertyDescriptor(r,e);return o.get?o.get.call(n):o.value}})(t,e,n||t)}function c(t,e){return (c=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function a(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return !1;if(Reflect.construct.sham)return !1;if("function"==typeof Proxy)return !0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(t){return !1}}();return function(){var n,r=f(t);if(e){var o=f(this).constructor;n=Reflect.construct(r,arguments,o);}else n=r.apply(this,arguments);return u(this,n)}}function u(t,e){return !e||"object"!==r(e)&&"function"!=typeof e?function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t):e}function f(t){return (f=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}Object.defineProperty(e,"__esModule",{value:!0}),e.StrictEventEmitter=void 0;var p=function(t){!function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),e&&c(t,e);}(p,t);var e,n,u=a(p);function p(){return o(this,p),u.apply(this,arguments)}return e=p,(n=[{key:"on",value:function(t,e){return s(f(p.prototype),"on",this).call(this,t,e),this}},{key:"once",value:function(t,e){return s(f(p.prototype),"once",this).call(this,t,e),this}},{key:"emit",value:function(t){for(var e,n=arguments.length,r=new Array(n>1?n-1:0),o=1;o<n;o++)r[o-1]=arguments[o];return (e=s(f(p.prototype),"emit",this)).call.apply(e,[this,t].concat(r)),this}},{key:"emitReserved",value:function(t){for(var e,n=arguments.length,r=new Array(n>1?n-1:0),o=1;o<n;o++)r[o-1]=arguments[o];return (e=s(f(p.prototype),"emit",this)).call.apply(e,[this,t].concat(r)),this}},{key:"listeners",value:function(t){return s(f(p.prototype),"listeners",this).call(this,t)}}])&&i(e.prototype,n),p}(n(1));e.StrictEventEmitter=p;},function(t,e,n){function r(t){return (r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}Object.defineProperty(e,"__esModule",{value:!0}),e.io=e.Socket=e.Manager=e.protocol=void 0;var o=n(19),i=n(7);t.exports=e=c;var s=e.managers={};function c(t,e){"object"===r(t)&&(e=t,t=void 0),e=e||{};var n,c=o.url(t,e.path||"/socket.io"),a=c.source,u=c.id,f=c.path,p=s[u]&&f in s[u].nsps;return e.forceNew||e["force new connection"]||!1===e.multiplex||p?n=new i.Manager(a,e):(s[u]||(s[u]=new i.Manager(a,e)),n=s[u]),c.query&&!e.query&&(e.query=c.queryKey),n.socket(c.path,e)}e.io=c;var a=n(5);Object.defineProperty(e,"protocol",{enumerable:!0,get:function(){return a.protocol}}),e.connect=c;var u=n(7);Object.defineProperty(e,"Manager",{enumerable:!0,get:function(){return u.Manager}});var f=n(14);Object.defineProperty(e,"Socket",{enumerable:!0,get:function(){return f.Socket}}),e.default=c;},function(t,e,n){Object.defineProperty(e,"__esModule",{value:!0}),e.url=void 0;var r=n(6);e.url=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2?arguments[2]:void 0,o=t;n=n||"undefined"!=typeof location&&location,null==t&&(t=n.protocol+"//"+n.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?n.protocol+t:n.host+t),/^(https?|wss?):\/\//.test(t)||(t=void 0!==n?n.protocol+"//"+t:"https://"+t),o=r(t)),o.port||(/^(http|ws)$/.test(o.protocol)?o.port="80":/^(http|ws)s$/.test(o.protocol)&&(o.port="443")),o.path=o.path||"/";var i=-1!==o.host.indexOf(":"),s=i?"["+o.host+"]":o.host;return o.id=o.protocol+"://"+s+":"+o.port+e,o.href=o.protocol+"://"+s+(n&&n.port===o.port?"":":"+o.port),o};},function(t,e,n){var r=n(21);t.exports=function(t,e){return new r(t,e)},t.exports.Socket=r,t.exports.protocol=r.protocol,t.exports.Transport=n(3),t.exports.transports=n(8),t.exports.parser=n(0);},function(t,e,n){function r(){return (r=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r]);}return t}).apply(this,arguments)}function o(t){return (o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function s(t,e){for(var n=0;n<e.length;n++){var r=e[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r);}}function c(t,e){return (c=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function a(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return !1;if(Reflect.construct.sham)return !1;if("function"==typeof Proxy)return !0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(t){return !1}}();return function(){var n,r=f(t);if(e){var o=f(this).constructor;n=Reflect.construct(r,arguments,o);}else n=r.apply(this,arguments);return u(this,n)}}function u(t,e){return !e||"object"!==o(e)&&"function"!=typeof e?function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t):e}function f(t){return (f=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}var p=n(8),l=n(1),h=n(0),y=n(6),d=n(4),v=function(t){!function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),e&&c(t,e);}(l,t);var e,n,f=a(l);function l(t){var e,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return i(this,l),e=f.call(this),t&&"object"===o(t)&&(n=t,t=null),t?(t=y(t),n.hostname=t.host,n.secure="https"===t.protocol||"wss"===t.protocol,n.port=t.port,t.query&&(n.query=t.query)):n.host&&(n.hostname=y(n.host).host),e.secure=null!=n.secure?n.secure:"undefined"!=typeof location&&"https:"===location.protocol,n.hostname&&!n.port&&(n.port=e.secure?"443":"80"),e.hostname=n.hostname||("undefined"!=typeof location?location.hostname:"localhost"),e.port=n.port||("undefined"!=typeof location&&location.port?location.port:e.secure?443:80),e.transports=n.transports||["polling","websocket"],e.readyState="",e.writeBuffer=[],e.prevBufferLen=0,e.opts=r({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,jsonp:!0,timestampParam:"t",rememberUpgrade:!1,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{}},n),e.opts.path=e.opts.path.replace(/\/$/,"")+"/","string"==typeof e.opts.query&&(e.opts.query=d.decode(e.opts.query)),e.id=null,e.upgrades=null,e.pingInterval=null,e.pingTimeout=null,e.pingTimeoutTimer=null,"function"==typeof addEventListener&&(addEventListener("beforeunload",(function(){e.transport&&(e.transport.removeAllListeners(),e.transport.close());}),!1),"localhost"!==e.hostname&&(e.offlineEventListener=function(){e.onClose("transport close");},addEventListener("offline",e.offlineEventListener,!1))),e.open(),e}return e=l,(n=[{key:"createTransport",value:function(t){var e=function(t){var e={};for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}(this.opts.query);e.EIO=h.protocol,e.transport=t,this.id&&(e.sid=this.id);var n=r({},this.opts.transportOptions[t],this.opts,{query:e,socket:this,hostname:this.hostname,secure:this.secure,port:this.port});return new p[t](n)}},{key:"open",value:function(){var t;if(this.opts.rememberUpgrade&&l.priorWebsocketSuccess&&-1!==this.transports.indexOf("websocket"))t="websocket";else {if(0===this.transports.length){var e=this;return void setTimeout((function(){e.emit("error","No transports available");}),0)}t=this.transports[0];}this.readyState="opening";try{t=this.createTransport(t);}catch(t){return this.transports.shift(),void this.open()}t.open(),this.setTransport(t);}},{key:"setTransport",value:function(t){var e=this;this.transport&&this.transport.removeAllListeners(),this.transport=t,t.on("drain",(function(){e.onDrain();})).on("packet",(function(t){e.onPacket(t);})).on("error",(function(t){e.onError(t);})).on("close",(function(){e.onClose("transport close");}));}},{key:"probe",value:function(t){var e=this.createTransport(t,{probe:1}),n=!1,r=this;function o(){if(r.onlyBinaryUpgrades){var t=!this.supportsBinary&&r.transport.supportsBinary;n=n||t;}n||(e.send([{type:"ping",data:"probe"}]),e.once("packet",(function(t){if(!n)if("pong"===t.type&&"probe"===t.data){if(r.upgrading=!0,r.emit("upgrading",e),!e)return;l.priorWebsocketSuccess="websocket"===e.name,r.transport.pause((function(){n||"closed"!==r.readyState&&(f(),r.setTransport(e),e.send([{type:"upgrade"}]),r.emit("upgrade",e),e=null,r.upgrading=!1,r.flush());}));}else {var o=new Error("probe error");o.transport=e.name,r.emit("upgradeError",o);}})));}function i(){n||(n=!0,f(),e.close(),e=null);}function s(t){var n=new Error("probe error: "+t);n.transport=e.name,i(),r.emit("upgradeError",n);}function c(){s("transport closed");}function a(){s("socket closed");}function u(t){e&&t.name!==e.name&&i();}function f(){e.removeListener("open",o),e.removeListener("error",s),e.removeListener("close",c),r.removeListener("close",a),r.removeListener("upgrading",u);}l.priorWebsocketSuccess=!1,e.once("open",o),e.once("error",s),e.once("close",c),this.once("close",a),this.once("upgrading",u),e.open();}},{key:"onOpen",value:function(){if(this.readyState="open",l.priorWebsocketSuccess="websocket"===this.transport.name,this.emit("open"),this.flush(),"open"===this.readyState&&this.opts.upgrade&&this.transport.pause)for(var t=0,e=this.upgrades.length;t<e;t++)this.probe(this.upgrades[t]);}},{key:"onPacket",value:function(t){if("opening"===this.readyState||"open"===this.readyState||"closing"===this.readyState)switch(this.emit("packet",t),this.emit("heartbeat"),t.type){case"open":this.onHandshake(JSON.parse(t.data));break;case"ping":this.resetPingTimeout(),this.sendPacket("pong"),this.emit("pong");break;case"error":var e=new Error("server error");e.code=t.data,this.onError(e);break;case"message":this.emit("data",t.data),this.emit("message",t.data);}}},{key:"onHandshake",value:function(t){this.emit("handshake",t),this.id=t.sid,this.transport.query.sid=t.sid,this.upgrades=this.filterUpgrades(t.upgrades),this.pingInterval=t.pingInterval,this.pingTimeout=t.pingTimeout,this.onOpen(),"closed"!==this.readyState&&this.resetPingTimeout();}},{key:"resetPingTimeout",value:function(){var t=this;clearTimeout(this.pingTimeoutTimer),this.pingTimeoutTimer=setTimeout((function(){t.onClose("ping timeout");}),this.pingInterval+this.pingTimeout),this.opts.autoUnref&&this.pingTimeoutTimer.unref();}},{key:"onDrain",value:function(){this.writeBuffer.splice(0,this.prevBufferLen),this.prevBufferLen=0,0===this.writeBuffer.length?this.emit("drain"):this.flush();}},{key:"flush",value:function(){"closed"!==this.readyState&&this.transport.writable&&!this.upgrading&&this.writeBuffer.length&&(this.transport.send(this.writeBuffer),this.prevBufferLen=this.writeBuffer.length,this.emit("flush"));}},{key:"write",value:function(t,e,n){return this.sendPacket("message",t,e,n),this}},{key:"send",value:function(t,e,n){return this.sendPacket("message",t,e,n),this}},{key:"sendPacket",value:function(t,e,n,r){if("function"==typeof e&&(r=e,e=void 0),"function"==typeof n&&(r=n,n=null),"closing"!==this.readyState&&"closed"!==this.readyState){(n=n||{}).compress=!1!==n.compress;var o={type:t,data:e,options:n};this.emit("packetCreate",o),this.writeBuffer.push(o),r&&this.once("flush",r),this.flush();}}},{key:"close",value:function(){var t=this;function e(){t.onClose("forced close"),t.transport.close();}function n(){t.removeListener("upgrade",n),t.removeListener("upgradeError",n),e();}function r(){t.once("upgrade",n),t.once("upgradeError",n);}return "opening"!==this.readyState&&"open"!==this.readyState||(this.readyState="closing",this.writeBuffer.length?this.once("drain",(function(){this.upgrading?r():e();})):this.upgrading?r():e()),this}},{key:"onError",value:function(t){l.priorWebsocketSuccess=!1,this.emit("error",t),this.onClose("transport error",t);}},{key:"onClose",value:function(t,e){"opening"!==this.readyState&&"open"!==this.readyState&&"closing"!==this.readyState||(clearTimeout(this.pingIntervalTimer),clearTimeout(this.pingTimeoutTimer),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),"function"==typeof removeEventListener&&removeEventListener("offline",this.offlineEventListener,!1),this.readyState="closed",this.id=null,this.emit("close",t,e),this.writeBuffer=[],this.prevBufferLen=0);}},{key:"filterUpgrades",value:function(t){for(var e=[],n=0,r=t.length;n<r;n++)~this.transports.indexOf(t[n])&&e.push(t[n]);return e}}])&&s(e.prototype,n),l}(l);v.priorWebsocketSuccess=!1,v.protocol=h.protocol,t.exports=v;},function(t,e){try{t.exports="undefined"!=typeof XMLHttpRequest&&"withCredentials"in new XMLHttpRequest;}catch(e){t.exports=!1;}},function(t,e,n){function r(t){return (r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function o(){return (o=Object.assign||function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(t[r]=n[r]);}return t}).apply(this,arguments)}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function s(t,e){for(var n=0;n<e.length;n++){var r=e[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r);}}function c(t,e,n){return e&&s(t.prototype,e),n&&s(t,n),t}function a(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),e&&u(t,e);}function u(t,e){return (u=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function f(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return !1;if(Reflect.construct.sham)return !1;if("function"==typeof Proxy)return !0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(t){return !1}}();return function(){var n,r=l(t);if(e){var o=l(this).constructor;n=Reflect.construct(r,arguments,o);}else n=r.apply(this,arguments);return p(this,n)}}function p(t,e){return !e||"object"!==r(e)&&"function"!=typeof e?function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t):e}function l(t){return (l=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}var h=n(9),y=n(10),d=n(1),v=n(13).pick,b=n(2);function m(){}var g=null!=new h({xdomain:!1}).responseType,k=function(t){a(n,t);var e=f(n);function n(t){var r;if(i(this,n),r=e.call(this,t),"undefined"!=typeof location){var o="https:"===location.protocol,s=location.port;s||(s=o?443:80),r.xd="undefined"!=typeof location&&t.hostname!==location.hostname||s!==t.port,r.xs=t.secure!==o;}var c=t&&t.forceBase64;return r.supportsBinary=g&&!c,r}return c(n,[{key:"request",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return o(t,{xd:this.xd,xs:this.xs},this.opts),new w(this.uri(),t)}},{key:"doWrite",value:function(t,e){var n=this.request({method:"POST",data:t}),r=this;n.on("success",e),n.on("error",(function(t){r.onError("xhr post error",t);}));}},{key:"doPoll",value:function(){var t=this.request(),e=this;t.on("data",(function(t){e.onData(t);})),t.on("error",(function(t){e.onError("xhr poll error",t);})),this.pollXhr=t;}}]),n}(y),w=function(t){a(n,t);var e=f(n);function n(t,r){var o;return i(this,n),(o=e.call(this)).opts=r,o.method=r.method||"GET",o.uri=t,o.async=!1!==r.async,o.data=void 0!==r.data?r.data:null,o.create(),o}return c(n,[{key:"create",value:function(){var t=v(this.opts,"agent","enablesXDR","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","autoUnref");t.xdomain=!!this.opts.xd,t.xscheme=!!this.opts.xs;var e=this.xhr=new h(t),r=this;try{e.open(this.method,this.uri,this.async);try{if(this.opts.extraHeaders)for(var o in e.setDisableHeaderCheck&&e.setDisableHeaderCheck(!0),this.opts.extraHeaders)this.opts.extraHeaders.hasOwnProperty(o)&&e.setRequestHeader(o,this.opts.extraHeaders[o]);}catch(t){}if("POST"===this.method)try{e.setRequestHeader("Content-type","text/plain;charset=UTF-8");}catch(t){}try{e.setRequestHeader("Accept","*/*");}catch(t){}"withCredentials"in e&&(e.withCredentials=this.opts.withCredentials),this.opts.requestTimeout&&(e.timeout=this.opts.requestTimeout),this.hasXDR()?(e.onload=function(){r.onLoad();},e.onerror=function(){r.onError(e.responseText);}):e.onreadystatechange=function(){4===e.readyState&&(200===e.status||1223===e.status?r.onLoad():setTimeout((function(){r.onError("number"==typeof e.status?e.status:0);}),0));},e.send(this.data);}catch(t){return void setTimeout((function(){r.onError(t);}),0)}"undefined"!=typeof document&&(this.index=n.requestsCount++,n.requests[this.index]=this);}},{key:"onSuccess",value:function(){this.emit("success"),this.cleanup();}},{key:"onData",value:function(t){this.emit("data",t),this.onSuccess();}},{key:"onError",value:function(t){this.emit("error",t),this.cleanup(!0);}},{key:"cleanup",value:function(t){if(void 0!==this.xhr&&null!==this.xhr){if(this.hasXDR()?this.xhr.onload=this.xhr.onerror=m:this.xhr.onreadystatechange=m,t)try{this.xhr.abort();}catch(t){}"undefined"!=typeof document&&delete n.requests[this.index],this.xhr=null;}}},{key:"onLoad",value:function(){var t=this.xhr.responseText;null!==t&&this.onData(t);}},{key:"hasXDR",value:function(){return "undefined"!=typeof XDomainRequest&&!this.xs&&this.enablesXDR}},{key:"abort",value:function(){this.cleanup();}}]),n}(d);if(w.requestsCount=0,w.requests={},"undefined"!=typeof document)if("function"==typeof attachEvent)attachEvent("onunload",_);else if("function"==typeof addEventListener){addEventListener("onpagehide"in b?"pagehide":"unload",_,!1);}function _(){for(var t in w.requests)w.requests.hasOwnProperty(t)&&w.requests[t].abort();}t.exports=k,t.exports.Request=w;},function(t,e,n){var r=n(11).PACKET_TYPES,o="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===Object.prototype.toString.call(Blob),i="function"==typeof ArrayBuffer,s=function(t,e){var n=new FileReader;return n.onload=function(){var t=n.result.split(",")[1];e("b"+t);},n.readAsDataURL(t)};t.exports=function(t,e,n){var c,a=t.type,u=t.data;return o&&u instanceof Blob?e?n(u):s(u,n):i&&(u instanceof ArrayBuffer||(c=u,"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(c):c&&c.buffer instanceof ArrayBuffer))?e?n(u instanceof ArrayBuffer?u:u.buffer):s(new Blob([u]),n):n(r[a]+(u||""))};},function(t,e,n){var r,o=n(11),i=o.PACKET_TYPES_REVERSE,s=o.ERROR_PACKET;"function"==typeof ArrayBuffer&&(r=n(26));var c=function(t,e){if(r){var n=r.decode(t);return a(n,e)}return {base64:!0,data:t}},a=function(t,e){switch(e){case"blob":return t instanceof ArrayBuffer?new Blob([t]):t;case"arraybuffer":default:return t}};t.exports=function(t,e){if("string"!=typeof t)return {type:"message",data:a(t,e)};var n=t.charAt(0);return "b"===n?{type:"message",data:c(t.substring(1),e)}:i[n]?t.length>1?{type:i[n],data:t.substring(1)}:{type:i[n]}:s};},function(t,e){!function(t){e.encode=function(e){var n,r=new Uint8Array(e),o=r.length,i="";for(n=0;n<o;n+=3)i+=t[r[n]>>2],i+=t[(3&r[n])<<4|r[n+1]>>4],i+=t[(15&r[n+1])<<2|r[n+2]>>6],i+=t[63&r[n+2]];return o%3==2?i=i.substring(0,i.length-1)+"=":o%3==1&&(i=i.substring(0,i.length-2)+"=="),i},e.decode=function(e){var n,r,o,i,s,c=.75*e.length,a=e.length,u=0;"="===e[e.length-1]&&(c--,"="===e[e.length-2]&&c--);var f=new ArrayBuffer(c),p=new Uint8Array(f);for(n=0;n<a;n+=4)r=t.indexOf(e[n]),o=t.indexOf(e[n+1]),i=t.indexOf(e[n+2]),s=t.indexOf(e[n+3]),p[u++]=r<<2|o>>4,p[u++]=(15&o)<<4|i>>2,p[u++]=(3&i)<<6|63&s;return f};}("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");},function(t,e,n){function r(t){return (r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function o(t,e){for(var n=0;n<e.length;n++){var r=e[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r);}}function i(t,e,n){return (i="undefined"!=typeof Reflect&&Reflect.get?Reflect.get:function(t,e,n){var r=function(t,e){for(;!Object.prototype.hasOwnProperty.call(t,e)&&null!==(t=f(t)););return t}(t,e);if(r){var o=Object.getOwnPropertyDescriptor(r,e);return o.get?o.get.call(n):o.value}})(t,e,n||t)}function s(t,e){return (s=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function c(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return !1;if(Reflect.construct.sham)return !1;if("function"==typeof Proxy)return !0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(t){return !1}}();return function(){var n,r=f(t);if(e){var o=f(this).constructor;n=Reflect.construct(r,arguments,o);}else n=r.apply(this,arguments);return a(this,n)}}function a(t,e){return !e||"object"!==r(e)&&"function"!=typeof e?u(t):e}function u(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function f(t){return (f=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}var p,l=n(10),h=n(2),y=/\n/g,d=/\\n/g,v=function(t){!function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),e&&s(t,e);}(l,t);var e,n,a=c(l);function l(t){var e;!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,l),(e=a.call(this,t)).query=e.query||{},p||(p=h.___eio=h.___eio||[]),e.index=p.length;var n=u(e);return p.push((function(t){n.onData(t);})),e.query.j=e.index,e}return e=l,(n=[{key:"doClose",value:function(){this.script&&(this.script.onerror=function(){},this.script.parentNode.removeChild(this.script),this.script=null),this.form&&(this.form.parentNode.removeChild(this.form),this.form=null,this.iframe=null),i(f(l.prototype),"doClose",this).call(this);}},{key:"doPoll",value:function(){var t=this,e=document.createElement("script");this.script&&(this.script.parentNode.removeChild(this.script),this.script=null),e.async=!0,e.src=this.uri(),e.onerror=function(e){t.onError("jsonp poll error",e);};var n=document.getElementsByTagName("script")[0];n?n.parentNode.insertBefore(e,n):(document.head||document.body).appendChild(e),this.script=e,"undefined"!=typeof navigator&&/gecko/i.test(navigator.userAgent)&&setTimeout((function(){var t=document.createElement("iframe");document.body.appendChild(t),document.body.removeChild(t);}),100);}},{key:"doWrite",value:function(t,e){var n,r=this;if(!this.form){var o=document.createElement("form"),i=document.createElement("textarea"),s=this.iframeId="eio_iframe_"+this.index;o.className="socketio",o.style.position="absolute",o.style.top="-1000px",o.style.left="-1000px",o.target=s,o.method="POST",o.setAttribute("accept-charset","utf-8"),i.name="d",o.appendChild(i),document.body.appendChild(o),this.form=o,this.area=i;}function c(){a(),e();}function a(){if(r.iframe)try{r.form.removeChild(r.iframe);}catch(t){r.onError("jsonp polling iframe removal error",t);}try{var t='<iframe src="javascript:0" name="'+r.iframeId+'">';n=document.createElement(t);}catch(t){(n=document.createElement("iframe")).name=r.iframeId,n.src="javascript:0";}n.id=r.iframeId,r.form.appendChild(n),r.iframe=n;}this.form.action=this.uri(),a(),t=t.replace(d,"\\\n"),this.area.value=t.replace(y,"\\n");try{this.form.submit();}catch(t){}this.iframe.attachEvent?this.iframe.onreadystatechange=function(){"complete"===r.iframe.readyState&&c();}:this.iframe.onload=c;}},{key:"supportsBinary",get:function(){return !1}}])&&o(e.prototype,n),l}(l);t.exports=v;},function(t,e,n){function r(t){return (r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function o(t,e){for(var n=0;n<e.length;n++){var r=e[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,r.key,r);}}function i(t,e){return (i=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function s(t){var e=function(){if("undefined"==typeof Reflect||!Reflect.construct)return !1;if(Reflect.construct.sham)return !1;if("function"==typeof Proxy)return !0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(t){return !1}}();return function(){var n,r=a(t);if(e){var o=a(this).constructor;n=Reflect.construct(r,arguments,o);}else n=r.apply(this,arguments);return c(this,n)}}function c(t,e){return !e||"object"!==r(e)&&"function"!=typeof e?function(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}(t):e}function a(t){return (a=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}var u=n(3),f=n(0),p=n(4),l=n(12),h=n(13).pick,y=n(29),d=y.WebSocket,v=y.usingBrowserWebSocket,b=y.defaultBinaryType,m="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),g=function(t){!function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),e&&i(t,e);}(a,t);var e,n,c=s(a);function a(t){var e;return function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,a),(e=c.call(this,t)).supportsBinary=!t.forceBase64,e}return e=a,(n=[{key:"doOpen",value:function(){if(this.check()){var t=this.uri(),e=this.opts.protocols,n=m?{}:h(this.opts,"agent","perMessageDeflate","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","localAddress","protocolVersion","origin","maxPayload","family","checkServerIdentity");this.opts.extraHeaders&&(n.headers=this.opts.extraHeaders);try{this.ws=v&&!m?e?new d(t,e):new d(t):new d(t,e,n);}catch(t){return this.emit("error",t)}this.ws.binaryType=this.socket.binaryType||b,this.addEventListeners();}}},{key:"addEventListeners",value:function(){var t=this;this.ws.onopen=function(){t.opts.autoUnref&&t.ws._socket.unref(),t.onOpen();},this.ws.onclose=this.onClose.bind(this),this.ws.onmessage=function(e){return t.onData(e.data)},this.ws.onerror=function(e){return t.onError("websocket error",e)};}},{key:"write",value:function(t){var e=this;this.writable=!1;for(var n=t.length,r=0,o=n;r<o;r++)!function(t){f.encodePacket(t,e.supportsBinary,(function(r){var o={};v||(t.options&&(o.compress=t.options.compress),e.opts.perMessageDeflate&&("string"==typeof r?Buffer.byteLength(r):r.length)<e.opts.perMessageDeflate.threshold&&(o.compress=!1));try{v?e.ws.send(r):e.ws.send(r,o);}catch(t){}--n||(e.emit("flush"),setTimeout((function(){e.writable=!0,e.emit("drain");}),0));}));}(t[r]);}},{key:"onClose",value:function(){u.prototype.onClose.call(this);}},{key:"doClose",value:function(){void 0!==this.ws&&(this.ws.close(),this.ws=null);}},{key:"uri",value:function(){var t=this.query||{},e=this.opts.secure?"wss":"ws",n="";return this.opts.port&&("wss"===e&&443!==Number(this.opts.port)||"ws"===e&&80!==Number(this.opts.port))&&(n=":"+this.opts.port),this.opts.timestampRequests&&(t[this.opts.timestampParam]=l()),this.supportsBinary||(t.b64=1),(t=p.encode(t)).length&&(t="?"+t),e+"://"+(-1!==this.opts.hostname.indexOf(":")?"["+this.opts.hostname+"]":this.opts.hostname)+n+this.opts.path+t}},{key:"check",value:function(){return !(!d||"__initialize"in d&&this.name===a.prototype.name)}},{key:"name",get:function(){return "websocket"}}])&&o(e.prototype,n),a}(u);t.exports=g;},function(t,e,n){var r=n(2);t.exports={WebSocket:r.WebSocket||r.MozWebSocket,usingBrowserWebSocket:!0,defaultBinaryType:"arraybuffer"};},function(t,e,n){function r(t){return (r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}Object.defineProperty(e,"__esModule",{value:!0}),e.reconstructPacket=e.deconstructPacket=void 0;var o=n(15);e.deconstructPacket=function(t){var e=[],n=t.data,i=t;return i.data=function t(e,n){if(!e)return e;if(o.isBinary(e)){var i={_placeholder:!0,num:n.length};return n.push(e),i}if(Array.isArray(e)){for(var s=new Array(e.length),c=0;c<e.length;c++)s[c]=t(e[c],n);return s}if("object"===r(e)&&!(e instanceof Date)){var a={};for(var u in e)e.hasOwnProperty(u)&&(a[u]=t(e[u],n));return a}return e}(n,e),i.attachments=e.length,{packet:i,buffers:e}},e.reconstructPacket=function(t,e){return t.data=function t(e,n){if(!e)return e;if(e&&e._placeholder)return n[e.num];if(Array.isArray(e))for(var o=0;o<e.length;o++)e[o]=t(e[o],n);else if("object"===r(e))for(var i in e)e.hasOwnProperty(i)&&(e[i]=t(e[i],n));return e}(t.data,e),t.attachments=void 0,t};},function(t,e){function n(t){t=t||{},this.ms=t.min||100,this.max=t.max||1e4,this.factor=t.factor||2,this.jitter=t.jitter>0&&t.jitter<=1?t.jitter:0,this.attempts=0;}t.exports=n,n.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var e=Math.random(),n=Math.floor(e*this.jitter*t);t=0==(1&Math.floor(10*e))?t-n:t+n;}return 0|Math.min(t,this.max)},n.prototype.reset=function(){this.attempts=0;},n.prototype.setMin=function(t){this.ms=t;},n.prototype.setMax=function(t){this.max=t;},n.prototype.setJitter=function(t){this.jitter=t;};}])}));
|
||
|
||
}(socket_io_min));
|
||
|
||
var lzString_min = {exports: {}};
|
||
|
||
(function (module) {
|
||
var LZString=function(){function o(o,r){if(!t[o]){t[o]={};for(var n=0;n<o.length;n++)t[o][o.charAt(n)]=n;}return t[o][r]}var r=String.fromCharCode,n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",t={},i={compressToBase64:function(o){if(null==o)return "";var r=i._compress(o,6,function(o){return n.charAt(o)});switch(r.length%4){default:case 0:return r;case 1:return r+"===";case 2:return r+"==";case 3:return r+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(e){return o(n,r.charAt(e))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(o){return null==o?"":""==o?null:i._decompress(o.length,16384,function(r){return o.charCodeAt(r)-32})},compressToUint8Array:function(o){for(var r=i.compress(o),n=new Uint8Array(2*r.length),e=0,t=r.length;t>e;e++){var s=r.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256;}return n},decompressFromUint8Array:function(o){if(null===o||void 0===o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;t>e;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o));}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(o){return null==o?"":i._compress(o,6,function(o){return e.charAt(o)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(n){return o(e,r.charAt(n))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(o,r,n){if(null==o)return "";var e,t,i,s={},p={},u="",c="",a="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<o.length;i+=1)if(u=o.charAt(i),Object.prototype.hasOwnProperty.call(s,u)||(s[u]=f++,p[u]=!0),c=a+u,Object.prototype.hasOwnProperty.call(s,c))a=c;else {if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;}else {for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a];}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++),s[c]=f++,a=String(u);}if(""!==a){if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;}else {for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a];}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++);}for(t=2,e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==r-1){d.push(n(m));break}v++;}return d.join("")},decompress:function(o){return null==o?"":""==o?null:i._decompress(o.length,32768,function(r){return o.charCodeAt(r)})},_decompress:function(o,n,e){var i,s,p,u,c,a,l,f=[],h=4,d=4,m=3,v="",w=[],A={val:e(0),position:n,index:1};for(i=0;3>i;i+=1)f[i]=i;for(p=0,c=Math.pow(2,2),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 2:return ""}for(f[3]=l,s=l,w.push(l);;){if(A.index>o)return "";for(p=0,c=Math.pow(2,m),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(l=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 2:return w.join("")}if(0==h&&(h=Math.pow(2,m),m++),f[l])v=f[l];else {if(l!==d)return null;v=s+s.charAt(0);}w.push(v),f[d++]=s+v.charAt(0),h--,s=v,0==h&&(h=Math.pow(2,m),m++);}}};return i}();null!=module&&(module.exports=LZString);
|
||
}(lzString_min));
|
||
|
||
var lzstring = lzString_min.exports;
|
||
|
||
/**
|
||
* This file contains methods related to retrieving, storing and reading the logs
|
||
* from a remote server, it is used for QA only and all of them should not get
|
||
* included in the bundle when using production mode
|
||
*/
|
||
/**
|
||
* Logger utility methods used for debugging
|
||
*
|
||
* @param {store} store - A storage solution, either sessionStorage, localStorage, indexeddb, etc
|
||
* @constructor
|
||
*/
|
||
const LoggerExternals = (store = sessionStorage) => {
|
||
{
|
||
return {
|
||
/**
|
||
* Compress a string to take less space during transmission
|
||
*
|
||
* @param {string} string - The string to compress
|
||
* @returns {string} - The compressed string
|
||
*/
|
||
compress: (string) => {
|
||
return lzstring.compressToUTF16(string);
|
||
},
|
||
/**
|
||
* Deletes the logs stored in the store
|
||
*
|
||
* @return {void}
|
||
*/
|
||
logClear: () => {
|
||
return store.clear();
|
||
},
|
||
/**
|
||
* Read the logs that were stored in the long term storage (compressed)
|
||
*
|
||
* @param {number} chunk - The chunk to read from the log file
|
||
* @param {number} chunkSize - The size of the chunk
|
||
* @return {string}
|
||
*/
|
||
logRead: (chunk = 0, chunkSize = LoggerExternals(store).logReadLength()) => {
|
||
const startPosition = chunk * chunkSize;
|
||
const endPosition = startPosition + chunkSize;
|
||
const log = [];
|
||
// Read each stored line until we read all the lines in a chunk
|
||
for (let i = startPosition; i <= endPosition; i += 1) {
|
||
const item = lzstring.decompress(store.getItem(String(i)));
|
||
if (item)
|
||
log.push(JSON.parse(item));
|
||
}
|
||
return JSON.stringify(log);
|
||
},
|
||
/**
|
||
* Read the size of the logs stored in the sessionStorage, used by the server to calculate
|
||
* chunk transmission size
|
||
*
|
||
* @return {number}
|
||
*/
|
||
logReadLength: () => {
|
||
return parseInt(store.getItem('logLength'), 10) || 0;
|
||
},
|
||
/**
|
||
* Store logs in the storage mechanism for later retrieval
|
||
*
|
||
* @param {string} level - The console level (ex: info, warn, etc)
|
||
* @param {*} logEvent - The data
|
||
*/
|
||
logStore: (level, logEvent) => {
|
||
store.setItem('logLength', String(LoggerExternals(store).logReadLength() + 1));
|
||
store.setItem(String(store.length), lzstring.compress(JSON.stringify(logEvent)));
|
||
},
|
||
};
|
||
}
|
||
};
|
||
var LoggerExternals$1 = LoggerExternals;
|
||
|
||
/**
|
||
* see https://tools.ietf.org/html/rfc1808
|
||
*
|
||
*
|
||
* This file was modified by Apple Inc.
|
||
* Modifications Copyright (c) 2018-
|
||
*
|
||
* The initial developer is https://github.com/tjenkinson/url-toolkit. Copyright (c) 2016 Tom Jenkinson.
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the “Licenseâ€); you may not
|
||
* use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software distributed
|
||
* under the License is distributed on an “AS IS†BASIS, WITHOUT WARRANTIES OR
|
||
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
||
* specific language governing permissions and limitations under the License.
|
||
*
|
||
*/
|
||
const URL_REGEX = /^((?:[^\/;?#]+:)?)(\/\/[^\/\;?#]*)?(.*?)??(;.*?)?(\?.*?)?(#.*?)?$/;
|
||
const FIRST_SEGMENT_REGEX = /^([^\/;?#]*)(.*)$/;
|
||
const SLASH_DOT_REGEX = /(?:\/|^)\.(?=\/)/g;
|
||
const SLASH_DOT_DOT_REGEX = /(?:\/|^)\.\.\/(?!\.\.\/).*?(?=\/)/g;
|
||
const URLToolkit = {
|
||
// jshint ignore:line
|
||
// If opts.alwaysNormalize is true then the path will always be normalized even when it starts with / or //
|
||
// E.g
|
||
// With opts.alwaysNormalize = false (default, spec compliant)
|
||
// http://a.com/b/cd + /e/f/../g => http://a.com/e/f/../g
|
||
// With opts.alwaysNormalize = true (default, not spec compliant)
|
||
// http://a.com/b/cd + /e/f/../g => http://a.com/e/g
|
||
buildAbsoluteURL: function (baseURL, relativeURL, opts) {
|
||
opts = opts || {};
|
||
// remove any remaining space and CRLF
|
||
baseURL = baseURL.trim();
|
||
relativeURL = relativeURL.trim();
|
||
if (!relativeURL) {
|
||
// 2a) If the embedded URL is entirely empty, it inherits the
|
||
// entire base URL (i.e., is set equal to the base URL)
|
||
// and we are done.
|
||
if (!opts.alwaysNormalize) {
|
||
return baseURL;
|
||
}
|
||
const basePartsForNormalise = URLToolkit.parseURL(baseURL);
|
||
if (!basePartsForNormalise) {
|
||
throw new Error('Error trying to parse base URL.');
|
||
}
|
||
basePartsForNormalise.path = URLToolkit.normalizePath(basePartsForNormalise.path);
|
||
return URLToolkit.buildURLFromParts(basePartsForNormalise);
|
||
}
|
||
const relativeParts = URLToolkit.parseURL(relativeURL);
|
||
if (!relativeParts) {
|
||
throw new Error('Error trying to parse relative URL.');
|
||
}
|
||
if (relativeParts.scheme) {
|
||
// 2b) If the embedded URL starts with a scheme name, it is
|
||
// interpreted as an absolute URL and we are done.
|
||
if (!opts.alwaysNormalize) {
|
||
return relativeURL;
|
||
}
|
||
relativeParts.path = URLToolkit.normalizePath(relativeParts.path);
|
||
return URLToolkit.buildURLFromParts(relativeParts);
|
||
}
|
||
const baseParts = URLToolkit.parseURL(baseURL);
|
||
if (!baseParts) {
|
||
throw new Error('Error trying to parse base URL.');
|
||
}
|
||
if (!baseParts.netLoc && baseParts.path && baseParts.path[0] !== '/') {
|
||
// If netLoc missing and path doesn't start with '/', assume everthing before the first '/' is the netLoc
|
||
// This causes 'example.com/a' to be handled as '//example.com/a' instead of '/example.com/a'
|
||
const pathParts = FIRST_SEGMENT_REGEX.exec(baseParts.path);
|
||
baseParts.netLoc = pathParts[1];
|
||
baseParts.path = pathParts[2];
|
||
}
|
||
if (baseParts.netLoc && !baseParts.path) {
|
||
baseParts.path = '/';
|
||
}
|
||
const builtParts = {
|
||
// 2c) Otherwise, the embedded URL inherits the scheme of
|
||
// the base URL.
|
||
scheme: baseParts.scheme,
|
||
netLoc: relativeParts.netLoc,
|
||
path: null,
|
||
params: relativeParts.params,
|
||
query: relativeParts.query,
|
||
fragment: relativeParts.fragment,
|
||
};
|
||
if (!relativeParts.netLoc) {
|
||
// 3) If the embedded URL's <net_loc> is non-empty, we skip to
|
||
// Step 7. Otherwise, the embedded URL inherits the <net_loc>
|
||
// (if any) of the base URL.
|
||
builtParts.netLoc = baseParts.netLoc;
|
||
// 4) If the embedded URL path is preceded by a slash "/", the
|
||
// path is not relative and we skip to Step 7.
|
||
if (relativeParts.path[0] !== '/') {
|
||
if (!relativeParts.path) {
|
||
// 5) If the embedded URL path is empty (and not preceded by a
|
||
// slash), then the embedded URL inherits the base URL path
|
||
builtParts.path = baseParts.path;
|
||
// 5a) if the embedded URL's <params> is non-empty, we skip to
|
||
// step 7; otherwise, it inherits the <params> of the base
|
||
// URL (if any) and
|
||
if (!relativeParts.params) {
|
||
builtParts.params = baseParts.params;
|
||
// 5b) if the embedded URL's <query> is non-empty, we skip to
|
||
// step 7; otherwise, it inherits the <query> of the base
|
||
// URL (if any) and we skip to step 7.
|
||
if (!relativeParts.query) {
|
||
builtParts.query = baseParts.query;
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
// Non-standard, allow non-empty paths to inherit the query
|
||
if (opts.inheritQuery) {
|
||
if (!relativeParts.query) {
|
||
builtParts.query = baseParts.query;
|
||
}
|
||
}
|
||
// 6) The last segment of the base URL's path (anything
|
||
// following the rightmost slash "/", or the entire path if no
|
||
// slash is present) is removed and the embedded URL's path is
|
||
// appended in its place.
|
||
const baseURLPath = baseParts.path;
|
||
const newPath = baseURLPath.substring(0, baseURLPath.lastIndexOf('/') + 1) + relativeParts.path;
|
||
builtParts.path = URLToolkit.normalizePath(newPath);
|
||
}
|
||
}
|
||
}
|
||
if (builtParts.path === null) {
|
||
builtParts.path = opts.alwaysNormalize ? URLToolkit.normalizePath(relativeParts.path) : relativeParts.path;
|
||
}
|
||
return URLToolkit.buildURLFromParts(builtParts);
|
||
},
|
||
parseURL: function (url) {
|
||
const parts = URL_REGEX.exec(url);
|
||
if (!parts) {
|
||
return null;
|
||
}
|
||
return {
|
||
scheme: parts[1] || '',
|
||
netLoc: parts[2] || '',
|
||
path: parts[3] || '',
|
||
params: parts[4] || '',
|
||
query: parts[5] || '',
|
||
fragment: parts[6] || '',
|
||
};
|
||
},
|
||
normalizePath: function (path) {
|
||
// The following operations are
|
||
// then applied, in order, to the new path:
|
||
// 6a) All occurrences of "./", where "." is a complete path
|
||
// segment, are removed.
|
||
// 6b) If the path ends with "." as a complete path segment,
|
||
// that "." is removed.
|
||
path = path.split('').reverse().join('').replace(SLASH_DOT_REGEX, '');
|
||
// 6c) All occurrences of "<segment>/../", where <segment> is a
|
||
// complete path segment not equal to "..", are removed.
|
||
// Removal of these path segments is performed iteratively,
|
||
// removing the leftmost matching pattern on each iteration,
|
||
// until no matching pattern remains.
|
||
// 6d) If the path ends with "<segment>/..", where <segment> is a
|
||
// complete path segment not equal to "..", that
|
||
// "<segment>/.." is removed.
|
||
while (path.length !== (path = path.replace(SLASH_DOT_DOT_REGEX, '')).length) { } // eslint-disable-line
|
||
return path.split('').reverse().join('');
|
||
},
|
||
buildURLFromParts: function (parts) {
|
||
return parts.scheme + parts.netLoc + parts.path + parts.params + parts.query + parts.fragment;
|
||
},
|
||
getHostName: function (url) {
|
||
let hostname;
|
||
if (!url) {
|
||
return hostname;
|
||
}
|
||
if (url.indexOf('://') > -1) {
|
||
hostname = url.split('/')[2];
|
||
}
|
||
else {
|
||
hostname = url.split('/')[0];
|
||
}
|
||
// remove port and query if present
|
||
hostname = hostname.split(':')[0];
|
||
hostname = hostname.split('?')[0];
|
||
return hostname;
|
||
},
|
||
};
|
||
var URLToolkit$1 = URLToolkit;
|
||
|
||
/**
|
||
* URL utils for hostname and other stuff
|
||
*/
|
||
function isCustomUrl(url) {
|
||
return url != null && !url.startsWith('http://') && !url.startsWith('https://');
|
||
}
|
||
function getHostName(url) {
|
||
if (url == null) {
|
||
return null;
|
||
}
|
||
if (isCustomUrl(url)) {
|
||
return null;
|
||
}
|
||
return URLToolkit.getHostName(url);
|
||
}
|
||
const hostsRequiringQueries = ['.*.itunes.apple.com'];
|
||
const URLQueries = {
|
||
deviceName: '&xapdn=',
|
||
deviceModel: '&xapdm=',
|
||
language: '&xapdl=',
|
||
dsid: '&xapdsid=',
|
||
subs: '&xapsub=',
|
||
};
|
||
function urlNeedsUpdate(url, enableQueryParamsForITunes) {
|
||
if (enableQueryParamsForITunes) {
|
||
const hostName = getHostName(url);
|
||
return (undefined !==
|
||
hostsRequiringQueries.find((regStr) => {
|
||
const regex = new RegExp(regStr);
|
||
return regex.exec(hostName);
|
||
}));
|
||
}
|
||
return false;
|
||
}
|
||
function shouldUpdateUrlWithDeviceNameAndModel(platformInfo) {
|
||
const deviceModel = platformInfo.model, deviceName = platformInfo.manufacturer;
|
||
if (!deviceModel || !deviceName) {
|
||
getLogger().warn(`Missing model/manufacturer in platformInfo model ${deviceModel} manufacturer ${deviceName}`);
|
||
}
|
||
return !!deviceModel && !!deviceName;
|
||
}
|
||
function updateUrlWithDeviceNameAndModel(url, platformInfo) {
|
||
const deviceModel = platformInfo.model, deviceName = platformInfo.manufacturer, urlHasQueryParams = url.indexOf('?') !== -1, deviceModelEnc = encodeURIComponent(deviceModel), deviceNameEnc = encodeURIComponent(deviceName);
|
||
url = urlHasQueryParams ? url : url + '?';
|
||
return url + URLQueries.deviceName + deviceNameEnc + URLQueries.deviceModel + deviceModelEnc;
|
||
}
|
||
function updateUrlWithOptionsQuery(url, queryParameters) {
|
||
const urlHasQueryParams = url.indexOf('?') !== -1;
|
||
url = urlHasQueryParams ? url : url + '?';
|
||
let query;
|
||
const logger = getLogger();
|
||
for (query in queryParameters) {
|
||
if (!queryParameters[query]) {
|
||
logger.warn(`Missing ${query} info`);
|
||
}
|
||
else {
|
||
url += URLQueries[query] + (query === 'subs' ? encodeURIComponent(queryParameters[query]) : queryParameters[query]);
|
||
}
|
||
}
|
||
return url;
|
||
}
|
||
function updateUrlWithQueryStrings(url, platformInfo, queryParameters) {
|
||
if (shouldUpdateUrlWithDeviceNameAndModel(platformInfo)) {
|
||
url = updateUrlWithDeviceNameAndModel(url, platformInfo);
|
||
}
|
||
url = updateUrlWithOptionsQuery(url, queryParameters);
|
||
return url;
|
||
}
|
||
function hasMatchingHost(hostName, url) {
|
||
return !hostName || hostName === getHostName(url);
|
||
}
|
||
function isRedirectStatusCode(status) {
|
||
return status === 300 || status === 302 || status === 303 || status === 305;
|
||
}
|
||
/**
|
||
* Return all the GET parameters passed to a page
|
||
* This function is different from parseParamsFromURL(),
|
||
* where it just extracts all the parameters
|
||
* without trying to determine the correct type
|
||
*
|
||
* @param {string} href - The url to extract
|
||
* @param {array} exclude - A list of parameters to exclude from the list
|
||
* @return {object} - Key value pair with parameter name as key and value
|
||
*/
|
||
const getURLParams = (href, exclude = []) => {
|
||
const params = {};
|
||
try {
|
||
const urlObject = new URL(href);
|
||
const urlSearch = urlObject.search;
|
||
const searchParams = new URLSearchParams(urlSearch);
|
||
for (const param of searchParams.entries()) {
|
||
if (!exclude.includes(param[0]))
|
||
params[param[0]] = param[1];
|
||
}
|
||
}
|
||
catch (e) {
|
||
getLogger().error(e);
|
||
}
|
||
return params;
|
||
};
|
||
function getTimeOffsetParameter(url) {
|
||
const paramString = URLToolkit.parseURL(url).fragment.substr(1);
|
||
if (paramString.length === 0) {
|
||
return null;
|
||
}
|
||
const searchParams = new URLSearchParams(paramString);
|
||
if (!searchParams.has('t')) {
|
||
return null;
|
||
}
|
||
const timeOffsetValue = Number(searchParams.get('t'));
|
||
if (isFiniteNumber(timeOffsetValue)) {
|
||
return timeOffsetValue;
|
||
}
|
||
return null;
|
||
}
|
||
/**
|
||
* Get specified parameters from the search query while
|
||
* trying to extract the correct type from each one.
|
||
*
|
||
* @param {array} extract - The list of parameters to extract
|
||
* @return {*}
|
||
*/
|
||
const parseParamsFromURL = (input, extract) => {
|
||
const url = new URL(input);
|
||
const params = url.searchParams;
|
||
const result = {};
|
||
const getTypedParam = (name, defaultValue = undefined, json = false, transform) => {
|
||
const param = params.get(name);
|
||
let value;
|
||
switch (param) {
|
||
case 'undefined':
|
||
value = undefined;
|
||
break;
|
||
case 'null':
|
||
value = null;
|
||
break;
|
||
case 'False':
|
||
case 'false':
|
||
value = false;
|
||
break;
|
||
case 'True':
|
||
case 'true':
|
||
value = true;
|
||
break;
|
||
default:
|
||
if (param) {
|
||
const num = Number(param);
|
||
if (isFiniteNumber(num)) {
|
||
value = num;
|
||
}
|
||
else {
|
||
value = param;
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
if (typeof value === 'string' && json) {
|
||
value = safeParseJSON(value);
|
||
}
|
||
if (typeof value === 'string' && transform) {
|
||
value = transform(value);
|
||
}
|
||
return value !== null && value !== void 0 ? value : defaultValue;
|
||
};
|
||
extract.forEach((param) => {
|
||
result[param.name] = getTypedParam(param.name, param.default, param.json, param.transform);
|
||
});
|
||
return result;
|
||
};
|
||
/**
|
||
* Safely parse a json, prevent throwing an error on non-parseable input
|
||
*
|
||
* @param {string} data - Hopefully a JSON string
|
||
* @return {*}
|
||
*/
|
||
const safeParseJSON = (data) => {
|
||
let value;
|
||
if (typeof data !== 'string')
|
||
return data;
|
||
try {
|
||
value = JSON.parse(data);
|
||
}
|
||
catch (err) {
|
||
// eslint-disable-next-line no-console
|
||
console.error(`failed to parse param: ${data}`);
|
||
}
|
||
return value;
|
||
};
|
||
/*
|
||
* Get a list of the dynamic range formats the user has selected
|
||
*
|
||
* Type 0: SDR
|
||
* Type 2: HDR10
|
||
* Type 3: DV
|
||
* Type 4: HLG
|
||
*
|
||
* @returns {array}
|
||
*/
|
||
function getVideoDynamicRangeFormats(dynamicRangeFormatSDR, dynamicRangeFormatHDR10, dynamicRangeFormatDV, dynamicRangeFormatHLG, averageBitrateCap, peakBitrateCap, clearBitrateCap) {
|
||
const videoDynamicRangeFormats = [
|
||
{ type: 0, highestPlayableAverageBitRate: undefined, highestPlayablePeakBitRate: undefined },
|
||
{ type: 2, highestPlayableAverageBitRate: undefined, highestPlayablePeakBitRate: undefined },
|
||
{ type: 3, highestPlayableAverageBitRate: undefined, highestPlayablePeakBitRate: undefined },
|
||
{ type: 4, highestPlayableAverageBitRate: undefined, highestPlayablePeakBitRate: undefined },
|
||
];
|
||
const videoDynamicRangeFormatsMap = {
|
||
0: dynamicRangeFormatSDR,
|
||
2: dynamicRangeFormatHDR10,
|
||
3: dynamicRangeFormatDV,
|
||
4: dynamicRangeFormatHLG,
|
||
};
|
||
for (let formatIndex = 0; formatIndex < videoDynamicRangeFormats.length; ++formatIndex) {
|
||
videoDynamicRangeFormats[formatIndex].highestPlayableAverageBitRate = averageBitrateCap;
|
||
videoDynamicRangeFormats[formatIndex].highestPlayablePeakBitRate = peakBitrateCap;
|
||
videoDynamicRangeFormats[formatIndex].highestPlayablePeakBitRateForClearContent = clearBitrateCap;
|
||
}
|
||
return videoDynamicRangeFormats.filter(function (format) {
|
||
return !!videoDynamicRangeFormatsMap[format.type];
|
||
});
|
||
}
|
||
/*
|
||
* Get a list of the video codecs the user has selected
|
||
*
|
||
* Type 16: H264
|
||
* Type 64: HEVC
|
||
*
|
||
* @returns {array}
|
||
*/
|
||
function getVideoCodecs(videoCodecH264, videoCodecHEVC, averageBitrateCap, peakBitrateCap) {
|
||
const videoCodecs = [
|
||
{ type: 16, highestPlayableAverageBitRate: undefined, highestPlayablePeakBitRate: undefined },
|
||
{ type: 64, highestPlayableAverageBitRate: undefined, highestPlayablePeakBitRate: undefined },
|
||
];
|
||
const videoCodecsMap = { 16: videoCodecH264, 64: videoCodecHEVC };
|
||
for (let formatIndex = 0; formatIndex < videoCodecs.length; ++formatIndex) {
|
||
videoCodecs[formatIndex].highestPlayableAverageBitRate = averageBitrateCap;
|
||
videoCodecs[formatIndex].highestPlayablePeakBitRate = peakBitrateCap;
|
||
}
|
||
return videoCodecs.filter(function (format) {
|
||
return !!videoCodecsMap[format.type];
|
||
});
|
||
}
|
||
|
||
class AutomationRemote {
|
||
constructor() {
|
||
this.onStop$ = new Subject();
|
||
this._logger = console;
|
||
this._playbackData = {
|
||
audio: [],
|
||
subtitles: [],
|
||
variants: [],
|
||
};
|
||
this._timers$ = {
|
||
cpu: undefined,
|
||
memory: undefined,
|
||
system: undefined,
|
||
};
|
||
/**
|
||
* Register the current parameters or player configuration
|
||
* This is used to check if the sockets from the automation server
|
||
* are requesting the page to be reloaded
|
||
* or the server is trying to load another player url
|
||
*
|
||
* @param {object} config - The list of GET parameters passed to the URL or other configuration values
|
||
* @return {void}
|
||
*/
|
||
this.setConfig = (config = {}) => {
|
||
this._config = JSON.stringify(config);
|
||
};
|
||
/**
|
||
* Trigger a socket command back to the server to perform backend tasks
|
||
*
|
||
* @return {void}
|
||
*/
|
||
this.triggerSocketCommand = (command) => {
|
||
if (!command)
|
||
throw new Error('No command value was passed');
|
||
this.socket.emit('action', {
|
||
type: 'SOCKETS/TRIGGER_CONTROL_SSH',
|
||
command,
|
||
});
|
||
};
|
||
}
|
||
/**
|
||
* Do setup/teardown if needed on hls change
|
||
*
|
||
* @param {object} hls
|
||
* @setter
|
||
* @return {void}
|
||
*/
|
||
setHls(hls) {
|
||
if (!hls) {
|
||
this.onStop$.next();
|
||
this._hls = null;
|
||
this._playbackData = {
|
||
audio: [],
|
||
subtitles: [],
|
||
variants: [],
|
||
};
|
||
return;
|
||
}
|
||
this._hls = hls;
|
||
this._hls.altAudioOptions$
|
||
.pipe(tap((altAudioOptions) => (this._playbackData.audio = altAudioOptions)), takeUntil(this.onStop$))
|
||
.subscribe();
|
||
this._hls.subtitleOptions$
|
||
.pipe(tap((subtitleOptions) => (this._playbackData.subtitles = subtitleOptions)), takeUntil(this.onStop$))
|
||
.subscribe();
|
||
this._hls.variantOptions$
|
||
.pipe(tap((variantOptions) => (this._playbackData.variants = variantOptions)), takeUntil(this.onStop$))
|
||
.subscribe();
|
||
}
|
||
/**
|
||
* Set the logger instance
|
||
*
|
||
* @param {object} logger
|
||
* @setter
|
||
* @return {void}
|
||
*/
|
||
setLogger(logger) {
|
||
this._logger = logger;
|
||
}
|
||
/**
|
||
* Execute a function that matches the command name
|
||
*
|
||
* @param {string} command - The function name (ex: seek)
|
||
* @param {string} value - The value to pass to the function (ex: 1000)
|
||
* @return {*}
|
||
*/
|
||
applyCommand(command, value) {
|
||
if (typeof this[command] === 'function') {
|
||
try {
|
||
return this[command](value) || true; // Always return a value for the socket server to know we responded
|
||
}
|
||
catch (e) {
|
||
this._logger.error(e);
|
||
}
|
||
}
|
||
}
|
||
respondToSocketAction(data, callback) {
|
||
const loggerExternals = LoggerExternals$1();
|
||
this._logger.qe({ critical: true, name: 'interact', data });
|
||
if (!this._hls) {
|
||
return this._logger.warn(`hls object not initialized while trying to execute ${data.command}`);
|
||
}
|
||
switch (data.command) {
|
||
case 'cpu':
|
||
this._logger.qe({ critical: true, name: 'cpu', data: replaceAll('\n', ' | ', data.value) });
|
||
break;
|
||
case 'free-memory':
|
||
this._logger.qe({ critical: true, name: 'freeMemory', data: replaceAll('\n', ' | ', data.value) });
|
||
break;
|
||
case 'log-clear':
|
||
callback(loggerExternals.logClear());
|
||
break;
|
||
case 'log-end':
|
||
//callback(utils.logEnd());
|
||
break;
|
||
case 'log-read':
|
||
callback({ chunk: data.chunk, log: loggerExternals.compress(loggerExternals.logRead(data.chunk, data.chunkSize)) });
|
||
break;
|
||
case 'log-read-length':
|
||
callback({ size: loggerExternals.logReadLength(), format: 'array' });
|
||
break;
|
||
case 'log-start':
|
||
//callback(Utils.logStart());
|
||
break;
|
||
case 'memory':
|
||
this._logger.qe({ critical: true, name: 'memory', data: getJSMemory() });
|
||
break;
|
||
case 'access-log':
|
||
this._logger.qe({ critical: true, name: 'accessLog', data: { accessLogItems: this._hls.accessLog } });
|
||
break;
|
||
case 'error-log':
|
||
this._logger.qe({ critical: true, name: 'errorLog', data: { errorLogItems: this._hls.errorLog } });
|
||
break;
|
||
default:
|
||
callback(this.applyCommand(data.command, data.value));
|
||
}
|
||
}
|
||
/**
|
||
* Connect to a remote socket.io server
|
||
*
|
||
* @return {object}
|
||
*/
|
||
initializeSocketService(socketurl, socketid) {
|
||
if (!socketurl)
|
||
throw new Error('Attempted to call logConnect() without a socket URL');
|
||
if (!socketid)
|
||
throw new Error('Attempted to call logConnect() without a socket ID');
|
||
this.socket = socket_io_min.exports.io(socketurl, { query: { ip: socketid } });
|
||
this._logger.info(`Connecting socket service with server: ${socketurl} and id: ${socketid}`);
|
||
this.socket.on('connect', () => this._logger.info(`Connected to: ${socketurl}`));
|
||
this.socket.on('disconnect', () => this._logger.info(`Disonnected from: ${socketurl}`));
|
||
this.socket.on('error', (e) => this._logger.error(e));
|
||
this.socket.on('action', this.respondToSocketAction.bind(this));
|
||
}
|
||
/**
|
||
* Load a playlist
|
||
*
|
||
* @param {object} data - Server object containing data needed for playback
|
||
* @param {object} options
|
||
* @param {number} initialSeekTime
|
||
* @return {void}
|
||
*/
|
||
loadURL(data, options = {}, initialSeekTime = 0) {
|
||
var _a;
|
||
const pageUrl = data.fullURL || location.href;
|
||
const params = parseParamsFromURL(pageUrl, [
|
||
{ name: 'averageBitrateCap' },
|
||
{ name: 'dynamicRangeFormatSDR', default: true },
|
||
{ name: 'dynamicRangeFormatHDR10', default: true },
|
||
{ name: 'dynamicRangeFormatDV', default: true },
|
||
{ name: 'dynamicRangeFormatHLG', default: true },
|
||
{ name: 'maxHdcpLevel', default: 'TYPE-2', transform: decodeURIComponent },
|
||
{ name: 'maxSecurityLevel', transform: decodeURIComponent },
|
||
{ name: 'peakBitrateCap' },
|
||
{ name: 'requiresCDMAttachOnStart', default: false, json: true },
|
||
{ name: 'src' },
|
||
{ name: 'rand' },
|
||
{ name: 'videoCodecH264', default: false },
|
||
{ name: 'videoCodecHEVC', default: false },
|
||
]);
|
||
// Reload if the server requests a different player
|
||
if (this._config !== JSON.stringify(getURLParams(pageUrl, ['src', 'rand'])) && !((_a = getURLParams(location === null || location === void 0 ? void 0 : location.href)) === null || _a === void 0 ? void 0 : _a.dev)) {
|
||
location.href = pageUrl;
|
||
return;
|
||
}
|
||
const platformInfo = {
|
||
videoDynamicRangeFormats: getVideoDynamicRangeFormats(params.dynamicRangeFormatSDR, params.dynamicRangeFormatHDR10, params.dynamicRangeFormatDV, params.dynamicRangeFormatHLG, params.averageBitrateCap, params.peakBitrateCap, params.clearBitrateCap),
|
||
videoCodecs: getVideoCodecs(params.videoCodecH264, params.videoCodecHEVC, params.averageBitrateCap, params.peakBitrateCap),
|
||
requiresCDMAttachOnStart: params.requiresCDMAttachOnStart,
|
||
maxHdcpLevel: params.maxHdcpLevel,
|
||
maxSecurityLevel: params.maxSecurityLevel,
|
||
};
|
||
const storebag_url = 'https://mediaservices.cdn-apple.com/store_bags/hlsjs_qa/v1/rtc_storebag.json';
|
||
const reportingStorebag = new rtc.exports.RTCStorebag.RTCReportingStoreBag(storebag_url, 'HLSJSDemoPlayer', 'com.apple.hlsjs.demo', 'DemoPage', '', 1, 0);
|
||
const reportingAgent = new rtc.exports.RTCReportingAgent({
|
||
sender: 'HLSJS',
|
||
_clientName: 'HLSJSDemoPlayer',
|
||
_serviceName: 'com.apple.hlsjs.demo',
|
||
reportingStoreBag: reportingStorebag,
|
||
applicationName: 'DemoPage',
|
||
});
|
||
const copyOptions = Object.assign({ appData: { clientName: 'HLSJSDemoPlayer', serviceName: 'com.apple.hlsjs.demo', appName: 'DemoPage', reportingAgent: reportingAgent }, userInfo: { internalBuild: true }, platformInfo }, options);
|
||
this._hls.loadSource(data.url, copyOptions, initialSeekTime);
|
||
}
|
||
/**
|
||
* Start playback if video is not detached
|
||
*/
|
||
play() {
|
||
this._hls.desiredRate = 1;
|
||
}
|
||
/**
|
||
* Set the scrubbing speed
|
||
*
|
||
* @param {number|string} rate
|
||
*/
|
||
playbackRate(rate) {
|
||
this._hls.setRate(rate);
|
||
}
|
||
/**
|
||
* Pause playback
|
||
*/
|
||
pause() {
|
||
this._hls.desiredRate = 0;
|
||
}
|
||
/**
|
||
* Reload page
|
||
*/
|
||
reload() {
|
||
window.location.reload();
|
||
}
|
||
/**
|
||
* Seek to a time value
|
||
*
|
||
* @param {number} time - Seek position
|
||
*/
|
||
seekTo(time) {
|
||
this._hls.seekTo = time;
|
||
}
|
||
/**
|
||
* Seek to a date
|
||
*
|
||
* @param {Date} date - Seek position
|
||
*/
|
||
seekToDate(date) {
|
||
this._hls.seekToDate(date);
|
||
}
|
||
/**
|
||
* Set the current audio
|
||
*
|
||
* @param {string} lang (ex: French)
|
||
*/
|
||
setAudio(lang) {
|
||
const mediaOption = this._playbackData.audio.find((option) => {
|
||
return option.MediaSelectionOptionsExtendedLanguageTag === lang || option.MediaSelectionOptionsName === lang;
|
||
}) || {};
|
||
this._hls.audioSelectedPersistentID = mediaOption.MediaSelectionOptionsPersistentID;
|
||
}
|
||
/**
|
||
* Set the current caption given a language
|
||
*
|
||
* @param {string} lang (ex: fr)
|
||
*/
|
||
setCaption(lang) {
|
||
const mediaOption = this._playbackData.subtitles.find((option) => {
|
||
return option.MediaSelectionOptionsExtendedLanguageTag === lang || option.MediaSelectionOptionsName === lang;
|
||
}) || {};
|
||
this._hls.subtitleSelectedPersistentID = mediaOption.MediaSelectionOptionsPersistentID;
|
||
}
|
||
/**
|
||
* Start a timer tied to the lifecycle of this class
|
||
* that will request a cpu value from the server
|
||
*
|
||
* @param schedule The periodicity of the timer
|
||
* @public
|
||
*/
|
||
setTimerCpu(schedule = 2000) {
|
||
if (this._timers$.cpu)
|
||
return;
|
||
this._timers$.cpu = timer(200, schedule)
|
||
.pipe(tap(() => {
|
||
this.triggerSocketCommand('cpu');
|
||
}), takeUntil(this.onStop$))
|
||
.subscribe();
|
||
return this._timers$.cpu;
|
||
}
|
||
/**
|
||
* Start a timer tied to the lifecycle of this class
|
||
* that will request the JS memory
|
||
*
|
||
* @param schedule The periodicity of the timer
|
||
* @public
|
||
*/
|
||
setTimerJSMemory(schedule = 2000) {
|
||
if (this._timers$.memory)
|
||
return;
|
||
this._timers$.memory = timer(200, schedule)
|
||
.pipe(tap(() => {
|
||
const { jsHeapSizeLimit, totalJSHeapSize, usedJSHeapSize } = getJSMemory();
|
||
this._logger.qe({ critical: true, name: 'memory', data: { jsHeapSizeLimit, totalJSHeapSize, usedJSHeapSize } });
|
||
}), takeUntil(this.onStop$))
|
||
.subscribe();
|
||
return this._timers$.memory;
|
||
}
|
||
/**
|
||
* Start a timer tied to the lifecycle of this class
|
||
* that will request the system memory (RAM) value from the server
|
||
*
|
||
* @param schedule The periodicity of the timer
|
||
* @public
|
||
*/
|
||
setTimerSystemMemory(schedule = 2000) {
|
||
if (this._timers$.system)
|
||
return;
|
||
this._timers$.system = timer(200, schedule)
|
||
.pipe(tap(() => {
|
||
this.triggerSocketCommand('free-memory');
|
||
}), takeUntil(this.onStop$))
|
||
.subscribe();
|
||
return this._timers$.system;
|
||
}
|
||
/**
|
||
* Trigger onStop observable
|
||
*
|
||
* @return {void}
|
||
*/
|
||
stop() {
|
||
if (!this._hls)
|
||
return;
|
||
this.onStop$.next();
|
||
}
|
||
}
|
||
let remote;
|
||
/**
|
||
* Get the JS memory if supported by the browser, otherwise return defaults
|
||
*
|
||
* @return {object}
|
||
*/
|
||
const getJSMemory = () => {
|
||
const defaultMemory = {
|
||
usedJSHeapSize: 0,
|
||
jsHeapSizeLimit: 0,
|
||
};
|
||
const performance = window.performance || {};
|
||
return performance.memory || defaultMemory;
|
||
};
|
||
/**
|
||
* Create an automation instance singleton.
|
||
*
|
||
* @param socketurl The socket server URL
|
||
* @param socketid The socket identifier
|
||
*/
|
||
function initialize(socketurl, socketid) {
|
||
if (socketurl && socketid) {
|
||
if (!remote) {
|
||
remote = new AutomationRemote();
|
||
remote.initializeSocketService(socketurl, socketid);
|
||
}
|
||
return remote;
|
||
}
|
||
}
|
||
/**
|
||
* Set the hls instance on the automation remote
|
||
*/
|
||
function setHls(hls) {
|
||
remote === null || remote === void 0 ? void 0 : remote.setHls(hls);
|
||
}
|
||
/**
|
||
* Set the logger instance
|
||
*/
|
||
function setLogger(logger) {
|
||
remote === null || remote === void 0 ? void 0 : remote.setLogger(logger);
|
||
}
|
||
|
||
var VideoCodecRank;
|
||
(function (VideoCodecRank) {
|
||
VideoCodecRank[VideoCodecRank["DOVI"] = 4] = "DOVI";
|
||
VideoCodecRank[VideoCodecRank["HEVC"] = 3] = "HEVC";
|
||
VideoCodecRank[VideoCodecRank["VP09"] = 2] = "VP09";
|
||
VideoCodecRank[VideoCodecRank["AVC"] = 1] = "AVC";
|
||
VideoCodecRank[VideoCodecRank["UNKNOWN"] = 0] = "UNKNOWN";
|
||
})(VideoCodecRank || (VideoCodecRank = {}));
|
||
var VideoRangeRank;
|
||
(function (VideoRangeRank) {
|
||
VideoRangeRank[VideoRangeRank["PQ"] = 3] = "PQ";
|
||
VideoRangeRank[VideoRangeRank["HLG"] = 2] = "HLG";
|
||
VideoRangeRank[VideoRangeRank["SDR"] = 1] = "SDR";
|
||
VideoRangeRank[VideoRangeRank["UNKNOWN"] = 0] = "UNKNOWN";
|
||
})(VideoRangeRank || (VideoRangeRank = {}));
|
||
var AudioCodecRank;
|
||
(function (AudioCodecRank) {
|
||
AudioCodecRank[AudioCodecRank["ALAC"] = 7] = "ALAC";
|
||
AudioCodecRank[AudioCodecRank["FLAC"] = 6] = "FLAC";
|
||
AudioCodecRank[AudioCodecRank["EC3"] = 5] = "EC3";
|
||
AudioCodecRank[AudioCodecRank["AC3"] = 4] = "AC3";
|
||
AudioCodecRank[AudioCodecRank["XHEAAC"] = 3] = "XHEAAC";
|
||
AudioCodecRank[AudioCodecRank["AAC"] = 2] = "AAC";
|
||
AudioCodecRank[AudioCodecRank["MP3"] = 1] = "MP3";
|
||
AudioCodecRank[AudioCodecRank["UNKNOWN"] = 0] = "UNKNOWN";
|
||
})(AudioCodecRank || (AudioCodecRank = {}));
|
||
var MatchRanking;
|
||
(function (MatchRanking) {
|
||
MatchRanking[MatchRanking["VALID"] = 1] = "VALID";
|
||
MatchRanking[MatchRanking["INVALID"] = 0] = "INVALID";
|
||
})(MatchRanking || (MatchRanking = {}));
|
||
|
||
|
||
// List of privacy allowed config headers for ServerInfo
|
||
// Headers need to be approved by Privacy. Here is a sample radar on how to request approval
|
||
// rdar://78172175
|
||
const privacyAllowedLoadConfigHeaders = ['via', 'x-apple-request-uuid'];
|
||
const defaultManifestLoadPolicy = () => ({
|
||
default: {
|
||
// Overall request configs
|
||
maxTimeToFirstByteMs: 10000,
|
||
maxLoadTimeMs: 20000,
|
||
autoRetry: false,
|
||
// Retries:
|
||
timeoutRetry: {
|
||
maxNumRetry: 2,
|
||
retryDelayMs: 0,
|
||
maxRetryDelayMs: 0, // Maximum delay between retries
|
||
},
|
||
errorRetry: {
|
||
maxNumRetry: 1,
|
||
retryDelayMs: 1000,
|
||
maxRetryDelayMs: 8000, // Maximum delay between retries
|
||
},
|
||
},
|
||
customURL: {
|
||
// Overall request configs
|
||
maxTimeToFirstByteMs: 10000,
|
||
maxLoadTimeMs: 10000,
|
||
autoRetry: false,
|
||
// Retries:
|
||
timeoutRetry: {
|
||
maxNumRetry: 2,
|
||
retryDelayMs: 0,
|
||
maxRetryDelayMs: 0, // Maximum delay between retries
|
||
},
|
||
errorRetry: {
|
||
maxNumRetry: 1,
|
||
retryDelayMs: 1000,
|
||
maxRetryDelayMs: 8000, // Maximum delay between retries
|
||
},
|
||
},
|
||
});
|
||
const fragTimeoutRetryDefaultConfig = {
|
||
// Timeout Retries default value
|
||
maxNumRetry: 4,
|
||
retryDelayMs: 0,
|
||
maxRetryDelayMs: 0, // Maximum delay between retries
|
||
};
|
||
const fragErrorRetryDefaultConfig = {
|
||
// Error Retries default value
|
||
maxNumRetry: 6,
|
||
retryDelayMs: 1000,
|
||
maxRetryDelayMs: 8000, // Maximum delay between retries
|
||
};
|
||
const defaultSessionDataAutoLoad = {
|
||
'com.apple.hls.chapters': true,
|
||
};
|
||
const defaultCertRetry = {
|
||
maxNumRetry: 0,
|
||
retryDelayMs: 0,
|
||
maxRetryDelayMs: 0, // Maximum delay between retries
|
||
};
|
||
const defaultCertLoadPolicy = {
|
||
default: {
|
||
maxTimeToFirstByteMs: 5000,
|
||
maxLoadTimeMs: 20000,
|
||
autoRetry: false,
|
||
timeoutRetry: defaultCertRetry,
|
||
errorRetry: defaultCertRetry,
|
||
},
|
||
customURL: {
|
||
maxTimeToFirstByteMs: 10000,
|
||
maxLoadTimeMs: 20000,
|
||
autoRetry: false,
|
||
timeoutRetry: defaultCertRetry,
|
||
errorRetry: defaultCertRetry,
|
||
},
|
||
};
|
||
// max overall load time should be ~30s ish
|
||
const defaultKeyRetry = {
|
||
maxNumRetry: 8,
|
||
retryDelayMs: 1000,
|
||
maxRetryDelayMs: 20000,
|
||
backoff: 'linear',
|
||
};
|
||
const timeoutKeyRetry = Object.assign(Object.assign({}, defaultKeyRetry), { maxNumRetry: 1 });
|
||
const defaultKeyLoadPolicy = {
|
||
default: {
|
||
maxTimeToFirstByteMs: 5000,
|
||
maxLoadTimeMs: 20000,
|
||
autoRetry: false,
|
||
timeoutRetry: timeoutKeyRetry,
|
||
errorRetry: defaultKeyRetry,
|
||
},
|
||
customURL: {
|
||
maxTimeToFirstByteMs: 10000,
|
||
maxLoadTimeMs: 20000,
|
||
autoRetry: false,
|
||
timeoutRetry: timeoutKeyRetry,
|
||
errorRetry: defaultKeyRetry,
|
||
},
|
||
};
|
||
const defaultTrickPlaybackConfig = {
|
||
enabled: true,
|
||
minIframeDuration: 8,
|
||
};
|
||
const hlsDefaultConfig = {
|
||
autoStartLoad: true,
|
||
startPosition: NaN,
|
||
defaultAudioCodec: void 0,
|
||
defaultVideoCodec: void 0,
|
||
debug: false,
|
||
debugLevel: 'info',
|
||
buildType: void 0,
|
||
minFramesBeforeSwitchingLevel: 11,
|
||
minTargetDurations: 3,
|
||
maxBufferLength: 60,
|
||
maxBufferHole: 0.5,
|
||
maxSeekHole: 2,
|
||
nudgeFromEventSeek: true,
|
||
jaggedSeekTolerance: 0,
|
||
discontinuitySeekTolerance: 2,
|
||
bufferedSegmentEjectionToleranceMs: 0.5,
|
||
almostDryBufferSec: 0.5,
|
||
maxTotalDurationTolerance: 0.1,
|
||
lowBufferThreshold: 0.5,
|
||
lowBufferWatchdogPeriod: 0.5,
|
||
highBufferWatchdogPeriod: 3,
|
||
seekWatchdogPeriod: 5,
|
||
nudgeOffset: 0.1,
|
||
nudgeMaxRetry: 3,
|
||
maxFragLookUpTolerance: 0.2,
|
||
initialLiveManifestSize: 1,
|
||
liveSyncDurationCount: 3,
|
||
liveMaxLatencyDurationCount: Infinity,
|
||
liveSyncDuration: void 0,
|
||
liveMaxLatencyDuration: void 0,
|
||
liveFlushExpiredFrags: true,
|
||
liveMaxUnchangedPlaylistRefresh: 3,
|
||
liveEdgeForZeroStartPositon: false,
|
||
livePlaylistUpdateStaleness: 2,
|
||
livePlaylistDurationNudge: 0.001,
|
||
allowFastSwitchUp: false,
|
||
minMatchGroupDuration: 5,
|
||
desiredIframeFPS: 8,
|
||
initialIframeFPS: 6,
|
||
minRemainingTimeInMediaPipeline: 3,
|
||
leftMediaTimeToAutoPause: 10,
|
||
startTargetDurationFactor: 0.9,
|
||
minRequiredStartDuration: 4,
|
||
maxRequiredStartDuration: 15,
|
||
enableWorker: true,
|
||
enableWebCrypto: true,
|
||
keySystemPreference: void 0,
|
||
useMultipleKeySessions: false,
|
||
enablePlayReadyKeySystem: false,
|
||
useMediaKeySystemAccessFilter: false,
|
||
playReadyMessageFormat: 'utf16',
|
||
startLevel: void 0,
|
||
livePlaylistRefreshDelay: 2500,
|
||
liveMinPlayingBufferLen: 5,
|
||
enableIFramePreloading: true,
|
||
useMediaCapabilities: false,
|
||
enableID3Cues: true,
|
||
certLoadPolicy: defaultCertLoadPolicy,
|
||
keyLoadPolicy: defaultKeyLoadPolicy,
|
||
manifestLoadPolicy: defaultManifestLoadPolicy(),
|
||
trickPlaybackConfig: defaultTrickPlaybackConfig,
|
||
playlistLoadPolicy: {
|
||
default: {
|
||
// Overall request configs
|
||
maxTimeToFirstByteMs: 10000,
|
||
maxLoadTimeMs: 20000,
|
||
autoRetry: false,
|
||
// Retries:
|
||
timeoutRetry: {
|
||
maxNumRetry: 2,
|
||
retryDelayMs: 0,
|
||
maxRetryDelayMs: 0, // Maximum delay between retries
|
||
},
|
||
errorRetry: {
|
||
maxNumRetry: 2,
|
||
retryDelayMs: 1000,
|
||
maxRetryDelayMs: 8000, // Maximum delay between retries
|
||
},
|
||
},
|
||
customURL: {
|
||
// Overall request configs
|
||
maxTimeToFirstByteMs: 10000,
|
||
maxLoadTimeMs: 10000,
|
||
autoRetry: false,
|
||
// Retries:
|
||
timeoutRetry: {
|
||
maxNumRetry: 2,
|
||
retryDelayMs: 0,
|
||
maxRetryDelayMs: 0, // Maximum delay between retries
|
||
},
|
||
errorRetry: {
|
||
maxNumRetry: 2,
|
||
retryDelayMs: 1000,
|
||
maxRetryDelayMs: 8000, // Maximum delay between retries
|
||
},
|
||
},
|
||
},
|
||
fragLoadPolicy: {
|
||
default: {
|
||
// Overall request configs
|
||
maxTimeToFirstByteMs: 5000,
|
||
maxLoadTimeMs: 20000,
|
||
autoRetry: false,
|
||
// Retries:
|
||
timeoutRetry: fragTimeoutRetryDefaultConfig,
|
||
errorRetry: fragErrorRetryDefaultConfig,
|
||
forceContentLenCheckIfNoHeader: false,
|
||
reportCDNServer: true,
|
||
},
|
||
customURL: {
|
||
// Overall request configs
|
||
maxTimeToFirstByteMs: 10000,
|
||
maxLoadTimeMs: 20000,
|
||
autoRetry: false,
|
||
// Retries:
|
||
timeoutRetry: fragTimeoutRetryDefaultConfig,
|
||
errorRetry: fragErrorRetryDefaultConfig,
|
||
reportCDNServer: true,
|
||
},
|
||
},
|
||
steeringManifestLoadPolicy: defaultManifestLoadPolicy(),
|
||
maxNumAddLevelToPenaltyBox: 4,
|
||
firstAudioMustOverlapVideoStart: false,
|
||
keyMinHoldTimeBeforeCleanup: 15000,
|
||
startFragPrefetch: false,
|
||
appendErrorMaxRetry: 3,
|
||
alwaysResetOnNewCC: false,
|
||
// loader: XhrLoader, // <jgainfort> deprecated in favor of rxjs fetch
|
||
// loader: FetchLoader, // <jgainfort> deprecated in favor of rxjs fetch
|
||
fLoader: void 0,
|
||
pLoader: void 0,
|
||
xhrSetup: void 0,
|
||
// fetchSetup: undefined,
|
||
// abrController: AbrController, // <jgainfort> deprecated in favor or redux/rxjs
|
||
// bufferController: BufferController, // <jgainfort> deprecated in favor or redux/rxjs
|
||
// #if altaudio
|
||
// audioStreamController: AudioStreamController, // <jgainfort> deprecated in favor or redux/rxjs
|
||
// audioTrackController: AudioTrackController, // <jgainfort> deprecated in favor or redux/rxjs
|
||
iframeMaxExitSeekDuration: 2000,
|
||
iframeStallMaxRetry: 5,
|
||
audioPrimingDelay: 0,
|
||
// #endif
|
||
// #if subtitle
|
||
// subtitleStreamController: SubtitleStreamController, // <jgainfort> deprecated in favor or redux/rxjs
|
||
// subtitleTrackController: SubtitleTrackController, // <jgainfort> deprecated in favor or redux/rxjs
|
||
// timelineController: TimelineController, // <jgainfort> deprecated in favor or redux/rxjs
|
||
enableCEA708Captions: true,
|
||
customTextTrackCueRenderer: false,
|
||
enableWebVTT: true,
|
||
captionsTextTrack1Label: 'English',
|
||
captionsTextTrack1LanguageCode: 'en',
|
||
captionsTextTrack2Label: 'Spanish',
|
||
captionsTextTrack2LanguageCode: 'es',
|
||
enableDualTrackSelection: false,
|
||
condenseSubtitleTrack: false,
|
||
nativeTextTrackChangeHandling: true,
|
||
earlyFragTolerance: 7,
|
||
vttConcurrentLoadCount: 1,
|
||
trottleCheckInterval: 2000,
|
||
subtitleLeadTime: 30,
|
||
lateTolerance: 2,
|
||
// #endif
|
||
stretchShortVideoTrack: false,
|
||
forceKeyFrameOnDiscontinuity: true,
|
||
useFirstLevelAtIncompatDiscontinuity: true,
|
||
abrBandwidthEstimator: 'bandwidth-history-controller',
|
||
abrEwmaDefaultEstimate: 500000,
|
||
abrDefaultEstimate: 500000,
|
||
abrBandWidthFactor: 0.95,
|
||
abrBandWidthUpFactor: 0.9,
|
||
abrMaxWithRealBitrate: false,
|
||
maxStarvationDelay: 4,
|
||
maxLoadingDelay: 4,
|
||
minAutoBitrate: 0,
|
||
enableRtcReporting: false,
|
||
rtcIntervalTimeout: 300000,
|
||
rtcSender: 'HLSJS',
|
||
rtcSessionTag: 'none',
|
||
useHTTPPlaybackSessionId: false,
|
||
warmupCdms: false,
|
||
enablePerformanceLogging: false,
|
||
overridePlaybackRate: false,
|
||
nativeControlsEnabled: false,
|
||
useCustomMediaFunctions: true,
|
||
seekEventThrottleMs: 150,
|
||
enableAdaptiveStartup: true,
|
||
bandwidthHistoryWindowSize: 120000,
|
||
bandwidthHistoryTTL: 600000,
|
||
bandwidthHistoryAggregationMethod: 'quadratic-time-weighted',
|
||
bandwidthHistoryGetEstimateThrottleMs: 1000,
|
||
defaultTargetDuration: 10,
|
||
targetStartupMs: 4000,
|
||
adaptiveStartupMetricsOverride: {
|
||
maxValidHeight: 1080,
|
||
maxValidBitrate: Infinity,
|
||
maxPreferredBitrate: Infinity, // the purpose of `maxPreferredBitrate` is to limit startup time. In adaptive startup we use `targetStartupMs` instead.
|
||
},
|
||
bandwidthHistoryStorageKey: 'AppleHLS-bandwidth-estimation',
|
||
storageKeyPrefix: 'AppleHLS-',
|
||
storage: {
|
||
get: typeof localStorage === 'undefined' ? undefined : localStorage.getItem.bind(localStorage),
|
||
set: typeof localStorage === 'undefined' ? undefined : localStorage.setItem.bind(localStorage),
|
||
},
|
||
minFragmentCount: 10,
|
||
minPlaylistCount: 5,
|
||
enableCDNFallback: true,
|
||
enableQueryParamsForITunes: false,
|
||
gapless: false,
|
||
useViewportSizeForLevelCap: false,
|
||
statDefaults: {
|
||
playlistLoadTimeMs: 500,
|
||
playlistParseTimeMs: 50,
|
||
fragParseTimeMs: 50,
|
||
fragBufferCreationDelayMs: 200,
|
||
dataFragAppendMs: 50,
|
||
initFragAppendMs: 50,
|
||
},
|
||
disableVideoCodecList: new Set([
|
||
// codecs that will be ignored during playback
|
||
]),
|
||
disableAudioCodecList: new Set([AudioCodecRank.ALAC, AudioCodecRank.FLAC, AudioCodecRank.XHEAAC]),
|
||
useHighestVideoCodecPrivate: true,
|
||
sessionDataAutoLoad: defaultSessionDataAutoLoad, // Attributes listed here will be auto-fetched if its URI is given.
|
||
};
|
||
|
||
const NoMediaOptionKey = { itemId: 'Nah', mediaOptionId: 'Nah' };
|
||
const NoMediaOption = Object.assign(Object.assign({}, NoMediaOptionKey), { mediaOptionType: undefined });
|
||
const isEnabledMediaOption = (mediaOptionInfo) => {
|
||
const { itemId, mediaOptionId } = mediaOptionInfo;
|
||
return itemId !== 'Nah' && mediaOptionId !== 'Nah';
|
||
};
|
||
const mediaOptionKeyEquals = (firstMediaOption, secondMediaOption) => {
|
||
return firstMediaOption.itemId === secondMediaOption.itemId && firstMediaOption.mediaOptionId === secondMediaOption.mediaOptionId;
|
||
};
|
||
var MediaOptionType;
|
||
(function (MediaOptionType) {
|
||
MediaOptionType[MediaOptionType["Variant"] = 0] = "Variant";
|
||
MediaOptionType[MediaOptionType["AltAudio"] = 1] = "AltAudio";
|
||
MediaOptionType[MediaOptionType["Subtitle"] = 2] = "Subtitle";
|
||
})(MediaOptionType || (MediaOptionType = {}));
|
||
const MediaOptionNames = ['variant', 'altAudio', 'subtitle'];
|
||
const MediaOptionTypes = [MediaOptionType.Variant, MediaOptionType.AltAudio, MediaOptionType.Subtitle];
|
||
const AVMediaOptionTypes = [MediaOptionType.Variant, MediaOptionType.AltAudio];
|
||
|
||
var SourceBufferType;
|
||
(function (SourceBufferType) {
|
||
SourceBufferType[SourceBufferType["Variant"] = 0] = "Variant";
|
||
SourceBufferType[SourceBufferType["AltAudio"] = 1] = "AltAudio";
|
||
})(SourceBufferType || (SourceBufferType = {}));
|
||
const SourceBufferNames = ['variant', 'altAudio'];
|
||
function mediaOptionTypeToSourceBufferType(optionType) {
|
||
switch (optionType) {
|
||
case MediaOptionType.Variant:
|
||
return SourceBufferType.Variant;
|
||
case MediaOptionType.AltAudio:
|
||
return SourceBufferType.AltAudio;
|
||
default:
|
||
return null;
|
||
}
|
||
}
|
||
function sourceBufferTypeToMediaOptionType(sbType) {
|
||
return sbType === SourceBufferType.Variant ? MediaOptionType.Variant : MediaOptionType.AltAudio;
|
||
}
|
||
function initSegmentEquals(a, b) {
|
||
const equals = a && b && a.itemId === b.itemId && a.mediaOptionId === b.mediaOptionId && a.discoSeqNum === b.discoSeqNum && a.keyId === b.keyId;
|
||
return equals;
|
||
}
|
||
|
||
var Allowed;
|
||
(function (Allowed) {
|
||
Allowed[Allowed["NO"] = 0] = "NO";
|
||
Allowed[Allowed["YES"] = 1] = "YES";
|
||
})(Allowed || (Allowed = {}));
|
||
|
||
var MediaTypeFourCC;
|
||
(function (MediaTypeFourCC) {
|
||
MediaTypeFourCC["UNKNOWN"] = "unkn";
|
||
MediaTypeFourCC["VIDEO"] = "vide";
|
||
MediaTypeFourCC["AUDIO"] = "soun";
|
||
MediaTypeFourCC["SUBTITLE"] = "sbtl";
|
||
MediaTypeFourCC["CLOSEDCAPTION"] = "clcp";
|
||
})(MediaTypeFourCC || (MediaTypeFourCC = {}));
|
||
|
||
|
||
const BinarySearch = {
|
||
/**
|
||
* Searches for an item in an array which matches a certain condition.
|
||
* This requires the condition to only match one item in the array,
|
||
* and for the array to be ordered.
|
||
*
|
||
* @param {Array} list The array to search.
|
||
* @param {Function} comparisonFunction
|
||
* Called and provided a candidate item as the first argument.
|
||
* Should return:
|
||
* > -1 if the item should be located at a lower index than the provided item.
|
||
* > 1 if the item should be located at a higher index than the provided item.
|
||
* > 0 if the item is the item you're looking for.
|
||
*
|
||
* @return {*} The object if it is found or undefined otherwise.
|
||
*/
|
||
search: function (list, comparisonFunction) {
|
||
let minIndex = 0;
|
||
let maxIndex = (list === null || list === void 0 ? void 0 : list.length) - 1;
|
||
let currentIndex;
|
||
let currentElement;
|
||
while (minIndex <= maxIndex) {
|
||
currentIndex = ((minIndex + maxIndex) / 2) | 0;
|
||
currentElement = list[currentIndex];
|
||
const comparisonResult = comparisonFunction(currentElement);
|
||
if (comparisonResult > 0) {
|
||
minIndex = currentIndex + 1;
|
||
}
|
||
else if (comparisonResult < 0) {
|
||
maxIndex = currentIndex - 1;
|
||
}
|
||
else {
|
||
return currentElement;
|
||
}
|
||
}
|
||
return null;
|
||
},
|
||
};
|
||
var BinarySearch$1 = BinarySearch;
|
||
|
||
/**
|
||
* Fragment finder utils, providing methods for determining next fragment to fetch
|
||
*
|
||
*
|
||
*
|
||
*
|
||
*/
|
||
function fragTag(frag, level) {
|
||
return `sn/cc/level: ${frag.mediaSeqNum}/${frag.discoSeqNum}/${level}`;
|
||
}
|
||
function rangeString(frag) {
|
||
let result = 'N/A';
|
||
if (frag.start >= 0 && frag.duration >= 0) {
|
||
result = `${frag.start.toFixed(2)}-${(frag.start + frag.duration).toFixed(2)}`;
|
||
}
|
||
return result;
|
||
}
|
||
const FragmentFetchHelper = {
|
||
/**
|
||
* Finds a fragment based on the SN of the previous fragment and current buffer.
|
||
* @param fragPrevious - The last frag successfully appended
|
||
* @param fragments - The array of fragments in level
|
||
* @param bufferEnd - The end of the contiguous buffer range within which playhead is
|
||
* @param end - End time of the level
|
||
* @param maxFragLookUpTolerance - acceptable tolerance limit
|
||
*/
|
||
findFragmentBySNAndBuffer: function (fragPrevious, fragments, bufferEnd = 0, end = 0, maxFragLookUpTolerance = 0) {
|
||
let foundFrag;
|
||
const fragNext = fragPrevious ? fragments[fragPrevious.mediaSeqNum - fragments[0].mediaSeqNum + 1] : null;
|
||
if (bufferEnd < end) {
|
||
if (bufferEnd > end - maxFragLookUpTolerance) {
|
||
maxFragLookUpTolerance = 0;
|
||
}
|
||
// Prefer the next fragment if it's within tolerance
|
||
if (fragNext && !this.fragmentWithinToleranceTest(bufferEnd, maxFragLookUpTolerance, fragNext)) {
|
||
foundFrag = fragNext;
|
||
}
|
||
else {
|
||
foundFrag = BinarySearch.search(fragments, this.fragmentWithinToleranceTest.bind(null, bufferEnd, maxFragLookUpTolerance));
|
||
}
|
||
}
|
||
else {
|
||
// reach end of playlist
|
||
foundFrag = fragments[fragments.length - 1];
|
||
}
|
||
return foundFrag;
|
||
},
|
||
fragmentWithinToleranceTest: function (bufferEnd, maxFragLookUpTolerance, candidate) {
|
||
// offset should be within fragment boundary - config.maxFragLookUpTolerance
|
||
// this is to cope with situations like
|
||
// bufferEnd = 9.991
|
||
// frag[Ø] : [0,10]
|
||
// frag[1] : [10,20]
|
||
// bufferEnd is within frag[0] range ... although what we are expecting is to return frag[1] here
|
||
// frag start frag start+duration
|
||
// |-----------------------------|
|
||
// <---> <--->
|
||
// ...--------><-----------------------------><---------....
|
||
// previous frag matching fragment next frag
|
||
// return -1 return 0 return 1
|
||
// logger.info(`level/sn/start/end/bufEnd:${level}/${candidate.sn}/${candidate.start}/${(candidate.start+candidate.duration)}/${bufferEnd}`);
|
||
// Set the lookup tolerance to be small enough to detect the current segment - ensures we don't skip over very small segments
|
||
const candidateLookupTolerance = Math.min(maxFragLookUpTolerance, candidate.duration);
|
||
if (candidate.start + candidate.duration - candidateLookupTolerance <= bufferEnd) {
|
||
return 1;
|
||
}
|
||
else if (candidate.start - candidateLookupTolerance > bufferEnd && candidate.start) {
|
||
// if maxFragLookUpTolerance will have negative value then don't return -1 for first element
|
||
return -1;
|
||
}
|
||
return 0;
|
||
},
|
||
};
|
||
var FragmentFetchHelper$1 = FragmentFetchHelper;
|
||
|
||
const loggerName$9 = { name: 'disco' };
|
||
const DiscoHelper = {
|
||
startFragmentInCC: function (fragments, cc, candidate) {
|
||
let rank = cc - candidate.discoSeqNum;
|
||
if (rank === 0) {
|
||
const fragPrevious = fragments[candidate.mediaSeqNum - fragments[0].mediaSeqNum - 1];
|
||
rank = fragPrevious && fragPrevious.discoSeqNum === candidate.discoSeqNum ? -1 : 0;
|
||
}
|
||
return rank;
|
||
},
|
||
endFragmentInCC: function (fragments, cc, candidate) {
|
||
let rank = cc - candidate.discoSeqNum;
|
||
if (rank === 0) {
|
||
const fragNext = fragments[candidate.mediaSeqNum - fragments[0].mediaSeqNum + 1];
|
||
rank = fragNext && fragNext.discoSeqNum === candidate.discoSeqNum ? 1 : 0;
|
||
}
|
||
return rank;
|
||
},
|
||
findStartEndFragmentsInCC: function (fragments, cc, logger) {
|
||
let startFrag;
|
||
let endFrag;
|
||
if ((fragments === null || fragments === void 0 ? void 0 : fragments.length) > 0 && isFiniteNumber(cc)) {
|
||
startFrag = BinarySearch.search(fragments, DiscoHelper.startFragmentInCC.bind(null, fragments, cc));
|
||
endFrag = BinarySearch.search(fragments, DiscoHelper.endFragmentInCC.bind(null, fragments, cc));
|
||
}
|
||
if (!endFrag) {
|
||
logger.info(loggerName$9, `cc ${cc} does not appear in fragments anymore`);
|
||
}
|
||
// handle bogus results due to undefined cc
|
||
if (startFrag && !isFiniteNumber(startFrag.discoSeqNum)) {
|
||
logger.info(loggerName$9, `findStartEndFragmentsInCC startFrag ${startFrag.mediaSeqNum} has undefined cc ${cc}`);
|
||
startFrag = undefined;
|
||
}
|
||
if (endFrag && !isFiniteNumber(endFrag.discoSeqNum)) {
|
||
logger.info(loggerName$9, `findStartEndFragmentsInCC endFrag ${endFrag.mediaSeqNum} has undefined cc ${cc}`);
|
||
endFrag = undefined;
|
||
}
|
||
return { startFrag, endFrag };
|
||
},
|
||
getTimeRangeDictForCC: function (fragments, cc, logger) {
|
||
const ccFragRange = DiscoHelper.findStartEndFragmentsInCC(fragments, cc, logger);
|
||
const { startFrag, endFrag } = ccFragRange;
|
||
const start = startFrag.start;
|
||
const end = endFrag.start + endFrag.duration;
|
||
return { start, end };
|
||
},
|
||
getTimeRangeForCC: function (fragments, cc, logger) {
|
||
const ccFragRange = DiscoHelper.findStartEndFragmentsInCC(fragments, cc, logger);
|
||
const { startFrag, endFrag } = ccFragRange;
|
||
let ccTimeRange = [];
|
||
if (startFrag && endFrag) {
|
||
ccTimeRange = [startFrag.start, endFrag.start + endFrag.duration];
|
||
}
|
||
return ccTimeRange;
|
||
},
|
||
snapToCCTimeRange: function (targetTime, fragments, cc, logger) {
|
||
const ccRange = DiscoHelper.getTimeRangeForCC(fragments, cc, logger);
|
||
if (!(ccRange === null || ccRange === void 0 ? void 0 : ccRange.length)) {
|
||
return targetTime;
|
||
}
|
||
return Math.min(ccRange[1], Math.max(ccRange[0], targetTime));
|
||
},
|
||
getMinTimeForCC(mainFragments, audioFragments, cc, logger) {
|
||
if (!(mainFragments === null || mainFragments === void 0 ? void 0 : mainFragments.length)) {
|
||
return 0;
|
||
}
|
||
// const fragments = this.variantManager.getVariantInfoIfValid(this.currentVariantId)?.details?.fragments;
|
||
let startCCTime = 0;
|
||
if (isFiniteNumber(cc) && mainFragments) {
|
||
const ccRange = DiscoHelper.getTimeRangeForCC(mainFragments, cc, logger);
|
||
if (ccRange) {
|
||
const audioCCRange = DiscoHelper.getTimeRangeForCC(audioFragments, cc, logger); // this.audioStreamController?.getAudioTimeRangeForCC(cc);
|
||
if (audioCCRange === null || audioCCRange === void 0 ? void 0 : audioCCRange.length) {
|
||
startCCTime = Math.max(audioCCRange[0], ccRange[0]);
|
||
}
|
||
else {
|
||
startCCTime = ccRange[0];
|
||
}
|
||
}
|
||
}
|
||
return startCCTime;
|
||
},
|
||
discoSeqNumForTime(fragments, pos, tolerance = 0) {
|
||
const foundFrag = BinarySearch.search(fragments, FragmentFetchHelper$1.fragmentWithinToleranceTest.bind(null, pos, tolerance));
|
||
return foundFrag === null || foundFrag === void 0 ? void 0 : foundFrag.discoSeqNum;
|
||
},
|
||
};
|
||
var DiscoHelper$1 = DiscoHelper;
|
||
|
||
const VOID = of(void 0);
|
||
/**
|
||
* Wait for some condition to be true on an observable
|
||
* @param source Source observable
|
||
* @param condition Condition to apply
|
||
* @param times How many times to allow observable to fire before completing. By default, 1
|
||
*/
|
||
function waitFor(source, condition, times = 1) {
|
||
return source.pipe(filter(condition), take(times));
|
||
}
|
||
|
||
/*
|
||
* Simple clock abstraction to help transform between clocks and their timelines
|
||
*
|
||
* 2020 Apple Inc. All rights reserved.
|
||
*/
|
||
/**
|
||
* Convert a SyncTimelineValue into a different timeline
|
||
*
|
||
* @param sourceValue The value to map to a different timeline
|
||
* @param destTimeline The timeline to map into
|
||
*
|
||
* @returns A new SyncTimelineValue in destTimeline
|
||
*/
|
||
function mapValueToTimeline(sourceValue, destTimeline) {
|
||
const sourceTimeline = sourceValue.timeline;
|
||
const newValue = ((sourceValue.seconds - sourceTimeline.rootTimeSeconds) / sourceTimeline.rate) * destTimeline.rate + destTimeline.rootTimeSeconds;
|
||
return { seconds: newValue, timeline: destTimeline };
|
||
}
|
||
|
||
class IframeClock {
|
||
constructor() {
|
||
this._timeline = null;
|
||
}
|
||
get forward() {
|
||
var _a;
|
||
return ((_a = this.timeline) === null || _a === void 0 ? void 0 : _a.rate) > 0;
|
||
}
|
||
get isStarted() {
|
||
return Boolean(this.hostClock);
|
||
}
|
||
start(timeline) {
|
||
this.stopTime = null;
|
||
this._timeline = Object.assign({}, timeline);
|
||
const performanceTimeline = { rootTimeSeconds: performance.now() / 1000, rate: 1 };
|
||
this.hostClock = {
|
||
timeline: performanceTimeline,
|
||
getCurrentTime: () => {
|
||
return { seconds: performance.now() / 1000, timeline: performanceTimeline };
|
||
},
|
||
};
|
||
}
|
||
pause() {
|
||
if (this.isStarted) {
|
||
this.stopTime = this.getCurrentTime();
|
||
}
|
||
}
|
||
stop() {
|
||
this.pause();
|
||
this.hostClock = null;
|
||
this._timeline = null;
|
||
}
|
||
// ClockSyncAdapter interface
|
||
get timeline() {
|
||
return this._timeline;
|
||
}
|
||
getCurrentTime() {
|
||
if (this.stopTime) {
|
||
return this.stopTime;
|
||
}
|
||
// map the performance clock into the iframe timeline
|
||
const hostValue = this.hostClock.getCurrentTime();
|
||
const myValue = mapValueToTimeline(hostValue, this.timeline);
|
||
return myValue;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* This is a view into our media element we're appending into. We don't care about any time other
|
||
* than the buffer end, so that's our time source
|
||
*/
|
||
class MediaAppendClock {
|
||
constructor(_timeline) {
|
||
this._timeline = _timeline;
|
||
// our start time is our current buffer end
|
||
this.mediaRootTime = this.lastTimeSeconds = _timeline.rootTimeSeconds;
|
||
}
|
||
// ClockSyncAdapter interface
|
||
get timeline() {
|
||
return Object.assign({}, this._timeline);
|
||
}
|
||
getCurrentTime() {
|
||
// the latest time we know about
|
||
return { seconds: this.lastTimeSeconds, timeline: Object.assign({}, this._timeline) };
|
||
}
|
||
}
|
||
|
||
/*
|
||
* Trickplay implementation
|
||
*
|
||
* 2020 Apple Inc. All rights reserved.
|
||
*/
|
||
const loggerName$8 = { name: 'ifm' };
|
||
/**
|
||
*
|
||
* A sort of 'controller' for iframes
|
||
*
|
||
* Managages two clocks:
|
||
* The iframe clock has a real time host and is untied to the media progression
|
||
* The media append clock represents the media element, where we are actually appending the iframes
|
||
*
|
||
* The responsibility of this class is to supply the iframes fragments via nextFragment so that
|
||
* the media append clock keeps pace with the iframe clock (no drift)
|
||
*
|
||
*/
|
||
class IframeMachine {
|
||
constructor(config, logger) {
|
||
this.config = config;
|
||
this.logger = logger;
|
||
// Fragments we've modified and need to cleanup
|
||
this.scaledFragments = [];
|
||
// this is the 'iframe clock', running at the iframe rate
|
||
// with a timebase anchored at the time trickplay startart
|
||
this.iframeClock = new IframeClock();
|
||
this.hasMore$ = new BehaviorSubject(true);
|
||
this.logger = logger.child(loggerName$8);
|
||
}
|
||
destroy() {
|
||
this.stop();
|
||
}
|
||
resetScaledSegments() {
|
||
this.scaledFragments = [];
|
||
}
|
||
get anchorFrag() {
|
||
return this.scaledFragments.length ? this.scaledFragments[0] : null;
|
||
}
|
||
get lastFrag() {
|
||
return this.scaledFragments.length ? this.scaledFragments.slice(-1)[0] : null;
|
||
}
|
||
get isStarted() {
|
||
return Boolean(this.iframeClock.isStarted);
|
||
}
|
||
get iframeRate() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.iframeClock.timeline) === null || _a === void 0 ? void 0 : _a.rate) !== null && _b !== void 0 ? _b : 0;
|
||
}
|
||
get iframeClockTimeSeconds() {
|
||
return Math.max(0, this.iframeClock.getCurrentTime().seconds);
|
||
}
|
||
get mediaAppendClockTimeSeconds() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.mediaAppendClock) === null || _a === void 0 ? void 0 : _a.getCurrentTime().seconds) !== null && _b !== void 0 ? _b : 0;
|
||
}
|
||
get mediaRootTime() {
|
||
var _a;
|
||
return (_a = this.mediaAppendClock) === null || _a === void 0 ? void 0 : _a.mediaRootTime;
|
||
}
|
||
pause() {
|
||
this.iframeClock.pause();
|
||
}
|
||
stop() {
|
||
this.logger.debug('stop');
|
||
this.hasMore$.next(true);
|
||
this.iframeClock.stop();
|
||
this.mediaAppendClock = null;
|
||
this.resetScaledSegments();
|
||
}
|
||
checkHasMore() {
|
||
this.hasMore$.next(true);
|
||
}
|
||
/**
|
||
* Re-anchor the iframe clock
|
||
* @param iframeFragments
|
||
* @param audioFragments
|
||
* @param rate
|
||
* @param pos
|
||
*/
|
||
startClocksAndGetFirstFragment(iframeFragments, audioFragments, rate, pos, disco) {
|
||
this.logger.info(`startClocksAndGetFirstFragment: rate(${rate}), pos(${pos})`);
|
||
let anchorFrag = BinarySearch.search(iframeFragments, FragmentFetchHelper$1.fragmentWithinToleranceTest.bind(null, pos, 0.0001));
|
||
if (!anchorFrag && pos >= iframeFragments[iframeFragments.length - 1].start) {
|
||
this.logger.info(`startClocksAndGetFirstFragment: pick last frag, start=${iframeFragments[iframeFragments.length - 1].start}`);
|
||
anchorFrag = iframeFragments[iframeFragments.length - 1];
|
||
}
|
||
if (!anchorFrag) {
|
||
this.logger.error(`startClocksAndGetFirstFragment => no anchorFrag for time ${pos}`);
|
||
this.hasMore$.next(false);
|
||
return null;
|
||
}
|
||
const nextDisco = isFiniteNumber(disco) ? disco : anchorFrag.discoSeqNum;
|
||
const startCCTime = DiscoHelper$1.getMinTimeForCC(iframeFragments, audioFragments, nextDisco, this.logger);
|
||
const newPosition = isFiniteNumber(disco) && rate > 1 ? startCCTime : pos;
|
||
let newMediaRootTime;
|
||
if (rate > 1) {
|
||
// fast forward should always have enough to anchor at the current point
|
||
newMediaRootTime = Math.ceil(newPosition);
|
||
}
|
||
else {
|
||
newMediaRootTime = Math.ceil(startCCTime);
|
||
}
|
||
this.iframeClock.start({ rootTimeSeconds: newPosition, rate });
|
||
this.mediaAppendClock = new MediaAppendClock({ rootTimeSeconds: newMediaRootTime, rate: 1 });
|
||
return { frag: anchorFrag, newMediaRootTime };
|
||
}
|
||
getNextFragment(fragments, bufferEnd, rate) {
|
||
const lastFrag = this.lastFrag;
|
||
const { iframeClock, mediaAppendClock } = this;
|
||
const iframeTimeline = iframeClock.timeline;
|
||
mediaAppendClock.lastTimeSeconds = bufferEnd;
|
||
this.logger.qe({
|
||
critical: true,
|
||
name: 'iframes',
|
||
data: { maxMediaTime: bufferEnd, rate, ifmat: this.mediaAppendClock.timeline.rootTimeSeconds, ifbt: this.iframeClock.timeline.rootTimeSeconds },
|
||
});
|
||
const ixStep = iframeClock.forward ? 1 : -1;
|
||
let ix = Math.max(0, Math.min(lastFrag.mediaSeqNum - fragments[0].mediaSeqNum + ixStep, fragments.length - 1));
|
||
let nextFrag;
|
||
if (lastFrag.mediaSeqNum !== fragments[ix].mediaSeqNum) {
|
||
// always allow first step to work for EOS cleanliness
|
||
do {
|
||
nextFrag = fragments[ix];
|
||
const seconds = iframeClock.forward ? nextFrag.start : nextFrag.start + nextFrag.duration;
|
||
const startAppendTime = mapValueToTimeline({ seconds, timeline: iframeTimeline }, mediaAppendClock.timeline);
|
||
if (startAppendTime.seconds >= bufferEnd && ((iframeClock.forward && seconds >= this.iframeClockTimeSeconds) || (!iframeClock.forward && seconds <= this.iframeClockTimeSeconds))) {
|
||
break;
|
||
}
|
||
ix += ixStep;
|
||
} while (ix > 0 && ix < fragments.length);
|
||
}
|
||
if (!nextFrag) {
|
||
// stream controller should have stopped and waited until we said so, and we should have found something new
|
||
this.logger.error(`getNextFragment(bufferEnd: ${bufferEnd}) => no more frags, but we should have found an end fragment, setting hasMore to false`);
|
||
this.hasMore$.next(false);
|
||
}
|
||
return { frag: nextFrag };
|
||
}
|
||
/**
|
||
* Get the next fragment to fetch
|
||
* @param iframeFragments
|
||
* @param audioFragments
|
||
* @param rate
|
||
* @param pos
|
||
* @param bufferLen
|
||
* @return anchor fragment
|
||
*/
|
||
nextFragment(iframeFragments, audioFragments, rate, pos) {
|
||
let foundFrag;
|
||
let rateSwitchPos;
|
||
if (this.isStarted && rate !== this.iframeRate) {
|
||
rateSwitchPos = this.iframeClockTimeSeconds;
|
||
this.stop();
|
||
}
|
||
if (!this.isStarted) {
|
||
foundFrag = this.startClocksAndGetFirstFragment(iframeFragments, audioFragments, rate, rateSwitchPos !== null && rateSwitchPos !== void 0 ? rateSwitchPos : pos);
|
||
if (!foundFrag) {
|
||
return;
|
||
}
|
||
}
|
||
else {
|
||
foundFrag = this.getNextFragment(iframeFragments, pos, rate);
|
||
if (!foundFrag.frag) {
|
||
return;
|
||
}
|
||
else if (foundFrag.frag.discoSeqNum !== this.anchorFrag.discoSeqNum) {
|
||
const iframeTimeSeconds = this.iframeClockTimeSeconds;
|
||
this.logger.info(`nextFragment iframeClockTime(${iframeTimeSeconds.toFixed(3)}) => found new disco, ${this.anchorFrag.discoSeqNum} to ${foundFrag.frag.discoSeqNum}`);
|
||
this.stop();
|
||
const { newMediaRootTime } = this.startClocksAndGetFirstFragment(iframeFragments, audioFragments, rate, iframeTimeSeconds, foundFrag.frag.discoSeqNum);
|
||
foundFrag.newMediaRootTime = newMediaRootTime;
|
||
}
|
||
}
|
||
const { frag, newMediaRootTime } = foundFrag;
|
||
const updatedFrag = this.handleNextFrag(iframeFragments, frag);
|
||
const appendStart = updatedFrag.iframeMediaStart, appendEnd = appendStart + updatedFrag.iframeMediaDuration;
|
||
this.logger.info(`nextFragment iframeClockTime(${this.iframeClockTimeSeconds.toFixed(3)}) => picked frag: ${fragTag(updatedFrag)}, source range: ${rangeString(updatedFrag)}, media append times: ${appendStart.toFixed(3)}-${appendEnd.toFixed(3)}${isFiniteNumber(newMediaRootTime) ? ', newMediaRootTime: ' + newMediaRootTime.toFixed(3) : ''}`);
|
||
return Object.assign(Object.assign({}, foundFrag), { frag: updatedFrag });
|
||
}
|
||
handleNextFrag(fragments, frag) {
|
||
const cpFrag = Object.assign(Object.assign({}, frag), { iframeMediaStart: NaN, iframeMediaDuration: NaN });
|
||
const { mediaAppendClock, iframeClock } = this;
|
||
// update our bounds just in case we're live
|
||
this.iframeClockBounds = DiscoHelper$1.getTimeRangeDictForCC(fragments, cpFrag.discoSeqNum, this.logger);
|
||
// the start of our iframe is the latest time of the media append clock
|
||
const bufferEnd = mediaAppendClock.getCurrentTime().seconds;
|
||
const iframeRate = iframeClock.timeline.rate;
|
||
if (!isFiniteNumber(cpFrag.iframeOriginalStart)) {
|
||
// iframe mode shouldn't have mangled frag.start
|
||
// we need the original frag.start to calculate initPTS.
|
||
// stash it in frag.iframeOriginalStart
|
||
cpFrag.iframeOriginalStart = cpFrag.start;
|
||
}
|
||
cpFrag.start = cpFrag.iframeMediaStart = bufferEnd;
|
||
cpFrag.iframeMediaDuration = Math.max(cpFrag.duration / Math.abs(iframeRate), 1 / this.config.minIframeDuration);
|
||
// track which frags we've modifed - TODO find a better way to do this
|
||
this.scaledFragments.push(cpFrag);
|
||
// check for EOS
|
||
if (this.isEndFrag(fragments, cpFrag)) {
|
||
this.logger.info(`checkEOS(frag: ${cpFrag.mediaSeqNum}) => found end frag, setting hasMore to false`);
|
||
this.hasMore$.next(false);
|
||
}
|
||
return cpFrag;
|
||
}
|
||
get hasMore() {
|
||
return this.hasMore$.value;
|
||
}
|
||
// anchor isn't necessary valid in this function, so don't reference it
|
||
/**
|
||
* Wait for there to be more fragments to be downloaded
|
||
*/
|
||
handleWaitForMore() {
|
||
this.logger.trace(`handleWaitForMore(): isStarted: ${this.isStarted}`);
|
||
if (!this.isStarted) {
|
||
return VOID;
|
||
}
|
||
const currentIframeTime = this.iframeClock.getCurrentTime();
|
||
const iframeClockTime = currentIframeTime.seconds;
|
||
const rate = currentIframeTime.timeline.rate;
|
||
const edgeTime = rate > 1 ? this.iframeClockBounds.end : this.iframeClockBounds.start;
|
||
const diff = edgeTime - currentIframeTime.seconds;
|
||
const wait = diff / rate; // the +/- work out with the rate and the diff
|
||
this.logger.info(`handleWaitForMore(): iframeClockTime: ${iframeClockTime}, edgeTime: ${edgeTime}, diff ${diff} => wait: ${wait}`);
|
||
return waitFor(this.hasMore$, (hasMore) => hasMore === true).pipe(tap(() => {
|
||
this.logger.info(`handleWaitForMore(): iframeClockTime: ${iframeClockTime} => hasMore became true while waiting`);
|
||
}));
|
||
}
|
||
isEndFrag(fragments, frag) {
|
||
const startFrag = fragments[0];
|
||
const endFrag = fragments[fragments.length - 1];
|
||
const ff = this.iframeClock.forward;
|
||
const rw = !ff;
|
||
return (rw && (startFrag === null || startFrag === void 0 ? void 0 : startFrag.mediaSeqNum) === frag.mediaSeqNum) || (ff && (endFrag === null || endFrag === void 0 ? void 0 : endFrag.mediaSeqNum) === frag.mediaSeqNum);
|
||
}
|
||
}
|
||
function createIframeMachine(config, logger) {
|
||
return new Observable((subscriber) => {
|
||
const iframeMachine = new IframeMachine(config, logger);
|
||
subscriber.next(iframeMachine);
|
||
return () => {
|
||
// cleanup
|
||
};
|
||
});
|
||
}
|
||
|
||
function _classCallCheck(instance, Constructor) {
|
||
if (!(instance instanceof Constructor)) {
|
||
throw new TypeError("Cannot call a class as a function");
|
||
}
|
||
}
|
||
|
||
function _defineProperties(target, props) {
|
||
for (var i = 0; i < props.length; i++) {
|
||
var descriptor = props[i];
|
||
descriptor.enumerable = descriptor.enumerable || false;
|
||
descriptor.configurable = true;
|
||
if ("value" in descriptor) descriptor.writable = true;
|
||
Object.defineProperty(target, descriptor.key, descriptor);
|
||
}
|
||
}
|
||
|
||
function _createClass(Constructor, protoProps, staticProps) {
|
||
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
|
||
if (staticProps) _defineProperties(Constructor, staticProps);
|
||
return Constructor;
|
||
}
|
||
|
||
function _defineProperty(obj, key, value) {
|
||
if (key in obj) {
|
||
Object.defineProperty(obj, key, {
|
||
value: value,
|
||
enumerable: true,
|
||
configurable: true,
|
||
writable: true
|
||
});
|
||
} else {
|
||
obj[key] = value;
|
||
}
|
||
|
||
return obj;
|
||
}
|
||
|
||
/**
|
||
* Use of this source code is governed by an MIT-style license that
|
||
* can be found in the LICENSE file at https://github.com/cartant/rxjs-spy
|
||
*/
|
||
|
||
/*tslint:disable:no-use-before-declare*/
|
||
function tag(tag) {
|
||
return function tagOperation(source) {
|
||
return source.lift(new TagOperator(tag));
|
||
};
|
||
}
|
||
|
||
var TagOperator = /*#__PURE__*/function () {
|
||
// It would be better if this were a symbol. However ...
|
||
// error TS1166: A computed property name in a class property declaration must directly refer to a built-in symbol.
|
||
function TagOperator(tag) {
|
||
_classCallCheck(this, TagOperator);
|
||
|
||
_defineProperty(this, "tag", void 0);
|
||
|
||
this.tag = tag;
|
||
}
|
||
|
||
_createClass(TagOperator, [{
|
||
key: "call",
|
||
value: function call(subscriber, source) {
|
||
return source.subscribe(subscriber);
|
||
}
|
||
}]);
|
||
|
||
return TagOperator;
|
||
}();
|
||
|
||
// Key request state from a macro level (substates in KeyRequestContext)
|
||
var KeyRequestMacroState;
|
||
(function (KeyRequestMacroState) {
|
||
KeyRequestMacroState["MustRequestResponse"] = "MustRequestResponse";
|
||
KeyRequestMacroState["WaitingForKeyResponse"] = "WaitingForKeyResponse";
|
||
KeyRequestMacroState["GotKeyResponse"] = "GotKeyResponse";
|
||
})(KeyRequestMacroState || (KeyRequestMacroState = {}));
|
||
// TODO convert DecryptData types to ArrayBuffer so we don't need these anymore
|
||
function keyTagInfoToEntity(keyInfo) {
|
||
var _a, _b, _c, _d, _e;
|
||
const { method, isEncrypted, uri, format, formatversions } = keyInfo;
|
||
return {
|
||
method,
|
||
isEncrypted,
|
||
uri,
|
||
format,
|
||
formatversions,
|
||
ivBuf: (_b = (_a = keyInfo.iv) === null || _a === void 0 ? void 0 : _a.buffer) !== null && _b !== void 0 ? _b : null,
|
||
keyIdBuf: (_c = keyInfo.keyId) === null || _c === void 0 ? void 0 : _c.buffer,
|
||
keyBuf: (_d = keyInfo.key) === null || _d === void 0 ? void 0 : _d.buffer,
|
||
psshBuf: (_e = keyInfo.key) === null || _e === void 0 ? void 0 : _e.buffer,
|
||
};
|
||
}
|
||
function entityToKeyTagInfo(entity) {
|
||
const { method, isEncrypted, uri, format, formatversions } = entity;
|
||
const keyTagInfo = {
|
||
method,
|
||
isEncrypted,
|
||
uri,
|
||
format,
|
||
formatversions,
|
||
iv: entity.ivBuf ? new Uint8Array(entity.ivBuf) : null,
|
||
};
|
||
if (entity.keyIdBuf) {
|
||
keyTagInfo.keyId = new Uint8Array(entity.keyIdBuf);
|
||
}
|
||
if (entity.keyBuf) {
|
||
keyTagInfo.key = new Uint8Array(entity.keyBuf);
|
||
}
|
||
if (entity.psshBuf) {
|
||
keyTagInfo.pssh = new Uint8Array(entity.psshBuf);
|
||
}
|
||
return keyTagInfo;
|
||
}
|
||
|
||
// Generic network error
|
||
class GenericNetworkError extends Error {
|
||
constructor(message, code) {
|
||
super(message);
|
||
this.code = code;
|
||
}
|
||
}
|
||
class HlsNetworkError extends HlsError {
|
||
constructor(details, fatal, reason, code, response, isTimeout) {
|
||
super(ErrorTypes.NETWORK_ERROR, details, fatal, reason, response);
|
||
this.code = code;
|
||
this.isTimeout = isTimeout;
|
||
this.response = response;
|
||
}
|
||
}
|
||
class ManifestNetworkError extends HlsNetworkError {
|
||
constructor(fatal, reason, code, response, isTimeout) {
|
||
super(isTimeout ? ErrorDetails.MANIFEST_LOAD_TIMEOUT : ErrorDetails.MANIFEST_LOAD_ERROR, fatal, reason, code, response, isTimeout);
|
||
}
|
||
}
|
||
// Problem when loading playlist or manifest
|
||
class PlaylistNetworkError extends HlsNetworkError {
|
||
constructor(fatal, reason, code, response, isTimeout, mediaOptionType, mediaOptionId, url) {
|
||
super('', fatal, reason, code, response, isTimeout);
|
||
this.mediaOptionType = mediaOptionType;
|
||
this.mediaOptionId = mediaOptionId;
|
||
this.url = url;
|
||
switch (mediaOptionType) {
|
||
case MediaOptionType.Variant:
|
||
this.details = isTimeout ? ErrorDetails.LEVEL_LOAD_TIMEOUT : ErrorDetails.LEVEL_LOAD_ERROR;
|
||
break;
|
||
case MediaOptionType.AltAudio:
|
||
this.details = isTimeout ? ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT : ErrorDetails.AUDIO_TRACK_LOAD_ERROR;
|
||
break;
|
||
case MediaOptionType.Subtitle:
|
||
this.details = isTimeout ? ErrorDetails.SUBTITLE_TRACK_LOAD_TIMEOUT : ErrorDetails.SUBTITLE_TRACK_LOAD_ERROR;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
// Problem when loading SessionData
|
||
class SessionDataNetworkError extends HlsNetworkError {
|
||
constructor(fatal, reason, code, response) {
|
||
super(ErrorDetails.SESSION_DATA_LOAD_ERROR, fatal, reason, code, response, false);
|
||
}
|
||
}
|
||
// Problem when loading fragment
|
||
class FragmentNetworkError extends HlsNetworkError {
|
||
constructor(fatal, reason, code, response, isTimeout, frag, stats) {
|
||
super(isTimeout ? ErrorDetails.FRAG_LOAD_TIMEOUT : ErrorDetails.FRAG_LOAD_ERROR, fatal, reason, code, response, isTimeout);
|
||
this.mediaOptionId = frag.mediaOptionId;
|
||
this.mediaOptionType = frag.mediaOptionType;
|
||
this.stats = stats;
|
||
}
|
||
}
|
||
class GenericTimeoutError extends TimeoutError {
|
||
constructor(message, code, stats) {
|
||
super();
|
||
this.message = message;
|
||
this.code = code;
|
||
this.stats = stats;
|
||
}
|
||
}
|
||
class FragmentAbortError extends HlsNetworkError {
|
||
constructor(frag, candidateMediaOptionId, response) {
|
||
super('fragAbortError', false, 'Fragment abort', 0, response, false);
|
||
this.candidateMediaOptionId = candidateMediaOptionId;
|
||
this.mediaOptionId = frag.mediaOptionId;
|
||
this.mediaOptionType = frag.mediaOptionType;
|
||
}
|
||
}
|
||
|
||
// Errors generated by CDM / KeySystem
|
||
class KeyRequestTimeoutError extends HlsNetworkError {
|
||
constructor(message, keyuri, response, mediaOptionIds = []) {
|
||
super(ErrorDetails.KEY_LOAD_TIMEOUT, false, message, response.code, response, true);
|
||
this.keyuri = keyuri;
|
||
this.response = response;
|
||
this.mediaOptionIds = mediaOptionIds;
|
||
}
|
||
}
|
||
var KeyRequestErrorReason;
|
||
(function (KeyRequestErrorReason) {
|
||
KeyRequestErrorReason[KeyRequestErrorReason["InvalidState"] = 0] = "InvalidState";
|
||
KeyRequestErrorReason[KeyRequestErrorReason["Abort"] = 1] = "Abort";
|
||
KeyRequestErrorReason[KeyRequestErrorReason["OutputRestricted"] = 2] = "OutputRestricted";
|
||
KeyRequestErrorReason[KeyRequestErrorReason["AlreadyFailedKey"] = 3] = "AlreadyFailedKey";
|
||
KeyRequestErrorReason[KeyRequestErrorReason["HttpError"] = 4] = "HttpError";
|
||
KeyRequestErrorReason[KeyRequestErrorReason["InternalError"] = 5] = "InternalError";
|
||
KeyRequestErrorReason[KeyRequestErrorReason["LicenseServerError"] = 6] = "LicenseServerError";
|
||
KeyRequestErrorReason[KeyRequestErrorReason["InsufficientCPC"] = 7] = "InsufficientCPC";
|
||
})(KeyRequestErrorReason || (KeyRequestErrorReason = {}));
|
||
// Something that failed in the actual request to the license server
|
||
class KeyRequestError extends HlsNetworkError {
|
||
constructor(message, keyuri, code, response, isOkToRetry, keyErrorReason, fatal = false, mediaOptionIds = []) {
|
||
super(ErrorDetails.KEY_LOAD_ERROR, fatal, message, code, response, false);
|
||
this.keyuri = keyuri;
|
||
this.isOkToRetry = isOkToRetry;
|
||
this.keyErrorReason = keyErrorReason;
|
||
this.mediaOptionIds = mediaOptionIds;
|
||
}
|
||
}
|
||
// Something that failed due to key exchange or key system initialization
|
||
class KeySystemError extends HlsError {
|
||
constructor(message, keyuri, code, response, keysystemstring) {
|
||
super(ErrorTypes.OTHER_ERROR, ErrorDetails.KEY_SYSTEM_GENERIC_ERROR, true, message, response);
|
||
this.keyuri = keyuri;
|
||
this.code = code;
|
||
this.response = response;
|
||
this.keysystemstring = keysystemstring;
|
||
}
|
||
}
|
||
function copyKeyError(error, mediaOptionIds) {
|
||
if (error instanceof KeyRequestError) {
|
||
return new KeyRequestError(error.message, error.keyuri, error.code, error.response, error.isOkToRetry, error.keyErrorReason, error.fatal, mediaOptionIds);
|
||
}
|
||
else if (error instanceof KeyRequestTimeoutError) {
|
||
return new KeyRequestTimeoutError(error.message, error.keyuri, ErrorResponses.CryptResponseReceivedSlowly, mediaOptionIds);
|
||
}
|
||
else if (error) {
|
||
return new ExceptionError(error.fatal, error.reason, ErrorResponses.InternalError);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/*
|
||
* FairPlayStreaming key system constants
|
||
*
|
||
* 2018 Apple Inc. All rights reserved.
|
||
*/
|
||
const FairPlaySecurityLevels = {
|
||
AppleBaseline: 0,
|
||
AppleMain: 1,
|
||
Main: 1,
|
||
Baseline: 0,
|
||
};
|
||
const FairPlayStreamingKeySystemProperties = {
|
||
id: 'fairplaystreaming',
|
||
systemStringPrefix: 'com.apple.fps',
|
||
keyFormatString: 'com.apple.streamingkeydelivery',
|
||
securityLevels: FairPlaySecurityLevels,
|
||
};
|
||
|
||
class LoaderQuery extends QueryEntity {
|
||
constructor(store) {
|
||
super(store);
|
||
this.store = store;
|
||
}
|
||
get unresolvedUriLoading$() {
|
||
return this.selectEntityAction(EntityActions.Add).pipe(map((entityIds) => entityIds.map((id) => this.getEntity(id))));
|
||
}
|
||
}
|
||
|
||
class LoaderStore extends EntityStore {
|
||
constructor() {
|
||
super({}, { name: 'loader', producerFn: produce_1, idKey: 'uri' });
|
||
}
|
||
}
|
||
|
||
class LoaderService {
|
||
constructor(store) {
|
||
this.store = store;
|
||
}
|
||
createUnresolvedUriLoading(uri, responseType, userAgent) {
|
||
logAction('loader.create.unresolvedUriLoading');
|
||
this.store.add({ uri, responseType, userAgent });
|
||
}
|
||
removeUnresolvedUriLoading(uri) {
|
||
logAction('loader.remove.unresolvedUriLoading');
|
||
this.store.remove(uri);
|
||
}
|
||
}
|
||
const store$1 = new LoaderStore();
|
||
let service$2 = null; // To be instantiated in loaderService()
|
||
/***********************************************
|
||
* Static helper functions that specifically use the above singletons
|
||
*/
|
||
function createLoaderQuery() {
|
||
return new LoaderQuery(store$1);
|
||
}
|
||
/**
|
||
* @returns The global instance of LoaderService that operates on global LoaderStore
|
||
*/
|
||
function getLoaderService() {
|
||
if (!service$2) {
|
||
service$2 = new LoaderService(store$1);
|
||
}
|
||
return service$2;
|
||
}
|
||
|
||
/*
|
||
* HLS Event Emitter
|
||
*
|
||
*
|
||
*/
|
||
/**
|
||
* Simple adapter sub-class of Nodejs-like EventEmitter.
|
||
*/
|
||
class HlsEventEmitter extends EventEmitter {
|
||
/**
|
||
* We simply want to pass along the event-name itself
|
||
* in every call to a handler, which is the purpose of our `trigger` method
|
||
* extending the standard API.
|
||
*/
|
||
trigger(event, data) {
|
||
try {
|
||
this.emit(event, event, data);
|
||
if (event.toString() !== 'hlsFragLoadProgress') {
|
||
// do not log fragLoadProgress, too noisy
|
||
// <rdar://35043665> Do NOT log data, which can be very large (for instance the data1: and data2: arguments from demuxer
|
||
// console will hold onto the arguments in here until the next event loop and can cause memory to balloon over time
|
||
let details = '';
|
||
if ((event === 'hlsInternalError' || event === 'hlsError') && data.length) {
|
||
details = JSON.stringify(data[0], ['fatal', 'details', 'reason']);
|
||
}
|
||
const eventData = { event: event.toString() };
|
||
if (details.length > 0) {
|
||
eventData.details = details;
|
||
}
|
||
getLogger().qe({ critical: true, name: 'eventTrigger', data: eventData });
|
||
}
|
||
}
|
||
catch (err) {
|
||
getLogger().warn(`error in event listener for ${event}: ${err.message}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
class EventTarget {
|
||
constructor(target, _this) {
|
||
this.target = target;
|
||
this._this = _this;
|
||
}
|
||
eventWithOptions(event, options, handler, _this = this._this) {
|
||
let observable = fromEvent(this.target, event, options);
|
||
if (this.target instanceof HlsEventEmitter) {
|
||
// HLS's raw event handlers accepts two arguments:
|
||
// - event: _Event
|
||
// - data: EventData<_EventDataMap, _Event>
|
||
// We need to map it to single data argument
|
||
observable = observable.pipe(map(([_, data]) => data));
|
||
}
|
||
if (handler) {
|
||
if (_this) {
|
||
handler = handler.bind(_this);
|
||
}
|
||
observable = observable.pipe(tap(handler));
|
||
}
|
||
return observable;
|
||
}
|
||
event(event, handler, _this = this._this) {
|
||
return this.eventWithOptions(event, undefined, handler, _this);
|
||
}
|
||
listen(event, notifier, handler, _this = this._this) {
|
||
return this.event(event, handler, _this).pipe(takeUntil(notifier)).subscribe();
|
||
}
|
||
}
|
||
function fromEventTarget(target, _this) {
|
||
return new EventTarget(target, _this);
|
||
}
|
||
// Note that vuze doesn't implement some events so may not be safe to use ev directly. Use to fill
|
||
function convertEvent(currentTarget, type, ev) {
|
||
var _b, _c;
|
||
return {
|
||
currentTarget: (_b = ev === null || ev === void 0 ? void 0 : ev.currentTarget) !== null && _b !== void 0 ? _b : currentTarget,
|
||
target: (_c = ev === null || ev === void 0 ? void 0 : ev.target) !== null && _c !== void 0 ? _c : currentTarget,
|
||
type: type,
|
||
};
|
||
}
|
||
|
||
|
||
function getByteRangeLength(byteRangeHeader) {
|
||
const BYTERANGE = /([0-9]+)\-([0-9]+)\/([0-9]+)/;
|
||
const result = BYTERANGE.exec(byteRangeHeader);
|
||
return result ? parseInt(result[2]) - parseInt(result[1]) + 1 : undefined;
|
||
}
|
||
function getContentLength(xhr) {
|
||
let contentLength;
|
||
const contentEncoding = xhr.getResponseHeader('Content-Encoding');
|
||
const transferEncoding = xhr.getResponseHeader('Transfer-Encoding');
|
||
const noContentEncoding = !contentEncoding || (contentEncoding && contentEncoding.toLowerCase() === 'identity');
|
||
const noTransferEncoding = !transferEncoding || (transferEncoding && transferEncoding.toLowerCase() === 'identity');
|
||
if (noContentEncoding && noTransferEncoding) {
|
||
// Length only makes sense without encoding/compression
|
||
contentLength = getByteRangeLength(xhr.getResponseHeader('Content-Range'));
|
||
if (!isFiniteNumber(contentLength)) {
|
||
contentLength = parseInt(xhr.getResponseHeader('Content-Length'));
|
||
}
|
||
}
|
||
return contentLength;
|
||
}
|
||
function fromXMLHttpRequest(context, loadConfig) {
|
||
return new Observable((subscriber) => {
|
||
const { maxTimeToFirstByteMs, maxLoadTimeMs } = loadConfig;
|
||
const xhr = new XMLHttpRequest();
|
||
const stats = {
|
||
trequest: performance.now(),
|
||
tfirst: NaN,
|
||
tload: NaN,
|
||
loaded: 0,
|
||
total: NaN,
|
||
complete: false,
|
||
};
|
||
const xhrTarget = fromEventTarget(xhr);
|
||
// register for events
|
||
const onprogress$ = xhrTarget.event('progress').pipe(share(), throttleTime(300, queueScheduler, { leading: true, trailing: true }), map((progressEvent) => {
|
||
if (isNaN(stats.tfirst)) {
|
||
stats.tfirst = performance.now();
|
||
}
|
||
stats.loaded = progressEvent.loaded;
|
||
if (progressEvent.lengthComputable) {
|
||
stats.total = progressEvent.total;
|
||
}
|
||
return progressEvent.target;
|
||
}), filter((req) => req.readyState >= 3));
|
||
const onreadystate$ = xhrTarget.event('readystatechange').pipe(share(), map((event) => event.target), filter((req) => req.readyState >= 2), tap((req) => {
|
||
if (isNaN(stats.tfirst) && req.readyState >= 3) {
|
||
stats.tfirst = performance.now();
|
||
}
|
||
}));
|
||
let timeToFirstByteTimeout$ = EMPTY;
|
||
if (isFinite(maxTimeToFirstByteMs) && maxTimeToFirstByteMs > 0) {
|
||
timeToFirstByteTimeout$ = merge(onprogress$, onreadystate$).pipe(take(1), timeout(context.extendMaxTTFB > 0 ? context.extendMaxTTFB : maxTimeToFirstByteMs), switchMap(() => EMPTY));
|
||
}
|
||
let totalLoadTimeout$ = EMPTY;
|
||
if (isFinite(maxLoadTimeMs) && maxLoadTimeMs > 0) {
|
||
totalLoadTimeout$ = onreadystate$.pipe(filter((req) => req.readyState >= 4), take(1), timeout(maxLoadTimeMs), switchMap(() => EMPTY));
|
||
}
|
||
let onProgressCb$ = EMPTY;
|
||
if (context.onProgress) {
|
||
onProgressCb$ = merge(of(xhr), onprogress$) // Emit on subscribe and then on every 'onprogress' event
|
||
.pipe(map((xhrReq) => {
|
||
const { getData, cb } = context.onProgress;
|
||
return cb(context.url, xhrReq.status, stats, getData ? xhrReq.response : undefined);
|
||
}), takeWhile((done) => !done, true))
|
||
.pipe(switchMapTo(EMPTY));
|
||
}
|
||
const sub = merge(onprogress$.pipe(switchMap(() => EMPTY)), onreadystate$, timeToFirstByteTimeout$, totalLoadTimeout$, onProgressCb$)
|
||
.pipe(filter((req) => req.readyState >= 4), take(1), switchMap((req) => {
|
||
stats.complete = true;
|
||
if (req.status >= 200 && req.status < 300) {
|
||
stats.tload = performance.now();
|
||
stats.contentType = req.getResponseHeader('Content-Type');
|
||
if (loadConfig.reportCDNServer) {
|
||
stats.cdnServer = req.getResponseHeader('CDN-Server');
|
||
}
|
||
stats.contentLength = loadConfig.forceContentLenCheckIfNoHeader ? getContentLength(req) : null; // forceContentLenCheckIfNoHeader is true only for non-browser platforms
|
||
_relateServerInstanceInfoToRequest(req, context);
|
||
if (context.responseType === 'arraybuffer') {
|
||
stats.loaded = req.response.byteLength;
|
||
}
|
||
else {
|
||
stats.loaded = req.responseText.length;
|
||
}
|
||
stats.total = stats.loaded;
|
||
if (context.checkContentLength && // checkContentLength is true only when loading fragment
|
||
(stats.total === 0 || (isFiniteNumber(stats.contentLength) && stats.total != stats.contentLength))) {
|
||
throw new GenericNetworkError('Network error', req.status);
|
||
}
|
||
const res = [req, stats];
|
||
return scheduled(of(res), queueScheduler);
|
||
}
|
||
else {
|
||
throw new GenericNetworkError('Network error', req.status);
|
||
}
|
||
}), catchError((err) => {
|
||
if (err instanceof TimeoutError) {
|
||
throw new GenericTimeoutError(err.message, 0, stats);
|
||
}
|
||
if (!(err instanceof GenericNetworkError)) {
|
||
throw new GenericNetworkError(err.message, 0);
|
||
}
|
||
throw err;
|
||
}))
|
||
.subscribe(subscriber);
|
||
// Setup XHR request
|
||
const { url, method, byteRangeOffset, responseType, body } = context;
|
||
if (context.mimeType) {
|
||
xhr.overrideMimeType(context.mimeType);
|
||
}
|
||
try {
|
||
const xhrSetup = context.xhrSetup;
|
||
if (xhrSetup) {
|
||
try {
|
||
xhrSetup(xhr, url);
|
||
}
|
||
catch (e) {
|
||
// fix xhrSetup: (xhr, url) => {xhr.setRequestHeader("Content-Language", "test");}
|
||
// not working, as xhr.setRequestHeader expects xhr.readyState === OPEN
|
||
xhr.open(method, context.url, true);
|
||
xhrSetup(xhr, context.url);
|
||
}
|
||
}
|
||
if (!xhr.readyState) {
|
||
xhr.open(method, context.url, true);
|
||
}
|
||
}
|
||
catch (e) {
|
||
// IE11 throws an exception on xhr.open if attempting to access an HTTP resource over HTTPS
|
||
throw new GenericNetworkError(e.message, xhr.status);
|
||
}
|
||
xhr.responseType = responseType;
|
||
// Set headers
|
||
if (byteRangeOffset && isFiniteNumber(byteRangeOffset.start) && isFiniteNumber(byteRangeOffset.end) && byteRangeOffset.start >= 0 && byteRangeOffset.end > byteRangeOffset.start) {
|
||
const { start, end } = byteRangeOffset;
|
||
xhr.setRequestHeader('Range', `bytes=${start}-${end - 1}`);
|
||
}
|
||
if (context.headers) {
|
||
for (const [name, value] of Object.entries(context.headers)) {
|
||
xhr.setRequestHeader(name, value);
|
||
}
|
||
}
|
||
// Start request
|
||
if (method === 'POST' && body) {
|
||
xhr.send(body);
|
||
}
|
||
else {
|
||
xhr.send();
|
||
}
|
||
return () => {
|
||
xhr.abort();
|
||
sub.unsubscribe();
|
||
};
|
||
});
|
||
}
|
||
function _relateServerInstanceInfoToRequest(xhr, context) {
|
||
if (context.collectServerInstanceInfo) {
|
||
context.serverInstanceInfo = {};
|
||
context.collectServerInstanceInfo.forEach((header) => {
|
||
const headerValue = xhr.getResponseHeader(header);
|
||
if (headerValue) {
|
||
context.serverInstanceInfo[header] = headerValue;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
const loggerName$7 = { name: 'CustomUrlLoader' };
|
||
class CustomUrlLoader {
|
||
constructor(loaderService) {
|
||
this.loaderService = loaderService;
|
||
this.requestMap = {};
|
||
this.logger = getLogger();
|
||
}
|
||
load(context, loadConfig) {
|
||
return new Observable((observer) => {
|
||
const url = context.url;
|
||
const { maxTimeToFirstByteMs } = loadConfig;
|
||
const stats = {
|
||
trequest: performance.now(),
|
||
tfirst: NaN,
|
||
tload: NaN,
|
||
loaded: 0,
|
||
total: NaN,
|
||
complete: false,
|
||
};
|
||
const request = (this.requestMap[url] = new AsyncSubject());
|
||
const source = request.pipe(timeout(context.extendMaxTTFB > 0 ? context.extendMaxTTFB : maxTimeToFirstByteMs), switchMap((response) => {
|
||
this.logger.info(loggerName$7, `loaded originalUri=${redactUrl(url)} uri=${redactUrl(response.uri)} durationMs=${performance.now() - stats.trequest}`);
|
||
stats.tfirst = performance.now();
|
||
return this.handleExternalResponse(response, context, loadConfig, stats);
|
||
}), catchError((err) => {
|
||
if (err instanceof TimeoutError) {
|
||
throw new GenericTimeoutError(err.message, 0, stats);
|
||
}
|
||
throw err;
|
||
}), finalize$1(() => {
|
||
this.requestMap[url] = undefined;
|
||
this.loaderService.removeUnresolvedUriLoading(url);
|
||
}));
|
||
this.logger.info(loggerName$7, `loading originalUri=${redactUrl(url)}`);
|
||
if (context.onProgress) {
|
||
context.onProgress.cb(url, 0, stats, undefined);
|
||
}
|
||
const sub = source.subscribe(observer);
|
||
this.loaderService.createUnresolvedUriLoading(url, context.responseType, navigator.userAgent);
|
||
return () => {
|
||
sub.unsubscribe();
|
||
this.requestMap[url] = undefined;
|
||
};
|
||
});
|
||
}
|
||
setCustomUrlResponse(url, response) {
|
||
const source = this.requestMap[url];
|
||
if (source) {
|
||
source.next(response);
|
||
source.complete();
|
||
this.requestMap[url] = undefined;
|
||
}
|
||
}
|
||
handleExternalResponse(data, context, loadConfig, stats) {
|
||
stats.tload = performance.now();
|
||
const status = data.response.status || 200;
|
||
// http status of 200 to 299 are all successfull
|
||
if (status >= 200 && status < 300) {
|
||
if (context.responseType === 'arraybuffer' && data.response.data instanceof ArrayBuffer) {
|
||
stats.loaded = data.response.data.byteLength;
|
||
}
|
||
else {
|
||
stats.loaded = data.response.data.toString().length;
|
||
}
|
||
stats.total = stats.loaded;
|
||
stats.complete = true;
|
||
return scheduled(of({ status, data, stats }), queueScheduler);
|
||
}
|
||
else if (isRedirectStatusCode(status)) {
|
||
this.logger.info(loggerName$7, `redirect status=${status} url=${redactUrl(data.response.uri)}`);
|
||
return this.redirectRequest(data.response.uri, context, loadConfig, stats);
|
||
}
|
||
else {
|
||
this.logger.warn(loggerName$7, `unable to load custom url > uri=${redactUrl(data.response.uri)}, status=${status}`);
|
||
return throwError(new GenericNetworkError('Unable to load custom url', status));
|
||
}
|
||
}
|
||
redirectRequest(uri, context, loadConfig, stats) {
|
||
const { maxLoadTimeMs, maxTimeToFirstByteMs } = loadConfig;
|
||
const updatedMaxLoadTimeMs = maxLoadTimeMs - (performance.now() - stats.trequest); // Recalculate timeout based on request time
|
||
const updatedMaxTimeToFirstByteMs = context.extendMaxTTFB > 0 ? context.extendMaxTTFB : maxTimeToFirstByteMs - (performance.now() - stats.trequest); // Recalculate timeout based on request time
|
||
const newConfig = Object.assign(Object.assign({}, loadConfig), { maxLoadTimeMs: updatedMaxLoadTimeMs, maxTimeToFirstByteMs: updatedMaxTimeToFirstByteMs });
|
||
const newContext = Object.assign(Object.assign({}, context), { url: uri });
|
||
if (updatedMaxLoadTimeMs <= 0 || updatedMaxTimeToFirstByteMs <= 0) {
|
||
return throwError(new TimeoutError());
|
||
}
|
||
return fromXMLHttpRequest(newContext, newConfig).pipe(map(([xhr, sample]) => {
|
||
const { responseURL, status } = xhr;
|
||
const uri = responseURL || '';
|
||
const data = {
|
||
uri,
|
||
response: {
|
||
status: status,
|
||
uri,
|
||
data: xhr.response,
|
||
},
|
||
};
|
||
stats.loaded = stats.total = sample.loaded;
|
||
stats.tload = performance.now();
|
||
stats.complete = true;
|
||
return { status: xhr.status, data, stats };
|
||
}));
|
||
}
|
||
}
|
||
let loader;
|
||
function getCustomUrlLoader(loaderService) {
|
||
if (!loader) {
|
||
const service = loaderService || getLoaderService();
|
||
loader = new CustomUrlLoader(service);
|
||
}
|
||
return loader;
|
||
}
|
||
|
||
function fromUrlArrayBuffer(context, loadConfig) {
|
||
const ctx = Object.assign(Object.assign({}, context), { method: 'GET', responseType: 'arraybuffer' });
|
||
const customUrlLoader = getCustomUrlLoader();
|
||
return isCustomUrl(ctx.url)
|
||
? customUrlLoader.load(ctx, loadConfig).pipe(map((res) => [res.data.response.data, res.stats, ctx.serverInstanceInfo]))
|
||
: fromXMLHttpRequest(ctx, loadConfig).pipe(map(([xhr, bwSample]) => [xhr.response, bwSample, ctx.serverInstanceInfo]));
|
||
}
|
||
|
||
/**
|
||
* Generic load utils for retry and such
|
||
*/
|
||
function getLoadConfig(loadable, policy) {
|
||
return !loadable.url || isCustomUrl(loadable.url) ? policy.customURL : policy.default;
|
||
}
|
||
function getRetryConfig(error, config) {
|
||
if (error instanceof HlsNetworkError) {
|
||
return error.isTimeout ? config.timeoutRetry : config.errorRetry;
|
||
}
|
||
return null;
|
||
}
|
||
/**
|
||
* Operator for converting TimeoutError & GenericNetworkError into ManifestNetworkError
|
||
*/
|
||
function convertToManifestNetworkError(fatal) {
|
||
return (source) => source.pipe(catchError((err) => {
|
||
if (err instanceof GenericTimeoutError) {
|
||
throw new ManifestNetworkError(fatal, 'Timeout', 0, ErrorResponses.ManifestTimeoutError, true);
|
||
}
|
||
else if (err instanceof GenericNetworkError) {
|
||
throw new ManifestNetworkError(fatal, err.message, err.code, { code: err.code, text: 'Manifest network error' }, false);
|
||
}
|
||
throw err;
|
||
}));
|
||
}
|
||
/**
|
||
* Operator for converting TimeoutError & GenericNetworkError into PlaylistNetworkError
|
||
*/
|
||
function convertToPlaylistNetworkError(mediaOptionType, mediaOptionId, fatal, url) {
|
||
return (source) => source.pipe(catchError((err) => {
|
||
if (err instanceof GenericTimeoutError) {
|
||
throw new PlaylistNetworkError(fatal, 'Timeout', 0, ErrorResponses.PlaylistTimeoutError, true, mediaOptionType, mediaOptionId, url);
|
||
}
|
||
else if (err instanceof GenericNetworkError) {
|
||
throw new PlaylistNetworkError(fatal, err.message, err.code, { code: err.code, text: 'Playlist Network Error' }, false, mediaOptionType, mediaOptionId, url);
|
||
}
|
||
throw err;
|
||
}));
|
||
}
|
||
/**
|
||
* Operator for converting TimeoutError & GenericNetworkError into FragmentError
|
||
*/
|
||
function convertToFragmentNetworkError(frag, fatal) {
|
||
return (source) => source.pipe(catchError((err) => {
|
||
if (err instanceof GenericTimeoutError) {
|
||
throw new FragmentNetworkError(fatal, 'Timeout', 0, ErrorResponses.FragmentTimeoutError, true, frag, err.stats);
|
||
}
|
||
else if (err instanceof GenericNetworkError) {
|
||
throw new FragmentNetworkError(fatal, err.message, err.code, { code: err.code, text: 'Fragment Network Error' }, false, frag);
|
||
}
|
||
throw err;
|
||
}));
|
||
}
|
||
/**
|
||
* Helper function to update playtype of levels
|
||
*/
|
||
function updatePlaylistAttributes(levelDetails) {
|
||
const playlistTypeString = levelDetails.type;
|
||
const isLive = levelDetails.liveOrEvent;
|
||
let playlistType = 'VOD';
|
||
if (playlistTypeString === 'EVENT' && isLive) {
|
||
playlistType = 'EVENT';
|
||
}
|
||
else if ((!playlistTypeString || playlistTypeString.length === 0 || playlistTypeString === 'LIVE') && isLive) {
|
||
playlistType = 'LIVE';
|
||
}
|
||
if (levelDetails.type !== playlistType) {
|
||
getLogger().info(`Playlist type updated from ${levelDetails.type} to ${playlistType}`);
|
||
levelDetails.type = playlistType;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 2018 Apple Inc. All rights reserved.
|
||
*/
|
||
const PlayReadySecurityLevels = {
|
||
SL2000: 0,
|
||
SL3000: 1,
|
||
};
|
||
const PlayReadyKeySystemProperties = {
|
||
id: 'playready',
|
||
systemStringPrefix: 'com.microsoft.playready',
|
||
keyFormatString: 'com.microsoft.playready',
|
||
securityLevels: PlayReadySecurityLevels,
|
||
};
|
||
|
||
/**
|
||
* 2018 Apple Inc. All rights reserved.
|
||
*/
|
||
const WidevineSecurityLevels = {
|
||
WIDEVINE_SOFTWARE: 0,
|
||
WIDEVINE_HARDWARE: 1,
|
||
};
|
||
const WidevineKeySystemProperties = {
|
||
id: 'widevine',
|
||
systemStringPrefix: 'com.widevine.alpha',
|
||
keyFormatString: 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
|
||
securityLevels: WidevineSecurityLevels,
|
||
};
|
||
|
||
/*
|
||
* Utilities for numeric encodings
|
||
*
|
||
* 2018 Apple Inc. All rights reserved.
|
||
*/
|
||
function base64ToBase64Url(base64encodedStr) {
|
||
return base64encodedStr.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '');
|
||
}
|
||
class BrowserNumericEncodingUtils {
|
||
static strToBase64Encode(str) {
|
||
return btoa(str);
|
||
}
|
||
static base64DecodeToStr(str) {
|
||
return atob(str);
|
||
}
|
||
static base64Encode(input) {
|
||
return btoa(String.fromCharCode(...input));
|
||
}
|
||
static base64UrlEncode(input) {
|
||
return base64ToBase64Url(BrowserNumericEncodingUtils.base64Encode(input));
|
||
}
|
||
static base64Decode(base64encodedStr) {
|
||
return Uint8Array.from(atob(base64encodedStr), (c) => c.charCodeAt(0));
|
||
}
|
||
}
|
||
class NodeJSNumericEncodingUtils {
|
||
static strToBase64Encode(str) {
|
||
return global$1.Buffer.from(str).toString('base64');
|
||
}
|
||
static base64DecodeToStr(str) {
|
||
return global$1.Buffer.from(str, 'base64').toString();
|
||
}
|
||
static base64Encode(input) {
|
||
return global$1.Buffer.from(input).toString('base64');
|
||
}
|
||
static base64UrlEncode(input) {
|
||
return base64ToBase64Url(NodeJSNumericEncodingUtils.base64Encode(input));
|
||
}
|
||
static base64Decode(base64encodedStr) {
|
||
const buffer = global$1.Buffer.from(base64encodedStr, 'base64');
|
||
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
||
}
|
||
}
|
||
const NumericEncodingUtils = typeof global$1.Buffer !== 'undefined' ? NodeJSNumericEncodingUtils : BrowserNumericEncodingUtils;
|
||
var NumericEncodingUtils$1 = NumericEncodingUtils;
|
||
|
||
/*
|
||
* Utilities for key systems
|
||
*
|
||
* 2018 Apple Inc. All rights reserved.
|
||
*/
|
||
// Map of prefix to mimeType
|
||
const kVideoCodecToMimeType = {
|
||
// AVC
|
||
avc1: 'video/mp4',
|
||
avc3: 'video/mp4',
|
||
dvav: 'video/mp4',
|
||
dva1: 'video/mp4',
|
||
// HEVC
|
||
hev1: 'video/mp4',
|
||
hvc1: 'video/mp4',
|
||
dvh1: 'video/mp4',
|
||
dvhe: 'video/mp4', // Dolby vission
|
||
};
|
||
const kAudioCodecToMimeType = {
|
||
mp4a: 'audio/mp4',
|
||
'ac-3': 'audio/mp4',
|
||
'ec-3': 'audio/mp4',
|
||
};
|
||
function isMediaKeySystemConfig(config) {
|
||
return config && typeof config === 'object'; // requestMediaKeySystemAccess accepts {}
|
||
}
|
||
// Input: set of codec strings for each type
|
||
// Output:
|
||
// {
|
||
// videoCapabilities: [
|
||
// { contentType: 'video/mp4;codecs=avc.###', robustness: '' } ],
|
||
// audioCapabilities: []
|
||
// }
|
||
function getMediaKeysystemMediaCapability(videoCodecs, audioCodecs) {
|
||
const result = {
|
||
videoCapabilities: [],
|
||
audioCapabilities: [],
|
||
};
|
||
if (videoCodecs) {
|
||
videoCodecs.forEach((videoCodec) => {
|
||
const prefix = videoCodec.split('.')[0].trim();
|
||
if (prefix in kVideoCodecToMimeType) {
|
||
result.videoCapabilities.push({
|
||
contentType: kVideoCodecToMimeType[prefix] + ';codecs=' + videoCodec,
|
||
robustness: '',
|
||
});
|
||
}
|
||
});
|
||
}
|
||
if (audioCodecs) {
|
||
audioCodecs.forEach((audioCodec) => {
|
||
const prefix = audioCodec.split('.')[0].trim();
|
||
if (prefix in kAudioCodecToMimeType) {
|
||
result.audioCapabilities.push({
|
||
contentType: kAudioCodecToMimeType[prefix] + ';codecs=' + audioCodec,
|
||
robustness: '',
|
||
});
|
||
}
|
||
});
|
||
}
|
||
return result;
|
||
}
|
||
/**
|
||
* Utility to change the endianness of Key ID
|
||
* @param keyID uint8array representing the keyId
|
||
* @returns uint8array the keyId after changing the endianness
|
||
*/
|
||
function changeEndianness(keyId) {
|
||
// eslint-disable-next-line func-style
|
||
const swap = function (array, from, to) {
|
||
const cur = array[from];
|
||
array[from] = array[to];
|
||
array[to] = cur;
|
||
};
|
||
swap(keyId, 0, 3);
|
||
swap(keyId, 1, 2);
|
||
swap(keyId, 4, 5);
|
||
swap(keyId, 6, 7);
|
||
}
|
||
/**
|
||
* Generate key id from an arbitrary string
|
||
* @param str String to convert into a 16 byte key
|
||
* @returns uint8array representing the keyId
|
||
*/
|
||
function getKeyIdBytes(str) {
|
||
const keyIdbytes = BufferUtils.strToUtf8array(str).subarray(0, 16);
|
||
const paddedkeyIdbytes = new Uint8Array(16);
|
||
paddedkeyIdbytes.set(keyIdbytes, 16 - keyIdbytes.length);
|
||
return paddedkeyIdbytes;
|
||
}
|
||
/**
|
||
* @param URI string
|
||
* @returns Uint8Array of URI data
|
||
*/
|
||
function convertDataUriToArrayBytes(uri) {
|
||
// data:[<media type][;attribute=value][;base64],<data>
|
||
const colonsplit = uri.split(':');
|
||
let keydata = null;
|
||
if (colonsplit[0] === 'data' && colonsplit.length === 2) {
|
||
const semicolonsplit = colonsplit[1].split(';');
|
||
const commasplit = semicolonsplit[semicolonsplit.length - 1].split(',');
|
||
if (commasplit.length === 2) {
|
||
const isbase64 = commasplit[0] === 'base64';
|
||
const data = commasplit[1];
|
||
if (isbase64) {
|
||
semicolonsplit.splice(-1, 1); // remove from processing
|
||
keydata = NumericEncodingUtils$1.base64Decode(data);
|
||
}
|
||
else {
|
||
keydata = getKeyIdBytes(data);
|
||
}
|
||
}
|
||
}
|
||
return keydata;
|
||
}
|
||
/**
|
||
* Generate 'keyids' init data from an array of key ids
|
||
* @param keyIds Array of key ids represented as uint8array[16] objects
|
||
* @returns The initData string to pass to generateRequest as part of EME
|
||
*/
|
||
function makeKeyIdsInitData(keyIds) {
|
||
const initDataObj = {
|
||
kids: keyIds.map(NumericEncodingUtils$1.base64UrlEncode),
|
||
};
|
||
// Convert string to utf-8 array
|
||
return BufferUtils.strToUtf8array(JSON.stringify(initDataObj));
|
||
}
|
||
// parse pssh list and convert to dictionary:
|
||
function parsePSSHList(psshList) {
|
||
const dv = new DataView(psshList);
|
||
let whichByte = 0;
|
||
const pssh = {}; // PSSH: systemID => data
|
||
while (whichByte < dv.buffer.byteLength) {
|
||
const thisBox = whichByte;
|
||
const boxSize = dv.getUint32(whichByte);
|
||
whichByte += 4;
|
||
const nextBox = thisBox + boxSize;
|
||
if (dv.getUint32(whichByte) !== 1886614376) {
|
||
whichByte = nextBox;
|
||
continue; // next box
|
||
}
|
||
whichByte += 4;
|
||
const version = dv.getUint8(whichByte);
|
||
switch (version) {
|
||
case 0: // NO KEY IDS
|
||
case 1:
|
||
whichByte += 1;
|
||
break;
|
||
default:
|
||
whichByte = nextBox;
|
||
break;
|
||
}
|
||
whichByte += 3; // flags
|
||
// UUID: 4 bytes - 2 bytes - 2 bytes - 2 bytes - 6 bytes
|
||
let systemID = '';
|
||
// 16 bytes
|
||
for (let i = 0; i < 16; ++i, ++whichByte) {
|
||
systemID += dv.getUint8(whichByte).toString(16);
|
||
switch (i) {
|
||
case 4:
|
||
case 6:
|
||
case 8:
|
||
case 10:
|
||
systemID += '-';
|
||
break;
|
||
}
|
||
}
|
||
whichByte += 4; // Data size
|
||
pssh[systemID] = dv.buffer.slice(thisBox, nextBox);
|
||
}
|
||
return pssh;
|
||
}
|
||
const keysystemutil = {
|
||
getCapabilities: getMediaKeysystemMediaCapability,
|
||
changeEndianness,
|
||
getKeyIdBytes,
|
||
convertDataUriToArrayBytes,
|
||
makeKeyIdsInitData,
|
||
parsePSSHList,
|
||
};
|
||
|
||
/*
|
||
* Decrypt data
|
||
*
|
||
* 2018 Apple Inc. All rights reserved.
|
||
*/
|
||
// Note that this will not get copied into Worker
|
||
let keyUriToKeyIdMap = {};
|
||
// Created in playlist-loader, modified in demux / remux
|
||
class DecryptData {
|
||
/**
|
||
* @param method From METHOD attribute, mandatory
|
||
* @param uri Absolute url of the key (calculated from URI)
|
||
* @param iv 128-bit unsigned int representing Initialization Vector, used when KEYFORMAT="identity", optional
|
||
* @param format From KEYFORMAT, optional with default value of "identity"
|
||
* @param formatversions Array From KEYFORMATVERSION, an array of unsigned int, optional
|
||
*/
|
||
constructor(method, uri, iv, format, formatversions) {
|
||
this.method = method;
|
||
this.uri = uri;
|
||
this.iv = iv;
|
||
this.format = format;
|
||
this.formatversions = formatversions;
|
||
this.isEncrypted = this.method && this.method !== 'NONE';
|
||
if (!this.formatversions || this.formatversions.length === 0) {
|
||
this.formatversions = [1];
|
||
}
|
||
// Set later
|
||
this.key = undefined;
|
||
this.keyId = undefined;
|
||
if (!this.isEncrypted) {
|
||
return;
|
||
}
|
||
// Initialize keyId if possible
|
||
const keyBytes = keysystemutil.convertDataUriToArrayBytes(this.uri);
|
||
if (keyBytes) {
|
||
switch (format) {
|
||
case PlayReadyKeySystemProperties.keyFormatString: {
|
||
// Playready
|
||
this.pssh = keyBytes;
|
||
const keyBytesUtf16 = new Uint16Array(keyBytes.buffer, keyBytes.byteOffset, keyBytes.byteLength / 2);
|
||
const keyByteStr = String.fromCharCode.apply(null, Array.from(keyBytesUtf16));
|
||
// we got entire PSSH data. Incase of Playready it's WRMHEADER XML object. Parse it.
|
||
const xmlKeyBytes = keyByteStr.substring(keyByteStr.indexOf('<'), keyByteStr.length);
|
||
const parser = new DOMParser();
|
||
const xmlDoc = parser.parseFromString(xmlKeyBytes, 'text/xml');
|
||
const keyData = xmlDoc.getElementsByTagName('KID')[0];
|
||
if (keyData) {
|
||
let keyId = null;
|
||
if (keyData.childNodes[0]) {
|
||
keyId = keyData.childNodes[0].nodeValue;
|
||
}
|
||
else {
|
||
keyId = keyData.getAttribute('VALUE');
|
||
}
|
||
if (keyId) {
|
||
const keyIdArray = NumericEncodingUtils$1.base64Decode(keyId).subarray(0, 16);
|
||
// KID value in PRO is a base64-encoded little endian GUID interpretation of UUID
|
||
// KID value in ‘tenc’ is a big endian UUID GUID interpretation of UUID
|
||
keysystemutil.changeEndianness(keyIdArray);
|
||
this.keyId = keyIdArray;
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
case WidevineKeySystemProperties.keyFormatString: {
|
||
// Widevine
|
||
this.pssh = keyBytes;
|
||
// In case of widevine keyID is embedded in PSSH box. Read Key ID.
|
||
if (keyBytes.length >= 22) {
|
||
this.keyId = keyBytes.subarray(keyBytes.length - 22, keyBytes.length - 6);
|
||
}
|
||
break;
|
||
}
|
||
default: {
|
||
// default handling
|
||
let keydata = keyBytes.subarray(0, 16);
|
||
if (keydata.length !== 16) {
|
||
const padded = new Uint8Array(16);
|
||
padded.set(keydata, 16 - keydata.length);
|
||
keydata = padded;
|
||
}
|
||
this.keyId = keydata;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
// Default behavior
|
||
if (!this.keyId || this.keyId.byteLength !== 16) {
|
||
let keyId = keyUriToKeyIdMap[this.uri];
|
||
if (!keyId) {
|
||
const val = Object.keys(keyUriToKeyIdMap).length % Number.MAX_SAFE_INTEGER;
|
||
// MAX_SAFE_INTEGER is huge (9007199254740991) so this /should/ be safe...
|
||
keyId = new Uint8Array(16);
|
||
const dv = new DataView(keyId.buffer, 12, 4); // Just set the last 4 bytes
|
||
dv.setUint32(0, val);
|
||
keyUriToKeyIdMap[this.uri] = keyId;
|
||
}
|
||
this.keyId = keyId;
|
||
}
|
||
}
|
||
get keyTagInfo() {
|
||
const { method, isEncrypted, uri, iv, keyId, key, format, formatversions } = this;
|
||
const keyTagInfo = { method, isEncrypted, uri, iv, keyId, key, format, formatversions };
|
||
return keyTagInfo;
|
||
}
|
||
static clearKeyUriToKeyIdMap() {
|
||
keyUriToKeyIdMap = {};
|
||
}
|
||
}
|
||
|
||
var KeyRequestState;
|
||
(function (KeyRequestState) {
|
||
KeyRequestState["NONE"] = "NONE";
|
||
KeyRequestState["GET_REQUEST_INFO"] = "GET_REQUEST_INFO";
|
||
KeyRequestState["GET_CHALLENGE"] = "GET_CHALLENGE";
|
||
KeyRequestState["GET_KEY_RESPONSE"] = "GET_KEY_RESPONSE";
|
||
KeyRequestState["PROCESS_LICENSE"] = "PROCESS_LICENSE";
|
||
})(KeyRequestState || (KeyRequestState = {}));
|
||
// Wrap subscription and teardown of MediaKeySession callbacks
|
||
class MediaKeySessionContext {
|
||
constructor(session, onkeystatuseschange, onkeymessage, logger) {
|
||
this.session = session;
|
||
this.onkeystatuseschange = onkeystatuseschange;
|
||
this.onkeymessage = onkeymessage;
|
||
this.logger = logger;
|
||
this.isClosing$ = new BehaviorSubject(false);
|
||
this.closed$ = new BehaviorSubject(false);
|
||
const target = fromEventTarget(this.session);
|
||
target.listen('keystatuseschange', this.isClosing$.pipe(filter((x) => x === true)), this.onkeystatuseschange);
|
||
// Message is weird because we still want the message until after close()
|
||
target.listen('message', this.closed$.pipe(filter((x) => x === true)), this.onkeymessage);
|
||
}
|
||
get isClosing() {
|
||
return this.isClosing$.value;
|
||
}
|
||
get isClosed() {
|
||
return this.closed$.value;
|
||
}
|
||
/**
|
||
* Remove and close all keys in MediaKeySession
|
||
*/
|
||
destroy() {
|
||
this.logger.info(`[Keys] : remove licenses & keys for session : ${this.session.sessionId} and close it`);
|
||
this.isClosing$.next(true);
|
||
const session = this.session;
|
||
// Unabortable section. Should execute no matter what.
|
||
return from(session
|
||
.remove()
|
||
.catch((error) => {
|
||
this.logger.info(`Could not remove session: ${error.message}`);
|
||
})
|
||
.then(() => {
|
||
return session.close();
|
||
})
|
||
.catch((error) => {
|
||
this.logger.info(`Could not close session: ${error.message}`);
|
||
})).pipe(tap(() => {
|
||
this.isClosing$.next(false);
|
||
this.closed$.next(true);
|
||
}), finalize$1(() => {
|
||
this.isClosing$.next(false);
|
||
this.closed$.next(true);
|
||
}));
|
||
}
|
||
}
|
||
/**
|
||
* @brief KeyContext keeps track of the current key request state and information about a given key.
|
||
*
|
||
* A key may exist without a key request being in progress. When a key request is in progress, there are
|
||
* multiple phases / states. Each state of a key request is asynchronous and can be completed either by
|
||
* the CDM or by the client. If we get out of sync with the current key request state, we will throw an error
|
||
* and all subscribers will be notified. We will also throw an error if aborted in the middle of the request.
|
||
* Note that subscribers will not get the abort error if they unsubscribe from the observable.
|
||
*/
|
||
class KeyContext {
|
||
constructor(decryptdata, session = null) {
|
||
this.decryptdata = decryptdata;
|
||
this._requestState$ = new BehaviorSubject(KeyRequestState.NONE); // Key request state. Note this is different from keystatus from CDM
|
||
this.destroy$ = new Subject(); // Destroy key. This should stop any renewal timers
|
||
// If in progress, the current observable
|
||
this.currentObservable = null;
|
||
this.session = null; // Current session. Could be shared by others (config.useMultipleKeySessions !== true)
|
||
this.oldSessions = [];
|
||
this.session = session;
|
||
}
|
||
get requestState() {
|
||
return this._requestState$.value;
|
||
}
|
||
get onKeyRequestState$() {
|
||
return this._requestState$;
|
||
}
|
||
destroy() {
|
||
this.destroy$.next();
|
||
// TODO actual cleanup?
|
||
}
|
||
abort() {
|
||
if (this.requestState !== KeyRequestState.NONE) {
|
||
const err = new KeyRequestError('Aborted', this.decryptdata.uri, 0, ErrorResponses.KeySystemAbort, true, KeyRequestErrorReason.Abort);
|
||
this.error(err);
|
||
}
|
||
}
|
||
/**
|
||
* Change state and return observable that gets completed when resolveState is called
|
||
*
|
||
* @param state The state to switch to
|
||
*/
|
||
setKeyRequestState(state) {
|
||
if (this.currentObservable) {
|
||
const err = new KeyRequestError(`Unexpected state transition ${this.requestState}->${state}`, this.decryptdata.uri, 0, ErrorResponses.KeySystemUnexpectedStateTransition, true, KeyRequestErrorReason.InvalidState);
|
||
this.error(err);
|
||
}
|
||
this._requestState$.next(state);
|
||
const returnVal = new AsyncSubject();
|
||
if (state === KeyRequestState.NONE) {
|
||
returnVal.complete();
|
||
this.currentObservable = null;
|
||
}
|
||
else {
|
||
//@ts-ignore
|
||
this.currentObservable = returnVal;
|
||
}
|
||
return returnVal;
|
||
}
|
||
/**
|
||
* Resolve current state observable
|
||
* @param state State to resolve
|
||
* @param value The value to resolve current observable with
|
||
*/
|
||
resolveState(state, value) {
|
||
if (!this.currentObservable) {
|
||
return;
|
||
}
|
||
if (state !== this.requestState) {
|
||
const err = new KeyRequestError(`Unexpected state ${this.requestState} != ${state}`, this.decryptdata.uri, 0, ErrorResponses.KeySystemUnexpectedState, true, KeyRequestErrorReason.InvalidState);
|
||
this.error(err);
|
||
return;
|
||
}
|
||
if (value instanceof Error) {
|
||
this.error(value);
|
||
}
|
||
else {
|
||
const obs = this.currentObservable;
|
||
this.currentObservable = null;
|
||
obs.next(value);
|
||
obs.complete();
|
||
}
|
||
}
|
||
/**
|
||
* Signal error for this key. Could be due to key request or some other internal error from CDM
|
||
* @param err The error
|
||
*/
|
||
error(err) {
|
||
if (this.currentObservable) {
|
||
const obs = this.currentObservable;
|
||
this.currentObservable = null;
|
||
obs.error(err);
|
||
}
|
||
this.setKeyRequestState(KeyRequestState.NONE);
|
||
// TODO: any other cleanup ?
|
||
}
|
||
}
|
||
|
||
/*
|
||
* Base class for key systems
|
||
*
|
||
* 2018 Apple Inc. All rights reserved.
|
||
*/
|
||
const MAX_CERT_LOAD_TIME_MS = 10000; // Maximum time for loading the certificate
|
||
function printKeyTag(tag) {
|
||
return `uri=${redactUrl(tag.uri)} keyId=${Hex$1.hexDump(tag.keyId)}`;
|
||
}
|
||
class KeySystem {
|
||
constructor(mediaKeys, systemString, config, eventEmitter, useSingleKeySession, sessionHandler, logger) {
|
||
this.mediaKeys = mediaKeys;
|
||
this.systemString = systemString;
|
||
this.config = config;
|
||
this.eventEmitter = eventEmitter;
|
||
this.useSingleKeySession = useSingleKeySession;
|
||
this.sessionHandler = sessionHandler;
|
||
this.logger = logger;
|
||
this.destroy$ = new Subject();
|
||
this.setCert = false; // Has set cert on MediaKeySession
|
||
this.certificate$ = new BehaviorSubject(null);
|
||
// This event will be signalled when a renewal request is needed or if an error occurs.
|
||
// The key system will not auto renew. Instead client is expected to call startKeyRequest
|
||
this._keyStatusChange$ = new Subject();
|
||
this.shouldDestroyMediaKeys = false;
|
||
// TJH: will playingItem raise a license challenge ?
|
||
//this.itemId = hls.loadingItem?.itemId || '';
|
||
this.itemId = '';
|
||
this.sessions = [];
|
||
this.keyIdToKeyInfo = {};
|
||
this.keyUriToKeyInfo = {};
|
||
this.sessionIdToKeyUri = {};
|
||
this.onkeystatuseschange = this.handleKeyStatusesChange.bind(this);
|
||
this.onkeymessage = this.handleKeyMessage.bind(this);
|
||
}
|
||
get keyStatusChange$() {
|
||
return this._keyStatusChange$;
|
||
}
|
||
destroy() {
|
||
this.isDestroying = true;
|
||
this.destroy$.next();
|
||
for (const info of Object.values(this.keyIdToKeyInfo)) {
|
||
this._abortKeyRequest(info);
|
||
}
|
||
// Remove sessions
|
||
const destroyPromises = this.sessions.map((ctx) => ctx.destroy());
|
||
const p = iif(() => destroyPromises.length === 0, VOID, forkJoin(destroyPromises)).pipe(mapTo(undefined), finalize$1(() => {
|
||
this.mediaKeys = undefined;
|
||
this.keyIdToKeyInfo = {};
|
||
this.keyUriToKeyInfo = {};
|
||
this.sessionIdToKeyUri = {};
|
||
}));
|
||
DecryptData.clearKeyUriToKeyIdMap();
|
||
return p;
|
||
}
|
||
/**
|
||
* @param certificate Used for generating challenge
|
||
* @returns {Promise} Fired when non-null certificate has been set on the object.
|
||
*/
|
||
setServerCertificate(certificate = null) {
|
||
this.logger.info(this.systemString + ' setServerCertificate(' + (certificate ? 'nonnull' : 'null') + ')');
|
||
if (!this.needsCert) {
|
||
return of(true);
|
||
}
|
||
if (certificate) {
|
||
this.certificate$.next(certificate);
|
||
}
|
||
const waitForCert$ = waitFor(this.certificate$, (x) => x != null).pipe(timeout(MAX_CERT_LOAD_TIME_MS));
|
||
return waitForCert$.pipe(switchMap((cert) => {
|
||
if (this.setCert) {
|
||
return of(true);
|
||
}
|
||
if (!this.setCertSubject || this.setCertSubject.isStopped) {
|
||
this.setCertSubject = new AsyncSubject();
|
||
return from(this.mediaKeys.setServerCertificate(cert)).pipe(tap((v) => {
|
||
// Sometimes the promise returned by the CDM doesn't carry boolean
|
||
// This will cause playback failure.
|
||
const certStatus = v !== undefined ? v : true;
|
||
this.setCert = certStatus;
|
||
this.setCertSubject.next(certStatus);
|
||
this.setCertSubject.complete();
|
||
}), catchError((err) => {
|
||
this.logger.info('Failed setServerCertificate');
|
||
this.setCert = false;
|
||
this.setCertSubject.error(err);
|
||
return VOID;
|
||
}), switchMap(() => {
|
||
return this.setCertSubject;
|
||
}));
|
||
}
|
||
return this.setCertSubject;
|
||
}), catchError((err) => {
|
||
this.logger.info(`setServerCertificate err=${err.message}`);
|
||
throw err;
|
||
}));
|
||
}
|
||
ensureKeyContext(decryptdata) {
|
||
const keyuri = decryptdata.uri;
|
||
if (!this.keyUriToKeyInfo[keyuri]) {
|
||
this.keyUriToKeyInfo[keyuri] = new KeyContext(decryptdata);
|
||
}
|
||
const keyInfo = this.keyUriToKeyInfo[keyuri];
|
||
if (keyInfo.session) {
|
||
return keyInfo;
|
||
}
|
||
if (this.useSingleKeySession && this.sessions.length > 0) {
|
||
keyInfo.session = this.sessions[0].session;
|
||
return keyInfo;
|
||
}
|
||
this.logger.info(`${this.systemString} createSession`);
|
||
const session = this.mediaKeys.createSession();
|
||
if (session) {
|
||
this.sessions.push(new MediaKeySessionContext(session, this.onkeystatuseschange, this.onkeymessage, this.logger));
|
||
}
|
||
else {
|
||
this.logger.info(`${this.systemString} FAIL: could not create MediaKeysSession`);
|
||
}
|
||
keyInfo.session = session;
|
||
return keyInfo;
|
||
}
|
||
/**
|
||
* Start a key request. called from key-system controller or internally on renewal.
|
||
*
|
||
* @param decryptdata DecryptData object from EXT-X-KEY
|
||
* @returns Promise (decryptdata)
|
||
*/
|
||
startKeyRequest(decryptdata) {
|
||
const keyuri = decryptdata.uri;
|
||
const keyInfo = this.ensureKeyContext(decryptdata);
|
||
const session = keyInfo === null || keyInfo === void 0 ? void 0 : keyInfo.session;
|
||
if (!session) {
|
||
return throwError(new KeySystemError('Could not create key session', decryptdata.uri, 0, ErrorResponses.KeySystemFailedToCreateSession, this.systemString));
|
||
}
|
||
const keyIdStr = BufferUtils.utf8arrayToStr(decryptdata.keyId);
|
||
this.keyIdToKeyInfo[keyIdStr] = keyInfo;
|
||
this.logger.info(`${this.systemString} startKeyRequest ${printKeyTag(decryptdata)} state=${keyInfo.requestState}`);
|
||
this.logger.qe({ critical: true, name: 'startKeyRequest', data: { uri: keyuri, keyId: keyIdStr } });
|
||
return forkJoin([this.getKeyRequestInfo(keyInfo), this.setServerCertificate()]).pipe(map((values) => values[0]), switchMap((requestInfo) => {
|
||
var _a;
|
||
(_a = this.sessionHandler) === null || _a === void 0 ? void 0 : _a.licenseChallengeSubmitted({ keyuri: keyuri, keyFormat: this.systemString });
|
||
return this.generateLicenseChallenge(keyInfo, requestInfo).pipe(catchError((error) => {
|
||
var _a;
|
||
this.logger.info(`${this.systemString} generateLicenseChallenge Failed ${error.message} ${printKeyTag(decryptdata)}`);
|
||
(_a = this.sessionHandler) === null || _a === void 0 ? void 0 : _a.licenseChallengeError({ keyuri: keyuri });
|
||
throw error;
|
||
}));
|
||
}), switchMap((licenseChallenge) => {
|
||
var _a;
|
||
this.logger.info(`${this.systemString} challenge created ${printKeyTag(decryptdata)}`);
|
||
this.logger.qe({ critical: true, name: 'challengeCreated', data: { uri: keyuri, keyId: keyIdStr } });
|
||
(_a = this.sessionHandler) === null || _a === void 0 ? void 0 : _a.licenseChallengeCreated({ keyuri: keyuri, cdmVersion: this.cdmVersion });
|
||
return this.getKeyRequestResponse(keyInfo, licenseChallenge);
|
||
}), switchMap((parsedKeyResponse) => {
|
||
var _a;
|
||
this.logger.info(`${this.systemString} onKeyResponseParsed ${printKeyTag(decryptdata)}`);
|
||
this.logger.qe({ critical: true, name: 'onKeyResponseParsed', data: { uri: keyuri, keyId: keyIdStr } });
|
||
(_a = this.sessionHandler) === null || _a === void 0 ? void 0 : _a.licenseResponseSubmitted({ keyuri: keyuri });
|
||
return this.handleParsedKeyResponse(keyInfo, parsedKeyResponse).pipe(catchError((error) => {
|
||
var _a;
|
||
this.logger.info(`${this.systemString} ParseKeyResponse Failed ${printKeyTag(decryptdata)} error=${error.message}`);
|
||
(_a = this.sessionHandler) === null || _a === void 0 ? void 0 : _a.licenseResponseError({ keyuri: keyuri });
|
||
throw error;
|
||
}));
|
||
}), map(() => {
|
||
var _a;
|
||
this.logger.info(`${this.systemString} New usable key: ${printKeyTag(decryptdata)} CDMVersion=${this.cdmVersion}`);
|
||
this.logger.qe({ critical: true, name: 'newUsableKey', data: { uri: keyuri, keyId: keyIdStr } });
|
||
(_a = this.sessionHandler) === null || _a === void 0 ? void 0 : _a.licenseResponseProcessed({ keyuri: keyuri });
|
||
keyInfo.setKeyRequestState(KeyRequestState.NONE);
|
||
this.removeSessions(keyInfo.decryptdata, false).subscribe();
|
||
return decryptdata; // Resolve
|
||
}), catchError((err) => {
|
||
this.handleKeyExchangeError(keyInfo, err);
|
||
throw err;
|
||
}),
|
||
// takeUntil(this.destroy$),
|
||
finalize$1(() => {
|
||
this._abortKeyRequest(keyInfo);
|
||
}));
|
||
}
|
||
/**
|
||
* Internal cleanup of key request
|
||
* @param keyInfo The key request to abort
|
||
*/
|
||
_abortKeyRequest(keyInfo) {
|
||
var _a;
|
||
if (keyInfo) {
|
||
const keyuri = keyInfo.decryptdata.uri;
|
||
const keyIdStr = BufferUtils.utf8arrayToStr(keyInfo.decryptdata.keyId);
|
||
if (keyInfo.requestState !== KeyRequestState.NONE) {
|
||
this.logger.info(`Aborting key ${redactUrl(keyuri)} state=${keyInfo.requestState}`);
|
||
this.logger.qe({ critical: true, name: 'keyAborted', data: { uri: keyuri, keyId: keyIdStr } });
|
||
(_a = this.sessionHandler) === null || _a === void 0 ? void 0 : _a.keyAborted({ keyuri });
|
||
}
|
||
keyInfo.abort();
|
||
}
|
||
}
|
||
// Do cleanup if needed
|
||
handleKeyExchangeError(keyInfo, err) {
|
||
keyInfo.error(err); // Mark as error so we don't erroneously report abort
|
||
const keyuri = keyInfo.decryptdata.uri;
|
||
const keyIdStr = BufferUtils.utf8arrayToStr(keyInfo.decryptdata.keyId);
|
||
this.logger.qe({ critical: true, name: 'keyExchangeError', data: { uri: keyuri, keyId: keyIdStr, errMsg: err.message } });
|
||
}
|
||
updateItemId(itemId) {
|
||
// Update the itemId to which the key belongs to. The license release message carries this info.
|
||
// When you generate challenge this itemId was being used.
|
||
this.itemId = itemId;
|
||
}
|
||
/**
|
||
* Remove a key from the keysystem
|
||
* @param decryptdata
|
||
*/
|
||
removeKey(decryptdata) {
|
||
return this.removeKeyInternal(decryptdata);
|
||
}
|
||
removeKeyInternal(decryptdata) {
|
||
this.logger.info(`removing key ${redactUrl(decryptdata.uri)}`);
|
||
const keyInfo = this.keyUriToKeyInfo[decryptdata.uri];
|
||
if (keyInfo && keyInfo.session) {
|
||
const session = keyInfo.session;
|
||
keyInfo.abort();
|
||
keyInfo.destroy();
|
||
const keyIdStr = BufferUtils.utf8arrayToStr(decryptdata.keyId);
|
||
this.keyIdToKeyInfo[keyIdStr] = undefined;
|
||
this.keyUriToKeyInfo[decryptdata.uri] = undefined;
|
||
return this.removeSession(session);
|
||
}
|
||
}
|
||
removeSessions(decryptdata, shouldRemoveCurrentSession) {
|
||
const keyInfo = this.keyUriToKeyInfo[decryptdata.uri];
|
||
if (keyInfo) {
|
||
// Remove all previous sessions
|
||
const oldRemoves = keyInfo.oldSessions.map((session) => {
|
||
return this.removeSession(session);
|
||
});
|
||
keyInfo.oldSessions = [];
|
||
return iif(() => oldRemoves.length === 0, VOID, forkJoin(oldRemoves)).pipe(switchMap(() => {
|
||
if (shouldRemoveCurrentSession) {
|
||
// Finally remove the current session
|
||
return this.removeKeyInternal(decryptdata);
|
||
}
|
||
return VOID;
|
||
}));
|
||
}
|
||
return VOID;
|
||
}
|
||
removeSession(session) {
|
||
this.logger.info(`remove session ${session.sessionId}`);
|
||
const index = this.sessions.findIndex((sessionCtx) => {
|
||
return sessionCtx.session === session;
|
||
});
|
||
const sessionCtx = this.sessions[index];
|
||
if (index > -1) {
|
||
this.sessions.splice(index, 1);
|
||
}
|
||
this.sessionIdToKeyUri[session.sessionId] = undefined;
|
||
if (sessionCtx) {
|
||
return sessionCtx.destroy();
|
||
}
|
||
return VOID;
|
||
}
|
||
getKeyRequestInfo(keyInfo) {
|
||
const decryptdata = keyInfo.decryptdata;
|
||
const keyuri = decryptdata.uri;
|
||
const obs = keyInfo.setKeyRequestState(KeyRequestState.GET_REQUEST_INFO);
|
||
this.eventEmitter.trigger(HlsEvent$1.KEY_REQUEST_STARTED, { keyuri: keyuri, decryptdata: decryptdata, timestamp: Date.now() });
|
||
return obs;
|
||
}
|
||
setKeyRequestInfo(keyuri, requestInfo) {
|
||
const keyInfo = this.keyUriToKeyInfo[keyuri];
|
||
if (!keyInfo) {
|
||
this.logger.info(`Unexpected key requested ${redactUrl(keyuri)}`);
|
||
return;
|
||
}
|
||
keyInfo.resolveState(KeyRequestState.GET_REQUEST_INFO, requestInfo);
|
||
}
|
||
sanitizeRequest(requestInfo) {
|
||
return requestInfo;
|
||
}
|
||
/**
|
||
* Generate a license challenge for the key
|
||
* @param keyInfo The key we're requesting
|
||
* @param requestInfo Additional parameters for making the key request
|
||
* @param state MediaKeyStatus for the keyId associated with keyInfo
|
||
*/
|
||
generateLicenseChallengeInternal(keyInfo, requestInfo, state) {
|
||
if (state === 'status-pending') {
|
||
this.logger.info('Request already in progress');
|
||
}
|
||
const decryptdata = keyInfo.decryptdata;
|
||
const session = keyInfo.session;
|
||
const keyuri = decryptdata.uri;
|
||
const keyId = decryptdata.keyId;
|
||
let generateRequest$;
|
||
const getKeyResponse$ = keyInfo.setKeyRequestState(KeyRequestState.GET_CHALLENGE);
|
||
if (!session.generateRequestPromise) {
|
||
const initDataResult = this.generateInitData(keyId, decryptdata, requestInfo);
|
||
this.logger.info(`challenge create start uri=${redactUrl(keyInfo.decryptdata.uri)} versions=${JSON.stringify(keyInfo.decryptdata.formatversions)}`);
|
||
session.generateRequestPromise = session.generateRequest(initDataResult.initDataType, initDataResult.initData);
|
||
// May only be called once
|
||
generateRequest$ = scheduled(session.generateRequestPromise, queueScheduler).pipe(tap(() => {
|
||
this.sessionIdToKeyUri[session.sessionId] = keyuri;
|
||
}), catchError((reason) => {
|
||
this.logger.info(`${this.systemString} FAIL: generateRequest fail keyuri=${redactUrl(keyInfo.decryptdata.uri)} message=${reason.message}`);
|
||
throw new KeySystemError(reason.message, keyInfo.decryptdata.uri, 0, ErrorResponses.KeySystemFailedToGenerateLicenseRequest, this.systemString);
|
||
}));
|
||
}
|
||
else {
|
||
// If generateRequest was already called
|
||
generateRequest$ = scheduled(session.generateRequestPromise, queueScheduler).pipe(switchMap(() => this.generateRequestInitialized(keyInfo, state === 'usable')));
|
||
}
|
||
return forkJoin([getKeyResponse$, generateRequest$]).pipe(map((result) => new Uint8Array(result[0])));
|
||
}
|
||
/**
|
||
* This is different from startKeyRequest in that it could be triggered
|
||
* from an externally generated event. (Hls.generateKeyRequest)
|
||
*
|
||
* @param keyInfo Key info object
|
||
* @param requestInfo Extra information needed to make this request
|
||
*/
|
||
generateLicenseChallenge(keyInfo, requestInfo) {
|
||
const decryptdata = keyInfo.decryptdata;
|
||
const session = keyInfo.session;
|
||
const keyuri = decryptdata.uri;
|
||
const keyId = decryptdata.keyId;
|
||
const keyIdStr = BufferUtils.utf8arrayToStr(decryptdata.keyId);
|
||
this.logger.info(`${this.systemString} generateLicenseChallenge ${printKeyTag(decryptdata)}`);
|
||
this.logger.qe({ critical: true, name: 'generateLicenseChallenge', data: { uri: keyuri, keyId: keyIdStr } });
|
||
if (keyInfo.licenseChallenge) {
|
||
// This is handling for key systems like Widevine where the license challenge for renewal is already generated and we just need to use it.
|
||
keyInfo.resolveState(KeyRequestState.GET_CHALLENGE, keyInfo.licenseChallenge);
|
||
}
|
||
requestInfo = this.sanitizeRequest(requestInfo);
|
||
keyInfo.requestInfo = requestInfo;
|
||
// In gapless mode itemId needs to be updated to the newly loading item
|
||
//this.itemId = this.hls.inGaplessMode ? this.hls.loadingItem.itemId : this.itemId;
|
||
this.sessionId = this.sessionId || (requestInfo && requestInfo.sessionId) || this.itemId; // Use first id
|
||
let state;
|
||
// If it's playready Key system we need to change the endianness of the keyid in the key status
|
||
if (this.systemString === PlayReadyKeySystemProperties.systemStringPrefix) {
|
||
// Use a different copy of KID, otherwise it will change the endianness of KID in DecryptData
|
||
// since it's a referrence
|
||
const curKid = new Uint8Array(keyId);
|
||
keysystemutil.changeEndianness(curKid);
|
||
state = session.keyStatuses.get(curKid);
|
||
}
|
||
else {
|
||
state = session.keyStatuses.get(keyId); // Keysystem state
|
||
}
|
||
let createRequestPromise;
|
||
switch (state) {
|
||
case 'status-pending': // Challenge created or key known
|
||
case 'usable': // Finished key request already, should renew
|
||
case 'expired':
|
||
case undefined: {
|
||
createRequestPromise = this.generateLicenseChallengeInternal(keyInfo, requestInfo, state);
|
||
break;
|
||
}
|
||
default:
|
||
createRequestPromise = throwError(new KeySystemError(`Bad internal state state=${state}`, keyuri, 0, ErrorResponses.KeySystemUnexpectedState, this.systemString));
|
||
break;
|
||
}
|
||
return createRequestPromise;
|
||
}
|
||
/**
|
||
* Set the parsed license response
|
||
* @param keyuri URI of key corresponding to request
|
||
* @param parsedResponse The parsed response for the key request. Specific to key system
|
||
*/
|
||
setParsedResponse(keyuri, parsedResponse) {
|
||
const keyInfo = this.keyUriToKeyInfo[keyuri];
|
||
if (keyInfo) {
|
||
keyInfo.resolveState(KeyRequestState.GET_KEY_RESPONSE, parsedResponse);
|
||
}
|
||
else {
|
||
this.logger.info(`${this.systemString} no keyInfo for keyuri: ${redactUrl(keyuri)}`);
|
||
}
|
||
}
|
||
// keystatuseschange handler
|
||
handleKeyStatusesChange(event) {
|
||
const ks = event.target;
|
||
ks.keyStatuses.forEach((status, keyid, parent) => {
|
||
const currentKeyId = new Uint8Array(keyid);
|
||
// If it's playready Key system we need to change the endianness of the keyid in the key status
|
||
if (this.systemString === PlayReadyKeySystemProperties.systemStringPrefix) {
|
||
keysystemutil.changeEndianness(currentKeyId);
|
||
}
|
||
const keyIdStr = BufferUtils.utf8arrayToStr(currentKeyId);
|
||
const keyInfo = this.keyIdToKeyInfo[keyIdStr];
|
||
if (keyInfo) {
|
||
this.handleKeyStatusForKey(status, keyInfo);
|
||
}
|
||
});
|
||
}
|
||
handleKeyStatusForKey(status, keyInfo) {
|
||
if (!keyInfo) {
|
||
return;
|
||
}
|
||
const decryptdata = keyInfo.decryptdata;
|
||
const keyuri = decryptdata.uri;
|
||
switch (status) {
|
||
case 'internal-error': {
|
||
this.logger.error(`${this.systemString} internal-error for key ${printKeyTag(decryptdata)}`);
|
||
this._signalError(keyInfo, new KeyRequestError('Got internal error from key system', keyuri, 0, ErrorResponses.KeySystemInternalError, false, KeyRequestErrorReason.InternalError));
|
||
break;
|
||
}
|
||
case 'usable': {
|
||
if (keyInfo.requestState === KeyRequestState.PROCESS_LICENSE) {
|
||
keyInfo.resolveState(KeyRequestState.PROCESS_LICENSE, undefined);
|
||
}
|
||
break;
|
||
}
|
||
case 'output-restricted': {
|
||
this.logger.warn(`${this.systemString} output-restricted for key ${printKeyTag(decryptdata)}`);
|
||
if (keyInfo.session) {
|
||
// Cleanup the session, so that we don't keep the keys and try to renew it later
|
||
this.removeSession(keyInfo.session).pipe(takeUntil(this.destroy$)).subscribe();
|
||
}
|
||
this._signalError(keyInfo, new KeyRequestError('output-restricted', keyuri, 0, ErrorResponses.KeySystemOutputRestricted, false, KeyRequestErrorReason.OutputRestricted));
|
||
break;
|
||
}
|
||
case 'expired': {
|
||
this.logger.warn(`${this.systemString} expired for key ${printKeyTag(decryptdata)}. Attempting renewal`);
|
||
this._signalRenewal(keyInfo);
|
||
break;
|
||
}
|
||
default:
|
||
this.logger.info(`key status ${printKeyTag(decryptdata)} ${status}`);
|
||
break;
|
||
}
|
||
}
|
||
_scheduleRenewal(keyInfo, renewalMs) {
|
||
this.logger.info(`${this.systemString} Scheduling renewal for ${printKeyTag(keyInfo.decryptdata)} in ${renewalMs} ms`);
|
||
timer(renewalMs)
|
||
.pipe(tap(() => this._signalRenewal(keyInfo)), takeUntil(race(keyInfo.destroy$, this.destroy$, waitFor(keyInfo.onKeyRequestState$, (state) => state === KeyRequestState.GET_REQUEST_INFO))))
|
||
.subscribe();
|
||
}
|
||
_signalRenewal(keyInfo) {
|
||
this._keyStatusChange$.next({ decryptdata: keyInfo.decryptdata, status: 'needs-renewal' });
|
||
}
|
||
_signalError(keyInfo, error) {
|
||
this._keyStatusChange$.next({ decryptdata: keyInfo.decryptdata, status: 'error', error });
|
||
keyInfo.error(error);
|
||
}
|
||
}
|
||
|
||
const kClearKeySystemString = 'org.w3.clearkey';
|
||
const ClearKeySecurityLevels = {
|
||
NONE: 0,
|
||
};
|
||
const ClearKeySystemProperties = {
|
||
id: 'clearkey',
|
||
systemStringPrefix: kClearKeySystemString,
|
||
keyFormatString: kClearKeySystemString,
|
||
securityLevels: ClearKeySecurityLevels,
|
||
};
|
||
|
||
/**
|
||
* 2018 Apple Inc. All rights reserved.
|
||
*/
|
||
const KeySystemIdValues = ['clearkey', 'fairplaystreaming', 'playready', 'widevine'];
|
||
|
||
/**
|
||
* Clear key system implementation
|
||
*
|
||
* Uses July 2016 spec:
|
||
* https://www.w3.org/TR/2016/CR-encrypted-media-20160705/
|
||
*
|
||
* 2018 Apple Inc. All rights reserved.
|
||
*/
|
||
// default configuration to pass into requestMediaKeySystemAccess
|
||
const kClearKeySystemConfig = {
|
||
initDataTypes: ['keyids', 'cenc'],
|
||
};
|
||
class ClearKeySystem extends KeySystem {
|
||
constructor(mediaKeys, keySystemString, config, eventEmitter, sessionHandler, logger) {
|
||
super(mediaKeys, keySystemString, config, eventEmitter, false, sessionHandler, logger);
|
||
}
|
||
// destroy()
|
||
static get requestAccessConfig() {
|
||
return kClearKeySystemConfig;
|
||
}
|
||
get needsCert() {
|
||
return false;
|
||
}
|
||
getKeyRequestResponse(keyInfo, licenseChallenge) {
|
||
const keyuri = keyInfo.decryptdata.uri;
|
||
const loaderConfig = {
|
||
maxLoadTimeMs: 0,
|
||
maxTimeToFirstByteMs: 0,
|
||
autoRetry: false,
|
||
timeoutRetry: null,
|
||
errorRetry: null,
|
||
};
|
||
return fromUrlArrayBuffer({ url: keyuri, xhrSetup: this.config.xhrSetup }, loaderConfig).pipe(map(([loadArrayBufferResult, _]) => {
|
||
const key = new Uint8Array(loadArrayBufferResult);
|
||
const response = {
|
||
response: this.parseResponse(licenseChallenge, key),
|
||
};
|
||
return response;
|
||
}));
|
||
}
|
||
parseResponse(licenseChallenge, key) {
|
||
const keyObj = {
|
||
kty: 'oct',
|
||
kid: NumericEncodingUtils$1.base64UrlEncode(licenseChallenge),
|
||
k: NumericEncodingUtils$1.base64UrlEncode(key),
|
||
};
|
||
return BufferUtils.strToUtf8array(JSON.stringify({ keys: [keyObj] }));
|
||
}
|
||
handleParsedKeyResponse(keyInfo, keyResponse) {
|
||
const responseBlob = keyResponse.response;
|
||
const processLicense = keyInfo.setKeyRequestState(KeyRequestState.PROCESS_LICENSE);
|
||
const updateSession = from(keyInfo.session.update(responseBlob)).pipe(tap(() => {
|
||
const keyId = keyInfo.decryptdata.keyId;
|
||
const state = keyInfo.session.keyStatuses.get(keyId);
|
||
this.handleKeyStatusForKey(state, keyInfo);
|
||
}), catchError((reason) => {
|
||
const response = { code: reason.code, text: 'Failed to update with key response' };
|
||
const err = new KeySystemError(reason.message, keyInfo.decryptdata.uri, reason.code, response, this.systemString);
|
||
throw err;
|
||
}));
|
||
return forkJoin([processLicense, updateSession]).pipe(mapTo(undefined));
|
||
}
|
||
generateInitData(keyId /* decryptdata, requestInfo*/) {
|
||
return { initData: keysystemutil.makeKeyIdsInitData([keyId]), initDataType: 'keyids' };
|
||
}
|
||
generateRequestInitialized() {
|
||
return VOID;
|
||
}
|
||
sanitizeRequest(requestInfo) {
|
||
return requestInfo;
|
||
}
|
||
// message
|
||
handleKeyMessage(event) {
|
||
if (this.isDestroying) {
|
||
this.logger.info('In the middle of destroying, ignore key message');
|
||
return;
|
||
}
|
||
if (event.messageType !== 'license-request') {
|
||
this.logger.info(`${this.systemString} Unexpected key message type ${event.messageType}`);
|
||
return;
|
||
}
|
||
const message = new Uint8Array(event.message);
|
||
const request = JSON.parse(BufferUtils.utf8arrayToStr(message).trim());
|
||
if (request.kids.length !== 1) {
|
||
this.logger.info(`${this.systemString} Unexpected number of keyIds ${request.kids.length}`);
|
||
return;
|
||
}
|
||
const keyId = NumericEncodingUtils$1.base64Decode(request.kids[0]); // just care about the first for now
|
||
const keyIdStr = BufferUtils.utf8arrayToStr(keyId);
|
||
const keyInfo = this.keyIdToKeyInfo[keyIdStr];
|
||
if (keyInfo) {
|
||
keyInfo.resolveState(KeyRequestState.GET_CHALLENGE, keyId);
|
||
}
|
||
else {
|
||
this.logger.info(`${this.systemString} no keyInfo for keyId ${JSON.stringify(keyId)}`);
|
||
}
|
||
}
|
||
}
|
||
var ClearKeySystem$1 = ClearKeySystem;
|
||
|
||
/*
|
||
* FairPlayStreaming key system constants
|
||
*
|
||
* 2018 Apple Inc. All rights reserved.
|
||
*/
|
||
const kFairPlayStreamingKeySystemConfig = {
|
||
initDataTypes: ['cenc'],
|
||
};
|
||
const kFairPlayStreamingKeySystemUUID = new Uint8Array([148, 206, 134, 251, 7, 255, 79, 67, 173, 184, 147, 210, 250, 150, 140, 162]);
|
||
var SchemeFourCC;
|
||
(function (SchemeFourCC) {
|
||
SchemeFourCC[SchemeFourCC["CENC"] = 1667591779] = "CENC";
|
||
SchemeFourCC[SchemeFourCC["CBCS"] = 1667392371] = "CBCS";
|
||
})(SchemeFourCC || (SchemeFourCC = {}));
|
||
/**
|
||
* Base class for FPS key systems
|
||
*/
|
||
class FairPlayStreamingKeySystemBase extends KeySystem {
|
||
constructor(mediaKeys, keySystemString, config, eventEmitter, useSingleKeySession, sessionHandler, logger) {
|
||
super(mediaKeys, keySystemString, config, eventEmitter, useSingleKeySession, sessionHandler, logger);
|
||
this._hasSetRenewal = false;
|
||
}
|
||
static get systemId() {
|
||
return kFairPlayStreamingKeySystemUUID;
|
||
}
|
||
static get requestAccessConfig() {
|
||
return kFairPlayStreamingKeySystemConfig;
|
||
}
|
||
get needsCert() {
|
||
return true;
|
||
}
|
||
sanitizeRequest(requestInfo) {
|
||
return {
|
||
assetId: requestInfo && requestInfo.assetId ? new Uint8Array(requestInfo.assetId) : undefined,
|
||
ssc: requestInfo && requestInfo.ssc ? new Uint8Array(requestInfo.ssc) : undefined,
|
||
sessionId: requestInfo && requestInfo.sessionId ? requestInfo.sessionId : undefined,
|
||
};
|
||
}
|
||
_scheduleRenewal(keyInfo, renewalMs) {
|
||
if (!this.useSingleKeySession) {
|
||
// Each key has its own timer
|
||
super._scheduleRenewal(keyInfo, renewalMs);
|
||
}
|
||
else if (!this._hasSetRenewal) {
|
||
this._hasSetRenewal = true;
|
||
this.logger.info(`${this.systemString} Scheduling renewal in ${renewalMs} ms`);
|
||
timer(renewalMs)
|
||
.pipe(tap(() => {
|
||
// use first available keyInfo
|
||
const keyInfo = Object.values(this.keyUriToKeyInfo)[0];
|
||
if (keyInfo) {
|
||
this._signalRenewal(keyInfo);
|
||
}
|
||
}), finalize$1(() => {
|
||
this._hasSetRenewal = false;
|
||
}), takeUntil(this.destroy$))
|
||
.subscribe();
|
||
}
|
||
}
|
||
/**
|
||
* Handle a parsed key response
|
||
* @param {Object} keyInfo Info about the key
|
||
* @param {Object} keyResponse The parsed key response object { statusCode, ckc, renewalData }
|
||
* @returns {Observable<void>} message to pass to update() for processing license
|
||
*/
|
||
handleParsedKeyResponse(keyInfo, keyResponse) {
|
||
const keyuri = keyInfo.decryptdata.uri;
|
||
const statusCode = keyResponse.statusCode;
|
||
const licenseResponse = keyResponse.ckc && keyResponse.ckc.byteLength !== 0 ? keyResponse.ckc : keyResponse.license && keyResponse.license.byteLength !== 0 ? keyResponse.license : undefined;
|
||
// Special handling in case of HTTP Errors during key requests.
|
||
if (statusCode === 0 && !licenseResponse) {
|
||
return throwError(new KeyRequestError('License request resulted in HTTP Error', keyuri, statusCode, { code: statusCode, text: 'HTTP Error' }, true, KeyRequestErrorReason.HttpError));
|
||
}
|
||
if (statusCode !== 0) {
|
||
return throwError(new KeyRequestError('License server responded with error', keyuri, statusCode, { code: statusCode, text: 'Server Error' }, false, KeyRequestErrorReason.LicenseServerError));
|
||
}
|
||
if (!licenseResponse) {
|
||
const err = new KeyRequestError('License server responded with invalid license', keyuri, statusCode, { code: statusCode, text: 'Invalid license' }, false, KeyRequestErrorReason.LicenseServerError);
|
||
return throwError(err);
|
||
}
|
||
const renewalDate = keyResponse.renewalDate;
|
||
const now = new Date();
|
||
const renewalMs = renewalDate > now ? renewalDate.getTime() - now.getTime() : 0;
|
||
if (renewalMs > 0) {
|
||
this._scheduleRenewal(keyInfo, renewalMs);
|
||
}
|
||
const ckcArray = this.makeProcessLicenseRequestMessage(keyInfo, licenseResponse, renewalMs);
|
||
// Only resolve if we got success, either from 'usable' keystatuschange event
|
||
// or on promise completion with 'usable' key status. Otherwise reject
|
||
const session = keyInfo.session;
|
||
const updateSession = from(session.update(ckcArray)).pipe(tap(() => {
|
||
const keyId = keyInfo.decryptdata.keyId;
|
||
const status = session.keyStatuses.get(keyId);
|
||
// Slightly faster than waiting for keystatuschange
|
||
this.handleKeyStatusForKey(status, keyInfo);
|
||
}), catchError((error) => {
|
||
const err = new KeySystemError(error.message, keyInfo.decryptdata.uri, 0, ErrorResponses.KeySystemFailedToUpdateSession, this.systemString);
|
||
throw err;
|
||
}));
|
||
const getLicense = keyInfo.setKeyRequestState(KeyRequestState.PROCESS_LICENSE);
|
||
return forkJoin([getLicense, updateSession]).pipe(mapTo(undefined));
|
||
}
|
||
makeKeyRequests(requestInfoArray) {
|
||
const keyRequests = [];
|
||
for (const r of requestInfoArray) {
|
||
const decryptdata = r.decryptdata;
|
||
const requestInfo = r.requestInfo;
|
||
const keyId = decryptdata.keyId;
|
||
keyRequests.push({
|
||
keyId: keyId,
|
||
assetId: requestInfo ? requestInfo.assetId : undefined,
|
||
ssc: requestInfo ? requestInfo.ssc : undefined,
|
||
versionList: decryptdata.formatversions,
|
||
});
|
||
}
|
||
return keyRequests;
|
||
}
|
||
getSchemeAndFlags(decryptdata) {
|
||
const scheme = decryptdata.method === 'ISO-23001-7' ? SchemeFourCC.CENC : SchemeFourCC.CBCS; // TODO pass real scheme through.
|
||
const flags = 0;
|
||
// TODO
|
||
/* if (this.hls.playingItem.secureStop) {
|
||
flags |= 0x01;
|
||
} */
|
||
return { scheme, flags };
|
||
}
|
||
generateInitData(keyId, decryptdata, requestInfo) {
|
||
// Generate a PSSH
|
||
const { scheme, flags } = this.getSchemeAndFlags(decryptdata);
|
||
const keyRequests = this.makeKeyRequests([{ decryptdata, requestInfo }]);
|
||
const pssh = this.makeFpsKeySystemInitData(scheme, flags, keyRequests);
|
||
// console.log('PSSH=' + Hex.hexDump(pssh));
|
||
return { initData: pssh, initDataType: 'cenc' };
|
||
}
|
||
/**
|
||
* Generate a key renewal request on an already initialized session
|
||
* @param keyInfo Info about the key we're requesting
|
||
* @param isRenewal Is this a renewal requewst
|
||
* @returns promise for when update call is done.
|
||
**/
|
||
generateRequestInitialized(keyInfo, isRenewal) {
|
||
tag(`[Keys] challenge create start uri=${redactUrl(keyInfo.decryptdata.uri)} versions=${JSON.stringify(keyInfo.decryptdata.formatversions)}`);
|
||
const message = this.makeKeyRequestMessage(keyInfo, isRenewal);
|
||
if (!message) {
|
||
const err = new KeyRequestError('Unable to generate request using existing keySession', keyInfo.decryptdata.uri, 0, ErrorResponses.KeySystemFailedToGenerateLicenseRequest, true, KeyRequestErrorReason.InvalidState);
|
||
return throwError(err);
|
||
}
|
||
return from(keyInfo.session.update(message)).pipe(tap(() => {
|
||
keyInfo.requestInfo = undefined; // Don't need requestInfo anymore.
|
||
}), catchError((reason) => {
|
||
tag(`[Keys] ${this.systemString} FAIL: generateRequestInitialized keyuri=${redactUrl(keyInfo.decryptdata.uri)} message=${reason.message}`);
|
||
throw new KeySystemError(reason.message, keyInfo.decryptdata.uri, 0, ErrorResponses.KeySystemFailedToGenerateLicenseRenewal, this.systemString);
|
||
}));
|
||
}
|
||
/**
|
||
* @param {Object} keyInfo Info about the key we're requesting
|
||
* @param {Uint8Array} licenseChallenge Challenge bytes
|
||
* @returns {Observable<Object>} Promise returning the license response
|
||
*/
|
||
getKeyRequestResponse(keyInfo, licenseChallenge) {
|
||
const keyuri = keyInfo.decryptdata.uri;
|
||
const getKeyResponse = keyInfo.setKeyRequestState(KeyRequestState.GET_KEY_RESPONSE);
|
||
this.eventEmitter.trigger(HlsEvent$1.LICENSE_CHALLENGE_CREATED, {
|
||
keyuri: keyuri,
|
||
licenseChallenge: licenseChallenge,
|
||
keysystem: this.systemString,
|
||
});
|
||
return getKeyResponse;
|
||
}
|
||
resolveSPCPromise(keyInfo, spc) {
|
||
keyInfo.resolveState(KeyRequestState.GET_CHALLENGE, spc);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* FairPlayStreaming key system
|
||
*
|
||
* 2018 Apple Inc. All rights reserved.
|
||
*/
|
||
//declare var __SECURESTOP_KEYS__: IFPSSecureStopKeys;
|
||
//const SecureStopKeys = __SECURESTOP_KEYS__;
|
||
const SecureStopKeys = {};
|
||
const MessageFourCC = {
|
||
NONE: 0,
|
||
CRKR: 1668442994,
|
||
RMKY: 1919773561,
|
||
CKCS: 1667982195,
|
||
CERT: 1667592820,
|
||
SPCS: 1936745331,
|
||
RNEW: 1919837559,
|
||
RLSE: 1919710053,
|
||
SSTP: 1936946288,
|
||
CDMI: 1667525993,
|
||
};
|
||
const APIProviderId = {
|
||
CENC: 'EC396D13-FB13-4993-9D0D-71518ACF3D6F',
|
||
CBCS: 'F19BF03B-7470-41A4-9655-86D078307D59',
|
||
};
|
||
/**
|
||
* Read a generic data field
|
||
*
|
||
* Format: 4B length of data, nB data bytes
|
||
*
|
||
* @param {Uint8Array} inBuf Buffer referenced by dv
|
||
* @param {DataView} dv DataView wrapper around inBuf
|
||
* @param {number} pos Starting position in buffer to read from
|
||
* @returns {Object} { pos: next byte to read, data: Uint8Array holding data parsed out of inBuf }
|
||
*/
|
||
function readDataField(inBuf, dv, pos) {
|
||
if (pos + 4 > inBuf.byteLength) {
|
||
return undefined;
|
||
}
|
||
const dataLen = dv.getUint32(pos);
|
||
pos += 4;
|
||
if (pos + dataLen > inBuf.byteLength) {
|
||
return undefined;
|
||
}
|
||
const data = inBuf.slice(pos, pos + dataLen);
|
||
pos += dataLen;
|
||
return { pos: pos, data: data };
|
||
}
|
||
// Read message:
|
||
// cmd (4)
|
||
// arrayLen (4)
|
||
// keyId (16)
|
||
// dataLen (4)
|
||
// data (dataLen)
|
||
function parseData(message) {
|
||
const keyIdToData = {};
|
||
const dv = new DataView(message.buffer);
|
||
let pos = 4;
|
||
const arrayLen = dv.getUint32(pos);
|
||
pos += 4;
|
||
for (let i = 0; i < arrayLen && pos < message.byteLength; ++i) {
|
||
if (pos + 16 > message.byteLength) {
|
||
break;
|
||
}
|
||
const keyId = message.slice(pos, pos + 16);
|
||
pos += 16;
|
||
if (pos + 4 > message.byteLength) {
|
||
break;
|
||
}
|
||
const tmp = readDataField(message, dv, pos);
|
||
if (tmp) {
|
||
pos = tmp.pos;
|
||
keyIdToData[BufferUtils.utf8arrayToStr(keyId)] = tmp.data;
|
||
}
|
||
else {
|
||
break;
|
||
}
|
||
}
|
||
return keyIdToData;
|
||
}
|
||
function addDataField(outBuf, dv, pos, data) {
|
||
const len = data ? data.byteLength : 0;
|
||
dv.setUint32(pos, len);
|
||
if (len) {
|
||
outBuf.set(data, pos + 4);
|
||
}
|
||
pos += 4 + len;
|
||
return pos;
|
||
}
|
||
// Including 4 byte arrayLen
|
||
function getKeyRequestArraySize(keyRequests) {
|
||
// arrayLen (4)
|
||
// --------- For each key:
|
||
// keyId(16)
|
||
// assetIdLen (4), assetId
|
||
// sscLen (4), ssc
|
||
// versionLen (4), versionList (each one 4B)
|
||
let bytesForArray = 4;
|
||
for (const kr of keyRequests) {
|
||
bytesForArray += 28 + (kr.assetId ? kr.assetId.byteLength : 0) + (kr.ssc ? kr.ssc.byteLength : 0) + (kr.versionList ? kr.versionList.length * 4 : 0);
|
||
}
|
||
return bytesForArray;
|
||
}
|
||
/**
|
||
* Fill existing array w/ key request data
|
||
* @param array uint8array
|
||
* @param dv dataview for array
|
||
* @param pos position to write to within array
|
||
* @param keyRequests Array of key requests [ { assetId, ssc, versionList } ]
|
||
* @returns
|
||
*/
|
||
function addKeyRequestArray(array, dv, pos, keyRequests) {
|
||
dv.setUint32(pos, keyRequests.length); // arrayLen
|
||
pos += 4;
|
||
for (const kr of keyRequests) {
|
||
array.set(kr.keyId, pos);
|
||
pos += 16;
|
||
pos = addDataField(array, dv, pos, kr.assetId);
|
||
pos = addDataField(array, dv, pos, kr.ssc);
|
||
dv.setUint32(pos, kr.versionList ? kr.versionList.length : 0);
|
||
pos += 4;
|
||
if (kr.versionList) {
|
||
for (const vl of kr.versionList) {
|
||
dv.setUint32(pos, vl);
|
||
pos += 4;
|
||
}
|
||
}
|
||
}
|
||
// console.log(`addKeyRequestArray: ${Hex.hexDump(array)}`);
|
||
return pos;
|
||
}
|
||
function makeFpsKeySystemInitData$1(scheme, protocol, flags, keyRequests) {
|
||
// Generate a PSSH
|
||
// Data for FPS:
|
||
// scheme: CC 4B
|
||
// protocol: 1B
|
||
// flags: 3B
|
||
// keyrequestarray
|
||
const keyRequestBytes = getKeyRequestArraySize(keyRequests);
|
||
let pos = 0;
|
||
const data = new Uint8Array(8 + keyRequestBytes);
|
||
const dv = new DataView(data.buffer);
|
||
dv.setUint32(pos, scheme); // schm
|
||
dv.setUint32(pos + 4, (protocol << 24) | (flags & 16777215)); // version | flags
|
||
pos += 8;
|
||
pos = addKeyRequestArray(data, dv, pos, keyRequests);
|
||
// console.log(`makeFpsKeySystemInitData: ${Hex.hexDump(data)}`);
|
||
return data;
|
||
}
|
||
function makeCreateKeyRequestMessage(keyRequests) {
|
||
// cmd (4)
|
||
// keyrequest array
|
||
// first calculate bytes to allocate
|
||
const bytesForArray = getKeyRequestArraySize(keyRequests);
|
||
let pos = 0;
|
||
const req = new Uint8Array(4 + bytesForArray);
|
||
const dv = new DataView(req.buffer);
|
||
dv.setUint32(0, MessageFourCC.CRKR); // cmd
|
||
pos += 4;
|
||
pos = addKeyRequestArray(req, dv, pos, keyRequests);
|
||
// console.log(`makeCreateKeyRequestMessage: ${Hex.hexDump(req)}`);
|
||
return req;
|
||
}
|
||
function makeProcessLicenseRequestMessage(licenses) {
|
||
// cmd (4)
|
||
// arrayLen (4)
|
||
// keyId (16)
|
||
// expiry (4)
|
||
// ckcLen (4)
|
||
// ckcBytes
|
||
// first calculate bytes to allocate
|
||
let bytesForArray = 0;
|
||
for (const l of licenses) {
|
||
bytesForArray += 24 + l.ckc.byteLength;
|
||
}
|
||
let pos = 0;
|
||
const req = new Uint8Array(8 + bytesForArray);
|
||
const dv = new DataView(req.buffer);
|
||
dv.setUint32(0, MessageFourCC.CKCS); // cmd
|
||
dv.setUint32(4, licenses.length); // arrayLen
|
||
pos += 8;
|
||
for (const l of licenses) {
|
||
req.set(l.keyId, pos);
|
||
pos += 16;
|
||
dv.setUint32(pos, l.expirySec);
|
||
pos += 4;
|
||
pos = addDataField(req, dv, pos, l.ckc);
|
||
}
|
||
// console.log(`makeProcessLicenseRequestMessage: ${Hex.hexDump(req)}`);
|
||
return req;
|
||
}
|
||
class FairPlayStreamingKeySystem extends FairPlayStreamingKeySystemBase {
|
||
constructor(mediaKeys, keySystemString, config, eventEmitter, sessionHandler, logger) {
|
||
const singleKeySession = typeof config.useMultipleKeySessions === 'undefined' || !config.useMultipleKeySessions;
|
||
super(mediaKeys, keySystemString, config, eventEmitter, singleKeySession, sessionHandler, logger);
|
||
}
|
||
makeProcessLicenseRequestMessage(keyInfo, licenseResponse, renewalMs) {
|
||
const ckcBytes = new Uint8Array(licenseResponse);
|
||
const ckcArray = makeProcessLicenseRequestMessage([
|
||
{
|
||
keyId: keyInfo.decryptdata.keyId,
|
||
expirySec: renewalMs / 1000,
|
||
ckc: ckcBytes,
|
||
},
|
||
]);
|
||
return ckcArray;
|
||
}
|
||
makeFpsKeySystemInitData(scheme, flags, keyRequests) {
|
||
const protocol = 1;
|
||
const data = makeFpsKeySystemInitData$1(scheme, protocol, flags, keyRequests);
|
||
const pssh = MP4$1.pssh(FairPlayStreamingKeySystem.systemId, [], data);
|
||
return pssh;
|
||
}
|
||
makeKeyRequestMessage(keyInfo /* , isRenewal*/) {
|
||
const crkrArray = makeCreateKeyRequestMessage([
|
||
{
|
||
keyId: keyInfo.decryptdata.keyId,
|
||
assetId: keyInfo.requestInfo ? keyInfo.requestInfo.assetId : undefined,
|
||
ssc: keyInfo.requestInfo ? keyInfo.requestInfo.ssc : undefined,
|
||
versionList: keyInfo.decryptdata.formatversions,
|
||
},
|
||
]);
|
||
return crkrArray;
|
||
}
|
||
handleKeyMessage(event) {
|
||
if (event.message.byteLength < 4) {
|
||
this.logger.warn('Unexpected message');
|
||
return;
|
||
}
|
||
// event.message (ArrayBuffer)
|
||
const message = new Uint8Array(event.message);
|
||
const dv = new DataView(event.message);
|
||
const cmd = dv.getUint32(0);
|
||
if (this.isDestroying && cmd !== MessageFourCC.RLSE) {
|
||
this.logger.warn(`In the middle of destroying, ignore command: ${cmd.toString(16)}`);
|
||
return;
|
||
}
|
||
this.logger.info(`[Keys] ${this.systemString} message 0x${cmd.toString(16)}`);
|
||
switch (cmd) {
|
||
case MessageFourCC.CERT:
|
||
this.logger.warn('Certificate not set!');
|
||
break;
|
||
case MessageFourCC.RNEW: {
|
||
const keyIdToData = parseData(message);
|
||
for (const keyIdStr in keyIdToData) {
|
||
if (Object.prototype.hasOwnProperty.call(keyIdToData, keyIdStr)) {
|
||
const keyInfo = this.keyIdToKeyInfo[keyIdStr];
|
||
if (keyInfo) {
|
||
this._signalRenewal(keyInfo);
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
case MessageFourCC.SPCS: {
|
||
const keyIdToData = parseData(message);
|
||
for (const keyIdStr in keyIdToData) {
|
||
if (Object.prototype.hasOwnProperty.call(keyIdToData, keyIdStr)) {
|
||
const keyInfo = this.keyIdToKeyInfo[keyIdStr];
|
||
const spc = keyIdToData[keyIdStr];
|
||
if (keyInfo && spc) {
|
||
this.resolveSPCPromise(keyInfo, spc);
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
case MessageFourCC.RLSE: {
|
||
this._handleLicenseRelease(message);
|
||
break;
|
||
}
|
||
case MessageFourCC.CDMI: {
|
||
// Message length = 4
|
||
// CDM version = remaining data
|
||
const pos = 4;
|
||
const tmp = readDataField(message, dv, pos);
|
||
if (tmp) {
|
||
const cdmVersion = (this.cdmVersion = BufferUtils.utf8arrayToStr(tmp.data));
|
||
this.logger.info(`[Keys] ${this.systemString} CDM Version : ${cdmVersion}`);
|
||
}
|
||
break;
|
||
}
|
||
default:
|
||
this.logger.warn(`Unrecognized command:'0x${cmd.toString(16)}'`);
|
||
break;
|
||
}
|
||
}
|
||
_handleLicenseRelease(message) {
|
||
const releaseRecord = {};
|
||
const dv = new DataView(message.buffer);
|
||
const type = dv.getUint32(4); // Release type
|
||
switch (type) {
|
||
case MessageFourCC.SSTP: {
|
||
// Secure stop
|
||
if (!SecureStopKeys) {
|
||
this.logger.warn('No secure stop keys defined');
|
||
break;
|
||
}
|
||
releaseRecord[SecureStopKeys.SessionId] = this.sessionId;
|
||
// scheme (uint32)
|
||
// movieIdLen (uint32)
|
||
// movieIdStr
|
||
// secureStopSPCLen (uint32)
|
||
// secureStopSPC
|
||
// sessionLifespanSPCLen (uint32)
|
||
// sessionLifespanSPC
|
||
let pos = 8;
|
||
if (pos + 4 > message.byteLength) {
|
||
break;
|
||
}
|
||
releaseRecord[SecureStopKeys.APIProvider] = dv.getUint32(pos) === SchemeFourCC.CENC ? APIProviderId.CENC : APIProviderId.CBCS;
|
||
pos += 4;
|
||
let tmp = readDataField(message, dv, pos);
|
||
if (tmp) {
|
||
pos = tmp.pos;
|
||
releaseRecord[SecureStopKeys.MovieID] = BufferUtils.utf8arrayToStr(tmp.data);
|
||
}
|
||
else {
|
||
break;
|
||
}
|
||
tmp = readDataField(message, dv, pos);
|
||
if (tmp) {
|
||
pos = tmp.pos;
|
||
releaseRecord[SecureStopKeys.SecureStopSPC] = tmp.data;
|
||
}
|
||
else {
|
||
break;
|
||
}
|
||
tmp = readDataField(message, dv, pos);
|
||
if (tmp) {
|
||
pos = tmp.pos;
|
||
releaseRecord[SecureStopKeys.SessionLifespanSPC] = tmp.data;
|
||
}
|
||
else {
|
||
break;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
this.eventEmitter.trigger(HlsEvent$1.LICENSE_RELEASED, {
|
||
keysystem: this.systemString,
|
||
itemId: this.itemId,
|
||
releaseRecord: releaseRecord,
|
||
});
|
||
}
|
||
}
|
||
var FPSKeySystem = FairPlayStreamingKeySystem;
|
||
|
||
const FpsBoxTypes = {
|
||
fpsd: BufferUtils.strToUtf8array('fpsd'),
|
||
fpsi: BufferUtils.strToUtf8array('fpsi'),
|
||
fpsk: BufferUtils.strToUtf8array('fpsk'),
|
||
fkri: BufferUtils.strToUtf8array('fkri'),
|
||
fkai: BufferUtils.strToUtf8array('fkai'),
|
||
fkcx: BufferUtils.strToUtf8array('fkcx'),
|
||
fkvl: BufferUtils.strToUtf8array('fkvl'), // version list
|
||
};
|
||
/**
|
||
* @param scheme cbcs or cenc
|
||
* @param flags keySystem flags
|
||
* @returns FpsKeySystemInitDataBox
|
||
*/
|
||
function makeFpsKeySystemInfoBox(scheme, flags) {
|
||
const schemeArray = new Uint8Array(4);
|
||
MP4$1.set32(scheme, schemeArray, 0);
|
||
return MP4$1.box(FpsBoxTypes.fpsi, new Uint8Array([
|
||
0,
|
||
(flags >> 16) & 255,
|
||
(flags >> 8) & 255,
|
||
flags & 255,
|
||
]), schemeArray);
|
||
}
|
||
/**
|
||
* Generate a FpsKeyRequestBox
|
||
* @param {ArrayBuffer} keyId The key ID
|
||
* @param {ArrayBuffer} assetId AssetID
|
||
* @param {ArrayBuffer} ssc Remote secure context
|
||
* @param {Array} versionList A list of Number from EXT-X-KEY:KEYFORMATVERSIONS
|
||
* @returns {ArrayBuffer} a FpsKeyRequestBox
|
||
*/
|
||
function makeFpsKeyRequestBox(keyId, assetId, ssc, versionList) {
|
||
const args = [FpsBoxTypes.fpsk];
|
||
// Mandatory
|
||
const fkri = MP4$1.box(FpsBoxTypes.fkri, new Uint8Array([0, 0, 0, 0]), keyId);
|
||
args.push(fkri);
|
||
// Optional boxes
|
||
if (assetId && assetId.byteLength) {
|
||
args.push(MP4$1.box(FpsBoxTypes.fkai, assetId));
|
||
}
|
||
if (ssc && ssc.byteLength) {
|
||
args.push(MP4$1.box(FpsBoxTypes.fkcx, ssc));
|
||
}
|
||
if (versionList && versionList.length) {
|
||
// List of integers
|
||
const versionListBuffer = new Uint8Array(4 * versionList.length);
|
||
let pos = 0;
|
||
for (const version of versionList) {
|
||
MP4$1.set32(version, versionListBuffer, pos);
|
||
pos += 4;
|
||
}
|
||
args.push(MP4$1.box(FpsBoxTypes.fkvl, versionListBuffer));
|
||
}
|
||
const fpsk = MP4$1.box.apply(null, args);
|
||
return fpsk;
|
||
}
|
||
/**
|
||
* @param {SchemeFourCC} scheme cbcs or cenc (fourCC)
|
||
* @param {number} flags keySystem flags (number)
|
||
* @param {Array} keyRequests array of requests [ { keyId, assetId, ssc, versionList } ]
|
||
* @returns {Uint8Array} a PSSH box for initializing the key system
|
||
*/
|
||
function makeFpsKeySystemInitData(scheme, flags, keyRequests) {
|
||
// Mandatory box for key system initialization
|
||
const args = [FpsBoxTypes.fpsd, makeFpsKeySystemInfoBox(scheme, flags)];
|
||
for (const kr of keyRequests) {
|
||
args.push(makeFpsKeyRequestBox(kr.keyId, kr.assetId, kr.ssc, kr.versionList));
|
||
}
|
||
const data = MP4$1.box.apply(null, args);
|
||
const pssh = MP4$1.pssh(FairPlayStreamingKeySystemBase.systemId, null, data);
|
||
return pssh;
|
||
}
|
||
class FairPlayStreamingKeySystemV3 extends FairPlayStreamingKeySystemBase {
|
||
constructor(mediaKeys, keySystemString, config, eventEmitter, sessionHandler, logger) {
|
||
super(mediaKeys, keySystemString, config, eventEmitter, false, sessionHandler, logger);
|
||
this.sessions = [];
|
||
this.keyIdToKeyInfo = {};
|
||
this.keyUriToKeyInfo = {};
|
||
this.sessionIdToKeyUri = {};
|
||
}
|
||
static get needsCert() {
|
||
return true;
|
||
}
|
||
handleKeyExchangeError(keyInfo, err) {
|
||
// If at any point we had an error during key exchange we need to destroy this session
|
||
this.removeKey(keyInfo.decryptdata).subscribe();
|
||
super.handleKeyExchangeError(keyInfo, err);
|
||
}
|
||
_abortKeyRequest(keyInfo) {
|
||
if (!this.isDestroying && keyInfo && keyInfo.requestState !== KeyRequestState.NONE) {
|
||
this.removeKey(keyInfo.decryptdata).subscribe();
|
||
}
|
||
return super._abortKeyRequest(keyInfo);
|
||
}
|
||
makeFpsKeySystemInitData(scheme, flags, keyRequests) {
|
||
return makeFpsKeySystemInitData(scheme, flags, keyRequests);
|
||
}
|
||
makeKeyRequestMessage(keyInfo, isRenewal) {
|
||
if (isRenewal) {
|
||
return BufferUtils.strToUtf8array('renew');
|
||
}
|
||
return undefined;
|
||
}
|
||
makeProcessLicenseRequestMessage(keyInfo, licenseResponse, renewalMs) {
|
||
const responseStr = JSON.stringify([
|
||
{
|
||
keyID: NumericEncodingUtils$1.base64Encode(keyInfo.decryptdata.keyId),
|
||
payload: NumericEncodingUtils$1.base64Encode(new Uint8Array(licenseResponse)),
|
||
},
|
||
]);
|
||
this.logger.debug(`[Keys] processLicense msg=${responseStr}`);
|
||
return BufferUtils.strToUtf8array(responseStr);
|
||
}
|
||
handleKeyMessage(event) {
|
||
const session = event.target;
|
||
const sessionId = session.sessionId;
|
||
const messageType = event.messageType;
|
||
this.logger.info(`[Keys] onKeyMessage sessionId=${sessionId} type=${messageType}`);
|
||
const keyuri = this.sessionIdToKeyUri[sessionId];
|
||
let keyInfo;
|
||
if (!keyuri) {
|
||
// key message could happen before generateRequest() promise completes
|
||
for (const ki of Object.values(this.keyUriToKeyInfo)) {
|
||
if (ki && ki.session === session) {
|
||
keyInfo = ki;
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
keyInfo = this.keyUriToKeyInfo[keyuri];
|
||
}
|
||
if (!keyInfo) {
|
||
this.logger.warn('[Keys] No key associated with session');
|
||
return;
|
||
}
|
||
switch (messageType) {
|
||
case 'license-request': {
|
||
const message = new Uint8Array(event.message);
|
||
const messagestr = BufferUtils.utf8arrayToStr(message);
|
||
try {
|
||
// Expect only one but we'll handle all of them anyway
|
||
const spcArray = JSON.parse(messagestr);
|
||
spcArray.forEach((payload) => {
|
||
const keyIdStr = NumericEncodingUtils$1.base64DecodeToStr(payload.keyID);
|
||
const spc = NumericEncodingUtils$1.base64Decode(payload.payload);
|
||
const keyInfo = this.keyIdToKeyInfo[keyIdStr];
|
||
if (keyInfo) {
|
||
this.resolveSPCPromise(keyInfo, spc);
|
||
}
|
||
});
|
||
}
|
||
catch (error) {
|
||
this.logger.warn('[Keys] got unexpected license-request format');
|
||
this.resolveSPCPromise(keyInfo, message); // Last ditch effort
|
||
}
|
||
break;
|
||
}
|
||
case 'license-renewal': {
|
||
const spc = new Uint8Array(event.message);
|
||
this.resolveSPCPromise(keyInfo, spc);
|
||
break;
|
||
}
|
||
case 'license-release':
|
||
this._handleLicenseRelease(session);
|
||
break;
|
||
default:
|
||
this.logger.warn(`[Keys] Unexpected messageType ${messageType}`);
|
||
break;
|
||
}
|
||
}
|
||
_handleLicenseRelease(keySession) {
|
||
keySession.update(BufferUtils.strToUtf8array('acknowledged')).catch((error) => {
|
||
this.logger.error(`Promise error: ${error.message}`);
|
||
});
|
||
}
|
||
}
|
||
var FPSKeySystemV3 = FairPlayStreamingKeySystemV3;
|
||
|
||
/**
|
||
* PlayReady key system
|
||
*/
|
||
const kPlayReadyKeySystemConfig = {
|
||
initDataTypes: ['cenc'],
|
||
};
|
||
const kPlayReadyKeySystemUUID = new Uint8Array([154, 4, 240, 121, 152, 64, 66, 134, 171, 146, 230, 91, 224, 136, 95, 149]);
|
||
class PlayReadyKeySystem extends KeySystem {
|
||
constructor(mediaKeys, systemString, config, eventEmitter, sessionHandler, logger) {
|
||
super(mediaKeys, systemString, config, eventEmitter, false, sessionHandler, logger);
|
||
this.shouldDestroyMediaKeys = true;
|
||
}
|
||
// destroy()
|
||
static get systemId() {
|
||
return kPlayReadyKeySystemUUID;
|
||
}
|
||
static get requestAccessConfig() {
|
||
return kPlayReadyKeySystemConfig;
|
||
}
|
||
get needsCert() {
|
||
return false;
|
||
}
|
||
generateInitData(keyId, decryptdata /* , requestInfo */) {
|
||
const data = decryptdata.pssh;
|
||
const pssh = MP4$1.pssh(PlayReadyKeySystem.systemId, [], data);
|
||
// console.log('PSSH=' + Hex.hexDump(pssh));
|
||
return { initData: pssh, initDataType: 'cenc' };
|
||
}
|
||
removeKey(decryptdata) {
|
||
return super.removeSessions(decryptdata, true);
|
||
}
|
||
ensureKeyContext(decryptdata) {
|
||
const keyuri = decryptdata.uri;
|
||
const keyInfo = this.keyUriToKeyInfo[keyuri];
|
||
// Create a new session for current key request and preserve previous sessions for
|
||
// cleanup later
|
||
if (keyInfo === null || keyInfo === void 0 ? void 0 : keyInfo.session) {
|
||
keyInfo.oldSessions.push(keyInfo.session);
|
||
keyInfo.session = null;
|
||
}
|
||
return super.ensureKeyContext(decryptdata);
|
||
}
|
||
getKeyRequestResponse(keyInfo, licenseChallenge) {
|
||
const keyuri = keyInfo.decryptdata.uri;
|
||
const getKeyResponse = keyInfo.setKeyRequestState(KeyRequestState.GET_KEY_RESPONSE);
|
||
this.eventEmitter.trigger(HlsEvent$1.LICENSE_CHALLENGE_CREATED, {
|
||
keyuri: keyuri,
|
||
licenseChallenge: licenseChallenge,
|
||
keysystem: this.systemString,
|
||
keyId: keyInfo.decryptdata.keyId,
|
||
});
|
||
return getKeyResponse;
|
||
}
|
||
/**
|
||
* Generate a key renewal request
|
||
* @param keyInfo Info about the key we're requesting
|
||
* @returns {Observable<Uint8Array>} Promise returning the license challenge
|
||
**/
|
||
generateRequestInitialized(keyInfo) {
|
||
const keyuri = keyInfo.decryptdata.uri;
|
||
const challenge = keyInfo.licenseChallenge;
|
||
this.logger.debug(`[Keys] challenge create start uri=${redactUrl(keyuri)} versions=${JSON.stringify(keyInfo.decryptdata.formatversions)}`);
|
||
keyInfo.requestInfo = undefined; // Don't need requestInfo anymore.
|
||
keyInfo.resolveState(KeyRequestState.GET_CHALLENGE, challenge);
|
||
return VOID;
|
||
}
|
||
// Default behavior is to just update using data.response
|
||
handleParsedKeyResponse(keyInfo, keyResponse) {
|
||
const keyuri = keyInfo.decryptdata.uri;
|
||
const statusCode = keyResponse.statusCode;
|
||
if (statusCode !== 0) {
|
||
return throwError(new KeyRequestError('License server responded with error', keyuri, statusCode, { code: statusCode, text: 'Server error' }, false, KeyRequestErrorReason.LicenseServerError));
|
||
}
|
||
if (!keyResponse.license || !keyResponse.license.byteLength) {
|
||
return throwError(new KeyRequestError('License server responded with invalid license', keyuri, statusCode, { code: statusCode, text: 'Invalid license' }, false, KeyRequestErrorReason.LicenseServerError));
|
||
}
|
||
if (keyResponse.renewalDate) {
|
||
const renewalDate = keyResponse.renewalDate;
|
||
const now = new Date();
|
||
const renewalMs = renewalDate > now ? renewalDate.getTime() - now.getTime() : 0;
|
||
if (renewalMs > 0) {
|
||
this._scheduleRenewal(keyInfo, renewalMs);
|
||
}
|
||
}
|
||
const processLicense = keyInfo.setKeyRequestState(KeyRequestState.PROCESS_LICENSE);
|
||
const updateSession = from(keyInfo.session.update(keyResponse.license)).pipe(tap(() => {
|
||
// PR CDM on Edge won't change key status till the buffers are pushed. To push buffer the key status should be usable
|
||
// So don't rely on key status to change the state like other key systems.
|
||
keyInfo.resolveState(KeyRequestState.PROCESS_LICENSE, undefined);
|
||
}), catchError((reason) => {
|
||
this.logger.error(`${this.systemString} FAIL: Failed to update with key response message=${reason.message}`);
|
||
const err = new KeySystemError(reason.message, keyInfo.decryptdata.uri, 0, ErrorResponses.KeySystemFailedToUpdateSession, this.systemString);
|
||
throw err;
|
||
}));
|
||
return forkJoin([processLicense, updateSession]).pipe(mapTo(undefined));
|
||
}
|
||
// message
|
||
handleKeyMessage(event) {
|
||
if (this.isDestroying) {
|
||
this.logger.warn('In the middle of destroying, ignore key message');
|
||
return;
|
||
}
|
||
const parser = new DOMParser();
|
||
// Verify the message format encoding, based on that create the typed array
|
||
// TO DO
|
||
const messageBuffer = this.config.playReadyMessageFormat === 'utf16' ? new Uint16Array(event.message.buffer || event.message) : new Uint8Array(event.message.buffer || event.message);
|
||
const message = String.fromCharCode.apply(null, Array.from(messageBuffer));
|
||
// Expected license challenge format
|
||
// <PlayReadyKeyMessage type="LicenseAcquisition">
|
||
// <LicenseAcquisition Version="1">
|
||
// <Challenge encoding="base64encoded">base64Challenge </Challenge>
|
||
// </LicenseAcquisition>
|
||
// </PlayReadyKeyMessage>
|
||
//
|
||
const keyMessage = parser.parseFromString(message, 'application/xml').getElementsByTagName('PlayReadyKeyMessage')[0];
|
||
if (!keyMessage || keyMessage.getAttribute('type') !== 'LicenseAcquisition') {
|
||
this.logger.warn(`${this.systemString} unrecognized message ignore it`);
|
||
return;
|
||
}
|
||
const challengeNode = parser.parseFromString(message, 'application/xml').getElementsByTagName('Challenge')[0];
|
||
if (!challengeNode || challengeNode.getAttribute('encoding') !== 'base64encoded' || challengeNode.childNodes.length === 0) {
|
||
this.logger.warn(`${this.systemString} wrong challenge format or empty challenge`);
|
||
return;
|
||
}
|
||
const challenge = NumericEncodingUtils$1.base64Decode(challengeNode.childNodes[0].nodeValue);
|
||
const challengeStr = BufferUtils.utf8arrayToStr(challenge);
|
||
// The license challenge contains the base64 encoded Key ID, extract it.
|
||
const keyData = parser.parseFromString(challengeStr, 'application/xml').getElementsByTagName('KID')[0];
|
||
let keyIdBase64 = null;
|
||
if (keyData.childNodes[0]) {
|
||
keyIdBase64 = keyData.childNodes[0].nodeValue;
|
||
}
|
||
else {
|
||
keyIdBase64 = keyData.getAttribute('VALUE');
|
||
}
|
||
// Using KeyID extract KeyInfo and Key URI
|
||
// KID value in PRO is a base64-encoded little endian GUID interpretation of UUID
|
||
// KID value in ‘tenc’ is a big endian UUID GUID interpretation of UUID
|
||
const keyId = NumericEncodingUtils$1.base64Decode(keyIdBase64).subarray(0, 16);
|
||
keysystemutil.changeEndianness(keyId);
|
||
const keyInfo = this.keyIdToKeyInfo[BufferUtils.utf8arrayToStr(keyId)];
|
||
if (!keyInfo) {
|
||
this.logger.info(`${this.systemString} no keyInfo for keyId ${JSON.stringify(keyId)}`);
|
||
return;
|
||
}
|
||
keyInfo.licenseChallenge = challenge;
|
||
// Check demo/index.xml for more details on how exactly license is acquired. There is switch-case statement for LICENSE_CHALLENGE_CREATED
|
||
keyInfo.resolveState(KeyRequestState.GET_CHALLENGE, challenge);
|
||
}
|
||
}
|
||
var PlayReadyKeySystem$1 = PlayReadyKeySystem;
|
||
|
||
/**
|
||
* Widevine key system
|
||
*/
|
||
const kWidevineKeySystemConfig = {
|
||
initDataTypes: ['cenc', 'keyids'],
|
||
};
|
||
const kWidevineKeySystemUUID = new Uint8Array([237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237]);
|
||
class WidevineKeySystem extends KeySystem {
|
||
constructor(mediaKeys, systemString, config, eventEmitter, sessionHandler, logger) {
|
||
super(mediaKeys, systemString, config, eventEmitter, false, sessionHandler, logger);
|
||
this.shouldDestroyMediaKeys = true;
|
||
}
|
||
// destroy()
|
||
static get systemId() {
|
||
return kWidevineKeySystemUUID;
|
||
}
|
||
static get requestAccessConfig() {
|
||
return kWidevineKeySystemConfig;
|
||
}
|
||
get needsCert() {
|
||
return true;
|
||
}
|
||
removeKey(decryptdata) {
|
||
return super.removeSessions(decryptdata, true);
|
||
}
|
||
ensureKeyContext(decryptdata) {
|
||
const keyuri = decryptdata.uri;
|
||
const keyInfo = this.keyUriToKeyInfo[keyuri];
|
||
// Create a new session for current key request and preserve previous sessions for
|
||
// cleanup later
|
||
if (keyInfo === null || keyInfo === void 0 ? void 0 : keyInfo.session) {
|
||
keyInfo.oldSessions.push(keyInfo.session);
|
||
keyInfo.session = null;
|
||
}
|
||
return super.ensureKeyContext(decryptdata);
|
||
}
|
||
getKeyRequestResponse(keyInfo, licenseChallenge) {
|
||
const keyuri = keyInfo.decryptdata.uri;
|
||
const getKeyResponse = keyInfo.setKeyRequestState(KeyRequestState.GET_KEY_RESPONSE);
|
||
this.eventEmitter.trigger(HlsEvent$1.LICENSE_CHALLENGE_CREATED, {
|
||
keyuri: keyuri,
|
||
licenseChallenge: licenseChallenge,
|
||
keysystem: this.systemString,
|
||
keyId: keyInfo.decryptdata.keyId,
|
||
});
|
||
return getKeyResponse;
|
||
}
|
||
handleParsedKeyResponse(keyInfo, keyResponse) {
|
||
// clear the license challenge
|
||
keyInfo.licenseChallenge = undefined;
|
||
const keyuri = keyInfo.decryptdata.uri;
|
||
const statusCode = keyResponse.statusCode;
|
||
if (statusCode !== 0) {
|
||
return throwError(new KeyRequestError('License server responded with error', keyuri, statusCode, { code: statusCode, text: 'Server error' }, false, KeyRequestErrorReason.LicenseServerError));
|
||
}
|
||
if (!keyResponse.license || !keyResponse.license.byteLength) {
|
||
const err = new KeyRequestError('License server responded with invalid license', keyuri, statusCode, { code: statusCode, text: 'Invalid license' }, false, KeyRequestErrorReason.LicenseServerError);
|
||
return throwError(err);
|
||
}
|
||
if (keyResponse.renewalDate) {
|
||
const renewalDate = keyResponse.renewalDate;
|
||
const now = new Date();
|
||
const renewalMs = renewalDate > now ? renewalDate.getTime() - now.getTime() : 0;
|
||
if (renewalMs > 0) {
|
||
this._scheduleRenewal(keyInfo, renewalMs);
|
||
}
|
||
}
|
||
const processLicense = keyInfo.setKeyRequestState(KeyRequestState.PROCESS_LICENSE);
|
||
const updateSession = from(keyInfo.session.update(keyResponse.license)).pipe(tap(() => {
|
||
const keyId = keyInfo.decryptdata.keyId;
|
||
const state = keyInfo.session.keyStatuses.get(keyId);
|
||
this.handleKeyStatusForKey(state, keyInfo);
|
||
}), catchError((reason) => {
|
||
this.logger.error(`${this.systemString} FAIL: Failed to update with key response code=${reason.code} message=${reason.message}`);
|
||
const err = new KeySystemError(reason.message, keyInfo.decryptdata.uri, reason.code, { code: reason.code, text: 'Failed to update with key response' }, this.systemString);
|
||
throw err;
|
||
}));
|
||
return forkJoin([processLicense, updateSession]).pipe(mapTo(undefined));
|
||
}
|
||
/*
|
||
The pssh has to be generated from the manifest:
|
||
1. Extract the keyID from the EXT-X-KEY tag (for example URI =“data:;base64,DZ/7Ld9qTnyTi92l+VPlBg==“)
|
||
2. Generate the initdata using generateInitData
|
||
|
||
generate Widevine pssh
|
||
0:3 : size of the 'pssh' box
|
||
4:7 : 'pssh'
|
||
8:11 : not set (all zeros)
|
||
12:27 : Widevine Key System (edef8ba9-79d6-4ace-a3c8-27dcd51d21ed)
|
||
28:31 : Widevine pssh data size (size of what follows)
|
||
32:51 : Protobuf-encoded Widevine pssh data
|
||
32 & 33 : (id = 0, wireType = int, value = 1) means this is using AES-CTR
|
||
34 & 35 : (id = 1, wireType = bytes, length = 16) size of the keyId
|
||
36:51 : KeyId bytes
|
||
*/
|
||
generateInitData(keyId, decryptdata /* , requestInfo*/) {
|
||
return { initData: decryptdata.pssh, initDataType: 'cenc' };
|
||
}
|
||
/**
|
||
* Generate a key renewal request
|
||
* @param keyInfo Info about the key we're requesting
|
||
* @returns {Observable<Uint8Array>} Promise returning the license challenge
|
||
**/
|
||
generateRequestInitialized(keyInfo) {
|
||
const keyuri = keyInfo.decryptdata.uri;
|
||
const challenge = keyInfo.licenseChallenge;
|
||
//this.logger.info(`[Keys] challenge create start uri=${this.hls.redactUrl(keyuri)} versions=${JSON.stringify(keyInfo.decryptdata.formatversions)}`);
|
||
this.logger.debug(`[Keys] challenge create start uri=${redactUrl(keyuri)} versions=${JSON.stringify(keyInfo.decryptdata.formatversions)}`);
|
||
keyInfo.requestInfo = undefined; // Don't need requestInfo anymore.
|
||
keyInfo.resolveState(KeyRequestState.GET_CHALLENGE, challenge);
|
||
return VOID;
|
||
}
|
||
// message
|
||
handleKeyMessage(event) {
|
||
if (this.isDestroying) {
|
||
this.logger.warn('In the middle of destroying, ignore key message');
|
||
return;
|
||
}
|
||
this.logger.info(`[Keys] ${this.systemString} message : ${event.messageType}`);
|
||
// Extract KeyUri and KeyInfo from the sessions info strored.
|
||
const session = event.target;
|
||
let keyuri = null;
|
||
let keyInfo = null;
|
||
if (session.sessionId in this.sessionIdToKeyUri) {
|
||
keyuri = this.sessionIdToKeyUri[session.sessionId];
|
||
keyInfo = this.keyUriToKeyInfo[keyuri];
|
||
}
|
||
else {
|
||
for (const [uri, info] of Object.entries(this.keyUriToKeyInfo)) {
|
||
if (info.session === session) {
|
||
keyuri = uri;
|
||
keyInfo = info;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (!keyInfo) {
|
||
this.logger.warn(`${this.systemString} empty keyuri and keyInfo`);
|
||
return;
|
||
}
|
||
const cmd = event.messageType;
|
||
switch (cmd) {
|
||
case 'license-request': {
|
||
// For widevine just base64 encode the challenge returned by the browser.
|
||
const challenge = new Uint8Array(event.message);
|
||
// Check demo/index.xml for more details on how exactly license is acquired. There is switch-case statement for LICENSE_CHALLENGE_CREATED
|
||
keyInfo.resolveState(KeyRequestState.GET_CHALLENGE, challenge);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
var WidevineKeySystem$1 = WidevineKeySystem;
|
||
|
||
/*
|
||
* Factory for making a key system
|
||
*
|
||
*
|
||
*/
|
||
const kKeySystemIdToPropertiesMap$1 = {
|
||
clearkey: ClearKeySystemProperties,
|
||
fairplaystreaming: FairPlayStreamingKeySystemProperties,
|
||
playready: PlayReadyKeySystemProperties,
|
||
widevine: WidevineKeySystemProperties,
|
||
};
|
||
const kKeySystemIdToConstructorsMap = {
|
||
clearkey: [['org.w3.clearkey', ClearKeySystem$1]],
|
||
fairplaystreaming: [
|
||
['com.apple.fps.3_0', FPSKeySystemV3],
|
||
['com.apple.fps', FPSKeySystem],
|
||
],
|
||
playready: [['com.microsoft.playready.recommendation', PlayReadyKeySystem$1]],
|
||
widevine: [['com.widevine.alpha', WidevineKeySystem$1]],
|
||
};
|
||
const DEFAULT_KEY_SYSTEM = ClearKeySystemProperties.id;
|
||
/**
|
||
* Generate a KeySystem object given a KeySystemProperties
|
||
*/
|
||
class KeySystemFactory$1 {
|
||
createMediaKeys(keysystemId, videoCodecs, audioCodecs, logger, keySystemConfig) {
|
||
let res = KeySystemFactory$1.idToMediaKeysInfoMap[keysystemId];
|
||
if (!res) {
|
||
logger.info(`[Keys] ${keysystemId} requesting key system access`);
|
||
const mediaCapabilities = keysystemutil.getCapabilities(videoCodecs, audioCodecs);
|
||
const mediaKeys$ = new AsyncSubject();
|
||
KeySystemFactory$1.requestKeySystemAccess(keysystemId, mediaCapabilities, keySystemConfig, logger)
|
||
.pipe(switchMap(function (keySystemAccess) {
|
||
KeySystemFactory$1.idToMediaKeysInfoMap[keysystemId].keySystemAccess = keySystemAccess;
|
||
// TODO: probably should use keySystemAccess.getConfiguration() to get supported types.
|
||
logger.info(`[Keys] ${keysystemId} creating CDM`);
|
||
return from(keySystemAccess.createMediaKeys());
|
||
}), tap((mediaKeys) => {
|
||
logger.info(`[Keys] ${keysystemId} created CDM`);
|
||
mediaKeys$.next(mediaKeys);
|
||
mediaKeys$.complete();
|
||
}), catchError((err) => {
|
||
logger.info(`[Keys] FAIL: could not initialize ${keysystemId} key system: ${err.message}`);
|
||
mediaKeys$.error(new KeySystemError(`could not initialize key system: ${err.message}`, undefined, 0, ErrorResponses.KeySystemFailedToInitialize, keysystemId));
|
||
return EMPTY;
|
||
}), takeUntil(KeySystemFactory$1.destroy$) // Keep alive until destroyMediaKeys
|
||
)
|
||
.subscribe();
|
||
res = KeySystemFactory$1.idToMediaKeysInfoMap[keysystemId] = { mediaKeys$, keySystemAccess: null };
|
||
}
|
||
return res.mediaKeys$;
|
||
}
|
||
destroyMediaKeys() {
|
||
// Remove keysystemstring => requestKeySystemAccess promise
|
||
KeySystemFactory$1.destroy$.next();
|
||
KeySystemFactory$1.idToMediaKeysInfoMap = {};
|
||
}
|
||
static getKeySystemIdForDecryptData(decryptdata) {
|
||
let selectedId;
|
||
if (decryptdata) {
|
||
selectedId = DEFAULT_KEY_SYSTEM;
|
||
const formatString = decryptdata.format;
|
||
for (const id of KeySystemIdValues) {
|
||
const p = kKeySystemIdToPropertiesMap$1[id];
|
||
if ((p === null || p === void 0 ? void 0 : p.keyFormatString) === formatString) {
|
||
selectedId = id;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if (!selectedId) {
|
||
throw Error('No matching key system');
|
||
}
|
||
return selectedId;
|
||
}
|
||
// Pick the first key system that works for this id
|
||
static requestKeySystemAccess(keysystemId, mediaCapabilities, keySystemConfig, logger) {
|
||
if (typeof navigator === 'undefined' || typeof navigator.requestMediaKeySystemAccess === 'undefined') {
|
||
return throwError(new KeySystemError('navigator undefined', undefined, 0, ErrorResponses.KeySystemUndefinedNavigator, keysystemId));
|
||
}
|
||
const keySystemConstructors = kKeySystemIdToConstructorsMap[keysystemId];
|
||
let obs = throwError(new KeySystemError('no key systems to try', undefined, 0, ErrorResponses.KeySystemNoKeySystemsToTry, keysystemId));
|
||
for (const pair of keySystemConstructors) {
|
||
const keySystemString = pair[0];
|
||
const KeySystemConstructor = pair[1];
|
||
const accesConfig = isMediaKeySystemConfig(keySystemConfig) ? keySystemConfig : KeySystemConstructor.requestAccessConfig;
|
||
const options = [Object.assign({}, accesConfig, mediaCapabilities)];
|
||
obs = obs.pipe(catchError(() => {
|
||
return KeySystemFactory$1.requestKeySystemInternal(keySystemString, options, logger);
|
||
}));
|
||
}
|
||
return obs;
|
||
}
|
||
/**
|
||
* @param keySystemString keySystemString passed to requestMediaKeySystemAccess. e.g. 'com.apple.fps'
|
||
* @param options MediaKeySystemConfiguration passed to requestMediaKeySystemAccess
|
||
*/
|
||
static requestKeySystemInternal(keySystemString, options, logger) {
|
||
logger.info(`[Keys] requestKeySystemAccess ${keySystemString} ${JSON.stringify(options)}`);
|
||
return defer(() => from(navigator.requestMediaKeySystemAccess(keySystemString, options)));
|
||
}
|
||
/**
|
||
* Create KeySystem object
|
||
*
|
||
* @param keysystemId Identifier for key system used by HLS.js. e.g. 'fairplaystreaming' 'widevine'
|
||
* @param hls Hls object
|
||
* @param mediaKeys MediaKeys for KeySystem to manage
|
||
* @param keyLoader The loader object
|
||
*/
|
||
make(keysystemId, mediaKeys, config, eventEmitter, sessionHandler, logger) {
|
||
var _a;
|
||
const keySystemAccess = (_a = KeySystemFactory$1.idToMediaKeysInfoMap[keysystemId]) === null || _a === void 0 ? void 0 : _a.keySystemAccess;
|
||
if (!keySystemAccess) {
|
||
throw new KeySystemError(`No keySystemAccess for ${keysystemId}`, undefined, 0, ErrorResponses.KeySystemNoKeySystemAccess, keysystemId);
|
||
}
|
||
let KeySystemConstructor;
|
||
// NOTE: this system string is used in the LICENSE_CHALLENGE_CREATED event for playready and widevine
|
||
// as the "keysystem" parameter. Change with caution.
|
||
const systemString = kKeySystemIdToPropertiesMap$1[keysystemId].systemStringPrefix;
|
||
// Find the matching constructor for the key system string
|
||
for (const pair of kKeySystemIdToConstructorsMap[keysystemId]) {
|
||
if (pair[0] === keySystemAccess.keySystem) {
|
||
KeySystemConstructor = pair[1];
|
||
break;
|
||
}
|
||
}
|
||
if (!KeySystemConstructor) {
|
||
throw new KeySystemError(`No constructor associated with ${keysystemId}`, undefined, 0, ErrorResponses.KeySystemNoConstructor, systemString);
|
||
}
|
||
const keySystem = new KeySystemConstructor(mediaKeys, systemString, config, eventEmitter, sessionHandler, logger);
|
||
return keySystem;
|
||
}
|
||
static get availableKeySystems() {
|
||
return Object.keys(kKeySystemIdToPropertiesMap$1);
|
||
}
|
||
/**
|
||
* Get the EXT-X-KEY:FORMAT value associated with the key system
|
||
* @param keySystemId Identifier string to request keyFormatString property for
|
||
*/
|
||
static getKeySystemFormat(keySystemId) {
|
||
const properties = kKeySystemIdToPropertiesMap$1[keySystemId];
|
||
return properties ? properties.keyFormatString : '';
|
||
}
|
||
static getKeySystemSecurityLevel(keySystemId) {
|
||
const properties = kKeySystemIdToPropertiesMap$1[keySystemId];
|
||
return properties ? properties.securityLevels : undefined;
|
||
}
|
||
}
|
||
KeySystemFactory$1.idToMediaKeysInfoMap = {};
|
||
KeySystemFactory$1.destroy$ = new Subject();
|
||
|
||
// Lets set the max slots as couple of slots less than the normally allowed max slot of 8
|
||
const MAX_ALLOWED_KEY_SLOTS = 6;
|
||
const kMediaKeySystemConfigCodecs = {
|
||
video: ['avc1.42E01E'],
|
||
audio: ['mp4a.40.2'],
|
||
};
|
||
function convertError(keyUri, error, mediaOptionIds) {
|
||
// Convert error if needed
|
||
if (!error) {
|
||
error = new KeyRequestError('Unknown error from CDM', keyUri, 0, ErrorResponses.KeySystemCDMUnknownError, false, KeyRequestErrorReason.InternalError);
|
||
}
|
||
else if (error instanceof TimeoutError) {
|
||
error = new KeyRequestTimeoutError('Key request timed out', keyUri, ErrorResponses.KeySystemRequestTimedOut);
|
||
}
|
||
else if (error instanceof KeySystemError) {
|
||
const response = (error === null || error === void 0 ? void 0 : error.response) || ErrorResponses.InternalError;
|
||
error = new KeyRequestError(error.message, keyUri, 0, response, false, KeyRequestErrorReason.InternalError, true);
|
||
}
|
||
if (error instanceof KeyRequestError || error instanceof KeyRequestTimeoutError) {
|
||
error.mediaOptionIds = [...mediaOptionIds];
|
||
}
|
||
return error;
|
||
}
|
||
class KeySystemAdapter {
|
||
constructor(ksService, mediaSink, config, platformQuery, eventEmitter, sessionHandler, logger, keySystemFactory = new KeySystemFactory$1() // For testing
|
||
) {
|
||
this.ksService = ksService;
|
||
this.mediaSink = mediaSink;
|
||
this.config = config;
|
||
this.platformQuery = platformQuery;
|
||
this.eventEmitter = eventEmitter;
|
||
this.sessionHandler = sessionHandler;
|
||
this.keySystemFactory = keySystemFactory;
|
||
this.reset$ = new Subject();
|
||
this.keyRequest$ = new Subject();
|
||
this.abort$ = new Subject(); // Abort(keyuri)
|
||
this.keySystem$ = new BehaviorSubject(null);
|
||
this._keyStatusChange$ = new Subject();
|
||
this.protectionData = {};
|
||
this.keySystemId = null; // Identifier for keySystem
|
||
this.keyUriToRequest = {}; // keyUri => request observable
|
||
this.ksQuery = ksService.getQuery();
|
||
this.logger = logger.child({ name: 'eme' });
|
||
if (this.config.warmupCdms) {
|
||
this.keySystemFactory.createMediaKeys(FairPlayStreamingKeySystemProperties.id, kMediaKeySystemConfigCodecs.video, kMediaKeySystemConfigCodecs.audio, this.logger, undefined).subscribe();
|
||
}
|
||
const platformInfoChange$ = platformQuery.platformInfo$.pipe(distinctUntilChanged((a, b) => a && b && a.requiresCDMAttachOnStart === b.requiresCDMAttachOnStart), switchMap((platformInfo) => {
|
||
if (platformInfo === null || platformInfo === void 0 ? void 0 : platformInfo.requiresCDMAttachOnStart) {
|
||
return this.attachMediaKeys().pipe(catchError((err) => {
|
||
this.logger.info(`onMediaAttach err:${err.message}`);
|
||
this.handleKeySystemError(err);
|
||
return EMPTY;
|
||
}));
|
||
}
|
||
else {
|
||
return VOID;
|
||
}
|
||
}), switchMapTo(EMPTY));
|
||
// This is where the key requests actually get subscribed
|
||
const keyRequests$ = this.keyRequest$.pipe(mergeMap((request) => {
|
||
return request.pipe(catchError(() => {
|
||
// In band error gets bubbled up to caller
|
||
return EMPTY;
|
||
}));
|
||
}));
|
||
const keyStatusChange$ = this.keySystem$.pipe(switchMap((ks) => {
|
||
if (!ks) {
|
||
return EMPTY;
|
||
}
|
||
return ks.keyStatusChange$.pipe(tap((event) => {
|
||
var _a;
|
||
const keyUri = event.decryptdata.uri;
|
||
this.logger.info(`key status change uri=${redactUrl(keyUri)} status=${event.status}`);
|
||
const entity = this.ksQuery.getKeyInfo(keyUri);
|
||
if (event.status === 'needs-renewal') {
|
||
this.ksService.updateKeyRequestState(keyUri, KeyRequestMacroState.MustRequestResponse, (state) => state === KeyRequestMacroState.GotKeyResponse);
|
||
}
|
||
else {
|
||
const error = convertError(keyUri, event.error, (_a = entity === null || entity === void 0 ? void 0 : entity.mediaOptionIds) !== null && _a !== void 0 ? _a : []);
|
||
this.ksService.setError(keyUri, error);
|
||
}
|
||
this._keyStatusChange$.next(event);
|
||
}));
|
||
}));
|
||
const bufferedKeyHandling$ = this.isKeyCleanupSupported()
|
||
? this.mediaSink.mediaQuery.bufferedSegmentsTuple$.pipe(mergeMap((bufferedSegments) => {
|
||
const [videoSegments, audioSegments] = bufferedSegments;
|
||
const bufferedKeyUris = new Set();
|
||
videoSegments.forEach((bufferedSegment) => {
|
||
var _a, _b;
|
||
const uri = (_b = (_a = bufferedSegment.frag) === null || _a === void 0 ? void 0 : _a.keyTagInfo) === null || _b === void 0 ? void 0 : _b.uri;
|
||
if (uri) {
|
||
bufferedKeyUris.add(uri);
|
||
}
|
||
});
|
||
audioSegments.forEach((bufferedSegment) => {
|
||
var _a, _b;
|
||
const uri = (_b = (_a = bufferedSegment.frag) === null || _a === void 0 ? void 0 : _a.keyTagInfo) === null || _b === void 0 ? void 0 : _b.uri;
|
||
if (uri) {
|
||
bufferedKeyUris.add(uri);
|
||
}
|
||
});
|
||
return this.handleKeyCleanup(bufferedKeyUris);
|
||
}))
|
||
: VOID;
|
||
merge(platformInfoChange$, keyRequests$, keyStatusChange$, bufferedKeyHandling$).pipe(takeUntil(this.reset$)).subscribe();
|
||
}
|
||
get keyStatusChange$() {
|
||
return this._keyStatusChange$;
|
||
}
|
||
get keySystem() {
|
||
return this.keySystem$.value;
|
||
}
|
||
destroy() {
|
||
this.reset$.next(); // Stop all requests & stop listening to events
|
||
this.ksService.removeAll(); // Clear state
|
||
this.keySystemId = null;
|
||
const keySystem = this.keySystem;
|
||
let ksDestroyed$ = VOID;
|
||
if (keySystem) {
|
||
this.keySystem$.next(null);
|
||
if (keySystem.shouldDestroyMediaKeys) {
|
||
this.keySystemFactory.destroyMediaKeys();
|
||
}
|
||
ksDestroyed$ = keySystem.destroy();
|
||
}
|
||
return combineLatest([ksDestroyed$, this.mediaSink.clearMediaKeys()]).pipe(mapTo(undefined));
|
||
}
|
||
attachMediaKeys() {
|
||
if (this.keySystem) {
|
||
// we already wait inside makeKeySystem. nothing to do. Could happen if
|
||
// detach happened after keysystem was created (recoverMediaError?)
|
||
return VOID;
|
||
}
|
||
else {
|
||
// Create and attach key system.
|
||
// Use 'NONE' so we don't get assigned a keyId for no reason
|
||
const keyFormatString = this.config.keySystemPreference ? KeySystemFactory$1.getKeySystemFormat(this.config.keySystemPreference) : FairPlayStreamingKeySystemProperties.keyFormatString;
|
||
return this.makeKeySystem(new DecryptData('NONE', null, null, keyFormatString, [1])).pipe(mapTo(void 0));
|
||
}
|
||
}
|
||
isKeyCleanupSupported() {
|
||
// At this point of time we have two configurations for fairplay. One with single key session and one with multiple key sessions.
|
||
// In case of single key session, we cannot remove the session or key. The cleanup is supported only when multiple key sessions are supported
|
||
// Till the app enables that configuration, we should not call cleanup
|
||
// By default PlayReady and Widevine use multiple key sessions. But app doesn't pass useMultipleKeySessions in this case. So check for the preference and enable it based on that.
|
||
return this.config.useMultipleKeySessions === true || this.config.keySystemPreference === 'widevine' || this.config.keySystemPreference === 'playready';
|
||
}
|
||
handleKeyCleanup(bufferedKeyUris) {
|
||
if (this.ksQuery.getCount() < MAX_ALLOWED_KEY_SLOTS) {
|
||
return VOID;
|
||
}
|
||
const currentTime = performance.now();
|
||
const keyEntities = this.ksQuery.getAll();
|
||
const removes$ = keyEntities.map((ksEntity) => {
|
||
const keyUri = ksEntity.keyUri;
|
||
if (!bufferedKeyUris.has(keyUri)) {
|
||
const keyTagInfo = entityToKeyTagInfo(ksEntity.decryptdata);
|
||
// We don't need to key cleanup for AES-128 and also check if the key has met the minimum holdtime
|
||
if (keyTagInfo.method !== 'AES-128' && currentTime > ksEntity.minHoldTime) {
|
||
// Remove the entry from store, so that it's no longer tracked
|
||
return this._removeKey(keyUri, keyTagInfo);
|
||
}
|
||
}
|
||
return VOID;
|
||
});
|
||
return removes$.length ? forkJoin(removes$).pipe(switchMapTo(VOID)) : VOID;
|
||
}
|
||
_removeKey(keyUri, keyTagInfo) {
|
||
this.abort$.next(keyUri);
|
||
this.ksService.removeKey(keyUri);
|
||
return this.keySystem.removeKey(keyTagInfo);
|
||
}
|
||
removeKeysForItems(ids) {
|
||
const removes$ = [];
|
||
applyTransaction(() => {
|
||
for (const itemId of ids) {
|
||
this.ksService.removeAllKeysForItem(itemId);
|
||
const keyInfos = this.ksQuery.getAll({
|
||
filterBy: (entity) => entity.itemIds.length === 0,
|
||
});
|
||
for (const keyInfo of keyInfos) {
|
||
removes$.push(this._removeKey(keyInfo.keyUri, entityToKeyTagInfo(keyInfo.decryptdata)));
|
||
}
|
||
}
|
||
});
|
||
return removes$.length ? forkJoin(removes$).pipe(switchMap(() => VOID)) : VOID;
|
||
}
|
||
get availableKeySystems() {
|
||
return KeySystemFactory$1.availableKeySystems;
|
||
}
|
||
initialize(config) {
|
||
var _a;
|
||
const oldProtectionData = this.protectionData;
|
||
this.protectionData = {};
|
||
const preference = this.config.keySystemPreference;
|
||
for (const keySystemId of KeySystemFactory$1.availableKeySystems) {
|
||
const keysystemConfig = config[keySystemId];
|
||
if (!keysystemConfig) {
|
||
continue;
|
||
}
|
||
if (preference !== keySystemId) {
|
||
this.logger.warn(`Key system ${keySystemId} does not match preference ${preference}, ignoring`);
|
||
continue;
|
||
}
|
||
this.logger.info(`Setting protectionData for ${keySystemId}`);
|
||
const cert = keysystemConfig.certificate;
|
||
const serverCertUrl = keysystemConfig.serverCertUrl ? URLToolkit$1.buildAbsoluteURL(window.location.href, keysystemConfig.serverCertUrl) : undefined;
|
||
this.protectionData[keySystemId] = {
|
||
serverCertUrl: serverCertUrl,
|
||
certificate: cert,
|
||
};
|
||
let loadCert$;
|
||
if (cert) {
|
||
this.logger.info('Got cert');
|
||
loadCert$ = of({ keysystem: keySystemId, certificate: cert });
|
||
}
|
||
else if (serverCertUrl && ((_a = oldProtectionData === null || oldProtectionData === void 0 ? void 0 : oldProtectionData[keySystemId]) === null || _a === void 0 ? void 0 : _a.serverCertUrl) !== serverCertUrl) {
|
||
this.logger.info(`Loading cert ${redactUrl(serverCertUrl)} `);
|
||
let loaderConfig;
|
||
if (isCustomUrl(serverCertUrl)) {
|
||
loaderConfig = this.config.certLoadPolicy.customURL;
|
||
}
|
||
else {
|
||
loaderConfig = this.config.certLoadPolicy.default;
|
||
}
|
||
loadCert$ = fromUrlArrayBuffer({ url: serverCertUrl, xhrSetup: this.config.xhrSetup }, loaderConfig).pipe(map(([loadArrayBufferResult, _]) => {
|
||
return { keysystem: keySystemId, certificate: new Uint8Array(loadArrayBufferResult) };
|
||
}));
|
||
}
|
||
if (loadCert$) {
|
||
loadCert$
|
||
.pipe(switchMap((data) => this.onServerCertificateLoaded(data)), catchError((err) => {
|
||
this.logger.error(`Error loading cert: ${err.message}`);
|
||
this.eventEmitter.trigger(HlsEvent$1.INTERNAL_ERROR, {
|
||
type: ErrorTypes.NETWORK_ERROR,
|
||
details: ErrorDetails.CERT_LOAD_ERROR,
|
||
fatal: false,
|
||
handled: true,
|
||
reason: 'Error handling cert',
|
||
response: ErrorResponses.KeySystemCertificateLoadError,
|
||
message: err.message,
|
||
name: 'certificateLoadError',
|
||
});
|
||
// TODO: Save the error and reject any key requests relying on this.
|
||
throw err;
|
||
}), takeUntil(this.reset$))
|
||
.subscribe();
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* Generate challenge using request info
|
||
*/
|
||
generateRequest(keyuri, requestInfo) {
|
||
if (this.keySystem) {
|
||
this.keySystem.setKeyRequestInfo(keyuri, requestInfo);
|
||
}
|
||
}
|
||
/**
|
||
* Set the challenge response
|
||
*/
|
||
setLicenseResponse(keyuri, response) {
|
||
if (this.keySystem) {
|
||
this.keySystem.setParsedResponse(keyuri, response);
|
||
}
|
||
}
|
||
/**
|
||
* Called from externally to retrieve key.
|
||
* @param decryptdata Info about the key to retrieve
|
||
* @param mediaOptionId Id of level associated with this key request. Used for error handling
|
||
* @returns Observable of type DecryptData. Will signal on success, or throw an Error
|
||
**/
|
||
getKeyFromDecryptData(decryptdata, mediaOptionKey) {
|
||
if (!decryptdata || !decryptdata.isEncrypted) {
|
||
return of(decryptdata); // Return immediately
|
||
}
|
||
let obs;
|
||
applyTransaction(() => {
|
||
obs = this._getKeyFromDecryptData(decryptdata, mediaOptionKey);
|
||
});
|
||
return obs;
|
||
}
|
||
_getKeyFromDecryptData(decryptdata, mediaOptionKey) {
|
||
let itemId = null;
|
||
let mediaOptionId = null;
|
||
if (mediaOptionKey) {
|
||
itemId = mediaOptionKey.itemId;
|
||
mediaOptionId = mediaOptionKey.mediaOptionId;
|
||
}
|
||
const keyUri = decryptdata.uri;
|
||
const keyEntity = this.ksQuery.getKeyInfo(keyUri);
|
||
if (keyEntity && mediaOptionKey != null) {
|
||
this.ksService.addMediaOption(keyUri, mediaOptionKey);
|
||
}
|
||
const permanentError = (keyEntity === null || keyEntity === void 0 ? void 0 : keyEntity.error) instanceof KeyRequestError && keyEntity.error.isOkToRetry === false;
|
||
if (permanentError) {
|
||
return throwError(keyEntity.error);
|
||
}
|
||
else if (!keyEntity || keyEntity.requestState === KeyRequestMacroState.MustRequestResponse) {
|
||
this.logger.info(`getKeyFromDecryptData mediaOptionId=${mediaOptionId} method=${decryptdata.method} keyuri=${redactUrl(keyUri)} state=${keyEntity === null || keyEntity === void 0 ? void 0 : keyEntity.requestState}`);
|
||
// specify minimum duration a key may exist in the system before it’s eligible for eviction
|
||
const minHoldTime = performance.now() + this.config.keyMinHoldTimeBeforeCleanup;
|
||
// Abort any old requests
|
||
this.abort$.next(keyUri);
|
||
this.ksService.upsertKey({
|
||
keyUri,
|
||
decryptdata: keyTagInfoToEntity(decryptdata),
|
||
minHoldTime,
|
||
mediaOptionIds: [mediaOptionId],
|
||
requestState: KeyRequestMacroState.WaitingForKeyResponse,
|
||
itemIds: [itemId],
|
||
});
|
||
// Now start fetching key
|
||
let request;
|
||
const method = decryptdata.method;
|
||
switch (method) {
|
||
case 'SAMPLE-AES':
|
||
case 'ISO-23001-7':
|
||
case 'SAMPLE-AES-CTR': {
|
||
const loadPolicy = this.config.keyLoadPolicy.customURL;
|
||
request = this.fetchKeyEME(decryptdata).pipe(timeout(loadPolicy.maxLoadTimeMs));
|
||
break;
|
||
}
|
||
case 'AES-128':
|
||
request = this.fetchKeyHTTP(decryptdata.uri, decryptdata, this.config.keyLoadPolicy);
|
||
break;
|
||
default:
|
||
const err = new ExceptionError(false, `Unexpected METHOD attribute ${method}`, ErrorResponses.KeySystemUnexpectedMETHOD);
|
||
return throwError(err);
|
||
}
|
||
// TODO: stats
|
||
const request$ = (this.keyUriToRequest[keyUri] = request.pipe(map((keyLoadedData) => {
|
||
const decryptdata = keyLoadedData.decryptdata;
|
||
this.ksService.updateKeyValue(keyUri, decryptdata.key);
|
||
this.eventEmitter.trigger(HlsEvent$1.KEY_LOADED, keyLoadedData);
|
||
return keyLoadedData.decryptdata;
|
||
}), catchError((err) => {
|
||
var _a;
|
||
const keyEntity = this.ksQuery.getKeyInfo(keyUri);
|
||
err = convertError(keyUri, err, (_a = keyEntity === null || keyEntity === void 0 ? void 0 : keyEntity.mediaOptionIds) !== null && _a !== void 0 ? _a : []);
|
||
this.ksService.setError(keyUri, err);
|
||
return throwError(err);
|
||
}), finalize$1(() => {
|
||
// Update state for abort() only if we were in progress
|
||
this.ksService.updateKeyRequestState(keyUri, KeyRequestMacroState.MustRequestResponse, (state) => state === KeyRequestMacroState.WaitingForKeyResponse);
|
||
this.keyUriToRequest[keyUri] = null;
|
||
this.logger.debug(`${printKeyTag(decryptdata)} finalize state=${this.ksQuery.getKeyInfo(keyUri).requestState}`);
|
||
}), share(), takeUntil(race(this.abort$.pipe(filter((uri) => uri === keyUri)), this.reset$).pipe(tap((uri) => this.logger.warn(uri ? `aborted ${redactUrl(uri)}` : 'got reset'))))));
|
||
this.keyRequest$.next(request$); // Subscribes to key request
|
||
return request$;
|
||
}
|
||
else if (keyEntity.requestState === KeyRequestMacroState.GotKeyResponse) {
|
||
return of(entityToKeyTagInfo(keyEntity.decryptdata));
|
||
}
|
||
// requestState === KeyRequestMacroState.WaitingForKeyResponse
|
||
return this.keyUriToRequest[keyUri];
|
||
}
|
||
fetchKeyEME(decryptdata) {
|
||
return this.requestKey(decryptdata).pipe(map((decryptdata) => {
|
||
const now = performance.now();
|
||
const data = {
|
||
timestamp: now,
|
||
keyuri: decryptdata.uri,
|
||
decryptdata,
|
||
/*stats: {
|
||
trequest: keyInfo.stats.trequest,
|
||
tfirst: keyInfo.stats.tfirst,
|
||
tload: now
|
||
}*/
|
||
};
|
||
return data;
|
||
}));
|
||
}
|
||
/**
|
||
* Make generic HTTP request for key
|
||
* @param uri The URL to make request to
|
||
* @param decryptdata Associated DecryptData object
|
||
* @param loadTimeoutMs
|
||
*/
|
||
fetchKeyHTTP(uri, decryptdata, loadPolicy) {
|
||
return KeySystemAdapter.fetchKeyHTTP(uri, this.config, decryptdata, loadPolicy);
|
||
}
|
||
static fetchKeyHTTP(uri, config, decryptdata, loadPolicy) {
|
||
const loadable = {
|
||
url: uri,
|
||
xhrSetup: config.xhrSetup,
|
||
};
|
||
return fromUrlArrayBuffer(loadable, getLoadConfig(loadable, loadPolicy)).pipe(map(([loadArrayBufferResult, _]) => {
|
||
decryptdata.key = new Uint8Array(loadArrayBufferResult);
|
||
const loadedData = {
|
||
decryptdata,
|
||
keyuri: uri,
|
||
timestamp: performance.now(),
|
||
/*stats: {
|
||
trequest: keyInfo.stats.trequest,
|
||
tfirst: keyInfo.stats.tfirst,
|
||
tload: now
|
||
}*/
|
||
};
|
||
return loadedData;
|
||
}));
|
||
}
|
||
requestKey(decryptdata) {
|
||
// Assumed called from key loader and that it already checked the method and that it's encrypted, etc.
|
||
this.logger.debug(`requestKey - uri=${redactUrl(decryptdata.uri)}`);
|
||
return this.makeKeySystem(decryptdata).pipe(switchMap((keySystem) => {
|
||
return keySystem.startKeyRequest(decryptdata);
|
||
}));
|
||
}
|
||
ensureKeySystem(mediaKeys) {
|
||
return defer(() => {
|
||
if (!this.keySystem && this.keySystemId) {
|
||
this.keySystem$.next(this.keySystemFactory.make(this.keySystemId, mediaKeys, this.config, this.eventEmitter, this.sessionHandler, this.logger));
|
||
const protData = this.protectionData && this.protectionData[this.keySystemId];
|
||
if (protData) {
|
||
return this.keySystem.setServerCertificate(protData.certificate).pipe(mapTo(this.keySystem));
|
||
}
|
||
}
|
||
return of(this.keySystem);
|
||
});
|
||
}
|
||
/**
|
||
* Make MediaKeys object for decryptdata and attach to current media element
|
||
* Also creates a KeySystem object
|
||
* @param decryptdata The KeyTagInfo object
|
||
*/
|
||
makeKeySystem(decryptdata) {
|
||
this.logger.debug(`makeKeySystem - uri=${redactUrl(decryptdata.uri)}`);
|
||
return this.ensureMediaKeys(decryptdata).pipe(switchMap((mediaKeys) => {
|
||
this.logger.debug(`setMediaKeys - uri=${redactUrl(decryptdata.uri)}`);
|
||
return this.mediaSink.setMediaKeys(mediaKeys).pipe(switchMap(() => {
|
||
this.logger.debug(`ensureKeySystem - uri=${redactUrl(decryptdata.uri)}`);
|
||
// Create KeySystem object
|
||
return this.ensureKeySystem(mediaKeys);
|
||
}));
|
||
}));
|
||
}
|
||
// Create MediaKeys object.
|
||
ensureMediaKeys(decryptdata) {
|
||
var _a;
|
||
const newKeySystemId = KeySystemFactory$1.getKeySystemIdForDecryptData(decryptdata);
|
||
if (this.keySystemId == null) {
|
||
this.keySystemId = newKeySystemId;
|
||
}
|
||
else if (this.keySystemId !== newKeySystemId) {
|
||
const err = new KeyRequestError(`New key system string does not match existing ${newKeySystemId} !== ${this.keySystemId}`, decryptdata.uri, 0, ErrorResponses.KeySystemUnmatchedString, false, KeyRequestErrorReason.InternalError);
|
||
return throwError(err);
|
||
}
|
||
// CreateMediaKeys will return the same promise for a given key system ID if it's already being created.
|
||
return this.keySystemFactory.createMediaKeys(this.keySystemId, kMediaKeySystemConfigCodecs.video, kMediaKeySystemConfigCodecs.audio, this.logger, (_a = this.platformQuery.platformInfo) === null || _a === void 0 ? void 0 : _a.keySystemConfig);
|
||
}
|
||
/**
|
||
*
|
||
* @param data { keysystem: keysystem identifier, certificate: the buffer containing the cert }
|
||
*/
|
||
onServerCertificateLoaded(data) {
|
||
const keySystemId = data.keysystem, certificate = data.certificate;
|
||
this.logger.info(`KeySystemAdapter: ${keySystemId} cert loaded ${certificate ? 'nonnull' : 'null'}`);
|
||
const protData = this.protectionData[keySystemId];
|
||
protData.certificate = certificate;
|
||
this.logger.debug(`current keySystemId: ${this.keySystemId}`);
|
||
if (this.keySystem && this.keySystemId === keySystemId) {
|
||
return this.keySystem.setServerCertificate(certificate).pipe(mapTo(undefined));
|
||
}
|
||
return VOID;
|
||
}
|
||
// Trigger error due to key system setup issue
|
||
handleKeySystemError(error) {
|
||
const keyError = new KeySystemError(error.message, undefined, undefined, ErrorResponses.KeySystemSetupError, undefined);
|
||
this.eventEmitter.trigger(HlsEvent$1.INTERNAL_ERROR, keyError);
|
||
}
|
||
}
|
||
|
||
class KeySystemQuery extends QueryEntity {
|
||
constructor(store) {
|
||
super(store);
|
||
}
|
||
/**
|
||
* @returns the key info for a particular key
|
||
*/
|
||
getKeyInfo(keyuri) {
|
||
const entity = this.getEntity(keyuri);
|
||
if (entity) {
|
||
// Populate with proper mediaOptionIds every time
|
||
return Object.assign(Object.assign({}, entity), { error: copyKeyError(entity.error, entity.mediaOptionIds) });
|
||
}
|
||
return null;
|
||
}
|
||
getKeyInfo$(keyuri) {
|
||
return this.selectEntity(keyuri).pipe(map((entity) => {
|
||
if (entity) {
|
||
// Populate with proper mediaOptionIds every time
|
||
return Object.assign(Object.assign({}, entity), { error: copyKeyError(entity.error, entity.mediaOptionIds) });
|
||
}
|
||
else {
|
||
return null;
|
||
}
|
||
}));
|
||
}
|
||
getKeyRequestState$(keyuri) {
|
||
return this.selectEntity(keyuri, (entity) => entity === null || entity === void 0 ? void 0 : entity.requestState);
|
||
}
|
||
/**
|
||
* @returns an observable that emits whenever the key status changes
|
||
*/
|
||
getKeyStatus$(keyuri) {
|
||
return this.selectEntity(keyuri, (entity) => entity === null || entity === void 0 ? void 0 : entity.status);
|
||
}
|
||
/**
|
||
* @returns an observable that emits whenever we got an error on a particular key
|
||
*/
|
||
getKeyError$(keyuri) {
|
||
return this.selectEntity(keyuri, (entity) => copyKeyError(entity === null || entity === void 0 ? void 0 : entity.error, entity === null || entity === void 0 ? void 0 : entity.mediaOptionIds)).pipe(filterNil);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief Backing store for the playback state
|
||
*/
|
||
class KeySystemStore extends EntityStore {
|
||
constructor() {
|
||
super({}, { name: 'key-system-store', idKey: 'keyUri', producerFn: produce_1 });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief Service that manages playback state
|
||
*/
|
||
class KeySystemService {
|
||
constructor(store) {
|
||
this.store = store;
|
||
}
|
||
getQuery() {
|
||
return new KeySystemQuery(this.store);
|
||
}
|
||
upsertKey(keyEntity) {
|
||
logAction('keys.upsert', keyEntity.keyUri);
|
||
const newItemIdSet = new Set(keyEntity.itemIds.filter((x) => x != null));
|
||
const newMediaOptionIdSet = new Set(keyEntity.mediaOptionIds.filter((x) => x != null));
|
||
this.store.upsert(keyEntity.keyUri,
|
||
// Update existing
|
||
(oldEntity) => {
|
||
const mergedEntity = Object.assign(Object.assign({}, oldEntity), keyEntity);
|
||
// Merge itemIds & mediaOptionIds
|
||
if ('itemIds' in oldEntity) {
|
||
for (const itemId of oldEntity.itemIds) {
|
||
newItemIdSet.add(itemId);
|
||
}
|
||
}
|
||
mergedEntity.itemIds = Array.from(newItemIdSet);
|
||
if ('mediaOptionIds' in oldEntity) {
|
||
for (const mediaOptionId of oldEntity.mediaOptionIds) {
|
||
newMediaOptionIdSet.add(mediaOptionId);
|
||
}
|
||
}
|
||
mergedEntity.mediaOptionIds = Array.from(newMediaOptionIdSet);
|
||
return mergedEntity;
|
||
},
|
||
// Create new entity
|
||
() => (Object.assign(Object.assign({}, keyEntity), { itemIds: Array.from(newItemIdSet), mediaOptionIds: Array.from(newMediaOptionIdSet) })));
|
||
}
|
||
removeKey(keyUri) {
|
||
logAction('keys.removeKey', keyUri);
|
||
this.store.remove(keyUri);
|
||
}
|
||
removeAllKeysForItem(itemId) {
|
||
logAction(`keys.removeAllKeysForItem ${itemId}`);
|
||
this.store.update(null, (entity) => {
|
||
// Remove itemid from itemId list
|
||
const idx = entity.itemIds.findIndex((id) => id === itemId);
|
||
if (idx >= 0) {
|
||
entity.itemIds.splice(idx, 1);
|
||
}
|
||
});
|
||
}
|
||
removeAll() {
|
||
logAction('keys.remove');
|
||
this.store.remove();
|
||
}
|
||
updateKeyValue(keyUri, key) {
|
||
logAction('keys.updateKeyValue', keyUri);
|
||
this.store.update(keyUri, (entity) => {
|
||
if (entity.decryptdata.keyBuf == null && key != null) {
|
||
entity.decryptdata.keyBuf = key.buffer;
|
||
}
|
||
entity.requestState = KeyRequestMacroState.GotKeyResponse;
|
||
});
|
||
}
|
||
updateKeyStatus(keyUri, status) {
|
||
logAction(`keys.updateKeyStatus ${status}`, keyUri);
|
||
this.store.update(keyUri, (entity) => {
|
||
entity.status = status;
|
||
});
|
||
}
|
||
updateKeyRequestState(keyUri, requestState, predicate) {
|
||
logAction(`keys.updateKeyRequestState ${requestState}`, keyUri);
|
||
this.store.update(keyUri, (entity) => {
|
||
if (!predicate || predicate(entity.requestState)) {
|
||
entity.requestState = requestState;
|
||
}
|
||
});
|
||
}
|
||
addMediaOption(keyUri, mediaOptionKey) {
|
||
const { itemId, mediaOptionId } = mediaOptionKey;
|
||
logAction(`keys.addMediaOption itemId: ${itemId}, mediaOptionId: ${mediaOptionId}`, keyUri);
|
||
this.store.update(keyUri, (entity) => {
|
||
if (mediaOptionId != null) {
|
||
if (entity.mediaOptionIds.every((id) => id !== mediaOptionId)) {
|
||
entity.mediaOptionIds.push(mediaOptionId);
|
||
}
|
||
}
|
||
if (itemId != null) {
|
||
if (entity.itemIds.every((id) => id !== itemId)) {
|
||
entity.itemIds.push(itemId);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
setError(keyUri, error) {
|
||
var _a;
|
||
logAction(`keys.setError ${(_a = error === null || error === void 0 ? void 0 : error.constructor) === null || _a === void 0 ? void 0 : _a.name}`, keyUri);
|
||
this.store.update(keyUri, (entity) => {
|
||
// Make a copy so that entity.error is not modified after storage
|
||
entity.error = copyKeyError(error);
|
||
entity.requestState = KeyRequestMacroState.MustRequestResponse;
|
||
});
|
||
}
|
||
}
|
||
// state for the service functions
|
||
const keySystemStore = new KeySystemStore();
|
||
let globalKeySystemService = null;
|
||
function keySystemService() {
|
||
if (!globalKeySystemService) {
|
||
globalKeySystemService = new KeySystemService(keySystemStore);
|
||
}
|
||
return globalKeySystemService;
|
||
}
|
||
const makeKeySystemService = (keySystemService, source$, itemRemove$, config, platformQuery, eventEmitter, sessionHandler, logger) => {
|
||
return source$.pipe(tag('[Keys] playback.keySystemServiceEpic.in'), switchMap((mediaSink) => {
|
||
if (!mediaSink) {
|
||
return of(null);
|
||
}
|
||
return new Observable((subscriber) => {
|
||
let keySystemAdapter = new KeySystemAdapter(keySystemService, mediaSink, config, platformQuery, eventEmitter, sessionHandler, logger);
|
||
const sub = merge(of(keySystemAdapter), itemRemove$.pipe(mergeMap((ids) => {
|
||
return keySystemAdapter.removeKeysForItems(ids);
|
||
}), switchMapTo(EMPTY))).subscribe(subscriber);
|
||
return function unsubscribe() {
|
||
logger.warn('[Keys] playback.keySystemServiceEpic.unsubscribe');
|
||
sub.unsubscribe();
|
||
keySystemAdapter.destroy().subscribe();
|
||
keySystemAdapter = undefined;
|
||
};
|
||
});
|
||
}), tag('[Keys] playback.keySystemServiceEpic.emit'));
|
||
};
|
||
|
||
function isAlternateMediaOption(option) {
|
||
return 'groupId' in option;
|
||
}
|
||
|
||
function safeAssignProperties(target, source) {
|
||
return Object.assign(target, source);
|
||
}
|
||
/**
|
||
* Exceptions from regular ASCII. CodePoints are mapped to UTF-16 codes
|
||
*/
|
||
const specialCea608CharsCodes = {
|
||
42: 225,
|
||
92: 233,
|
||
94: 237,
|
||
95: 243,
|
||
96: 250,
|
||
123: 231,
|
||
124: 247,
|
||
125: 209,
|
||
126: 241,
|
||
127: 9608,
|
||
// THIS BLOCK INCLUDES THE 16 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS
|
||
// THAT COME FROM HI BYTE=0x11 AND LOW BETWEEN 0x30 AND 0x3F
|
||
// THIS MEANS THAT \x50 MUST BE ADDED TO THE VALUES
|
||
128: 174,
|
||
129: 176,
|
||
130: 189,
|
||
131: 191,
|
||
132: 8482,
|
||
133: 162,
|
||
134: 163,
|
||
135: 9834,
|
||
136: 224,
|
||
137: 32,
|
||
138: 232,
|
||
139: 226,
|
||
140: 234,
|
||
141: 238,
|
||
142: 244,
|
||
143: 251,
|
||
// THIS BLOCK INCLUDES THE 32 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS
|
||
// THAT COME FROM HI BYTE=0x12 AND LOW BETWEEN 0x20 AND 0x3F
|
||
144: 193,
|
||
145: 201,
|
||
146: 211,
|
||
147: 218,
|
||
148: 220,
|
||
149: 252,
|
||
150: 8216,
|
||
151: 161,
|
||
152: 42,
|
||
153: 8217,
|
||
154: 9473,
|
||
155: 169,
|
||
156: 8480,
|
||
157: 8226,
|
||
158: 8220,
|
||
159: 8221,
|
||
160: 192,
|
||
161: 194,
|
||
162: 199,
|
||
163: 200,
|
||
164: 202,
|
||
165: 203,
|
||
166: 235,
|
||
167: 206,
|
||
168: 207,
|
||
169: 239,
|
||
170: 212,
|
||
171: 217,
|
||
172: 249,
|
||
173: 219,
|
||
174: 171,
|
||
175: 187,
|
||
// THIS BLOCK INCLUDES THE 32 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS
|
||
// THAT COME FROM HI BYTE=0x13 AND LOW BETWEEN 0x20 AND 0x3F
|
||
176: 195,
|
||
177: 227,
|
||
178: 205,
|
||
179: 204,
|
||
180: 236,
|
||
181: 210,
|
||
182: 242,
|
||
183: 213,
|
||
184: 245,
|
||
185: 123,
|
||
186: 125,
|
||
187: 92,
|
||
188: 94,
|
||
189: 95,
|
||
190: 124,
|
||
191: 8764,
|
||
192: 196,
|
||
193: 228,
|
||
194: 214,
|
||
195: 246,
|
||
196: 223,
|
||
197: 165,
|
||
198: 164,
|
||
199: 9475,
|
||
200: 197,
|
||
201: 229,
|
||
202: 216,
|
||
203: 248,
|
||
204: 9487,
|
||
205: 9491,
|
||
206: 9495,
|
||
207: 9499, // Box drawings heavy up and left
|
||
};
|
||
/**
|
||
* Utils
|
||
*/
|
||
const getCharForByte = function (byte) {
|
||
let charCode = byte;
|
||
// eslint-disable-next-line no-prototype-builtins
|
||
if (specialCea608CharsCodes.hasOwnProperty(byte)) {
|
||
charCode = specialCea608CharsCodes[byte];
|
||
}
|
||
return String.fromCharCode(charCode);
|
||
};
|
||
const NR_ROWS = 15, NR_COLS = 100;
|
||
// Tables to look up row from PAC data
|
||
const rowsLowCh1 = { 17: 1, 18: 3, 21: 5, 22: 7, 23: 9, 16: 11, 19: 12, 20: 14 };
|
||
const rowsHighCh1 = { 17: 2, 18: 4, 21: 6, 22: 8, 23: 10, 19: 13, 20: 15 };
|
||
const rowsLowCh2 = { 25: 1, 26: 3, 29: 5, 30: 7, 31: 9, 24: 11, 27: 12, 28: 14 };
|
||
const rowsHighCh2 = { 25: 2, 26: 4, 29: 6, 30: 8, 31: 10, 27: 13, 28: 15 };
|
||
const backgroundColors = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta', 'black', 'transparent'];
|
||
const logger = {
|
||
verboseFilter: { DATA: 3, DEBUG: 3, INFO: 2, WARNING: 2, TEXT: 1, ERROR: 0 },
|
||
time: null,
|
||
verboseLevel: 0,
|
||
setTime: function (newTime) {
|
||
this.time = newTime;
|
||
},
|
||
log: function (severity, msg) {
|
||
const minLevel = this.verboseFilter[severity];
|
||
if (this.verboseLevel >= minLevel) {
|
||
// eslint-disable-next-line no-console
|
||
console.log(this.time + ' [' + severity + '] ' + msg);
|
||
}
|
||
},
|
||
};
|
||
const numArrayToHexArray = function (numArray) {
|
||
const hexArray = [];
|
||
for (let j = 0; j < numArray.length; j++) {
|
||
hexArray.push(numArray[j].toString(16));
|
||
}
|
||
return hexArray;
|
||
};
|
||
class PenState {
|
||
constructor(foreground, underline, italics, background, flash) {
|
||
this.foreground = foreground || 'white';
|
||
this.underline = underline || false;
|
||
this.italics = italics || false;
|
||
this.background = background || 'black';
|
||
this.flash = flash || false;
|
||
}
|
||
reset() {
|
||
this.foreground = 'white';
|
||
this.underline = false;
|
||
this.italics = false;
|
||
this.background = 'black';
|
||
this.flash = false;
|
||
}
|
||
setStyles(styles) {
|
||
safeAssignProperties(this, styles);
|
||
}
|
||
isDefault() {
|
||
return this.foreground === 'white' && !this.underline && !this.italics && this.background === 'black' && !this.flash;
|
||
}
|
||
equals(other) {
|
||
return this.foreground === other.foreground && this.underline === other.underline && this.italics === other.italics && this.background === other.background && this.flash === other.flash;
|
||
}
|
||
copy(newPenState) {
|
||
this.foreground = newPenState.foreground;
|
||
this.underline = newPenState.underline;
|
||
this.italics = newPenState.italics;
|
||
this.background = newPenState.background;
|
||
this.flash = newPenState.flash;
|
||
}
|
||
toString() {
|
||
return 'color=' + this.foreground + ', underline=' + this.underline + ', italics=' + this.italics + ', background=' + this.background + ', flash=' + this.flash;
|
||
}
|
||
}
|
||
/**
|
||
* Unicode character with styling and background.
|
||
* @constructor
|
||
*/
|
||
class StyledUnicodeChar {
|
||
constructor(uchar, foreground, underline, italics, background, flash) {
|
||
this.uchar = uchar || ' '; // unicode character
|
||
this.penState = new PenState(foreground, underline, italics, background, flash);
|
||
}
|
||
reset() {
|
||
this.uchar = ' ';
|
||
this.penState.reset();
|
||
}
|
||
setChar(uchar, newPenState) {
|
||
this.uchar = uchar;
|
||
this.penState.copy(newPenState);
|
||
}
|
||
setPenState(newPenState) {
|
||
this.penState.copy(newPenState);
|
||
}
|
||
equals(other) {
|
||
return this.uchar === other.uchar && this.penState.equals(other.penState);
|
||
}
|
||
copy(newChar) {
|
||
this.uchar = newChar.uchar;
|
||
this.penState.copy(newChar.penState);
|
||
}
|
||
isEmpty() {
|
||
return this.uchar === ' ' && this.penState.isDefault();
|
||
}
|
||
}
|
||
/**
|
||
* CEA-608 row consisting of NR_COLS instances of StyledUnicodeChar.
|
||
* @constructor
|
||
*/
|
||
class Row {
|
||
constructor() {
|
||
this.chars = [];
|
||
for (let i = 0; i < NR_COLS; i++) {
|
||
this.chars.push(new StyledUnicodeChar());
|
||
}
|
||
this.pos = 0;
|
||
this.currPenState = new PenState();
|
||
}
|
||
equals(other) {
|
||
let equal = true;
|
||
for (let i = 0; i < NR_COLS; i++) {
|
||
if (!this.chars[i].equals(other.chars[i])) {
|
||
equal = false;
|
||
break;
|
||
}
|
||
}
|
||
return equal;
|
||
}
|
||
copy(other) {
|
||
for (let i = 0; i < NR_COLS; i++) {
|
||
this.chars[i].copy(other.chars[i]);
|
||
}
|
||
}
|
||
isEmpty() {
|
||
let empty = true;
|
||
for (let i = 0; i < NR_COLS; i++) {
|
||
if (!this.chars[i].isEmpty()) {
|
||
empty = false;
|
||
break;
|
||
}
|
||
}
|
||
return empty;
|
||
}
|
||
/**
|
||
* Set the cursor to a valid column.
|
||
*/
|
||
setCursor(absPos) {
|
||
if (this.pos !== absPos) {
|
||
this.pos = absPos;
|
||
}
|
||
if (this.pos < 0) {
|
||
logger.log('ERROR', 'Negative cursor position ' + this.pos);
|
||
this.pos = 0;
|
||
}
|
||
else if (this.pos > NR_COLS) {
|
||
logger.log('ERROR', 'Too large cursor position ' + this.pos);
|
||
this.pos = NR_COLS;
|
||
}
|
||
}
|
||
/**
|
||
* Move the cursor relative to current position.
|
||
*/
|
||
moveCursor(relPos) {
|
||
const newPos = this.pos + relPos;
|
||
if (relPos > 1) {
|
||
for (let i = this.pos + 1; i < newPos + 1; i++) {
|
||
this.chars[i].setPenState(this.currPenState);
|
||
}
|
||
}
|
||
this.setCursor(newPos);
|
||
}
|
||
/**
|
||
* Backspace, move one step back and clear character.
|
||
*/
|
||
backSpace() {
|
||
this.moveCursor(-1);
|
||
this.chars[this.pos].setChar(' ', this.currPenState);
|
||
}
|
||
insertChar(byte) {
|
||
if (byte >= 144) {
|
||
// Extended char
|
||
this.backSpace();
|
||
}
|
||
const char = getCharForByte(byte);
|
||
if (this.pos >= NR_COLS) {
|
||
logger.log('ERROR', 'Cannot insert ' + byte.toString(16) + ' (' + char + ') at position ' + this.pos + '. Skipping it!');
|
||
return;
|
||
}
|
||
this.chars[this.pos].setChar(char, this.currPenState);
|
||
this.moveCursor(1);
|
||
}
|
||
clearFromPos(startPos) {
|
||
let i;
|
||
for (i = startPos; i < NR_COLS; i++) {
|
||
this.chars[i].reset();
|
||
}
|
||
}
|
||
clear() {
|
||
this.clearFromPos(0);
|
||
this.pos = 0;
|
||
this.currPenState.reset();
|
||
}
|
||
clearToEndOfRow() {
|
||
this.clearFromPos(this.pos);
|
||
}
|
||
getTextString() {
|
||
const chars = [];
|
||
let empty = true;
|
||
for (let i = 0; i < NR_COLS; i++) {
|
||
const char = this.chars[i].uchar;
|
||
if (char !== ' ') {
|
||
empty = false;
|
||
}
|
||
chars.push(char);
|
||
}
|
||
if (empty) {
|
||
return '';
|
||
}
|
||
else {
|
||
return chars.join('');
|
||
}
|
||
}
|
||
setPenStyles(styles) {
|
||
this.currPenState.setStyles(styles);
|
||
const currChar = this.chars[this.pos];
|
||
currChar.setPenState(this.currPenState);
|
||
}
|
||
}
|
||
/**
|
||
* Keep a CEA-608 screen of 32x15 styled characters
|
||
* @constructor
|
||
*/
|
||
class CaptionScreen {
|
||
constructor() {
|
||
this.rows = [];
|
||
for (let i = 0; i < NR_ROWS; i++) {
|
||
this.rows.push(new Row()); // Note that we use zero-based numbering (0-14)
|
||
}
|
||
this.currRow = NR_ROWS - 1;
|
||
this.nrRollUpRows = null;
|
||
this.reset();
|
||
}
|
||
reset() {
|
||
for (let i = 0; i < NR_ROWS; i++) {
|
||
this.rows[i].clear();
|
||
}
|
||
this.currRow = NR_ROWS - 1;
|
||
}
|
||
equals(other) {
|
||
let equal = true;
|
||
for (let i = 0; i < NR_ROWS; i++) {
|
||
if (!this.rows[i].equals(other.rows[i])) {
|
||
equal = false;
|
||
break;
|
||
}
|
||
}
|
||
return equal;
|
||
}
|
||
copy(other) {
|
||
for (let i = 0; i < NR_ROWS; i++) {
|
||
this.rows[i].copy(other.rows[i]);
|
||
}
|
||
}
|
||
isEmpty() {
|
||
let empty = true;
|
||
for (let i = 0; i < NR_ROWS; i++) {
|
||
if (!this.rows[i].isEmpty()) {
|
||
empty = false;
|
||
break;
|
||
}
|
||
}
|
||
return empty;
|
||
}
|
||
backSpace() {
|
||
const row = this.rows[this.currRow];
|
||
row.backSpace();
|
||
}
|
||
clearToEndOfRow() {
|
||
const row = this.rows[this.currRow];
|
||
row.clearToEndOfRow();
|
||
}
|
||
/**
|
||
* Insert a character (without styling) in the current row.
|
||
*/
|
||
insertChar(char) {
|
||
const row = this.rows[this.currRow];
|
||
row.insertChar(char);
|
||
}
|
||
setPen(styles) {
|
||
const row = this.rows[this.currRow];
|
||
row.setPenStyles(styles);
|
||
}
|
||
moveCursor(relPos) {
|
||
const row = this.rows[this.currRow];
|
||
row.moveCursor(relPos);
|
||
}
|
||
setCursor(absPos) {
|
||
logger.log('INFO', 'setCursor: ' + absPos);
|
||
const row = this.rows[this.currRow];
|
||
row.setCursor(absPos);
|
||
}
|
||
setPAC(pacData) {
|
||
logger.log('INFO', 'pacData = ' + JSON.stringify(pacData));
|
||
let newRow = pacData.row - 1;
|
||
if (this.nrRollUpRows && newRow < this.nrRollUpRows - 1) {
|
||
newRow = this.nrRollUpRows - 1;
|
||
}
|
||
// Make sure this only affects Roll-up Captions by checking this.nrRollUpRows
|
||
if (this.nrRollUpRows && this.currRow !== newRow) {
|
||
// clear all rows first
|
||
for (let i = 0; i < NR_ROWS; i++) {
|
||
this.rows[i].clear();
|
||
}
|
||
// Copy this.nrRollUpRows rows from lastOutputScreen and place it in the newRow location
|
||
// topRowIndex - the start of rows to copy (inclusive index)
|
||
const topRowIndex = this.currRow + 1 - this.nrRollUpRows;
|
||
// We only copy if the last position was already shown.
|
||
// We use the cueStartTime value to check this.
|
||
const lastOutputScreen = this.lastOutputScreen;
|
||
if (lastOutputScreen) {
|
||
const prevLineTime = lastOutputScreen.rows[topRowIndex].cueStartTime;
|
||
if (prevLineTime && prevLineTime < logger.time) {
|
||
for (let i = 0; i < this.nrRollUpRows; i++) {
|
||
this.rows[newRow - this.nrRollUpRows + i + 1].copy(lastOutputScreen.rows[topRowIndex + i]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
this.currRow = newRow;
|
||
const row = this.rows[this.currRow];
|
||
if (pacData.indent !== null) {
|
||
const indent = pacData.indent;
|
||
const prevPos = Math.max(indent - 1, 0);
|
||
row.setCursor(pacData.indent);
|
||
pacData.color = row.chars[prevPos].penState.foreground;
|
||
}
|
||
const styles = { foreground: pacData.color, underline: pacData.underline, italics: pacData.italics, background: 'black', flash: false };
|
||
this.setPen(styles);
|
||
}
|
||
/**
|
||
* Set background/extra foreground, but first do back_space, and then insert space (backwards compatibility).
|
||
*/
|
||
setBkgData(bkgData) {
|
||
logger.log('INFO', 'bkgData = ' + JSON.stringify(bkgData));
|
||
this.backSpace();
|
||
this.setPen(bkgData);
|
||
this.insertChar(32); // Space
|
||
}
|
||
setRollUpRows(nrRows) {
|
||
this.nrRollUpRows = nrRows;
|
||
}
|
||
rollUp() {
|
||
if (this.nrRollUpRows === null) {
|
||
logger.log('DEBUG', 'roll_up but nrRollUpRows not set yet');
|
||
return; // Not properly setup
|
||
}
|
||
logger.log('INFO', 'TEXT ' + this.getDisplayText());
|
||
const topRowIndex = this.currRow + 1 - this.nrRollUpRows;
|
||
const topRow = this.rows.splice(topRowIndex, 1)[0];
|
||
topRow.clear();
|
||
this.rows.splice(this.currRow, 0, topRow);
|
||
logger.log('INFO', 'Rolling up');
|
||
}
|
||
/**
|
||
* Get all non-empty rows with as unicode text.
|
||
*/
|
||
getDisplayText(asOneRow) {
|
||
asOneRow = asOneRow || false;
|
||
const displayText = [];
|
||
let text = '';
|
||
let rowNr = -1;
|
||
for (let i = 0; i < NR_ROWS; i++) {
|
||
const rowText = this.rows[i].getTextString();
|
||
if (rowText) {
|
||
rowNr = i + 1;
|
||
if (asOneRow) {
|
||
displayText.push('Row ' + rowNr + ': \'' + rowText + '\'');
|
||
}
|
||
else {
|
||
displayText.push(rowText.trim());
|
||
}
|
||
}
|
||
}
|
||
if (displayText.length > 0) {
|
||
if (asOneRow) {
|
||
text = '[' + displayText.join(' | ') + ']';
|
||
}
|
||
else {
|
||
text = displayText.join('\n');
|
||
}
|
||
}
|
||
return text;
|
||
}
|
||
getTextAndFormat() {
|
||
return this.rows;
|
||
}
|
||
}
|
||
// var modes = ['MODE_ROLL-UP', 'MODE_POP-ON', 'MODE_PAINT-ON', 'MODE_TEXT'];
|
||
class Cea608Channel {
|
||
constructor(channelNumber, outputFilter) {
|
||
this.chNr = channelNumber;
|
||
this.outputFilter = outputFilter;
|
||
this.mode = null;
|
||
this.verbose = 0;
|
||
this.displayedMemory = new CaptionScreen();
|
||
this.nonDisplayedMemory = new CaptionScreen();
|
||
this.lastOutputScreen = new CaptionScreen();
|
||
this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1];
|
||
this.writeScreen = this.displayedMemory;
|
||
this.mode = null;
|
||
this.cueStartTime = null; // Keeps track of where a cue started.
|
||
}
|
||
reset() {
|
||
this.mode = null;
|
||
this.displayedMemory.reset();
|
||
this.nonDisplayedMemory.reset();
|
||
this.lastOutputScreen.reset();
|
||
this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1];
|
||
this.writeScreen = this.displayedMemory;
|
||
this.mode = null;
|
||
this.cueStartTime = null;
|
||
this.lastCueEndTime = null;
|
||
}
|
||
getHandler() {
|
||
return this.outputFilter;
|
||
}
|
||
setHandler(newHandler) {
|
||
this.outputFilter = newHandler;
|
||
}
|
||
setPAC(pacData) {
|
||
this.writeScreen.setPAC(pacData);
|
||
}
|
||
setBkgData(bkgData) {
|
||
this.writeScreen.setBkgData(bkgData);
|
||
}
|
||
setMode(newMode) {
|
||
if (newMode === this.mode) {
|
||
return;
|
||
}
|
||
this.mode = newMode;
|
||
logger.log('INFO', 'MODE=' + newMode);
|
||
if (this.mode === 'MODE_POP-ON') {
|
||
this.writeScreen = this.nonDisplayedMemory;
|
||
}
|
||
else {
|
||
this.writeScreen = this.displayedMemory;
|
||
this.writeScreen.reset();
|
||
}
|
||
if (this.mode !== 'MODE_ROLL-UP') {
|
||
this.displayedMemory.nrRollUpRows = null;
|
||
this.nonDisplayedMemory.nrRollUpRows = null;
|
||
}
|
||
this.mode = newMode;
|
||
}
|
||
insertChars(chars) {
|
||
for (let i = 0; i < chars.length; i++) {
|
||
this.writeScreen.insertChar(chars[i]);
|
||
}
|
||
const screen = this.writeScreen === this.displayedMemory ? 'DISP' : 'NON_DISP';
|
||
logger.log('INFO', screen + ': ' + this.writeScreen.getDisplayText(true));
|
||
if (this.mode === 'MODE_PAINT-ON' || this.mode === 'MODE_ROLL-UP') {
|
||
logger.log('TEXT', 'DISPLAYED: ' + this.displayedMemory.getDisplayText(true));
|
||
this.outputDataUpdate();
|
||
}
|
||
}
|
||
ccRCL() {
|
||
// Resume Caption Loading (switch mode to Pop On)
|
||
logger.log('INFO', 'RCL - Resume Caption Loading');
|
||
this.setMode('MODE_POP-ON');
|
||
}
|
||
ccBS() {
|
||
// BackSpace
|
||
logger.log('INFO', 'BS - BackSpace');
|
||
if (this.mode === 'MODE_TEXT') {
|
||
return;
|
||
}
|
||
this.writeScreen.backSpace();
|
||
if (this.writeScreen === this.displayedMemory) {
|
||
this.outputDataUpdate();
|
||
}
|
||
}
|
||
ccAOF() {
|
||
// Reserved (formerly Alarm Off)
|
||
return;
|
||
}
|
||
ccAON() {
|
||
// Reserved (formerly Alarm On)
|
||
return;
|
||
}
|
||
ccDER() {
|
||
// Delete to End of Row
|
||
logger.log('INFO', 'DER- Delete to End of Row');
|
||
this.writeScreen.clearToEndOfRow();
|
||
this.outputDataUpdate();
|
||
}
|
||
ccRU(nrRows) {
|
||
// Roll-Up Captions-2,3,or 4 Rows
|
||
logger.log('INFO', 'RU(' + nrRows + ') - Roll Up');
|
||
this.writeScreen = this.displayedMemory;
|
||
this.setMode('MODE_ROLL-UP');
|
||
this.writeScreen.setRollUpRows(nrRows);
|
||
}
|
||
ccFON() {
|
||
// Flash On
|
||
logger.log('INFO', 'FON - Flash On');
|
||
this.writeScreen.setPen({ flash: true });
|
||
}
|
||
ccRDC() {
|
||
// Resume Direct Captioning (switch mode to PaintOn)
|
||
logger.log('INFO', 'RDC - Resume Direct Captioning');
|
||
this.setMode('MODE_PAINT-ON');
|
||
}
|
||
ccTR() {
|
||
// Text Restart in text mode (not supported, however)
|
||
logger.log('INFO', 'TR');
|
||
this.setMode('MODE_TEXT');
|
||
}
|
||
ccRTD() {
|
||
// Resume Text Display in Text mode (not supported, however)
|
||
logger.log('INFO', 'RTD');
|
||
this.setMode('MODE_TEXT');
|
||
}
|
||
ccEDM() {
|
||
// Erase Displayed Memory
|
||
logger.log('INFO', 'EDM - Erase Displayed Memory');
|
||
this.displayedMemory.reset();
|
||
this.outputDataUpdate(true);
|
||
}
|
||
ccCR() {
|
||
// Carriage Return
|
||
logger.log('INFO', 'CR - Carriage Return');
|
||
this.writeScreen.rollUp();
|
||
this.outputDataUpdate(true);
|
||
}
|
||
ccENM() {
|
||
// Erase Non-Displayed Memory
|
||
logger.log('INFO', 'ENM - Erase Non-displayed Memory');
|
||
this.nonDisplayedMemory.reset();
|
||
}
|
||
ccEOC() {
|
||
// End of Caption (Flip Memories)
|
||
logger.log('INFO', 'EOC - End Of Caption');
|
||
if (this.mode === 'MODE_POP-ON') {
|
||
const tmp = this.displayedMemory;
|
||
this.displayedMemory = this.nonDisplayedMemory;
|
||
this.nonDisplayedMemory = tmp;
|
||
this.writeScreen = this.nonDisplayedMemory;
|
||
logger.log('TEXT', 'DISP: ' + this.displayedMemory.getDisplayText());
|
||
}
|
||
this.outputDataUpdate(true);
|
||
}
|
||
ccTO(nrCols) {
|
||
// Tab Offset 1,2, or 3 columns
|
||
logger.log('INFO', 'TO(' + nrCols + ') - Tab Offset');
|
||
this.writeScreen.moveCursor(nrCols);
|
||
}
|
||
ccMIDROW(secondByte) {
|
||
// Parse MIDROW command
|
||
const styles = { flash: false, underline: false, italics: false };
|
||
styles.underline = secondByte % 2 === 1;
|
||
styles.italics = secondByte >= 46;
|
||
if (!styles.italics) {
|
||
const colorIndex = Math.floor(secondByte / 2) - 16;
|
||
const colors = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta'];
|
||
styles.foreground = colors[colorIndex];
|
||
}
|
||
else {
|
||
styles.foreground = 'white';
|
||
}
|
||
logger.log('INFO', 'MIDROW: ' + JSON.stringify(styles));
|
||
this.writeScreen.setPen(styles);
|
||
}
|
||
outputDataUpdate(dispatch = false) {
|
||
const t = logger.time;
|
||
if (t === null) {
|
||
return;
|
||
}
|
||
if (this.outputFilter) {
|
||
if (this.outputFilter.updateData) {
|
||
this.outputFilter.updateData(t, this.displayedMemory);
|
||
}
|
||
if (this.cueStartTime === null && !this.displayedMemory.isEmpty()) {
|
||
// Start of a new cue
|
||
this.cueStartTime = t;
|
||
}
|
||
else {
|
||
if (!this.displayedMemory.equals(this.lastOutputScreen)) {
|
||
if (this.outputFilter.newCue) {
|
||
this.outputFilter.newCue(this.cueStartTime, t, this.lastOutputScreen);
|
||
if (dispatch === true && this.outputFilter.dispatchCue) {
|
||
this.outputFilter.dispatchCue();
|
||
}
|
||
}
|
||
this.cueStartTime = this.displayedMemory.isEmpty() ? null : t;
|
||
}
|
||
}
|
||
this.lastOutputScreen.copy(this.displayedMemory);
|
||
}
|
||
}
|
||
cueSplitAtTime(t) {
|
||
if (this.outputFilter) {
|
||
if (!this.displayedMemory.isEmpty()) {
|
||
if (this.outputFilter.newCue) {
|
||
this.outputFilter.newCue(this.cueStartTime, t, this.displayedMemory);
|
||
}
|
||
this.cueStartTime = t;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
class Cea608Parser {
|
||
constructor(field = 1, out1, out2) {
|
||
this.field = field;
|
||
this.currChNr = -1; // Will be 1 or 2
|
||
this.lastCmdA = null; // First byte of last command
|
||
this.lastCmdB = null; // Second byte of last command
|
||
this.channels = [new Cea608Channel(1, out1), new Cea608Channel(2, out2)];
|
||
this.dataCounters = { padding: 0, char: 0, cmd: 0, other: 0 };
|
||
}
|
||
getHandler(index) {
|
||
return this.channels[index].getHandler();
|
||
}
|
||
setHandler(index, newHandler) {
|
||
this.channels[index].setHandler(newHandler);
|
||
}
|
||
/**
|
||
* Add data for time t in forms of list of bytes (unsigned ints). The bytes are treated as pairs.
|
||
*/
|
||
addData(t, byteList) {
|
||
let cmdFound, a, b;
|
||
let charsFound = null;
|
||
logger.setTime(t);
|
||
for (let i = 0; i < byteList.length; i += 2) {
|
||
a = byteList[i] & 127;
|
||
b = byteList[i + 1] & 127;
|
||
if (a >= 16 && a <= 31 && a === this.lastCmdA && b === this.lastCmdB) {
|
||
this.lastCmdA = null;
|
||
this.lastCmdB = null;
|
||
logger.log('DEBUG', 'Repeated command (' + numArrayToHexArray([a, b]) + ') is dropped');
|
||
continue; // Repeated commands are dropped (once)
|
||
}
|
||
if (a === 0 && b === 0) {
|
||
this.dataCounters.padding += 2;
|
||
continue;
|
||
}
|
||
else {
|
||
logger.log('DATA', '[' + numArrayToHexArray([byteList[i], byteList[i + 1]]) + '] -> (' + numArrayToHexArray([a, b]) + ')');
|
||
}
|
||
cmdFound = this.parseCmd(a, b);
|
||
if (!cmdFound) {
|
||
cmdFound = this.parseMidrow(a, b);
|
||
}
|
||
if (!cmdFound) {
|
||
cmdFound = this.parsePAC(a, b);
|
||
}
|
||
if (!cmdFound) {
|
||
cmdFound = this.parseBackgroundAttributes(a, b);
|
||
}
|
||
if (!cmdFound) {
|
||
charsFound = this.parseChars(a, b);
|
||
if (charsFound) {
|
||
if (this.currChNr && this.currChNr >= 0) {
|
||
const channel = this.channels[this.currChNr - 1];
|
||
channel.insertChars(charsFound);
|
||
}
|
||
else {
|
||
logger.log('WARNING', 'No channel found yet. TEXT-MODE?');
|
||
}
|
||
}
|
||
}
|
||
if (cmdFound) {
|
||
this.dataCounters.cmd += 2;
|
||
}
|
||
else if (charsFound) {
|
||
this.dataCounters.char += 2;
|
||
}
|
||
else {
|
||
this.dataCounters.other += 2;
|
||
logger.log('WARNING', 'Couldn\'t parse cleaned data ' + numArrayToHexArray([a, b]) + ' orig: ' + numArrayToHexArray([byteList[i], byteList[i + 1]]));
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* Parse Command.
|
||
* @returns {Boolean} Tells if a command was found
|
||
*/
|
||
parseCmd(a, b) {
|
||
let chNr = null;
|
||
const cond1 = (a === 20 || a === 21 || a === 28 || a === 29) && 32 <= b && b <= 47;
|
||
const cond2 = (a === 23 || a === 31) && 33 <= b && b <= 35;
|
||
if (!(cond1 || cond2)) {
|
||
return false;
|
||
}
|
||
if (a === 20 || a === 23) {
|
||
chNr = 1;
|
||
}
|
||
else {
|
||
chNr = 2; // (a === 0x1C || a=== 0x1f)
|
||
}
|
||
const channel = this.channels[chNr - 1];
|
||
if (a === 20 || a === 21 || a === 28 || a === 29) {
|
||
if (b === 32) {
|
||
channel.ccRCL();
|
||
}
|
||
else if (b === 33) {
|
||
channel.ccBS();
|
||
}
|
||
else if (b === 34) {
|
||
channel.ccAOF();
|
||
}
|
||
else if (b === 35) {
|
||
channel.ccAON();
|
||
}
|
||
else if (b === 36) {
|
||
channel.ccDER();
|
||
}
|
||
else if (b === 37) {
|
||
channel.ccRU(2);
|
||
}
|
||
else if (b === 38) {
|
||
channel.ccRU(3);
|
||
}
|
||
else if (b === 39) {
|
||
channel.ccRU(4);
|
||
}
|
||
else if (b === 40) {
|
||
channel.ccFON();
|
||
}
|
||
else if (b === 41) {
|
||
channel.ccRDC();
|
||
}
|
||
else if (b === 42) {
|
||
channel.ccTR();
|
||
}
|
||
else if (b === 43) {
|
||
channel.ccRTD();
|
||
}
|
||
else if (b === 44) {
|
||
channel.ccEDM();
|
||
}
|
||
else if (b === 45) {
|
||
channel.ccCR();
|
||
}
|
||
else if (b === 46) {
|
||
channel.ccENM();
|
||
}
|
||
else if (b === 47) {
|
||
channel.ccEOC();
|
||
}
|
||
}
|
||
else {
|
||
// a == 0x17 || a == 0x1F
|
||
channel.ccTO(b - 32);
|
||
}
|
||
this.lastCmdA = a;
|
||
this.lastCmdB = b;
|
||
this.currChNr = chNr;
|
||
return true;
|
||
}
|
||
/**
|
||
* Parse midrow styling command
|
||
* @returns {Boolean}
|
||
*/
|
||
parseMidrow(a, b) {
|
||
let chNr = null;
|
||
if ((a === 17 || a === 25) && 32 <= b && b <= 47) {
|
||
if (a === 17) {
|
||
chNr = 1;
|
||
}
|
||
else {
|
||
chNr = 2;
|
||
}
|
||
if (chNr !== this.currChNr) {
|
||
logger.log('ERROR', 'Mismatch channel in midrow parsing');
|
||
return false;
|
||
}
|
||
const channel = this.channels[chNr - 1];
|
||
// cea608 spec says midrow codes should inject a space
|
||
channel.insertChars([32]);
|
||
channel.ccMIDROW(b);
|
||
logger.log('DEBUG', 'MIDROW (' + numArrayToHexArray([a, b]) + ')');
|
||
this.lastCmdA = a;
|
||
this.lastCmdB = b;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
/**
|
||
* Parse Preable Access Codes (Table 53).
|
||
* @returns {Boolean} Tells if PAC found
|
||
*/
|
||
parsePAC(a, b) {
|
||
let chNr = null;
|
||
let row = null;
|
||
const case1 = ((17 <= a && a <= 23) || (25 <= a && a <= 31)) && 64 <= b && b <= 127;
|
||
const case2 = (a === 16 || a === 24) && 64 <= b && b <= 95;
|
||
if (!(case1 || case2)) {
|
||
return false;
|
||
}
|
||
chNr = a <= 23 ? 1 : 2;
|
||
if (64 <= b && b <= 95) {
|
||
row = chNr === 1 ? rowsLowCh1[a] : rowsLowCh2[a];
|
||
}
|
||
else {
|
||
// 0x60 <= b <= 0x7F
|
||
row = chNr === 1 ? rowsHighCh1[a] : rowsHighCh2[a];
|
||
}
|
||
const pacData = this.interpretPAC(row, b);
|
||
const channel = this.channels[chNr - 1];
|
||
channel.setPAC(pacData);
|
||
this.lastCmdA = a;
|
||
this.lastCmdB = b;
|
||
this.currChNr = chNr;
|
||
return true;
|
||
}
|
||
/**
|
||
* Interpret the second byte of the pac, and return the information.
|
||
* @returns {Object} pacData with style parameters.
|
||
*/
|
||
interpretPAC(row, byte) {
|
||
let pacIndex = byte;
|
||
const pacData = { color: null, italics: false, indent: null, underline: false, row: row };
|
||
if (byte > 95) {
|
||
pacIndex = byte - 96;
|
||
}
|
||
else {
|
||
pacIndex = byte - 64;
|
||
}
|
||
pacData.underline = (pacIndex & 1) === 1;
|
||
if (pacIndex <= 13) {
|
||
pacData.color = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta', 'white'][Math.floor(pacIndex / 2)];
|
||
}
|
||
else if (pacIndex <= 15) {
|
||
pacData.italics = true;
|
||
pacData.color = 'white';
|
||
}
|
||
else {
|
||
pacData.indent = Math.floor((pacIndex - 16) / 2) * 4;
|
||
}
|
||
return pacData; // Note that row has zero offset. The spec uses 1.
|
||
}
|
||
/**
|
||
* Parse characters.
|
||
* @returns An array with 1 to 2 codes corresponding to chars, if found. null otherwise.
|
||
*/
|
||
parseChars(a, b) {
|
||
let channelNr = null, charCodes = null, charCode1 = null;
|
||
if (a >= 25) {
|
||
channelNr = 2;
|
||
charCode1 = a - 8;
|
||
}
|
||
else {
|
||
channelNr = 1;
|
||
charCode1 = a;
|
||
}
|
||
if (17 <= charCode1 && charCode1 <= 19) {
|
||
// Special character
|
||
let oneCode = b;
|
||
if (charCode1 === 17) {
|
||
oneCode = b + 80;
|
||
}
|
||
else if (charCode1 === 18) {
|
||
oneCode = b + 112;
|
||
}
|
||
else {
|
||
oneCode = b + 144;
|
||
}
|
||
logger.log('INFO', 'Special char \'' + getCharForByte(oneCode) + '\' in channel ' + channelNr);
|
||
charCodes = [oneCode];
|
||
this.lastCmdA = a;
|
||
this.lastCmdB = b;
|
||
}
|
||
else if (32 <= a && a <= 127) {
|
||
charCodes = b === 0 ? [a] : [a, b];
|
||
this.lastCmdA = null;
|
||
this.lastCmdB = null;
|
||
}
|
||
if (charCodes) {
|
||
const hexCodes = numArrayToHexArray(charCodes);
|
||
logger.log('DEBUG', `Char codes = ${hexCodes.join(',')}`);
|
||
}
|
||
return charCodes;
|
||
}
|
||
/**
|
||
* Parse extended background attributes as well as new foreground color black.
|
||
* @returns{Boolean} Tells if background attributes are found
|
||
*/
|
||
parseBackgroundAttributes(a, b) {
|
||
let bkgData, index, chNr, channel;
|
||
const case1 = (a === 16 || a === 24) && 32 <= b && b <= 47;
|
||
const case2 = (a === 23 || a === 31) && 45 <= b && b <= 47;
|
||
if (!(case1 || case2)) {
|
||
return false;
|
||
}
|
||
// eslint-disable-next-line prefer-const
|
||
bkgData = { underline: false };
|
||
if (a === 16 || a === 24) {
|
||
index = Math.floor((b - 32) / 2);
|
||
bkgData.background = backgroundColors[index];
|
||
if (b % 2 === 1) {
|
||
bkgData.background = bkgData.background + '_semi';
|
||
}
|
||
}
|
||
else if (b === 45) {
|
||
bkgData.background = 'transparent';
|
||
}
|
||
else {
|
||
bkgData.foreground = 'black';
|
||
if (b === 47) {
|
||
bkgData.underline = true;
|
||
}
|
||
}
|
||
// eslint-disable-next-line prefer-const
|
||
chNr = a < 24 ? 1 : 2;
|
||
// eslint-disable-next-line prefer-const
|
||
channel = this.channels[chNr - 1];
|
||
channel.setBkgData(bkgData);
|
||
this.lastCmdA = null;
|
||
this.lastCmdB = null;
|
||
return true;
|
||
}
|
||
/**
|
||
* Reset state of parser and its channels.
|
||
*/
|
||
reset() {
|
||
for (let i = 0; i < this.channels.length; i++) {
|
||
if (this.channels[i]) {
|
||
this.channels[i].reset();
|
||
}
|
||
}
|
||
this.lastCmdA = null;
|
||
this.lastCmdB = null;
|
||
}
|
||
/**
|
||
* Trigger the generation of a cue, and the start of a new one if displayScreens are not empty.
|
||
*/
|
||
cueSplitAtTime(t) {
|
||
for (let i = 0; i < this.channels.length; i++) {
|
||
if (this.channels[i]) {
|
||
this.channels[i].cueSplitAtTime(t);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
var Cea608Parser$1 = Cea608Parser;
|
||
|
||
class OutputFilter {
|
||
constructor(handler, track) {
|
||
this.handler = handler;
|
||
this.track = track;
|
||
this.startTime = null;
|
||
this.endTime = null;
|
||
this.screen = null;
|
||
}
|
||
dispatchCue() {
|
||
if (this.startTime === null) {
|
||
return;
|
||
}
|
||
this.handler.addCues('cc' + this.track, this.startTime, this.endTime, this.screen);
|
||
this.startTime = null;
|
||
}
|
||
newCue(startTime, endTime, screen) {
|
||
if (this.startTime === null || this.startTime > startTime) {
|
||
this.startTime = startTime;
|
||
}
|
||
this.endTime = endTime;
|
||
this.screen = screen;
|
||
this.handler.createHTMLCaptionsTrack(this.track);
|
||
}
|
||
}
|
||
|
||
var lib = {};
|
||
|
||
var vttparser = {};
|
||
|
||
var vttcue = {};
|
||
|
||
var vttvalidator = {};
|
||
|
||
/**
|
||
* Copyright 2019 vtt.js Contributors
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
Object.defineProperty(vttvalidator, "__esModule", { value: true });
|
||
function isValidPercentValue(value) {
|
||
return typeof value === 'number' && value >= 0 && value <= 100;
|
||
}
|
||
vttvalidator.isValidPercentValue = isValidPercentValue;
|
||
function isValidAlignSetting(value) {
|
||
return (typeof value === 'string' &&
|
||
['start', 'center', 'end', 'left', 'right', 'middle'].includes(value));
|
||
}
|
||
vttvalidator.isValidAlignSetting = isValidAlignSetting;
|
||
function isValidDirectionSetting(value) {
|
||
return typeof value === 'string' && ['', 'rl', 'lr'].includes(value);
|
||
}
|
||
vttvalidator.isValidDirectionSetting = isValidDirectionSetting;
|
||
function isValidLineAndPositionSetting(value) {
|
||
return typeof value === 'number' || value === 'auto';
|
||
}
|
||
vttvalidator.isValidLineAndPositionSetting = isValidLineAndPositionSetting;
|
||
function isValidLineAlignSetting(value) {
|
||
return typeof value === 'string' && ['start', 'center', 'end'].includes(value);
|
||
}
|
||
vttvalidator.isValidLineAlignSetting = isValidLineAlignSetting;
|
||
function isValidPositionAlignSetting(value) {
|
||
return (typeof value === 'string' &&
|
||
['line-left', 'center', 'line-right', 'auto', 'left', 'start', 'middle', 'end', 'right'].includes(value));
|
||
}
|
||
vttvalidator.isValidPositionAlignSetting = isValidPositionAlignSetting;
|
||
function isValidScrollSetting(value) {
|
||
return ['', 'up'].includes(value);
|
||
}
|
||
vttvalidator.isValidScrollSetting = isValidScrollSetting;
|
||
|
||
var vttparserUtility = {};
|
||
|
||
Object.defineProperty(vttparserUtility, "__esModule", { value: true });
|
||
const ESCAPE = {
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'‎': '\u200e',
|
||
'‏': '\u200f',
|
||
' ': '\u00a0'
|
||
};
|
||
const TAG_NAME = {
|
||
c: 'span',
|
||
i: 'i',
|
||
b: 'b',
|
||
u: 'u',
|
||
ruby: 'ruby',
|
||
rt: 'rt',
|
||
v: 'span',
|
||
lang: 'span'
|
||
};
|
||
const TAG_ANNOTATION = {
|
||
v: 'title',
|
||
lang: 'lang'
|
||
};
|
||
const NEEDS_PARENT = {
|
||
rt: 'ruby'
|
||
};
|
||
// "text-combine-upright" must be remapped into "-webkit-text-combine"
|
||
// until browsers support this property
|
||
// In the future: https://developer.mozilla.org/en-US/docs/Web/CSS/text-combine-upright
|
||
const styleMap = {
|
||
'text-combine-upright': '-webkit-text-combine:horizontal; text-orientation: mixed;'
|
||
};
|
||
class ParserUtility {
|
||
// Try to parse input as a time stamp.
|
||
static parseTimeStamp(input) {
|
||
function computeSeconds(arr) {
|
||
const [h, m, s, f] = arr.map(n => (n ? parseInt('' + n) : 0));
|
||
return h * 3600 + m * 60 + s + f / 1000;
|
||
}
|
||
const m = /^(\d+):(\d{2})(:\d{2})?\.(\d{3})/.exec(input);
|
||
if (!m) {
|
||
return null;
|
||
}
|
||
if (m[3]) {
|
||
// Timestamp takes the form of [hours]:[minutes]:[seconds].[milliseconds]
|
||
return computeSeconds([m[1], m[2], m[3].substring(1), m[4]]);
|
||
}
|
||
else if (parseInt(m[1]) > 59) {
|
||
// Timestamp takes the form of [hours]:[minutes].[milliseconds]
|
||
// First position is hours as it's over 59.
|
||
return computeSeconds([m[1], m[2], null, m[4]]);
|
||
}
|
||
else {
|
||
// Timestamp takes the form of [minutes]:[seconds].[milliseconds]
|
||
return computeSeconds([null, m[1], m[2], m[4]]);
|
||
}
|
||
}
|
||
// Parse content into a document fragment.
|
||
static parseContent(window, cue, globalStyleCollection) {
|
||
let input = cue.text;
|
||
function nextToken() {
|
||
// Check for end-of-string.
|
||
if (!input) {
|
||
return null;
|
||
}
|
||
// Consume 'n' characters from the input.
|
||
function consume(result) {
|
||
input = input.substr(result.length);
|
||
return result;
|
||
}
|
||
const m = /^([^<]*)(<[^>]+>?)?/.exec(input);
|
||
// If there is some text before the next tag, return it, otherwise return
|
||
// the tag.
|
||
return consume(m[1] ? m[1] : m[2]);
|
||
}
|
||
// Unescape a string 's'.
|
||
function unescape1(e) {
|
||
return ESCAPE[e];
|
||
}
|
||
function unescape(s) {
|
||
return s.replace(/&(amp|lt|gt|lrm|rlm|nbsp);/g, unescape1);
|
||
}
|
||
function shouldAdd(current, element) {
|
||
return (!NEEDS_PARENT[element.dataset.localName] ||
|
||
NEEDS_PARENT[element.dataset.localName] === current.dataset.localName);
|
||
}
|
||
// Create an element for this tag.
|
||
function createElement(type, cssSelector, annotation) {
|
||
function createStyleString(styles) {
|
||
let styleString = '';
|
||
for (const key in styles) {
|
||
if (styleMap[key]) {
|
||
styleString += styleMap[key];
|
||
}
|
||
else {
|
||
styleString += key + ':' + styles[key] + ';';
|
||
}
|
||
}
|
||
return styleString;
|
||
}
|
||
const tagName = TAG_NAME[type];
|
||
if (!tagName) {
|
||
return null;
|
||
}
|
||
const element = window.document.createElement(tagName);
|
||
// localName is a read only property, but we can store this as an HTML data-attribute
|
||
element.dataset.localName = tagName;
|
||
const name = TAG_ANNOTATION[type];
|
||
if (name && annotation) {
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
element[name] = annotation.trim();
|
||
}
|
||
if (cssSelector) {
|
||
if (globalStyleCollection[cssSelector]) {
|
||
const elementStyleCollection = globalStyleCollection[cssSelector];
|
||
const styleString = createStyleString(elementStyleCollection);
|
||
element.setAttribute('style', styleString);
|
||
}
|
||
else {
|
||
console.info(`WebVTT: parseContent: Style referenced, but no style defined for '${cssSelector}'!`);
|
||
}
|
||
}
|
||
return element;
|
||
}
|
||
const rootDiv = window.document.createElement('div'), tagStack = [];
|
||
let current = rootDiv, t, m;
|
||
while ((t = nextToken()) !== null) {
|
||
if (t[0] === '<') {
|
||
if (t[1] === '/') {
|
||
// If the closing tag matches, move back up to the parent node.
|
||
if (tagStack.length &&
|
||
tagStack[tagStack.length - 1] === t.substr(2).replace('>', '')) {
|
||
tagStack.pop();
|
||
current = current.parentNode;
|
||
}
|
||
// Otherwise just ignore the end tag.
|
||
continue;
|
||
}
|
||
const ts = ParserUtility.parseTimeStamp(t.substr(1, t.length - 2));
|
||
let node;
|
||
if (ts) {
|
||
// Timestamps are lead nodes as well.
|
||
node = window.document.createProcessingInstruction('timestamp', ts.toString()
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
);
|
||
current.appendChild(node);
|
||
continue;
|
||
}
|
||
m = /^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/.exec(t);
|
||
// If we can't parse the tag, skip to the next tag.
|
||
if (!m) {
|
||
continue;
|
||
}
|
||
// Try to construct an element, and ignore the tag if we couldn't.
|
||
node = createElement(m[1], m[2], m[3]);
|
||
if (!node) {
|
||
continue;
|
||
}
|
||
// Determine if the tag should be added based on the context of where it
|
||
// is placed in the cuetext.
|
||
if (!shouldAdd(current, node)) {
|
||
continue;
|
||
}
|
||
// Append the node to the current node, and enter the scope of the new
|
||
// node.
|
||
tagStack.push(m[1]);
|
||
current.appendChild(node);
|
||
current = node;
|
||
continue;
|
||
}
|
||
// Text nodes are leaf nodes.
|
||
current.appendChild(window.document.createTextNode(unescape(t)));
|
||
}
|
||
return rootDiv;
|
||
}
|
||
}
|
||
vttparserUtility.default = ParserUtility;
|
||
|
||
/**
|
||
* Copyright 2013 vtt.js Contributors
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
var __decorate$1 = (commonjsGlobal && commonjsGlobal.__decorate) || function (decorators, target, key, desc) {
|
||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||
};
|
||
Object.defineProperty(vttcue, "__esModule", { value: true });
|
||
const vttvalidator_1$1 = vttvalidator;
|
||
const vttparser_utility_1$2 = vttparserUtility;
|
||
let VTTCue = class VTTCue {
|
||
constructor(startTime, endTime, text) {
|
||
/**
|
||
* VTTCue and TextTrackCue properties
|
||
* http://dev.w3.org/html5/webvtt/#vttcue-interface
|
||
*/
|
||
this._id = '';
|
||
this._pauseOnExit = false;
|
||
this._region = null;
|
||
this._vertical = '';
|
||
this._snapToLines = true;
|
||
this._line = 'auto';
|
||
this._lineAlign = 'start';
|
||
this._position = 'auto';
|
||
this._positionAlign = 'auto';
|
||
this._size = 100;
|
||
this._align = 'center';
|
||
/**
|
||
* Other <track> spec defined properties
|
||
*/
|
||
/**
|
||
* Shim implementation specific properties. These properties are not in
|
||
* the spec.
|
||
*/
|
||
// Lets us know when the VTTCue's data has changed in such a way that we need
|
||
// to recompute its display state. This lets us compute its display state
|
||
// lazily.
|
||
this.hasBeenReset = false;
|
||
this._startTime = startTime;
|
||
this._endTime = endTime;
|
||
this._text = text;
|
||
}
|
||
get id() {
|
||
return this._id;
|
||
}
|
||
set id(value) {
|
||
this._id = '' + value;
|
||
}
|
||
get pauseOnExit() {
|
||
return this._pauseOnExit;
|
||
}
|
||
set pauseOnExit(value) {
|
||
this._pauseOnExit = !!value;
|
||
}
|
||
get startTime() {
|
||
return this._startTime;
|
||
}
|
||
set startTime(value) {
|
||
if (typeof value !== 'number') {
|
||
throw new TypeError(`Start time must be set to a number: ${value}`);
|
||
}
|
||
this._startTime = value;
|
||
this.hasBeenReset = true;
|
||
}
|
||
get endTime() {
|
||
return this._endTime;
|
||
}
|
||
set endTime(value) {
|
||
if (typeof value !== 'number') {
|
||
throw new TypeError(`End time must be set to a number: ${value}`);
|
||
}
|
||
this._endTime = value;
|
||
this.hasBeenReset = true;
|
||
}
|
||
get text() {
|
||
return this._text;
|
||
}
|
||
set text(value) {
|
||
this._text = '' + value;
|
||
this.hasBeenReset = true;
|
||
}
|
||
get region() {
|
||
return this._region;
|
||
}
|
||
set region(value) {
|
||
this._region = value;
|
||
this.hasBeenReset = true;
|
||
}
|
||
get vertical() {
|
||
return this._vertical;
|
||
}
|
||
set vertical(value) {
|
||
if (!vttvalidator_1$1.isValidDirectionSetting(value)) {
|
||
throw new SyntaxError(`An invalid or illegal string was specified for vertical: ${value}`);
|
||
}
|
||
this._vertical = value;
|
||
this.hasBeenReset = true;
|
||
}
|
||
get snapToLines() {
|
||
return this._snapToLines;
|
||
}
|
||
set snapToLines(value) {
|
||
this._snapToLines = !!value;
|
||
this.hasBeenReset = true;
|
||
}
|
||
get line() {
|
||
return this._line;
|
||
}
|
||
set line(value) {
|
||
if (!vttvalidator_1$1.isValidLineAndPositionSetting(value)) {
|
||
throw new SyntaxError(`An invalid number or illegal string was specified for line: ${value}`);
|
||
}
|
||
this._line = value;
|
||
this.hasBeenReset = true;
|
||
}
|
||
get lineAlign() {
|
||
return this._lineAlign;
|
||
}
|
||
set lineAlign(value) {
|
||
if (!vttvalidator_1$1.isValidLineAlignSetting(value)) {
|
||
throw new SyntaxError(`An invalid or illegal string was specified for lineAlign: ${value}`);
|
||
}
|
||
this._lineAlign = value;
|
||
this.hasBeenReset = true;
|
||
}
|
||
get position() {
|
||
return this._position;
|
||
}
|
||
set position(value) {
|
||
if (!vttvalidator_1$1.isValidLineAndPositionSetting(value)) {
|
||
throw new Error(`Position must be between 0 and 100 or auto: ${value}`);
|
||
}
|
||
this._position = value;
|
||
this.hasBeenReset = true;
|
||
}
|
||
get positionAlign() {
|
||
return this._positionAlign;
|
||
}
|
||
set positionAlign(value) {
|
||
if (!vttvalidator_1$1.isValidPositionAlignSetting(value)) {
|
||
throw new SyntaxError(`An invalid or illegal string was specified for positionAlign: ${value}`);
|
||
}
|
||
this._positionAlign = value;
|
||
this.hasBeenReset = true;
|
||
}
|
||
get size() {
|
||
return this._size;
|
||
}
|
||
set size(value) {
|
||
if (value < 0 || value > 100) {
|
||
throw new Error(`Size must be between 0 and 100: ${value}`);
|
||
}
|
||
this._size = value;
|
||
this.hasBeenReset = true;
|
||
}
|
||
get align() {
|
||
return this._align;
|
||
}
|
||
set align(value) {
|
||
if (!vttvalidator_1$1.isValidAlignSetting(value)) {
|
||
throw new SyntaxError(`An invalid or illegal string was specified for align: ${value}`);
|
||
}
|
||
this._align = value;
|
||
this.hasBeenReset = true;
|
||
}
|
||
getCueAsHTML() {
|
||
return vttparser_utility_1$2.default.parseContent(window, this, {});
|
||
}
|
||
static create(options) {
|
||
if (!options.hasOwnProperty('startTime') ||
|
||
!options.hasOwnProperty('endTime') ||
|
||
!options.hasOwnProperty('text')) {
|
||
throw new Error('You must at least have start time, end time, and text.');
|
||
}
|
||
const cue = new this(options.startTime, options.endTime, options.text);
|
||
Object.keys(options).forEach((key) => {
|
||
if (cue.hasOwnProperty(key)) {
|
||
cue[key] = options[key];
|
||
}
|
||
});
|
||
return cue;
|
||
}
|
||
static fromJSON(json) {
|
||
return this.create(JSON.parse(json));
|
||
}
|
||
toJSON() {
|
||
const json = {};
|
||
Object.keys(this).forEach((key) => {
|
||
if (this.hasOwnProperty(key) &&
|
||
key !== 'getCueAsHTML' &&
|
||
key !== 'hasBeenReset' &&
|
||
key !== 'displayState') {
|
||
json[key] = this[key];
|
||
}
|
||
});
|
||
return json;
|
||
}
|
||
};
|
||
VTTCue = __decorate$1([
|
||
replaceVTTCue
|
||
], VTTCue);
|
||
vttcue.VTTCue = VTTCue;
|
||
function replaceVTTCue(constructor) {
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
let cls = constructor;
|
||
if (typeof window !== 'undefined' && window.VTTCue != null) {
|
||
cls = window.VTTCue;
|
||
cls.create = constructor.create;
|
||
cls.fromJSON = constructor.fromJSON;
|
||
cls.prototype.toJSON = constructor.prototype.toJSON;
|
||
}
|
||
return cls;
|
||
}
|
||
|
||
var vttregion = {};
|
||
|
||
/**
|
||
* Copyright 2013 vtt.js Contributors
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
var __decorate = (commonjsGlobal && commonjsGlobal.__decorate) || function (decorators, target, key, desc) {
|
||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||
};
|
||
Object.defineProperty(vttregion, "__esModule", { value: true });
|
||
const vttvalidator_1 = vttvalidator;
|
||
// VTTRegion shim http://dev.w3.org/html5/webvtt/#vttregion-interface
|
||
let VTTRegion = class VTTRegion {
|
||
constructor() {
|
||
this._id = '';
|
||
this._lines = 3;
|
||
this._regionAnchorX = 0;
|
||
this._regionAnchorY = 100;
|
||
this._scroll = '';
|
||
this._viewportAnchorX = 0;
|
||
this._viewportAnchorY = 100;
|
||
this._width = 100;
|
||
}
|
||
get id() {
|
||
return this._id;
|
||
}
|
||
set id(value) {
|
||
if (typeof value !== 'string') {
|
||
throw new Error('ID must be a string.');
|
||
}
|
||
this._id = value;
|
||
}
|
||
get lines() {
|
||
return this._lines;
|
||
}
|
||
set lines(value) {
|
||
if (typeof value !== 'number') {
|
||
throw new TypeError('Lines must be set to a number.');
|
||
}
|
||
this._lines = value;
|
||
}
|
||
get regionAnchorX() {
|
||
return this._regionAnchorX;
|
||
}
|
||
set regionAnchorX(value) {
|
||
if (!vttvalidator_1.isValidPercentValue(value)) {
|
||
throw new TypeError('RegionAnchorX must be between 0 and 100.');
|
||
}
|
||
this._regionAnchorX = value;
|
||
}
|
||
get regionAnchorY() {
|
||
return this._regionAnchorY;
|
||
}
|
||
set regionAnchorY(value) {
|
||
if (!vttvalidator_1.isValidPercentValue(value)) {
|
||
throw new TypeError('RegionAnchorY must be between 0 and 100.');
|
||
}
|
||
this._regionAnchorY = value;
|
||
}
|
||
get scroll() {
|
||
return this._scroll;
|
||
}
|
||
set scroll(value) {
|
||
if (typeof value === 'string') {
|
||
const setting = value.toLowerCase();
|
||
if (vttvalidator_1.isValidScrollSetting(setting)) {
|
||
this._scroll = setting;
|
||
return;
|
||
}
|
||
}
|
||
throw new SyntaxError('An invalid or illegal string was specified.');
|
||
}
|
||
get viewportAnchorX() {
|
||
return this._viewportAnchorX;
|
||
}
|
||
set viewportAnchorX(value) {
|
||
if (!vttvalidator_1.isValidPercentValue(value)) {
|
||
throw new TypeError('ViewportAnchorX must be between 0 and 100.');
|
||
}
|
||
this._viewportAnchorX = value;
|
||
}
|
||
get viewportAnchorY() {
|
||
return this._viewportAnchorY;
|
||
}
|
||
set viewportAnchorY(value) {
|
||
if (!vttvalidator_1.isValidPercentValue(value)) {
|
||
throw new TypeError('ViewportAnchorY must be between 0 and 100.');
|
||
}
|
||
this._viewportAnchorY = value;
|
||
}
|
||
get width() {
|
||
return this._width;
|
||
}
|
||
set width(value) {
|
||
if (!vttvalidator_1.isValidPercentValue(value)) {
|
||
throw new TypeError('Width must be between 0 and 100.');
|
||
}
|
||
this._lines = value;
|
||
}
|
||
toJSON() {
|
||
const json = {};
|
||
Object.keys(this).forEach((key) => {
|
||
if (this.hasOwnProperty(key)) {
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
json[key] = this[key];
|
||
}
|
||
});
|
||
return json;
|
||
}
|
||
static create(options) {
|
||
const region = new this();
|
||
Object.keys(options).forEach((key) => {
|
||
if (region.hasOwnProperty(key)) {
|
||
region[key] = options[key];
|
||
}
|
||
});
|
||
return region;
|
||
}
|
||
static fromJSON(json) {
|
||
return this.create(JSON.parse(json));
|
||
}
|
||
};
|
||
VTTRegion = __decorate([
|
||
replaceVTTRegion
|
||
], VTTRegion);
|
||
vttregion.VTTRegion = VTTRegion;
|
||
function replaceVTTRegion(constructor) {
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
let cls = constructor;
|
||
if (typeof window !== 'undefined' && window.VTTRegion != null) {
|
||
cls = window.VTTRegion;
|
||
cls.create = constructor.create;
|
||
cls.fromJSON = constructor.fromJSON;
|
||
cls.prototype.toJSON = constructor.prototype.toJSON;
|
||
}
|
||
return cls;
|
||
}
|
||
|
||
/**
|
||
* Copyright 2013 vtt.js Contributors
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
Object.defineProperty(vttparser, "__esModule", { value: true });
|
||
const vttcue_1$1 = vttcue;
|
||
vttparser.VTTCue = vttcue_1$1.VTTCue;
|
||
const vttregion_1 = vttregion;
|
||
vttparser.VTTRegion = vttregion_1.VTTRegion;
|
||
const vttparser_utility_1$1 = vttparserUtility;
|
||
// Creates a new ParserError object from an errorData object. The errorData
|
||
// object should have default code and message properties. The default message
|
||
// property can be overridden by passing in a message parameter.
|
||
// See ParsingError.Errors below for acceptable errors.
|
||
class ParsingError extends Error {
|
||
constructor(errorData, message) {
|
||
super();
|
||
this.name = 'ParsingError';
|
||
if (typeof errorData === 'number') {
|
||
this.code = errorData;
|
||
}
|
||
else {
|
||
this.code = errorData.code;
|
||
}
|
||
if (message) {
|
||
this.message = message;
|
||
}
|
||
else if (errorData instanceof ParsingError) {
|
||
this.message = errorData.message;
|
||
}
|
||
}
|
||
}
|
||
vttparser.ParsingError = ParsingError;
|
||
// ParsingError metadata for acceptable ParsingErrors.
|
||
ParsingError.Errors = {
|
||
BadSignature: new ParsingError(0, 'Malformed WebVTT signature.'),
|
||
BadTimeStamp: new ParsingError(1, 'Malformed time stamp.')
|
||
};
|
||
// A settings object holds key/value pairs and will ignore anything but the first
|
||
// assignment to a specific key.
|
||
class Settings {
|
||
constructor() {
|
||
this.values = {};
|
||
}
|
||
// Only accept the first assignment to any key.
|
||
set(k, v) {
|
||
if (!this.get(k) && v !== '') {
|
||
this.values[k] = v;
|
||
}
|
||
}
|
||
get(k, dflt, defaultKey) {
|
||
if (typeof dflt === 'object' && typeof defaultKey === 'string') {
|
||
return this.has(k) ? this.values[k] : dflt[defaultKey];
|
||
}
|
||
return this.has(k) ? this.values[k] : dflt;
|
||
}
|
||
// Check whether we have a value for a key.
|
||
has(k) {
|
||
return k in this.values;
|
||
}
|
||
// Accept a setting if its one of the given alternatives.
|
||
alt(k, v, a) {
|
||
for (let n = 0; n < a.length; ++n) {
|
||
if (v === a[n]) {
|
||
this.set(k, v);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
// Accept a setting if its a valid (signed) integer.
|
||
integer(k, v) {
|
||
if (/^-?\d+$/.test(v)) {
|
||
this.set(k, parseInt(v, 10));
|
||
}
|
||
}
|
||
// Accept a setting if its a valid percentage.
|
||
percent(k, v) {
|
||
const m = v.match(/^([\d]{1,3})(\.[\d]*)?%$/);
|
||
if (m) {
|
||
try {
|
||
const vf = parseFloat(v);
|
||
if (vf >= 0 && vf <= 100) {
|
||
this.set(k, vf);
|
||
return true;
|
||
}
|
||
}
|
||
catch (err) {
|
||
return false;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
class WebVTTParser {
|
||
constructor(window, decoder, onStylesParsedCallback) {
|
||
this.window = window;
|
||
this.state = 'INITIAL';
|
||
this.styleCollector = '';
|
||
this.buffer = '';
|
||
this.decoder = decoder || new TextDecoder('utf8');
|
||
this.regionList = [];
|
||
this.onStylesParsedCallback = onStylesParsedCallback;
|
||
this._styles = {};
|
||
}
|
||
// Helper to allow strings to be decoded instead of the default binary utf8 data.
|
||
static StringDecoder() {
|
||
return {
|
||
decode: (input) => {
|
||
if (!input) {
|
||
return '';
|
||
}
|
||
if (typeof input !== 'string') {
|
||
throw new Error('Error - expected string data.');
|
||
}
|
||
return decodeURIComponent(encodeURIComponent(input));
|
||
}
|
||
};
|
||
}
|
||
// If the error is a ParsingError then report it to the consumer if
|
||
// possible. If it's not a ParsingError then throw it like normal.
|
||
reportOrThrowError(e) {
|
||
if (e instanceof ParsingError &&
|
||
typeof this.onparsingerror === 'function') {
|
||
this.onparsingerror(e);
|
||
}
|
||
else {
|
||
throw e;
|
||
}
|
||
}
|
||
// Helper function to parse input into groups separated by 'groupDelim', and
|
||
// interprete each group as a key/value pair separated by 'keyValueDelim'.
|
||
parseOptions(input, callback, keyValueDelim, groupDelim) {
|
||
const groups = groupDelim ? input.split(groupDelim) : [input];
|
||
for (const group of groups) {
|
||
if (typeof group !== 'string') {
|
||
continue;
|
||
}
|
||
const kv = group.split(keyValueDelim);
|
||
if (kv.length !== 2) {
|
||
continue;
|
||
}
|
||
const k = kv[0];
|
||
const v = kv[1];
|
||
callback(k, v);
|
||
}
|
||
}
|
||
parseCue(input, cue, regionList) {
|
||
// Remember the original input if we need to throw an error.
|
||
const oInput = input;
|
||
// 4.1 WebVTT timestamp
|
||
const consumeTimeStamp = () => {
|
||
const ts = vttparser_utility_1$1.default.parseTimeStamp(input);
|
||
if (ts === null) {
|
||
throw new ParsingError(ParsingError.Errors.BadTimeStamp, 'Malformed timestamp: ' + oInput);
|
||
}
|
||
// Remove time stamp from input.
|
||
input = input.replace(/^[^\sa-zA-Z-]+/, '');
|
||
return ts;
|
||
};
|
||
// 4.4.2 WebVTT cue settings
|
||
const consumeCueSettings = (input, cue) => {
|
||
const settings = new Settings();
|
||
this.parseOptions(input, (k, v) => {
|
||
let vals, vals0;
|
||
switch (k) {
|
||
case 'region':
|
||
// Find the last region we parsed with the same region id.
|
||
for (let i = regionList.length - 1; i >= 0; i--) {
|
||
if (regionList[i].id === v) {
|
||
settings.set(k, regionList[i].region);
|
||
break;
|
||
}
|
||
}
|
||
break;
|
||
case 'vertical':
|
||
settings.alt(k, v, ['rl', 'lr']);
|
||
break;
|
||
case 'line':
|
||
vals = v.split(',');
|
||
vals0 = vals[0];
|
||
settings.integer(k, vals0);
|
||
if (settings.percent(k, vals0)) {
|
||
settings.set('snapToLines', false);
|
||
}
|
||
settings.alt(k, vals0, ['auto']);
|
||
if (vals.length === 2) {
|
||
settings.alt('lineAlign', vals[1], [
|
||
'start',
|
||
'center',
|
||
'end'
|
||
]);
|
||
}
|
||
break;
|
||
case 'position':
|
||
vals = v.split(',');
|
||
settings.percent(k, vals[0]);
|
||
if (vals.length === 2) {
|
||
let positionAlignSettings = ['line-left', 'line-right',
|
||
'center', 'auto', 'left', 'start', 'middle', 'end', 'right'];
|
||
settings.alt('positionAlign', vals[1], positionAlignSettings);
|
||
}
|
||
break;
|
||
case 'size':
|
||
settings.percent(k, v);
|
||
break;
|
||
case 'align':
|
||
let alignSettings = ['start', 'center', 'end', 'left', 'right', 'middle'];
|
||
settings.alt(k, v, alignSettings);
|
||
break;
|
||
}
|
||
}, /:/, /\s/);
|
||
// Apply default values for any missing fields.
|
||
cue.region = settings.get('region', null);
|
||
cue.vertical = settings.get('vertical', '');
|
||
// Workaround to rdar://57719277
|
||
// Waiting Webkit team to provide a proper fix, tracking in rdar://57778093
|
||
cue.line = settings.get('line', typeof cue.line === 'undefined' ? 'auto' : cue.line);
|
||
cue.lineAlign = settings.get('lineAlign', 'start');
|
||
cue.snapToLines = settings.get('snapToLines', true);
|
||
cue.size = settings.get('size', 100);
|
||
let tempAlign = settings.get('align', 'center');
|
||
cue.align = tempAlign === 'middle' ? 'center' : tempAlign;
|
||
cue.position = settings.get('position', 'auto');
|
||
let tempPositionAlign = settings.get('positionAlign', {
|
||
'start': 'start',
|
||
'left': 'start',
|
||
'center': 'center',
|
||
'right': 'end',
|
||
'end': 'end',
|
||
}, cue.align // We should default to 'auto' here - but might break in other places
|
||
);
|
||
let positionAlignMap = {
|
||
'start': 'start',
|
||
'line-left': 'start',
|
||
'left': 'start',
|
||
'center': 'center',
|
||
'middle': 'center',
|
||
'line-right': 'end',
|
||
'right': 'end',
|
||
'end': 'end',
|
||
};
|
||
cue.positionAlign = positionAlignMap[tempPositionAlign];
|
||
};
|
||
const skipWhitespace = () => {
|
||
input = input.replace(/^\s+/, '');
|
||
};
|
||
// 4.1 WebVTT cue timings.
|
||
skipWhitespace();
|
||
cue.startTime = consumeTimeStamp(); // (1) collect cue start time
|
||
skipWhitespace();
|
||
if (input.substr(0, 3) !== '-->') {
|
||
// (3) next characters must match "-->"
|
||
throw new ParsingError(ParsingError.Errors.BadTimeStamp, `Malformed time stamp (time stamps must be separated by '-->'): ${oInput}`);
|
||
}
|
||
input = input.substr(3);
|
||
skipWhitespace();
|
||
cue.endTime = consumeTimeStamp(); // (5) collect cue end time
|
||
// 4.1 WebVTT cue settings list.
|
||
skipWhitespace();
|
||
consumeCueSettings(input, cue);
|
||
}
|
||
// 3.4 WebVTT region and WebVTT region settings syntax
|
||
parseRegion(input) {
|
||
const settings = new Settings();
|
||
this.parseOptions(input, (k, v) => {
|
||
switch (k) {
|
||
case 'id':
|
||
settings.set(k, v);
|
||
break;
|
||
case 'width':
|
||
settings.percent(k, v);
|
||
break;
|
||
case 'lines':
|
||
settings.integer(k, v);
|
||
break;
|
||
case 'regionanchor':
|
||
case 'viewportanchor': {
|
||
const xy = v.split(',');
|
||
if (xy.length !== 2) {
|
||
break;
|
||
}
|
||
// We have to make sure both x and y parse, so use a temporary
|
||
// settings object here.
|
||
const anchor = new Settings();
|
||
anchor.percent('x', xy[0]);
|
||
anchor.percent('y', xy[1]);
|
||
if (!anchor.has('x') || !anchor.has('y')) {
|
||
break;
|
||
}
|
||
settings.set(k + 'X', anchor.get('x'));
|
||
settings.set(k + 'Y', anchor.get('y'));
|
||
break;
|
||
}
|
||
case 'scroll':
|
||
settings.alt(k, v, ['up']);
|
||
break;
|
||
}
|
||
}, /=/, /\s/);
|
||
// Create the region, using default values for any values that were not
|
||
// specified.
|
||
if (settings.has('id')) {
|
||
const region = new vttregion_1.VTTRegion();
|
||
region.width = settings.get('width', 100);
|
||
region.lines = settings.get('lines', 3);
|
||
region.regionAnchorX = settings.get('regionanchorX', 0);
|
||
region.regionAnchorY = settings.get('regionanchorY', 100);
|
||
region.viewportAnchorX = settings.get('viewportanchorX', 0);
|
||
region.viewportAnchorY = settings.get('viewportanchorY', 100);
|
||
region.scroll = settings.get('scroll', '');
|
||
// Register the region.
|
||
if (this.onregion) {
|
||
this.onregion(region);
|
||
}
|
||
// Remember the VTTRegion for later in case we parse any VTTCues that
|
||
// reference it.
|
||
this.regionList.push({
|
||
id: settings.get('id'),
|
||
region: region
|
||
});
|
||
}
|
||
}
|
||
// 1.3 WebVTT style block syntax
|
||
// Works on single line CSS only
|
||
parseStyle(input) {
|
||
const parseStyles = (css) => {
|
||
const rules = {};
|
||
const properties = css.split(';');
|
||
for (let i = 0; i < properties.length; i++) {
|
||
if (properties[i].includes(':')) {
|
||
const keyValue = properties[i].split(':', 2);
|
||
const key = keyValue[0].trim();
|
||
const value = keyValue[1].trim();
|
||
if (key !== '' && value !== '') {
|
||
rules[key] = value;
|
||
}
|
||
}
|
||
}
|
||
return rules;
|
||
};
|
||
const blocks = input.split('}');
|
||
blocks.pop();
|
||
for (const block of blocks) {
|
||
let selector = null;
|
||
let styles = null;
|
||
const pair = block.split('{');
|
||
if (pair[0]) {
|
||
selector = pair[0].trim();
|
||
}
|
||
if (pair[1]) {
|
||
styles = parseStyles(pair[1]);
|
||
}
|
||
if (selector && styles) {
|
||
this._styles[selector] = styles;
|
||
}
|
||
}
|
||
if (this.onStylesParsedCallback) {
|
||
this.onStylesParsedCallback(this._styles);
|
||
}
|
||
}
|
||
// 3.2 WebVTT metadata header syntax
|
||
parseHeader(input) {
|
||
this.parseOptions(input, function (k, v) {
|
||
switch (k) {
|
||
case 'Region':
|
||
// 3.3 WebVTT region metadata header syntax
|
||
this.parseRegion(v);
|
||
break;
|
||
}
|
||
}, /:/);
|
||
}
|
||
parse(data) {
|
||
// If there is no data then we won't decode it, but will just try to parse
|
||
// whatever is in buffer already. This may occur in circumstances, for
|
||
// example when flush() is called.
|
||
if (data) {
|
||
// Try to decode the data that we received.
|
||
this.buffer += this.decoder.decode(data, { stream: true });
|
||
}
|
||
const collectNextLine = () => {
|
||
const buffer = this.buffer;
|
||
let pos = 0;
|
||
const calculateBreakPosition = (buffer, index) => {
|
||
const breakPosition = { start: -1, length: -1 };
|
||
if (buffer[index] === '\r') {
|
||
breakPosition.start = index;
|
||
breakPosition.length = 1;
|
||
}
|
||
else if (buffer[index] === '\n') {
|
||
breakPosition.start = index;
|
||
breakPosition.length = 1;
|
||
}
|
||
else if (buffer[index] === '<') {
|
||
if (index + 1 < buffer.length && buffer[index + 1] === 'b') {
|
||
if (index + 2 < buffer.length && buffer[index + 2] === 'r') {
|
||
let endPosition = index + 2;
|
||
while (endPosition < buffer.length) {
|
||
if (buffer[endPosition++] === '>') {
|
||
break;
|
||
}
|
||
}
|
||
breakPosition.start = index;
|
||
breakPosition.length = endPosition - index;
|
||
}
|
||
}
|
||
}
|
||
return breakPosition;
|
||
};
|
||
let breakPosition = { start: buffer.length, length: 0 };
|
||
while (pos < buffer.length) {
|
||
const foundBreakPosition = calculateBreakPosition(buffer, pos);
|
||
if (foundBreakPosition.length > 0) {
|
||
breakPosition = foundBreakPosition;
|
||
break;
|
||
}
|
||
++pos;
|
||
}
|
||
const line = buffer.substr(0, breakPosition.start);
|
||
this.buffer = buffer.substr(breakPosition.start + breakPosition.length);
|
||
return line;
|
||
};
|
||
// 5.1 WebVTT file parsing.
|
||
try {
|
||
let line;
|
||
if (this.state === 'INITIAL') {
|
||
// We can't start parsing until we have the first line.
|
||
if (!/\r\n|\n/.test(this.buffer)) {
|
||
return this;
|
||
}
|
||
line = collectNextLine();
|
||
// strip of UTF-8 BOM if any
|
||
// https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8
|
||
const m = /^()?WEBVTT([ \t].*)?$/.exec(line);
|
||
if (!m || !m[0]) {
|
||
throw new ParsingError(ParsingError.Errors.BadSignature);
|
||
}
|
||
this.state = 'HEADER';
|
||
}
|
||
let alreadyCollectedLine = false;
|
||
while (this.buffer) {
|
||
// We can't parse a line until we have the full line.
|
||
if (!/\r\n|\n/.test(this.buffer)) {
|
||
return this;
|
||
}
|
||
if (!alreadyCollectedLine) {
|
||
line = collectNextLine();
|
||
}
|
||
else {
|
||
alreadyCollectedLine = false;
|
||
}
|
||
switch (this.state) {
|
||
case 'HEADER':
|
||
// 13-18 - Allow a header (metadata) under the WEBVTT line.
|
||
if (line.includes(':')) {
|
||
this.parseHeader(line);
|
||
}
|
||
else if (!line) {
|
||
// An empty line terminates the header and starts the body (cues).
|
||
this.state = 'ID';
|
||
}
|
||
continue;
|
||
case 'NOTE':
|
||
// Ignore NOTE blocks.
|
||
if (!line) {
|
||
this.state = 'ID';
|
||
}
|
||
continue;
|
||
case 'STYLE':
|
||
// Parse style blocks.
|
||
if (!line) {
|
||
this.parseStyle(this.styleCollector);
|
||
this.state = 'ID';
|
||
this.styleCollector = '';
|
||
}
|
||
else {
|
||
this.styleCollector += line;
|
||
}
|
||
continue;
|
||
case 'ID':
|
||
// Check for the start of NOTE blocks.
|
||
if (/^NOTE($|[ \t])/.test(line)) {
|
||
this.state = 'NOTE';
|
||
break;
|
||
}
|
||
if (/^STYLE($|[ \t])/.test(line)) {
|
||
this.state = 'STYLE';
|
||
break;
|
||
}
|
||
// 19-29 - Allow any number of line terminators, then initialize new cue values.
|
||
if (!line) {
|
||
continue;
|
||
}
|
||
this.cue = new vttcue_1$1.VTTCue(0, 0, '');
|
||
this.state = 'CUE';
|
||
// 30-39 - Check if self line contains an optional identifier or timing data.
|
||
if (!line.includes('-->')) {
|
||
this.cue.id = line;
|
||
continue;
|
||
}
|
||
// Process line as start of a cue.
|
||
/* falls through */
|
||
case 'CUE':
|
||
// 40 - Collect cue timings and settings.
|
||
try {
|
||
this.parseCue(line, this.cue, this.regionList);
|
||
}
|
||
catch (e) {
|
||
this.reportOrThrowError(e);
|
||
// In case of an error ignore rest of the cue.
|
||
this.cue = null;
|
||
this.state = 'BADCUE';
|
||
continue;
|
||
}
|
||
this.state = 'CUETEXT';
|
||
continue;
|
||
case 'CUETEXT': {
|
||
const hasSubstring = line.includes('-->');
|
||
// 34 - If we have an empty line then report the cue.
|
||
// 35 - If we have the special substring '-->' then report the cue,
|
||
// but do not collect the line as we need to process the current
|
||
// one as a new cue.
|
||
if (!line || hasSubstring) {
|
||
// We are done parsing self cue.
|
||
alreadyCollectedLine = true;
|
||
if (this.oncue) {
|
||
this.oncue(this.cue);
|
||
}
|
||
this.cue = null;
|
||
this.state = 'ID';
|
||
continue;
|
||
}
|
||
if (this.cue.text) {
|
||
this.cue.text += '\n';
|
||
}
|
||
this.cue.text += line;
|
||
continue;
|
||
}
|
||
case 'BADCUE': // BADCUE
|
||
// 54-62 - Collect and discard the remaining cue.
|
||
if (!line) {
|
||
this.state = 'ID';
|
||
}
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
catch (e) {
|
||
this.reportOrThrowError(e);
|
||
// If we are currently parsing a cue, report what we have.
|
||
if (this.state === 'CUETEXT' && this.cue && this.oncue) {
|
||
this.oncue(this.cue);
|
||
}
|
||
this.cue = null;
|
||
// Enter BADWEBVTT state if header was not parsed correctly otherwise
|
||
// another exception occurred so enter BADCUE state.
|
||
this.state = this.state === 'INITIAL' ? 'BADWEBVTT' : 'BADCUE';
|
||
}
|
||
return this;
|
||
}
|
||
flush() {
|
||
try {
|
||
// Finish decoding the stream.
|
||
this.buffer += this.decoder.decode();
|
||
// Synthesize the end of the current cue or region.
|
||
if (this.cue || this.state === 'HEADER') {
|
||
this.buffer += '\n\n';
|
||
this.parse();
|
||
}
|
||
// If we've flushed, parsed, and we're still on the INITIAL state then
|
||
// that means we don't have enough of the stream to parse the first
|
||
// line.
|
||
if (this.state === 'INITIAL') {
|
||
throw new ParsingError(ParsingError.Errors.BadSignature);
|
||
}
|
||
}
|
||
catch (e) {
|
||
this.reportOrThrowError(e);
|
||
}
|
||
if (this.onflush) {
|
||
this.onflush();
|
||
}
|
||
return this;
|
||
}
|
||
styles() {
|
||
return this._styles;
|
||
}
|
||
}
|
||
vttparser.default = WebVTTParser;
|
||
vttparser.WebVTTParser = WebVTTParser;
|
||
|
||
var vtthtmlrenderer = {};
|
||
|
||
/**
|
||
* Copyright 2013 vtt.js Contributors
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
Object.defineProperty(vtthtmlrenderer, "__esModule", { value: true });
|
||
const vttcue_1 = vttcue;
|
||
vtthtmlrenderer.VTTCue = vttcue_1.VTTCue;
|
||
const vttparser_utility_1 = vttparserUtility;
|
||
const CUE_BACKGROUND_PADDING = '1.5%';
|
||
const OVERLAP_PADDING = 0; // Pixels
|
||
const PERCENT_EDGE_OFFSET = 0.05;
|
||
const MAX_PLACEMENT_ATTEMPTS = 10;
|
||
const DEFAULT_LINE_HEIGHT = 41; // line height for 36px Helvetica font
|
||
const foregroundStyleSelector = '::cue';
|
||
const backgroundStyleSelector = '::-webkit-media-text-track-display';
|
||
const classSelectorRegex = /^(::cue\()(\..*)(\))/;
|
||
const idSelectorRegex = /^(::cue\()(#.*)(\))/;
|
||
const typeSelectorRegex = /^(::cue\()(c|i|b|u|ruby|rt|v|lang)(\))/;
|
||
const globalStyleRegex = [
|
||
classSelectorRegex,
|
||
idSelectorRegex,
|
||
typeSelectorRegex
|
||
];
|
||
// This is a list of all the Unicode characters that have a strong
|
||
// right-to-left category. What this means is that these characters are
|
||
// written right-to-left for sure. It was generated by pulling all the strong
|
||
// right-to-left characters out of the Unicode data table. That table can
|
||
// found at: http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
|
||
const strongRTLRanges = [
|
||
[0x5be, 0x5be],
|
||
[0x5c0, 0x5c0],
|
||
[0x5c3, 0x5c3],
|
||
[0x5c6, 0x5c6],
|
||
[0x5d0, 0x5ea],
|
||
[0x5f0, 0x5f4],
|
||
[0x608, 0x608],
|
||
[0x60b, 0x60b],
|
||
[0x60d, 0x60d],
|
||
[0x61b, 0x61b],
|
||
[0x61e, 0x64a],
|
||
[0x66d, 0x66f],
|
||
[0x671, 0x6d5],
|
||
[0x6e5, 0x6e6],
|
||
[0x6ee, 0x6ef],
|
||
[0x6fa, 0x70d],
|
||
[0x70f, 0x710],
|
||
[0x712, 0x72f],
|
||
[0x74d, 0x7a5],
|
||
[0x7b1, 0x7b1],
|
||
[0x7c0, 0x7ea],
|
||
[0x7f4, 0x7f5],
|
||
[0x7fa, 0x7fa],
|
||
[0x800, 0x815],
|
||
[0x81a, 0x81a],
|
||
[0x824, 0x824],
|
||
[0x828, 0x828],
|
||
[0x830, 0x83e],
|
||
[0x840, 0x858],
|
||
[0x85e, 0x85e],
|
||
[0x8a0, 0x8a0],
|
||
[0x8a2, 0x8ac],
|
||
[0x200f, 0x200f],
|
||
[0xfb1d, 0xfb1d],
|
||
[0xfb1f, 0xfb28],
|
||
[0xfb2a, 0xfb36],
|
||
[0xfb38, 0xfb3c],
|
||
[0xfb3e, 0xfb3e],
|
||
[0xfb40, 0xfb41],
|
||
[0xfb43, 0xfb44],
|
||
[0xfb46, 0xfbc1],
|
||
[0xfbd3, 0xfd3d],
|
||
[0xfd50, 0xfd8f],
|
||
[0xfd92, 0xfdc7],
|
||
[0xfdf0, 0xfdfc],
|
||
[0xfe70, 0xfe74],
|
||
[0xfe76, 0xfefc],
|
||
[0x10800, 0x10805],
|
||
[0x10808, 0x10808],
|
||
[0x1080a, 0x10835],
|
||
[0x10837, 0x10838],
|
||
[0x1083c, 0x1083c],
|
||
[0x1083f, 0x10855],
|
||
[0x10857, 0x1085f],
|
||
[0x10900, 0x1091b],
|
||
[0x10920, 0x10939],
|
||
[0x1093f, 0x1093f],
|
||
[0x10980, 0x109b7],
|
||
[0x109be, 0x109bf],
|
||
[0x10a00, 0x10a00],
|
||
[0x10a10, 0x10a13],
|
||
[0x10a15, 0x10a17],
|
||
[0x10a19, 0x10a33],
|
||
[0x10a40, 0x10a47],
|
||
[0x10a50, 0x10a58],
|
||
[0x10a60, 0x10a7f],
|
||
[0x10b00, 0x10b35],
|
||
[0x10b40, 0x10b55],
|
||
[0x10b58, 0x10b72],
|
||
[0x10b78, 0x10b7f],
|
||
[0x10c00, 0x10c48],
|
||
[0x1ee00, 0x1ee03],
|
||
[0x1ee05, 0x1ee1f],
|
||
[0x1ee21, 0x1ee22],
|
||
[0x1ee24, 0x1ee24],
|
||
[0x1ee27, 0x1ee27],
|
||
[0x1ee29, 0x1ee32],
|
||
[0x1ee34, 0x1ee37],
|
||
[0x1ee39, 0x1ee39],
|
||
[0x1ee3b, 0x1ee3b],
|
||
[0x1ee42, 0x1ee42],
|
||
[0x1ee47, 0x1ee47],
|
||
[0x1ee49, 0x1ee49],
|
||
[0x1ee4b, 0x1ee4b],
|
||
[0x1ee4d, 0x1ee4f],
|
||
[0x1ee51, 0x1ee52],
|
||
[0x1ee54, 0x1ee54],
|
||
[0x1ee57, 0x1ee57],
|
||
[0x1ee59, 0x1ee59],
|
||
[0x1ee5b, 0x1ee5b],
|
||
[0x1ee5d, 0x1ee5d],
|
||
[0x1ee5f, 0x1ee5f],
|
||
[0x1ee61, 0x1ee62],
|
||
[0x1ee64, 0x1ee64],
|
||
[0x1ee67, 0x1ee6a],
|
||
[0x1ee6c, 0x1ee72],
|
||
[0x1ee74, 0x1ee77],
|
||
[0x1ee79, 0x1ee7c],
|
||
[0x1ee7e, 0x1ee7e],
|
||
[0x1ee80, 0x1ee89],
|
||
[0x1ee8b, 0x1ee9b],
|
||
[0x1eea1, 0x1eea3],
|
||
[0x1eea5, 0x1eea9],
|
||
[0x1eeab, 0x1eebb],
|
||
[0x10fffd, 0x10fffd]
|
||
];
|
||
class StyleBox {
|
||
// Apply styles to a div. If there is no div passed then it defaults to the
|
||
// div on 'this'.
|
||
applyStyles(styles, div) {
|
||
div = div || this.div;
|
||
for (const prop in styles) {
|
||
if (styles.hasOwnProperty(prop)) {
|
||
div.style[prop] = styles[prop];
|
||
}
|
||
}
|
||
}
|
||
formatStyle(val, unit) {
|
||
return val === 0 ? '0' : val + unit;
|
||
}
|
||
}
|
||
vtthtmlrenderer.StyleBox = StyleBox;
|
||
// Constructs the computed display state of the cue (a div). Places the div
|
||
// into the overlay which should be a block level element (usually a div).
|
||
class CueStyleBox extends StyleBox {
|
||
constructor(window, cue, foregroundStyleOptions, backgroundStyleOptions, globalStyleCollection) {
|
||
super();
|
||
this.cue = cue;
|
||
let textAlignMap = {
|
||
'start': 'left',
|
||
'line-left': 'left',
|
||
'left': 'left',
|
||
'center': 'center',
|
||
'middle': 'center',
|
||
'line-right': 'right',
|
||
'right': 'right',
|
||
'end': 'right',
|
||
};
|
||
let textAlign = textAlignMap[cue.positionAlign] || cue.align;
|
||
let styles = {
|
||
textAlign: textAlign,
|
||
whiteSpace: 'pre-line',
|
||
position: 'absolute'
|
||
};
|
||
styles.direction = this.determineBidi(this.cueDiv);
|
||
styles.writingMode = this.directionSettingToWritingMode(cue.vertical);
|
||
styles.unicodeBidi = 'plaintext';
|
||
// Create an absolutely positioned div that will be used to position the cue
|
||
// div. Note, all WebVTT cue-setting alignments are equivalent to the CSS
|
||
// mirrors of them except "middle" which is "center" in CSS.
|
||
this.div = window.document.createElement('div');
|
||
this.applyStyles(styles);
|
||
styles = {
|
||
backgroundColor: backgroundStyleOptions.backgroundColor,
|
||
display: 'inline-block'
|
||
};
|
||
// If the background opacity is 0 or undefined don't want to increase
|
||
// the cue size by adding padding to the background div
|
||
if (this.parseOpacity(styles.backgroundColor)) {
|
||
styles.padding = '5px';
|
||
styles.borderRadius = '5px';
|
||
}
|
||
this.backgroundDiv = window.document.createElement('div');
|
||
this.applyStyles(styles, this.backgroundDiv);
|
||
styles = {
|
||
color: foregroundStyleOptions.color,
|
||
backgroundColor: foregroundStyleOptions.backgroundColor,
|
||
textShadow: foregroundStyleOptions.textShadow,
|
||
fontSize: foregroundStyleOptions.fontSize,
|
||
fontFamily: foregroundStyleOptions.fontFamily,
|
||
position: 'relative',
|
||
left: '0',
|
||
right: '0',
|
||
top: '0',
|
||
bottom: '0',
|
||
display: 'inline-block',
|
||
textOrientation: 'upright'
|
||
};
|
||
styles.writingMode = this.directionSettingToWritingMode(cue.vertical);
|
||
styles.unicodeBidi = 'plaintext';
|
||
// Parse our cue's text into a DOM tree rooted at 'cueDiv'. This div will
|
||
// have inline-block positioning and will function as the cue foreground box.
|
||
this.cueDiv = vttparser_utility_1.default.parseContent(window, cue, globalStyleCollection);
|
||
this.applyStyles(styles, this.cueDiv);
|
||
this.backgroundDiv.appendChild(this.cueDiv);
|
||
this.div.appendChild(this.backgroundDiv);
|
||
// Calculate the distance from the reference edge of the viewport to the text
|
||
// position of the cue box. The reference edge will be resolved later when
|
||
// the box orientation styles are applied.
|
||
let textPos = 0;
|
||
if (typeof cue.position === 'number') {
|
||
let cueAlignment = cue.positionAlign || cue.align;
|
||
if (cueAlignment) {
|
||
switch (cueAlignment) {
|
||
case 'start':
|
||
case 'left':
|
||
textPos = cue.position;
|
||
break;
|
||
case 'center':
|
||
case 'middle':
|
||
textPos = cue.position - cue.size / 2;
|
||
break;
|
||
case 'end':
|
||
case 'right':
|
||
textPos = cue.position - cue.size;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
// Horizontal box orientation; textPos is the distance from the left edge of the
|
||
// area to the left edge of the box and cue.size is the distance extending to
|
||
// the right from there.
|
||
if (cue.vertical === '') {
|
||
this.applyStyles({
|
||
left: this.formatStyle(textPos, '%'),
|
||
width: this.formatStyle(cue.size, '%')
|
||
});
|
||
// Vertical box orientation; textPos is the distance from the top edge of the
|
||
// area to the top edge of the box and cue.size is the height extending
|
||
// downwards from there.
|
||
}
|
||
else {
|
||
this.applyStyles({
|
||
top: this.formatStyle(textPos, '%'),
|
||
height: this.formatStyle(cue.size, '%')
|
||
});
|
||
}
|
||
}
|
||
determineBidi(cueDiv) {
|
||
let nodeStack = [];
|
||
let text = '';
|
||
let charCode;
|
||
if (!cueDiv || !cueDiv.childNodes) {
|
||
return 'ltr';
|
||
}
|
||
function pushNodes(nodeStack, node) {
|
||
for (let i = node.childNodes.length - 1; i >= 0; i--) {
|
||
nodeStack.push(node.childNodes[i]);
|
||
}
|
||
}
|
||
function nextTextNode(nodeStack) {
|
||
if (!nodeStack || !nodeStack.length) {
|
||
return null;
|
||
}
|
||
let node = nodeStack.pop();
|
||
let text = node.textContent || node.innerText;
|
||
if (text) {
|
||
// TODO: This should match all unicode type B characters (paragraph
|
||
// separator characters). See issue #115.
|
||
const m = /^.*(\n|\r)/.exec(text);
|
||
if (m) {
|
||
nodeStack.length = 0;
|
||
return m[0];
|
||
}
|
||
return text;
|
||
}
|
||
if (node.tagName === 'ruby') {
|
||
return nextTextNode(nodeStack);
|
||
}
|
||
if (node.childNodes) {
|
||
pushNodes(nodeStack, node);
|
||
return nextTextNode(nodeStack);
|
||
}
|
||
}
|
||
function isContainedInCharacterList(charCode, characterList) {
|
||
for (const currentRange of characterList) {
|
||
if (charCode >= currentRange[0] && charCode <= currentRange[1]) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
pushNodes(nodeStack, cueDiv);
|
||
while ((text = nextTextNode(nodeStack))) {
|
||
for (let i = 0; i < text.length; i++) {
|
||
charCode = text.charCodeAt(i);
|
||
if (isContainedInCharacterList(charCode, strongRTLRanges)) {
|
||
return 'rtl';
|
||
}
|
||
}
|
||
}
|
||
return 'ltr';
|
||
}
|
||
parseOpacity(colorString) {
|
||
if (!colorString || typeof colorString !== 'string') {
|
||
return null;
|
||
}
|
||
// Trim whitespace, strip out "rgba(" and ")" from the string
|
||
colorString = colorString.replace(/ /g, '').replace('rgba(', '').replace(')', '');
|
||
// Split r, g, b, a into an array for easier processing - we just need the opacity
|
||
const parts = colorString.split(',');
|
||
if (parts && parts.length >= 4) {
|
||
return parts[3];
|
||
}
|
||
return null;
|
||
}
|
||
directionSettingToWritingMode(directionSetting) {
|
||
if (directionSetting === '') {
|
||
return 'horizontal-tb';
|
||
}
|
||
else if (directionSetting === 'lr') {
|
||
return 'vertical-lr';
|
||
}
|
||
else {
|
||
return 'vertical-rl';
|
||
}
|
||
}
|
||
move(box) {
|
||
this.applyStyles({
|
||
top: this.formatStyle(box.top, 'px'),
|
||
bottom: this.formatStyle(box.bottom, 'px'),
|
||
left: this.formatStyle(box.left, 'px'),
|
||
right: this.formatStyle(box.right, 'px'),
|
||
height: this.formatStyle(box.height, 'px'),
|
||
width: this.formatStyle(box.width, 'px')
|
||
});
|
||
}
|
||
}
|
||
vtthtmlrenderer.CueStyleBox = CueStyleBox;
|
||
// Represents the co-ordinates of an Element in a way that we can easily
|
||
// compute things with such as if it overlaps or intersects with another Element.
|
||
// Can initialize it with either a StyleBox or another BoxPosition.
|
||
class BoxPosition {
|
||
constructor(obj) {
|
||
function isVertical(cue) {
|
||
return cue && cue.vertical !== '';
|
||
}
|
||
if (obj instanceof CueStyleBox && obj.cue) {
|
||
if (isVertical(obj.cue)) {
|
||
this.property = 'width';
|
||
}
|
||
else {
|
||
this.property = 'height';
|
||
}
|
||
}
|
||
else if (obj instanceof BoxPosition) {
|
||
this.property = obj.property || 'height';
|
||
}
|
||
// Either a BoxPosition was passed in and we need to copy it, or a StyleBox
|
||
// was passed in and we need to copy the results of 'getBoundingClientRect'
|
||
// as the object returned is readonly. All co-ordinate values are in reference
|
||
// to the viewport origin (top left).
|
||
let lh, slh, height, width, top, rect;
|
||
if (obj instanceof CueStyleBox && obj.div) {
|
||
height = obj.div.offsetHeight;
|
||
width = obj.div.offsetWidth;
|
||
top = obj.div.offsetTop;
|
||
const outerDiv = obj.div.firstChild;
|
||
// When we're initializing a BoxPosition with a CueStyleBox want to use the child element which just has
|
||
// the width / length of the text, otherwise the full length will overflow outside of the container
|
||
// causing the CueStyleBox.within function to return false, and then the overlap algorithm to fail
|
||
if (outerDiv) {
|
||
rect = outerDiv.getBoundingClientRect();
|
||
}
|
||
else {
|
||
rect = obj.div.getBoundingClientRect();
|
||
}
|
||
lh = (rect && rect[this.property]) || null;
|
||
if (outerDiv && outerDiv.firstChild) {
|
||
const innerDiv = outerDiv.firstChild;
|
||
if (innerDiv && typeof innerDiv.textContent === 'string') {
|
||
const numOfLines = this.calculateNewLines(innerDiv.textContent);
|
||
slh = lh / numOfLines;
|
||
}
|
||
}
|
||
}
|
||
else if (obj instanceof BoxPosition) {
|
||
rect = obj;
|
||
}
|
||
this.left = rect.left;
|
||
this.right = rect.right;
|
||
this.top = rect.top || top;
|
||
this.height = rect.height || height;
|
||
this.bottom = rect.bottom || top + (rect.height || height);
|
||
this.width = rect.width || width;
|
||
this.lineHeight = lh !== null ? lh : rect.lineHeight;
|
||
this.singleLineHeight = slh !== null ? slh : rect.singleLineHeight;
|
||
if (!this.singleLineHeight) {
|
||
this.singleLineHeight = DEFAULT_LINE_HEIGHT;
|
||
}
|
||
}
|
||
calculateNewLines(str) {
|
||
let numOfLines = 1;
|
||
for (let i = 0; i < str.length; i++) {
|
||
if (str[i] === '\n') {
|
||
numOfLines++;
|
||
}
|
||
}
|
||
return numOfLines;
|
||
}
|
||
// Move the box along a particular axis. Optionally pass in an amount to move
|
||
// the box. If no amount is passed then the default is the line height of the
|
||
// box.
|
||
move(axis, toMove) {
|
||
toMove = toMove !== undefined ? toMove : this.singleLineHeight;
|
||
switch (axis) {
|
||
case '+x':
|
||
this.left += toMove;
|
||
this.right += toMove;
|
||
break;
|
||
case '-x':
|
||
this.left -= toMove;
|
||
this.right -= toMove;
|
||
break;
|
||
case '+y':
|
||
this.top += toMove;
|
||
this.bottom += toMove;
|
||
break;
|
||
case '-y':
|
||
this.top -= toMove;
|
||
this.bottom -= toMove;
|
||
break;
|
||
}
|
||
}
|
||
// Check if this box overlaps another box, b2.
|
||
overlaps(b2) {
|
||
return (this.left < b2.right &&
|
||
this.right > b2.left &&
|
||
this.top < b2.bottom &&
|
||
this.bottom > b2.top);
|
||
}
|
||
// Check if this box overlaps any other boxes in boxes.
|
||
overlapsAny(boxes) {
|
||
for (const box of boxes) {
|
||
if (this.overlaps(box)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
// Check if this box is within another box.
|
||
within(container) {
|
||
return (this.top >= container.top &&
|
||
this.bottom <= container.bottom &&
|
||
this.left >= container.left &&
|
||
this.right <= container.right);
|
||
}
|
||
// Check if this box is entirely within the container or it is overlapping
|
||
// on the edge opposite of the axis direction passed. For example, if "+x" is
|
||
// passed and the box is overlapping on the left edge of the container, then
|
||
// move the left edge of the box to align with the left edge of the container
|
||
moveIfOutOfBounds(container, axis) {
|
||
switch (axis) {
|
||
case '+x':
|
||
if (this.left < container.left) {
|
||
this.left = container.left;
|
||
this.right = this.left + this.width;
|
||
}
|
||
break;
|
||
case '-x':
|
||
if (this.right > container.right) {
|
||
this.right = container.right;
|
||
this.left = this.right - this.width;
|
||
}
|
||
break;
|
||
case '+y':
|
||
if (this.top < container.top) {
|
||
this.top = container.top;
|
||
this.bottom = this.top + this.height;
|
||
}
|
||
break;
|
||
case '-y':
|
||
if (this.bottom > container.bottom) {
|
||
this.bottom = container.bottom;
|
||
this.top = this.bottom - this.height;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
// Convert the positions from this box to CSS compatible positions using
|
||
// the reference container's positions. This has to be done because this
|
||
// box's positions are in reference to the viewport origin, whereas, CSS
|
||
// values are in reference to their respective edges.
|
||
toCSSCompatValues(reference) {
|
||
return {
|
||
top: this.top - reference.top,
|
||
bottom: reference.bottom - this.bottom,
|
||
left: this.left - reference.left,
|
||
right: reference.right - this.right,
|
||
height: this.height,
|
||
width: this.width
|
||
};
|
||
}
|
||
// Get an object that represents the box's position without anything extra.
|
||
// Can pass a StyleBox or HTMLElement.
|
||
static getSimpleBoxPosition(obj) {
|
||
let element = null;
|
||
if (obj instanceof StyleBox && obj.div) {
|
||
element = obj.div;
|
||
}
|
||
else if (obj instanceof HTMLElement) {
|
||
element = obj;
|
||
}
|
||
let height = element.offsetHeight || 0;
|
||
let width = element.offsetWidth || 0;
|
||
let top = element.offsetTop || 0;
|
||
let bottom = top + height;
|
||
let rect = element.getBoundingClientRect();
|
||
const { left, right } = rect;
|
||
if (rect.top) {
|
||
top = rect.top;
|
||
}
|
||
if (rect.height) {
|
||
height = rect.height;
|
||
}
|
||
if (rect.width) {
|
||
width = rect.width;
|
||
}
|
||
if (rect.bottom) {
|
||
bottom = rect.bottom;
|
||
}
|
||
return { left, right, top, height, bottom, width };
|
||
}
|
||
static getBoxPosition(boxPositions, direction) {
|
||
if (boxPositions && boxPositions.length > 0) {
|
||
let savedIndex = 0;
|
||
let maxValue = boxPositions[0][direction];
|
||
for (let i = 0; i < boxPositions.length; i++) {
|
||
if (direction in ['top', 'right']) {
|
||
if (boxPositions[i][direction] > maxValue) {
|
||
savedIndex = i;
|
||
maxValue = boxPositions[i][direction];
|
||
}
|
||
}
|
||
else if (direction in ['bottom', 'left']) {
|
||
if (boxPositions[i][direction] < maxValue) {
|
||
savedIndex = i;
|
||
maxValue = boxPositions[i][direction];
|
||
}
|
||
}
|
||
}
|
||
return boxPositions[savedIndex];
|
||
}
|
||
else {
|
||
return null;
|
||
}
|
||
}
|
||
// Move our box (cue), b, so there's no overlap with any other cues along that axis
|
||
// For example, if our axis is +y, we'll want to place our box right above the top-most cue
|
||
static moveToMinimumDistancePlacement(b, axis, boxes) {
|
||
if (b.property === 'height') {
|
||
if (axis === '+y') {
|
||
b.top = boxes.topMostBoxPosition.bottom + OVERLAP_PADDING;
|
||
b.bottom = b.top + b.height;
|
||
}
|
||
else if (axis === '-y') {
|
||
b.bottom = boxes.bottomMostBoxPosition.top - OVERLAP_PADDING;
|
||
b.top = b.bottom - b.height;
|
||
}
|
||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||
}
|
||
else if (b.property === 'width') {
|
||
if (axis === '+x') {
|
||
b.left = boxes.rightMostBoxPosition.right + OVERLAP_PADDING;
|
||
b.right = b.left + b.width;
|
||
}
|
||
else if (axis === '-x') {
|
||
b.right = boxes.leftMostBoxPosition.left - OVERLAP_PADDING;
|
||
b.left = b.right - b.width;
|
||
}
|
||
}
|
||
}
|
||
// Move a StyleBox to its specified, or next best, position. The containerBox
|
||
// is the box that contains the StyleBox, such as a div. boxPositions are
|
||
// a list of other boxes that the styleBox can't overlap with.
|
||
static moveBoxToLinePosition(styleBox, containerBox, boxPositions) {
|
||
// Find the best position for a cue box, b, on the video. The axis parameter
|
||
// is a list of axis, the order of which, it will move the box along. For example:
|
||
// Passing ["+x", "-x"] will move the box first along the x axis in the positive
|
||
// direction. If it doesn't find a good position for it there it will then move
|
||
// it along the x axis in the negative direction.
|
||
function findBestPosition(b, axis) {
|
||
let minimumDistanceBoxes;
|
||
for (let i = 0; i < axis.length; i++) {
|
||
// Step 1. check if our current cue is within our render area boundary and re-position our cue if needed
|
||
b.moveIfOutOfBounds(containerBox, axis[i]);
|
||
// We'll do our best to place the next cue, but want
|
||
// another way to exit this while loop in case we get stuck
|
||
let currentAttempt = 0;
|
||
let attemptedMinimumDistancePlacement = false;
|
||
// Step 2. check if our current cue overlaps any other cues and re-position our cue if needed
|
||
while (b.overlapsAny(boxPositions)) {
|
||
if (currentAttempt > MAX_PLACEMENT_ATTEMPTS - 1) {
|
||
break;
|
||
}
|
||
if (!attemptedMinimumDistancePlacement) {
|
||
if (boxPositions && boxPositions.length > 0) {
|
||
// Calculating here so we don't spend time on this calculation unless there was collision
|
||
if (!minimumDistanceBoxes) {
|
||
minimumDistanceBoxes = {
|
||
topMostBoxPosition: BoxPosition.getBoxPosition(boxPositions, 'top'),
|
||
bottomMostBoxPosition: BoxPosition.getBoxPosition(boxPositions, 'bottom'),
|
||
leftMostBoxPosition: BoxPosition.getBoxPosition(boxPositions, 'left'),
|
||
rightMostBoxPosition: BoxPosition.getBoxPosition(boxPositions, 'right')
|
||
};
|
||
}
|
||
BoxPosition.moveToMinimumDistancePlacement(b, axis[i], minimumDistanceBoxes);
|
||
}
|
||
attemptedMinimumDistancePlacement = true;
|
||
}
|
||
else {
|
||
b.move(axis[i]);
|
||
}
|
||
currentAttempt++;
|
||
}
|
||
}
|
||
// Our original position if we were already in bounds and
|
||
// didn't overlap with other cues, otherwise was moved by above
|
||
return b;
|
||
}
|
||
function computeLinePos(cue) {
|
||
if (typeof cue.line === 'number' &&
|
||
(cue.snapToLines || (cue.line >= 0 && cue.line <= 100))) {
|
||
return cue.line;
|
||
}
|
||
if (!cue.track ||
|
||
!cue.track.textTrackList ||
|
||
!cue.track.textTrackList.mediaElement) {
|
||
return -1;
|
||
}
|
||
let count = 0;
|
||
const track = cue.track, trackList = track.textTrackList;
|
||
for (let i = 0; i < trackList.length && trackList[i] !== track; i++) {
|
||
if (trackList[i].mode === 'showing') {
|
||
count++;
|
||
}
|
||
}
|
||
return ++count * -1;
|
||
}
|
||
const cue = styleBox.cue;
|
||
let boxPosition = new BoxPosition(styleBox), linePos = computeLinePos(cue), axis = [], size;
|
||
// If we have a line number to align the cue to.
|
||
if (cue.snapToLines) {
|
||
let position = 0;
|
||
switch (cue.vertical) {
|
||
case '':
|
||
axis = ['+y', '-y'];
|
||
size = 'height';
|
||
break;
|
||
case 'rl':
|
||
axis = ['+x', '-x'];
|
||
size = 'width';
|
||
break;
|
||
case 'lr':
|
||
axis = ['-x', '+x'];
|
||
size = 'width';
|
||
break;
|
||
}
|
||
const step = boxPosition.lineHeight, maxPosition = containerBox[size] + step, initialAxis = axis[0];
|
||
// If computed line position returns negative then line numbers are
|
||
// relative to the bottom of the video instead of the top. Therefore, we
|
||
// need to increase our initial position by the length or width of the
|
||
// video, depending on the writing direction, and reverse our axis directions.
|
||
// Provide a reasonable automatic position that is closest to the "towards direction"
|
||
if (linePos < 0) {
|
||
let initialOffset = 0;
|
||
switch (cue.vertical) {
|
||
case '':
|
||
initialOffset = containerBox.height - step - containerBox.height * PERCENT_EDGE_OFFSET; // default to bottom
|
||
break;
|
||
// Depending on positioning, chromium will fip layout when writing-mode:vertical-rl is set
|
||
// https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/core/layout/README.md
|
||
// In this case, chromium writing-mode:vertical-lr property defaults the div to the left, and writing-mode:vertical-rl defaults the div to the right
|
||
// For WebVTT default values we want to render the initial values at the opposite end, so will use the equations below to adjust
|
||
case 'rl':
|
||
initialOffset = -containerBox.width + step + containerBox.width * PERCENT_EDGE_OFFSET; // rl: 100% <----- 0%; default to left
|
||
break;
|
||
case 'lr':
|
||
initialOffset = -containerBox.width + step + containerBox.width * PERCENT_EDGE_OFFSET; // lr: 0% -----> 100%; default to right
|
||
break;
|
||
}
|
||
position = initialOffset;
|
||
axis = axis.reverse();
|
||
}
|
||
else {
|
||
// We have been given an actual line position. Calculate appropriate layout.
|
||
switch (cue.vertical) {
|
||
case '':
|
||
position = step * Math.round(linePos); // step down from top
|
||
break;
|
||
case 'rl':
|
||
position = containerBox.width - step * Math.round(linePos); // step down from right side
|
||
break;
|
||
case 'lr':
|
||
position = step * Math.round(linePos); // step down from left side
|
||
break;
|
||
}
|
||
// If the specified initial position is greater then the max position then
|
||
// clamp the box to the amount of steps it would take for the box to
|
||
// reach the max position.
|
||
if (Math.abs(position) > maxPosition) {
|
||
position = position < 0 ? -1 : 1;
|
||
position *= Math.ceil(maxPosition / step) * step;
|
||
}
|
||
}
|
||
// Move the box to the specified position. This may not be its best position.
|
||
boxPosition.move(initialAxis, position);
|
||
}
|
||
else {
|
||
// If we have a percentage line value for the cue.
|
||
const percentageBase = cue.vertical === '' ? containerBox.height : containerBox.width;
|
||
const calculatedPercentage = (boxPosition.lineHeight / percentageBase) * 100;
|
||
switch (cue.lineAlign) {
|
||
case 'center':
|
||
linePos -= calculatedPercentage / 2;
|
||
break;
|
||
case 'end':
|
||
linePos -= calculatedPercentage;
|
||
break;
|
||
}
|
||
// Apply initial line position to the cue box.
|
||
switch (cue.vertical) {
|
||
case '':
|
||
styleBox.applyStyles({
|
||
top: styleBox.formatStyle(linePos, '%') // step down from top
|
||
});
|
||
break;
|
||
case 'rl':
|
||
styleBox.applyStyles({
|
||
right: styleBox.formatStyle(linePos, '%') // step down from right
|
||
});
|
||
break;
|
||
case 'lr':
|
||
styleBox.applyStyles({
|
||
left: styleBox.formatStyle(linePos, '%') // step down from left
|
||
});
|
||
break;
|
||
}
|
||
axis = ['+y', '-y', '+x', '-x'];
|
||
// Using this as a hint of which direction we want to place the next cues
|
||
// Only doing this for horizontally positioned subtitles for now
|
||
// TODO: rdar://50386682 (Improve overlap rendering for vertical subtitles [Nice to Have])
|
||
if (cue.axis === '+y') {
|
||
axis = ['+y', '-y', '+x', '-x'];
|
||
}
|
||
else if (cue.axis === '-y') {
|
||
axis = ['-y', '+y', '+x', '-x'];
|
||
}
|
||
// Get the box position again after we've applied the specified positioning to it.
|
||
boxPosition = new BoxPosition(styleBox);
|
||
}
|
||
const bestPosition = findBestPosition(boxPosition, axis);
|
||
styleBox.move(bestPosition.toCSSCompatValues(containerBox));
|
||
}
|
||
}
|
||
vtthtmlrenderer.BoxPosition = BoxPosition;
|
||
class WebVTTRenderer {
|
||
constructor(window, overlay, loggingEnabled = true) {
|
||
if (!window) {
|
||
return null;
|
||
}
|
||
this.window = window;
|
||
this.overlay = overlay;
|
||
this.loggingEnabled = loggingEnabled;
|
||
this.foregroundStyleOptions = {
|
||
fontFamily: 'Helvetica',
|
||
fontSize: '36px',
|
||
color: 'rgba(255, 255, 255, 1)',
|
||
textShadow: '',
|
||
backgroundColor: 'rgba(0, 0, 0, 0)' // Text Highlight
|
||
};
|
||
this.backgroundStyleOptions = {
|
||
backgroundColor: 'rgba(0, 0, 0, 0.5)'
|
||
};
|
||
this.globalStyleCollection = {};
|
||
const paddedOverlay = window.document.createElement('div');
|
||
paddedOverlay.style.position = 'absolute';
|
||
paddedOverlay.style.left = '0';
|
||
paddedOverlay.style.right = '0';
|
||
paddedOverlay.style.top = '0';
|
||
paddedOverlay.style.bottom = '0';
|
||
paddedOverlay.style.margin = CUE_BACKGROUND_PADDING;
|
||
this.paddedOverlay = paddedOverlay;
|
||
overlay.appendChild(this.paddedOverlay);
|
||
this.initSubtitleCSS();
|
||
}
|
||
/**
|
||
* Sometimes, certain CSS isn't being applied to the first cue rendered
|
||
* We can init some of the CSS here by rendering a dummy cue
|
||
*/
|
||
initSubtitleCSS() {
|
||
const dummyCue = new vttcue_1.VTTCue(0, 0, `String to init CSS - Won't be visible to user`);
|
||
const dummyCueTextTrack = [dummyCue];
|
||
// Hide the overlay so we don't get a flash of the text
|
||
this.paddedOverlay.style.opacity = '0';
|
||
this.processCues(dummyCueTextTrack);
|
||
this.processCues([]);
|
||
this.paddedOverlay.style.opacity = '1';
|
||
}
|
||
convertCueToDOMTree(cue) {
|
||
if (!cue) {
|
||
return null;
|
||
}
|
||
return vttparser_utility_1.default.parseContent(this.window, cue, this.globalStyleCollection);
|
||
}
|
||
setStyles(styles) {
|
||
function applyStyles(collection, newStyles, confine) {
|
||
for (const prop in newStyles) {
|
||
if (newStyles.hasOwnProperty(prop)) {
|
||
if (confine === true && collection[prop] !== undefined) {
|
||
collection[prop] = newStyles[prop];
|
||
}
|
||
else if (confine === false) {
|
||
collection[prop] = newStyles[prop];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for (const selector in styles) {
|
||
let confinedStyleOption = false;
|
||
let styleOptions = null;
|
||
if (selector === foregroundStyleSelector) {
|
||
styleOptions = this.foregroundStyleOptions;
|
||
confinedStyleOption = true;
|
||
}
|
||
else if (selector === backgroundStyleSelector) {
|
||
styleOptions = this.backgroundStyleOptions;
|
||
confinedStyleOption = true;
|
||
}
|
||
const selectorStyles = styles[selector];
|
||
if (confinedStyleOption === true) {
|
||
// Limited set of allowed styles to update
|
||
applyStyles(styleOptions, selectorStyles, confinedStyleOption);
|
||
}
|
||
else {
|
||
// Styles with a selector. Parse out selector: ::cue(<some selector>)
|
||
for (let index = 0; index < globalStyleRegex.length; index++) {
|
||
const regex = globalStyleRegex[index];
|
||
const match = regex.exec(selector);
|
||
if (match && match.length === 4) {
|
||
const newSelector = match[2];
|
||
const newCollection = {};
|
||
applyStyles(newCollection, selectorStyles, confinedStyleOption);
|
||
this.globalStyleCollection[newSelector] = newCollection;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
this.initSubtitleCSS();
|
||
if (this.loggingEnabled) {
|
||
console.log('WebVTTRenderer setStyles foregroundStyleOptions: ' + JSON.stringify(this.foregroundStyleOptions));
|
||
console.log('WebVTTRenderer setStyles backgroundStyleOptions: ' + JSON.stringify(this.backgroundStyleOptions));
|
||
console.log('WebVTTRenderer setStyles globalStyleCollection: ' + JSON.stringify(this.globalStyleCollection));
|
||
}
|
||
}
|
||
/**
|
||
* Runs the WebVTT processing model over the cues passed to it.
|
||
* See https://www.w3.org/TR/webvtt1/#rendering for implementation details.
|
||
*
|
||
* @param {VTTCue|TextTrackCueList} cues list of cues to be rendered
|
||
*/
|
||
processCues(cues) {
|
||
if (!cues) {
|
||
return;
|
||
}
|
||
// Remove all previous children.
|
||
while (this.paddedOverlay.firstChild) {
|
||
this.paddedOverlay.removeChild(this.paddedOverlay.firstChild);
|
||
}
|
||
function sortCues(cues) {
|
||
const cueList = [];
|
||
let avgLine = 0;
|
||
for (let i = 0; i < cues.length; i++) {
|
||
let cue = cues[i];
|
||
// If any of the line positions aren't a number return our original TextTrackCueList
|
||
if (typeof cue.line !== 'number') {
|
||
return cues;
|
||
}
|
||
avgLine += cue.line;
|
||
cueList.push(cue);
|
||
}
|
||
avgLine = avgLine / cues.length;
|
||
if (avgLine > 50) {
|
||
// Render highest line value first
|
||
cueList.forEach(function (cue) {
|
||
cue.axis = '-y';
|
||
});
|
||
cueList.sort((a, b) => b.line - a.line);
|
||
}
|
||
else {
|
||
// Render lowest line value first
|
||
cueList.forEach(function (cue) {
|
||
cue.axis = '+y';
|
||
});
|
||
cueList.sort((a, b) => a.line - b.line);
|
||
}
|
||
// Just a normal array where we had a TextTrackCueList before, but should be okay
|
||
return cueList;
|
||
}
|
||
// Determine if we need to compute the display states of the cues. This could
|
||
// be the case if a cue's state has been changed since the last computation or
|
||
// if it has not been computed yet.
|
||
function shouldCompute(cues) {
|
||
for (let i = 0; i < cues.length; i++) {
|
||
if (cues[i].hasBeenReset || !cues[i].displayState) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
// We don't need to recompute the cues' display states. Just reuse them.
|
||
if (!shouldCompute(cues)) {
|
||
for (let i = 0; i < cues.length; i++) {
|
||
this.paddedOverlay.appendChild(cues[i].displayState);
|
||
}
|
||
return;
|
||
}
|
||
const boxPositions = [], containerBox = BoxPosition.getSimpleBoxPosition(this.paddedOverlay);
|
||
// cues is of type TextTrackCueList which has a length property
|
||
if (cues.length > 1) {
|
||
cues = sortCues(cues);
|
||
}
|
||
for (let i = 0; i < cues.length; i++) {
|
||
let cue = cues[i];
|
||
// Compute the initial position and styles of the cue div.
|
||
let styleBox = new CueStyleBox(this.window, cue, this.foregroundStyleOptions, this.backgroundStyleOptions, this.globalStyleCollection);
|
||
this.paddedOverlay.appendChild(styleBox.div);
|
||
// Move the cue div to it's correct line position.
|
||
BoxPosition.moveBoxToLinePosition(styleBox, containerBox, boxPositions);
|
||
// Remember the computed div so that we don't have to recompute it later
|
||
// if we don't have too.
|
||
cue.displayState = styleBox.div;
|
||
boxPositions.push(BoxPosition.getSimpleBoxPosition(styleBox));
|
||
}
|
||
}
|
||
/**
|
||
* Update the dimensions of the overlay container
|
||
* @param width width in pixels
|
||
* @param height height in pixels
|
||
*/
|
||
setSize(width, height) {
|
||
if (width) {
|
||
this.overlay.style.width = width + "px";
|
||
}
|
||
if (height) {
|
||
this.overlay.style.height = height + "px";
|
||
}
|
||
}
|
||
/**
|
||
* Returns the HTMLElement passed in to instantiate this class
|
||
*/
|
||
getOverlay() {
|
||
return this.overlay;
|
||
}
|
||
}
|
||
vtthtmlrenderer.default = WebVTTRenderer;
|
||
vtthtmlrenderer.WebVTTRenderer = WebVTTRenderer;
|
||
|
||
(function (exports) {
|
||
/**
|
||
* Copyright 2013 vtt.js Contributors
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
function __export(m) {
|
||
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
|
||
}
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
// Default exports for Node. Export the extended versions of VTTCue and
|
||
// VTTRegion in Node since we likely want the capability to convert back and
|
||
// forth between JSON. If we don't then it's not that big of a deal since we're
|
||
// off browser.
|
||
__export(vttparser);
|
||
__export(vtthtmlrenderer);
|
||
}(lib));
|
||
|
||
|
||
const tsTimescale = 90000; // MPEG-TS timescale value
|
||
function isExtendedTextTrackCue(value) {
|
||
return Boolean(value.webVTTCue);
|
||
}
|
||
// String.prototype.startsWith is not supported in IE11
|
||
function startsWith(input, search, position) {
|
||
return input.substr(position || 0, search.length) === search;
|
||
}
|
||
function cueString2millis(timeString) {
|
||
let ts = parseInt(timeString.substr(-3));
|
||
const secs = parseInt(timeString.substr(-6, 2));
|
||
const mins = parseInt(timeString.substr(-9, 2));
|
||
const hours = timeString.length > 9 ? parseInt(timeString.substr(0, timeString.indexOf(':'))) : 0;
|
||
if (!isFiniteNumber(ts) || !isFiniteNumber(secs) || !isFiniteNumber(mins) || !isFiniteNumber(hours)) {
|
||
return -1;
|
||
}
|
||
ts += 1000 * secs;
|
||
ts += 60000 * mins;
|
||
ts += 3600000 * hours;
|
||
return ts;
|
||
}
|
||
// From https://github.com/darkskyapp/string-hash
|
||
function hash(text) {
|
||
let hash = 5381;
|
||
let i = text.length;
|
||
while (i) {
|
||
hash = (hash * 33) ^ text.charCodeAt(--i);
|
||
}
|
||
return (hash >>> 0).toString();
|
||
}
|
||
function snapToHalfSecond(time) {
|
||
const lower = Math.floor(time);
|
||
const mid = lower + 0.5;
|
||
const upper = lower + 1;
|
||
let snappedTime;
|
||
if (time >= mid) {
|
||
snappedTime = time - mid >= upper - time ? upper : mid;
|
||
}
|
||
else {
|
||
snappedTime = time - lower >= mid - time ? mid : lower;
|
||
}
|
||
return snappedTime;
|
||
}
|
||
// find value x where:
|
||
// x = value + n * cardinality, for some integer n such that:
|
||
// |x - reference| <= cardinality / 2
|
||
function wrapPtsInteger(value, reference = 0, cardinality = 8589934592) {
|
||
if (!Number.isFinite(reference)) {
|
||
return value;
|
||
}
|
||
const midpoint = cardinality / 2;
|
||
const diff = Math.abs(value - reference) % cardinality;
|
||
const offset = diff > midpoint ? cardinality - diff : -diff;
|
||
const direction = value > reference ? -1 : 1;
|
||
return reference + direction * offset;
|
||
}
|
||
const HLSVTTParser = {
|
||
parse: function (vttByteArray, initPTS, // initPTS of current cc
|
||
wrapReference, // reference time for wrapping overflowed timestamp (pick a time close to the actual time. e.g., frag.start)
|
||
cc, // current cc
|
||
callBack, errorCallBack, stylesParsedCallback, logger) {
|
||
// Convert byteArray into string, replacing any somewhat exotic linefeeds with "\n", then split on that character.
|
||
const re = /\r\n|\n\r|\n|\r/g;
|
||
// Uint8Array.prototype.reduce is not implemented in IE11
|
||
const vttLines = BufferUtils.utf8arrayToStr(new Uint8Array(vttByteArray)).trim().replace(re, '\n').split('\n');
|
||
const initPTS90k = convertTimescale(initPTS, tsTimescale); // Convert initPTS to 90k timescale
|
||
// X-TIMESTAMP-MAP MPEGTS attribute integer value (in 90k timescale)
|
||
// assume MPEGTS = 0 if X-TIMESTAMP-MAP is absent
|
||
let mpegTs = 0; // basetime with timescale 90000
|
||
// X-TIMESTAMP-MAP LOCAL attribute value parsed as number of seconds
|
||
let localTime = 0;
|
||
// Calculate wrapped ((MPEGTS - initPTS) / 90k)
|
||
// The number of seconds on the player timeline that the anchor point of the X-TIMESTAMP-MAP maps to
|
||
function calculateTimelineOffset() {
|
||
// wrapReference should be close to the final wrapped integer to prevent overflowed number after wrapped
|
||
// logger.debug(`mpegTs ${wrapPtsInteger(mpegTs)} - initPTS ${initPTS}, reference ${wrapReference} (${wrapReference * 90000})`);
|
||
return convertTimestampToSeconds({
|
||
baseTime: wrapPtsInteger(wrapPtsInteger(mpegTs) - initPTS90k.baseTime, wrapReference * tsTimescale),
|
||
timescale: tsTimescale,
|
||
});
|
||
}
|
||
const cues = [];
|
||
let parsingError = null;
|
||
let inHeader = true;
|
||
// let VTTCue = VTTCue || window.TextTrackCue;
|
||
// Create parser object using VTTCue with TextTrackCue fallback on certain browsers.
|
||
const parser = new lib.WebVTTParser(window, lib.WebVTTParser.StringDecoder(), stylesParsedCallback);
|
||
parser.oncue = function (cue) {
|
||
// cue offset = MPEGTS adjusted by InitPTS, assuming localtime is zero
|
||
const cueOffset = calculateTimelineOffset();
|
||
// logger.log(`cueOffset ${cueOffset}`);
|
||
cue.startTime = wrapPtsInteger(cue.startTime + cueOffset - localTime, 0, 95443.7176888889);
|
||
cue.endTime = wrapPtsInteger(cue.endTime + cueOffset - localTime, 0, 95443.7176888889);
|
||
// Create a unique hash id for a cue based on start/end times and text.
|
||
// This helps timeline-controller to avoid showing repeated captions.
|
||
cue.id = hash(snapToHalfSecond(cue.startTime).toString()) + hash(snapToHalfSecond(cue.endTime - cue.startTime).toString()) + hash(cue.text);
|
||
// Fix encoding of special characters. TODO: Test with all sorts of weird characters.
|
||
cue.text = decodeURIComponent(encodeURIComponent(cue.text));
|
||
if (cue.endTime > 0) {
|
||
cues.push(cue);
|
||
}
|
||
};
|
||
parser.onparsingerror = function (e) {
|
||
parsingError = e;
|
||
};
|
||
parser.onflush = function () {
|
||
if (parsingError && errorCallBack) {
|
||
errorCallBack(parsingError);
|
||
return;
|
||
}
|
||
callBack(cues);
|
||
};
|
||
// Go through contents line by line.
|
||
vttLines.forEach((line) => {
|
||
if (inHeader) {
|
||
// Look for X-TIMESTAMP-MAP in header.
|
||
if (startsWith(line, 'X-TIMESTAMP-MAP=')) {
|
||
// Once found, no more are allowed anyway, so stop searching.
|
||
inHeader = false;
|
||
// Extract LOCAL and MPEGTS.
|
||
line
|
||
.substr(16)
|
||
.split(',')
|
||
.forEach((timestamp) => {
|
||
if (startsWith(timestamp, 'LOCAL:')) {
|
||
let localTimeMs;
|
||
try {
|
||
localTimeMs = cueString2millis(timestamp.substr(6));
|
||
}
|
||
catch (e) {
|
||
localTimeMs = -1;
|
||
}
|
||
if (localTimeMs !== -1) {
|
||
localTime = localTimeMs / 1000;
|
||
}
|
||
else {
|
||
parsingError = new Error(`Malformed X-TIMESTAMP-MAP: ${line}`);
|
||
}
|
||
}
|
||
else if (startsWith(timestamp, 'MPEGTS:')) {
|
||
mpegTs = parseInt(timestamp.substr(7));
|
||
}
|
||
});
|
||
// Return without parsing X-TIMESTAMP-MAP line.
|
||
return;
|
||
}
|
||
}
|
||
// Parse line by default.
|
||
parser.parse(line + '\n');
|
||
});
|
||
parser.flush();
|
||
},
|
||
};
|
||
var HLSVTTParser$1 = HLSVTTParser;
|
||
|
||
|
||
const Cues = {
|
||
newCue: function (track, startTime, endTime, screen, media) {
|
||
let cue;
|
||
let indenting;
|
||
let indent;
|
||
let text;
|
||
let line;
|
||
const context = {
|
||
foreground: false,
|
||
background: false,
|
||
italics: false,
|
||
underline: false,
|
||
flash: false,
|
||
styleStack: [],
|
||
};
|
||
for (const [r, row] of screen.rows.entries()) {
|
||
indenting = true;
|
||
indent = 0;
|
||
text = '';
|
||
if (!row.isEmpty()) {
|
||
for (let c = 0; c < row.chars.length; c++) {
|
||
if (row.chars[c].uchar.match(/\s/) && indenting) {
|
||
indent++;
|
||
}
|
||
else {
|
||
text += this.getFormattedChar(row.chars[c], context);
|
||
indenting = false;
|
||
}
|
||
}
|
||
// To be used for cleaning-up orphaned roll-up captions
|
||
row.cueStartTime = startTime;
|
||
// Give a slight bump to the endTime if it's equal to startTime to avoid a SyntaxError in IE
|
||
if (startTime === endTime) {
|
||
endTime += 0.0001;
|
||
}
|
||
text = text.trim().replace(/<br(?: \/)?>/gi, '\n');
|
||
text += this.closeStyles(context);
|
||
// console.log('VTTCue ' + startTime + ' - ' + endTime + ': ' + text);
|
||
cue = new lib.VTTCue(startTime, endTime, text);
|
||
if (indent >= 16) {
|
||
indent--;
|
||
}
|
||
else {
|
||
indent++;
|
||
}
|
||
// VTTCue.line get's flakey when using controls, so let's now include line 13&14
|
||
// also, drop line 1 since it's to close to the top
|
||
if (navigator.userAgent.match(/Firefox\//)) {
|
||
line = r + 1;
|
||
}
|
||
else {
|
||
line = r > 7 ? r : r + 1;
|
||
}
|
||
cue.snapToLines = false; // Need this to be false for position % to work
|
||
cue.line = 10 + line * 5.33; // ( 10% top padding + line number * (80% vertical render length / 15 rows) )
|
||
cue.align = 'left';
|
||
cue.position = this.getPosition(indent, media);
|
||
track.addCue(cue);
|
||
}
|
||
}
|
||
},
|
||
getPosition: function (indent, media) {
|
||
// Assume our aspect ratio is 4:3
|
||
let aspectRatio = 1.3333333333333333;
|
||
if (media && media.offsetWidth && media.offsetHeight) {
|
||
const mediaRatio = media.offsetWidth / media.offsetHeight;
|
||
// There are some 16:10 screens out there, which is closer to 16:9 than a 4:3 aspect ratio
|
||
// So, anything with a ratio of 16:10 or wider will treat as a 16:9 screen for our calculations
|
||
if (mediaRatio >= 1.6) {
|
||
aspectRatio = 1.7777777777777777;
|
||
}
|
||
}
|
||
// CEA-608 has 32 columns, and defines 10% padding on each side
|
||
// ( 10% left padding + indent * (80% horizontal render length / 32 columns) )
|
||
let position = 10 + (indent / 32) * 80;
|
||
let leftBound = 10;
|
||
let rightBound = 90;
|
||
if (aspectRatio === 1.7777777777777777) {
|
||
// CEA-608 captions assume a 4:3 screen size, but since we'll be playing on a 16:9 need to adjust some numbers
|
||
// 4:3 => 12:9, and a 12:9 screen's width is 3/4 the size of a 16:9 screen
|
||
// 3/4 * 16 = 4, and 4/16 = 25%, which means we need to pad by 12.5% on each side
|
||
// Also, when calculating the left offset it was originally for a 4:3 screen, so need to divide by 3/4 to adjust to 16:9 percentages
|
||
position = 12.5 + position * 0.75;
|
||
leftBound = 20;
|
||
rightBound = 80;
|
||
}
|
||
// Clamp the position based on the aspect ratio - if out of these bounds, Firefox throws an exception and captions break
|
||
return Math.max(leftBound, Math.min(rightBound, position + (navigator.userAgent.match(/Firefox\//) ? 50 : 0)));
|
||
},
|
||
// return 'c' for all possible class tags (for color and bg_color); or the given style tag untouched (e.g., u, i, blink)
|
||
getRootStyleTag: function (style) {
|
||
const styleKey = style[0];
|
||
return styleKey === 'c' ? styleKey : style;
|
||
},
|
||
closeStyles: function (context) {
|
||
let postfix = '';
|
||
for (let i = context.styleStack.length - 1; i >= 0; --i) {
|
||
const styleTag = this.getRootStyleTag(context.styleStack[i]);
|
||
postfix += '</' + styleTag + '>';
|
||
context.styleStack.pop();
|
||
}
|
||
return postfix;
|
||
},
|
||
beginStyleAndBalance: function (context, newStyle) {
|
||
let balancedStyles = '';
|
||
if (newStyle[0] === 'c') {
|
||
balancedStyles += this.closeStyleAndBalance(context, 'c');
|
||
}
|
||
balancedStyles += '<' + newStyle + '>';
|
||
context.styleStack.push(newStyle);
|
||
return balancedStyles;
|
||
},
|
||
closeStyleAndBalance: function (context, oldStyle) {
|
||
const totalStyleCount = context.styleStack.length;
|
||
let childrenStyleCount = 0;
|
||
let closingChildrenStyles = '';
|
||
let balancedStyles = '';
|
||
for (let i = totalStyleCount - 1; i >= 0; --i) {
|
||
const styleUsed = context.styleStack[i];
|
||
const styleTag = this.getRootStyleTag(styleUsed);
|
||
if (oldStyle[0] === styleUsed[0]) {
|
||
closingChildrenStyles += '</' + styleTag + '>';
|
||
context.styleStack.splice(totalStyleCount - 1 - childrenStyleCount);
|
||
break;
|
||
}
|
||
else {
|
||
if (styleTag[0] === 'c') {
|
||
context.background = '';
|
||
context.foreground = '';
|
||
context.flash = false;
|
||
closingChildrenStyles += '</c>';
|
||
}
|
||
else if (styleTag[0] === 'u') {
|
||
context.underline = false;
|
||
closingChildrenStyles += '</u>';
|
||
}
|
||
else if (styleTag[0] === 'i') {
|
||
context.italics = false;
|
||
closingChildrenStyles += '</i>';
|
||
}
|
||
childrenStyleCount++;
|
||
}
|
||
}
|
||
balancedStyles = closingChildrenStyles;
|
||
return balancedStyles;
|
||
},
|
||
getFormattedChar: function (c, context) {
|
||
let prefix = '';
|
||
let formattedChar = c.uchar;
|
||
// color styles
|
||
let newColorStyle = '';
|
||
const foregroundChanged = c.penState.foreground !== context.foreground;
|
||
const backgroundChanged = c.penState.background !== context.background;
|
||
const blinkChanged = c.penState.flash !== context.flash;
|
||
if (foregroundChanged || backgroundChanged || blinkChanged) {
|
||
// always include current colors in tag
|
||
newColorStyle = '.' + c.penState.foreground;
|
||
newColorStyle += '.bg_' + c.penState.background;
|
||
if (c.penState.flash && blinkChanged) {
|
||
newColorStyle += '.blink'; // but blink may be optional
|
||
}
|
||
if (c.penState.foreground || c.penState.background || c.penState.blink) {
|
||
prefix += this.beginStyleAndBalance(context, 'c' + newColorStyle);
|
||
}
|
||
else {
|
||
// changed to undefined foreground _and_ background color
|
||
prefix += this.closeStyleAndBalance(context, 'c');
|
||
}
|
||
if (foregroundChanged) {
|
||
context.foreground = c.penState.foreground;
|
||
}
|
||
if (backgroundChanged) {
|
||
context.background = c.penState.background;
|
||
}
|
||
if (blinkChanged) {
|
||
context.flash = c.penState.flash;
|
||
}
|
||
}
|
||
// other styles
|
||
if (c.penState.underline !== context.underline) {
|
||
prefix += c.penState.underline ? this.beginStyleAndBalance(context, 'u') : this.closeStyleAndBalance(context, 'u');
|
||
context.underline = c.penState.underline;
|
||
}
|
||
if (c.penState.italics !== context.italics) {
|
||
prefix += c.penState.italics ? this.beginStyleAndBalance(context, 'i') : this.closeStyleAndBalance(context, 'i');
|
||
context.italics = c.penState.italics;
|
||
}
|
||
formattedChar = prefix + formattedChar;
|
||
return formattedChar;
|
||
},
|
||
};
|
||
var Cues$1 = Cues;
|
||
|
||
/*
|
||
* LegibleSystemAdapter
|
||
*
|
||
*
|
||
*/
|
||
var ParsedFragQuality;
|
||
(function (ParsedFragQuality) {
|
||
ParsedFragQuality["CloseEnough"] = "CloseEnough";
|
||
ParsedFragQuality["TooFar"] = "TooFar";
|
||
ParsedFragQuality["Unknown"] = "Unknown";
|
||
})(ParsedFragQuality || (ParsedFragQuality = {}));
|
||
const isChannelTextTrackKey = (value) => {
|
||
return value === 'cc1' || value === 'cc2';
|
||
};
|
||
function filterSelectableTextTracks(textTrackList) {
|
||
const tracks = [];
|
||
for (let ix = 0; ix < textTrackList.length; ix++) {
|
||
const textTrack = textTrackList[ix];
|
||
if (textTrack.kind === 'captions' || textTrack.kind === 'subtitles' || (textTrack.kind === 'metadata' && textTrack.customTextTrackCueRenderer)) {
|
||
tracks.push(textTrack);
|
||
}
|
||
}
|
||
return tracks;
|
||
}
|
||
const cea608Duration = 0.03336666666666667;
|
||
function clearCurrentCues(track) {
|
||
if (track && track.cues) {
|
||
while (track.cues.length > 0) {
|
||
track.removeCue(track.cues[0]);
|
||
}
|
||
}
|
||
}
|
||
function intersection(x1, x2, y1, y2) {
|
||
return Math.min(x2, y2) - Math.max(x1, y1);
|
||
}
|
||
class LegibleSystemAdapter extends Observable {
|
||
constructor(mediaSink, config, hls, logger) {
|
||
super((subscriber) => {
|
||
const hlsTarget = fromEventTarget(this.hls, this);
|
||
subscriber.add(hlsTarget
|
||
.event(HlsEvent$1.INLINE_STYLES_PARSED, this.onInlineStylesParsed)
|
||
.pipe(finalize$1(() => this.destroy()))
|
||
.subscribe());
|
||
subscriber.add(timer(0, this.config.trottleCheckInterval)
|
||
.pipe(switchMap(() => {
|
||
this.checkReadyToLoadNextSubtitleFragment();
|
||
return VOID;
|
||
}))
|
||
.subscribe());
|
||
if (this.config.nativeTextTrackChangeHandling) {
|
||
const useTextTrackPolling = !(this.mediaSink.textTracks && 'onchange' in this.mediaSink.textTracks);
|
||
if (useTextTrackPolling) {
|
||
subscriber.add(timer(0, 500)
|
||
.pipe(switchMap(() => {
|
||
this._onTextTracksChanged();
|
||
return VOID;
|
||
}))
|
||
.subscribe());
|
||
}
|
||
else {
|
||
const textTrackListTarget = fromEventTarget(this.mediaSink.textTracks, this);
|
||
subscriber.add(textTrackListTarget.event('change', this._onTextTracksChanged).subscribe());
|
||
}
|
||
}
|
||
});
|
||
this.config = config;
|
||
this.hls = hls;
|
||
this.logger = logger.child({ name: 'legible' });
|
||
this.mediaSink = mediaSink;
|
||
// Add the id3 text track to the legibleSystemAdapter
|
||
this.id3Track = mediaSink.id3TextTrack;
|
||
this.enableCaption = true;
|
||
this.Cues = Cues$1;
|
||
this.tracks = [];
|
||
this.cueRanges = [];
|
||
this.channelToTrackMap = {};
|
||
this.htmlTextTrackMap = new Map();
|
||
this.lastCueEndTime = 0;
|
||
this.gotTracks = false;
|
||
this.tryAgain$ = new BehaviorSubject(true);
|
||
this.needNextSubtitle$ = new BehaviorSubject(true);
|
||
}
|
||
destroy() {
|
||
clearCurrentCues(this.textTrack1);
|
||
clearCurrentCues(this.textTrack2);
|
||
this.mediaSink = undefined;
|
||
this.nativeSubtitleTrackChange$ = undefined;
|
||
}
|
||
convertCuesIntoSubtitleFragInfo(cues) {
|
||
const fragCueRecord = {};
|
||
if (cues != null && cues.length > 0) {
|
||
// When we parse WebVTT cues, we assign the cue's containing
|
||
// fragment's sequence number to cue.fragSN.
|
||
for (let i = 0; i < cues.length; i++) {
|
||
const cue = cues[i];
|
||
if (!isFiniteNumber(cue.fragSN)) {
|
||
continue;
|
||
}
|
||
const record = fragCueRecord[cue.fragSN];
|
||
if (!record) {
|
||
fragCueRecord[cue.fragSN] = { count: 1, startTime: cue.startTime, endTime: cue.endTime };
|
||
}
|
||
else {
|
||
record.count++;
|
||
record.startTime = Math.min(cue.startTime, record.startTime);
|
||
record.endTime = Math.max(cue.endTime, record.endTime);
|
||
}
|
||
}
|
||
}
|
||
return fragCueRecord;
|
||
}
|
||
checkReadyToLoadNextSubtitleFragment() {
|
||
const currentTime = this.mediaSink.mediaQuery.currentTime;
|
||
const lastParsedCue = this.lastCueEndTime;
|
||
let need = false;
|
||
if (currentTime >= lastParsedCue - this.config.subtitleLeadTime) {
|
||
need = true; // less than (default) 30 seconds away from last parsed cue
|
||
}
|
||
this.logger.trace(`[subtitle] needtoLoadNextSubtitle: ${need}: ${currentTime} >= ${lastParsedCue} - ${this.config.subtitleLeadTime}`);
|
||
this.needNextSubtitle$.next(need);
|
||
}
|
||
checkReadyToLoadNextSubtitleFragment$(frag, generatedFrags) {
|
||
var _a;
|
||
if (frag.mediaSeqNum === ((_a = generatedFrags[0]) === null || _a === void 0 ? void 0 : _a.mediaSeqNum)) {
|
||
return of(true); // don't delay the first fragment because we may need to reload if it's too far
|
||
}
|
||
this.checkReadyToLoadNextSubtitleFragment();
|
||
return this.needNextSubtitle$;
|
||
}
|
||
getNextFragment(details, mediaFragment) {
|
||
const nextSN = mediaFragment.mediaSeqNum + 1;
|
||
const nextFrag = nextSN < details.fragments.length ? details.fragments[nextSN - details.startSN] : null;
|
||
return nextFrag;
|
||
}
|
||
// aggressive loading: always load subtitles until there is none left,
|
||
// without checking for waterlevel in the current HTML5 texttrack
|
||
//
|
||
// In mediaSink.archiveParsedSubtitleFragmentRecord():
|
||
// After the legibleSystemAdapter parses a subtitle fragment,
|
||
// it records the startTime (of the first cue in the fragment), endTime (of the last cue),
|
||
// and number of added cues in the fragment on its corresponding fragInfo object.
|
||
// In this way, hls.js knows the time boundary of each parsed subtitle fragment and how many cues it has generated.
|
||
// In legibleSystemAdapter.calculateFragInfoMap():
|
||
// Iterate through the cues in the current HTML5 texttrack
|
||
// to tally the cues for each fragment.
|
||
// * If this "remaining cue" count does not match the "added cue" count in archiveParsedSubtitleFragmentRecord(),
|
||
// we will pretend that the fragment has not been loaded (probably error'ed out during cue creation)
|
||
//
|
||
// When starting up, seeking or track switching, hls.js needs to find the first subtitle fragment to load.
|
||
// It is the first unloaded fragment beyond the playback position.
|
||
// * Ignore subtitle fragments earlier than the current playback position
|
||
// * Find any continuous series of loaded subtitle fragments that overlaps the current playback position.
|
||
// Determine the media sequence number of the last fragment in this series. hls.js will load the next fragment.
|
||
// * If no fragment overlaps the playback position, find the 1st fragment with start time >=
|
||
// playback position.
|
||
// * To deal with subtitle playlist timestamp inaccuracies, we use the parsed timestamps calculated in
|
||
// mediaSink.archiveParsedSubtitleFragmentRecord() to anchor the timeline. That way, we can use each fragment's
|
||
// parsed duration to approximate the right fragment to load.
|
||
calculateFragInfoMap(position, cues, subtitleParsedRecord, details) {
|
||
const actualFragInfoMap = this.convertCuesIntoSubtitleFragInfo(cues);
|
||
let bufferInfo = { len: 0, start: position, end: position };
|
||
let start = position;
|
||
let end = position;
|
||
let prevFragSN = null;
|
||
let nextFragSN = null;
|
||
// ES6 will sort numeric property keys in ascending order automatically
|
||
// https://exploringjs.com/es6/ch_oop-besides-classes.html#_traversal-order-of-properties
|
||
for (const propertyKey in actualFragInfoMap) {
|
||
if (!Object.prototype.hasOwnProperty.call(actualFragInfoMap, propertyKey)) {
|
||
continue;
|
||
}
|
||
const sn = Number(propertyKey);
|
||
if (!isFiniteNumber(sn)) {
|
||
this.logger.warn(`$fragInfoMap has invalid key ${sn}`);
|
||
continue; // should not happen
|
||
}
|
||
const fragInfo = actualFragInfoMap[sn];
|
||
if (!this.isFragmentCompleteOrEmpty(sn, fragInfo.count, subtitleParsedRecord)) {
|
||
continue;
|
||
}
|
||
// current search position is earlier than the very 1st cue in the entire fragment details
|
||
// re-adjust search position to 1st cue's start time so that we can anchor the search from this 1st (buffered) frag
|
||
if (sn === details.startSN && position < fragInfo.startTime) {
|
||
start = end = bufferInfo.start = bufferInfo.end = position = fragInfo.startTime;
|
||
}
|
||
// found a fragment where start < position and is the leading frag in a frag sequence
|
||
if (position >= fragInfo.startTime &&
|
||
(start === position || // 1st frag in the loadPos
|
||
(isFiniteNumber(prevFragSN) && sn - prevFragSN > 1))) {
|
||
// frag disjoint from the last one
|
||
start = end = fragInfo.startTime;
|
||
}
|
||
// look for the overlapping fragment
|
||
if (position >= fragInfo.startTime) {
|
||
// potentially inside a frag, assume its endTime as our next load position, and keep going ...
|
||
end = fragInfo.endTime;
|
||
prevFragSN = sn;
|
||
continue;
|
||
}
|
||
if (isFiniteNumber(prevFragSN) && sn - prevFragSN === 1) {
|
||
// Consecutive SN between 2 neighboring cues, keep scanning down the frag chain
|
||
end = fragInfo.endTime;
|
||
prevFragSN = sn;
|
||
continue;
|
||
}
|
||
// found the end of a continuous fragment series which overlaps the current position
|
||
nextFragSN = sn;
|
||
break;
|
||
}
|
||
bufferInfo = { len: end - start, start, end };
|
||
const subtitleBufferInfo = { fragInfoMap: actualFragInfoMap, bufferInfo, prevFragSN, nextFragSN };
|
||
return subtitleBufferInfo;
|
||
}
|
||
findFrags$(details, activeDiscoSeq) {
|
||
return this.tryAgain$.pipe(observeOn(asyncScheduler), switchMap(() => {
|
||
const findFragResult = this.findFragmentsForPosition(this.mediaSink.mediaQuery.currentTime, activeDiscoSeq, details);
|
||
const nextFrags = findFragResult.foundFrags;
|
||
if (!nextFrags) {
|
||
return EMPTY; // no more frag left to process
|
||
}
|
||
this.lastCueEndTime = 0; // reset needNextSubtitle$ check
|
||
this.needNextSubtitle$.next(true);
|
||
return of(findFragResult);
|
||
}));
|
||
}
|
||
// return true to continue loading the entire fragment batch
|
||
reviewParsedFrag(parsedFragResult, findFragsResult, details) {
|
||
var _a, _b, _c, _d;
|
||
const frag = parsedFragResult.frag;
|
||
const parsedCueRecord = parsedFragResult.cueRange;
|
||
const subtitleBufferInfo = findFragsResult.subtitleBufferInfo;
|
||
const subtitleParsedInfo = findFragsResult.subtitleParsedInfo;
|
||
const currentTime = this.mediaSink.mediaQuery.currentTime;
|
||
const generatedFrags = findFragsResult.foundFrags;
|
||
let success = true;
|
||
if (frag.mediaSeqNum === generatedFrags[0].mediaSeqNum) {
|
||
if (!findFragsResult.timelineEstablished) {
|
||
this.logger.info(`[subtitle] frag sn ${frag.mediaSeqNum} parsed. retry findFrag`);
|
||
return ParsedFragQuality.TooFar;
|
||
}
|
||
// only check the starting frag
|
||
if (!parsedCueRecord) {
|
||
// missing parsed data: should not happen. let currentTime move until the findFrag returns another frag
|
||
this.logger.warn(`[subtitle] 1st frag sn ${frag.mediaSeqNum} has no cue; details ${details.fragments.length} frags`);
|
||
return ParsedFragQuality.Unknown;
|
||
}
|
||
// Check whether we inferred the first frag close enough.
|
||
if (parsedCueRecord.startTime < currentTime) {
|
||
const actualDurationToPlayHead = currentTime - parsedCueRecord.startTime;
|
||
const fragments = details.fragments;
|
||
const startIdx = frag.mediaSeqNum - details.startSN;
|
||
let i = startIdx;
|
||
let fragTime = parsedCueRecord.startTime;
|
||
for (; i < fragments.length; ++i) {
|
||
fragTime += fragments[i].duration;
|
||
if (fragTime >= currentTime) {
|
||
break;
|
||
}
|
||
}
|
||
const fragCount = i - startIdx + 1;
|
||
success = fragCount <= this.config.earlyFragTolerance; // find another frag if we are too many fragments away
|
||
this.logger.info(`[subtitle] 1st frag sn ${frag.mediaSeqNum} is ${actualDurationToPlayHead}s earlier and ${fragCount} frags away from currentTime ${currentTime}; success ${success}`);
|
||
}
|
||
else if (parsedCueRecord.startTime > currentTime && frag.mediaSeqNum !== details.startSN) {
|
||
const actualDurationToPlayHead = parsedCueRecord.startTime - currentTime;
|
||
const latestBufferedFrag = subtitleBufferInfo.prevFragSN;
|
||
this.logger.info(`[subtitle] last buffered frag is complete: ${(_a = subtitleBufferInfo.fragInfoMap[latestBufferedFrag]) === null || _a === void 0 ? void 0 : _a.count} vs ${(_b = subtitleParsedInfo[latestBufferedFrag]) === null || _b === void 0 ? void 0 : _b.count}`);
|
||
if (frag.mediaSeqNum === latestBufferedFrag + 1 && ((_c = subtitleBufferInfo.fragInfoMap[latestBufferedFrag]) === null || _c === void 0 ? void 0 : _c.count) === ((_d = subtitleParsedInfo[latestBufferedFrag]) === null || _d === void 0 ? void 0 : _d.count)) {
|
||
// parsed frag is later than currentTime, but it is just the next consecutive frag from a latest *processed* frag. success.
|
||
success = true;
|
||
this.logger.info(`[subtitle] parsed frag sn ${frag.mediaSeqNum} is ${actualDurationToPlayHead}s later than currentTime ${currentTime} but continues from previous frag ${subtitleBufferInfo.prevFragSN}; success ${success}`);
|
||
}
|
||
else {
|
||
// parsed frag is later than currentTime. This should not happen because the cues are usually later than the frag start time,
|
||
// causing the findFrag function to (always) return an earlier frag than expected.
|
||
success = actualDurationToPlayHead <= this.config.lateTolerance;
|
||
this.logger.info(`[subtitle] 1st frag sn ${frag.mediaSeqNum} is ${actualDurationToPlayHead}s later than currentTime ${currentTime} and is ${frag.duration}s long; success ${success}`);
|
||
}
|
||
}
|
||
}
|
||
return success ? ParsedFragQuality.CloseEnough : ParsedFragQuality.TooFar;
|
||
}
|
||
isFragmentEmpty(subtitleParsedRecord) {
|
||
return subtitleParsedRecord && !isFiniteNumber(subtitleParsedRecord.startTime) && subtitleParsedRecord.count === 0;
|
||
}
|
||
isFragmentCompleteOrEmpty(mediaSeqNum, cueCount, subtitleParsedRecord) {
|
||
const parsedRecord = subtitleParsedRecord ? subtitleParsedRecord[mediaSeqNum] : null;
|
||
const result = (parsedRecord === null || parsedRecord === void 0 ? void 0 : parsedRecord.count) === cueCount || this.isFragmentEmpty(parsedRecord);
|
||
return result;
|
||
}
|
||
// use one *unbuffered* fragment early because we used cue startTime as fragment startTime to find media fragment
|
||
getEarlierFragmentInSameDisco(details, frag, subtitleParsedRecord) {
|
||
const fragIdx = frag.mediaSeqNum - details.startSN - 1;
|
||
if (fragIdx < 0 || fragIdx > details.fragments.length - 1) {
|
||
this.logger.error(`[subtitle] getEarlierFragmentInSameDisco index ${fragIdx} out of range`);
|
||
return frag; // return original frag
|
||
}
|
||
const prevFrag = details.fragments[fragIdx];
|
||
return prevFrag && prevFrag.discoSeqNum === frag.discoSeqNum && !subtitleParsedRecord[frag.mediaSeqNum] ? prevFrag : frag;
|
||
}
|
||
// subtitleParsedInfo: Records containing cue count, start and end time of parsed fragments.
|
||
// subtitleParsedInfo is calculated when we parse a frag to add its WebVTT cues
|
||
// (archiveParsedSubtitleFragmentRecord)
|
||
// subtitleBufferInfo: Records containing cue count, start and end time of buffered fragments.
|
||
// subtitleBufferInfo is calculated when we enumarate added (remaining) cues in text tracks
|
||
// (calculateFragInfoMap)
|
||
// 2 main differences:
|
||
// (1) subtitleParsedInfo is stored in akita store for the entire session. Each subtitle persistent id
|
||
// has one record (That means primary and backup subtitle track may share the same record).
|
||
// subtitleBufferInfo is calculated on-demand from the cues in the current enabled text track only.
|
||
// (2) The calculated frag (cue) start and end time in subtitleParsedInfo should be more accurate than
|
||
// the start and end time in subtitleBufferInfo. This is in case MSE or the app removes used cues from the text tracks.
|
||
inferSubtitleFragmentForPosition(position, activeDiscoSeqNum, subtitleParsedInfo, subtitleBufferInfo, details) {
|
||
let foundFrag;
|
||
let startFragIdx;
|
||
let startParsedRecord;
|
||
let endFragIdx;
|
||
let endParsedRecord;
|
||
if (isFiniteNumber(subtitleBufferInfo.prevFragSN)) {
|
||
startFragIdx = subtitleBufferInfo.prevFragSN - details.startSN;
|
||
startParsedRecord = subtitleParsedInfo[subtitleBufferInfo.prevFragSN];
|
||
}
|
||
if (isFiniteNumber(subtitleBufferInfo.nextFragSN)) {
|
||
endFragIdx = subtitleBufferInfo.nextFragSN - details.startSN;
|
||
endParsedRecord = subtitleParsedInfo[subtitleBufferInfo.nextFragSN];
|
||
}
|
||
if (isFiniteNumber(startFragIdx) && startFragIdx >= 0 && startFragIdx < details.fragments.length && startParsedRecord) {
|
||
let startPos = startParsedRecord.startTime;
|
||
const lastIdx = isFiniteNumber(endFragIdx) ? endFragIdx : details.fragments.length;
|
||
for (let i = startFragIdx; i < lastIdx; ++i) {
|
||
const mediaFragment = details.fragments[i];
|
||
if (isFiniteNumber(activeDiscoSeqNum) && mediaFragment.discoSeqNum !== activeDiscoSeqNum) {
|
||
continue;
|
||
}
|
||
if (i === lastIdx - 1) {
|
||
// get the fragment before nextFragSN (in case we guessed too late during the last inference)
|
||
this.logger.info(`[subtitle] from frag sn ${subtitleBufferInfo.prevFragSN}, infer frag sn ${mediaFragment.mediaSeqNum} using lastIdx ${lastIdx}. startPos ${startPos} duration ${mediaFragment.duration} position ${position}`);
|
||
foundFrag = { foundFrag: mediaFragment, timelineEstablished: true };
|
||
break;
|
||
}
|
||
else if (startPos + mediaFragment.duration > position && i > startFragIdx) {
|
||
// we have already loaded frag prevFragSN, so we are really only interested in frag (prevFragSN + 1) and beyond
|
||
this.logger.info(`[subtitle] from frag sn ${subtitleBufferInfo.prevFragSN}, infer frag sn ${mediaFragment.mediaSeqNum}: startPos ${startPos} + duration ${mediaFragment.duration} > ${position}`);
|
||
foundFrag = { foundFrag: mediaFragment, timelineEstablished: true };
|
||
break;
|
||
}
|
||
startPos += mediaFragment.duration;
|
||
}
|
||
}
|
||
else if (isFiniteNumber(endFragIdx) && endFragIdx >= 0 && endFragIdx < details.fragments.length && endParsedRecord) {
|
||
let startPos = endParsedRecord.startTime; // endParsedRecord.startTime is fragments[endFragIdx].start
|
||
for (let i = endFragIdx - 1; i >= 0; --i) {
|
||
// keep probing the earlier fragment as long as the later/previous fragment's startPos > position
|
||
const mediaFragment = details.fragments[i];
|
||
if (isFiniteNumber(activeDiscoSeqNum) && mediaFragment.discoSeqNum !== activeDiscoSeqNum) {
|
||
continue;
|
||
}
|
||
if (startPos <= position) {
|
||
// we want one fragment earlier than position, which is mediaFragment (because startPos belongs to the later/previous fragment)
|
||
this.logger.info(`[subtitle] from frag sn ${subtitleBufferInfo.nextFragSN}, infer frag sn ${mediaFragment.mediaSeqNum}: startPos ${startPos} <= ${position}`);
|
||
foundFrag = { foundFrag: mediaFragment, timelineEstablished: true };
|
||
break;
|
||
}
|
||
startPos -= mediaFragment.duration;
|
||
}
|
||
}
|
||
else {
|
||
for (let i = 0; i < details.fragments.length; ++i) {
|
||
const mediaFragment = details.fragments[i];
|
||
if (isFiniteNumber(activeDiscoSeqNum) && mediaFragment.discoSeqNum === activeDiscoSeqNum) {
|
||
this.logger.info(`[subtitle] infer frag sn ${mediaFragment.mediaSeqNum} in cc ${activeDiscoSeqNum}`);
|
||
foundFrag = { foundFrag: mediaFragment, timelineEstablished: false };
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return foundFrag;
|
||
}
|
||
generateFragmentBatch(batchSize, activeDiscoSeqNum, fragResult, subtitleParsedInfo, subtitleBufferInfo, details) {
|
||
var _a;
|
||
const generatedFrags = [];
|
||
const anchorFrag = fragResult === null || fragResult === void 0 ? void 0 : fragResult.foundFrag;
|
||
if (!anchorFrag) {
|
||
return { foundFrags: undefined, subtitleParsedInfo: undefined, subtitleBufferInfo: undefined, timelineEstablished: fragResult === null || fragResult === void 0 ? void 0 : fragResult.timelineEstablished };
|
||
}
|
||
const startIdx = anchorFrag ? anchorFrag.mediaSeqNum - details.startSN : details.fragments.length;
|
||
for (let i = startIdx; i < details.fragments.length && generatedFrags.length < batchSize; ++i) {
|
||
const frag = details.fragments[i];
|
||
if (frag.discoSeqNum !== activeDiscoSeqNum) {
|
||
continue;
|
||
}
|
||
const bufferedCueCount = (_a = subtitleBufferInfo.fragInfoMap[frag.mediaSeqNum]) === null || _a === void 0 ? void 0 : _a.count;
|
||
if (!this.isFragmentCompleteOrEmpty(frag.mediaSeqNum, bufferedCueCount !== null && bufferedCueCount !== void 0 ? bufferedCueCount : 0, subtitleParsedInfo)) {
|
||
generatedFrags.push(frag); // include frag if not fully parsed
|
||
}
|
||
}
|
||
return { foundFrags: generatedFrags, subtitleParsedInfo, subtitleBufferInfo, timelineEstablished: fragResult === null || fragResult === void 0 ? void 0 : fragResult.timelineEstablished };
|
||
}
|
||
findFragmentsForPosition(currentTime, activeDiscoSeqNum, details) {
|
||
// get past addCue records
|
||
const subtitleParsedRecord = this.mediaSink.mediaQuery.getParsedSubtitleRecordsForMediaOption(this.selectedTrack.persistentID);
|
||
// compose addCue records from remaining cues in current texttrack
|
||
const cues = this.getCuesOfEnabledTrack(this.selectedMediaOption.mediaOptionId, false /* this.enabledTrack.sideTrackId !== undefined */);
|
||
const subtitleBufferInfo = this.calculateFragInfoMap(currentTime, cues, subtitleParsedRecord, details);
|
||
const bufferInfo = subtitleBufferInfo.bufferInfo;
|
||
const loadPos = Math.max(currentTime, bufferInfo.end);
|
||
const fragResult = this.inferSubtitleFragmentForPosition(loadPos, activeDiscoSeqNum, subtitleParsedRecord, subtitleBufferInfo, details);
|
||
return this.generateFragmentBatch(Infinity, activeDiscoSeqNum, fragResult, subtitleParsedRecord, subtitleBufferInfo, details);
|
||
}
|
||
get selectedMediaOption() {
|
||
return this.selectedTrack ? this.selectedTrack : this._disabledMediaOption;
|
||
}
|
||
set selectedMediaOption(mediaOption) {
|
||
this.selectedTrack = isAlternateMediaOption(mediaOption) ? mediaOption : undefined;
|
||
}
|
||
get selectedTrack() {
|
||
return this._selectedMediaOption;
|
||
}
|
||
set selectedTrack(track) {
|
||
if (track === this._selectedMediaOption) {
|
||
return;
|
||
}
|
||
this.logger.info(`[subtitle] pick track #${track === null || track === void 0 ? void 0 : track.id} mediaOptionId ${track === null || track === void 0 ? void 0 : track.mediaOptionId} persistentId ${track === null || track === void 0 ? void 0 : track.persistentID}`);
|
||
this._selectedMediaOption = track;
|
||
this.updateTextTrackState();
|
||
}
|
||
getTrack(mediaOptionId) {
|
||
return this._availableMediaOptions.find((mediaOption) => mediaOption.mediaOptionId === mediaOptionId);
|
||
}
|
||
updateTextTrackState() {
|
||
if (!this.mediaSink.textTracks) {
|
||
// catch an update later
|
||
return;
|
||
}
|
||
const selectedHTMLTextTrack = this.selectedTrack ? this.getExistingHTMLTextTrack(this.selectedTrack) : undefined;
|
||
const textTracks = filterSelectableTextTracks(this.mediaSink.textTracks);
|
||
for (let id = 0; id < textTracks.length; id++) {
|
||
const aTrack = textTracks[id];
|
||
if (aTrack === selectedHTMLTextTrack && textTracks[id].mode !== 'showing') {
|
||
textTracks[id].mode = 'showing';
|
||
this.logger.info(`textTracks[${id}].mode = 'showing'`);
|
||
}
|
||
else if (aTrack !== selectedHTMLTextTrack && textTracks[id].mode !== 'hidden') {
|
||
textTracks[id].mode = 'hidden';
|
||
this.logger.info(`textTracks[${id}].mode = 'hidden'`);
|
||
}
|
||
}
|
||
}
|
||
mapHTMLTextTrackIndexToMediaOptionId(searchIndex) {
|
||
const searchTextTrack = this.mediaSink.textTracks[searchIndex];
|
||
let foundOptionId;
|
||
this.htmlTextTrackMap.forEach((textTrack, optionId) => {
|
||
if (searchTextTrack === textTrack) {
|
||
foundOptionId = optionId;
|
||
}
|
||
});
|
||
return foundOptionId;
|
||
}
|
||
get mediaSelectionOptions() {
|
||
return this._availableMediaOptions;
|
||
}
|
||
_makeDisableOption(mediaOption) {
|
||
return { itemId: mediaOption.itemId, mediaOptionType: mediaOption.mediaOptionType, mediaOptionId: 'Nah' };
|
||
}
|
||
_onTextTracksChanged() {
|
||
// Media is undefined when switching streams via loadSource()
|
||
if (!this.mediaSink) {
|
||
return;
|
||
}
|
||
let newTrackEvent = false;
|
||
let showingPersistentId;
|
||
const textTracks = filterSelectableTextTracks(this.mediaSink.textTracks);
|
||
let newTracksSeenAndIgnored = 0;
|
||
for (let id = 0; id < textTracks.length; id++) {
|
||
if (textTracks[id].seen) {
|
||
// Existing text tracks
|
||
if (textTracks[id].mode === 'showing') {
|
||
showingPersistentId = textTracks[id].persistentId;
|
||
}
|
||
}
|
||
else {
|
||
// Newly added text track always has 'hidden' mode by default.
|
||
// We must not make any selection change based on "new track" event.
|
||
// A real track change event may be in-flight and will be overriden later if we re-select an active subtitle track or 'undefined' now.
|
||
textTracks[id].seen = true;
|
||
newTracksSeenAndIgnored += 1;
|
||
newTrackEvent = true;
|
||
}
|
||
}
|
||
this.logger.info(`New tracks marked seen and ignored: ${newTracksSeenAndIgnored} vs total: ${textTracks.length}`);
|
||
if (!newTrackEvent) {
|
||
const currentSelectedMediaOption = this.selectedTrack;
|
||
if ((currentSelectedMediaOption === null || currentSelectedMediaOption === void 0 ? void 0 : currentSelectedMediaOption.persistentID) !== showingPersistentId) {
|
||
const newOption = this.mediaSelectionOptions.find(function (mediaOption) {
|
||
return mediaOption.persistentID === showingPersistentId;
|
||
});
|
||
this.nativeSubtitleTrackChange$.next(newOption ? newOption : this._disabledMediaOption);
|
||
}
|
||
}
|
||
}
|
||
addCues(channel, startTime, endTime, screen) {
|
||
// skip cues which overlap more than 50% with previously parsed time ranges
|
||
const ranges = this.cueRanges;
|
||
let merged = false;
|
||
for (let i = ranges.length; i--;) {
|
||
const cueRange = ranges[i];
|
||
const overlap = intersection(cueRange[0], cueRange[1], startTime, endTime);
|
||
if (overlap >= 0) {
|
||
cueRange[0] = Math.min(cueRange[0], startTime);
|
||
cueRange[1] = Math.max(cueRange[1], endTime);
|
||
merged = true;
|
||
if (overlap / (endTime - startTime) > 0.5) {
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
if (!merged) {
|
||
ranges.push([startTime, endTime]);
|
||
}
|
||
this.Cues.newCue(this.channelToTrackMap[channel], startTime, endTime, screen, this.mediaSink);
|
||
}
|
||
getExistingHTMLTextTrackWithChannelNumber(channelNumber) {
|
||
const mediaSink = this.mediaSink;
|
||
if (mediaSink) {
|
||
for (let i = 0; i < mediaSink.textTracks.length; i++) {
|
||
const textTrack = mediaSink.textTracks[i];
|
||
const propName = 'cc' + channelNumber;
|
||
if (isChannelTextTrackKey(propName) && textTrack[propName] === true) {
|
||
return textTrack;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
sendAddTrackEvent(track, mediaSink) {
|
||
let e = null;
|
||
try {
|
||
e = new window.Event('addtrack');
|
||
}
|
||
catch (err) {
|
||
// for IE11
|
||
e = document.createEvent('Event');
|
||
e.initEvent('addtrack', false, false);
|
||
}
|
||
e.track = track;
|
||
mediaSink.dispatchEvent(e);
|
||
}
|
||
createHTMLCaptionsTrackGuts(channelNumber, name, languageCode, forced) {
|
||
const trackVar = 'cc' + channelNumber;
|
||
if (!this.channelToTrackMap[trackVar]) {
|
||
// Enable reuse of existing closed caption text track.
|
||
const existingHTMLTrack = this.getExistingHTMLTextTrackWithChannelNumber(channelNumber);
|
||
if (!existingHTMLTrack) {
|
||
const textTrack = this.createHTMLTextTrackGuts('captions', name, languageCode, forced);
|
||
if (textTrack && isChannelTextTrackKey(trackVar)) {
|
||
textTrack[trackVar] = true;
|
||
this.channelToTrackMap[trackVar] = textTrack;
|
||
}
|
||
}
|
||
else {
|
||
this.channelToTrackMap[trackVar] = existingHTMLTrack;
|
||
clearCurrentCues(this.channelToTrackMap[trackVar]);
|
||
this.sendAddTrackEvent(this.channelToTrackMap[trackVar], this.mediaSink);
|
||
}
|
||
}
|
||
return this.channelToTrackMap[trackVar];
|
||
}
|
||
createHTMLCaptionsTrack(channel) {
|
||
return this.createHTMLCaptionsTrackGuts(channel, this.config[channel === 1 ? 'captionsTextTrack1Label' : 'captionsTextTrack2Label'], this.config.captionsTextTrack1LanguageCode, false);
|
||
}
|
||
getExistingHTMLTextTrack(track) {
|
||
// this.logger.info(`getExistingHTMLTextTrack condenseSubtitleTrack ${this.config.condenseSubtitleTrack} track.persistentID ${track.persistentID} track.id ${track.id}`);
|
||
return this.config.condenseSubtitleTrack ? this.htmlTextTrackMap.get(track.persistentID) : this.htmlTextTrackMap.get(track.id);
|
||
}
|
||
getExistingHTMLTextTrackWithSubtitleTrackId(id) {
|
||
const altOption = this._availableMediaOptions.find((mediaOption) => mediaOption.id === id);
|
||
const htmlTextTrack = altOption ? this.getExistingHTMLTextTrack(altOption) : undefined;
|
||
this.logger.debug(`[subtitle] map track id ${id} to ${htmlTextTrack === null || htmlTextTrack === void 0 ? void 0 : htmlTextTrack.label} ${htmlTextTrack === null || htmlTextTrack === void 0 ? void 0 : htmlTextTrack.kind} ${htmlTextTrack === null || htmlTextTrack === void 0 ? void 0 : htmlTextTrack.language}`);
|
||
return htmlTextTrack;
|
||
}
|
||
getExistingHTMLTextTrackIndex(track) {
|
||
const htmlTextTrack = this.getExistingHTMLTextTrack(track);
|
||
const tracks = this.mediaSink.textTracks;
|
||
let found = -1;
|
||
for (let i = 0; i < tracks.length; ++i) {
|
||
if (tracks[i] === htmlTextTrack) {
|
||
found = i;
|
||
break;
|
||
}
|
||
}
|
||
return found;
|
||
}
|
||
setExistingHTMLTextTrack(track, textTrack) {
|
||
textTrack.persistentId = track.persistentID;
|
||
if (this.config.condenseSubtitleTrack) {
|
||
// this.logger.info(`setExistingHTMLTextTrack: track.persistentID ${track.persistentID}, textTrack ${textTrack?.id} ${textTrack}`);
|
||
return this.htmlTextTrackMap.set(track.persistentID, textTrack);
|
||
}
|
||
else {
|
||
// this.logger.info(`setExistingHTMLTextTrack: track.id ${track.id}, textTrack ${textTrack?.id} ${textTrack}`);
|
||
return this.htmlTextTrackMap.set(track.id, textTrack);
|
||
}
|
||
}
|
||
createHTMLTextTrack(track) {
|
||
let HTMLTextTrack = this.getExistingHTMLTextTrack(track);
|
||
if (!HTMLTextTrack) {
|
||
if (track.mediaType === 'sbtl') {
|
||
// this.logger.info(`create subtitle track ${track.name} from manifest`);
|
||
this.subtitleTracksCreated += 1;
|
||
HTMLTextTrack = this.createHTMLTextTrackGuts('subtitles', track.name, track.lang, track.forced);
|
||
}
|
||
else {
|
||
let channelNumber = 1;
|
||
if (track.inStreamID) {
|
||
channelNumber = Number(track.inStreamID.substring(2));
|
||
}
|
||
// this.logger.info(`create caption track ${track.name} from manifest`);
|
||
this.captionTracksCreated += 1;
|
||
HTMLTextTrack = this.createHTMLCaptionsTrackGuts(channelNumber, track.name, track.lang, false);
|
||
}
|
||
if (HTMLTextTrack) {
|
||
this.setExistingHTMLTextTrack(track, HTMLTextTrack);
|
||
/* const data = {
|
||
trackId: track.id,
|
||
mediaOptionId: track.mediaOptionId,
|
||
persistentId: track.persistentID,
|
||
kind: HTMLTextTrack.kind,
|
||
label: HTMLTextTrack.label,
|
||
language: HTMLTextTrack.language,
|
||
};
|
||
this.logger.info(`textTrackCreated: ${JSON.stringify(data)}`);
|
||
*/
|
||
}
|
||
else {
|
||
this.logger.error(`failed to create HTML text track for track ${track.id}: persistent id ${track.persistentID} name ${track.name} lang ${track.lang} inStreamID ${track.inStreamID}`);
|
||
this.tracksFailed += 1;
|
||
}
|
||
}
|
||
else {
|
||
// this.logger.info(`reuse HTML text track for track ${track.id}: persistent id ${HTMLTextTrack.persistentId} kind ${HTMLTextTrack.kind} label ${HTMLTextTrack.label} lang ${HTMLTextTrack.language}`);
|
||
this.tracksReused += 1;
|
||
}
|
||
return HTMLTextTrack;
|
||
}
|
||
createHTMLTextTrackGuts(kind, label, lang, forced) {
|
||
const mediaSink = this.mediaSink;
|
||
if (mediaSink) {
|
||
let customTextTrackCueRenderer = false;
|
||
if (kind !== 'metadata' && this.config.customTextTrackCueRenderer) {
|
||
customTextTrackCueRenderer = true;
|
||
kind = 'metadata';
|
||
}
|
||
const textTrack = mediaSink.addTextTrack(kind, label, lang);
|
||
if (customTextTrackCueRenderer) {
|
||
textTrack.customTextTrackCueRenderer = true;
|
||
}
|
||
return textTrack;
|
||
}
|
||
return undefined;
|
||
}
|
||
resetLoadSource() {
|
||
this.resetTracks();
|
||
}
|
||
resetTracks() {
|
||
this.logger.info('clean out all cues and cueRanges');
|
||
this._cleanTracks();
|
||
this.cueRanges = [];
|
||
}
|
||
_cleanTracks() {
|
||
// clear outdated subtitles
|
||
const mediaSink = this.mediaSink;
|
||
if (mediaSink) {
|
||
const textTracks = mediaSink.textTracks;
|
||
if (textTracks) {
|
||
for (let i = 0; i < textTracks.length; i++) {
|
||
clearCurrentCues(textTracks[i]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
getCuesOfEnabledTrack(trackId, checkForWebVTTCue = false) {
|
||
let cues = [];
|
||
if (!checkForWebVTTCue) {
|
||
this.logger.info(`[subtitle] return regular cue list for track ${trackId}`);
|
||
cues = this._getCuesOfEnabledTrack(trackId);
|
||
}
|
||
else {
|
||
const sideTrackCues = this._getCuesOfEnabledTrack(trackId);
|
||
for (let i = 0; i < sideTrackCues.length; i++) {
|
||
const targetCue = sideTrackCues[i];
|
||
if (isExtendedTextTrackCue(targetCue)) {
|
||
// this.logger.info(`[subtitle] consider WebVTT cue: ${targetCue.startTime} ${targetCue.endTime} '${targetCue.text}'`);
|
||
cues.push(targetCue);
|
||
}
|
||
}
|
||
}
|
||
return cues;
|
||
}
|
||
_getCuesOfEnabledTrack(trackId) {
|
||
const track = this.getTrack(trackId);
|
||
const htmlTrackKey = this.config.condenseSubtitleTrack ? track === null || track === void 0 ? void 0 : track.persistentID : track === null || track === void 0 ? void 0 : track.id;
|
||
const enabledHTMLTrack = this.htmlTextTrackMap.get(htmlTrackKey);
|
||
if (enabledHTMLTrack && enabledHTMLTrack.cues) {
|
||
return Array.from(enabledHTMLTrack.cues);
|
||
}
|
||
else {
|
||
return [];
|
||
}
|
||
}
|
||
attachSubtitleTracks() {
|
||
// We need both onSubtitleTrackUpdated and onMediaAttaching to resolve before attaching subtitle tracks
|
||
if (!this.gotTracks) {
|
||
return;
|
||
}
|
||
this.subtitleTracksCreated = 0;
|
||
this.captionTracksCreated = 0;
|
||
this.tracksReused = 0;
|
||
this.tracksFailed = 0;
|
||
this.tracks.forEach((track) => {
|
||
this.createHTMLTextTrack(track);
|
||
});
|
||
const data = {
|
||
totalTracks: this.tracks.length,
|
||
subtitleTracksCreated: this.subtitleTracksCreated,
|
||
captionTracksCreated: this.captionTracksCreated,
|
||
tracksReused: this.tracksReused,
|
||
tracksFailed: this.tracksFailed,
|
||
};
|
||
this.logger.qe({ critical: true, name: 'textTrackCreated', data });
|
||
}
|
||
setTracks(mediaOptions, enabledMediaOption, disabledMediaOption) {
|
||
this._cleanTracks();
|
||
this.htmlTextTrackMap = new Map();
|
||
this.cueRanges = [];
|
||
if (this.config.enableWebVTT) {
|
||
this.tracks = mediaOptions || [];
|
||
}
|
||
this.gotTracks = true;
|
||
this._availableMediaOptions = mediaOptions;
|
||
this._disabledMediaOption = disabledMediaOption;
|
||
this.attachSubtitleTracks();
|
||
this.selectedTrack = enabledMediaOption;
|
||
this.nativeSubtitleTrackChange$ = new Subject();
|
||
this.mediaSink.textTracksCreated = true;
|
||
}
|
||
// Need this to suppress errors when no client is listening for these styles
|
||
onInlineStylesParsed(data) { }
|
||
processSubtitleFrag(enabledMediaOption, frag, initPTS, payload) {
|
||
const data = new Uint8Array(payload);
|
||
const trackIndex = this.getExistingHTMLTextTrackIndex(enabledMediaOption);
|
||
// Convert byteArray into string, replacing any somewhat exotic linefeeds with "\n", then split on that character.
|
||
/*
|
||
let re = /\r\n|\n\r|\n|\r/g;
|
||
let vtt = BufferUtils.utf8arrayToStr(data).trim().replace(re, '\n').split('\n');
|
||
vtt.forEach((line) => {
|
||
this.logger.info(`vtt.subtitle: ${line}`);
|
||
});
|
||
*/
|
||
if (frag && payload.byteLength) {
|
||
const fragCueRange = this._parseVTTs(trackIndex, frag, initPTS, data);
|
||
if (fragCueRange && isFiniteNumber(fragCueRange.startTime)) {
|
||
// startTime is valid only if there are cues (duplicated or otherwise) in the fragment
|
||
this.lastCueEndTime = Math.max(this.lastCueEndTime, fragCueRange.endTime);
|
||
this.logger.debug(`[subtitle] update lastCueEndTime ${this.lastCueEndTime}`);
|
||
}
|
||
return fragCueRange;
|
||
}
|
||
return;
|
||
}
|
||
_parseVTTs(textTrackIdx, frag, initPTS, payload) {
|
||
// Parse the WebVTT file contents.
|
||
let parsedfragCueRecord;
|
||
HLSVTTParser$1.parse(payload, initPTS, frag.start, frag.discoSeqNum, (cues) => {
|
||
// Sometimes there are cue overlaps on segmented vtts so the same
|
||
// cue can appear more than once in different vtt files.
|
||
// This avoid showing duplicated cues with same timecode and text.
|
||
const textTrack = this.mediaSink.textTracks[textTrackIdx];
|
||
// Add cues and trigger event with success true.
|
||
const fragCueRecord = { count: 0, startTime: Number.POSITIVE_INFINITY, endTime: 0 };
|
||
cues.map((cue) => {
|
||
if (textTrack && (!textTrack.cues || !textTrack.cues.getCueById(cue.id))) {
|
||
this.logger.trace(`new cue for subtitle track[${frag.mediaOptionId}] frag sn ${frag.mediaSeqNum}: #${cue.id} [${cue.startTime}, ${cue.endTime}]: ${cue.text}`);
|
||
cue.fragSN = frag.mediaSeqNum;
|
||
cue.webVTTCue = true;
|
||
textTrack.addCue(cue);
|
||
fragCueRecord.count++;
|
||
this.logger.debug(`[subtitle] cue added. fragCueRecord ${JSON.stringify(fragCueRecord)}`);
|
||
}
|
||
fragCueRecord.startTime = Math.min(cue.startTime, fragCueRecord.startTime);
|
||
fragCueRecord.endTime = Math.max(cue.endTime, fragCueRecord.endTime);
|
||
this.logger.debug(`[subtitle] frag processed. fragCueRecord ${JSON.stringify(fragCueRecord)}`);
|
||
});
|
||
this.logger.debug(`[subtitle] frag processing done. fragCueRecord ${JSON.stringify(fragCueRecord)}`);
|
||
// fragCueRecord:
|
||
// * startTime is a finite number only if there is at least 1 cue in the fragment
|
||
// * count may be zero if duplicated cue (finite startTime) or no cue (PostiveInfinity startTime)
|
||
parsedfragCueRecord = fragCueRecord;
|
||
this.mediaSink.archiveParsedSubtitleFragmentRecord(this.selectedTrack.persistentID, frag.mediaSeqNum, fragCueRecord);
|
||
}, (e) => {
|
||
// Something went wrong while parsing. Trigger event with success false.
|
||
this.logger.info(`Failed to parse VTT cue: ${e}`);
|
||
}, (styles) => {
|
||
this.hls.trigger(HlsEvent$1.INLINE_STYLES_PARSED, { styles: styles });
|
||
}, this.logger);
|
||
return parsedfragCueRecord;
|
||
}
|
||
_ensureParser() {
|
||
if (!this.cea608Parser) {
|
||
const channel1 = new OutputFilter(this, 1);
|
||
const channel2 = new OutputFilter(this, 2);
|
||
this.cea608Parser = new Cea608Parser$1(0, channel1, channel2);
|
||
}
|
||
}
|
||
setupForFrag(frag) {
|
||
if (!frag || frag.mediaOptionType !== MediaOptionType.Variant || frag.iframe) {
|
||
return;
|
||
}
|
||
const sn = frag.mediaSeqNum;
|
||
// if this frag isn't contiguous, clear the parser so cues with bad start/end times aren't added to the textTrack
|
||
if (sn !== this.lastVariantSeqNum + 1) {
|
||
this.resetClosedCaptionParser();
|
||
}
|
||
this.lastVariantSeqNum = sn;
|
||
}
|
||
resetClosedCaptionParser() {
|
||
var _a;
|
||
this.logger.info('reset cea608Parser');
|
||
(_a = this.cea608Parser) === null || _a === void 0 ? void 0 : _a.reset();
|
||
}
|
||
/**
|
||
* Add CLCP / ID3 samples
|
||
* @param offsetTimestamp offsetTimestamp is offset used for SourceBuffer timestampOffset calculation. All samples should be shifted by this value
|
||
* @param captionData Parsed caption data from demuxer
|
||
* @param id3Samples Parsed id3 samples from demuxer
|
||
* @param endPTS end Presentation Timestamp of the fragment
|
||
*/
|
||
addLegibleSamples(offsetTimestamp, captionData, id3Samples, endPTS) {
|
||
if (captionData) {
|
||
this.addClosedCaptionSamples(offsetTimestamp, captionData);
|
||
}
|
||
if (id3Samples && id3Samples.length > 0) {
|
||
this.addId3Samples(offsetTimestamp, id3Samples, convertTimestampToSeconds(endPTS));
|
||
}
|
||
}
|
||
addClosedCaptionSamples(offsetTimestamp, captionData) {
|
||
if (captionData.mp4) {
|
||
this.addMP4CaptionSamples(offsetTimestamp, captionData.mp4);
|
||
}
|
||
else if (captionData.ts) {
|
||
this.addTSCaptionSamples(offsetTimestamp, captionData.ts);
|
||
}
|
||
}
|
||
addMP4CaptionSamples(offsetTimestamp, samples) {
|
||
// push all of the CEA-608 messages into the interpreter
|
||
// immediately. It will create the proper timestamps based on our PTS value
|
||
if (this.enableCaption && this.config.enableCEA708Captions) {
|
||
const offsetTimestampSec = convertTimestampToSeconds(offsetTimestamp);
|
||
this._ensureParser();
|
||
for (let i = 0; i < samples.length; i++) {
|
||
let startSec = samples[i].pts - offsetTimestampSec;
|
||
const ccData = samples[i].bytes;
|
||
for (let j = 0; j < ccData.length; j += 2) {
|
||
const doubleByte = [];
|
||
doubleByte.push(ccData[j]);
|
||
if (j + 1 < ccData.length) {
|
||
doubleByte.push(ccData[j + 1]);
|
||
}
|
||
else {
|
||
this.logger.info(`CEA608 sample ${i} not even length (index ${j + 1} >= length ${ccData.length})`);
|
||
doubleByte.push(80); // safest character to push is 80 (noop)
|
||
}
|
||
this.cea608Parser.addData(startSec, doubleByte);
|
||
startSec += cea608Duration;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
addTSCaptionSamples(offsetTimestamp, samples) {
|
||
if (this.enableCaption && this.config.enableCEA708Captions) {
|
||
const offsetTimestampSec = convertTimestampToSeconds(offsetTimestamp);
|
||
this._ensureParser();
|
||
for (let i = 0; i < samples.length; i++) {
|
||
const startSec = samples[i].pts - offsetTimestampSec;
|
||
const ccdatas = LegibleSystemAdapter.extractCea608Data(samples[i].bytes);
|
||
this.cea608Parser.addData(startSec, ccdatas);
|
||
}
|
||
}
|
||
}
|
||
addId3Samples(offsetTimestamp, samples, endPTSSec) {
|
||
if (!this.config.enableID3Cues) {
|
||
this.logger.info('id3 cues disabled by config');
|
||
return;
|
||
}
|
||
// Attempt to recreate Safari functionality by creating
|
||
// WebKitDataCue objects when available and store the decoded
|
||
// ID3 data in the value property of the cue
|
||
const Cue = window.WebKitDataCue || window.VTTCue || window.TextTrackCue;
|
||
const offsetTimestampSec = convertTimestampToSeconds(offsetTimestamp);
|
||
for (let i = 0; i < samples.length; i++) {
|
||
const startSec = samples[i].pts - offsetTimestampSec;
|
||
let endSec = (i < samples.length - 1 ? samples[i + 1].pts : endPTSSec) - offsetTimestampSec;
|
||
// Give a slight bump to the endTime if it's equal to startTime to avoid a SyntaxError in IE
|
||
if (startSec === endSec) {
|
||
endSec += 0.0001;
|
||
}
|
||
if (samples[i].frames) {
|
||
samples[i].frames.forEach((frame) => {
|
||
if (frame && !this.id3shouldIgnore(frame)) {
|
||
const cue = new Cue(startSec, endSec, '');
|
||
cue.value = frame;
|
||
this.logger.trace(`[id3] addCue [${startSec},${endSec}] ${JSON.stringify(cue.value).substr(0, 20)}...`);
|
||
this.id3Track.addCue(cue);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
id3shouldIgnore(frame) {
|
||
return frame.key === 'PRIV' && (frame.info === 'com.apple.streaming.transportStreamTimestamp' || frame.info === 'com.apple.streaming.audioDescription');
|
||
}
|
||
static extractCea608Data(byteArray) {
|
||
const count = byteArray[0] & 31;
|
||
let position = 2;
|
||
let tmpByte, ccbyte1, ccbyte2, ccValid, ccType;
|
||
const actualCCBytes = [];
|
||
for (let j = 0; j < count; j++) {
|
||
tmpByte = byteArray[position++];
|
||
ccbyte1 = 127 & byteArray[position++];
|
||
ccbyte2 = 127 & byteArray[position++];
|
||
ccValid = (4 & tmpByte) !== 0;
|
||
ccType = 3 & tmpByte;
|
||
if (ccbyte1 === 0 && ccbyte2 === 0) {
|
||
continue;
|
||
}
|
||
if (ccValid) {
|
||
if (ccType === 0) {
|
||
// || ccType === 1
|
||
actualCCBytes.push(ccbyte1);
|
||
actualCCBytes.push(ccbyte2);
|
||
}
|
||
}
|
||
}
|
||
return actualCCBytes;
|
||
}
|
||
}
|
||
|
||
const makeLegibleService = (source$, config, eventEmitter, logger) => {
|
||
return source$.pipe(tag('playback.legibleServiceEpic.in'), switchMap((mediaSink) => {
|
||
if (!mediaSink) {
|
||
return of(null);
|
||
}
|
||
const textTrackAdapter = new LegibleSystemAdapter(mediaSink, config, eventEmitter, logger);
|
||
return merge(of(textTrackAdapter), textTrackAdapter);
|
||
}), tag('playback.legibleServiceEpic.emit'));
|
||
};
|
||
|
||
const SessionDataStatusCode = {
|
||
// Typucally (positive) HTTP Status code for network errors.
|
||
// Plus these ...
|
||
JSON_PARSE_ERROR: -1,
|
||
};
|
||
|
||
const loggerName$6 = { name: 'plist' };
|
||
function stringIsXMLPlist(data) {
|
||
const XML_TAG_REGEX = /[\s]*<\?xml/i;
|
||
const PLIST_TAG_REGEX = /[\s]*<plist/i;
|
||
XML_TAG_REGEX.lastIndex = 0;
|
||
const isXML = XML_TAG_REGEX.exec(data);
|
||
if (!isXML) {
|
||
return !!PLIST_TAG_REGEX.exec(data);
|
||
}
|
||
return !!isXML;
|
||
}
|
||
function stringIsJSONPlist(data) {
|
||
const JSON_REGEX = /[\s]*[\{\[]/;
|
||
JSON_REGEX.lastIndex = 0;
|
||
return !!JSON_REGEX.exec(data);
|
||
}
|
||
function convertPlistToJSON(domNode) {
|
||
var _a, _b;
|
||
if (!domNode) {
|
||
return null;
|
||
}
|
||
const logger = getLogger();
|
||
let jsonObj = null;
|
||
const childNodes = domNode.childNodes;
|
||
if (childNodes) {
|
||
if (domNode.tagName) {
|
||
const tagName = (_a = domNode.tagName) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
||
// plist branch elements all have valid tag MediaOptionNames
|
||
if (tagName === 'plist') {
|
||
jsonObj = [];
|
||
for (let i = 0; i < childNodes.length; ++i) {
|
||
const child = childNodes[i];
|
||
if (child.tagName) {
|
||
jsonObj.push(convertPlistToJSON(child));
|
||
}
|
||
}
|
||
if (jsonObj.length === 1) {
|
||
jsonObj = jsonObj[0];
|
||
}
|
||
}
|
||
if (tagName === 'array') {
|
||
jsonObj = [];
|
||
for (let i = 0; i < childNodes.length; ++i) {
|
||
const child = childNodes[i];
|
||
if (child.tagName) {
|
||
jsonObj.push(convertPlistToJSON(child));
|
||
}
|
||
}
|
||
}
|
||
if (tagName === 'dict') {
|
||
let key;
|
||
let value;
|
||
let validIndex = 0;
|
||
jsonObj = {};
|
||
for (let i = 0; i < childNodes.length; i++) {
|
||
const child = childNodes[i];
|
||
const cTagName = (_b = child.tagName) === null || _b === void 0 ? void 0 : _b.toLowerCase();
|
||
if (cTagName) {
|
||
if (validIndex % 2 === 0) {
|
||
if (cTagName === 'key') {
|
||
key = convertPlistToJSON(child);
|
||
validIndex++;
|
||
}
|
||
else {
|
||
logger.info(loggerName$6, `Skipping unknown dict element ${i} => ${cTagName}`);
|
||
}
|
||
}
|
||
else {
|
||
value = convertPlistToJSON(child);
|
||
jsonObj[key] = value;
|
||
key = null;
|
||
value = null;
|
||
validIndex++;
|
||
}
|
||
}
|
||
}
|
||
if (key) {
|
||
// Last value may be missing. Treat it as NULL.
|
||
jsonObj[key] = value;
|
||
logger.info(loggerName$6, `Orphaned pair: ${key} = ${JSON.stringify(value)}`);
|
||
key = null;
|
||
value = null;
|
||
}
|
||
}
|
||
else if (tagName === 'key') {
|
||
// Leaf dict elements are wrapped in the sole child of TextNode. Reach down 1 level to get the value.
|
||
const firstChild = childNodes[0];
|
||
if (firstChild) {
|
||
jsonObj = firstChild.nodeValue;
|
||
}
|
||
else {
|
||
// Should not happen
|
||
logger.warn(loggerName$6, 'Invalid dict key: Key is null, probably like this: <key/>');
|
||
jsonObj = null;
|
||
}
|
||
}
|
||
else if (tagName === 'string') {
|
||
const firstChild = childNodes[0];
|
||
if (firstChild) {
|
||
jsonObj = firstChild.nodeValue;
|
||
}
|
||
else {
|
||
jsonObj = null;
|
||
}
|
||
}
|
||
else if (tagName === 'integer') {
|
||
const firstChild = childNodes[0];
|
||
if (firstChild) {
|
||
jsonObj = parseInt(firstChild.nodeValue);
|
||
}
|
||
else {
|
||
jsonObj = 0;
|
||
}
|
||
}
|
||
else if (tagName === 'float') {
|
||
const firstChild = childNodes[0];
|
||
if (firstChild) {
|
||
jsonObj = parseFloat(firstChild.nodeValue);
|
||
}
|
||
else {
|
||
jsonObj = 0;
|
||
}
|
||
}
|
||
else if (tagName === 'date') {
|
||
const firstChild = childNodes[0];
|
||
if (firstChild) {
|
||
jsonObj = new Date(firstChild.nodeValue);
|
||
}
|
||
else {
|
||
jsonObj = null;
|
||
}
|
||
}
|
||
else if (tagName === 'data') {
|
||
const firstChild = childNodes[0];
|
||
if (firstChild) {
|
||
// base64 encoded
|
||
jsonObj = atob(firstChild.nodeValue);
|
||
}
|
||
else {
|
||
jsonObj = null;
|
||
}
|
||
}
|
||
else if (tagName === 'true') {
|
||
jsonObj = true;
|
||
}
|
||
else if (tagName === 'false') {
|
||
jsonObj = false;
|
||
}
|
||
}
|
||
else {
|
||
// Node without tagName may exist in the XML DOM tree.
|
||
// See if its child/children are valid plist elements.
|
||
if (childNodes.length < 1) {
|
||
// This should never happen
|
||
logger.warn(loggerName$6, `unknown node with unknown value > nodeType=${domNode.nodeType} tagName=${domNode.tagName} nodeName=${domNode.nodeName} value=${domNode.nodeValue}`);
|
||
}
|
||
else {
|
||
jsonObj = [];
|
||
for (let i = 0; i < childNodes.length; ++i) {
|
||
const child = childNodes[i];
|
||
if (child.tagName) {
|
||
jsonObj.push(convertPlistToJSON(child));
|
||
}
|
||
}
|
||
if (jsonObj.length === 1) {
|
||
// If unknown node has only 1 child, replace it with its child's value.
|
||
// Essentially drop the unknown node.
|
||
jsonObj = jsonObj[0];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return jsonObj;
|
||
}
|
||
|
||
class SessionDataLoader {
|
||
constructor(config, xhrLoader, customUrlLoader, logger) {
|
||
this.config = config;
|
||
this.xhrLoader = xhrLoader;
|
||
this.customUrlLoader = customUrlLoader;
|
||
this.sessionDataCheckForCompleteness = (sessionData) => {
|
||
const { sessionDataAutoLoad } = this.config;
|
||
const result = Object.assign({}, sessionData);
|
||
if (!sessionData.complete) {
|
||
if (sessionData.itemList) {
|
||
// Playlist wihout SESSION-DATA tag has an empty itemList array
|
||
result.complete = sessionData.itemList.every((metadata) => {
|
||
if (sessionDataAutoLoad[metadata['DATA-ID']] && !metadata.VALUE && !metadata._STATUS && metadata.URI) {
|
||
this.logger.warn(`Incomplete because ${metadata['DATA-ID']} was autoloaded but no response yet`);
|
||
return false;
|
||
}
|
||
if (sessionDataAutoLoad[metadata['DATA-ID']] && !metadata.URI) {
|
||
this.logger.warn(`id=${metadata['DATA-ID']} missing uri`);
|
||
}
|
||
return true;
|
||
});
|
||
}
|
||
else {
|
||
this.logger.warn('Uninitialized SessionData');
|
||
}
|
||
}
|
||
return result;
|
||
};
|
||
this.logger = logger.child({ name: 'SessionDataLoader' });
|
||
}
|
||
loadSessionData(sessionData) {
|
||
const { sessionDataAutoLoad } = this.config;
|
||
const itemList = sessionData.itemList || [];
|
||
let loadItems$ = of(sessionData);
|
||
itemList.forEach((item) => {
|
||
const id = item['DATA-ID'];
|
||
const uri = item.URI;
|
||
//this.logger.info(`SessionData autoload ${id}: ${JSON.stringify(sessionDataAutoLoad[id])}`);
|
||
if (uri && sessionDataAutoLoad[id]) {
|
||
const url = URLToolkit$1.buildAbsoluteURL(sessionData.baseUrl, uri, { alwaysNormalize: true });
|
||
const responseType = ''; // Use default responseType to allow XML parsing where necessary
|
||
loadItems$ = loadItems$.pipe(switchMap((sd) => {
|
||
return this.loadSessionDataItemWithUrl(url, id, responseType, this.config, sd, this.xhrLoader, this.customUrlLoader).pipe(catchError((err) => {
|
||
this.logger.error(`Error loading SessionData > url=${url}, id=${id}, err=${err}`);
|
||
return of(sd);
|
||
}));
|
||
}));
|
||
}
|
||
});
|
||
return loadItems$.pipe(map((sd) => {
|
||
if (itemList.length < 1) {
|
||
return sd;
|
||
}
|
||
const updated = this.sessionDataCheckForCompleteness(sd);
|
||
if (updated.complete) {
|
||
return updated;
|
||
}
|
||
else {
|
||
throw new ExceptionError(false, 'Session data not complete after loading all items', ErrorResponses.IncompleteSessionData);
|
||
}
|
||
}), finalize$1(() => {
|
||
this.logger.info('finalized');
|
||
}));
|
||
}
|
||
// Loading session data
|
||
// url : session data attribute URL, id : sessionData attribute data-id, responseType: "text", "arraybuffer", or other XMLHTTPRequest responseType
|
||
loadSessionDataItemWithUrl(url, id, responseType, config, sessionData, xhrLoader, customUrlLoader) {
|
||
const logger = getLogger();
|
||
const context = {
|
||
url,
|
||
method: 'GET',
|
||
responseType,
|
||
xhrSetup: config.xhrSetup,
|
||
mimeType: 'application/xml',
|
||
};
|
||
const loadConfig = getLoadConfig({ url }, config.fragLoadPolicy);
|
||
let loader;
|
||
if (isCustomUrl(url)) {
|
||
loader = customUrlLoader(context, loadConfig).pipe(map((res) => {
|
||
return this.onLoadSuccess(sessionData, id, res.data.response.data.toString(), res.data.response.data);
|
||
}));
|
||
}
|
||
else {
|
||
loader = xhrLoader(context, loadConfig).pipe(map(([xhr]) => {
|
||
return this.onLoadSuccess(sessionData, id, xhr.response, xhr.responseXML);
|
||
}));
|
||
}
|
||
return loader.pipe(catchError((err) => {
|
||
if (err instanceof TimeoutError) {
|
||
err = new SessionDataNetworkError(false, err.message, 0, ErrorResponses.SessionDataLoadTimeout);
|
||
}
|
||
else if (err instanceof GenericNetworkError) {
|
||
err = new SessionDataNetworkError(false, err.message, err.code, { code: err.code, text: 'Failed to load SessionData' });
|
||
}
|
||
logger.error(`Unable to load SessionData > err=${err}`);
|
||
return of(this.onLoadError(sessionData, id, err));
|
||
}));
|
||
}
|
||
onLoadSuccess(sessionData, id, response, responseXML) {
|
||
let plistDOM = null;
|
||
let result = sessionData;
|
||
if (stringIsXMLPlist(response) && (plistDOM = responseXML)) {
|
||
// Looks like XML. Use built-in XMLHttpRequest XML parser.
|
||
const attrValue = convertPlistToJSON(plistDOM);
|
||
result = this.setSessionData(sessionData, id, 'VALUE', attrValue);
|
||
}
|
||
else if (stringIsJSONPlist(response)) {
|
||
// JSON format
|
||
try {
|
||
const attrValue = JSON.parse(response);
|
||
result = this.setSessionData(sessionData, id, 'VALUE', attrValue);
|
||
}
|
||
catch (err) {
|
||
this.logger.error(`JSON parser error: ${err}`);
|
||
result = this.setSessionData(sessionData, id, 'VALUE', response);
|
||
result = this.setSessionData(result, id, '_STATUS', SessionDataStatusCode.JSON_PARSE_ERROR);
|
||
}
|
||
}
|
||
else {
|
||
this.logger.info(`Unknown ${id} format. Using raw value`);
|
||
result = this.setSessionData(sessionData, id, 'VALUE', response);
|
||
}
|
||
return result;
|
||
}
|
||
setSessionData(sessionData, id, attrName, obj) {
|
||
let result = sessionData;
|
||
if (sessionData.itemList) {
|
||
let i;
|
||
const itemList = [...sessionData.itemList];
|
||
for (i = 0; i < sessionData.itemList.length; ++i) {
|
||
const metadata = Object.assign({}, itemList[i]);
|
||
if (metadata['DATA-ID'] === id) {
|
||
metadata[attrName] = obj;
|
||
itemList[i] = metadata;
|
||
break;
|
||
}
|
||
}
|
||
if (i === sessionData.itemList.length) {
|
||
this.logger.error(`Can't set ${attrName} of session data ${id}`);
|
||
}
|
||
result = Object.assign(Object.assign({}, sessionData), { itemList });
|
||
}
|
||
else {
|
||
this.logger.error(`Can't set ${attrName} on uninitialized session data`);
|
||
}
|
||
return result;
|
||
}
|
||
onLoadError(sessionData, id, err) {
|
||
var _a;
|
||
return this.setSessionData(sessionData, id, '_STATUS', (_a = err.response) === null || _a === void 0 ? void 0 : _a.code);
|
||
}
|
||
}
|
||
|
||
function cacheEntityToInfoEntity(initSegmentEntity) {
|
||
const { itemId, mediaOptionId, discoSeqNum, keyTagInfo } = initSegmentEntity;
|
||
const initSegmentInfo = { itemId, mediaOptionId, discoSeqNum, keyId: Hex.hexDump(keyTagInfo === null || keyTagInfo === void 0 ? void 0 : keyTagInfo.keyId) };
|
||
return initSegmentInfo;
|
||
}
|
||
class MediaElementStore extends EntityStore {
|
||
constructor() {
|
||
super({}, { name: 'media-element-store', producerFn: produce_1 });
|
||
this._activeId = '';
|
||
}
|
||
get activeId() {
|
||
return this._activeId;
|
||
}
|
||
startMediaSession(meState, maxBufferSeconds, almostDryWaterLevelSeconds, targetDurationSeconds) {
|
||
logAction('playback.session.start');
|
||
this._activeId = `media session: ${new Date().toISOString()}`;
|
||
applyTransaction(() => {
|
||
const lowWaterLevelSeconds = targetDurationSeconds;
|
||
const highWaterLevelSeconds = Math.max(lowWaterLevelSeconds, maxBufferSeconds - lowWaterLevelSeconds);
|
||
const mediaElementEntity = {
|
||
id: this.activeId,
|
||
desiredRate: !meState.autoplay && meState.paused ? 0 : 1,
|
||
paused: meState.paused,
|
||
gotPlaying: false,
|
||
gotLoadStart: false,
|
||
firstPlayTime: undefined,
|
||
seeking: meState.seeking,
|
||
flushing: false,
|
||
readyState: meState.readyState,
|
||
ended: meState.ended,
|
||
bufferedRanges: [],
|
||
haveEnough: false,
|
||
mediaSourceEntity: null,
|
||
expectedSbCount: NaN,
|
||
bufferMonitorInfo: {
|
||
waterLevelType: null,
|
||
almostDryWaterLevelSeconds,
|
||
lowWaterLevelSeconds,
|
||
highWaterLevelSeconds,
|
||
maxBufferSeconds,
|
||
},
|
||
mediaOptionParsedSubtitleRecord: [],
|
||
textTracksCreated: false,
|
||
waitingForDisco: false,
|
||
};
|
||
this.add(mediaElementEntity);
|
||
this.setActive(this.activeId);
|
||
});
|
||
this.logger = getLogger().child({ name: 'UpdateBufferedSegments' });
|
||
return this.activeId;
|
||
}
|
||
setMediaSourceEntity(objectUrl, readyState) {
|
||
logAction('playback.set.msObjectUrl');
|
||
this.updateActive((mediaElementEntity) => {
|
||
if (objectUrl != null && readyState != null) {
|
||
mediaElementEntity.mediaSourceEntity = { objectUrl, readyState, duration: NaN, sourceBufferEntities: [null, null] };
|
||
}
|
||
else {
|
||
mediaElementEntity.mediaSourceEntity = null;
|
||
}
|
||
mediaElementEntity.bufferedRanges = [];
|
||
mediaElementEntity.haveEnough = false;
|
||
mediaElementEntity.readyState = 0;
|
||
mediaElementEntity.bufferMonitorInfo.waterLevelType = null;
|
||
});
|
||
}
|
||
set mediaElementDuration(duration) {
|
||
logAction('playback.set.mediaElementDuration');
|
||
this.updateActive((mediaElementEntity) => {
|
||
if (mediaElementEntity) {
|
||
mediaElementEntity.mediaElementDuration = duration;
|
||
}
|
||
});
|
||
}
|
||
set msReadyState(readyState) {
|
||
logAction('playback.set.msReadyState');
|
||
this.updateActive(({ mediaSourceEntity }) => {
|
||
if (mediaSourceEntity) {
|
||
mediaSourceEntity.readyState = readyState;
|
||
}
|
||
});
|
||
}
|
||
set readyState(readyState) {
|
||
logAction(`playback.set.readyState ${readyState}`);
|
||
this.updateActive((active) => {
|
||
active.readyState = readyState;
|
||
});
|
||
}
|
||
set ended(ended) {
|
||
logAction(`playback.set.ended ${ended}`);
|
||
this.updateActive((active) => {
|
||
active.ended = ended;
|
||
});
|
||
}
|
||
set msDuration(duration) {
|
||
logAction('playback.set.msDuration');
|
||
this.updateActive((mediaElementEntity) => {
|
||
mediaElementEntity.mediaSourceEntity.duration = duration;
|
||
});
|
||
}
|
||
set textTracksCreated(created) {
|
||
logAction('playback.set.textTracksCreated ${created}');
|
||
this.updateActive((mediaElementEntity) => {
|
||
mediaElementEntity.textTracksCreated = created;
|
||
});
|
||
}
|
||
set expectedSbCount(expectedSbCount) {
|
||
logAction('playback.set.expectedSbCount');
|
||
this.updateActive((active) => {
|
||
active.expectedSbCount = expectedSbCount;
|
||
});
|
||
}
|
||
set postFlushSeek(postFlushSeek) {
|
||
this.updateActive({ postFlushSeek });
|
||
}
|
||
setSeekToPos(seekToPos, fromEvent, discoSeqNum) {
|
||
logAction(`playback.set.seekToPos: ${seekToPos === null || seekToPos === void 0 ? void 0 : seekToPos.toFixed(3)} cc: ${discoSeqNum}`);
|
||
this.updateActive((entity) => {
|
||
if (isFiniteNumber(seekToPos)) {
|
||
entity.seekTo = { pos: seekToPos, fromEvent, discoSeqNum };
|
||
entity.gotPlaying = false;
|
||
entity.haveEnough = false;
|
||
}
|
||
else {
|
||
entity.seekTo = null;
|
||
entity.postFlushSeek = undefined;
|
||
}
|
||
if (fromEvent) {
|
||
entity.seeking = isFiniteNumber(seekToPos);
|
||
}
|
||
});
|
||
}
|
||
set seeking(seeking) {
|
||
logAction(`playback.set.seeking: ${seeking}`);
|
||
this.updateActive((entity) => {
|
||
entity.seeking = seeking;
|
||
});
|
||
}
|
||
set paused(paused) {
|
||
logAction(`playback.set.paused: ${paused}`);
|
||
this.updateActive((entity) => {
|
||
entity.paused = paused;
|
||
if (paused) {
|
||
entity.gotPlaying = false;
|
||
}
|
||
});
|
||
}
|
||
// Got playing event
|
||
gotPlayingEvent() {
|
||
logAction('playback.set.playing');
|
||
this.updateActive((entity) => {
|
||
if (!entity.paused) {
|
||
entity.gotPlaying = true;
|
||
entity.firstPlayTime = entity.firstPlayTime || performance.now();
|
||
}
|
||
});
|
||
}
|
||
gotLoadStartEvent() {
|
||
logAction('playback.set.loadstart');
|
||
this.updateActive((entity) => {
|
||
entity.gotLoadStart = true;
|
||
});
|
||
}
|
||
set desiredRate(desiredRate) {
|
||
logAction(`playback.set.desiredRate: ${desiredRate}`);
|
||
this.updateActive((active) => {
|
||
active.desiredRate = desiredRate;
|
||
});
|
||
}
|
||
set haveEnough(haveEnough) {
|
||
logAction(`playback.set.haveEnough: ${haveEnough}`);
|
||
this.updateActive((active) => {
|
||
active.haveEnough = haveEnough;
|
||
});
|
||
}
|
||
set flushing(flushing) {
|
||
logAction(`playback.set.flushing: ${flushing}`);
|
||
this.updateActive({ flushing });
|
||
}
|
||
set waitingForDisco(waitingForDisco) {
|
||
logAction(`playback.set.waitingForDisco: ${waitingForDisco}`);
|
||
this.updateActive((entity) => {
|
||
if (entity) {
|
||
entity.waitingForDisco = waitingForDisco;
|
||
}
|
||
});
|
||
}
|
||
setSourceBufferUpdating(type) {
|
||
logAction(`playback.set.sourcebuffers[${SourceBufferNames[type]}].updating`);
|
||
this.updateActive(({ mediaSourceEntity }) => {
|
||
var _a;
|
||
const sbEntity = (_a = mediaSourceEntity === null || mediaSourceEntity === void 0 ? void 0 : mediaSourceEntity.sourceBufferEntities) === null || _a === void 0 ? void 0 : _a[type];
|
||
if (sbEntity) {
|
||
sbEntity.updating = true;
|
||
sbEntity.error = undefined;
|
||
}
|
||
});
|
||
}
|
||
setTimestampOffset(type, timestampOffset) {
|
||
logAction(`playback.set.sourcebuffers[${SourceBufferNames[type]}].timestampOffset`);
|
||
this.updateActive(({ mediaSourceEntity }) => {
|
||
var _a;
|
||
const sbEntity = (_a = mediaSourceEntity === null || mediaSourceEntity === void 0 ? void 0 : mediaSourceEntity.sourceBufferEntities) === null || _a === void 0 ? void 0 : _a[type];
|
||
if (sbEntity) {
|
||
sbEntity.timestampOffset = timestampOffset;
|
||
}
|
||
});
|
||
}
|
||
setBufferedRangesUpdated(type, bufferedRanges, combinedBuffer, gotQuotaExceeded, config) {
|
||
logAction(`playback.set.sourcebuffers[${SourceBufferNames[type]}].bufferupdated`);
|
||
this.updateActive((mediaElementEntity) => {
|
||
var _a;
|
||
const mediaSourceEntity = mediaElementEntity === null || mediaElementEntity === void 0 ? void 0 : mediaElementEntity.mediaSourceEntity;
|
||
const sbEntity = (_a = mediaSourceEntity === null || mediaSourceEntity === void 0 ? void 0 : mediaSourceEntity.sourceBufferEntities) === null || _a === void 0 ? void 0 : _a[type];
|
||
if (sbEntity) {
|
||
const msDuration = mediaSourceEntity === null || mediaSourceEntity === void 0 ? void 0 : mediaSourceEntity.duration;
|
||
sbEntity.updating = false;
|
||
sbEntity.bufferedRanges = [...bufferedRanges];
|
||
mergeInflightSegment(sbEntity);
|
||
// updateBufferedSegments even if bufferedRanges is empty
|
||
updateBufferedSegments(sbEntity, gotQuotaExceeded, msDuration, config, this.logger);
|
||
}
|
||
mediaElementEntity.bufferedRanges = [...combinedBuffer];
|
||
});
|
||
}
|
||
setSourceBufferEntity(type, compatInfo) {
|
||
logAction(`playback.set.sourcebuffers[${SourceBufferNames[type]}].setSourceBufferEntity`);
|
||
this.updateActive(({ mediaSourceEntity }) => {
|
||
if (!mediaSourceEntity)
|
||
return;
|
||
const { mimeType, audioCodec, videoCodec } = compatInfo;
|
||
const sbEntity = {
|
||
mimeType,
|
||
audioCodec,
|
||
videoCodec,
|
||
updating: false,
|
||
bufferedRanges: [],
|
||
timestampOffset: 0,
|
||
// Calculated
|
||
inFlight: null,
|
||
bufferedSegments: [],
|
||
totalBytes: 0,
|
||
maxTotalBytes: 0,
|
||
gotQuotaExceeded: false,
|
||
totalDuration: Infinity,
|
||
};
|
||
mediaSourceEntity.sourceBufferEntities[type] = sbEntity;
|
||
});
|
||
}
|
||
setInflightSegment(type, segment) {
|
||
logAction(`playback.set.sourcebuffers[${SourceBufferNames[type]}].setInflightSegment`);
|
||
this.updateActive(({ mediaSourceEntity }) => {
|
||
var _a;
|
||
const sbEntity = (_a = mediaSourceEntity === null || mediaSourceEntity === void 0 ? void 0 : mediaSourceEntity.sourceBufferEntities) === null || _a === void 0 ? void 0 : _a[type];
|
||
if (sbEntity) {
|
||
sbEntity.inFlight = segment;
|
||
}
|
||
});
|
||
}
|
||
setInitSegmentEntity(type, initSegmentEntity) {
|
||
logAction(`playback.set.sourcebuffers[${SourceBufferNames[type]}].setInitSegmentEntity`);
|
||
this.updateActive(({ mediaSourceEntity }) => {
|
||
var _a;
|
||
const sbEntity = (_a = mediaSourceEntity === null || mediaSourceEntity === void 0 ? void 0 : mediaSourceEntity.sourceBufferEntities) === null || _a === void 0 ? void 0 : _a[type];
|
||
if (sbEntity) {
|
||
sbEntity.initSegmentInfo = initSegmentEntity;
|
||
}
|
||
});
|
||
}
|
||
setSourceBufferError(type, error) {
|
||
logAction(`playback.set.sourcebuffers[${type}].error: ${error}`);
|
||
this.updateActive(({ mediaSourceEntity }) => {
|
||
var _a;
|
||
const sbEntity = (_a = mediaSourceEntity === null || mediaSourceEntity === void 0 ? void 0 : mediaSourceEntity.sourceBufferEntities) === null || _a === void 0 ? void 0 : _a[type];
|
||
if (sbEntity) {
|
||
sbEntity.inFlight = null;
|
||
sbEntity.updating = false;
|
||
sbEntity.error = error;
|
||
}
|
||
});
|
||
}
|
||
setStallInfo(stallInfo) {
|
||
logAction(`playback.set.stallInfo stalled=${stallInfo != null}`);
|
||
this.updateActive((active) => {
|
||
active.stallInfo = stallInfo;
|
||
});
|
||
}
|
||
setNudgeInfo(nudgeInfo) {
|
||
logAction(`playback.set.nudgeInfo ${stringifyWithPrecision(nudgeInfo)}`);
|
||
this.updateActive((active) => {
|
||
active.nudgeInfo = nudgeInfo;
|
||
});
|
||
}
|
||
/**
|
||
* Update water level information
|
||
* @param combined Water level of combined buffer
|
||
* @param sbTuple Water level of each source buffer
|
||
*/
|
||
updateWaterLevels(combined, sbTuple) {
|
||
logAction('playback.set.updateWaterLevels');
|
||
this.updateActive((entity) => {
|
||
entity.bufferMonitorInfo.waterLevelType = { combined, sbTuple: [...sbTuple] };
|
||
});
|
||
}
|
||
/**
|
||
* @param targetDurationSeconds The duration in seconds to use for determining low and high water.
|
||
* Low water threshold is 1 target duration, high water threshold is Math.max(lowWater, maxBuffer - targetDuration)
|
||
*/
|
||
set bufferMonitorTargetDuration(targetDurationSeconds) {
|
||
logAction(`playback.set.targetDuration: ${targetDurationSeconds}`);
|
||
this.updateActive((entity) => {
|
||
if (isFiniteNumber(targetDurationSeconds) && targetDurationSeconds > 0) {
|
||
const bufMonitorInfo = entity.bufferMonitorInfo;
|
||
bufMonitorInfo.lowWaterLevelSeconds = Math.min(targetDurationSeconds, bufMonitorInfo.maxBufferSeconds);
|
||
bufMonitorInfo.highWaterLevelSeconds = Math.max(bufMonitorInfo.lowWaterLevelSeconds, bufMonitorInfo.maxBufferSeconds - targetDurationSeconds);
|
||
}
|
||
});
|
||
}
|
||
archiveParsedSubtitleFragmentRecord(persistentId, mediaSeqNum, fragCueRecord) {
|
||
logAction(`playback.cues.set persistentId ${persistentId} mediaSeqNum ${mediaSeqNum}: parsed ${fragCueRecord.count} time-range ${fragCueRecord.startTime}:${fragCueRecord.endTime}`);
|
||
this.updateActive((entity) => {
|
||
let trackInfo = entity.mediaOptionParsedSubtitleRecord[persistentId];
|
||
if (!trackInfo) {
|
||
trackInfo = {};
|
||
entity.mediaOptionParsedSubtitleRecord[persistentId] = trackInfo;
|
||
}
|
||
trackInfo[mediaSeqNum] = fragCueRecord;
|
||
});
|
||
}
|
||
}
|
||
function mergeInflightSegment(sbEntity) {
|
||
const inFlight = sbEntity.inFlight;
|
||
const mergedBufferSeg = sbEntity.bufferedSegments;
|
||
if (inFlight && isFiniteNumber(inFlight.startPTS) && isFiniteNumber(inFlight.endPTS)) {
|
||
mergeToBuffer(mergedBufferSeg, inFlight);
|
||
}
|
||
sbEntity.inFlight = null;
|
||
}
|
||
// Helper functions for updating buffered segments, modifies sbEntity.bufferedSegments in-place
|
||
function updateBufferedSegments(sbEntity, gotQuotaExceeded, msDuration, config, logger) {
|
||
const { maxBufferHole, bufferedSegmentEjectionToleranceMs } = config;
|
||
// Modify in-place since we do not read anyway
|
||
const mergedBufferSeg = sbEntity.bufferedSegments;
|
||
const realBuffered = sbEntity.bufferedRanges;
|
||
let totalBytes = 0;
|
||
let lastFragEjected = false;
|
||
let lastFrag;
|
||
if (realBuffered.length) {
|
||
for (let i = mergedBufferSeg.length - 1; i > -1; i--) {
|
||
const segment = mergedBufferSeg[i];
|
||
const notIframe = !segment.frag.iframe;
|
||
if (notIframe && segment.frag.isLastFragment) {
|
||
lastFrag = segment.frag;
|
||
}
|
||
const segmentDuration = segment.endPTS - segment.startPTS;
|
||
if (segmentDuration <= 0) {
|
||
mergedBufferSeg.splice(i, 1);
|
||
logger === null || logger === void 0 ? void 0 : logger.warn(`Ejecting segment from bufferedSegments due to segmentDuration <= 0 > segment=${stringifyWithPrecision(segment)}`);
|
||
lastFragEjected = segment.frag === lastFrag;
|
||
continue; // Should never happen but...
|
||
}
|
||
const overlapping = getMaxOverlappingRange(realBuffered, segment);
|
||
// startPTS/endPTS is sometimes a bit off from what is reported in buffered,
|
||
// especially for audio. Add the byteLength approximation with overlap factor.
|
||
if (overlapping) {
|
||
// Estimate total bytes using all buffered time ranges
|
||
const overlapStart = Math.max(overlapping.start, segment.startPTS);
|
||
const overlapEnd = Math.min(overlapping.end, segment.endPTS);
|
||
const appendedDuration = overlapEnd - overlapStart;
|
||
totalBytes += (segment.bytes * appendedDuration) / segmentDuration;
|
||
if (notIframe) {
|
||
// ignore tiny overlaps
|
||
if (appendedDuration < Math.min(segmentDuration, maxBufferHole)) {
|
||
mergedBufferSeg.splice(i, 1);
|
||
logger === null || logger === void 0 ? void 0 : logger.warn(`Ejecting segment from bufferedSegments due to tiny overlaps > segment=${stringifyWithPrecision(segment)}, bufferedRanges=${stringifyWithPrecision(realBuffered)}`);
|
||
lastFragEjected = segment.frag === lastFrag;
|
||
continue;
|
||
}
|
||
// Keep track of initial appended overlap, and remove the segment if the current overlap has changed
|
||
// except when the segment duration was modified because of an overlapping append in mergeToBuffer
|
||
const initialAppendedValue = segment.appendedDuration;
|
||
const appendedDurationDelta = (initialAppendedValue || 0) - appendedDuration;
|
||
// rdar://84943043 tolerance is used to avoid floating point arithmetic issues
|
||
const tolerance = Math.min(bufferedSegmentEjectionToleranceMs * 0.001, segmentDuration);
|
||
if (!initialAppendedValue) {
|
||
segment.appendedDuration = appendedDuration;
|
||
}
|
||
else if (appendedDurationDelta > tolerance && appendedDuration !== segmentDuration) {
|
||
// Eject, unless last segment and buffer end equals known mediaSink duration
|
||
if (!segment.frag.isLastFragment || overlapEnd !== msDuration) {
|
||
mergedBufferSeg.splice(i, 1);
|
||
logger === null || logger === void 0 ? void 0 : logger.warn(`Ejecting segment from bufferedSegments due to change in current overlap > segment=${stringifyWithPrecision(segment)}, delta=${appendedDurationDelta}, bufferedRanges=${stringifyWithPrecision(realBuffered)}`);
|
||
lastFragEjected = segment.frag === lastFrag;
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
logger === null || logger === void 0 ? void 0 : logger.warn(`Ejecting segment from bufferedSegments due to no overlap > segment=${stringifyWithPrecision(segment)}, bufferedRanges=${stringifyWithPrecision(realBuffered)}`);
|
||
mergedBufferSeg.splice(i, 1);
|
||
lastFragEjected = segment.frag === lastFrag;
|
||
}
|
||
}
|
||
}
|
||
else if (mergedBufferSeg.length) {
|
||
logger === null || logger === void 0 ? void 0 : logger.info('Flushing buffered segments in response to flush');
|
||
mergedBufferSeg.splice(0, mergedBufferSeg.length); // flushed out
|
||
}
|
||
// if last frag was ejected this round, realBuffered[realBuffered.length - 1].end will not have the right totalduration.
|
||
// if everything was flushed out, totalduration will remain unchanged (lastFrag will be false)
|
||
sbEntity.totalDuration = lastFrag && !lastFragEjected && realBuffered.length > 0 ? realBuffered[realBuffered.length - 1].end : Infinity;
|
||
sbEntity.gotQuotaExceeded = sbEntity.gotQuotaExceeded || gotQuotaExceeded;
|
||
sbEntity.totalBytes = totalBytes;
|
||
sbEntity.maxTotalBytes = Math.max(sbEntity.totalBytes, sbEntity.maxTotalBytes);
|
||
}
|
||
// Merge the newly appended segment into what we think we already have buffered
|
||
// Assumed buffered has no overlaps already and is sorted by startPTS
|
||
// Modifies in-place
|
||
function mergeToBuffer(buffered, appended) {
|
||
let appendedAdded = false;
|
||
for (let i = buffered.length - 1; i > -1; i--) {
|
||
const seg = buffered[i];
|
||
const overlapStart = Math.max(appended.startPTS, seg.startPTS);
|
||
const overlapEnd = Math.min(appended.endPTS, seg.endPTS);
|
||
if (overlapStart >= overlapEnd) {
|
||
continue;
|
||
}
|
||
const bytes = (1 - (overlapEnd - overlapStart) / (seg.endPTS - seg.startPTS)) * seg.bytes;
|
||
if (bytes <= 0) {
|
||
buffered.splice(i, 1);
|
||
continue;
|
||
}
|
||
seg.bytes = bytes;
|
||
if (seg.startPTS < appended.startPTS) {
|
||
seg.endPTS = overlapStart;
|
||
}
|
||
else {
|
||
seg.startPTS = overlapEnd;
|
||
if (!appendedAdded) {
|
||
buffered.splice(i, 0, appended);
|
||
appendedAdded = true;
|
||
}
|
||
}
|
||
}
|
||
if (!appendedAdded) {
|
||
buffered.push(appended);
|
||
}
|
||
}
|
||
/**
|
||
* Returns length of overlap of segment with given bufferedRange
|
||
*/
|
||
function segmentOverlapLength(segment, range) {
|
||
return Math.min(segment.endPTS, range.end) - Math.max(segment.startPTS, range.start);
|
||
}
|
||
/**
|
||
* Returns the BufferedRange that has maximum ovelap with given segment.
|
||
*/
|
||
function getMaxOverlappingRange(bufferedRanges, segment) {
|
||
let maxOverlappingRange = undefined;
|
||
let lastOverlapLength = 0;
|
||
// TODO: Use binary search over bufferedRanges rdar://89159252
|
||
for (const range of bufferedRanges) {
|
||
// Check only when there is overlap
|
||
if (range.start <= segment.endPTS && range.end > segment.startPTS) {
|
||
const currentOverlapLength = segmentOverlapLength(segment, range);
|
||
if (currentOverlapLength > lastOverlapLength) {
|
||
maxOverlappingRange = range;
|
||
lastOverlapLength = currentOverlapLength;
|
||
}
|
||
}
|
||
else if (lastOverlapLength > 0) {
|
||
// bufferedRanges is Normalized TimeRanges object i.e "The ranges in such an object are ordered, don't overlap, and don't touch"
|
||
// No point iterating through rest of ranges if overlap is found previously and no overlap afterwards.
|
||
break;
|
||
}
|
||
}
|
||
return maxOverlappingRange;
|
||
}
|
||
|
||
function isIframeMediaFragment(frag) {
|
||
return frag != null && 'iframeMediaDuration' in frag && 'iframeMediaStart' in frag;
|
||
}
|
||
function fragEqual(a, b) {
|
||
return a === b || (a && b && a.itemId === b.itemId && a.mediaOptionId === b.mediaOptionId && a.mediaSeqNum === b.mediaSeqNum && a.discoSeqNum === b.discoSeqNum);
|
||
}
|
||
function fragPrint(frag) {
|
||
return JSON.stringify(frag, ['mediaOptionId', 'mediaSeqNum', 'discoSeqNum', 'start', 'duration']);
|
||
}
|
||
|
||
function filterNullOrUndefined() {
|
||
return (source) => source.pipe(filter((x) => x != null), map((x) => x));
|
||
}
|
||
|
||
function getVideoCodecRanking(videoCodec) {
|
||
if (MediaUtil.isDolby(videoCodec)) {
|
||
return VideoCodecRank.DOVI;
|
||
}
|
||
else if (MediaUtil.isHEVC(videoCodec)) {
|
||
return VideoCodecRank.HEVC;
|
||
}
|
||
else if (MediaUtil.isVP09(videoCodec)) {
|
||
return VideoCodecRank.VP09;
|
||
}
|
||
else if (MediaUtil.isAVC(videoCodec)) {
|
||
return VideoCodecRank.AVC;
|
||
}
|
||
else {
|
||
return VideoCodecRank.UNKNOWN;
|
||
}
|
||
}
|
||
function getVideoCodecFamily(videoCodec) {
|
||
return videoCodec === null || videoCodec === void 0 ? void 0 : videoCodec.split('.')[0];
|
||
}
|
||
function getAudioCodecRanking(audioCodec) {
|
||
if (MediaUtil.isALAC(audioCodec)) {
|
||
return AudioCodecRank.ALAC;
|
||
}
|
||
else if (MediaUtil.isFLAC(audioCodec)) {
|
||
return AudioCodecRank.FLAC;
|
||
}
|
||
else if (MediaUtil.isEC3(audioCodec)) {
|
||
return AudioCodecRank.EC3;
|
||
}
|
||
else if (MediaUtil.isAC3(audioCodec)) {
|
||
return AudioCodecRank.AC3;
|
||
}
|
||
else if (MediaUtil.isXHEAAC(audioCodec)) {
|
||
return AudioCodecRank.XHEAAC;
|
||
}
|
||
else if (MediaUtil.isAAC(audioCodec)) {
|
||
return AudioCodecRank.AAC;
|
||
}
|
||
else if (MediaUtil.isMP3(audioCodec)) {
|
||
return AudioCodecRank.MP3;
|
||
}
|
||
else {
|
||
return AudioCodecRank.UNKNOWN;
|
||
}
|
||
}
|
||
function getVideoRangeRanking(videoRange) {
|
||
if (videoRange === 'PQ') {
|
||
return VideoRangeRank.PQ;
|
||
}
|
||
else if (videoRange === 'HLG') {
|
||
return VideoRangeRank.HLG;
|
||
}
|
||
else if (videoRange === 'SDR') {
|
||
return VideoRangeRank.SDR;
|
||
}
|
||
else {
|
||
return VideoRangeRank.UNKNOWN;
|
||
}
|
||
}
|
||
function getAudioVideoCodecRanks(codecInfo) {
|
||
return { videoCodecRank: getVideoCodecRanking(codecInfo.videoCodec), audioCodecRank: getAudioCodecRanking(codecInfo.audioCodec) };
|
||
}
|
||
class MediaOptionRank {
|
||
constructor(...identifier) {
|
||
this.identifier = identifier;
|
||
}
|
||
ensureSameIdentifierLength(rank) {
|
||
if (this.identifier.length !== rank.identifier.length) {
|
||
throw new Error(`Identifiers have non-matching lengths! (${this.identifier.length} vs ${rank.identifier.length})`);
|
||
}
|
||
}
|
||
isGreaterThan(rank) {
|
||
this.ensureSameIdentifierLength(rank);
|
||
for (let i = 0; i < this.identifier.length; ++i) {
|
||
if (this.identifier[i] < rank.identifier[i]) {
|
||
return false;
|
||
}
|
||
else if (this.identifier[i] > rank.identifier[i]) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
isEqualTo(rank) {
|
||
this.ensureSameIdentifierLength(rank);
|
||
return this.identifier.every((entry, i) => entry === rank.identifier[i]);
|
||
}
|
||
}
|
||
|
||
function isIframeRate(rate) {
|
||
return isFiniteNumber(rate) && rate !== 0 && rate !== 1;
|
||
}
|
||
|
||
class MutexBusyError extends Error {
|
||
}
|
||
const __executeAndUnlock = (value, update, unlock, operation) => {
|
||
if (!operation) {
|
||
// no operation() provided, caller should excplicitly call unlock()
|
||
return of(value);
|
||
}
|
||
let result;
|
||
let obs;
|
||
try {
|
||
result = operation(value, update);
|
||
}
|
||
catch (err) {
|
||
obs = throwError(() => err);
|
||
}
|
||
if (!obs) {
|
||
if (typeof result === 'undefined') {
|
||
obs = of(value);
|
||
}
|
||
else {
|
||
obs = from(result);
|
||
}
|
||
}
|
||
// auto-unlock if operation() has been called
|
||
return obs.pipe(finalize$1(unlock));
|
||
};
|
||
/**
|
||
* Mutex is a concept borrowed from multi-threaded programming. Although it's
|
||
* not the first time multi-thread syncing primitives are used in single
|
||
* threaded program. Python 3 for example has a collection of single threaded
|
||
* coroutine syncing primitives in the asyncio package. Any single threaded
|
||
* program with an event loop would have the problem of "data race" before and
|
||
* after the event loop context switch. Note that event loop achieves concurrent
|
||
* processing similar to system level process and thread scheduling on a single
|
||
* core. Therefore, classical syncing primitives are also applicable for event
|
||
* loops.
|
||
*
|
||
* IMPORTANT: Always justify your reason if you use Mutex in your code. The only
|
||
* permitable use-case is the protected operation can be called from different
|
||
* threads and only one can execute at one time. If the component has multiple
|
||
* such operations need this synchronization, please consider using command
|
||
* queue pattern instead of Mutex.
|
||
*/
|
||
class Mutex {
|
||
/**
|
||
* Mutex can optionally carry the data it's protecting. Doing so can make it
|
||
* more explicit that the data should be accessed while holing a lock of this
|
||
* mutex.
|
||
* @param value data to be stored on the Mutex
|
||
*/
|
||
constructor(value) {
|
||
this.value = value;
|
||
this.waiters = [];
|
||
this.wcounter = 0;
|
||
this.rcounter = 0;
|
||
}
|
||
lock(operation, tryLock = false) {
|
||
return this._lock(true, operation, tryLock);
|
||
}
|
||
/**
|
||
* Release a lock previously acquired on the mutex.
|
||
*/
|
||
unlock() {
|
||
this._unlock(true);
|
||
}
|
||
readLock(operation, tryLock = false) {
|
||
return this._lock(false, operation, tryLock);
|
||
}
|
||
/**
|
||
* Release a read-only lock previously acquired on the mutex.
|
||
*/
|
||
readUnlock() {
|
||
this._unlock(false);
|
||
}
|
||
_schedule() {
|
||
const _waitersToWake = [];
|
||
this.waiters = this.waiters.filter(waiter => {
|
||
if (this._canLock(waiter.rw)) {
|
||
if (waiter.rw) {
|
||
++this.wcounter;
|
||
}
|
||
else {
|
||
++this.rcounter;
|
||
}
|
||
_waitersToWake.push(waiter);
|
||
return false;
|
||
}
|
||
return true;
|
||
});
|
||
for (const waiter of _waitersToWake) {
|
||
waiter.observer.next(this.value);
|
||
waiter.observer.complete();
|
||
}
|
||
}
|
||
_canLock(rw) {
|
||
return (rw && this.wcounter === 0 && this.rcounter === 0) || (!rw && this.wcounter === 0);
|
||
}
|
||
_lock(rw, operation, tryLock = false) {
|
||
if (typeof operation === 'boolean') {
|
||
[tryLock, operation] = [operation, undefined];
|
||
}
|
||
const obs = new Observable(observer => {
|
||
const canLock = this._canLock(rw);
|
||
if (tryLock && !canLock) {
|
||
throw new MutexBusyError();
|
||
}
|
||
if (canLock) {
|
||
if (rw) {
|
||
++this.wcounter;
|
||
}
|
||
else {
|
||
++this.rcounter;
|
||
}
|
||
observer.next();
|
||
observer.complete();
|
||
}
|
||
else {
|
||
this.waiters.push({ rw, observer });
|
||
}
|
||
});
|
||
if (operation) {
|
||
return obs.pipe(mergeMap(() => __executeAndUnlock(this.value, newval => void (this.value = newval), () => this._unlock(rw), operation)));
|
||
}
|
||
return obs;
|
||
}
|
||
_unlock(rw) {
|
||
if (rw) {
|
||
this.wcounter = Math.max(this.wcounter - 1, 0);
|
||
}
|
||
else {
|
||
this.rcounter = Math.max(this.rcounter - 1, 0);
|
||
}
|
||
this._schedule();
|
||
}
|
||
}
|
||
/**
|
||
* WaitGroup is a concept borrowed from Golang used to wait for async routines
|
||
* to finish. We can use it to signal async shutdown completion to subscribers.
|
||
*
|
||
* class Hls {
|
||
* public destroy$ = new Subject<void>();
|
||
* public destroyWG = new WaitGroup();
|
||
* constructor() {
|
||
* observableWithAsyncTeardown(this).pipe(
|
||
* takeUntil(this.destroy$) // chain its lifecycle to Hls
|
||
* ).subscribe();
|
||
* }
|
||
* public destroy(): Promise<void> {
|
||
* this.destroy$.next();
|
||
* // all lifecyles chained below Hls will teardown. Async routines
|
||
* // will be registered with hls.destroyWG.add()
|
||
*
|
||
* return this.destroyWG.toPromise();
|
||
* // returned promise will resolve when all async routines
|
||
* // registered are finished with hls.destroyWG.done()
|
||
* }
|
||
* }
|
||
* const observableWithAsyncTeardown = (hls: Hls) =>
|
||
* new Observable<void>((subscriber) => {
|
||
* return () => { // teardown function
|
||
* hls.destroyWG.add(); // register an async routine
|
||
* setTimeout(() => {
|
||
* console.log('async teardown finished');
|
||
* hls.destroyWG.done(); // signal finish of a async routine
|
||
* }, 1000);
|
||
* };
|
||
* });
|
||
*
|
||
* And there is a shorthand for Promise or Observable like async routine:
|
||
*
|
||
* wg.wrap(promise).subscribe();
|
||
*
|
||
*/
|
||
class WaitGroup extends Observable {
|
||
constructor() {
|
||
super((subscriber) => this._count$
|
||
.pipe(
|
||
// emits only when count reaches to zero
|
||
filter((count) => count === 0),
|
||
// completes once count reaches to zero
|
||
take(1),
|
||
// convert to empty value (undefined)
|
||
mapTo(void 0))
|
||
.subscribe(subscriber));
|
||
this._count$ = new BehaviorSubject(0);
|
||
}
|
||
/**
|
||
* Wrap an async operation to wait for.
|
||
* @param routine an async operation to wait for
|
||
*/
|
||
wrap(routine) {
|
||
return defer(() => {
|
||
this.add();
|
||
return from(routine);
|
||
}).pipe(
|
||
// propagate error in the async routine
|
||
tap({ error: (e) => this._count$.error(e) }),
|
||
// decrement the count when the async routine finishes
|
||
finalize$1(() => this.done()));
|
||
}
|
||
/**
|
||
* Increase the wait counter.
|
||
* @param n delta to increase the counter
|
||
*/
|
||
add(n = 1) {
|
||
this._count$.next(this._count$.value + n);
|
||
}
|
||
/**
|
||
* Decrease the wait counter.
|
||
* @param n delta to decrease the counter
|
||
*/
|
||
done(n = 1) {
|
||
this._count$.next(this._count$.value - n);
|
||
}
|
||
}
|
||
|
||
|
||
const BufferHelper = {
|
||
isBuffered(buffered, position) {
|
||
for (let i = 0; buffered && i < buffered.length; i++) {
|
||
if (position >= buffered.start(i) && position <= buffered.end(i)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
},
|
||
/**
|
||
* Return Array of [ {start, end} ]
|
||
* @param media Something with a buffered TimeRanges property (HTMLMediaElement, SourceBuffer, etc)
|
||
*/
|
||
timeRangesToBufferedRange(timeRanges) {
|
||
const bufferedRangeArray = [];
|
||
for (let ix = 0; timeRanges && ix < timeRanges.length; ix++) {
|
||
bufferedRangeArray.push({
|
||
start: timeRanges.start(ix),
|
||
end: timeRanges.end(ix),
|
||
});
|
||
}
|
||
return bufferedRangeArray;
|
||
},
|
||
subtitleBufferInfo(cues, pos, maxHoleDuration) {
|
||
if (cues) {
|
||
const buffered = this.bufferedCues(cues);
|
||
return this.getBufferedInfo(buffered, pos, maxHoleDuration);
|
||
}
|
||
return { len: 0, start: pos, end: pos, nextStart: undefined };
|
||
},
|
||
fragmentsBufferedInfo(bufferedFrags, pos, maxHoleDuration) {
|
||
const buffered = [];
|
||
for (const frag of bufferedFrags) {
|
||
buffered.push({ start: frag.start, end: frag.start + frag.duration });
|
||
}
|
||
return this.getBufferedInfo(buffered, pos, maxHoleDuration);
|
||
},
|
||
bufferedCues(cues) {
|
||
const buffered = [];
|
||
if (cues) {
|
||
for (let ix = 0; ix < cues.length; ix++) {
|
||
buffered.push({ start: cues[ix].startTime, end: cues[ix].endTime });
|
||
}
|
||
}
|
||
return buffered;
|
||
},
|
||
bufferedInfoFromMedia(media, pos, maxHoleDuration) {
|
||
return BufferHelper.getBufferedInfo(BufferHelper.timeRangesToBufferedRange(media.buffered), pos, maxHoleDuration);
|
||
},
|
||
getBufferedInfo(inBufferedRanges, pos, maxHoleDuration) {
|
||
const buffered2 = [];
|
||
// bufferStart and bufferEnd are buffer boundaries around current video position
|
||
let bufferLen, bufferStart, bufferEnd, bufferStartNext, i;
|
||
// sort on buffer.start/smaller end (IE does not always return sorted buffered range)
|
||
const bufferedRanges = inBufferedRanges.map(({ start, end }) => ({ start, end }));
|
||
bufferedRanges.sort((a, b) => {
|
||
const diff = a.start - b.start;
|
||
if (diff) {
|
||
return diff;
|
||
}
|
||
return b.end - a.end;
|
||
});
|
||
// there might be some small holes between buffer time range
|
||
// consider that holes smaller than maxHoleDuration are irrelevant and build another
|
||
// buffer time range representations that discards those holes
|
||
for (i = 0; i < bufferedRanges.length; i++) {
|
||
const buf2len = buffered2.length;
|
||
if (buf2len) {
|
||
const buf2end = buffered2[buf2len - 1].end;
|
||
// if small hole (value between 0 or maxHoleDuration ) or overlapping (negative)
|
||
if (bufferedRanges[i].start - buf2end < maxHoleDuration) {
|
||
// merge overlapping time ranges
|
||
// update lastRange.end only if smaller than item.end
|
||
// e.g. [ 1, 15] with [ 2,8] => [ 1,15] (no need to modify lastRange.end)
|
||
// whereas [ 1, 8] with [ 2,15] => [ 1,15] ( lastRange should switch from [1,8] to [1,15])
|
||
if (bufferedRanges[i].end > buf2end) {
|
||
buffered2[buf2len - 1].end = bufferedRanges[i].end;
|
||
}
|
||
}
|
||
else {
|
||
// big hole
|
||
buffered2.push(bufferedRanges[i]);
|
||
}
|
||
}
|
||
else {
|
||
// first value
|
||
buffered2.push(bufferedRanges[i]);
|
||
}
|
||
}
|
||
for (i = 0, bufferLen = 0, bufferStart = bufferEnd = pos; i < buffered2.length; i++) {
|
||
const { start } = buffered2[i], { end } = buffered2[i];
|
||
if (pos + maxHoleDuration >= start && pos < end) {
|
||
// play position is inside this buffer TimeRange, retrieve end of buffer position and buffer length
|
||
bufferStart = start;
|
||
bufferEnd = end;
|
||
bufferLen = bufferEnd - pos;
|
||
}
|
||
else if (pos + maxHoleDuration < start) {
|
||
bufferStartNext = start;
|
||
break;
|
||
}
|
||
}
|
||
return {
|
||
len: bufferLen,
|
||
start: bufferStart,
|
||
end: bufferEnd,
|
||
nextStart: bufferStartNext,
|
||
};
|
||
},
|
||
toRangeString(bufferInfo) {
|
||
return `[${bufferInfo.start.toFixed(3)},${bufferInfo.end.toFixed(3)}]`;
|
||
},
|
||
};
|
||
|
||
var StallType;
|
||
(function (StallType) {
|
||
StallType["Seek"] = "Seek";
|
||
StallType["HighBuffer"] = "HighBuffer";
|
||
StallType["LowBuffer"] = "LowBuffer";
|
||
})(StallType || (StallType = {}));
|
||
var BufferWaterLevel;
|
||
(function (BufferWaterLevel) {
|
||
BufferWaterLevel["AlmostDry"] = "AlmostDry";
|
||
BufferWaterLevel["LowWater"] = "LowWater";
|
||
BufferWaterLevel["HighWater"] = "HighWater";
|
||
BufferWaterLevel["AboveHighWater"] = "AboveHighWater";
|
||
})(BufferWaterLevel || (BufferWaterLevel = {}));
|
||
const waterLevelToValue = {
|
||
[BufferWaterLevel.AlmostDry]: 0,
|
||
[BufferWaterLevel.LowWater]: 1,
|
||
[BufferWaterLevel.HighWater]: 2,
|
||
[BufferWaterLevel.AboveHighWater]: 3,
|
||
};
|
||
/**
|
||
* @returns whether from water level is > to water level
|
||
*/
|
||
function waterLevelFell(from, to) {
|
||
return waterLevelToValue[from] > waterLevelToValue[to];
|
||
}
|
||
|
||
/**
|
||
* Highest threshold less than waterLevelSec
|
||
* @param waterLevelSec The waterlevel
|
||
*/
|
||
function getHighestThresholdBelowWater(waterLevelSec, info) {
|
||
const result = [
|
||
{ threshold: info.highWaterLevelSeconds, level: BufferWaterLevel.HighWater },
|
||
{ threshold: info.lowWaterLevelSeconds, level: BufferWaterLevel.LowWater },
|
||
{ threshold: info.almostDryWaterLevelSeconds, level: BufferWaterLevel.AlmostDry },
|
||
].find(({ threshold }) => {
|
||
return waterLevelSec > threshold;
|
||
});
|
||
return result;
|
||
}
|
||
/**
|
||
* Lowest threshold higher than waterLevelSec
|
||
* @param waterLevelSec The water level
|
||
*/
|
||
function getLowestThresholdAboveWaterLevel(waterLevelSec, info) {
|
||
const result = [
|
||
{ threshold: info.almostDryWaterLevelSeconds, level: BufferWaterLevel.AlmostDry },
|
||
{ threshold: info.lowWaterLevelSeconds, level: BufferWaterLevel.LowWater },
|
||
{ threshold: info.highWaterLevelSeconds, level: BufferWaterLevel.HighWater },
|
||
{ threshold: Infinity, level: BufferWaterLevel.AboveHighWater },
|
||
].find(({ threshold }) => {
|
||
return waterLevelSec <= threshold;
|
||
});
|
||
return result;
|
||
}
|
||
function updateWaterLevels(meQuery, maxBufferHole) {
|
||
const combined = getLowestThresholdAboveWaterLevel(meQuery.getCurrentWaterLevel(maxBufferHole), meQuery.bufferMonitorInfo).level;
|
||
const sbTuple = [null, null];
|
||
[SourceBufferType.Variant, SourceBufferType.AltAudio].forEach((sbType) => {
|
||
if (meQuery.sourceBufferEntityByType(sbType) != null) {
|
||
sbTuple[sbType] = getLowestThresholdAboveWaterLevel(meQuery.getCurrentWaterLevelByType(sbType, maxBufferHole), meQuery.bufferMonitorInfo).level;
|
||
}
|
||
});
|
||
return { combined, sbTuple };
|
||
}
|
||
/**
|
||
* Observable that re-calculates water level when threshold or buffered range changes
|
||
*/
|
||
function waterLevelChangedFromBuffer(meQuery, maxBufferHole) {
|
||
return combineQueries([meQuery.bufferMonitorThresholds$, meQuery.combinedBuffer$, meQuery.seeking$]).pipe(map(([thresholds]) => {
|
||
if (thresholds == null) {
|
||
return null;
|
||
}
|
||
return updateWaterLevels(meQuery, maxBufferHole);
|
||
}));
|
||
}
|
||
/**
|
||
* Observable that re-calculates water level during regular playback
|
||
*/
|
||
function waterLevelChangedFromPlayback(meQuery, maxBufferHole, logger) {
|
||
return combineQueries([meQuery.combinedBuffer$, meQuery.gotPlaying$, meQuery.seeking$, meQuery.waterLevelChangedForType$(null), meQuery.stallInfo$]).pipe(switchMap(([combinedBuffer, playing, seeking, waterLevelType, stallInfo]) => {
|
||
if (combinedBuffer.length === 0 || !playing || seeking || waterLevelType == null || stallInfo != null) {
|
||
return EMPTY;
|
||
}
|
||
return scheduleWaterLevelCheck(meQuery, maxBufferHole);
|
||
}));
|
||
}
|
||
function scheduleWaterLevelCheck(meQuery, maxBufferHole, logger) {
|
||
// Combined water level should be minimum of the source buffers
|
||
const combinedWaterLevel = meQuery.getCurrentWaterLevel(maxBufferHole);
|
||
const thresholdInfo = getHighestThresholdBelowWater(combinedWaterLevel, meQuery.bufferMonitorInfo);
|
||
if (thresholdInfo) {
|
||
const { threshold } = thresholdInfo;
|
||
const delayMs = Math.ceil((combinedWaterLevel - threshold) * 1000);
|
||
return timer(delayMs).pipe(switchMap(() => {
|
||
const newCombinedWaterLevel = meQuery.getCurrentWaterLevel(maxBufferHole);
|
||
const newThresholdInfo = getHighestThresholdBelowWater(newCombinedWaterLevel, meQuery.bufferMonitorInfo);
|
||
if ((newThresholdInfo === null || newThresholdInfo === void 0 ? void 0 : newThresholdInfo.level) === thresholdInfo.level) {
|
||
// Didn't actually cross threshold, re-schedule check
|
||
return scheduleWaterLevelCheck(meQuery, maxBufferHole);
|
||
}
|
||
return VOID;
|
||
}), map(() => {
|
||
return updateWaterLevels(meQuery, maxBufferHole);
|
||
}));
|
||
}
|
||
return EMPTY;
|
||
}
|
||
/**
|
||
* Epic for updating the water level
|
||
*/
|
||
function bufferMonitorEpic(meQuery, meStore, maxBufferHole, logger) {
|
||
return merge(waterLevelChangedFromBuffer(meQuery, maxBufferHole), waterLevelChangedFromPlayback(meQuery, maxBufferHole)).pipe(observeOn(asyncScheduler), tap(({ combined, sbTuple }) => {
|
||
meStore.updateWaterLevels(combined, sbTuple);
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* @brief Query interface to playback state
|
||
*/
|
||
class MediaElementQuery extends QueryEntity {
|
||
constructor(mediaElement, mediaElementStore) {
|
||
super(mediaElementStore);
|
||
this.mediaElement = mediaElement;
|
||
}
|
||
get mediaElementDuration$() {
|
||
return this.selectActive(({ mediaElementDuration }) => mediaElementDuration);
|
||
}
|
||
get mediaElementDuration() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.mediaElementDuration) !== null && _b !== void 0 ? _b : Infinity;
|
||
}
|
||
get msDuration() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.mediaSourceEntity) === null || _a === void 0 ? void 0 : _a.duration) !== null && _b !== void 0 ? _b : Infinity;
|
||
}
|
||
// return the smaller of all the sourceBuffer totaldurations
|
||
// return infinity if any of the source buffer is not fully buffered
|
||
get minSBDuration() {
|
||
var _a, _b;
|
||
let minDuration = Number.POSITIVE_INFINITY;
|
||
(_b = (_a = this.mediaSourceEntity) === null || _a === void 0 ? void 0 : _a.sourceBufferEntities) === null || _b === void 0 ? void 0 : _b.forEach((entity, index) => {
|
||
if (entity) {
|
||
// may be muxed or unmuxed
|
||
if (isFiniteNumber(entity.totalDuration)) {
|
||
// entity.totalDuration is a finite number if sb is fully buffered
|
||
minDuration = Math.min(minDuration, entity.totalDuration);
|
||
}
|
||
else {
|
||
// one of the SBs not fully buffered
|
||
minDuration = Number.NEGATIVE_INFINITY;
|
||
}
|
||
}
|
||
});
|
||
return isFiniteNumber(minDuration) ? minDuration : Infinity;
|
||
}
|
||
get currentTime() {
|
||
return this.mediaElement.currentTime;
|
||
}
|
||
get clientWidth() {
|
||
return this.mediaElement.clientWidth;
|
||
}
|
||
get clientHeight() {
|
||
return this.mediaElement.clientHeight;
|
||
}
|
||
getBufferedDuration(maxBufferDuration = 0.5) {
|
||
const ranges = BufferHelper.timeRangesToBufferedRange(this.mediaElement.buffered);
|
||
const bufferInfo = BufferHelper.getBufferedInfo(ranges, this.currentTime, maxBufferDuration);
|
||
return bufferInfo.end - bufferInfo.start;
|
||
}
|
||
get mediaSourceEntity() {
|
||
var _a;
|
||
return (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.mediaSourceEntity;
|
||
}
|
||
get msReadyState() {
|
||
var _a;
|
||
return (_a = this.mediaSourceEntity) === null || _a === void 0 ? void 0 : _a.readyState;
|
||
}
|
||
get sourceBufferEntities() {
|
||
var _a;
|
||
return (_a = this.mediaSourceEntity) === null || _a === void 0 ? void 0 : _a.sourceBufferEntities;
|
||
}
|
||
sourceBufferEntityByType(sbType) {
|
||
var _a;
|
||
return (_a = this.sourceBufferEntities) === null || _a === void 0 ? void 0 : _a[sbType];
|
||
}
|
||
initSegmentEntityByType(sbType) {
|
||
var _a;
|
||
return (_a = this.sourceBufferEntityByType(sbType)) === null || _a === void 0 ? void 0 : _a.initSegmentInfo;
|
||
}
|
||
get maxBufferSize() {
|
||
var _a, _b;
|
||
const entity = (_a = this.sourceBufferEntities) === null || _a === void 0 ? void 0 : _a[SourceBufferType.Variant];
|
||
let maxBufferSize = Infinity;
|
||
if (entity === null || entity === void 0 ? void 0 : entity.gotQuotaExceeded) {
|
||
maxBufferSize = (_b = entity.maxTotalBytes) !== null && _b !== void 0 ? _b : Infinity;
|
||
}
|
||
return maxBufferSize;
|
||
}
|
||
get postFlushSeek() {
|
||
var _a;
|
||
return (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.postFlushSeek;
|
||
}
|
||
get seekable() {
|
||
return this.mediaElement.seekable;
|
||
}
|
||
get desiredRate() {
|
||
var _a;
|
||
return ((_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.desiredRate) || 0;
|
||
}
|
||
get desiredRate$() {
|
||
return this.selectActive(({ desiredRate }) => desiredRate !== null && desiredRate !== void 0 ? desiredRate : 0);
|
||
}
|
||
get effectiveRate() {
|
||
if (this.isIframeRate) {
|
||
return this.desiredRate;
|
||
}
|
||
return this.paused ? 0 : 1;
|
||
}
|
||
get playbackRate() {
|
||
return this.mediaElement.playbackRate;
|
||
}
|
||
get isIframeRate() {
|
||
const rate = this.desiredRate;
|
||
return isIframeRate(rate);
|
||
}
|
||
get isIframeRate$() {
|
||
return this.desiredRate$.pipe(map(isIframeRate));
|
||
}
|
||
get msObjectUrl$() {
|
||
return this.selectActive(({ mediaSourceEntity }) => mediaSourceEntity === null || mediaSourceEntity === void 0 ? void 0 : mediaSourceEntity.objectUrl).pipe(distinctUntilChanged());
|
||
}
|
||
get msReadyState$() {
|
||
return this.selectActive(({ mediaSourceEntity }) => { var _a; return (_a = mediaSourceEntity === null || mediaSourceEntity === void 0 ? void 0 : mediaSourceEntity.readyState) !== null && _a !== void 0 ? _a : null; });
|
||
}
|
||
get readyState() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.readyState) !== null && _b !== void 0 ? _b : 0;
|
||
}
|
||
get readyState$() {
|
||
return this.selectActive(({ readyState }) => readyState !== null && readyState !== void 0 ? readyState : 0);
|
||
}
|
||
get mediaSourceEntity$() {
|
||
return this.selectActive(({ mediaSourceEntity }) => mediaSourceEntity);
|
||
}
|
||
get expectedSbCount$() {
|
||
return this.selectActive(({ expectedSbCount }) => expectedSbCount);
|
||
}
|
||
get expectedSbCount() {
|
||
var _a;
|
||
return (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.expectedSbCount;
|
||
}
|
||
get paused$() {
|
||
return this.selectActive(({ paused }) => paused);
|
||
}
|
||
get paused() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.paused) !== null && _b !== void 0 ? _b : true;
|
||
}
|
||
get playbackStarted() {
|
||
var _a;
|
||
return isFiniteNumber((_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.firstPlayTime);
|
||
}
|
||
get flushing$() {
|
||
return this.selectActive(({ flushing }) => flushing);
|
||
}
|
||
get flushing() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.flushing) !== null && _b !== void 0 ? _b : false;
|
||
}
|
||
get waitingForDisco$() {
|
||
return this.selectActive(({ waitingForDisco }) => waitingForDisco);
|
||
}
|
||
get waitingForDisco() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.waitingForDisco) !== null && _b !== void 0 ? _b : false;
|
||
}
|
||
get gotPlaying() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.gotPlaying) !== null && _b !== void 0 ? _b : false;
|
||
}
|
||
get gotPlaying$() {
|
||
return this.selectActive(({ gotPlaying }) => gotPlaying);
|
||
}
|
||
get gotLoadStart$() {
|
||
return this.selectActive(({ gotLoadStart }) => gotLoadStart);
|
||
}
|
||
get seekTo() {
|
||
var _a;
|
||
return (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.seekTo;
|
||
}
|
||
get seekTo$() {
|
||
return this.selectActive(({ seekTo }) => seekTo);
|
||
}
|
||
get seeking() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.seeking) !== null && _b !== void 0 ? _b : false;
|
||
}
|
||
get seeking$() {
|
||
return this.selectActive(({ seeking }) => seeking);
|
||
}
|
||
get nudgeTarget$() {
|
||
return this.selectActive(({ nudgeInfo }) => nudgeInfo === null || nudgeInfo === void 0 ? void 0 : nudgeInfo.nudgeTarget);
|
||
}
|
||
get nudgeCount() {
|
||
var _a, _b, _c;
|
||
return (_c = (_b = (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.nudgeInfo) === null || _b === void 0 ? void 0 : _b.nudgeCount) !== null && _c !== void 0 ? _c : 0;
|
||
}
|
||
get sourceBufferEntities$() {
|
||
return this.selectActive(({ mediaSourceEntity }) => mediaSourceEntity === null || mediaSourceEntity === void 0 ? void 0 : mediaSourceEntity.sourceBufferEntities);
|
||
}
|
||
sourceBufferEntityByType$(sbType) {
|
||
return this.selectActive(({ mediaSourceEntity }) => { var _a; return (_a = mediaSourceEntity === null || mediaSourceEntity === void 0 ? void 0 : mediaSourceEntity.sourceBufferEntities) === null || _a === void 0 ? void 0 : _a[sbType]; });
|
||
}
|
||
// Get info on what we think is currently in the buffer by type
|
||
bufferedSegmentsByType$(sbType) {
|
||
return this.selectActive(({ mediaSourceEntity }) => { var _a, _b, _c; return (_c = (_b = (_a = mediaSourceEntity === null || mediaSourceEntity === void 0 ? void 0 : mediaSourceEntity.sourceBufferEntities) === null || _a === void 0 ? void 0 : _a[sbType]) === null || _b === void 0 ? void 0 : _b.bufferedSegments) !== null && _c !== void 0 ? _c : []; });
|
||
}
|
||
getBufferedSegmentsByType(sbType) {
|
||
var _a, _b;
|
||
return (_b = (_a = this.sourceBufferEntityByType(sbType)) === null || _a === void 0 ? void 0 : _a.bufferedSegments) !== null && _b !== void 0 ? _b : [];
|
||
}
|
||
// Get info on what we think is currently in both source buffers
|
||
get bufferedSegmentsTuple$() {
|
||
return combineQueries([this.bufferedSegmentsByType$(SourceBufferType.Variant), this.bufferedSegmentsByType$(SourceBufferType.AltAudio)]).pipe(debounceTime(10));
|
||
}
|
||
get timeupdate$() {
|
||
const target = fromEventTarget(this.mediaElement);
|
||
return target.event('timeupdate').pipe(observeOn(asyncScheduler), share(), throttleTime(125, undefined, { leading: true, trailing: true }), map((_) => this.currentTime), filter((pos) => isFiniteNumber(pos)));
|
||
}
|
||
get playingEvent$() {
|
||
const target = fromEventTarget(this.mediaElement);
|
||
return target.event('playing').pipe(map(() => undefined));
|
||
}
|
||
get mediaElementEntity$() {
|
||
return this.selectActive((entity) => Boolean(entity));
|
||
}
|
||
get ended$() {
|
||
return this.selectActive((entity) => { var _a; return (_a = entity === null || entity === void 0 ? void 0 : entity.ended) !== null && _a !== void 0 ? _a : false; });
|
||
}
|
||
sbUpdating$(type) {
|
||
return this.selectActive((entity) => { var _a, _b, _c, _d; return (_d = (_c = (_b = (_a = entity === null || entity === void 0 ? void 0 : entity.mediaSourceEntity) === null || _a === void 0 ? void 0 : _a.sourceBufferEntities) === null || _b === void 0 ? void 0 : _b[type]) === null || _c === void 0 ? void 0 : _c.updating) !== null && _d !== void 0 ? _d : false; });
|
||
}
|
||
sbUpdating(type) {
|
||
var _a, _b, _c, _d, _e;
|
||
return (_e = (_d = (_c = (_b = (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.mediaSourceEntity) === null || _b === void 0 ? void 0 : _b.sourceBufferEntities) === null || _c === void 0 ? void 0 : _c[type]) === null || _d === void 0 ? void 0 : _d.updating) !== null && _e !== void 0 ? _e : false;
|
||
}
|
||
sbError$(type) {
|
||
return this.selectActive((entity) => { var _a, _b, _c; return (_c = (_b = (_a = entity === null || entity === void 0 ? void 0 : entity.mediaSourceEntity) === null || _a === void 0 ? void 0 : _a.sourceBufferEntities) === null || _b === void 0 ? void 0 : _b[type]) === null || _c === void 0 ? void 0 : _c.error; });
|
||
}
|
||
get updating$() {
|
||
return combineQueries([this.sbUpdating$(SourceBufferType.Variant), this.sbUpdating$(SourceBufferType.AltAudio)]).pipe(map((values) => values.some((updating) => updating)));
|
||
}
|
||
get bufferedRangeTuple$() {
|
||
return combineQueries([
|
||
this.selectActive((entity) => { var _a, _b, _c, _d; return (_d = (_c = (_b = (_a = entity === null || entity === void 0 ? void 0 : entity.mediaSourceEntity) === null || _a === void 0 ? void 0 : _a.sourceBufferEntities) === null || _b === void 0 ? void 0 : _b[MediaOptionType.Variant]) === null || _c === void 0 ? void 0 : _c.bufferedRanges) !== null && _d !== void 0 ? _d : null; }),
|
||
this.selectActive((entity) => { var _a, _b, _c, _d; return (_d = (_c = (_b = (_a = entity === null || entity === void 0 ? void 0 : entity.mediaSourceEntity) === null || _a === void 0 ? void 0 : _a.sourceBufferEntities) === null || _b === void 0 ? void 0 : _b[MediaOptionType.AltAudio]) === null || _c === void 0 ? void 0 : _c.bufferedRanges) !== null && _d !== void 0 ? _d : null; }),
|
||
]);
|
||
}
|
||
getBufferedRangeByType(type) {
|
||
var _a, _b;
|
||
return (_b = (_a = this.sourceBufferEntities[type]) === null || _a === void 0 ? void 0 : _a.bufferedRanges) !== null && _b !== void 0 ? _b : [];
|
||
}
|
||
get combinedBuffer$() {
|
||
return this.selectActive((entity) => { var _a; return (_a = entity === null || entity === void 0 ? void 0 : entity.bufferedRanges) !== null && _a !== void 0 ? _a : []; });
|
||
}
|
||
getBufferInfo(pos, maxBufferHole) {
|
||
var _a;
|
||
const bufferedRanges = (_a = this.sourceBufferEntities) === null || _a === void 0 ? void 0 : _a.map((x) => x === null || x === void 0 ? void 0 : x.bufferedRanges);
|
||
const defaultInfo = { buffered: { start: pos, end: pos, len: 0 }, bufferedSegments: [] };
|
||
const res = [defaultInfo, defaultInfo];
|
||
if (!bufferedRanges) {
|
||
return res;
|
||
}
|
||
bufferedRanges.forEach((ranges, type) => {
|
||
var _a;
|
||
if (ranges) {
|
||
const bufferInfo = BufferHelper.getBufferedInfo(ranges, pos, maxBufferHole);
|
||
const segments = (_a = this.sourceBufferEntities[type].bufferedSegments) !== null && _a !== void 0 ? _a : [];
|
||
const bufferedSegments = segments.filter((s) => !(s.endPTS < bufferInfo.start || s.startPTS > bufferInfo.end));
|
||
res[type] = { buffered: bufferInfo, bufferedSegments };
|
||
}
|
||
});
|
||
return res;
|
||
}
|
||
getCombinedBufferInfo(pos, maxBufferHole) {
|
||
const entity = this.getActive();
|
||
if (entity) {
|
||
return BufferHelper.getBufferedInfo(entity.bufferedRanges, pos, maxBufferHole);
|
||
}
|
||
return null;
|
||
}
|
||
get bufferMonitorInfo() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.bufferMonitorInfo) !== null && _b !== void 0 ? _b : null;
|
||
}
|
||
get bufferMonitorThresholds$() {
|
||
return this.selectActive((entity) => {
|
||
const info = entity === null || entity === void 0 ? void 0 : entity.bufferMonitorInfo;
|
||
if (!info) {
|
||
return null;
|
||
}
|
||
const { almostDryWaterLevelSeconds, lowWaterLevelSeconds, highWaterLevelSeconds, maxBufferSeconds } = info;
|
||
return { almostDryWaterLevelSeconds, lowWaterLevelSeconds, highWaterLevelSeconds, maxBufferSeconds };
|
||
}).pipe(distinctUntilChanged((a, b) => (a === null || a === void 0 ? void 0 : a.lowWaterLevelSeconds) === (b === null || b === void 0 ? void 0 : b.lowWaterLevelSeconds)));
|
||
}
|
||
get waterLevelType$() {
|
||
return this.selectActive((entity) => { var _a; return (_a = entity === null || entity === void 0 ? void 0 : entity.bufferMonitorInfo.waterLevelType) !== null && _a !== void 0 ? _a : null; });
|
||
}
|
||
waterLevelForType(sbType) {
|
||
var _a, _b, _c;
|
||
return (_c = (_b = (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.bufferMonitorInfo.waterLevelType) === null || _b === void 0 ? void 0 : _b.sbTuple[sbType]) !== null && _c !== void 0 ? _c : null;
|
||
}
|
||
waterLevelChangedForType$(sbType) {
|
||
return this.waterLevelType$.pipe(map((waterLevelType) => {
|
||
if (waterLevelType == null) {
|
||
return null;
|
||
}
|
||
if (sbType == null) {
|
||
return waterLevelType.combined;
|
||
}
|
||
return waterLevelType.sbTuple[sbType];
|
||
}));
|
||
}
|
||
/**
|
||
* @returns emits whether we crossed LowWaterLevel or AlmostDryWater thresholds
|
||
*/
|
||
get fellBelowLowWater$() {
|
||
return this.waterLevelChangedForType$(SourceBufferType.Variant).pipe(pairwise(), map(([from, to]) => {
|
||
return waterLevelFell(from, to) && (to === BufferWaterLevel.LowWater || to === BufferWaterLevel.AlmostDry);
|
||
}), withLatestFrom(this.seekTo$, this.waitingForDisco$), map(([fellBelowThreshold, seekTo, waitingForDisco]) => {
|
||
return fellBelowThreshold && !isFiniteNumber(seekTo === null || seekTo === void 0 ? void 0 : seekTo.pos) && !waitingForDisco;
|
||
}), startWith(false));
|
||
}
|
||
/**
|
||
* @returns whether the current position has buffer all the way up to the end
|
||
* FIXME: Gapless mode uses mediaElementDuration because current sb.totalduration may confuse item preloading
|
||
* (consider reseting sb.totalduration or use another variable to track preloading state)
|
||
*/
|
||
isBufferedToEnd$(maxBufferHole, nonGapless = true) {
|
||
return combineQueries([
|
||
this.combinedBuffer$,
|
||
this.selectActive((entity) => entity.bufferMonitorInfo).pipe(filterNullOrUndefined(),
|
||
// Threshold value
|
||
map((bufferInfo) => (nonGapless ? bufferInfo.almostDryWaterLevelSeconds : Math.max(bufferInfo.almostDryWaterLevelSeconds, bufferInfo.lowWaterLevelSeconds / 2)))),
|
||
this.seeking$,
|
||
]).pipe(map(([combinedBuffer, closeToEndThreshold]) => {
|
||
const totalSBBufferedDuration = this.minSBDuration;
|
||
if (!combinedBuffer || (!isFiniteNumber(totalSBBufferedDuration) && nonGapless)) {
|
||
return false;
|
||
}
|
||
// TODO: <rdar://78592207> may need to tweak this for gapless mode
|
||
const combined = BufferHelper.getBufferedInfo(combinedBuffer, this.currentTime, maxBufferHole);
|
||
const bufferEnd = combined.end;
|
||
let duration;
|
||
let ended;
|
||
if (nonGapless) {
|
||
// one last combined buffer duration check against known sb.totalduration (should be the same value)
|
||
duration = totalSBBufferedDuration;
|
||
ended = Math.abs(duration - bufferEnd) <= closeToEndThreshold; // remove closeToEndThreshould if we can confirm combined buffered len is as accurate as sb buffered len
|
||
}
|
||
else {
|
||
duration = this.mediaElementDuration;
|
||
ended = duration - bufferEnd <= closeToEndThreshold;
|
||
}
|
||
getLogger().trace('Fully buffered calculation duration=%d, bufferEnd=%d, closeToEndThreshold=%d ended=%d', totalSBBufferedDuration, bufferEnd, closeToEndThreshold, ended);
|
||
return ended;
|
||
}), distinctUntilChanged());
|
||
}
|
||
needData$(maxBufferHole, inGaplessMode = false) {
|
||
const useMinSbDuration = !inGaplessMode;
|
||
return combineQueries([
|
||
this.msReadyState$,
|
||
this.waterLevelChangedForType$(null),
|
||
this.isBufferedToEnd$(maxBufferHole, useMinSbDuration),
|
||
this.bufferedRangeTuple$,
|
||
this.seekTo$,
|
||
this.mediaElementDuration$,
|
||
]).pipe(debounceTime(10),
|
||
// observeOn(asyncScheduler), debounceTime already uses asyncScheduler
|
||
map(([readyState, waterLevelType, bufferedToEnd, _, seekTo]) => {
|
||
if (readyState === 'closed') {
|
||
return false; // Allow append if 'ended'
|
||
}
|
||
if (inGaplessMode) {
|
||
getLogger().debug({ name: 'MediaQuery' }, 'Preloading is true, forcing needData to true');
|
||
return true;
|
||
}
|
||
const haveRoomToLoad = waterLevelType == null || (!bufferedToEnd && waterLevelType !== BufferWaterLevel.AboveHighWater);
|
||
const userIsActive = this.isIframeRate || !!seekTo; // buffered to end but user may seek or trickplay away to unbuffered ranges
|
||
getLogger().debug({ name: 'MediaQuery' }, `needData = haveRoomToLoad ${haveRoomToLoad} = (bufferedToEnd: ${bufferedToEnd}, waterLevelType: ${waterLevelType}) or userIsActive ${userIsActive} = (isIframeRate: ${this.isIframeRate}, seekTo.pos: ${seekTo === null || seekTo === void 0 ? void 0 : seekTo.pos})`);
|
||
return haveRoomToLoad || (waterLevelType !== BufferWaterLevel.AboveHighWater && userIsActive);
|
||
}), tag('needData'));
|
||
}
|
||
getSourceBufferInfoAction(needData, anchorTime, switchContexts, maxBufferHole) {
|
||
const { currentTime, sourceBufferEntities, msReadyState } = this;
|
||
let bufferInfoTuple = [null, null];
|
||
if (!needData && switchContexts.every((x) => !(x === null || x === void 0 ? void 0 : x.userInitiated))) {
|
||
return null;
|
||
}
|
||
if (msReadyState !== 'open' || !sourceBufferEntities || sourceBufferEntities[0] == null) {
|
||
return { position: anchorTime === null || anchorTime === void 0 ? void 0 : anchorTime.pos, discoSeqNum: anchorTime === null || anchorTime === void 0 ? void 0 : anchorTime.discoSeqNum, bufferInfoTuple, switchContexts };
|
||
}
|
||
bufferInfoTuple = this.getBufferInfo(currentTime, maxBufferHole);
|
||
return { position: currentTime, discoSeqNum: anchorTime === null || anchorTime === void 0 ? void 0 : anchorTime.discoSeqNum, bufferInfoTuple, switchContexts };
|
||
}
|
||
get haveEnough() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.haveEnough) !== null && _b !== void 0 ? _b : false;
|
||
}
|
||
get haveEnough$() {
|
||
return this.selectActive(({ haveEnough }) => haveEnough);
|
||
}
|
||
static likelyToKeepUp(mediaElement, haveEnough, readyState) {
|
||
return haveEnough && readyState >= mediaElement.HAVE_FUTURE_DATA;
|
||
}
|
||
get playbackLikelyToKeepUp() {
|
||
return MediaElementQuery.likelyToKeepUp(this.mediaElement, this.haveEnough, this.readyState);
|
||
}
|
||
get playbackLikelyToKeepUp$() {
|
||
return combineQueries([this.haveEnough$, this.readyState$]).pipe(map(([haveEnough, readyState]) => MediaElementQuery.likelyToKeepUp(this.mediaElement, haveEnough, readyState)));
|
||
}
|
||
// Combined water level at current position
|
||
getCurrentWaterLevel(maxBufferHole) {
|
||
var _a, _b;
|
||
const currentTime = this.currentTime;
|
||
const bufferedRanges = (_b = (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.bufferedRanges) !== null && _b !== void 0 ? _b : [];
|
||
const bufferInfo = BufferHelper.getBufferedInfo(bufferedRanges, currentTime, maxBufferHole);
|
||
return bufferInfo.len;
|
||
}
|
||
//Returns buffer level info, this is only being used for QE logging.
|
||
getCombinedMediaSourceBufferInfo(maxBufferHole) {
|
||
var _a, _b, _c, _d;
|
||
const currentTime = this.currentTime;
|
||
const [variantBufferedRanges, altAudioBufferedRanges] = (_b = (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.mediaSourceEntity) === null || _b === void 0 ? void 0 : _b.sourceBufferEntities;
|
||
const variantBufferInfo = BufferHelper.getBufferedInfo((_c = variantBufferedRanges === null || variantBufferedRanges === void 0 ? void 0 : variantBufferedRanges.bufferedRanges) !== null && _c !== void 0 ? _c : [], currentTime, maxBufferHole);
|
||
const altAudioBufferInfo = BufferHelper.getBufferedInfo((_d = altAudioBufferedRanges === null || altAudioBufferedRanges === void 0 ? void 0 : altAudioBufferedRanges.bufferedRanges) !== null && _d !== void 0 ? _d : [], currentTime, maxBufferHole);
|
||
return [variantBufferInfo, altAudioBufferInfo];
|
||
}
|
||
// SourceBuffer water level at current position
|
||
getCurrentWaterLevelByType(sbType, maxBufferHole) {
|
||
var _a;
|
||
const currentTime = this.currentTime;
|
||
const sbEntity = this.sourceBufferEntityByType(sbType);
|
||
const bufferedRanges = (_a = sbEntity === null || sbEntity === void 0 ? void 0 : sbEntity.bufferedRanges) !== null && _a !== void 0 ? _a : [];
|
||
const bufferInfo = BufferHelper.getBufferedInfo(bufferedRanges, currentTime, maxBufferHole);
|
||
return bufferInfo.len;
|
||
}
|
||
/**
|
||
* For live, returns true, If the live update, has overlap with
|
||
* already appended buffer, and therefore can be played back without gap
|
||
*/
|
||
canContinuePlaybackWithoutGap(details, lastUpdateMillis, playlistEstimate, maxBufferHole) {
|
||
if (details.type !== 'LIVE') {
|
||
// even for EVENT, return true as predictedStartPosition will be 0.
|
||
return true;
|
||
}
|
||
if (!details.ptsKnown) {
|
||
return false;
|
||
}
|
||
const position = this.currentTime;
|
||
const predictedReceiveTime = performance.now() + playlistEstimate.avgPlaylistLoadTimeMs + details.targetduration * 1000;
|
||
const predictedStartPosition = details.fragments[0].start + (predictedReceiveTime - lastUpdateMillis) / 1000;
|
||
const bufferInfo = this.getCombinedBufferInfo(position, maxBufferHole);
|
||
let end = bufferInfo.end;
|
||
if (end >= details.fragments[0].start - maxBufferHole && end <= details.fragments[0].start + details.totalduration) {
|
||
end = details.fragments[0].start + details.totalduration;
|
||
}
|
||
return predictedStartPosition <= end;
|
||
}
|
||
get stallInfo$() {
|
||
return this.selectActive((entity) => { var _a; return (_a = entity === null || entity === void 0 ? void 0 : entity.stallInfo) !== null && _a !== void 0 ? _a : null; });
|
||
}
|
||
get textTracks() {
|
||
return this.mediaElement.textTracks;
|
||
}
|
||
get textTracksCreated$() {
|
||
return this.selectActive((entity) => entity === null || entity === void 0 ? void 0 : entity.textTracksCreated);
|
||
}
|
||
get mediaOptionParsedSubtitleRecord() {
|
||
var _a;
|
||
return (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.mediaOptionParsedSubtitleRecord;
|
||
}
|
||
getParsedSubtitleRecordsForMediaOption(persistentId) {
|
||
if (!this.mediaOptionParsedSubtitleRecord)
|
||
return null;
|
||
const subtitleParsedInfoDict = this.mediaOptionParsedSubtitleRecord[persistentId];
|
||
return subtitleParsedInfoDict ? subtitleParsedInfoDict : {};
|
||
}
|
||
}
|
||
|
||
class MediaFunctions {
|
||
constructor(mediaSink, media, config, logger) {
|
||
this.mediaSink = mediaSink;
|
||
this.media = media;
|
||
this.logger = logger;
|
||
this.useCustomMediaFunctions = config.useCustomMediaFunctions;
|
||
this.overridePlaybackRate = config.overridePlaybackRate;
|
||
}
|
||
install() {
|
||
const media = this.media;
|
||
if (!media) {
|
||
return;
|
||
}
|
||
// * guarantee original play, pause methods will be in media.originalPlay and media.originalPause.
|
||
// * always override media.play and media.pause with new methods.
|
||
if (this.useCustomMediaFunctions && media && media.play && media.pause) {
|
||
// not a bogus media object for unit tests
|
||
// always preserve the original play, pause methods
|
||
if (!media.originalPlay) {
|
||
media.originalPlay = media.play.bind(media);
|
||
}
|
||
if (!media.originalPause) {
|
||
media.originalPause = media.pause.bind(media);
|
||
}
|
||
// always install new functions over old ones.
|
||
this.logger.debug('install custom pause & play methods');
|
||
media.play = () => {
|
||
this.logger.debug('overridden play');
|
||
this.mediaSink.checkForReplay();
|
||
this.mediaSink.desiredRate = 1;
|
||
const isPlaying = media.currentTime > 0 && !media.paused && !media.ended && media.readyState > 2 ? true : false;
|
||
if (!isPlaying) {
|
||
// media is not playing now, save the promise and handle whenever the originalPlay promise returns
|
||
return new Promise((resolve, reject) => {
|
||
if (!this.pendingPlayPromises) {
|
||
this.pendingPlayPromises = [];
|
||
}
|
||
this.pendingPlayPromises.push({ resolve, reject });
|
||
});
|
||
}
|
||
else {
|
||
return Promise.resolve();
|
||
}
|
||
};
|
||
media.pause = () => {
|
||
this.logger.debug('overridden pause');
|
||
this.mediaSink.desiredRate = 0;
|
||
};
|
||
}
|
||
if (typeof HTMLMediaElement === 'function' && this.overridePlaybackRate) {
|
||
Object.defineProperty(media, 'playbackRate', {
|
||
enumerable: true,
|
||
configurable: true,
|
||
get: function () {
|
||
// pretend it's always 1 to all callers
|
||
return 1;
|
||
},
|
||
set: function (newValue) {
|
||
// allow passthrough set to signal trickplay mode to the media engine
|
||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate').set.call(this, newValue);
|
||
},
|
||
});
|
||
}
|
||
this.playPromise = null;
|
||
this.expectPauseEvent = this.expectPlayEvent = false;
|
||
}
|
||
uninstall() {
|
||
const media = this.media;
|
||
if (media) {
|
||
if (media.originalPlay) {
|
||
this.logger.debug('restore media.play');
|
||
media.play = media.originalPlay;
|
||
delete media.originalPlay;
|
||
}
|
||
if (media.originalPause) {
|
||
this.logger.debug('restore media.pause');
|
||
media.pause = media.originalPause;
|
||
delete media.originalPause;
|
||
}
|
||
if (this.overridePlaybackRate) {
|
||
media.playbackRate = 1; // restore original value
|
||
delete media.playbackRate;
|
||
}
|
||
}
|
||
this.playPromise = null;
|
||
this.expectPauseEvent = this.expectPlayEvent = false;
|
||
}
|
||
play() {
|
||
if (this.media) {
|
||
const flushing = this.mediaSink.flushing;
|
||
if (this.playPromise || flushing) {
|
||
this.logger.warn(`Ignoring play command playPromise/flushing ${Boolean(this.playPromise)}/${flushing}`);
|
||
return;
|
||
}
|
||
this.expectPlayEvent = this.expectPlayEvent || this.media.paused;
|
||
this.logger.info(`play() expectPlayEvent=${this.expectPlayEvent}`);
|
||
this.playPromise = this._mediaPlayInternal();
|
||
if (this.playPromise) {
|
||
this.playPromise
|
||
.then(function () {
|
||
this.logger.info('play() complete');
|
||
this.playPromise = null;
|
||
this._handlePendingPlayPromises(null);
|
||
}.bind(this))
|
||
.catch(function (err) {
|
||
// Could be aborted by pause
|
||
this.playPromise = null;
|
||
this.expectPlayEvent = false;
|
||
this._handlePendingPlayPromises(err || new Error('Play rejected for unknown reason'));
|
||
if ((err === null || err === void 0 ? void 0 : err.name) === 'NotAllowedError') {
|
||
this.logger.warn('play() not allowed, going back to rate 0');
|
||
this.mediaSink.desiredRate = 0;
|
||
}
|
||
else {
|
||
this.logger.error(`play() error: ${err === null || err === void 0 ? void 0 : err.message}`);
|
||
}
|
||
}.bind(this));
|
||
}
|
||
}
|
||
}
|
||
pause() {
|
||
if (this.media) {
|
||
this.logger.info(`pause() playPromise=${Boolean(this.playPromise)}`);
|
||
if (this.playPromise) {
|
||
// this.playPromise is cleared as soon
|
||
// as it resolves in _mediaPlay so we shouldn't fall in here twice
|
||
this.playPromise
|
||
.then(() => {
|
||
const mediaQuery = this.mediaSink.mediaQuery;
|
||
if (this.mediaSink.desiredRate === 0 || (mediaQuery.seeking && !mediaQuery.playbackLikelyToKeepUp)) {
|
||
this._mediaPauseInternal();
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
this.logger.error(`Promise error in pause(): ${error.message}`);
|
||
});
|
||
}
|
||
else {
|
||
this._mediaPauseInternal();
|
||
}
|
||
}
|
||
}
|
||
_handlePendingPlayPromises(err) {
|
||
var _a;
|
||
const count = (_a = this.pendingPlayPromises) === null || _a === void 0 ? void 0 : _a.length;
|
||
if (!err) {
|
||
// resolve all pending promises
|
||
for (let i = 0; i < count; i++) {
|
||
this.pendingPlayPromises[i].resolve();
|
||
}
|
||
}
|
||
else {
|
||
// reject all pending promises
|
||
for (let i = 0; i < count; i++) {
|
||
this.pendingPlayPromises[i].reject(err);
|
||
}
|
||
}
|
||
this.pendingPlayPromises = [];
|
||
}
|
||
_mediaPlayInternal() {
|
||
const playFn = this.media.originalPlay || this.media.play.bind(this.media);
|
||
return playFn();
|
||
}
|
||
_mediaPauseInternal() {
|
||
this.expectPauseEvent = this.expectPauseEvent || !this.media.paused;
|
||
this.logger.info(`pause() expectPauseEvent=${this.expectPauseEvent}`);
|
||
const pauseFn = this.media.originalPause || this.media.pause.bind(this.media);
|
||
return pauseFn();
|
||
}
|
||
}
|
||
|
||
class MseError extends Error {
|
||
}
|
||
// Generic SourceBuffer error
|
||
class SourceBufferError extends HlsError {
|
||
constructor(details, fatal, reason, response, sbType) {
|
||
super(ErrorTypes.MEDIA_ERROR, details, fatal, reason, response);
|
||
this.sbType = sbType;
|
||
this.response = response;
|
||
}
|
||
}
|
||
// Got error when calling addSourceBuffer (unsupported format)
|
||
class CreateSourceBufferError extends SourceBufferError {
|
||
constructor(message, response, sbType, mediaOptionId) {
|
||
super(ErrorDetails.BUFFER_ADD_CODEC_ERROR, false, message, response, sbType);
|
||
this.mediaOptionId = mediaOptionId;
|
||
this.mediaOptionType = sourceBufferTypeToMediaOptionType(this.sbType);
|
||
}
|
||
}
|
||
class AppendBufferError extends SourceBufferError {
|
||
constructor(details, fatal, reason, response, sbType, isTimeout) {
|
||
super(details, fatal, reason, response, sbType);
|
||
this.isTimeout = isTimeout;
|
||
this.mediaOptionType = sourceBufferTypeToMediaOptionType(this.sbType);
|
||
}
|
||
}
|
||
class BufferFullError extends AppendBufferError {
|
||
constructor(reason, response, sbType, maxTotalBytes) {
|
||
super(ErrorDetails.BUFFER_FULL_ERROR, false, reason, response, sbType, false);
|
||
this.maxTotalBytes = maxTotalBytes;
|
||
}
|
||
}
|
||
class AppendTimeoutError extends AppendBufferError {
|
||
constructor(reason, response, sbType) {
|
||
super(ErrorDetails.BUFFER_APPEND_ERROR, false, reason, response, sbType, true);
|
||
}
|
||
}
|
||
// Got decode error during append
|
||
class MediaDecodeError extends AppendBufferError {
|
||
constructor(reason, response, sbType, mediaOptionId) {
|
||
super(ErrorDetails.BUFFER_APPEND_ERROR, false, reason, response, sbType, false);
|
||
this.mediaOptionId = mediaOptionId;
|
||
this.mediaOptionType = sourceBufferTypeToMediaOptionType(this.sbType);
|
||
}
|
||
}
|
||
class BufferStallError extends HlsError {
|
||
constructor(details, fatal, reason, response, stallType, bufferLen, nudgePosition = NaN) {
|
||
super(ErrorTypes.MEDIA_ERROR, details, fatal, reason, response);
|
||
this.stallType = stallType;
|
||
this.bufferLen = bufferLen;
|
||
this.nudgePosition = nudgePosition;
|
||
this.response = response;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Observable SourceBuffer adapter that cleans up after itself on unsubscribe
|
||
* From src/mse-rxjs/sourcebuffer-adapter.ts
|
||
*/
|
||
const APPEND_MAX_MS = 10000; // Failsafe append timeout value
|
||
class SourceBufferAdapter extends Observable {
|
||
constructor(mediaElementStore, mediaElementQuery, mediaElement, mediaSource, type, sourceBuffer, compatInfo, parentLogger, config) {
|
||
super((subscriber) => {
|
||
const target = fromEventTarget(sourceBuffer);
|
||
const logger = parentLogger.child({ sb: type });
|
||
mediaElementStore.setSourceBufferEntity(type, compatInfo);
|
||
if (compatInfo.mimeType.includes('audio/mpeg')) {
|
||
this.updateMp3Timestamps = true;
|
||
}
|
||
const sub = merge(target.event('updatestart').pipe(tap(() => {
|
||
logger.trace(`[sb${this.type}] updatestart`);
|
||
mediaElementStore.setSourceBufferUpdating(type);
|
||
})),
|
||
// Note we need to do this to keep buffer data updated,
|
||
// unless we start allowing abort() as part of teardown / finalize on append/remove
|
||
target.event('updateend').pipe(observeOn(asyncScheduler), tap(() => {
|
||
const bufferedRanges = BufferHelper.timeRangesToBufferedRange(sourceBuffer.buffered);
|
||
const combinedBuffer = BufferHelper.timeRangesToBufferedRange(mediaElement.buffered);
|
||
logger.trace(`[sb${this.type}] updateend`);
|
||
mediaElementStore.setBufferedRangesUpdated(type, bufferedRanges, combinedBuffer, false, config);
|
||
})), target.event('error').pipe(tap(() => {
|
||
mediaElementStore.setSourceBufferError(type, 'Got source buffer error');
|
||
})))
|
||
.pipe(switchMap(() => EMPTY))
|
||
.subscribe(subscriber);
|
||
return () => {
|
||
sub.unsubscribe();
|
||
try {
|
||
if (mediaSource.readyState === 'open') {
|
||
sourceBuffer.abort();
|
||
}
|
||
mediaSource.removeSourceBuffer(sourceBuffer);
|
||
}
|
||
catch (err) {
|
||
logger.error(`Error aborting SourceBuffer on unsubscribe: ${err.message}`);
|
||
}
|
||
};
|
||
});
|
||
this.mediaElementStore = mediaElementStore;
|
||
this.mediaElementQuery = mediaElementQuery;
|
||
this.mediaElement = mediaElement;
|
||
this.type = type;
|
||
this.sourceBuffer = sourceBuffer;
|
||
this.config = config;
|
||
this.updateMp3Timestamps = false;
|
||
}
|
||
get buffered() {
|
||
return this.sourceBuffer.buffered;
|
||
}
|
||
/**
|
||
* @param data The buffer to append
|
||
* @param segment Info about the segment. null means init segment
|
||
*/
|
||
appendBuffer(data, segment) {
|
||
// Use defer so that on re-subscribe we will call appendBuffer again
|
||
return defer(() => {
|
||
if (this.sourceBuffer.updating) {
|
||
return this._waitForUpdateEndOrError().pipe(switchMap(() => this.appendBuffer(data, segment)));
|
||
}
|
||
return this._appendBufferAsync(data, segment);
|
||
});
|
||
}
|
||
_appendBufferAsync(data, segment) {
|
||
let startAppend = NaN;
|
||
let inFlight = null;
|
||
const mediaOptionId = 'startPTS' in segment ? segment.frag.mediaOptionId : segment.mediaOptionId;
|
||
try {
|
||
if ('startPTS' in segment) {
|
||
inFlight = { startPTS: segment.startPTS, endPTS: segment.endPTS, bytes: segment.bytes, frag: Object.assign({}, segment.frag) };
|
||
}
|
||
this.mediaElementStore.setInflightSegment(this.type, inFlight);
|
||
startAppend = performance.now();
|
||
this.sourceBuffer.appendBuffer(data);
|
||
}
|
||
catch (err) {
|
||
// Synchronous errors come from prepare append algorithm
|
||
// https://www.w3.org/TR/media-source/#sourcebuffer-prepare-append
|
||
switch (err.code) {
|
||
case 22: {
|
||
// QuotaExceededError: http://www.w3.org/TR/html5/infrastructure.html#quotaexceedederror
|
||
// let's stop appending any segments, and report BUFFER_FULL_ERROR error
|
||
this.mediaElementStore.setBufferedRangesUpdated(this.type, BufferHelper.timeRangesToBufferedRange(this.sourceBuffer.buffered), BufferHelper.timeRangesToBufferedRange(this.mediaElement.buffered), true, this.config);
|
||
return throwError(new BufferFullError(err.message, ErrorResponses.AllocationFailed, this.type, this.maxTotalBytes));
|
||
}
|
||
default:
|
||
this.mediaElementStore.setInflightSegment(this.type, null);
|
||
if (this.mediaElement.error) {
|
||
return throwError(new MediaDecodeError(err.message, ErrorResponses.VideoDecoderBadDataErr, this.type, mediaOptionId));
|
||
}
|
||
// 1. buffer removed from MediaSource
|
||
// 2. updating === true
|
||
// 3. other reason
|
||
return throwError(err);
|
||
}
|
||
}
|
||
return this._waitForUpdateEndOrError().pipe(map(() => ({
|
||
startAppend: startAppend,
|
||
endAppend: performance.now(),
|
||
bytesAppend: data.byteLength,
|
||
})), timeout(APPEND_MAX_MS), catchError((err) => {
|
||
if (err instanceof TimeoutError) {
|
||
this.sourceBuffer.abort();
|
||
err = new AppendTimeoutError(`Append took longer than ${APPEND_MAX_MS}ms`, ErrorResponses.InternalError, this.type);
|
||
}
|
||
else if (err instanceof SourceBufferError) {
|
||
err = new MediaDecodeError('Decode error', ErrorResponses.VideoDecoderBadDataErr, this.type, mediaOptionId);
|
||
}
|
||
throw err; // always throw
|
||
}));
|
||
}
|
||
remove(start, end) {
|
||
return this._waitForUpdateEndOrError().pipe(switchMap(this._removeAsync.bind(this, start, end)));
|
||
}
|
||
_removeAsync(start, end) {
|
||
try {
|
||
this.sourceBuffer.remove(start, end);
|
||
}
|
||
catch (err) {
|
||
return throwError(new SourceBufferError(ErrorDetails.INTERNAL_EXCEPTION, false, err.message, ErrorResponses.InternalError, this.type));
|
||
}
|
||
return this._waitForUpdateEndOrError();
|
||
}
|
||
abort() {
|
||
try {
|
||
this.sourceBuffer.abort();
|
||
}
|
||
catch (err) {
|
||
return throwError(new SourceBufferError(ErrorDetails.INTERNAL_EXCEPTION, false, err.message, ErrorResponses.InternalError, this.type));
|
||
}
|
||
return this._waitForUpdateEndOrError();
|
||
}
|
||
get updating() {
|
||
return this.sourceBuffer.updating;
|
||
}
|
||
get timestampOffset() {
|
||
return this.sourceBuffer.timestampOffset;
|
||
}
|
||
set timestampOffset(value) {
|
||
this.sourceBuffer.timestampOffset = value;
|
||
}
|
||
/**
|
||
* Whether we ever got QuotaExceeded error
|
||
*/
|
||
get gotQuotaExceeded() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.mediaElementQuery.sourceBufferEntityByType(this.type)) === null || _a === void 0 ? void 0 : _a.gotQuotaExceeded) !== null && _b !== void 0 ? _b : false;
|
||
}
|
||
/**
|
||
* Which segments we think are in the buffer
|
||
*/
|
||
get bufferedSegments() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.mediaElementQuery.sourceBufferEntityByType(this.type)) === null || _a === void 0 ? void 0 : _a.bufferedSegments) !== null && _b !== void 0 ? _b : [];
|
||
}
|
||
/**
|
||
* Currently appended byte total
|
||
*/
|
||
get totalBytes() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.mediaElementQuery.sourceBufferEntityByType(this.type)) === null || _a === void 0 ? void 0 : _a.totalBytes) !== null && _b !== void 0 ? _b : 0;
|
||
}
|
||
/**
|
||
* Max number of bytes we estimate this SourceBuffer can hold
|
||
*/
|
||
get maxTotalBytes() {
|
||
var _a, _b;
|
||
const maxTotalBytes = (_b = (_a = this.mediaElementQuery.sourceBufferEntityByType(this.type)) === null || _a === void 0 ? void 0 : _a.maxTotalBytes) !== null && _b !== void 0 ? _b : Infinity;
|
||
return this.gotQuotaExceeded ? maxTotalBytes : Infinity;
|
||
}
|
||
_waitForUpdateEndOrError() {
|
||
if (this.sourceBuffer.updating) {
|
||
// Just in case we haven't fired updatestart yet
|
||
this.mediaElementStore.setSourceBufferUpdating(this.type);
|
||
}
|
||
// Ensures we have up to date buffered range values by using updating$
|
||
return this.mediaElementQuery.sbUpdating$(this.type).pipe(filter((updating) => updating === false), withLatestFrom(this.mediaElementQuery.sbError$(this.type)), map(([_updating, error]) => {
|
||
if (error) {
|
||
throw new SourceBufferError(ErrorDetails.INTERNAL_EXCEPTION, false, 'Got error during sourceBuffer operation', ErrorResponses.InternalError, this.type);
|
||
}
|
||
return undefined;
|
||
}), take(1));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Observable MediaSOurce adapter that cleans up after itself on unsubscribe
|
||
* From src/mse-rxjs/mediasource-adapter.ts
|
||
*/
|
||
class MediaSourceAdapter extends Observable {
|
||
constructor(mediaElement, mediaElementStore, mediaElementQuery, mediaSource, logger) {
|
||
// Register for events. This updates the mediaElementStore and also sets up teardown
|
||
// when this object is unsubscribed from
|
||
super((subscriber) => {
|
||
const target = fromEventTarget(mediaSource);
|
||
const readyStateChange$ = merge(target.event('sourceopen'), target.event('sourceclose'), target.event('sourceended')).pipe(tap((event) => {
|
||
var _a;
|
||
// Vuze doesn't implement event
|
||
const ms = (_a = event === null || event === void 0 ? void 0 : event.target) !== null && _a !== void 0 ? _a : mediaSource;
|
||
const readyState = ms.readyState;
|
||
mediaElementStore.msReadyState = readyState;
|
||
}));
|
||
const sourceBuffers$ = this.sourceBuffers$.pipe(switchMap((sbTuple) => {
|
||
if (!sbTuple)
|
||
return EMPTY;
|
||
return merge(...sbTuple.filter((info) => info != null));
|
||
}));
|
||
const sub = merge(readyStateChange$, sourceBuffers$)
|
||
.pipe(switchMap(() => EMPTY))
|
||
.subscribe(subscriber);
|
||
const objectUrl = URL.createObjectURL(mediaSource);
|
||
mediaElement.src = objectUrl;
|
||
logger.info(`set MediaSource ${objectUrl}`);
|
||
mediaElementStore.setMediaSourceEntity(objectUrl, mediaSource.readyState);
|
||
return () => {
|
||
sub.unsubscribe();
|
||
// Hand off clean up to teardownWorker to avoid race with clearMediaKeys
|
||
logger.info(`remove MediaSource ${objectUrl}`);
|
||
URL.revokeObjectURL(objectUrl);
|
||
if (mediaElement.src === objectUrl) {
|
||
mediaElement.removeAttribute('src');
|
||
mediaElement.load();
|
||
mediaElementStore.setMediaSourceEntity(null);
|
||
}
|
||
this.sourceBuffers$.next(null);
|
||
};
|
||
});
|
||
this.mediaElement = mediaElement;
|
||
this.mediaElementStore = mediaElementStore;
|
||
this.mediaElementQuery = mediaElementQuery;
|
||
this.mediaSource = mediaSource;
|
||
this.logger = logger;
|
||
this.sourceBuffers$ = new BehaviorSubject(null);
|
||
}
|
||
get readyState() {
|
||
return this.mediaSource.readyState;
|
||
}
|
||
set duration(value) {
|
||
this.mediaSource.duration = value;
|
||
}
|
||
get duration() {
|
||
return this.mediaSource.duration;
|
||
}
|
||
endOfStream(error) {
|
||
this.mediaSource.endOfStream(error);
|
||
}
|
||
createSourceBuffers(compatInfoTuple, config) {
|
||
const mediaSource = this.mediaSource;
|
||
applyTransaction(() => {
|
||
try {
|
||
const newSbs = [null, null];
|
||
compatInfoTuple.forEach((compatInfo, type) => {
|
||
if (compatInfo) {
|
||
const { mimeType, mediaOptionId } = compatInfo;
|
||
let sb;
|
||
try {
|
||
sb = mediaSource.addSourceBuffer(mimeType);
|
||
this.logger.info(`[${SourceBufferNames[type]}]: sourceBuffer added ${mimeType}`);
|
||
}
|
||
catch (err) {
|
||
throw new CreateSourceBufferError(err.message, ErrorResponses.IncompatibleAsset, type, mediaOptionId);
|
||
}
|
||
newSbs[type] = new SourceBufferAdapter(this.mediaElementStore, this.mediaElementQuery, this.mediaElement, this.mediaSource, type, sb, compatInfo, this.logger, config);
|
||
}
|
||
});
|
||
this.sourceBuffers$.next(newSbs);
|
||
}
|
||
catch (err) {
|
||
if (!(err instanceof HlsError)) {
|
||
throw new MseError(`error initializing sourcebuffers ${err.message} readyState=${mediaSource.readyState}`);
|
||
}
|
||
throw err;
|
||
}
|
||
});
|
||
}
|
||
get needSourceBuffers() {
|
||
return this.sourceBuffers$.value == null || this.sourceBuffers$.value[0] == null;
|
||
}
|
||
get sourceBuffers() {
|
||
return this.sourceBuffers$.value;
|
||
}
|
||
getSourceBufferByType(type) {
|
||
const sbAdapterTuple = this.sourceBuffers$.value;
|
||
if (!sbAdapterTuple) {
|
||
return null;
|
||
}
|
||
return sbAdapterTuple[type];
|
||
}
|
||
updateLiveSeekableRange(start, end) {
|
||
const mediaSource = this.mediaSource;
|
||
if ((mediaSource === null || mediaSource === void 0 ? void 0 : mediaSource.setLiveSeekableRange) && (mediaSource === null || mediaSource === void 0 ? void 0 : mediaSource.readyState) === 'open') {
|
||
this.logger.debug(`setLiveSeekableRange range called with ${start} to ${end}`);
|
||
mediaSource.setLiveSeekableRange(start, end);
|
||
}
|
||
}
|
||
clearLiveSeekableRange() {
|
||
const mediaSource = this.mediaSource;
|
||
if ((mediaSource === null || mediaSource === void 0 ? void 0 : mediaSource.clearLiveSeekableRange) && (mediaSource === null || mediaSource === void 0 ? void 0 : mediaSource.readyState) === 'open') {
|
||
this.logger.debug('clearLiveSeekableRange called');
|
||
mediaSource.clearLiveSeekableRange();
|
||
}
|
||
}
|
||
}
|
||
|
||
const MIN_STALL_CHECK_MS = 100; // Minimum time for next check
|
||
function shouldCheckForStall(pos, desiredRate, ended, combinedBuffer, seeking) {
|
||
const isBuffered = combinedBuffer.some((r) => r.start <= pos && r.end > pos);
|
||
return !(desiredRate !== 1 || ended || combinedBuffer.length === 0 || (seeking && !isBuffered));
|
||
}
|
||
/**
|
||
* monitors the media element for stalling in low and high buffer or on seek
|
||
* It will update media store with information about the stall if the position has not changed
|
||
* after the configured period. emits stall info if stall is detected, null if we're not stalled
|
||
*/
|
||
function stallMonitor(logger, meQuery, config) {
|
||
const { lowBufferThreshold, lowBufferWatchdogPeriod, highBufferWatchdogPeriod, seekWatchdogPeriod } = config;
|
||
const shouldCheckForStall$ = combineQueries([meQuery.desiredRate$, meQuery.ended$, meQuery.combinedBuffer$, meQuery.seekTo$]).pipe(map((state) => {
|
||
const [desiredRate, ended, combinedBuffer, seekTo] = state;
|
||
const currentTime = meQuery.currentTime;
|
||
const seeking = isFinite(seekTo === null || seekTo === void 0 ? void 0 : seekTo.pos);
|
||
return shouldCheckForStall(currentTime, desiredRate, ended, combinedBuffer, seeking);
|
||
}), distinctUntilChanged());
|
||
const combinedBufferChanged$ = meQuery.combinedBuffer$.pipe(map(() => {
|
||
return meQuery.getCurrentWaterLevel(0) <= config.lowBufferThreshold || !meQuery.haveEnough ? StallType.LowBuffer : StallType.HighBuffer;
|
||
}), distinctUntilChanged());
|
||
const gotStall$ = combineQueries([shouldCheckForStall$, meQuery.seekTo$, meQuery.gotPlaying$, combinedBufferChanged$]).pipe(switchMap((state) => {
|
||
const [shouldCheckForStall, seekTo, gotPlaying] = state;
|
||
logger.debug(`[stall] state=${stringifyWithPrecision({ shouldCheckForStall, seekTo, gotPlaying })}`);
|
||
if (!shouldCheckForStall) {
|
||
return of(null);
|
||
}
|
||
// some systems need longer wait period for seeks / playback resume, use backoff period
|
||
const backoffSec = 1;
|
||
const nudgeCount = meQuery.nudgeCount;
|
||
const seekPeriodSec = seekWatchdogPeriod + nudgeCount * backoffSec;
|
||
const seeking = isFinite(seekTo === null || seekTo === void 0 ? void 0 : seekTo.pos);
|
||
if (seeking || !gotPlaying) {
|
||
logger.debug(`[stall] start seek stall watchdog period=${seekPeriodSec}s`);
|
||
return startSeekStallWatchdog(meQuery, performance.now(), seekPeriodSec, lowBufferThreshold);
|
||
}
|
||
const highBufferPeriodSec = highBufferWatchdogPeriod + nudgeCount * backoffSec;
|
||
logger.debug(`[stall] start playing stall watchdog low period=${lowBufferWatchdogPeriod} high period=${highBufferPeriodSec}`);
|
||
return startPlayingStallWatchdog(meQuery, logger, lowBufferWatchdogPeriod, highBufferPeriodSec, lowBufferThreshold);
|
||
}));
|
||
const gotResume$ = gotStall$.pipe(filterNullOrUndefined(), withLatestFrom(meQuery.combinedBuffer$), switchMap(([stallInfo, combinedBuffer]) => {
|
||
logger.debug(`[stall] got stall ${stringifyWithPrecision(stallInfo)} buffered:${stringifyWithPrecision(combinedBuffer)}`);
|
||
return combineQueries([meQuery.seeking$, meQuery.paused$]);
|
||
}), switchMap(([seeking, paused]) => {
|
||
if (seeking || paused) {
|
||
return EMPTY;
|
||
}
|
||
return meQuery.timeupdate$.pipe(pairwise(), filter(([a, b]) => isFiniteNumber(a) && isFiniteNumber(b) && b > a), take(1));
|
||
}), map(() => {
|
||
logger.info('[stall] resume from stall');
|
||
return null;
|
||
}));
|
||
return merge(gotStall$, gotResume$);
|
||
}
|
||
// start seek watchdog timer. Assumed that at start, seeking || !playing
|
||
// Fire if time from (seeking -> playing) or (seeked -> playing) >= seekWatchdogPeriod
|
||
function startSeekStallWatchdog(meQuery, tstalled, seekWatchdogPeriod, lowBufferThreshold) {
|
||
return timer(seekWatchdogPeriod * 1000).pipe(map(() => {
|
||
const currentTime = meQuery.currentTime;
|
||
const bufferInfo = meQuery.getCombinedBufferInfo(currentTime, 0);
|
||
return makeStallInfo(StallType.Seek, currentTime, tstalled, bufferInfo, lowBufferThreshold, meQuery.haveEnough);
|
||
}));
|
||
}
|
||
// start playing watchdog timer: when now - tlastCurrentTime > low or high buffer watchdog period
|
||
function startPlayingStallWatchdog(meQuery, logger, lowBufferWatchdogPeriod, highBufferWatchdogPeriod, lowBufferThreshold) {
|
||
// Current position hasn't changed for some period of time
|
||
return merge(of(meQuery.currentTime), meQuery.timeupdate$).pipe(switchMap((lastCurrentTime) => {
|
||
const tstalled = performance.now();
|
||
const bufferInfo = meQuery.getCombinedBufferInfo(lastCurrentTime, 0);
|
||
let type;
|
||
let watchdogPeriod; // How long from reference point to schedule the check
|
||
const bufferLenSec = bufferInfo.len;
|
||
// prettier-ignore
|
||
if ((bufferLenSec <= lowBufferThreshold) || !meQuery.haveEnough) {
|
||
watchdogPeriod = lowBufferWatchdogPeriod;
|
||
type = StallType.LowBuffer;
|
||
}
|
||
else {
|
||
watchdogPeriod = highBufferWatchdogPeriod;
|
||
type = StallType.HighBuffer;
|
||
}
|
||
const stallCheckMs = Math.max(MIN_STALL_CHECK_MS, watchdogPeriod * 1000);
|
||
return timer(stallCheckMs).pipe(map(() => {
|
||
// Check once to see if playback has moved ahead before making stallInfo Object
|
||
// On Roku 'timeupdate' event (aka meQuery.timeupdate$ triggers) are inconsistent.
|
||
if (lastCurrentTime < meQuery.currentTime) {
|
||
return null;
|
||
}
|
||
return makeStallInfo(type, lastCurrentTime, tstalled, bufferInfo, lowBufferThreshold, meQuery.haveEnough);
|
||
}));
|
||
}));
|
||
}
|
||
/**
|
||
* Check if we've changed positions since we armed the timers
|
||
*/
|
||
function makeStallInfo(type, currentTime, tstalled, bufferInfo, lowBufferThreshold, haveEnough) {
|
||
const now = performance.now();
|
||
const stallDurationMs = now - tstalled;
|
||
const bufferLenSec = bufferInfo.len;
|
||
// prettier-ignore
|
||
const isLowBufferStall = (bufferLenSec <= lowBufferThreshold) || !haveEnough;
|
||
return { type, isLowBufferStall, tstalled, stallDurationMs, currentTime };
|
||
}
|
||
|
||
class MediaSink extends Observable {
|
||
constructor(mediaElement, mediaElementStore, config, hlsGapless, logger, teardownWG$, rtcService) {
|
||
super((subscriber) => {
|
||
this.logger.info('subscribe MediaSink');
|
||
const config = this.config;
|
||
const sessionId = mediaElementStore.startMediaSession(mediaElement, config.maxBufferLength, config.almostDryBufferSec, config.defaultTargetDuration);
|
||
const mediaElementEvents$ = hookMediaElementsEvents(mediaElement, mediaElementStore, this._mediaQuery, this, this.hlsGapless, config, this.logger, this.rtcService);
|
||
const mediaSource$ = this.mediaSource$.pipe(switchMap((ms) => ms || EMPTY));
|
||
const seeks$ = this._mediaQuery.seekTo$.pipe(seekEpic(mediaElement, this._mediaQuery, this, this.config, this.logger));
|
||
const rateChange$ = this._mediaQuery.desiredRate$.pipe(rateChangeEpic(mediaElement, this._mediaQuery, this));
|
||
this.liveSeekableWindow = { start: NaN, end: NaN };
|
||
// setup media functions overrides
|
||
this.mediaFunctions = this.mediaFunctions || new MediaFunctions(this, mediaElement, config, this.logger);
|
||
this.mediaFunctions.install();
|
||
const stallHandling$ = merge(stallMonitor(this.logger, this._mediaQuery, this.config).pipe(tap((stallInfo) => {
|
||
mediaElementStore.setStallInfo(stallInfo);
|
||
})), this.mediaQuery.stallInfo$.pipe(stallEpic(this, mediaElementStore, this.config, this.logger)));
|
||
const bufferMonitor$ = bufferMonitorEpic(this.mediaQuery, mediaElementStore, config.maxBufferHole);
|
||
merge(mediaElementEvents$, mediaSource$, seeks$, rateChange$, stallHandling$, bufferMonitor$)
|
||
.pipe(switchMapTo(EMPTY), finalize$1(() => {
|
||
this.logger.info('finalize MediaSink');
|
||
mediaElementStore.remove(sessionId);
|
||
this.mediaFunctions.uninstall();
|
||
this.mediaFunctions = undefined;
|
||
}))
|
||
.subscribe(subscriber); // Propagates any errors up
|
||
// extra teardown ?
|
||
});
|
||
this.mediaElement = mediaElement;
|
||
this.mediaElementStore = mediaElementStore;
|
||
this.config = config;
|
||
this.hlsGapless = hlsGapless;
|
||
this.logger = logger;
|
||
this.teardownWG$ = teardownWG$;
|
||
this.rtcService = rtcService;
|
||
this.mediaSource$ = new BehaviorSubject(null);
|
||
// We use a Mutex here because only one setMediaKeys() can run at one time,
|
||
// and multiple setMediaKeys() operations can be queued for execution from
|
||
// different threads. Since it's the only type of operation needs
|
||
// synchronization in the MediaSink component, using a Mutex makes more sense
|
||
// than converting to command queue pattern.
|
||
this.mediaKeysMutex = new Mutex();
|
||
this._mediaQuery = new MediaElementQuery(mediaElement, mediaElementStore);
|
||
this.logger = logger.child({ name: 'mse' });
|
||
// Muze/Vuze: Need to create id3 text track before MEDIA_ATTACHED
|
||
this.createId3Track(mediaElement);
|
||
this.mediaFunctions = new MediaFunctions(this, mediaElement, config, this.logger);
|
||
}
|
||
get mediaSourceAdapter() {
|
||
return this.mediaSource$.value;
|
||
}
|
||
get sourceBuffers() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.mediaSourceAdapter) === null || _a === void 0 ? void 0 : _a.sourceBuffers) !== null && _b !== void 0 ? _b : [];
|
||
}
|
||
get needSourceBuffers() {
|
||
return this.sourceBuffers[0] ? false : true;
|
||
}
|
||
get mediaQuery() {
|
||
return this._mediaQuery;
|
||
}
|
||
sourceBuffersBufferedRangeByType(type) {
|
||
var _a, _b;
|
||
const sb = (_b = (_a = this.mediaSourceAdapter) === null || _a === void 0 ? void 0 : _a.sourceBuffers) === null || _b === void 0 ? void 0 : _b[type];
|
||
if (sb) {
|
||
return BufferHelper.timeRangesToBufferedRange(sb.sourceBuffer.buffered);
|
||
}
|
||
else {
|
||
return null;
|
||
}
|
||
}
|
||
createId3Track(mediaElement) {
|
||
this.logger.info('create id3 texttrack');
|
||
this.id3Track = mediaElement.addTextTrack('metadata', 'id3');
|
||
this.id3Track.mode = 'hidden';
|
||
}
|
||
/**
|
||
* Check if mediaElementStore buffer values are in-sync with real SourceBuffer
|
||
* especially when trying to seek.
|
||
*/
|
||
checkForInconsistentStoreBufferRangesAndUpdate() {
|
||
var _a, _b, _c, _d;
|
||
const mediaElementTimeRanges = BufferHelper.timeRangesToBufferedRange(this.mediaElement.buffered);
|
||
const realVariantBufferedRanges = this.sourceBuffersBufferedRangeByType(SourceBufferType.Variant);
|
||
const realAltAudioBufferedRanges = this.sourceBuffersBufferedRangeByType(SourceBufferType.AltAudio);
|
||
const storeVariantBufferedRanges = (_b = (_a = this.mediaQuery.sourceBufferEntityByType(SourceBufferType.Variant)) === null || _a === void 0 ? void 0 : _a.bufferedRanges) !== null && _b !== void 0 ? _b : null;
|
||
const storeAltAudioBufferedRanges = (_d = (_c = this.mediaQuery.sourceBufferEntityByType(SourceBufferType.AltAudio)) === null || _c === void 0 ? void 0 : _c.bufferedRanges) !== null && _d !== void 0 ? _d : null;
|
||
// If there is discrepency with any of sourceBufferRanges then update store values
|
||
if (this.shouldUpdateStoreValues(realVariantBufferedRanges, storeVariantBufferedRanges)) {
|
||
this.logger.warn(`[${SourceBufferNames[SourceBufferType.Variant]}] SourceBuffer's loaded bufferedRanges ${JSON.stringify(realVariantBufferedRanges)} & mediaElementStore's bufferedRanges ${JSON.stringify(storeVariantBufferedRanges)} are out of sync!`);
|
||
this.updateMediaElementStoreBufferedRanges(mediaElementTimeRanges, SourceBufferType.Variant);
|
||
}
|
||
if (this.shouldUpdateStoreValues(realAltAudioBufferedRanges, storeAltAudioBufferedRanges)) {
|
||
this.logger.warn(`[${SourceBufferNames[SourceBufferType.AltAudio]}] SourceBuffer's loaded bufferedRanges ${JSON.stringify(realAltAudioBufferedRanges)} & mediaElementStore's bufferedRanges ${JSON.stringify(storeAltAudioBufferedRanges)} are out of sync!`);
|
||
this.updateMediaElementStoreBufferedRanges(mediaElementTimeRanges, SourceBufferType.AltAudio);
|
||
}
|
||
}
|
||
shouldUpdateStoreValues(sbLoadedTimeRanges, storeBufferedRanges) {
|
||
if (sbLoadedTimeRanges == null && storeBufferedRanges == null) {
|
||
return false;
|
||
}
|
||
// Unequal lengths then we need updating
|
||
if ((sbLoadedTimeRanges === null || sbLoadedTimeRanges === void 0 ? void 0 : sbLoadedTimeRanges.length) != (storeBufferedRanges === null || storeBufferedRanges === void 0 ? void 0 : storeBufferedRanges.length)) {
|
||
return true;
|
||
}
|
||
// If there is discrepency with any of the loaded Ranges then update store values
|
||
return !!sbLoadedTimeRanges.find((sbCurrentLoadedTimeRange) => {
|
||
const binarySearchcompareFunction = (storeCurrentInterval) => {
|
||
if (sbCurrentLoadedTimeRange.start >= storeCurrentInterval.start && sbCurrentLoadedTimeRange.end <= storeCurrentInterval.end) {
|
||
return 0;
|
||
}
|
||
else if (sbCurrentLoadedTimeRange.end < storeCurrentInterval.start) {
|
||
return -1;
|
||
}
|
||
else {
|
||
return 1;
|
||
}
|
||
};
|
||
// Binary search as SourceBuffer.buffered is normalized TimeRanges object.
|
||
const matchingStoreBufferedRange = BinarySearch.search(storeBufferedRanges, binarySearchcompareFunction);
|
||
// If no match we need updating
|
||
if (matchingStoreBufferedRange == null) {
|
||
return true;
|
||
}
|
||
if (matchingStoreBufferedRange.start != sbCurrentLoadedTimeRange.start || matchingStoreBufferedRange.end != sbCurrentLoadedTimeRange.end) {
|
||
return true;
|
||
}
|
||
});
|
||
}
|
||
/**
|
||
* Update mediaElementStore with latest from SourceBuffers.
|
||
*/
|
||
updateMediaElementStoreBufferedRanges(mediaElementBuffered, type) {
|
||
const sb = this.sourceBuffersBufferedRangeByType(type);
|
||
if (sb && !this.mediaQuery.sbUpdating(type)) {
|
||
this.logger.info(`[${SourceBufferNames[type]}] Updating buffer status`);
|
||
this.mediaElementStore.setBufferedRangesUpdated(type, sb, mediaElementBuffered, false, this.config);
|
||
}
|
||
}
|
||
destroyMediaSource() {
|
||
this.mediaSource$.next(null);
|
||
}
|
||
// Can stub over this for testing if needed
|
||
makeMediaSource() {
|
||
return new MediaSource();
|
||
}
|
||
openMediaSource(mediaSource) {
|
||
applyTransaction(() => {
|
||
if (mediaSource) {
|
||
const mediaAdapter = new MediaSourceAdapter(this.mediaElement, this.mediaElementStore, this.mediaQuery, mediaSource, this.logger);
|
||
this.mediaSource$.next(mediaAdapter);
|
||
}
|
||
else {
|
||
this.mediaSource$.next(null);
|
||
}
|
||
});
|
||
}
|
||
createSourceBuffers(compatInfoTuple) {
|
||
this.logger.info(`createSourceBuffers ${JSON.stringify(compatInfoTuple.map((c) => c === null || c === void 0 ? void 0 : c.mimeType))}`);
|
||
const msAdapter = this.mediaSource$.value;
|
||
if (!msAdapter) {
|
||
throw new Error('createSourceBuffers empty mediaSource');
|
||
}
|
||
msAdapter.createSourceBuffers(compatInfoTuple, this.config);
|
||
}
|
||
_waitForMediaSourceOpen(appendData) {
|
||
const msObjectUrl = this.mediaQuery.mediaSourceEntity.objectUrl; // msAdapter at the start of wait
|
||
return combineQueries([this.mediaQuery.msReadyState$, this.mediaQuery.msObjectUrl$]).pipe(switchMap(([state, msUrl]) => {
|
||
if (msUrl !== msObjectUrl) {
|
||
this.logger.info('media source changed while waiting');
|
||
return of(null);
|
||
}
|
||
if (state === 'open' || state === 'ended') {
|
||
// Can append again even if ended
|
||
return of(appendData);
|
||
}
|
||
return EMPTY;
|
||
}));
|
||
}
|
||
get appendOrder() {
|
||
return this.mediaQuery.isIframeRate ? [SourceBufferType.Variant, SourceBufferType.AltAudio] : [SourceBufferType.AltAudio, SourceBufferType.Variant];
|
||
}
|
||
clearFlush(appendData) {
|
||
appendData.forEach((sbData) => {
|
||
if (sbData) {
|
||
// hls will not reset the MediaOptionSwitchContext if flushBeforeAppend is null/undefined.
|
||
sbData.dataSeg.flushBeforeAppend = { start: 0, end: 0 };
|
||
}
|
||
});
|
||
}
|
||
getSwitchPosition(appendData) {
|
||
return appendData.reduce((prevSwitchPos, next) => {
|
||
const switchPos = next ? next.dataSeg.switchPosition : undefined;
|
||
if (isFiniteNumber(switchPos)) {
|
||
return isFiniteNumber(prevSwitchPos) ? Math.min(prevSwitchPos, switchPos) : switchPos;
|
||
}
|
||
else {
|
||
return prevSwitchPos;
|
||
}
|
||
}, undefined);
|
||
}
|
||
checkForReplay() {
|
||
const media = this.mediaElement;
|
||
// rdar://80790960 ([HLS JS 2.1 beta] Marcom - Retry playback fails). Pulled from 2.0 stream-controller.ts.
|
||
if (media.paused && !media.seeking && media.duration && media.currentTime && media.currentTime >= media.duration - this.config.maxTotalDurationTolerance) {
|
||
this.seekTo = 0; // auto-restart if scrubber is at end of video. media.ended may be reset upon seek or append.
|
||
}
|
||
}
|
||
/**
|
||
* Reset media source if needed and wait for 'open'
|
||
* @returns an observable that emits AppendDataTuple if we should append, null if we something did a reset
|
||
*/
|
||
resetMediaSourceIfNeeded(appendData) {
|
||
const { mediaQuery } = this;
|
||
const { sourceBufferEntities } = mediaQuery;
|
||
const { expectedSbCount } = mediaQuery.getActive();
|
||
if (!sourceBufferEntities || this.needSourceBuffers) {
|
||
this.logger.info('need source buffers #disco');
|
||
return this._waitForMediaSourceOpen(appendData);
|
||
}
|
||
const compatInfo = isCompatibleWithSourceBuffers(appendData, sourceBufferEntities, expectedSbCount, this.config.maxBufferHole, this.logger);
|
||
if (compatInfo.compatible) {
|
||
this.logger.info('got compatible source buffers #disco');
|
||
this.logger.qe({ critical: true, name: 'disco', data: { type: 'compatible' } });
|
||
return this._waitForMediaSourceOpen(appendData);
|
||
}
|
||
let boundary = compatInfo.boundary;
|
||
const boundaryAllowance = compatInfo.allowance;
|
||
const requestedSwitchPos = this.getSwitchPosition(appendData);
|
||
if (isFiniteNumber(requestedSwitchPos)) {
|
||
this.logger.info(`override boundary with switch position=${boundary.toFixed(3)}->${requestedSwitchPos}`);
|
||
boundary = requestedSwitchPos;
|
||
}
|
||
if (!isFiniteNumber(boundary)) {
|
||
this.logger.warn('not enough info #disco');
|
||
return of(null);
|
||
}
|
||
this.logger.info(`start wait pos=${mediaQuery.currentTime.toFixed(3)} boundary=${boundary.toFixed(3)} allowance=${boundaryAllowance.toFixed(3)} #disco`);
|
||
this.logger.qe({ critical: true, name: 'disco', data: { type: 'incompatible' } });
|
||
const hitDiscoBoundary$ = race(
|
||
// Play or seek across boundary (0 tolerance)
|
||
waitFor(merge(of(mediaQuery.currentTime), mediaQuery.timeupdate$), (pos) => pos >= boundary),
|
||
// Stalled playback and we're close enough
|
||
waitFor(mediaQuery.stallInfo$.pipe(map((stallInfo) => { var _a; return (_a = stallInfo === null || stallInfo === void 0 ? void 0 : stallInfo.currentTime) !== null && _a !== void 0 ? _a : NaN; })), (pos) => pos >= boundary - boundaryAllowance - this.config.discontinuitySeekTolerance));
|
||
this.mediaElementStore.waitingForDisco = true;
|
||
return hitDiscoBoundary$.pipe(mapTo(boundary), switchMap((boundary) => {
|
||
const startResetTime = performance.now();
|
||
const currentTime = mediaQuery.currentTime;
|
||
this.logger.info(`end wait pos=${currentTime.toFixed(3)} boundary=${boundary.toFixed(3)} #disco`);
|
||
this.logger.qe({ critical: true, name: 'disco', data: { type: 'resetStart' } });
|
||
const duration = this.msDuration;
|
||
this.resetMediaSource(Math.max(currentTime, boundary), compatInfo.discoSeqNum);
|
||
return this._waitForMediaSourceOpen(appendData).pipe(tap(() => {
|
||
const resetDuration = performance.now() - startResetTime;
|
||
this.logger.qe({ critical: true, name: 'disco', data: { type: 'resetComplete', resetDuration } });
|
||
this.msDuration = duration;
|
||
}));
|
||
}), finalize$1(() => {
|
||
this.mediaElementStore.waitingForDisco = false;
|
||
}));
|
||
}
|
||
/**
|
||
* @brief External hard media reset
|
||
* @param position The position to seek to on reset. NaN means use current seekTo or currentTime. seekTo has precedence > currentTime
|
||
*/
|
||
resetMediaSource(position = NaN, discoSeqNum) {
|
||
var _a, _b, _c;
|
||
this.logger.info(`resetMediaSource ${position} ${discoSeqNum}`);
|
||
if (!isFiniteNumber(position)) {
|
||
position = (_b = (_a = this.mediaQuery.seekTo) === null || _a === void 0 ? void 0 : _a.pos) !== null && _b !== void 0 ? _b : this.mediaQuery.currentTime;
|
||
}
|
||
if (!isFiniteNumber(discoSeqNum)) {
|
||
discoSeqNum = (_c = this.mediaQuery.seekTo) === null || _c === void 0 ? void 0 : _c.discoSeqNum;
|
||
}
|
||
if (this.sourceBuffers.length > 0) {
|
||
this.openMediaSource(this.makeMediaSource());
|
||
this.logger.info(`resetMediaSource seek=${position} cc=${discoSeqNum}`);
|
||
this.setSeekToWithDiscontinuity(position, discoSeqNum); // always update after reset, use given cc
|
||
}
|
||
}
|
||
setExpectedSbCount(sbCount) {
|
||
this.mediaElementStore.expectedSbCount = sbCount;
|
||
}
|
||
appendInitSegments(appendData, appendErrorPolicy) {
|
||
const { mediaQuery, mediaElementStore, sourceBuffers, logger } = this;
|
||
const { sourceBufferEntities } = mediaQuery;
|
||
if (!sourceBuffers) {
|
||
throw new Error('appendInitSegments: null sourceBuffers');
|
||
}
|
||
if (!sourceBufferEntities) {
|
||
throw new Error('appendInitSegments: null sourceBufferEntities');
|
||
}
|
||
const initAppendOps$ = this.appendOrder
|
||
.map((type) => {
|
||
const sbDataTuple = appendData[type];
|
||
if (!sbDataTuple)
|
||
return;
|
||
const sb = sourceBuffers[type];
|
||
const sbAppendData = appendData[type];
|
||
const currentSbEntity = sourceBufferEntities[type];
|
||
const { initSeg } = sbAppendData;
|
||
if (!currentSbEntity) {
|
||
throw new Error(`appendInitSegments: sb[${SourceBufferNames[type]}] null currentSbEntity`);
|
||
}
|
||
if (!sb) {
|
||
throw new Error(`appendInitSegments: sb[${SourceBufferNames[type]}] null source buffer`);
|
||
}
|
||
const curInitInfo = currentSbEntity.initSegmentInfo;
|
||
const newInitInfo = cacheEntityToInfoEntity(initSeg);
|
||
if (initSegmentEquals(newInitInfo, curInitInfo)) {
|
||
logger.debug(`[${MediaOptionNames[type]}] skip init segment append ${JSON.stringify(newInitInfo)}`);
|
||
return of(null); // noop
|
||
}
|
||
const mediaOptionType = sourceBufferTypeToMediaOptionType(type);
|
||
return sb.appendBuffer(initSeg.data, initSeg).pipe(tap((am) => {
|
||
logger.info(`[${MediaOptionNames[type]}] append init segment ${JSON.stringify(newInitInfo)} ${am === null || am === void 0 ? void 0 : am.startAppend}/${am === null || am === void 0 ? void 0 : am.endAppend}/${am === null || am === void 0 ? void 0 : am.bytesAppend}`);
|
||
mediaElementStore.setInitSegmentEntity(type, newInitInfo);
|
||
}), appendErrorPolicy(sb, mediaOptionType, initSeg.mediaOptionId, this.config, this.mediaQuery));
|
||
})
|
||
.filter((initAppendOp) => Boolean(initAppendOp));
|
||
if (initAppendOps$.length === 0) {
|
||
return of(null); // noop
|
||
}
|
||
return forkJoin(initAppendOps$);
|
||
}
|
||
appendDataSegments(appendData, appendErrorPolicy) {
|
||
const dataAppendOps$ = this.appendOrder
|
||
.map((type) => {
|
||
const sbDataTuple = appendData[type];
|
||
const { mediaQuery, sourceBuffers, logger } = this;
|
||
const { sourceBufferEntities } = mediaQuery;
|
||
if (!sourceBuffers) {
|
||
throw new Error('appendDataSegments: null sourceBuffers');
|
||
}
|
||
if (!sourceBufferEntities) {
|
||
throw new Error('appendDataSegments: null sourceBufferEntities');
|
||
}
|
||
if (!sbDataTuple)
|
||
return null;
|
||
const sb = sourceBuffers[type];
|
||
const sbAppendData = appendData[type];
|
||
const currentSbEntity = sourceBufferEntities[type];
|
||
if (!currentSbEntity) {
|
||
throw new Error('appendDataSegments: null currentSbEntity');
|
||
}
|
||
const currentInitSegmentInfo = currentSbEntity.initSegmentInfo;
|
||
const { dataSeg } = sbAppendData;
|
||
if (!currentInitSegmentInfo) {
|
||
throw new Error(`appendDataSegments: sb[${SourceBufferNames[type]}] null currentInitSegmentInfo`);
|
||
}
|
||
if (!currentSbEntity) {
|
||
throw new Error(`appendDataSegments: sb[${SourceBufferNames[type]}] null currentSbEntity`);
|
||
}
|
||
if (!sb) {
|
||
throw new Error(`appendDataSegments: sb[${SourceBufferNames[type]}] null source buffer`);
|
||
}
|
||
const timestampOffset = sb.timestampOffset;
|
||
const startPTS = convertTimestampToSeconds(dataSeg.startPts) + timestampOffset;
|
||
const endPTS = convertTimestampToSeconds(dataSeg.endPts) + timestampOffset;
|
||
const firstKeyframePts = dataSeg.firstKeyframePts ? convertTimestampToSeconds(dataSeg.firstKeyframePts) + timestampOffset : undefined;
|
||
const seginfo = {
|
||
startPTS,
|
||
endPTS,
|
||
firstKeyframePts,
|
||
bytes: dataSeg.data2 ? dataSeg.data1.byteLength + dataSeg.data2.byteLength : dataSeg.data1.byteLength,
|
||
frag: {
|
||
itemId: dataSeg.itemId,
|
||
mediaOptionId: dataSeg.mediaOptionId,
|
||
mediaSeqNum: dataSeg.mediaSeqNum,
|
||
discoSeqNum: dataSeg.discoSeqNum,
|
||
keyTagInfo: dataSeg.keyTagInfo,
|
||
isLastFragment: dataSeg.isLastFragment,
|
||
iframe: dataSeg.iframe,
|
||
framesWithoutIDR: dataSeg.framesWithoutIDR,
|
||
dropped: dataSeg.dropped,
|
||
},
|
||
};
|
||
const mediaOptionType = sourceBufferTypeToMediaOptionType(type);
|
||
let optionalFlush = VOID;
|
||
const flushRange = sbDataTuple.dataSeg.flushBeforeAppend;
|
||
if (flushRange && flushRange.start !== flushRange.end) {
|
||
logger.info(`sb[${SourceBufferNames[type]}] flush [${flushRange.start},${flushRange.end}] before append`);
|
||
optionalFlush = this.flushData(type, flushRange.start, flushRange.end);
|
||
}
|
||
return optionalFlush.pipe(switchMap(() => {
|
||
const source = of(dataSeg.data1, dataSeg.data2).pipe(filterNullOrUndefined());
|
||
return source.pipe(concatMap((data) => {
|
||
logger.info(`[${MediaOptionNames[type]}] appending timestampOffset:${currentSbEntity.timestampOffset} segment:${stringifyWithPrecision({
|
||
startPTS,
|
||
endPTS,
|
||
frag: fragPrint(dataSeg),
|
||
key: redactUrl(dataSeg.keyTagInfo.uri),
|
||
})}`);
|
||
return sb.appendBuffer(data, seginfo).pipe(appendErrorPolicy(sb, mediaOptionType, dataSeg.mediaOptionId, this.config, this.mediaQuery));
|
||
}));
|
||
}), tap(() => {
|
||
const bufferedRanges = mediaQuery.getBufferedRangeByType(type);
|
||
logger.info(`[${MediaOptionNames[type]}] appended pos:${mediaQuery.currentTime.toFixed(3)} buffered:${stringifyWithPrecision(bufferedRanges)}`);
|
||
}));
|
||
})
|
||
.filter((dataAppendOp) => Boolean(dataAppendOp));
|
||
if (dataAppendOps$.length === 0) {
|
||
return of(null); // noop
|
||
}
|
||
return forkJoin(dataAppendOps$);
|
||
}
|
||
setStoreSbTimeoffsets(appendDataTuple) {
|
||
const { mediaElementStore, sourceBuffers } = this;
|
||
sourceBuffers.forEach((sb, type) => {
|
||
if (!sb || !appendDataTuple[type]) {
|
||
return;
|
||
}
|
||
const { offsetTimestamp, dataSeg } = appendDataTuple[type];
|
||
const startPts = convertTimestampToSeconds(dataSeg.startPts);
|
||
let timestampOffset = convertTimestampToSeconds(offsetTimestamp) * -1;
|
||
if (sb.updateMp3Timestamps) {
|
||
// Adjusting `SourceBuffer.timestampOffset` (desired point in the timeline where the next frames should be appended)
|
||
// in Chrome browser when we detect MPEG audio container and time delta between level PTS and `SourceBuffer.timestampOffset`
|
||
// is greater than 100ms (this is enough to handle seek for VOD or level change for LIVE videos). At the time of change we issue
|
||
// More info here: https://github.com/video-dev/hls.js/issues/332#issuecomment-257986486
|
||
const delta = Math.abs(sb.timestampOffset - startPts);
|
||
// adjust timestamp offset if time delta is greater than 100ms
|
||
if (delta > 0.1) {
|
||
timestampOffset = startPts + timestampOffset;
|
||
}
|
||
}
|
||
if (sb.timestampOffset !== timestampOffset) {
|
||
this.logger.info(`${MediaOptionNames[type]} timestampOffset=${timestampOffset}`);
|
||
sb.timestampOffset = timestampOffset;
|
||
mediaElementStore.setTimestampOffset(type, sb.timestampOffset);
|
||
}
|
||
});
|
||
}
|
||
adjustJaggedStart(appendData) {
|
||
var _a;
|
||
const { mediaQuery, logger } = this;
|
||
const { sourceBufferEntities, currentTime, seekTo } = mediaQuery;
|
||
const appendEndTime = appendData.reduce((end, append) => {
|
||
return (append === null || append === void 0 ? void 0 : append.dataSeg.endPts) ? Math.min(diffSeconds(append.dataSeg.endPts, append.offsetTimestamp), end) : end;
|
||
}, Number.POSITIVE_INFINITY);
|
||
if (!sourceBufferEntities) {
|
||
throw new Error('appendSourceBufferData null currentSbEntity');
|
||
}
|
||
const position = (seekTo === null || seekTo === void 0 ? void 0 : seekTo.pos) || currentTime;
|
||
let seekToPos = NaN;
|
||
sourceBufferEntities.forEach((sbEntity, type) => {
|
||
if (!sbEntity)
|
||
return;
|
||
const bufferInfo = BufferHelper.getBufferedInfo(sbEntity.bufferedRanges, position, 0);
|
||
if (bufferInfo.len === 0) {
|
||
const { nextStart } = bufferInfo;
|
||
const tolerance = isFiniteNumber(this.config.jaggedSeekTolerance) ? this.config.jaggedSeekTolerance : 0;
|
||
logger.warn(`sb[${SourceBufferNames[type]}] jagged start: ${nextStart} appendEndTime=${appendEndTime} current=${seekToPos} tolerance=${tolerance}`);
|
||
if (isFiniteNumber(nextStart) && (!isFiniteNumber(seekToPos) || nextStart - seekToPos > tolerance)) {
|
||
seekToPos = nextStart;
|
||
}
|
||
}
|
||
});
|
||
if (isFiniteNumber(seekToPos) && appendEndTime > seekToPos) {
|
||
// when appendEndTime < seekToPos, no need to initiate jagged seek.
|
||
// wait for subsequent append to fill the buffer.
|
||
logger.warn(`[seek] jagged start, adjusting currentTime:${currentTime.toFixed(3)} seekTo=${(_a = seekTo === null || seekTo === void 0 ? void 0 : seekTo.pos) === null || _a === void 0 ? void 0 : _a.toFixed(3)}->${seekToPos} appendEndTime=${appendEndTime}`);
|
||
this.seekTo = seekToPos;
|
||
}
|
||
}
|
||
addCues(texttrackIdx, cues) {
|
||
const textTrack = this.mediaElement.textTracks[texttrackIdx];
|
||
if (textTrack) {
|
||
cues.forEach((cue) => {
|
||
textTrack.addCue(cue);
|
||
});
|
||
}
|
||
}
|
||
_flushInternal(sb, start, end) {
|
||
return defer(() => {
|
||
this.logger.info(`[${SourceBufferNames[sb.type]}] remove(${start},${end}) start`);
|
||
return sb.remove(start, end);
|
||
}).pipe(tap(() => {
|
||
this.logger.info(`[${SourceBufferNames[sb.type]}] remove(${start},${end}) end`);
|
||
}));
|
||
}
|
||
// Flush all source buffers
|
||
flushAll(start, end, force = false) {
|
||
if (this.sourceBuffers.length === 0) {
|
||
return VOID;
|
||
}
|
||
return forkJoin(this.sourceBuffers.map((sb, type) => (sb ? this.flushData(type, start, end, force) : VOID))).pipe(mapTo(undefined));
|
||
}
|
||
flushData(sbType, start, end, force = false) {
|
||
var _a;
|
||
const { mediaQuery, logger } = this;
|
||
this.logger.info(`flushData ${start.toFixed(3)} ${end.toFixed(3)} updating=${(_a = mediaQuery.sourceBufferEntities) === null || _a === void 0 ? void 0 : _a.some((sb) => sb === null || sb === void 0 ? void 0 : sb.updating)}`);
|
||
return waitFor(mediaQuery.updating$, (updating) => updating === false).pipe(switchMap(() => {
|
||
const { sourceBufferEntities } = mediaQuery;
|
||
const sbEntity = sourceBufferEntities[sbType];
|
||
if (sbEntity === null || sbEntity === void 0 ? void 0 : sbEntity.updating) {
|
||
this.logger.warn(`trying to flush while updating ${sbType}`);
|
||
}
|
||
const sb = this.sourceBuffers[sbType];
|
||
if (!sb) {
|
||
return VOID;
|
||
}
|
||
let bufStart;
|
||
let bufEnd;
|
||
let flushes = VOID;
|
||
const flushSingleRange = navigator.userAgent.toLowerCase().indexOf('firefox') === -1;
|
||
this.flushing = true;
|
||
if (flushSingleRange) {
|
||
return this._flushInternal(sb, start, end);
|
||
}
|
||
// workaround firefox not able to properly flush multiple buffered range.
|
||
for (let i = 0; i < sb.buffered.length; i++) {
|
||
let flushStart;
|
||
let flushEnd;
|
||
bufStart = sb.buffered.start(i);
|
||
bufEnd = sb.buffered.end(i);
|
||
if (end === Infinity) {
|
||
flushStart = start;
|
||
flushEnd = end;
|
||
}
|
||
else {
|
||
flushStart = Math.max(bufStart, start);
|
||
flushEnd = Math.min(bufEnd, end);
|
||
}
|
||
/*
|
||
sometimes sourcebuffer.remove() does not flush
|
||
the exact expected time range.
|
||
to avoid rounding issues/infinite loop,
|
||
only flush buffer range of length greater than 500ms.
|
||
*/
|
||
if (Math.min(flushEnd, bufEnd) > flushStart && (force || Math.min(flushEnd, bufEnd) - flushStart > 0.5)) {
|
||
flushes = flushes.pipe(switchMapTo(this._flushInternal(sb, flushStart, flushEnd)));
|
||
}
|
||
else {
|
||
logger.warn(`ignoring sb[${SourceBufferNames[sbType]}] flush ${flushStart},${flushEnd}`);
|
||
}
|
||
}
|
||
return flushes;
|
||
}), finalize$1(() => {
|
||
// clear flushing
|
||
this.flushing = false;
|
||
}));
|
||
}
|
||
static convertInitSegToCompatInfo(initSeg) {
|
||
return {
|
||
mimeType: initSeg.mimeType,
|
||
audioCodec: initSeg.initParsedData.audioCodec,
|
||
videoCodec: initSeg.initParsedData.videoCodec,
|
||
startPTSSec: undefined,
|
||
endPTSSec: undefined,
|
||
discoSeqNum: initSeg.discoSeqNum,
|
||
mediaOptionId: initSeg.mediaOptionId,
|
||
};
|
||
}
|
||
static combineAppendDataInfoWithCompatInfo(appendData, oldCompatInfoTuple, highestVideoCodec, logger = null) {
|
||
const newCompatInfo = [...oldCompatInfoTuple];
|
||
appendData.forEach((d, type) => ((d === null || d === void 0 ? void 0 : d.initSeg) ? (newCompatInfo[type] = MediaSink.convertInitSegToCompatInfo(d.initSeg)) : null));
|
||
const currentVideoCodecString = newCompatInfo[SourceBufferType.Variant].videoCodec;
|
||
const currentVideoCodecFamily = getVideoCodecFamily(currentVideoCodecString);
|
||
// Replace to highest video codec string.
|
||
if (highestVideoCodec && highestVideoCodec.has(currentVideoCodecFamily)) {
|
||
// if configured to use highestCodec
|
||
const highestVideoCodecString = highestVideoCodec.get(currentVideoCodecFamily);
|
||
logger === null || logger === void 0 ? void 0 : logger.info(`override with highest video codec ${highestVideoCodecString}`);
|
||
newCompatInfo[SourceBufferType.Variant].mimeType = newCompatInfo[SourceBufferType.Variant].mimeType.replace(currentVideoCodecString, highestVideoCodecString);
|
||
newCompatInfo[SourceBufferType.Variant].videoCodec = highestVideoCodecString;
|
||
logger === null || logger === void 0 ? void 0 : logger.info(`compatibility info overrridden from ${currentVideoCodecString} to ${newCompatInfo[SourceBufferType.Variant].videoCodec}/${newCompatInfo[SourceBufferType.Variant].mimeType}`);
|
||
}
|
||
return newCompatInfo;
|
||
}
|
||
convertSourceBufferEntitiesToCompatInfo(mediaQuery) {
|
||
const sbEntities = mediaQuery.sourceBufferEntities;
|
||
const compatInfos = [null, null];
|
||
sbEntities.forEach((sbEntity, type) => {
|
||
var _a;
|
||
if (!sbEntity)
|
||
return;
|
||
compatInfos[type] = {
|
||
mimeType: sbEntity.mimeType,
|
||
audioCodec: sbEntity.audioCodec,
|
||
videoCodec: sbEntity.videoCodec,
|
||
startPTSSec: undefined,
|
||
endPTSSec: undefined,
|
||
discoSeqNum: undefined,
|
||
// right after resetMediaSource, sbEntity may not have initSegmentInfo yet.
|
||
// mediaOptionId to be filled in by combineAppendDataInfoWithCompatInfo before createSourceBuffers.
|
||
mediaOptionId: (_a = sbEntity.initSegmentInfo) === null || _a === void 0 ? void 0 : _a.mediaOptionId,
|
||
};
|
||
});
|
||
return compatInfos;
|
||
}
|
||
appendData(appendData, appendErrorPolicy, highestVideoCodec) {
|
||
const { mediaQuery, logger } = this;
|
||
const oldCompatInfoTuple = this.convertSourceBufferEntitiesToCompatInfo(mediaQuery);
|
||
if (appendData.every((data) => data == null)) {
|
||
return of([]); // noop
|
||
}
|
||
return this.resetMediaSourceIfNeeded(appendData).pipe(switchMap((appendData) => {
|
||
if (!appendData) {
|
||
return of(null); // Got an unexpected reset, stop what we're doing
|
||
}
|
||
// Don't want updating to trigger switchMap above which will abort the append below.
|
||
return mediaQuery.updating$.pipe(filter((updating) => updating === false), take(1), mapTo(appendData));
|
||
}), switchMap((appendData) => {
|
||
if (!appendData) {
|
||
return of([]);
|
||
}
|
||
let bufferCreationStart = NaN;
|
||
let bufferCreationEnd = NaN;
|
||
if (this.needSourceBuffers) {
|
||
bufferCreationStart = performance.now();
|
||
const newCompatInfoTuple = MediaSink.combineAppendDataInfoWithCompatInfo(appendData, oldCompatInfoTuple, highestVideoCodec, logger);
|
||
this.createSourceBuffers(newCompatInfoTuple);
|
||
bufferCreationEnd = performance.now();
|
||
logger.info(`[Buffers] source buffers created start/end: ${bufferCreationStart}/${bufferCreationEnd}`);
|
||
this.clearFlush(appendData);
|
||
}
|
||
appendData.forEach((data) => {
|
||
const mediaData = data === null || data === void 0 ? void 0 : data.dataSeg;
|
||
if ((mediaData === null || mediaData === void 0 ? void 0 : mediaData.cues) && isFiniteNumber(mediaData === null || mediaData === void 0 ? void 0 : mediaData.texttrackIdx)) {
|
||
this.addCues(mediaData.texttrackIdx, mediaData.cues);
|
||
}
|
||
});
|
||
this.setStoreSbTimeoffsets(appendData);
|
||
return concat(defer(() => this.appendInitSegments(appendData, appendErrorPolicy)), defer(() => this.appendDataSegments(appendData, appendErrorPolicy))).pipe(reduce((acc, value) => {
|
||
acc.push(value);
|
||
return acc;
|
||
}, new Array()), map(([initSegmentMetrics, dataSegmentMetrics]) => {
|
||
const metrics = [null, null];
|
||
[SourceBufferType.Variant, SourceBufferType.AltAudio].forEach((type) => {
|
||
if ((initSegmentMetrics === null || initSegmentMetrics === void 0 ? void 0 : initSegmentMetrics[type]) == null)
|
||
return;
|
||
const metric = {
|
||
fragmentType: sourceBufferTypeToMediaOptionType(type),
|
||
bufferCreationStart,
|
||
bufferCreationEnd,
|
||
startInitAppend: initSegmentMetrics[type].startAppend,
|
||
endInitAppend: initSegmentMetrics[type].endAppend,
|
||
initBytesAppend: initSegmentMetrics[type].bytesAppend,
|
||
startDataAppend: dataSegmentMetrics[type].startAppend,
|
||
endDataAppend: dataSegmentMetrics[type].endAppend,
|
||
dataBytesAppend: dataSegmentMetrics[type].bytesAppend,
|
||
};
|
||
metrics[type] = metric;
|
||
});
|
||
return metrics;
|
||
}), tap((_) => {
|
||
this.adjustJaggedStart(appendData);
|
||
}));
|
||
}), take(1));
|
||
}
|
||
endStream() {
|
||
// Will throw error if msReadyState !== open or any source buffer is updating
|
||
try {
|
||
this.mediaSourceAdapter.endOfStream();
|
||
}
|
||
catch (err) {
|
||
this.logger.warn(`endOfStream failed: ${err.message}`);
|
||
}
|
||
}
|
||
/**
|
||
* Set CDM on media element
|
||
* @param mediaKeys The CDM to attach, or null
|
||
*/
|
||
setMediaKeys(mediaKeys) {
|
||
const { logger } = this;
|
||
return this.teardownWG$.wrap(this.mediaKeysMutex
|
||
.lock(() => from(this.mediaElement.setMediaKeys(mediaKeys)))
|
||
.pipe(tap(() => {
|
||
logger.info(`[Keys] setMediaKeys(${mediaKeys}) success`);
|
||
}), retryWhen((errors) => errors.pipe(mergeMap((err, i) => {
|
||
logger.info(`[Keys] setMediaKeys(${mediaKeys} fail ${err.message})`);
|
||
if (i < 3) {
|
||
logger.info(`[Keys] Retry setMediaKeys delay=${i * 100}`);
|
||
return timer(i * 100);
|
||
}
|
||
throw err;
|
||
})))));
|
||
}
|
||
clearMediaKeys() {
|
||
return defer(() => {
|
||
if (!this.mediaElement) {
|
||
return VOID;
|
||
}
|
||
const isChrome = navigator.userAgent.toLowerCase().indexOf('chrome') > -1;
|
||
const src = this.mediaElement.src;
|
||
if (isChrome) {
|
||
//https://github.com/Dash-Industry-Forum/dash.js/issues/623
|
||
this.mediaElement.src = '';
|
||
}
|
||
return this.setMediaKeys(null).pipe(tap(() => (this.mediaElement.src = src)));
|
||
});
|
||
}
|
||
set postFlushSeek(time) {
|
||
this.mediaElementStore.postFlushSeek = time;
|
||
}
|
||
schedulePostFlushSeek(newSeekTo) {
|
||
applyTransaction(() => {
|
||
if (this.mediaQuery.seekTo) {
|
||
// <rdar://89566883> forget unfinished internal seek
|
||
this.seekTo = null; // this will also clear mediaSink.postFlushSeek
|
||
}
|
||
this.postFlushSeek = newSeekTo;
|
||
});
|
||
}
|
||
set seekTo(seekTo) {
|
||
this.mediaElementStore.setSeekToPos(seekTo, false);
|
||
}
|
||
setSeekToWithDiscontinuity(seekTo, discoSeqNum) {
|
||
this.mediaElementStore.setSeekToPos(seekTo, false, discoSeqNum);
|
||
}
|
||
nudgeSeek(seekTo, nudgeCount) {
|
||
applyTransaction(() => {
|
||
this.mediaElementStore.setSeekToPos(seekTo, false);
|
||
this.mediaElementStore.setNudgeInfo({ nudgeTarget: seekTo, nudgeCount });
|
||
});
|
||
}
|
||
set desiredRate(desiredRate) {
|
||
this.mediaElementStore.desiredRate = desiredRate;
|
||
// play / pause done in rateChange epic
|
||
}
|
||
toggleTrickPlaybackMode(enabled) {
|
||
// Override playbackRate value
|
||
if (this.config.overridePlaybackRate) {
|
||
const mediaRate = enabled ? 2 : 1;
|
||
try {
|
||
this.mediaElement.playbackRate = mediaRate;
|
||
this.logger.info({ name: 'iframes' }, `setting playbackRate to ${mediaRate}`);
|
||
}
|
||
catch (err) {
|
||
this.logger.error({ name: 'iframes' }, `Exception when setting playbackRate=${mediaRate}: ${err.message}`);
|
||
}
|
||
}
|
||
// Toggle mute value
|
||
const prev = this.muteValueOnTrickPlaybackToggle;
|
||
if (enabled && prev === undefined) {
|
||
this.muteValueOnTrickPlaybackToggle = this.mediaElement.muted;
|
||
this.mediaElement.muted = enabled;
|
||
}
|
||
else if (!enabled && prev !== undefined) {
|
||
this.mediaElement.muted = prev;
|
||
this.muteValueOnTrickPlaybackToggle = undefined;
|
||
}
|
||
}
|
||
play() {
|
||
this.logger.info('play()');
|
||
this.mediaFunctions.play();
|
||
}
|
||
pause() {
|
||
this.logger.info('pause()');
|
||
this.mediaFunctions.pause();
|
||
}
|
||
get expectPlayEvent() {
|
||
return this.mediaFunctions.expectPlayEvent;
|
||
}
|
||
set expectPlayEvent(value) {
|
||
this.mediaFunctions.expectPlayEvent = value;
|
||
}
|
||
get expectPauseEvent() {
|
||
return this.mediaFunctions.expectPauseEvent;
|
||
}
|
||
set expectPauseEvent(value) {
|
||
this.mediaFunctions.expectPauseEvent = value;
|
||
}
|
||
set textTracksCreated(created) {
|
||
const { mediaElementStore } = this;
|
||
mediaElementStore.textTracksCreated = created;
|
||
}
|
||
get msDuration() {
|
||
return this._mediaQuery.msDuration;
|
||
}
|
||
set msDuration(duration) {
|
||
try {
|
||
const { mediaElementStore } = this;
|
||
const mediaSource = this.mediaSource$.value;
|
||
if (mediaSource.duration !== duration) {
|
||
this.logger.debug(`set msduration ${duration}`);
|
||
mediaSource.duration = duration;
|
||
mediaElementStore.msDuration = duration;
|
||
}
|
||
}
|
||
catch (err) {
|
||
this.logger.warn(`Error setting duration ${err.message}`);
|
||
}
|
||
}
|
||
set haveEnough(haveEnough) {
|
||
this.mediaElementStore.haveEnough = haveEnough;
|
||
}
|
||
set flushing(flushing) {
|
||
this.mediaElementStore.flushing = flushing;
|
||
}
|
||
set bufferMonitorTargetDuration(durationSeconds) {
|
||
this.mediaElementStore.bufferMonitorTargetDuration = durationSeconds;
|
||
}
|
||
get textTracks() {
|
||
return this.mediaElement.textTracks;
|
||
}
|
||
get id3TextTrack() {
|
||
return this.id3Track;
|
||
}
|
||
addTextTrack(kind, label, language) {
|
||
return this.mediaElement.addTextTrack(kind, label, language);
|
||
}
|
||
dispatchEvent(e) {
|
||
return this.mediaElement.dispatchEvent(e);
|
||
}
|
||
get offsetWidth() {
|
||
return this.mediaElement.offsetWidth;
|
||
}
|
||
get offsetHeight() {
|
||
return this.mediaElement.offsetHeight;
|
||
}
|
||
getliveSeekableWindow() {
|
||
return this.liveSeekableWindow;
|
||
}
|
||
archiveParsedSubtitleFragmentRecord(persistentId, mediaSeqNum, fragCueRecord) {
|
||
return this.mediaElementStore.archiveParsedSubtitleFragmentRecord(persistentId, mediaSeqNum, fragCueRecord);
|
||
}
|
||
updateLiveSeekableRange(levelDetails) {
|
||
const fragments = levelDetails.fragments;
|
||
const len = fragments.length;
|
||
if (len > 1) {
|
||
const start = Math.max(fragments[0].start, 0);
|
||
const end = fragments[len - 1].start + fragments[len - 1].duration;
|
||
this.mediaSource$.value.updateLiveSeekableRange(start, end);
|
||
this.liveSeekableWindow.start = start;
|
||
this.liveSeekableWindow.end = end;
|
||
}
|
||
}
|
||
clearLiveSeekableRange() {
|
||
this.mediaSource$.value.clearLiveSeekableRange();
|
||
this.liveSeekableWindow.start = NaN;
|
||
this.liveSeekableWindow.end = NaN;
|
||
}
|
||
}
|
||
/**
|
||
* @brief Translate media element events to store updates
|
||
*/
|
||
const hookMediaElementsEvents = (mediaElement, mediaElementStore, mediaQuery, mediaSink, gaplessInstance, config, logger, rtcService) => {
|
||
if (!mediaElement)
|
||
return EMPTY;
|
||
const target = fromEventTarget(mediaElement);
|
||
return merge(target.event('durationchange').pipe(map((ev) => convertEvent(mediaElement, 'durationchange', ev)), tap((ev) => {
|
||
const media = ev.currentTarget;
|
||
mediaElementStore.mediaElementDuration = media.duration;
|
||
})), target.event('seeking').pipe(throttleTime(config.seekEventThrottleMs), map((ev) => convertEvent(mediaElement, 'seeking', ev)), tap((ev) => {
|
||
const media = ev.currentTarget;
|
||
const seekToValue = media.currentTime;
|
||
if (media.readyState >= media.HAVE_METADATA) {
|
||
const seekTo = mediaQuery.seekTo;
|
||
if (!seekTo || (!seekTo.fromEvent && Math.abs(seekTo.pos - seekToValue) > 0.00001)) {
|
||
// Gapless Stuff
|
||
if (gaplessInstance.inGaplessMode) {
|
||
gaplessSeek(seekToValue, gaplessInstance, mediaSink, mediaElementStore, logger);
|
||
return;
|
||
}
|
||
else {
|
||
if (mediaSink &&
|
||
// eslint-disable-next-line no-prototype-builtins
|
||
mediaSink.hasOwnProperty('liveSeekableWindow') &&
|
||
isFiniteNumber(mediaSink.getliveSeekableWindow().start) &&
|
||
isFiniteNumber(mediaSink.getliveSeekableWindow().end) &&
|
||
(seekToValue < mediaSink.getliveSeekableWindow().start || seekToValue > mediaSink.getliveSeekableWindow().end)) {
|
||
// Adjust seek to within live window
|
||
liveAdjustedSeek(seekToValue, mediaSink.getliveSeekableWindow().start, mediaSink.getliveSeekableWindow().end, config, mediaElementStore, logger);
|
||
}
|
||
else {
|
||
mediaElementStore.setSeekToPos(seekToValue, true);
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
// seekTo is already set (not from event) so we just need to update seeking state until "seeked"
|
||
mediaElementStore.seeking = true;
|
||
}
|
||
}
|
||
})), target.event('seeked').pipe(map((ev) => convertEvent(mediaElement, 'seeked', ev)), tap(() => {
|
||
mediaElementStore.setSeekToPos(null, true);
|
||
const currentTime = mediaElement.currentTime;
|
||
if (isFiniteNumber(currentTime)) {
|
||
logger.qe({ critical: true, name: 'seeked', data: { seekTo: currentTime.toFixed(3) } });
|
||
}
|
||
})), target.event('play').pipe(map((ev) => convertEvent(mediaElement, 'play', ev)), withLatestFrom(mediaQuery.desiredRate$), map(([ev, desiredRate]) => {
|
||
const media = ev.currentTarget;
|
||
mediaElementStore.paused = media.paused;
|
||
const expectPlayEvent = mediaSink.expectPlayEvent;
|
||
const allowSetRate = media.controls || config.nativeControlsEnabled;
|
||
logger.info(`media play desiredRate=${desiredRate} expectPlayEvent=${expectPlayEvent} allowed=${allowSetRate}`);
|
||
if (!expectPlayEvent && allowSetRate) {
|
||
mediaSink.checkForReplay();
|
||
mediaElementStore.desiredRate = 1;
|
||
}
|
||
mediaSink.expectPlayEvent = false;
|
||
return ev;
|
||
})), target.event('playing').pipe(map((ev) => convertEvent(mediaElement, 'playing', ev)), tap((ev) => {
|
||
const media = ev.currentTarget;
|
||
// log playing event immediately.
|
||
logger.qe({ critical: true, name: 'playerEvent', data: { type: ev.type, pos: media.currentTime.toFixed(3), dur: media.duration.toFixed(3) } });
|
||
mediaElementStore.paused = media.paused;
|
||
mediaElementStore.gotPlayingEvent();
|
||
})), target.event('loadstart').pipe(map((ev) => convertEvent(mediaElement, 'loadstart', ev)), tap(() => {
|
||
mediaElementStore.gotLoadStartEvent();
|
||
})), target.event('pause').pipe(map((ev) => convertEvent(mediaElement, 'pause', ev)), tap((ev) => {
|
||
const media = ev.currentTarget;
|
||
mediaElementStore.paused = media.paused;
|
||
const expectPauseEvent = mediaSink.expectPauseEvent;
|
||
const allowSetRate = media.controls || config.nativeControlsEnabled;
|
||
logger.info(`media pause desiredRate=${mediaQuery.desiredRate} expectPauseEvent=${expectPauseEvent} allowed=${allowSetRate}`);
|
||
if (!expectPauseEvent && allowSetRate) {
|
||
mediaElementStore.desiredRate = 0;
|
||
}
|
||
mediaSink.expectPauseEvent = false;
|
||
})),
|
||
// readystate change events. May not be fired for all platforms so take
|
||
// with a grain of salt
|
||
target.event('loadedmetadata').pipe(map((ev) => convertEvent(mediaElement, 'loadedmetadata', ev))), // HAVE_METADATA
|
||
target.event('loadeddata').pipe(map((ev) => convertEvent(mediaElement, 'loadeddata', ev))), // HAVE_CURRENT_DATA
|
||
target.event('canplay').pipe(map((ev) => convertEvent(mediaElement, 'canplay', ev))), // HAVE_FUTURE_DATA
|
||
target.event('canplaythrough').pipe(map((ev) => convertEvent(mediaElement, 'canplaythrough', ev))), // HAVE_ENOUGH_DATA
|
||
target.event('waiting').pipe(map((ev) => convertEvent(mediaElement, 'waiting', ev))), // less than HAVE_CURRENT_DATA and not paused
|
||
target.event('emptied').pipe(map((ev) => convertEvent(mediaElement, 'emptied', ev))), // HAVE_NOTHING (media.load() was called)
|
||
target.event('error').pipe(map((ev) => convertEvent(mediaElement, 'error', ev)), concatMap((ev) => {
|
||
return throwError(mediaElement.error);
|
||
})), // HAVE_NOTHING
|
||
target.event('ended').pipe(map((ev) => convertEvent(mediaElement, 'ended', ev)))).pipe(withLatestFrom(mediaQuery.bufferedRangeTuple$), withTransaction(([ev, bufferedRanges]) => {
|
||
const media = ev.currentTarget;
|
||
const readyState = media.readyState;
|
||
mediaElementStore.readyState = readyState;
|
||
mediaElementStore.ended = media.ended;
|
||
logger.info(`media event: ${ev.type} pos:${media.currentTime.toFixed(3)} dur:${media.duration.toFixed(3)} buffered:${stringifyWithPrecision(bufferedRanges)} readyState:${readyState}`);
|
||
// playing event is logged immediately when event occurs, so skip it here
|
||
if (ev.type != 'playing') {
|
||
logger.qe({ critical: true, name: 'playerEvent', data: { type: ev.type, pos: media.currentTime.toFixed(3), dur: media.duration.toFixed(3) } });
|
||
}
|
||
}), catchError((err) => {
|
||
if (err instanceof MediaError) {
|
||
logger.warn(`mediaElementError, code: ${err.code}, message: ${err.message}`);
|
||
rtcService === null || rtcService === void 0 ? void 0 : rtcService.handleMediaElementError(err);
|
||
}
|
||
else {
|
||
logger.error(`media event error: ${err.message}`);
|
||
}
|
||
return EMPTY;
|
||
}), switchMapTo(NEVER), catchError((err) => {
|
||
if (err instanceof MediaError) {
|
||
logger.warn(`mediaElementError, code: ${err.code}, message: ${err.message}`);
|
||
return throwError(err); // re-throw media element errors for reporting
|
||
}
|
||
else {
|
||
logger.error(`media event error: ${err.message}`);
|
||
return EMPTY;
|
||
}
|
||
}));
|
||
};
|
||
// Helper function for seeking when in gapless mode
|
||
function gaplessSeek(seekToValue, gaplessInstance, mediaSink, mediaElementStore, logger) {
|
||
let seekValueAdjusted = false;
|
||
// Adjust seekToValue if needed
|
||
logger.debug(`seekTo before gapless adjust: ${seekToValue}, startOffset: ${gaplessInstance.playingItem.itemStartOffset}`);
|
||
if (seekToValue < gaplessInstance.playingItem.itemStartOffset) {
|
||
logger.warn(`[Gapless] Seeking past track boundary oldSeek=${seekToValue}, adjustedSeek=${gaplessInstance.playingItem.itemStartOffset}`);
|
||
seekToValue = gaplessInstance.playingItem.itemStartOffset;
|
||
seekValueAdjusted = true;
|
||
}
|
||
if (gaplessInstance.isPreloading) {
|
||
if (seekToValue > gaplessInstance.loadingItem.itemStartOffset) {
|
||
logger.warn(`[Gapless] Seeking past track boundary oldSeek=${seekToValue}, adjustedSeek=${gaplessInstance.loadingItem.itemStartOffset}`);
|
||
seekToValue = gaplessInstance.loadingItem.itemStartOffset;
|
||
seekValueAdjusted = true;
|
||
}
|
||
// Unable to get the state before the seek event, hence need to dequeue on every seek.
|
||
gaplessInstance.dequeueSource('SeekToUnbufferedTimeRanges');
|
||
}
|
||
if (seekValueAdjusted) {
|
||
mediaSink.resetMediaSource(seekToValue);
|
||
}
|
||
else {
|
||
mediaElementStore.setSeekToPos(seekToValue, true);
|
||
}
|
||
}
|
||
// TODO consolidate this with live-position sanitizeLiveSeek
|
||
function liveAdjustedSeek(seekValue, liveSeekableWindowStart, liveSeekableWindowEnd, config, mediaElementStore, logger) {
|
||
let adjustedSeek = seekValue;
|
||
if (seekValue < liveSeekableWindowStart) {
|
||
adjustedSeek = liveSeekableWindowStart;
|
||
}
|
||
else if (seekValue > liveSeekableWindowEnd) {
|
||
let targetLatency = config.defaultTargetDuration;
|
||
if (isFiniteNumber(config.liveSyncDuration)) {
|
||
targetLatency = config.liveSyncDuration;
|
||
}
|
||
else if (isFiniteNumber(config.liveSyncDurationCount)) {
|
||
targetLatency = config.liveSyncDurationCount * config.defaultTargetDuration;
|
||
}
|
||
adjustedSeek = Math.max(0, liveSeekableWindowEnd - targetLatency);
|
||
}
|
||
logger.warn(`[live] liveAdjustedSeek seekTo:${toFixed(seekValue, 3)}, adjustedSeek:${toFixed(adjustedSeek, 3)}, liveWindowStart:${toFixed(liveSeekableWindowStart, 3)}, liveWindowEnd:${toFixed(liveSeekableWindowEnd, 3)}`);
|
||
mediaElementStore.setSeekToPos(adjustedSeek, true);
|
||
}
|
||
function getMatchingInfo(sbEntity, fragStart, fragEnd, discoSeqNum, mediaOptionId, logger) {
|
||
const buffered = sbEntity === null || sbEntity === void 0 ? void 0 : sbEntity.bufferedSegments;
|
||
if (!buffered) {
|
||
logger.warn('getMatchingInfo trying to query null sbEntity');
|
||
return null;
|
||
}
|
||
const seg = buffered.find((seg) => {
|
||
const matchDisco = seg.frag.discoSeqNum === discoSeqNum;
|
||
const overlap = Math.max(fragStart, seg.startPTS) < Math.min(fragEnd, seg.endPTS);
|
||
return matchDisco && overlap;
|
||
});
|
||
if (seg != null) {
|
||
const { audioCodec, videoCodec, mimeType } = sbEntity;
|
||
return { mimeType, audioCodec, videoCodec, startPTSSec: seg.startPTS, endPTSSec: seg.endPTS, discoSeqNum, mediaOptionId };
|
||
}
|
||
return null;
|
||
}
|
||
/**
|
||
* check whether the appendData is compatible with the curent source buffers.
|
||
* @param appendData the data to append. It is assumed that we will never have mismatched discoSeqNum for these two
|
||
* @param offsetTimestamp timestamp value that will be used to SourceBuffer.timestampOffset. Use for position calculation
|
||
* @param sourceBufferEntities info about the current set of buffers
|
||
* @param expectedSbCount the expected number of sourceBuffers
|
||
* @param logger a logger instance
|
||
* @returns compatible: if compatible with current source buffers
|
||
* boundary: the discontinuity boundary. NaN if we didn't get enough info
|
||
*/
|
||
function isCompatibleWithSourceBuffers(appendData, sourceBufferEntities, expectedSbCount, gapTolerance, logger) {
|
||
var _a, _b;
|
||
const curSbCount = sourceBufferEntities.filter((sbEntity) => Boolean(sbEntity)).length;
|
||
const newSbCount = appendData.filter((sbDataTuple) => Boolean(sbDataTuple)).length;
|
||
const compatInfos = [null, null];
|
||
appendData.forEach((sbAppendData, type) => {
|
||
if (sbAppendData) {
|
||
const { offsetTimestamp } = sbAppendData;
|
||
const tsOffsetSec = convertTimestampToSeconds(offsetTimestamp);
|
||
const mimeType = sbAppendData.initSeg.mimeType;
|
||
const { audioCodec, videoCodec } = sbAppendData.initSeg.initParsedData;
|
||
const { dataSeg } = sbAppendData;
|
||
const startPTS = convertTimestampToSeconds(dataSeg.startPts) - tsOffsetSec;
|
||
const endPTS = convertTimestampToSeconds(dataSeg.endPts) - tsOffsetSec;
|
||
const discoSeqNum = dataSeg.discoSeqNum;
|
||
compatInfos[type] = { audioCodec, videoCodec, mimeType, startPTSSec: startPTS, endPTSSec: endPTS, discoSeqNum, mediaOptionId: sbAppendData.initSeg.mediaOptionId };
|
||
logger.trace(`compatInfos[${type}] = { ${audioCodec}, ${videoCodec}, ${mimeType}, ${startPTS}, ${endPTS}, ${discoSeqNum}, ${sbAppendData.initSeg.mediaOptionId}`);
|
||
}
|
||
});
|
||
let compatible = curSbCount === expectedSbCount;
|
||
let gotEnoughData = newSbCount === expectedSbCount;
|
||
// check appendData count against enabled tracks (expectedSbCount) instead of existing sourceBuffer counts (curSbCount).
|
||
// when switching between muxed and alt audio, curSbCount may be wrong and need a resetMediaSource before appending.
|
||
logger.trace(`mediaSink setup: enabledMediaOption# ${expectedSbCount} sourceBuffer# ${curSbCount} appendData# ${newSbCount}: compatible ${compatible} gotEnoughData ${gotEnoughData}`);
|
||
if (newSbCount === 1 && newSbCount < expectedSbCount && curSbCount !== 0) {
|
||
const curType = appendData[MediaOptionType.Variant] ? MediaOptionType.Variant : MediaOptionType.AltAudio;
|
||
const otherType = 1 - curType;
|
||
const curFrag = compatInfos[curType];
|
||
const foundFrag = (compatInfos[otherType] = getMatchingInfo(sourceBufferEntities[otherType], curFrag.startPTSSec, curFrag.endPTSSec, curFrag.discoSeqNum, curFrag.mediaOptionId, logger));
|
||
if (!foundFrag) {
|
||
const iframeMode = (_b = (_a = appendData[MediaOptionType.Variant]) === null || _a === void 0 ? void 0 : _a.dataSeg) === null || _b === void 0 ? void 0 : _b.iframe;
|
||
if (iframeMode && curType === MediaOptionType.Variant && compatInfos[curType]) {
|
||
// in iframe mode, audio may have large loading gaps. Allow iframes to append even when no matching audio frag is present.
|
||
const currentVideoCodec = sourceBufferEntities[curType].videoCodec;
|
||
const newVideoCodec = compatInfos[curType].videoCodec;
|
||
compatible = compatible && (currentVideoCodec === newVideoCodec || MediaUtil.isCompatibleVideoCodec(currentVideoCodec, newVideoCodec));
|
||
if (compatible) {
|
||
logger.info(`${MediaOptionNames[curType]} allow iframe=${currentVideoCodec}->${newVideoCodec} with no matching audio #disco`);
|
||
return { compatible, boundary: NaN, allowance: NaN, discoSeqNum: compatInfos[curType].discoSeqNum };
|
||
}
|
||
}
|
||
else {
|
||
logger.warn(`${MediaOptionNames[curType]} No matching frag found ${stringifyWithPrecision(curFrag)} buffered=${stringifyWithPrecision(sourceBufferEntities[otherType].bufferedSegments.map((seg) => {
|
||
const { mediaSeqNum, discoSeqNum } = seg.frag;
|
||
return { mediaSeqNum, discoSeqNum, startPTS: seg.startPTS, endPTS: seg.endPTS };
|
||
}))}`);
|
||
}
|
||
}
|
||
else {
|
||
logger.trace(`foundFrag = compatInfos[${otherType}] = { ${foundFrag.audioCodec}, ${foundFrag.videoCodec}, ${foundFrag.mimeType}, ${foundFrag.startPTSSec}, ${foundFrag.endPTSSec}, ${foundFrag.discoSeqNum}, ${foundFrag.mediaOptionId}`);
|
||
}
|
||
gotEnoughData = foundFrag != null;
|
||
}
|
||
let boundary = NaN;
|
||
let allowance = NaN;
|
||
let cc = NaN;
|
||
if (gotEnoughData) {
|
||
compatInfos.forEach((info, type) => {
|
||
var _a;
|
||
if (!info)
|
||
return null;
|
||
// to be used for incompat. discontinuity only:
|
||
// set cc if all append & buffered data at this pos have the same cc
|
||
// determine the next discontinuity sequence number after resetMediaSource
|
||
if (!isFiniteNumber(cc)) {
|
||
cc = info.discoSeqNum;
|
||
}
|
||
else if (cc !== info.discoSeqNum) {
|
||
cc = NaN;
|
||
}
|
||
const currentSbEntity = sourceBufferEntities[type];
|
||
if (!currentSbEntity) {
|
||
compatible = false;
|
||
}
|
||
else {
|
||
const currentAudioCodec = currentSbEntity.audioCodec;
|
||
const currentVideoCodec = currentSbEntity.videoCodec;
|
||
const { audioCodec: newAudioCodec, videoCodec: newVideoCodec } = info;
|
||
compatible = compatible && (currentVideoCodec === newVideoCodec || MediaUtil.isCompatibleVideoCodec(currentVideoCodec, newVideoCodec));
|
||
compatible = compatible && (currentAudioCodec === newAudioCodec || MediaUtil.isCompatibleAudioCodec(currentAudioCodec, newAudioCodec));
|
||
logger.info(`${MediaOptionNames[type]} video=${currentVideoCodec}->${newVideoCodec} audio=${currentAudioCodec}->${newAudioCodec} start=${(_a = info.startPTSSec) === null || _a === void 0 ? void 0 : _a.toFixed(3)} #disco`);
|
||
}
|
||
if (isFiniteNumber(boundary)) {
|
||
allowance = Math.abs(info.startPTSSec - boundary);
|
||
boundary = Math.max(info.startPTSSec, boundary);
|
||
}
|
||
else {
|
||
allowance = 0;
|
||
boundary = info.startPTSSec;
|
||
}
|
||
});
|
||
}
|
||
return { compatible: compatible && gotEnoughData, boundary, allowance, discoSeqNum: cc };
|
||
}
|
||
function seekEpic(mediaElement, mediaQuery, mediaSink, config, logger) {
|
||
logger = logger.child({ name: 'seek' });
|
||
return (seekTo$) => seekTo$.pipe(tap((seekTo) => logger.info(`seekTo=${JSON.stringify(seekTo)}`)), filter((seekTo) => seekTo && isFiniteNumber(seekTo.pos)), switchMap((seekTo) => {
|
||
return mediaQuery.readyState$.pipe(filter((readyState) => readyState >= mediaElement.HAVE_METADATA), take(1), mapTo(seekTo), switchMap(({ pos, fromEvent }) => {
|
||
logger.info(`startSeek(${pos === null || pos === void 0 ? void 0 : pos.toFixed(3)},${fromEvent})`);
|
||
mediaSink.checkForInconsistentStoreBufferRangesAndUpdate();
|
||
if (!mediaElement.paused) {
|
||
mediaSink.pause();
|
||
}
|
||
if (!fromEvent) {
|
||
mediaElement.currentTime = pos;
|
||
}
|
||
return waitFor(mediaQuery.haveEnough$, (x) => x).pipe(mapTo({ pos, fromEvent }));
|
||
}), switchMap(({ pos, fromEvent }) => {
|
||
const combinedBuffer = mediaQuery.getCombinedBufferInfo(pos, 0);
|
||
const nextStart = combinedBuffer.nextStart;
|
||
const applyNudge = !fromEvent || config.nudgeFromEventSeek;
|
||
logger.info(`haveEnough readystate=${mediaQuery.readyState} fromEvent=${fromEvent} applyNudge=${applyNudge} nextStart ${nextStart} pos ${pos} combinedBuffer.len ${combinedBuffer.len}`);
|
||
if (applyNudge && combinedBuffer.len === 0 && isFiniteNumber(nextStart) && nextStart > pos && nextStart - pos <= config.maxSeekHole) {
|
||
logger.info(`haveEnough but current position not actually buffered, nudge ${pos.toFixed(3)}->${nextStart.toFixed(3)}`);
|
||
mediaSink.seekTo = nextStart;
|
||
return EMPTY;
|
||
}
|
||
return of(pos);
|
||
}), withLatestFrom(mediaQuery.desiredRate$), map(([pos, desiredRate]) => {
|
||
logger.info(`completeSeek(${pos === null || pos === void 0 ? void 0 : pos.toFixed(3)}) readyState=${mediaElement.readyState} paused=${mediaElement.paused} desiredRate=${desiredRate}`);
|
||
if (mediaElement.paused && desiredRate !== 0) {
|
||
mediaSink.play();
|
||
}
|
||
}), switchMapTo(EMPTY), catchError((err) => {
|
||
logger.error(`error during seek ${err.message}`);
|
||
return EMPTY;
|
||
}));
|
||
}));
|
||
}
|
||
function rateChangeEpic(mediaElement, mediaQuery, mediaSink, logger) {
|
||
return (rateChange$) => rateChange$.pipe(withLatestFrom(mediaQuery.seekTo$), switchMap(([rate, seekTo]) => {
|
||
if (isFiniteNumber(seekTo === null || seekTo === void 0 ? void 0 : seekTo.pos)) {
|
||
return EMPTY; // Taken care of by seekEpic
|
||
}
|
||
if (rate === 0) {
|
||
if (!mediaElement.paused) {
|
||
mediaSink.pause();
|
||
}
|
||
return EMPTY;
|
||
}
|
||
return waitFor(mediaQuery.haveEnough$, (x) => x).pipe(tap(() => {
|
||
if (mediaElement.paused) {
|
||
mediaSink.play();
|
||
}
|
||
}));
|
||
}), switchMapTo(EMPTY));
|
||
}
|
||
function stallEpic(mediaSink, meStore, config, logger) {
|
||
return (stallInfo$) => stallInfo$.pipe(exhaustMap((stallInfo) => {
|
||
if (!stallInfo) {
|
||
return of(NaN);
|
||
}
|
||
return nudgeSeek(mediaSink, meStore, config, logger);
|
||
}));
|
||
}
|
||
function nudgeSeek(mediaSink, meStore, config, logger) {
|
||
const meQuery = mediaSink.mediaQuery;
|
||
const gotUserSeek$ = combineQueries([meQuery.seekTo$, meQuery.nudgeTarget$]).pipe(filter(([seekTo, nudgeTarget]) => seekTo && isFiniteNumber(seekTo.pos) && isFiniteNumber(nudgeTarget) && (seekTo.pos < nudgeTarget || seekTo.pos - nudgeTarget > config.maxSeekHole)), mapTo(null));
|
||
return merge(gotUserSeek$, meQuery.stallInfo$).pipe(withLatestFrom(meQuery.desiredRate$), observeOn(asyncScheduler), map(([stallInfo, desiredRate], index) => {
|
||
if (!stallInfo) {
|
||
return NaN;
|
||
}
|
||
const bufferInfo = meQuery.getCombinedBufferInfo(stallInfo.currentTime, 0); // maxBufferHole 0
|
||
const iframeMode = isIframeRate(desiredRate);
|
||
return nudgeIfNeeded(mediaSink, config, logger, stallInfo, bufferInfo, iframeMode, index);
|
||
}), takeWhile((nudgeTarget) => isFiniteNumber(nudgeTarget)), finalize$1(() => {
|
||
logger.debug('nudge seek finalize');
|
||
meStore.setNudgeInfo(null);
|
||
}));
|
||
}
|
||
function nudgeIfNeeded(mediaSink, config, logger, stallInfo, bufferInfo, iframeMode, nudgeRetry) {
|
||
const { type, isLowBufferStall, currentTime } = stallInfo;
|
||
const bufferLen = bufferInfo.len;
|
||
const jumpThreshold = config.maxSeekHole;
|
||
let nudgePosition = NaN;
|
||
if (isLowBufferStall) {
|
||
// if buffer len is below threshold, try to jump to start of next buffer range if close
|
||
// no buffer available @ currentTime, check if next buffer is close (within a config.maxSeekHole second range)
|
||
const nextBufferStart = bufferInfo.nextStart - currentTime <= jumpThreshold ? bufferInfo.nextStart : Infinity; //this._ensureNudgeStartTimeForIncompatibleCC(bufferInfo.nextStart, currentTime, config.maxSeekHole);
|
||
if (isFiniteNumber(nextBufferStart)) {
|
||
// next buffer is close ! adjust currentTime to nextBufferStart
|
||
// this will ensure effective video decoding
|
||
logger.info(`adjust currentTime from ${currentTime} to next buffered @ ${nextBufferStart}`);
|
||
nudgePosition = nextBufferStart;
|
||
}
|
||
else if (mediaSink.mediaQuery.msDuration - currentTime < 0.1) {
|
||
nudgePosition = currentTime + 0.1;
|
||
}
|
||
}
|
||
else {
|
||
// if (this.relaxHighBufferStallRule) {
|
||
// this.relaxHighBufferStallRule--;
|
||
// logger.log(`ignore stall. ${this.relaxHighBufferStallRule} free pass available. staying put @ ${media.currentTime}`);
|
||
// payload = new BufferStallError(ErrorDetails.BUFFER_STALLED_ERROR, false,
|
||
// 'got buffer stalled error', ErrorResponses.VideoDecoderBadDataErr, type, bufferLen);
|
||
// } else
|
||
if (nudgeRetry < config.nudgeMaxRetry) {
|
||
// playback stalled in buffered area ... let's nudge currentTime to try to overcome this
|
||
nudgePosition = currentTime + config.nudgeOffset;
|
||
}
|
||
else if (iframeMode) {
|
||
// maxed number of nudges - but in iframeMode
|
||
logger.error(`still stuck in high buffer @${currentTime} after ${config.nudgeMaxRetry}, non fatal in iframeMode`);
|
||
}
|
||
else {
|
||
logger.error(`still stuck in high buffer @${currentTime} after ${config.nudgeMaxRetry}, raise fatal error`);
|
||
throw new BufferStallError(ErrorDetails.BUFFER_STALLED_ERROR, true, 'got fatal buffer error', ErrorResponses.VideoDecoderBadDataErr, type, bufferLen);
|
||
}
|
||
}
|
||
if (isFiniteNumber(nudgePosition)) {
|
||
logger.info(`stall ${type} nudge ${currentTime}->${nudgePosition} nudgeRetry=${nudgeRetry}`);
|
||
mediaSink.nudgeSeek(nudgePosition, nudgeRetry + 1);
|
||
}
|
||
return nudgePosition;
|
||
}
|
||
|
||
/**
|
||
* @brief Service that manages playback state
|
||
*/
|
||
// state for the service functions
|
||
const mediaElementStore = new MediaElementStore();
|
||
/**
|
||
* media component managment
|
||
*/
|
||
const mediaElementServiceEpic = (makeMediaSource, config, gaplessInstance, logger, teardownWG$, rtcService) => (mediaElementSource$) => {
|
||
return mediaElementSource$.pipe(tag('playback.mediaElementServiceEpic.in'), switchMap((mediaElement) => {
|
||
if (!mediaElement) {
|
||
return of(null);
|
||
}
|
||
const mediaSink = new MediaSink(mediaElement, mediaElementStore, config, gaplessInstance, logger, teardownWG$, rtcService);
|
||
mediaSink.openMediaSource(makeMediaSource());
|
||
return merge(of(mediaSink), mediaSink);
|
||
}), tag('playback.mediaElementServiceEpic.emit'));
|
||
};
|
||
|
||
const displaySupportsHdr$ = () => {
|
||
if (typeof matchMedia === 'function') {
|
||
const mediaQueryList = matchMedia('(dynamic-range: high)');
|
||
const badQuery = matchMedia('bad query');
|
||
if (mediaQueryList.media !== badQuery.media) {
|
||
return merge(of(mediaQueryList), fromEvent(mediaQueryList, 'change')).pipe(map((e) => e.matches));
|
||
}
|
||
}
|
||
// assume hdr is supported by display
|
||
return of(true);
|
||
};
|
||
|
||
class PlatformQuery extends Query {
|
||
constructor(store) {
|
||
super(store);
|
||
this.store = store;
|
||
this.displaySupportsHdr$ = this.select('supportsHdr');
|
||
this.platformInfo$ = this.select('platformInfo');
|
||
this.viewportInfo$ = this.select('viewportInfo');
|
||
}
|
||
get platformInfo() {
|
||
return this.getValue().platformInfo;
|
||
}
|
||
get displaySupportsHdr() {
|
||
return this.getValue().supportsHdr;
|
||
}
|
||
get viewportInfo() {
|
||
return this.getValue().viewportInfo;
|
||
}
|
||
}
|
||
|
||
function createInitialState() {
|
||
return { supportsHdr: true };
|
||
}
|
||
class PlatformStore extends Store {
|
||
constructor() {
|
||
super(createInitialState(), { name: 'platform', producerFn: produce_1 });
|
||
}
|
||
}
|
||
|
||
class PlatformService {
|
||
constructor(store) {
|
||
this.store = store;
|
||
}
|
||
getQuery() {
|
||
return new PlatformQuery(this.store);
|
||
}
|
||
updateSupportsHdr(supported) {
|
||
this.store.update((state) => {
|
||
state.supportsHdr = supported;
|
||
});
|
||
}
|
||
updatePlatformInfo(platformInfo) {
|
||
this.store.update({ platformInfo });
|
||
}
|
||
updateViewportInfo(viewportInfo) {
|
||
this.store.update((state) => {
|
||
state.viewportInfo = viewportInfo;
|
||
});
|
||
}
|
||
}
|
||
const store = new PlatformStore();
|
||
let service$1 = null; // To be instantiated in platformService();
|
||
/***********************************************
|
||
* Static helper functions that specifically use the above singletons
|
||
*/
|
||
function createPlatformQuery() {
|
||
return new PlatformQuery(store);
|
||
}
|
||
/**
|
||
* @returns The global instance of PlatformService that operates on global PlatformStore
|
||
*/
|
||
function platformService() {
|
||
if (!service$1) {
|
||
service$1 = new PlatformService(store);
|
||
}
|
||
return service$1;
|
||
}
|
||
function listenForHdrUpdates(platformService) {
|
||
return displaySupportsHdr$().pipe(tap((supported) => {
|
||
platformService.updateSupportsHdr(supported);
|
||
}));
|
||
}
|
||
|
||
// The vanilla RPCService uses old-school callback pattern to minimize the
|
||
// dependency surface in restricted environments, e.g. in Web Worker.
|
||
// However, if resource permitted, one can use this RPCServiceObservable wrapper
|
||
// to convert RPCService interface into Observable pattern for better
|
||
// interoperability with RxJs code.
|
||
class RPCServiceObservable extends Observable {
|
||
constructor(_vanillaRPC, teardownWG$) {
|
||
super(subscriber => {
|
||
teardownWG$ === null || teardownWG$ === void 0 ? void 0 : teardownWG$.add();
|
||
subscriber.add(this._handler$
|
||
.pipe(mergeMap(([handler, args, callback]) => handler(...args).pipe(tap({
|
||
next(result) {
|
||
callback(result, null);
|
||
},
|
||
error(error) {
|
||
callback(null, error);
|
||
},
|
||
}))))
|
||
.subscribe());
|
||
});
|
||
this._vanillaRPC = _vanillaRPC;
|
||
this.teardownWG$ = teardownWG$;
|
||
this._handler$ = new Subject();
|
||
}
|
||
register(command, handler) {
|
||
const asyncHandler = (...args) => (callback) => {
|
||
this._handler$.next([handler, args, callback]);
|
||
};
|
||
return this._vanillaRPC.register(command, asyncHandler);
|
||
}
|
||
invoke(command, args, transfer) {
|
||
return new Observable((subscriber) => {
|
||
this._vanillaRPC.invoke(command, args, transfer)((result, error) => {
|
||
if (error != null) {
|
||
subscriber.error(error);
|
||
}
|
||
else {
|
||
subscriber.next(result);
|
||
subscriber.complete();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
// CryptoRPCClient only runs in main process, so I has more resources to include
|
||
// the RxJS as dependency and use the Observable pattern. So we can extend on
|
||
// RPCServiceObservable instead of the vanilla RPCService class.
|
||
class CryptoRPCClient extends RPCServiceObservable {
|
||
constructor(rpcService) {
|
||
super(rpcService);
|
||
}
|
||
decrypt(key, iv, alg, cipherText, options) {
|
||
return this.invoke('decrypt', [key, iv, alg, cipherText, options], [key, iv, cipherText]);
|
||
}
|
||
}
|
||
|
||
class DemuxRPCClient extends RPCServiceObservable {
|
||
constructor(rpcService) {
|
||
super(rpcService);
|
||
this.rpcService = rpcService;
|
||
this.sessions = {};
|
||
this._onEvent = (demuxSessionID, event, data) => () => {
|
||
if (this.sessions[demuxSessionID] != null) {
|
||
this.sessions[demuxSessionID].observer.trigger(event, data);
|
||
}
|
||
};
|
||
this.rpcService.register('demuxer.event', this._onEvent);
|
||
}
|
||
init(typeSupported, config, vendor) {
|
||
config = (({ maxSeekHole, maxBufferHole, audioPrimingDelay, stretchShortVideoTrack, forceKeyFrameOnDiscontinuity }) => ({
|
||
maxSeekHole,
|
||
maxBufferHole,
|
||
audioPrimingDelay,
|
||
stretchShortVideoTrack,
|
||
forceKeyFrameOnDiscontinuity,
|
||
}))(config);
|
||
return this.invoke('demuxer.init', [typeSupported, config, vendor], []).pipe(map((demuxSessionID) => {
|
||
const session = new DemuxRPCSession(this, demuxSessionID);
|
||
this.sessions[demuxSessionID] = session;
|
||
return session;
|
||
}));
|
||
}
|
||
}
|
||
class DemuxRPCSession {
|
||
constructor(rpc, demuxSessionID) {
|
||
this.rpc = rpc;
|
||
this.demuxSessionID = demuxSessionID;
|
||
this.observer = new Observer();
|
||
}
|
||
push(data, keyTagInfo, initSegment, timeOffset, discontinuity, trackSwitch, contiguous, duration, accurateTimeOffset, defaultInitPTS, iframeMediaStart, iframeDuration, transfer) {
|
||
return this.rpc.invoke('demuxer.push', [this.demuxSessionID, data, keyTagInfo, initSegment, timeOffset, discontinuity, trackSwitch, contiguous, duration, accurateTimeOffset, defaultInitPTS, iframeMediaStart, iframeDuration], transfer !== null && transfer !== void 0 ? transfer : [data]);
|
||
}
|
||
pushWithoutTransfer(data, keyTagInfo, initSegment, timeOffset, discontinuity, trackSwitch, contiguous, duration, accurateTimeOffset, defaultInitPTS, iframeMediaStart, iframeDuration) {
|
||
return this.push(data, keyTagInfo, initSegment, timeOffset, discontinuity, trackSwitch, contiguous, duration, accurateTimeOffset, defaultInitPTS, iframeMediaStart, iframeDuration, []);
|
||
}
|
||
destroy() {
|
||
this.observer.removeAllListeners();
|
||
this.rpc.invoke('demuxer.destroy', [this.demuxSessionID], []).subscribe();
|
||
}
|
||
}
|
||
|
||
const createRPCClients = (rpcService) => ({
|
||
crypto: new CryptoRPCClient(rpcService),
|
||
mux: new DemuxRPCClient(rpcService),
|
||
});
|
||
|
||
const LoggerRPCServer = (rpcService, logger) => {
|
||
rpcService.register('logger.log', (bindings, level, ...args) => (callback) => {
|
||
try {
|
||
for (const b of bindings) {
|
||
logger = logger.child(b);
|
||
}
|
||
if (typeof logger[level] === 'function') {
|
||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||
logger[level](...args);
|
||
}
|
||
callback();
|
||
}
|
||
catch (err) {
|
||
callback(undefined, err);
|
||
}
|
||
});
|
||
};
|
||
|
||
// Inline service treat the client and server running in the same process, and
|
||
// just forward messages directly by function calls.
|
||
class RPCInlineService {
|
||
constructor() {
|
||
this.handlers = {};
|
||
}
|
||
register(command, handler) {
|
||
if (this.handlers[command] != null) {
|
||
return false;
|
||
}
|
||
this.handlers[command] = handler;
|
||
}
|
||
unregister(command) {
|
||
if (this.handlers[command] != null) {
|
||
return false;
|
||
}
|
||
delete this.handlers[command];
|
||
}
|
||
invoke(command, args) {
|
||
return (callback = RPCInlineService._fallbackCallback) => {
|
||
try {
|
||
if (this.handlers[command] == null) {
|
||
throw new Error(`command ${command} not found`);
|
||
}
|
||
this.handlers[command](...args)(callback);
|
||
}
|
||
catch (error) {
|
||
callback(undefined, error);
|
||
}
|
||
};
|
||
}
|
||
teardown(done) {
|
||
this.handlers = null;
|
||
done();
|
||
}
|
||
}
|
||
RPCInlineService._fallbackCallback = (result, error) => {
|
||
if (error != null) {
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
const createRPCInlineService = (logger) => {
|
||
logger = logger.child({ name: 'InlineRPCService' });
|
||
const rpcService = new RPCInlineService();
|
||
new CryptoRPCServer(rpcService, logger);
|
||
new DemuxRPCServer(rpcService, logger);
|
||
logger.info('Inline RPCService has started');
|
||
return rpcService;
|
||
};
|
||
let worker;
|
||
const createRPCWorkerService = (logger) => {
|
||
if (!hasUMDWorker()) {
|
||
throw new Error('UMD WebWorker is not bundled in this file');
|
||
}
|
||
try {
|
||
if (worker == null) {
|
||
const blob = new Blob(['var exports = {};var module = { exports: exports };function define(f){f()};define.amd = true;(' + __HLS_UMD_BUNDLE__.toString() + ')(true);'], {
|
||
type: 'text/javascript',
|
||
});
|
||
const workerUrl = URL.createObjectURL(blob);
|
||
worker = new Worker(workerUrl);
|
||
}
|
||
const rpcService = new RPCWorkerService(worker);
|
||
LoggerRPCServer(rpcService, logger.child({ name: 'WorkerRPCService' }));
|
||
return rpcService;
|
||
}
|
||
catch (err) {
|
||
throw new Error('Failed to create WebWorker');
|
||
}
|
||
};
|
||
const createRPCServiceWithFallbacks = (...fallbacks) => (logger) => {
|
||
for (let i = 0; i < fallbacks.length; i++) {
|
||
const create = fallbacks[i];
|
||
try {
|
||
return create(logger);
|
||
}
|
||
catch (err) {
|
||
if (i === fallbacks.length - 1) {
|
||
throw err;
|
||
}
|
||
logger.warn(err);
|
||
}
|
||
}
|
||
};
|
||
const createRPCService = (logger) => {
|
||
return createRPCServiceWithFallbacks(createRPCWorkerService, createRPCInlineService)(logger);
|
||
};
|
||
|
||
/*
|
||
* RTC types
|
||
*
|
||
*
|
||
*/
|
||
/*
|
||
* Events that RTC sends out to the backend.
|
||
* https://confluence.sd.apple.com/display/ISM/Matchpoint+Event-Key+List
|
||
*/
|
||
class RTCEventGroup {
|
||
}
|
||
RTCEventGroup.PlayEnded = 6101;
|
||
RTCEventGroup.Periodic = 6110;
|
||
RTCEventGroup.PlayStalled = 6103;
|
||
RTCEventGroup.KeySessionComplete = 6104;
|
||
RTCEventGroup.PlayLikelyToKeepUp = 6105;
|
||
RTCEventGroup.PlayRateChanged = 6106;
|
||
RTCEventGroup.PlayError = 6107;
|
||
RTCEventGroup.MediaEngineStalled = 6108;
|
||
RTCEventGroup.SwitchComplete = 6109;
|
||
RTCEventGroup.VariantEnded = 6111;
|
||
RTCEventGroup.NwError = 6202;
|
||
const RTCVideoQualityIndex = {
|
||
avc1: 1,
|
||
avc3: 1,
|
||
hvc1: { SDR: 2, HLG: 10, PQ: 11 },
|
||
hev1: { SDR: 2, HLG: 10, PQ: 11 },
|
||
vp09: { SDR: 3, HLG: 14, PQ: 13 },
|
||
dvh1: { PQ: 12 },
|
||
};
|
||
|
||
const loggerName$5 = { name: 'rtc-component' };
|
||
class RTCComponent {
|
||
constructor(query, logger) {
|
||
this.query = query;
|
||
this.logger = logger;
|
||
}
|
||
setReportingAgent(reportingAgent) {
|
||
this.reportingAgent = reportingAgent;
|
||
}
|
||
sendPlayEnded(id) {
|
||
this.logger.debug(loggerName$5, `sendPlayEnded id=${id}`);
|
||
const method = RTCEventGroup.PlayEnded;
|
||
this.fillAndFire(method, this.query.playEnded(id));
|
||
}
|
||
sendPlayStalled(id) {
|
||
const method = RTCEventGroup.PlayStalled;
|
||
this.fillAndFire(method, this.query.playStalled(id));
|
||
}
|
||
sendMediaEngineStalled(id) {
|
||
const method = RTCEventGroup.MediaEngineStalled;
|
||
this.fillAndFire(method, this.query.mediaEngineStalled(id));
|
||
}
|
||
sendKeySessionComplete(id) {
|
||
const method = RTCEventGroup.KeySessionComplete;
|
||
this.fillAndFire(method, this.query.keySessionComplete(id));
|
||
}
|
||
sendPlayLikelyToKeepUp(id) {
|
||
const method = RTCEventGroup.PlayLikelyToKeepUp;
|
||
this.fillAndFire(method, this.query.playLikelyToKeepUp(id));
|
||
}
|
||
sendPlayRateChange(id) {
|
||
this.logger.debug(loggerName$5, `sendPlayRateChange id=${id}`);
|
||
const method = RTCEventGroup.PlayRateChanged;
|
||
this.fillAndFire(method, this.query.playRateChanged(id));
|
||
}
|
||
sendSwitchComplete(id) {
|
||
const method = RTCEventGroup.SwitchComplete;
|
||
this.fillAndFire(method, this.query.switchComplete(id));
|
||
}
|
||
sendVariantEnded(id) {
|
||
const method = RTCEventGroup.VariantEnded;
|
||
this.fillAndFire(method, this.query.variantEnded(id));
|
||
}
|
||
sendPlayError(id) {
|
||
const method = RTCEventGroup.PlayError;
|
||
this.fillAndFire(method, this.query.playError(id));
|
||
}
|
||
sendNwError(id) {
|
||
const method = RTCEventGroup.NwError;
|
||
this.fillAndFire(method, this.query.nwError(id));
|
||
}
|
||
sendPeriodic(id) {
|
||
const method = RTCEventGroup.Periodic;
|
||
this.fillAndFire(method, this.query.periodic(id));
|
||
}
|
||
fillAndFire(method, record) {
|
||
this.logger.debug(loggerName$5, 'RTC fillAndFire');
|
||
// don't send periodic event if there is no playtime or active fragment download time
|
||
if (method === RTCEventGroup.Periodic && !record.PlayTimeWC && !record.ADT) {
|
||
this.logger.info(loggerName$5, `RTC Method: ${method}, Skipping due to no playback/download activity`);
|
||
return;
|
||
}
|
||
const type = method === RTCEventGroup.PlayEnded || method === RTCEventGroup.Periodic ? 1 : 0;
|
||
this.logger.info(loggerName$5, `RTC Method: ${method}, Type:${type}`);
|
||
if (this.reportingAgent) {
|
||
this.logger.debug(loggerName$5, 'Reporting Agent is set!');
|
||
let eventPayload = {};
|
||
Object.entries(record).forEach(([key, value]) => {
|
||
// round all numeric values to two decimal points
|
||
if (isFiniteNumber(value)) {
|
||
value = Number(Number(value).toFixed(2));
|
||
}
|
||
// Extract keys from ServerInfo and populate the event payload; no other object types allowed. Ref: privacyAllowedLoadConfigHeaders
|
||
if (typeof value === 'object') {
|
||
if (key === 'ServerInfo') {
|
||
Object.entries(value).forEach(([k, v]) => {
|
||
eventPayload[k] = v;
|
||
});
|
||
}
|
||
else {
|
||
this.logger.debug(loggerName$5, `Object type unsupported for key: ${key}`);
|
||
}
|
||
}
|
||
else {
|
||
eventPayload[key] = value;
|
||
}
|
||
});
|
||
this.logger.debug(loggerName$5, 'RTC Method: %s, payload=%o', method, eventPayload);
|
||
// copy record into extensible payload
|
||
eventPayload = JSON.parse(JSON.stringify(eventPayload));
|
||
try {
|
||
this.reportingAgent.issueReportingEvent(method, eventPayload, type);
|
||
}
|
||
catch (err) {
|
||
this.logger.info(loggerName$5, 'Cannot report, reportingAgent failed');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
class RTCQuery extends QueryEntity {
|
||
constructor(rtcStore, logger) {
|
||
super(rtcStore);
|
||
this.logger = logger;
|
||
}
|
||
// generic getters
|
||
get activeEntity() {
|
||
return this.getActive();
|
||
}
|
||
entity(id) {
|
||
return this.getEntity(id);
|
||
}
|
||
playEnded(id) {
|
||
var _a;
|
||
return (_a = this.getEntity(id)) === null || _a === void 0 ? void 0 : _a.playEndedRecord;
|
||
}
|
||
periodic(id) {
|
||
var _a;
|
||
return (_a = this.getEntity(id)) === null || _a === void 0 ? void 0 : _a.periodicRecord;
|
||
}
|
||
playStalled(id) {
|
||
var _a;
|
||
return (_a = this.getEntity(id)) === null || _a === void 0 ? void 0 : _a.playStalledRecord;
|
||
}
|
||
mediaEngineStalled(id) {
|
||
var _a;
|
||
return (_a = this.getEntity(id)) === null || _a === void 0 ? void 0 : _a.mediaEngineStalledRecord;
|
||
}
|
||
keySessionComplete(id) {
|
||
var _a;
|
||
return (_a = this.getEntity(id)) === null || _a === void 0 ? void 0 : _a.keySessionCompleteRecord;
|
||
}
|
||
playLikelyToKeepUp(id) {
|
||
var _a;
|
||
return (_a = this.getEntity(id)) === null || _a === void 0 ? void 0 : _a.playLikelyToKeepUpRecord;
|
||
}
|
||
playRateChanged(id) {
|
||
var _a;
|
||
return (_a = this.getEntity(id)) === null || _a === void 0 ? void 0 : _a.playRateChangedRecord;
|
||
}
|
||
switchComplete(id) {
|
||
var _a;
|
||
return (_a = this.getEntity(id)) === null || _a === void 0 ? void 0 : _a.switchCompleteRecord;
|
||
}
|
||
variantEnded(id) {
|
||
var _a;
|
||
return (_a = this.getEntity(id)) === null || _a === void 0 ? void 0 : _a.variantEndedRecord;
|
||
}
|
||
playError(id) {
|
||
var _a;
|
||
return (_a = this.getEntity(id)) === null || _a === void 0 ? void 0 : _a.playErrorRecord;
|
||
}
|
||
nwError(id) {
|
||
var _a;
|
||
return (_a = this.getEntity(id)) === null || _a === void 0 ? void 0 : _a.nwErrorRecord;
|
||
}
|
||
}
|
||
|
||
class RTCStore extends EntityStore {
|
||
constructor(logger) {
|
||
super({}, { name: 'rtc-store', idKey: 'itemId', producerFn: produce_1, resettable: true });
|
||
this.logger = logger;
|
||
}
|
||
/*
|
||
* Create an RTC Entity with the necessary records
|
||
*/
|
||
createEntity(itemId) {
|
||
const rtcEntity = {
|
||
itemId,
|
||
sessionControlRecord: {
|
||
state: 'RTC_STATE_INIT',
|
||
rate: 0,
|
||
oldRate: 0,
|
||
eventStartTime: Date.now(),
|
||
sessionStartTime: Date.now(),
|
||
lastLikelyToKeepUpTime: Date.now(),
|
||
lastPeriodicTime: Date.now(),
|
||
playLikelyToKeepUpEventCounter: 1,
|
||
periodicEventCounter: 1,
|
||
activeKeySessions: {},
|
||
intervalVariantList: {},
|
||
sessionVariantList: {},
|
||
},
|
||
playEndedRecord: {},
|
||
periodicRecord: {},
|
||
playStalledRecord: {},
|
||
keySessionCompleteRecord: {},
|
||
playLikelyToKeepUpRecord: {},
|
||
playRateChangedRecord: {},
|
||
playErrorRecord: {},
|
||
mediaEngineStalledRecord: {},
|
||
switchCompleteRecord: {},
|
||
variantEndedRecord: {},
|
||
nwErrorRecord: {},
|
||
};
|
||
this.add(rtcEntity);
|
||
}
|
||
/*
|
||
* The below updates require an RTC event to be sent out, and hence an event payload needs to be prepared.
|
||
* => PlayEnded (6101)
|
||
* => Periodic (6110)
|
||
* => PlayStalled (6103)
|
||
* => KeySessionComplete (6104)
|
||
* => PlayLikelyToKeepUp (6105)
|
||
* => PlayRateChanged (6106)
|
||
* => PlayError (6107)
|
||
* => MediaEngineStalled (6108)
|
||
* => SwitchComplete (6109)
|
||
* => VariantEnded (6111)
|
||
* => NwError (6202)
|
||
*/
|
||
/* Indicates playback ended */
|
||
updateEnded(itemId) {
|
||
this._prepareEventPlayEnded(itemId);
|
||
}
|
||
/* Indicates periodic timer fired */
|
||
updatePeriodic(itemId, isFinal) {
|
||
this._prepareEventPeriodic(itemId, { isFinal });
|
||
}
|
||
/* Indicates playback stall due to bad network */
|
||
updateBufferStalled(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, variantEndedRecord, periodicRecord, playEndedRecord }) => {
|
||
sessionControlRecord.rate = 0;
|
||
variantEndedRecord.StallCount = (variantEndedRecord.StallCount || 0) + 1;
|
||
periodicRecord.StallCount = (periodicRecord.StallCount || 0) + 1;
|
||
playEndedRecord.StallCount = (playEndedRecord.StallCount || 0) + 1;
|
||
});
|
||
this._prepareEventPlayStalled(itemId, data);
|
||
}
|
||
/* Indicates identity key session success / failure */
|
||
updateSegmentKeyLoaded(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
const keySessionInfo = {};
|
||
const now = data.timestamp;
|
||
keySessionInfo.keyFormat = 'identity';
|
||
keySessionInfo.keyDeliveryTime = data.adt;
|
||
keySessionInfo.currentMediaTime = data.currentTime;
|
||
if (sessionControlRecord.state === 'RTC_STATE_INIT') {
|
||
keySessionInfo.keyInitTime = now - sessionControlRecord.sessionStartTime;
|
||
}
|
||
sessionControlRecord.activeKeySessions[data.keyuri] = keySessionInfo;
|
||
});
|
||
this._prepareEventKeySessionComplete(itemId, data);
|
||
}
|
||
/* Indicates non-identity (FPS/Widevine/PlayReady) key session success / failure events */
|
||
updateLicenseResponseProcessed(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
const keySessionInfo = sessionControlRecord.activeKeySessions[data.keyuri];
|
||
keySessionInfo.licenseResponseProcessTime = data.timestamp - keySessionInfo.licenseResponseSubmitTime;
|
||
sessionControlRecord.lastKeyDeliveryTime = keySessionInfo.keyDeliveryTime = data.timestamp - keySessionInfo.licenseChallengeStartTime;
|
||
keySessionInfo.currentMediaTime = data.currentTime;
|
||
sessionControlRecord.finishedKeyUri = data.keyuri;
|
||
});
|
||
this._prepareEventKeySessionComplete(itemId, data);
|
||
}
|
||
updateLicenseChallengeError(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
const keySessionInfo = sessionControlRecord.activeKeySessions[data.keyuri];
|
||
sessionControlRecord.lastKeyErrorType = keySessionInfo.keyErrorType = 'licenseChallengeError';
|
||
sessionControlRecord.finishedKeyUri = data.keyuri;
|
||
});
|
||
this._prepareEventKeySessionComplete(itemId, data);
|
||
}
|
||
updateLicenseResponseError(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
const keySessionInfo = sessionControlRecord.activeKeySessions[data.keyuri];
|
||
sessionControlRecord.lastKeyErrorType = keySessionInfo.keyErrorType = 'licenseResponseError';
|
||
sessionControlRecord.finishedKeyUri = data.keyuri;
|
||
});
|
||
this._prepareEventKeySessionComplete(itemId, data);
|
||
}
|
||
updateKeyAborted(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
const keySessionInfo = sessionControlRecord.activeKeySessions[data.keyuri];
|
||
keySessionInfo.keyErrorType = 'keyAborted';
|
||
sessionControlRecord.finishedKeyUri = data.keyuri;
|
||
});
|
||
this._prepareEventKeySessionComplete(itemId, data);
|
||
}
|
||
/* Indicates likely to keep up */
|
||
updateCanPlay(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
// if (sessionControlRecord.state === 'RTC_STATE_PLAY') {
|
||
// /* Ignore if we're already in PLAY */
|
||
// return;
|
||
// }
|
||
});
|
||
this._prepareEventPlayLikelyToKeepUp(itemId, data);
|
||
}
|
||
/* Indicates rate changed */
|
||
updateRateChanged(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
sessionControlRecord.rate = data.rate * 100;
|
||
if (data.rate > 0 && sessionControlRecord.oldRate === 0) {
|
||
if (!sessionControlRecord.playInfo) {
|
||
sessionControlRecord.playInfo = [];
|
||
}
|
||
sessionControlRecord.playInfo.push({ latency: data.latency });
|
||
// if curLevelUrl is not set at this point, we are in a scenario where the play event beats the levelSwitched event in a race
|
||
// conditon. To avoid a crash, set the curLevelUrl here.
|
||
if (!sessionControlRecord.curLevelUrl) {
|
||
sessionControlRecord.curLevelUrl = data.url;
|
||
}
|
||
}
|
||
});
|
||
this._prepareEventPlayRateChanged(itemId, data);
|
||
}
|
||
/* Indicates play error */
|
||
updateMediaError(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, periodicRecord, playEndedRecord }) => {
|
||
periodicRecord.PlayerErrCount = (periodicRecord.PlayerErrCount || 0) + 1;
|
||
playEndedRecord.PlayerErrCount = (playEndedRecord.PlayerErrCount || 0) + 1;
|
||
if (data.fatal) {
|
||
sessionControlRecord.rate = 0;
|
||
periodicRecord.FatalPlayerErrCount = (periodicRecord.FatalPlayerErrCount || 0) + 1;
|
||
playEndedRecord.FatalPlayerErrCount = (playEndedRecord.FatalPlayerErrCount || 0) + 1;
|
||
playEndedRecord.ErrCode = data.details;
|
||
playEndedRecord.ErrReason = data.code;
|
||
playEndedRecord.ErrIsFatal = true;
|
||
playEndedRecord.ErrDomain = 'mediaError';
|
||
}
|
||
});
|
||
if (data.fatal) {
|
||
this._prepareEventPlayError(itemId, data);
|
||
}
|
||
else {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
// Non-fatal errors won't be sent immediately for each occurrence. Instead, they will be sent before the periodic event with the count of occurrences.
|
||
// We send separate events if the error code and reason are different. For e.g. fragLoadError due to 403 and fragLoadError due to 404 are counted separately.
|
||
const key = data.details + '/' + data.code;
|
||
sessionControlRecord.nonFatalPlayErrList[key] = (sessionControlRecord.nonFatalPlayErrList[key] || 0) + 1;
|
||
});
|
||
}
|
||
}
|
||
/* Indicates media element error */
|
||
updateMediaElementError(itemId, data) {
|
||
/*
|
||
Parse the media element error message. Luna encodes ErrReason & ErrDetail as follows.
|
||
"message": '{
|
||
"ErrReason": -5001, // Luna error code
|
||
"ErrDetail": "0xce0defba" // Third party error code
|
||
}'
|
||
|
||
Note: mediaElementError is not inserted into the 6101 payload.
|
||
*/
|
||
let errJson;
|
||
try {
|
||
errJson = JSON.parse(data.message);
|
||
}
|
||
catch (error) {
|
||
this.logger.warn(`message is not JSON, ignoring; ${data.message}`);
|
||
}
|
||
const mediaElemReason = errJson ? parseInt(errJson['ErrReason']) : null;
|
||
const mediaElemDetail = errJson ? parseInt(errJson['ErrDetail']) : null;
|
||
this._prepareEventPlayError(itemId, {
|
||
domain: 'mediaElementError',
|
||
mediaElemCode: data.code,
|
||
mediaElemReason: mediaElemReason,
|
||
mediaElemDetail: mediaElemDetail,
|
||
// These are fixed values for mediaElementError and set in _prepareEventPlayError
|
||
code: null,
|
||
details: null,
|
||
fatal: null,
|
||
});
|
||
}
|
||
/* Indicates plaback stall due to media engine problem */
|
||
updateMediaEngineStalled(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, variantEndedRecord, periodicRecord, playEndedRecord }) => {
|
||
sessionControlRecord.rate = 0;
|
||
variantEndedRecord.MediaEngineStallCount = (variantEndedRecord.MediaEngineStallCount || 0) + 1;
|
||
periodicRecord.MediaEngineStallCount = (periodicRecord.MediaEngineStallCount || 0) + 1;
|
||
playEndedRecord.MediaEngineStallCount = (playEndedRecord.MediaEngineStallCount || 0) + 1;
|
||
});
|
||
this._prepareEventMediaEngineStalled(itemId, data);
|
||
}
|
||
/* Indicates level switch complete */
|
||
updateLevelSwitched(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, switchCompleteRecord, periodicRecord, playEndedRecord }) => {
|
||
periodicRecord.SwCnt = (periodicRecord.SwCnt || 0) + 1;
|
||
playEndedRecord.SwCnt = (playEndedRecord.SwCnt || 0) + 1;
|
||
const now = Date.now();
|
||
const prevLevelUrl = sessionControlRecord.curLevelUrl;
|
||
const curLevelUrl = data.url;
|
||
const prevVarInfo = this._getVariantInfo(prevLevelUrl, sessionControlRecord);
|
||
const curVarInfo = this._getVariantInfo(curLevelUrl, sessionControlRecord);
|
||
let curSwDir;
|
||
let iframes = false;
|
||
if (prevLevelUrl) {
|
||
if (curVarInfo.bandwidth < prevVarInfo.bandwidth) {
|
||
curSwDir = 'Down';
|
||
}
|
||
else {
|
||
curSwDir = 'Up';
|
||
}
|
||
iframes = curVarInfo.iframes;
|
||
}
|
||
switchCompleteRecord.BadSw = this._isBadSw(curSwDir, sessionControlRecord.lastSwitchDir || curSwDir, iframes, sessionControlRecord.lastLevelIsIframe || iframes, now, sessionControlRecord.lastSwitchTime || now, data.isSeeking);
|
||
sessionControlRecord.lastSwitchDir = curSwDir;
|
||
sessionControlRecord.lastSwitchTime = now;
|
||
sessionControlRecord.lastLevelIsIframe = iframes;
|
||
sessionControlRecord.curLevelUrl = curLevelUrl;
|
||
sessionControlRecord.variantStartTimeMedia = data.currentTime;
|
||
});
|
||
this._prepareEventSwitchComplete(itemId, data);
|
||
}
|
||
/* Indicates level switch error */
|
||
updateLevelLoadError(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, switchCompleteRecord, periodicRecord, playEndedRecord }) => {
|
||
const now = Date.now();
|
||
let curSwDir;
|
||
let iframes = false;
|
||
if (sessionControlRecord.curLevelUrl) {
|
||
const prevVarInfo = this._getVariantInfo(sessionControlRecord.curLevelUrl, sessionControlRecord);
|
||
const curVarInfo = this._getVariantInfo(data.url, sessionControlRecord);
|
||
if (curVarInfo.bandwidth < prevVarInfo.bandwidth) {
|
||
curSwDir = 'Down';
|
||
}
|
||
else {
|
||
curSwDir = 'Up';
|
||
}
|
||
iframes = curVarInfo.iframes;
|
||
}
|
||
periodicRecord.SwCnt = (periodicRecord.SwCnt || 0) + 1;
|
||
playEndedRecord.SwCnt = (playEndedRecord.SwCnt || 0) + 1;
|
||
switchCompleteRecord.BadSw = this._isBadSw(curSwDir, sessionControlRecord.lastSwitchDir || curSwDir, iframes, sessionControlRecord.lastLevelIsIframe || iframes, now, sessionControlRecord.lastSwitchTime || now, data.isSeeking);
|
||
switchCompleteRecord.SwFail = true;
|
||
});
|
||
this._prepareEventSwitchComplete(itemId, data);
|
||
}
|
||
/* Indicates variant end */
|
||
updateVariantEnd(itemId, data) {
|
||
this._prepareEventVariantEnded(itemId, data);
|
||
}
|
||
/* Indicates network error */
|
||
updateNwError(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, periodicRecord, playEndedRecord }) => {
|
||
periodicRecord.NwErrCount = (periodicRecord.NwErrCount || 0) + 1;
|
||
playEndedRecord.NwErrCount = (playEndedRecord.NwErrCount || 0) + 1;
|
||
if (data.fatal) {
|
||
sessionControlRecord.rate = 0;
|
||
periodicRecord.FatalNwErrCount = (periodicRecord.FatalNwErrCount || 0) + 1;
|
||
playEndedRecord.FatalNwErrCount = (playEndedRecord.FatalNwErrCount || 0) + 1;
|
||
playEndedRecord.ErrCode = data.details;
|
||
playEndedRecord.ErrReason = data.code;
|
||
playEndedRecord.ErrIsFatal = true;
|
||
playEndedRecord.ErrDomain = 'networkError';
|
||
}
|
||
});
|
||
if (data.fatal) {
|
||
this._prepareEventNwError(itemId, data);
|
||
}
|
||
else {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
// Non-fatal errors won't be sent immediately for each occurrence. Instead, they will be sent before the periodic event with the count of occurrences.
|
||
// We send separate events if the error code and reason are different. For e.g. fragLoadError due to 403 and fragLoadError due to 404 are counted separately.
|
||
const key = data.details + '/' + data.code;
|
||
sessionControlRecord.nonFatalNwErrList[key] = (sessionControlRecord.nonFatalNwErrList[key] || 0) + 1;
|
||
});
|
||
}
|
||
}
|
||
/*
|
||
* The below method is called after an event is sent out, to update the internal states.
|
||
*/
|
||
finalize(itemId, event) {
|
||
switch (event) {
|
||
case RTCEventGroup.PlayEnded: {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
sessionControlRecord.state = 'RTC_STATE_STOP';
|
||
sessionControlRecord.oldRate = 0;
|
||
});
|
||
/* Reset the event record */
|
||
this.update(itemId, (state) => {
|
||
state.playEndedRecord = {};
|
||
});
|
||
break;
|
||
}
|
||
case RTCEventGroup.Periodic: {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
sessionControlRecord.lastPeriodicTime = Date.now();
|
||
sessionControlRecord.periodicEventCounter += 1;
|
||
/* Re-initialize playtime for all levels */
|
||
const levels = Object.keys(sessionControlRecord.intervalVariantList);
|
||
levels.forEach((level) => {
|
||
sessionControlRecord.intervalVariantList[level].playTime = 0;
|
||
});
|
||
});
|
||
/* Reset the event record */
|
||
this.update(itemId, (state) => {
|
||
state.periodicRecord = {};
|
||
});
|
||
break;
|
||
}
|
||
case RTCEventGroup.PlayStalled: {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
sessionControlRecord.state = 'RTC_STATE_STALL';
|
||
sessionControlRecord.oldRate = 0;
|
||
});
|
||
/* Reset the event record */
|
||
this.update(itemId, (state) => {
|
||
state.playStalledRecord = {};
|
||
});
|
||
break;
|
||
}
|
||
case RTCEventGroup.KeySessionComplete: {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
delete sessionControlRecord.activeKeySessions[sessionControlRecord.finishedKeyUri];
|
||
});
|
||
/* Reset the event record */
|
||
this.update(itemId, (state) => {
|
||
state.keySessionCompleteRecord = {};
|
||
});
|
||
break;
|
||
}
|
||
case RTCEventGroup.PlayLikelyToKeepUp: {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
if (sessionControlRecord.state !== 'RTC_STATE_PLAY') {
|
||
/* Ignore if we're already in PLAY */
|
||
sessionControlRecord.state = 'RTC_STATE_CANPLAY';
|
||
sessionControlRecord.lastLikelyToKeepUpTime = Date.now();
|
||
sessionControlRecord.playLikelyToKeepUpEventCounter += 1;
|
||
}
|
||
});
|
||
/* Reset the event record */
|
||
this.update(itemId, (state) => {
|
||
state.playLikelyToKeepUpRecord = {};
|
||
});
|
||
break;
|
||
}
|
||
case RTCEventGroup.PlayRateChanged: {
|
||
this.update(itemId, ({ sessionControlRecord, playEndedRecord, playStalledRecord, mediaEngineStalledRecord, playErrorRecord, nwErrorRecord }) => {
|
||
if (sessionControlRecord.rate !== 0) {
|
||
sessionControlRecord.state = 'RTC_STATE_PLAY';
|
||
// reset the below keys as we resume playback
|
||
delete playEndedRecord.LastStall;
|
||
delete playEndedRecord.LastMediaEngineStall;
|
||
delete playEndedRecord.LastPause;
|
||
delete playErrorRecord.LastPause;
|
||
delete nwErrorRecord.LastPause;
|
||
}
|
||
else {
|
||
sessionControlRecord.state = 'RTC_STATE_PAUSE';
|
||
// reset the below keys as we pause playback
|
||
delete playStalledRecord.LastResume;
|
||
delete mediaEngineStalledRecord.LastResume;
|
||
delete playErrorRecord.LastResume;
|
||
delete nwErrorRecord.LastResume;
|
||
}
|
||
/* Update the old rate */
|
||
sessionControlRecord.oldRate = sessionControlRecord.rate;
|
||
});
|
||
/* Reset the event record */
|
||
this.update(itemId, (state) => {
|
||
state.playRateChangedRecord = {};
|
||
});
|
||
break;
|
||
}
|
||
case RTCEventGroup.PlayError: {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
sessionControlRecord.state = 'RTC_STATE_PLAYERROR';
|
||
});
|
||
/* Reset the event record */
|
||
this.update(itemId, (state) => {
|
||
state.playErrorRecord = {};
|
||
});
|
||
break;
|
||
}
|
||
case RTCEventGroup.MediaEngineStalled: {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
sessionControlRecord.state = 'RTC_STATE_MEDIAENGINESTALL';
|
||
sessionControlRecord.oldRate = 0;
|
||
});
|
||
/* Reset the event record */
|
||
this.update(itemId, (state) => {
|
||
state.mediaEngineStalledRecord = {};
|
||
});
|
||
break;
|
||
}
|
||
case RTCEventGroup.SwitchComplete: {
|
||
/* Reset the event record */
|
||
this.update(itemId, (state) => {
|
||
state.switchCompleteRecord = {};
|
||
// record the level transition
|
||
state.sessionControlRecord.prevLevelUrl = state.sessionControlRecord.curLevelUrl;
|
||
});
|
||
break;
|
||
}
|
||
case RTCEventGroup.VariantEnded: {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
sessionControlRecord.decodedFramesForVariant = 0;
|
||
sessionControlRecord.decodedFramesForVariantSampleCount = 0;
|
||
});
|
||
/* Reset the event record */
|
||
this.update(itemId, (state) => {
|
||
state.variantEndedRecord = {};
|
||
});
|
||
break;
|
||
}
|
||
case RTCEventGroup.NwError: {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
sessionControlRecord.state = 'RTC_STATE_NWERROR';
|
||
});
|
||
/* Reset the event record */
|
||
this.update(itemId, (state) => {
|
||
state.nwErrorRecord = {};
|
||
});
|
||
break;
|
||
}
|
||
}
|
||
/* Finally, reset the event clock */
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
sessionControlRecord.eventStartTime = Date.now();
|
||
});
|
||
}
|
||
/*
|
||
* The below updates don't involve an RTC event to be sent out.
|
||
* These updates just aggregate data in the store for various RTC records.
|
||
*/
|
||
updatePlaybackInfo(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, periodicRecord, playEndedRecord }) => {
|
||
if (data.droppedVideoFrames < sessionControlRecord.droppedVideoFrames) {
|
||
// MSE counter reset, so reset ours too
|
||
sessionControlRecord.droppedVideoFrames = 0;
|
||
}
|
||
if (data.decodedFrameCount < sessionControlRecord.decodedFrameCount) {
|
||
// MSE counter reset, so reset ours too
|
||
sessionControlRecord.decodedFrameCount = 0;
|
||
}
|
||
// we get the dropped frames and decoded frames from the MSE object every second. Calculate the current frame drop and frame rate.
|
||
const curFrameDrop = data.droppedVideoFrames - (sessionControlRecord.droppedVideoFrames || 0);
|
||
const decodedFramesPerSec = data.decodedFrameCount - (sessionControlRecord.decodedFrameCount || 0);
|
||
sessionControlRecord.droppedVideoFrames = data.droppedVideoFrames;
|
||
sessionControlRecord.decodedFrameCount = data.decodedFrameCount;
|
||
// update the decode frames for the current variant to compute average decode frame count for the variant
|
||
sessionControlRecord.decodedFramesForVariant += decodedFramesPerSec;
|
||
sessionControlRecord.decodedFramesForVariantSampleCount += 1;
|
||
if (curFrameDrop) {
|
||
// if we drop 3 or more frames per second, then that's a group frame drop.
|
||
if (curFrameDrop >= 3) {
|
||
periodicRecord.GroupViFrDr = (periodicRecord.GroupViFrDr || 0) + curFrameDrop;
|
||
periodicRecord.GroupViFrDrEvtCount = (periodicRecord.GroupViFrDrEvtCount || 0) + 1;
|
||
playEndedRecord.GroupViFrDr = (playEndedRecord.GroupViFrDr || 0) + curFrameDrop;
|
||
playEndedRecord.GroupViFrDrEvtCount = (playEndedRecord.GroupViFrDrEvtCount || 0) + 1;
|
||
}
|
||
else {
|
||
periodicRecord.SparseViFrDr = (periodicRecord.SparseViFrDr || 0) + curFrameDrop;
|
||
periodicRecord.SparseViFrDrEvtCount = (periodicRecord.SparseViFrDrEvtCount || 0) + 1;
|
||
playEndedRecord.SparseViFrDr = (playEndedRecord.SparseViFrDr || 0) + curFrameDrop;
|
||
playEndedRecord.SparseViFrDrEvtCount = (playEndedRecord.SparseViFrDrEvtCount || 0) + 1;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
updateBufferAppended(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
if (!sessionControlRecord.bufferAppendInfo) {
|
||
sessionControlRecord.bufferAppendInfo = [];
|
||
}
|
||
sessionControlRecord.bufferAppendInfo.push(data);
|
||
});
|
||
}
|
||
updateSeeked(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
if (!sessionControlRecord.seekInfo) {
|
||
sessionControlRecord.seekInfo = [];
|
||
}
|
||
sessionControlRecord.seekInfo.push(data);
|
||
});
|
||
}
|
||
updateManifestParsed(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, periodicRecord, playEndedRecord }) => {
|
||
periodicRecord.MasterPlaylistADT = (periodicRecord.MasterPlaylistADT || 0) + data.adt;
|
||
playEndedRecord.MasterPlaylistADT = (playEndedRecord.MasterPlaylistADT || 0) + data.adt;
|
||
const result = this._computeVariantInfo(data.levels);
|
||
// update the common keys
|
||
playEndedRecord.IsAudioOnly = data.isAudioOnly;
|
||
playEndedRecord.IsGapless = data.isGapless;
|
||
playEndedRecord.IsFirstItem = data.isFirstItem;
|
||
playEndedRecord.ItemID = data.itemID;
|
||
playEndedRecord.MaxVideoQltyIndex = result.maxVideoQltyIndex;
|
||
playEndedRecord.MaxReWd = result.maxWidth;
|
||
playEndedRecord.MaxReHt = result.maxHeight;
|
||
sessionControlRecord.manifestData = { variantList: result.variantList, varListString: result.varListString };
|
||
});
|
||
}
|
||
updateFragLoaded(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, periodicRecord, playEndedRecord }) => {
|
||
periodicRecord.MediaRequestsSent = (periodicRecord.MediaRequestsSent || 0) + 1;
|
||
playEndedRecord.MediaRequestsSent = (playEndedRecord.MediaRequestsSent || 0) + 1;
|
||
if (data.cdnServer) {
|
||
const cdnServer = data.cdnServer.toLowerCase();
|
||
if (cdnServer === 'aapl' || cdnServer === 'akam' || cdnServer === 'llnw') {
|
||
playEndedRecord.LastMediaCDNServer = cdnServer;
|
||
}
|
||
}
|
||
if (data.serverInfo) {
|
||
playEndedRecord.ServerInfo = data.serverInfo;
|
||
}
|
||
if (!sessionControlRecord.segmentMimeTypes) {
|
||
sessionControlRecord.segmentMimeTypes = [];
|
||
}
|
||
if (sessionControlRecord.segmentMimeTypes.find((mime) => mime == data.contentType) === undefined) {
|
||
sessionControlRecord.segmentMimeTypes.push(data.contentType);
|
||
}
|
||
if (data.fragType === MediaOptionType.Variant) {
|
||
// accumulate the 'main' (muxed) / 'video' (unmuxed) bytes & duration to calculate AvgVideoBitrate
|
||
sessionControlRecord.variantVideoBytes = (sessionControlRecord.variantVideoBytes || 0) + data.bytes;
|
||
sessionControlRecord.variantVideoDuration = (sessionControlRecord.variantVideoDuration || 0) + data.duration;
|
||
sessionControlRecord.intervalVideoBytes = (sessionControlRecord.intervalVideoBytes || 0) + data.bytes;
|
||
sessionControlRecord.intervalVideoDuration = (sessionControlRecord.intervalVideoDuration || 0) + data.duration;
|
||
sessionControlRecord.sessionVideoBytes = (sessionControlRecord.sessionVideoBytes || 0) + data.bytes;
|
||
sessionControlRecord.sessionVideoDuration = (sessionControlRecord.sessionVideoDuration || 0) + data.duration;
|
||
sessionControlRecord.obrLast = (data.bytes * 8) / (data.duration / 1000);
|
||
/* Update periodic record */
|
||
periodicRecord.NetBytes = (periodicRecord.NetBytes || 0) + data.bytes;
|
||
periodicRecord.ADT = (periodicRecord.ADT || 0) + data.adt;
|
||
periodicRecord.SegmentProcessTime = (periodicRecord.SegmentProcessTime || 0) + data.processTime;
|
||
/* Update playEnded record */
|
||
playEndedRecord.ADT = (playEndedRecord.ADT || 0) + data.adt;
|
||
playEndedRecord.NetBytes = (playEndedRecord.NetBytes || 0) + data.bytes;
|
||
playEndedRecord.SegmentProcessTime = (playEndedRecord.SegmentProcessTime || 0) + data.processTime;
|
||
}
|
||
else if (data.fragType === MediaOptionType.AltAudio) {
|
||
// accumulate the 'audio' bytes & duration to calculate AvgAudioBitrate
|
||
sessionControlRecord.variantAudioBytes = (sessionControlRecord.variantAudioBytes || 0) + data.bytes;
|
||
sessionControlRecord.variantAudioDuration = (sessionControlRecord.variantAudioDuration || 0) + data.duration;
|
||
sessionControlRecord.intervalAudioBytes = (sessionControlRecord.intervalAudioBytes || 0) + data.bytes;
|
||
sessionControlRecord.intervalAudioDuration = (sessionControlRecord.intervalAudioDuration || 0) + data.duration;
|
||
sessionControlRecord.sessionAudioBytes = (sessionControlRecord.sessionAudioBytes || 0) + data.bytes;
|
||
sessionControlRecord.sessionAudioDuration = (sessionControlRecord.sessionAudioDuration || 0) + data.duration;
|
||
}
|
||
});
|
||
}
|
||
updateFragBuffered(itemId, data) {
|
||
this.update(itemId, ({ periodicRecord, playEndedRecord }) => {
|
||
// use only 'main' (muxed) / 'video' (unmuxed) data to calculate below keys
|
||
if (data.fragType === MediaOptionType.Variant) {
|
||
/* Update periodic record */
|
||
periodicRecord.SegmentParseTime = (periodicRecord.SegmentParseTime || 0) + data.parseTime;
|
||
/* Update playEnded record */
|
||
playEndedRecord.SegmentParseTime = (playEndedRecord.SegmentParseTime || 0) + data.parseTime;
|
||
}
|
||
});
|
||
}
|
||
updateLevelLoaded(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, playLikelyToKeepUpRecord, periodicRecord, playEndedRecord }) => {
|
||
playLikelyToKeepUpRecord.PlaylistADT = (playLikelyToKeepUpRecord.PlaylistADT || 0) + data.adt;
|
||
periodicRecord.PlaylistADT = (periodicRecord.PlaylistADT || 0) + data.adt;
|
||
playEndedRecord.PlaylistADT = (playEndedRecord.PlaylistADT || 0) + data.adt;
|
||
periodicRecord.MaxPlaylistDT = data.adt > periodicRecord.MaxPlaylistDT ? data.adt : periodicRecord.MaxPlaylistDT;
|
||
playEndedRecord.MaxPlaylistDT = data.adt > playEndedRecord.MaxPlaylistDT ? data.adt : playEndedRecord.MaxPlaylistDT;
|
||
playEndedRecord.PlayType = data.playType;
|
||
this._setTargetDuration(data.url, data.targetduration, sessionControlRecord);
|
||
if (!sessionControlRecord.playlistMimeTypes) {
|
||
sessionControlRecord.playlistMimeTypes = [];
|
||
}
|
||
if (sessionControlRecord.playlistMimeTypes.find((mime) => mime == data.contentType) === undefined) {
|
||
sessionControlRecord.playlistMimeTypes.push(data.contentType);
|
||
}
|
||
const curLevelUrl = data.url;
|
||
const curVarInfo = this._getVariantInfo(curLevelUrl, sessionControlRecord);
|
||
// Initialize the variant info if it's not seen before
|
||
if (!sessionControlRecord.intervalVariantList[curLevelUrl]) {
|
||
sessionControlRecord.intervalVariantList[curLevelUrl] = Object.assign({}, curVarInfo);
|
||
}
|
||
if (!sessionControlRecord.sessionVariantList[curLevelUrl]) {
|
||
sessionControlRecord.sessionVariantList[curLevelUrl] = Object.assign({}, curVarInfo);
|
||
}
|
||
});
|
||
}
|
||
updateLevelsChanged(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, playEndedRecord }) => {
|
||
const result = this._computeVariantInfo(data.levels);
|
||
playEndedRecord.MaxVideoQltyIndex = result.maxVideoQltyIndex;
|
||
playEndedRecord.MaxReWd = result.maxWidth;
|
||
playEndedRecord.MaxReHt = result.maxHeight;
|
||
sessionControlRecord.manifestData = { variantList: result.variantList, varListString: result.varListString };
|
||
// Update bitrate rank in sessionVariantList and intervalVariantList
|
||
Object.keys(result.variantList).forEach((url) => {
|
||
if (sessionControlRecord.intervalVariantList[url]) {
|
||
sessionControlRecord.intervalVariantList[url].brRnk = result.variantList[url].brRnk;
|
||
}
|
||
if (sessionControlRecord.sessionVariantList[url]) {
|
||
sessionControlRecord.sessionVariantList[url].brRnk = result.variantList[url].brRnk;
|
||
}
|
||
});
|
||
});
|
||
}
|
||
updateLicenseChallengeRequested(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
const keySessionInfo = {};
|
||
const now = data.timestamp;
|
||
keySessionInfo.licenseChallengeStartTime = now;
|
||
// send the keyInitTime if we're initializing playback
|
||
if (sessionControlRecord.state === 'RTC_STATE_INIT') {
|
||
keySessionInfo.keyInitTime = now - sessionControlRecord.sessionStartTime;
|
||
}
|
||
sessionControlRecord.activeKeySessions[data.keyuri] = keySessionInfo;
|
||
});
|
||
}
|
||
updateLicenseChallengeReceived(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
const keySessionInfo = sessionControlRecord.activeKeySessions[data.keyuri];
|
||
keySessionInfo.licenseChallengeRequestTime = data.timestamp - keySessionInfo.licenseChallengeStartTime;
|
||
});
|
||
}
|
||
updateLicenseChallengeSubmitted(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
const keySessionInfo = sessionControlRecord.activeKeySessions[data.keyuri];
|
||
keySessionInfo.keyFormat = data.keyFormat;
|
||
keySessionInfo.licenseChallengeSubmitTime = data.timestamp;
|
||
});
|
||
}
|
||
updateLicenseChallengeCreated(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
const keySessionInfo = sessionControlRecord.activeKeySessions[data.keyuri];
|
||
keySessionInfo.cdmVersion = data.cdmVersion;
|
||
keySessionInfo.licenseChallengeCreationTime = data.timestamp - keySessionInfo.licenseChallengeSubmitTime;
|
||
});
|
||
}
|
||
updateLicenseResponseRequested(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
const keySessionInfo = sessionControlRecord.activeKeySessions[data.keyuri];
|
||
keySessionInfo.licenseResponseRequestTime = data.timestamp;
|
||
});
|
||
}
|
||
updateLicenseResponseReceived(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
const keySessionInfo = sessionControlRecord.activeKeySessions[data.keyuri];
|
||
keySessionInfo.licenseResponseReceiveTime = data.timestamp - keySessionInfo.licenseResponseRequestTime;
|
||
});
|
||
}
|
||
updateLicenseResponseSubmitted(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord }) => {
|
||
const keySessionInfo = sessionControlRecord.activeKeySessions[data.keyuri];
|
||
keySessionInfo.licenseResponseSubmitTime = data.timestamp;
|
||
});
|
||
}
|
||
/*
|
||
* Private Methods
|
||
*/
|
||
_prepareEventPlayEnded(itemId) {
|
||
this.update(itemId, ({ sessionControlRecord, playEndedRecord }) => {
|
||
var _a;
|
||
playEndedRecord.AvgVideoBitrate = ((sessionControlRecord.sessionVideoBytes || 0) * 8) / (sessionControlRecord.sessionVideoDuration || 1);
|
||
playEndedRecord.AvgAudioBitrate = ((sessionControlRecord.sessionAudioBytes || 0) * 8) / (sessionControlRecord.sessionAudioDuration || 1);
|
||
// round off NetBytes to the nearest 1000 to obscure the content size - as requested by privacy team.
|
||
const netBytes = Math.round((playEndedRecord.NetBytes || 0) / 1000) * 1000;
|
||
playEndedRecord.NetBytes = netBytes;
|
||
const adt = playEndedRecord.ADT || 1;
|
||
const processTime = playEndedRecord.ADT + playEndedRecord.SegmentProcessTime || 1;
|
||
const parseTime = playEndedRecord.ADT + playEndedRecord.SegmentProcessTime + playEndedRecord.SegmentParseTime || 1;
|
||
playEndedRecord.TWOBR = (netBytes * 8) / (adt / 1000);
|
||
playEndedRecord.PerceivedTWOBR = (netBytes * 8) / (processTime / 1000);
|
||
playEndedRecord.NetTWOBR = (netBytes * 8) / (parseTime / 1000);
|
||
// Use the playtime for the current level too, before generating time weighted values
|
||
const twVal = this._findTimeWeightedValues(sessionControlRecord.state === 'RTC_STATE_PLAY' ? Date.now() - sessionControlRecord.eventStartTime : 0, sessionControlRecord.sessionVariantList, sessionControlRecord.curLevelUrl);
|
||
playEndedRecord.PlayerTWIBR = twVal.twIBR;
|
||
playEndedRecord.PlayerTWIABR = twVal.twIABR;
|
||
playEndedRecord.TWBitRk = twVal.twBRnk;
|
||
const varInfo = this._getVariantInfo(sessionControlRecord.curLevelUrl, sessionControlRecord);
|
||
playEndedRecord.TargetDur = varInfo.targetduration;
|
||
playEndedRecord.ReWd = varInfo.width;
|
||
playEndedRecord.ReHt = varInfo.height;
|
||
playEndedRecord.VariantList = (_a = sessionControlRecord.manifestData) === null || _a === void 0 ? void 0 : _a.varListString;
|
||
let playlistMimeType = '';
|
||
if (sessionControlRecord.playlistMimeTypes) {
|
||
sessionControlRecord.playlistMimeTypes.forEach((item) => {
|
||
playlistMimeType += item + ',';
|
||
});
|
||
playlistMimeType = playlistMimeType.slice(0, -1); // remove the trailing comma
|
||
}
|
||
let segmentMimeType = '';
|
||
if (sessionControlRecord.segmentMimeTypes) {
|
||
sessionControlRecord.segmentMimeTypes.forEach((item) => {
|
||
segmentMimeType += item + ',';
|
||
});
|
||
segmentMimeType = segmentMimeType.slice(0, -1); // remove the trailing comma
|
||
}
|
||
playEndedRecord.PlaylistMimeType = playlistMimeType;
|
||
playEndedRecord.SegmentMimeType = segmentMimeType;
|
||
playEndedRecord.Rate = sessionControlRecord.rate;
|
||
});
|
||
this._aggregateTimes(itemId);
|
||
}
|
||
_prepareEventPeriodic(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, periodicRecord, playEndedRecord }) => {
|
||
var _a;
|
||
periodicRecord.EventCounter = sessionControlRecord.periodicEventCounter;
|
||
periodicRecord.PInterval = Date.now() - sessionControlRecord.lastPeriodicTime;
|
||
periodicRecord.AvgVideoBitrate = ((sessionControlRecord.intervalVideoBytes || 0) * 8) / (sessionControlRecord.intervalVideoDuration || 1);
|
||
periodicRecord.AvgAudioBitrate = ((sessionControlRecord.intervalAudioBytes || 0) * 8) / (sessionControlRecord.intervalAudioDuration || 1);
|
||
let netBytes = periodicRecord.NetBytes || 0;
|
||
if (data.isFinal) {
|
||
// round off NetBytes to the nearest 1000 to obscure the content size - as requested by privacy team. (for periodic event, we need this only for the last event)
|
||
periodicRecord.NetBytes = netBytes = Math.round(netBytes / 1000) * 1000;
|
||
}
|
||
const adt = periodicRecord.ADT || 1;
|
||
const processTime = periodicRecord.ADT + periodicRecord.SegmentProcessTime || 1;
|
||
const parseTime = periodicRecord.ADT + periodicRecord.SegmentProcessTime + periodicRecord.SegmentParseTime || 1;
|
||
periodicRecord.TWOBR = (netBytes * 8) / (adt / 1000);
|
||
periodicRecord.PerceivedTWOBR = (netBytes * 8) / (processTime / 1000);
|
||
periodicRecord.NetTWOBR = (netBytes * 8) / (parseTime / 1000);
|
||
// Use the playtime for the current level too, before generating time weighted values
|
||
const twVal = this._findTimeWeightedValues(sessionControlRecord.state === 'RTC_STATE_PLAY' ? Date.now() - sessionControlRecord.eventStartTime : 0, sessionControlRecord.intervalVariantList, sessionControlRecord.curLevelUrl);
|
||
periodicRecord.PlayerTWIBR = twVal.twIBR;
|
||
periodicRecord.PlayerTWIABR = twVal.twIABR;
|
||
periodicRecord.TWBitRk = twVal.twBRnk;
|
||
const varInfo = this._getVariantInfo(sessionControlRecord.curLevelUrl, sessionControlRecord);
|
||
periodicRecord.TargetDur = varInfo.targetduration;
|
||
periodicRecord.ReWd = varInfo.width;
|
||
periodicRecord.ReHt = varInfo.height;
|
||
periodicRecord.VariantList = (_a = sessionControlRecord.manifestData) === null || _a === void 0 ? void 0 : _a.varListString;
|
||
periodicRecord.Rate = sessionControlRecord.rate;
|
||
const stats = this._computeMediaStats(sessionControlRecord);
|
||
if (stats) {
|
||
periodicRecord.MedianBufferAppendLatency = stats.bufLatencyInfo.median;
|
||
periodicRecord.MaxBufferAppendLatency = stats.bufLatencyInfo.max;
|
||
periodicRecord.MedianBufferAppendSize = stats.bufSizeInfo.median;
|
||
periodicRecord.MaxBufferAppendSize = stats.bufSizeInfo.max;
|
||
periodicRecord.MedianSeekLatency = stats.seekLatencyInfo.median;
|
||
periodicRecord.MaxSeekLatency = stats.seekLatencyInfo.max;
|
||
periodicRecord.MedianPlayLatency = stats.playLatencyInfo.median;
|
||
periodicRecord.MaxPlayLatency = stats.playLatencyInfo.max;
|
||
}
|
||
this._copyCommonKeys(periodicRecord, playEndedRecord);
|
||
});
|
||
this._aggregateTimes(itemId);
|
||
}
|
||
_prepareEventPlayStalled(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, playStalledRecord, playEndedRecord }) => {
|
||
const varInfo = this._getVariantInfo(sessionControlRecord.curLevelUrl, sessionControlRecord);
|
||
const now = Date.now();
|
||
playStalledRecord.MediaDur = data.mediaDur;
|
||
playStalledRecord.BitRnk = varInfo.brRnk;
|
||
playStalledRecord.Codecs = varInfo.codecs;
|
||
playStalledRecord.LastLikelyToKeepUp = now - sessionControlRecord.lastLikelyToKeepUpTime;
|
||
playStalledRecord.LastSwitch = now - sessionControlRecord.lastSwitchTime;
|
||
playStalledRecord.StallDetectionTime = data.stallDurationMs;
|
||
playStalledRecord.StallType = data.type;
|
||
playStalledRecord.BufferLength = data.bufferLen;
|
||
if (playEndedRecord.ErrDomain === 'networkError') {
|
||
playStalledRecord.NwErrTime = playEndedRecord.NwErrTime;
|
||
playStalledRecord.NwErrCode = playEndedRecord.ErrCode;
|
||
}
|
||
playStalledRecord.LaSwDir = sessionControlRecord.lastSwitchDir;
|
||
playStalledRecord.TargetDur = varInfo.targetduration;
|
||
playStalledRecord.PlayerIABR = varInfo.avgBandwidth;
|
||
playStalledRecord.PlayerIBR = varInfo.bandwidth;
|
||
playStalledRecord.Rate = sessionControlRecord.rate;
|
||
playStalledRecord.OBRLast = sessionControlRecord.obrLast;
|
||
playStalledRecord.OBRMean = (playEndedRecord.NetBytes * 8) / (playEndedRecord.ADT / 1000);
|
||
this._copyCommonKeys(playStalledRecord, playEndedRecord);
|
||
});
|
||
this._aggregateTimes(itemId);
|
||
}
|
||
_prepareEventKeySessionComplete(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, keySessionCompleteRecord, playEndedRecord }) => {
|
||
const keySessionInfo = sessionControlRecord.activeKeySessions[data.keyuri];
|
||
if (!keySessionInfo) {
|
||
this.logger.warn(`_prepareEventKeySessionComplete no keySessionInfo for ${redactUrl(data.keyuri)}`);
|
||
return;
|
||
}
|
||
keySessionCompleteRecord.KeyFormat = keySessionInfo.keyFormat;
|
||
keySessionCompleteRecord.CDMVersion = keySessionInfo.cdmVersion;
|
||
keySessionCompleteRecord.LicenseChallengeRequestTime = keySessionInfo.licenseChallengeRequestTime;
|
||
keySessionCompleteRecord.LicenseChallengeCreationTime = keySessionInfo.licenseChallengeCreationTime;
|
||
keySessionCompleteRecord.LicenseResponseReceiveTime = keySessionInfo.licenseResponseReceiveTime;
|
||
keySessionCompleteRecord.LicenseResponseProcessTime = keySessionInfo.licenseResponseProcessTime;
|
||
keySessionCompleteRecord.KeyDeliveryTime = keySessionInfo.keyDeliveryTime;
|
||
keySessionCompleteRecord.CurrentMediaTime = keySessionInfo.currentMediaTime * 1000;
|
||
keySessionCompleteRecord.KeyInitTime = keySessionInfo.keyInitTime;
|
||
keySessionCompleteRecord.KeyErrorType = keySessionInfo.keyErrorType;
|
||
this._copyCommonKeys(keySessionCompleteRecord, playEndedRecord);
|
||
});
|
||
this._aggregateTimes(itemId);
|
||
}
|
||
_prepareEventPlayLikelyToKeepUp(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, playLikelyToKeepUpRecord, playEndedRecord }) => {
|
||
const varInfo = this._getVariantInfo(sessionControlRecord.curLevelUrl, sessionControlRecord);
|
||
playLikelyToKeepUpRecord.EventCounter = sessionControlRecord.playLikelyToKeepUpEventCounter;
|
||
playLikelyToKeepUpRecord.PlayerIABR = varInfo.avgBandwidth;
|
||
playLikelyToKeepUpRecord.PlayerIBR = varInfo.bandwidth;
|
||
playLikelyToKeepUpRecord.MediaDur = data.mediaDur;
|
||
playLikelyToKeepUpRecord.BitRnk = varInfo.brRnk;
|
||
playLikelyToKeepUpRecord.Codecs = varInfo.codecs;
|
||
playLikelyToKeepUpRecord.TargetDur = varInfo.targetduration;
|
||
this._copyCommonKeys(playLikelyToKeepUpRecord, playEndedRecord);
|
||
});
|
||
this._aggregateTimes(itemId);
|
||
}
|
||
_prepareEventPlayRateChanged(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, playRateChangedRecord, playEndedRecord }) => {
|
||
const varInfo = this._getVariantInfo(sessionControlRecord.curLevelUrl, sessionControlRecord);
|
||
playRateChangedRecord.Rate = sessionControlRecord.rate;
|
||
playRateChangedRecord.StNPT = data.currentTime;
|
||
playRateChangedRecord.PlayerIABR = varInfo.avgBandwidth;
|
||
playRateChangedRecord.PlayerIBR = varInfo.bandwidth;
|
||
playRateChangedRecord.BitRnk = varInfo.brRnk;
|
||
playRateChangedRecord.MediaDur = data.mediaDur;
|
||
playRateChangedRecord.Codecs = varInfo.codecs;
|
||
this._copyCommonKeys(playRateChangedRecord, playEndedRecord);
|
||
});
|
||
this._aggregateTimes(itemId);
|
||
}
|
||
_prepareEventPlayError(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, playErrorRecord, playEndedRecord }) => {
|
||
const varInfo = this._getVariantInfo(sessionControlRecord.curLevelUrl, sessionControlRecord);
|
||
const now = Date.now();
|
||
if (data.domain == 'mediaElementError') {
|
||
playErrorRecord.ErrDomain = playErrorRecord.ErrCode = 'mediaElementError';
|
||
playErrorRecord.ErrIsFatal = true;
|
||
playErrorRecord.ErrCodeMediaElement = data.mediaElemCode;
|
||
playErrorRecord.ErrReason = data.mediaElemReason;
|
||
playErrorRecord.ErrDetail = data.mediaElemDetail;
|
||
}
|
||
else {
|
||
playErrorRecord.ErrCode = data.details;
|
||
playErrorRecord.ErrReason = data.code;
|
||
playErrorRecord.ErrIsFatal = data.fatal;
|
||
playErrorRecord.ErrDomain = 'mediaError';
|
||
}
|
||
playErrorRecord.PlayerErrCount = data.count || 1;
|
||
playErrorRecord.BitRnk = varInfo.brRnk;
|
||
playErrorRecord.MediaDur = data.mediaDur;
|
||
playErrorRecord.PlayTime = playEndedRecord.PlayTime;
|
||
playErrorRecord.PlayTimeWC = playEndedRecord.PlayTimeWC;
|
||
playErrorRecord.PlayerIABR = varInfo.avgBandwidth;
|
||
playErrorRecord.PlayerIBR = varInfo.bandwidth;
|
||
playErrorRecord.LastLikelyToKeepUp = now - sessionControlRecord.lastLikelyToKeepUpTime;
|
||
playErrorRecord.LastSwitch = now - sessionControlRecord.lastSwitchTime;
|
||
playErrorRecord.VideoQltyIndex = varInfo.qltyIndex;
|
||
playErrorRecord.Rate = sessionControlRecord.rate;
|
||
playErrorRecord.KeyErrorType = sessionControlRecord.lastKeyErrorType;
|
||
playErrorRecord.KeyDeliveryTime = sessionControlRecord.lastKeyDeliveryTime;
|
||
this._copyCommonKeys(playErrorRecord, playEndedRecord);
|
||
});
|
||
this._aggregateTimes(itemId);
|
||
}
|
||
_prepareEventMediaEngineStalled(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, mediaEngineStalledRecord, playEndedRecord }) => {
|
||
const now = Date.now();
|
||
const varInfo = this._getVariantInfo(sessionControlRecord.curLevelUrl, sessionControlRecord);
|
||
mediaEngineStalledRecord.MediaDur = data.mediaDur;
|
||
mediaEngineStalledRecord.BitRnk = varInfo.brRnk;
|
||
mediaEngineStalledRecord.Codecs = varInfo.codecs;
|
||
mediaEngineStalledRecord.LastLikelyToKeepUp = now - sessionControlRecord.lastLikelyToKeepUpTime;
|
||
mediaEngineStalledRecord.LastSwitch = now - sessionControlRecord.lastSwitchTime;
|
||
mediaEngineStalledRecord.StallDetectionTime = data.stallDurationMs;
|
||
mediaEngineStalledRecord.StallType = data.type;
|
||
mediaEngineStalledRecord.BufferLength = data.bufferLen;
|
||
mediaEngineStalledRecord.LaSwDir = sessionControlRecord.lastSwitchDir;
|
||
mediaEngineStalledRecord.TargetDur = varInfo.targetduration;
|
||
mediaEngineStalledRecord.PlayerIABR = varInfo.avgBandwidth;
|
||
mediaEngineStalledRecord.PlayerIBR = varInfo.bandwidth;
|
||
mediaEngineStalledRecord.Rate = sessionControlRecord.rate;
|
||
mediaEngineStalledRecord.OBRLast = sessionControlRecord.obrLast;
|
||
mediaEngineStalledRecord.OBRMean = (playEndedRecord.NetBytes * 8) / (playEndedRecord.ADT / 1000);
|
||
this._copyCommonKeys(mediaEngineStalledRecord, playEndedRecord);
|
||
});
|
||
this._aggregateTimes(itemId);
|
||
}
|
||
_prepareEventSwitchComplete(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, switchCompleteRecord, playEndedRecord }) => {
|
||
const fromVarInfo = this._getVariantInfo(sessionControlRecord.prevLevelUrl, sessionControlRecord);
|
||
const toVarInfo = this._getVariantInfo(sessionControlRecord.curLevelUrl, sessionControlRecord);
|
||
switchCompleteRecord.FrBitRnk = fromVarInfo.brRnk;
|
||
switchCompleteRecord.ToBitRnk = toVarInfo.brRnk;
|
||
switchCompleteRecord.TimeToBitrate = Date.now() - sessionControlRecord.sessionStartTime;
|
||
switchCompleteRecord.MediaDur = data.mediaDur;
|
||
switchCompleteRecord.Rate = sessionControlRecord.rate;
|
||
switchCompleteRecord.PlayerIBR = toVarInfo.bandwidth;
|
||
switchCompleteRecord.PlayerIABR = toVarInfo.avgBandwidth;
|
||
switchCompleteRecord.LastPlayerIBR = fromVarInfo.bandwidth;
|
||
switchCompleteRecord.LastPlayerIABR = fromVarInfo.avgBandwidth;
|
||
switchCompleteRecord.ReWd = toVarInfo.width;
|
||
switchCompleteRecord.ReHt = toVarInfo.height;
|
||
this._copyCommonKeys(switchCompleteRecord, playEndedRecord);
|
||
});
|
||
this._aggregateTimes(itemId);
|
||
}
|
||
_prepareEventVariantEnded(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, variantEndedRecord, playEndedRecord }) => {
|
||
const varInfo = this._getVariantInfo(sessionControlRecord.curLevelUrl, sessionControlRecord);
|
||
variantEndedRecord.Rate = sessionControlRecord.rate;
|
||
variantEndedRecord.VarAvgBitrate = varInfo.avgBandwidth;
|
||
variantEndedRecord.VarPeakBitrate = varInfo.bandwidth;
|
||
variantEndedRecord.VarBitRk = varInfo.brRnk;
|
||
variantEndedRecord.VarSTTime = sessionControlRecord.variantStartTimeMedia;
|
||
variantEndedRecord.VarEndTime = data.currentTime * 1000;
|
||
variantEndedRecord.IFR = varInfo.framerate;
|
||
variantEndedRecord.ODR = sessionControlRecord.decodedFramesForVariant / sessionControlRecord.decodedFramesForVariantSampleCount;
|
||
variantEndedRecord.ReWd = varInfo.width;
|
||
variantEndedRecord.ReHt = varInfo.height;
|
||
variantEndedRecord.Codecs = varInfo.codecs;
|
||
variantEndedRecord.AvgVideoBitrate = ((sessionControlRecord.variantVideoBytes || 0) * 8) / (sessionControlRecord.variantVideoDuration || 1);
|
||
variantEndedRecord.AvgAudioBitrate = ((sessionControlRecord.variantAudioBytes || 0) * 8) / (sessionControlRecord.variantAudioDuration || 1);
|
||
this._copyCommonKeys(variantEndedRecord, playEndedRecord);
|
||
});
|
||
this._aggregateTimes(itemId);
|
||
}
|
||
_prepareEventNwError(itemId, data) {
|
||
this.update(itemId, ({ sessionControlRecord, nwErrorRecord, playEndedRecord }) => {
|
||
const varInfo = this._getVariantInfo(sessionControlRecord.curLevelUrl, sessionControlRecord);
|
||
nwErrorRecord.ErrCode = data.details;
|
||
nwErrorRecord.ErrReason = data.code;
|
||
nwErrorRecord.ErrIsFatal = data.fatal;
|
||
nwErrorRecord.NwErrCount = data.count || 1;
|
||
nwErrorRecord.ErrDomain = 'networkError';
|
||
nwErrorRecord.PlayTime = playEndedRecord.PlayTime;
|
||
nwErrorRecord.PlayTimeWC = playEndedRecord.PlayTimeWC;
|
||
nwErrorRecord.BitRnk = varInfo.brRnk;
|
||
nwErrorRecord.Rate = sessionControlRecord.rate;
|
||
nwErrorRecord.KeyErrorType = sessionControlRecord.lastKeyErrorType;
|
||
nwErrorRecord.KeyDeliveryTime = sessionControlRecord.lastKeyDeliveryTime;
|
||
this._copyCommonKeys(nwErrorRecord, playEndedRecord);
|
||
});
|
||
this._aggregateTimes(itemId);
|
||
}
|
||
_copyCommonKeys(dst, src) {
|
||
dst.PlayType = src.PlayType;
|
||
dst.LastMediaCDNServer = src.LastMediaCDNServer;
|
||
dst.MaxVideoQltyIndex = src.MaxVideoQltyIndex;
|
||
dst.MaxReWd = src.MaxReWd;
|
||
dst.MaxReHt = src.MaxReHt;
|
||
dst.IsGapless = src.IsGapless;
|
||
dst.IsAudioOnly = src.IsAudioOnly;
|
||
dst.IsFirstItem = src.IsFirstItem;
|
||
dst.ItemID = src.ItemID;
|
||
dst.ServerInfo = src.ServerInfo;
|
||
}
|
||
_computeVariantInfo(levels) {
|
||
const variantList = {};
|
||
let maxWidth = 0;
|
||
let maxHeight = 0;
|
||
let maxProd = 0;
|
||
let maxVideoQltyIndex = 0;
|
||
const maxNormalizedPeak = this._getMaxNormalizedPeak(levels);
|
||
levels.forEach((item) => {
|
||
if (item.attrs) {
|
||
const fourCC = this._getVideoFourCC(item.attrs.CODECS);
|
||
const qltyIndex = this._getVideoQualityIndex(fourCC, item.attrs['VIDEO-RANGE']) || 0;
|
||
const variantInfo = {
|
||
codecs: item.attrs.CODECS,
|
||
width: item.width,
|
||
height: item.height,
|
||
bandwidth: item.attrs.BANDWIDTH,
|
||
avgBandwidth: item.attrs['AVERAGE-BANDWIDTH'],
|
||
framerate: item.attrs['FRAME-RATE'],
|
||
iframes: item.iframes,
|
||
brRnk: this._getBitrateRank(item.attrs.BANDWIDTH, fourCC, maxNormalizedPeak),
|
||
qltyIndex: qltyIndex,
|
||
targetduration: 0,
|
||
playTime: 0,
|
||
};
|
||
maxVideoQltyIndex = qltyIndex > maxVideoQltyIndex ? qltyIndex : maxVideoQltyIndex;
|
||
// store variant data against the url
|
||
variantList[item.url] = variantInfo;
|
||
}
|
||
const prod = item.width * item.height;
|
||
if (prod > maxProd) {
|
||
maxProd = prod;
|
||
maxWidth = item.width;
|
||
maxHeight = item.height;
|
||
}
|
||
});
|
||
const varListString = Object.keys(variantList).length > 0
|
||
? Object.values(variantList)
|
||
.map((item) => item.bandwidth + ':' + item.avgBandwidth)
|
||
.join(',')
|
||
: undefined;
|
||
return { variantList: variantList, varListString: varListString, maxVideoQltyIndex: maxVideoQltyIndex, maxWidth: maxWidth, maxHeight: maxHeight };
|
||
}
|
||
_getMaxNormalizedPeak(levels) {
|
||
let maxNormalizedPeak = 0;
|
||
levels.forEach((level) => {
|
||
const attrs = level.attrs;
|
||
if (attrs) {
|
||
const peak = attrs.BANDWIDTH || 0;
|
||
const normPeak = this._getNormalizedPeak(peak, this._getVideoFourCC(attrs.CODECS));
|
||
const curMax = Math.max(peak, normPeak);
|
||
maxNormalizedPeak = curMax > maxNormalizedPeak ? curMax : maxNormalizedPeak;
|
||
}
|
||
});
|
||
return maxNormalizedPeak;
|
||
}
|
||
_getNormalizedPeak(peak, fourCC) {
|
||
// Normalization factor for avc family is 1 and for hevc/dolby family is 1.5.
|
||
return typeof RTCVideoQualityIndex[fourCC] === 'object' ? peak * 1.5 : peak * 1;
|
||
}
|
||
_getVideoFourCC(codecs) {
|
||
return codecs
|
||
? codecs
|
||
.split(',')
|
||
.map((item) => item.split('.')[0].trim())
|
||
.find((item) => !!RTCVideoQualityIndex[item])
|
||
: codecs;
|
||
}
|
||
_getVideoQualityIndex(fourCC, range) {
|
||
return typeof RTCVideoQualityIndex[fourCC] === 'object' ? RTCVideoQualityIndex[fourCC][range] : RTCVideoQualityIndex[fourCC];
|
||
}
|
||
_getBitrateRank(peak, fourCC, maxNormalizedPeak) {
|
||
const normPeak = Math.max(1, this._getNormalizedPeak(peak, fourCC));
|
||
return Math.ceil((normPeak * 100) / maxNormalizedPeak);
|
||
}
|
||
_findTimeWeightedValues(playTime, list, curLevel) {
|
||
const result = { twBRnk: 0, twIBR: 0, twIABR: 0 };
|
||
if (list) {
|
||
let totalBitRk = 0;
|
||
let totalTWIB = 0;
|
||
let totalPlaytime = 0;
|
||
let totalTWIAB = 0;
|
||
let totalPlaytimeForAvgBw = 0;
|
||
Object.values(list).forEach((item) => {
|
||
let varPlayTime = item.playTime;
|
||
if (item === list[curLevel]) {
|
||
varPlayTime += playTime || 0;
|
||
}
|
||
if (varPlayTime) {
|
||
totalTWIB += item.bandwidth * varPlayTime;
|
||
totalBitRk += item.brRnk * varPlayTime;
|
||
totalPlaytime += varPlayTime;
|
||
// Avg bandwidth is optional, use its playtime only if it's reported
|
||
if (item.avgBandwidth) {
|
||
totalTWIAB += item.avgBandwidth * varPlayTime;
|
||
totalPlaytimeForAvgBw += varPlayTime;
|
||
}
|
||
}
|
||
});
|
||
if (totalPlaytime) {
|
||
result.twBRnk = totalBitRk / totalPlaytime;
|
||
result.twIBR = totalTWIB / totalPlaytime;
|
||
}
|
||
if (totalTWIAB && totalPlaytimeForAvgBw) {
|
||
result.twIABR = totalTWIAB / totalPlaytimeForAvgBw;
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
_computeMediaStats(record) {
|
||
const bufferAppendInfo = record.bufferAppendInfo;
|
||
let latencies = [];
|
||
const sizes = [];
|
||
if (bufferAppendInfo && bufferAppendInfo.forEach) {
|
||
bufferAppendInfo.forEach((item) => {
|
||
latencies.push(item.latency);
|
||
sizes.push(item.size);
|
||
});
|
||
}
|
||
const bufLatencyInfo = this._computeStats(latencies);
|
||
const bufSizeInfo = this._computeStats(sizes);
|
||
// calculate seek stats
|
||
const seekInfo = record.seekInfo;
|
||
latencies = [];
|
||
if (seekInfo && seekInfo.forEach) {
|
||
record.seekInfo.forEach((item) => latencies.push(item.latency));
|
||
}
|
||
const seekLatencyInfo = this._computeStats(latencies);
|
||
// calculate play stats
|
||
const playInfo = record.playInfo;
|
||
latencies = [];
|
||
if (playInfo && playInfo.forEach) {
|
||
record.playInfo.forEach((item) => latencies.push(item.latency));
|
||
}
|
||
const playLatencyInfo = this._computeStats(latencies);
|
||
return { bufLatencyInfo, bufSizeInfo, seekLatencyInfo, playLatencyInfo };
|
||
}
|
||
_computeStats(inputArr) {
|
||
let max;
|
||
let median;
|
||
if (inputArr) {
|
||
const arr = inputArr.filter((value) => value > 0); // ensure there are only positive values
|
||
if (arr.length > 0) {
|
||
max = arr.reduce((a, b) => {
|
||
return Math.max(a, b);
|
||
});
|
||
const len = arr.length;
|
||
const sortedArr = arr.sort((a, b) => a - b);
|
||
const mid = Math.ceil(len / 2);
|
||
median = len % 2 === 0 ? (sortedArr[mid] + sortedArr[mid - 1]) / 2 : sortedArr[mid - 1];
|
||
}
|
||
}
|
||
return { max, median };
|
||
}
|
||
_getVariantInfo(url, record) {
|
||
return url && record.manifestData && record.manifestData.variantList && record.manifestData.variantList[url] ? record.manifestData.variantList[url] : {};
|
||
}
|
||
_setTargetDuration(url, targetduration, record) {
|
||
if (url && record.manifestData && record.manifestData.variantList && record.manifestData.variantList[url]) {
|
||
record.manifestData.variantList[url].targetduration = targetduration;
|
||
}
|
||
}
|
||
_isBadSw(curSwDir, lastSwitchDir, curLevelIsIframe, lastLevelIsIframe, curSwitchTime, lastSwitchTime, isSeeking) {
|
||
const minSwitchWindow = 10000; // if we reverse the switch direction within this window, it's considered as a bad switch
|
||
let isBadSw = false;
|
||
if (curSwitchTime - lastSwitchTime < minSwitchWindow && lastSwitchDir !== curSwDir && lastLevelIsIframe === curLevelIsIframe && !isSeeking) {
|
||
isBadSw = true;
|
||
}
|
||
return isBadSw;
|
||
}
|
||
_aggregateTimes(itemId) {
|
||
this.update(itemId, ({ sessionControlRecord, playLikelyToKeepUpRecord, playStalledRecord, mediaEngineStalledRecord, switchCompleteRecord, playRateChangedRecord, variantEndedRecord, playErrorRecord, nwErrorRecord, periodicRecord, playEndedRecord, }) => {
|
||
const now = Date.now();
|
||
const eventTime = now - sessionControlRecord.eventStartTime;
|
||
const state = sessionControlRecord.state;
|
||
switch (state) {
|
||
case 'RTC_STATE_INIT': {
|
||
playLikelyToKeepUpRecord.StartupTime = (playLikelyToKeepUpRecord.StartupTime || 0) + eventTime;
|
||
periodicRecord.InitTime = (periodicRecord.InitTime || 0) + eventTime;
|
||
playEndedRecord.InitTime = (playEndedRecord.InitTime || 0) + eventTime;
|
||
break;
|
||
}
|
||
case 'RTC_STATE_CANPLAY': {
|
||
playRateChangedRecord.StartupTime = (playRateChangedRecord.StartupTime || 0) + eventTime;
|
||
periodicRecord.InitTime = (periodicRecord.InitTime || 0) + eventTime;
|
||
playEndedRecord.InitTime = (playEndedRecord.InitTime || 0) + eventTime;
|
||
break;
|
||
}
|
||
case 'RTC_STATE_PAUSE': {
|
||
periodicRecord.PauseTime = (periodicRecord.PauseTime || 0) + eventTime;
|
||
playEndedRecord.PauseTime = (playEndedRecord.PauseTime || 0) + eventTime;
|
||
playRateChangedRecord.LastPause = (playRateChangedRecord.LastPause || 0) + eventTime;
|
||
playEndedRecord.LastPause = (playEndedRecord.LastPause || 0) + eventTime;
|
||
playErrorRecord.LastPause = (playErrorRecord.LastPause || 0) + eventTime;
|
||
nwErrorRecord.LastPause = (nwErrorRecord.LastPause || 0) + eventTime;
|
||
break;
|
||
}
|
||
case 'RTC_STATE_STALL': {
|
||
variantEndedRecord.StallTime = (variantEndedRecord.StallTime || 0) + eventTime;
|
||
periodicRecord.StallTime = (periodicRecord.StallTime || 0) + eventTime;
|
||
playEndedRecord.StallTime = (playEndedRecord.StallTime || 0) + eventTime;
|
||
playRateChangedRecord.LastStall = (playRateChangedRecord.LastStall || 0) + eventTime;
|
||
playErrorRecord.LastStall = (playErrorRecord.LastStall || 0) + eventTime;
|
||
playEndedRecord.LastStall = (playEndedRecord.LastStall || 0) + eventTime;
|
||
break;
|
||
}
|
||
case 'RTC_STATE_MEDIAENGINESTALL': {
|
||
variantEndedRecord.MediaEngineStallTime = (variantEndedRecord.MediaEngineStallTime || 0) + eventTime;
|
||
periodicRecord.MediaEngineStallTime = (periodicRecord.MediaEngineStallTime || 0) + eventTime;
|
||
playEndedRecord.MediaEngineStallTime = (playEndedRecord.MediaEngineStallTime || 0) + eventTime;
|
||
playRateChangedRecord.LastMediaEngineStall = (playRateChangedRecord.LastMediaEngineStall || 0) + eventTime;
|
||
playErrorRecord.LastMediaEngineStall = (playErrorRecord.LastMediaEngineStall || 0) + eventTime;
|
||
playEndedRecord.LastMediaEngineStall = (playEndedRecord.LastMediaEngineStall || 0) + eventTime;
|
||
break;
|
||
}
|
||
case 'RTC_STATE_NWERROR': {
|
||
playEndedRecord.NwErrTime = (playEndedRecord.NwErrTime || 0) + eventTime;
|
||
periodicRecord.NwErrTime = (periodicRecord.NwErrTime || 0) + eventTime;
|
||
break;
|
||
}
|
||
case 'RTC_STATE_PLAYERROR': {
|
||
periodicRecord.PlayErrTime = (periodicRecord.PlayErrTime || 0) + eventTime;
|
||
playEndedRecord.PlayErrTime = (playEndedRecord.PlayErrTime || 0) + eventTime;
|
||
break;
|
||
}
|
||
case 'RTC_STATE_PLAY': {
|
||
playRateChangedRecord.RateChangePlayTime = (playRateChangedRecord.RateChangePlayTime || 0) + eventTime;
|
||
switchCompleteRecord.PlayTime = (switchCompleteRecord.PlayTime || 0) + eventTime;
|
||
switchCompleteRecord.PlayTimeLastSW = (switchCompleteRecord.PlayTimeLastSW || 0) + eventTime * (sessionControlRecord.oldRate / 100);
|
||
playStalledRecord.LastResume = (playStalledRecord.LastResume || 0) + eventTime;
|
||
mediaEngineStalledRecord.LastResume = (mediaEngineStalledRecord.LastResume || 0) + eventTime;
|
||
playErrorRecord.LastResume = (playErrorRecord.LastResume || 0) + eventTime;
|
||
nwErrorRecord.LastResume = (nwErrorRecord.LastResume || 0) + eventTime;
|
||
variantEndedRecord.VarPlayTimeWC = (variantEndedRecord.VarPlayTimeWC || 0) + eventTime;
|
||
variantEndedRecord.VarPlayTime = (variantEndedRecord.VarPlayTime || 0) + eventTime * (sessionControlRecord.oldRate / 100);
|
||
periodicRecord.PlayTimeWC = (periodicRecord.PlayTimeWC || 0) + eventTime;
|
||
periodicRecord.PlayTime = (periodicRecord.PlayTime || 0) + eventTime * (sessionControlRecord.oldRate / 100);
|
||
playEndedRecord.PlayTimeWC = (playEndedRecord.PlayTimeWC || 0) + eventTime;
|
||
playEndedRecord.PlayTime = (playEndedRecord.PlayTime || 0) + eventTime * (sessionControlRecord.oldRate / 100);
|
||
// Update stall playtime
|
||
mediaEngineStalledRecord.PlayTime = playEndedRecord.PlayTime;
|
||
playStalledRecord.PlayTime = playEndedRecord.PlayTime;
|
||
// Update the playtime for the current level to calculate time weighted values
|
||
const curLevelUrl = sessionControlRecord.curLevelUrl;
|
||
let variantInfo = sessionControlRecord.intervalVariantList[curLevelUrl];
|
||
variantInfo.playTime = (variantInfo.playTime || 0) + eventTime;
|
||
variantInfo = sessionControlRecord.sessionVariantList[curLevelUrl];
|
||
variantInfo.playTime = (variantInfo.playTime || 0) + eventTime;
|
||
break;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
const loggerName$4 = { name: 'rtc-service' };
|
||
const defaultIntervalValue = 300000;
|
||
/*
|
||
* @brief RTC service is responsible for subscribing to queries and updating the RTC store.
|
||
*/
|
||
class RTCService {
|
||
constructor(hls, config, accessLog, logger) {
|
||
this.hls = hls;
|
||
this.config = config;
|
||
this.accessLog = accessLog;
|
||
this.logger = logger;
|
||
this.destroy$ = new Subject();
|
||
this.isSeeking = false;
|
||
this.seekStart = null;
|
||
this.periodicInterval = config.rtcIntervalTimeout ? config.rtcIntervalTimeout : defaultIntervalValue;
|
||
this.intervalFunc = null;
|
||
this.rtcStore = new RTCStore(this.logger);
|
||
this.rtcQuery = new RTCQuery(this.rtcStore, this.logger);
|
||
this.rtcComponent = new RTCComponent(this.rtcQuery, this.logger);
|
||
accessLog.setRTCQuery(this.rtcQuery);
|
||
this.subscribeAndUpdateStore();
|
||
this.registerForEvents();
|
||
}
|
||
destroy() {
|
||
this.destroy$.next();
|
||
this.clearPeriodic();
|
||
this.rtcStore.reset();
|
||
}
|
||
detachMedia() {
|
||
this.clearPeriodic();
|
||
let t = 0;
|
||
try {
|
||
// if detach was as a result of an error, this will throw.
|
||
t = this.hls.realCurrentTime;
|
||
this.logger.qe({ critical: true, name: 'playEnded', data: { itemId: this.rtcEventItemId(true) } });
|
||
this.rtcStore.updateVariantEnd(this.rtcEventItemId(true), { currentTime: t });
|
||
this.sendAndFinalize(this.rtcEventItemId(true), RTCEventGroup.VariantEnded);
|
||
this.rtcStore.updatePeriodic(this.rtcEventItemId(true), true);
|
||
this.sendAndFinalize(this.rtcEventItemId(true), RTCEventGroup.Periodic);
|
||
this.rtcStore.updateEnded(this.rtcEventItemId(true));
|
||
this.sendAndFinalize(this.rtcEventItemId(true), RTCEventGroup.PlayEnded);
|
||
}
|
||
catch (err) {
|
||
this.logger.warn(loggerName$4, err);
|
||
}
|
||
}
|
||
handleError(error) {
|
||
var _a, _b;
|
||
let newError;
|
||
if (error instanceof HlsError) {
|
||
newError = error;
|
||
}
|
||
else {
|
||
newError = new ExceptionError(true, error.message, ErrorResponses.InternalError);
|
||
}
|
||
if (newError instanceof PlaylistNetworkError && !newError.fatal) {
|
||
this.rtcStore.updateLevelLoadError(this.rtcEventItemId(), { url: newError.url, mediaDur: this.hls.bufferedDuration, isSeeking: this.isSeeking });
|
||
this.sendAndFinalize(this.rtcEventItemId(), RTCEventGroup.SwitchComplete);
|
||
}
|
||
else if (newError.type === ErrorTypes.NETWORK_ERROR) {
|
||
this.rtcStore.updateNwError(this.rtcEventItemId(), { fatal: newError.fatal, details: newError.details, code: (_a = newError.response) === null || _a === void 0 ? void 0 : _a.code });
|
||
this.sendAndFinalize(this.rtcEventItemId(), RTCEventGroup.NwError);
|
||
}
|
||
else {
|
||
this.rtcStore.updateMediaError(this.rtcEventItemId(), { fatal: newError.fatal, details: newError.details, code: (_b = newError.response) === null || _b === void 0 ? void 0 : _b.code });
|
||
this.sendAndFinalize(this.rtcEventItemId(), RTCEventGroup.PlayError);
|
||
}
|
||
}
|
||
handleMediaElementError(err) {
|
||
this.rtcStore.updateMediaElementError(this.rtcEventItemId(), err);
|
||
this.sendAndFinalize(this.rtcEventItemId(), RTCEventGroup.PlayError);
|
||
}
|
||
handleFragLoaded(frag, stats) {
|
||
var _a;
|
||
this.logger.trace(loggerName$4, 'MediaFragment=%o, Stats=%o', fragPrint(frag), stats);
|
||
if (!this.checkMediaOptionType(frag.mediaOptionType)) {
|
||
return;
|
||
}
|
||
if (frag.itemId !== this.rtcEventItemId()) {
|
||
this.logger.warn(loggerName$4, `Frag id does not match current item id. Frag Id=${frag.itemId}, playing id=${this.rtcEventItemId(true)}, loading id=${(_a = this.hls.loadingItem) === null || _a === void 0 ? void 0 : _a.itemId}`);
|
||
}
|
||
const adt = stats.tload - stats.trequest;
|
||
const processTime = stats.tload - stats.tfirst;
|
||
const serverInfo = this.serverInfoInstance ? this.serverInfoInstance : {};
|
||
this.rtcStore.updateFragLoaded(frag.itemId, {
|
||
fragType: frag.mediaOptionType,
|
||
bytes: stats.loaded,
|
||
duration: frag.duration,
|
||
adt: adt,
|
||
processTime: processTime,
|
||
contentType: stats.contentType,
|
||
cdnServer: stats.cdnServer,
|
||
serverInfo: serverInfo,
|
||
});
|
||
this.accessLog.updateFragLoaded(frag.itemId, this.isSeeking, {
|
||
fragType: frag.mediaOptionType,
|
||
bytes: stats.loaded,
|
||
duration: frag.duration,
|
||
adt: adt,
|
||
processTime: processTime,
|
||
startPTS: frag.start,
|
||
endPTS: frag.start + frag.duration,
|
||
});
|
||
}
|
||
handleFragBuffered(metric) {
|
||
this.logger.trace(loggerName$4, 'Frag Buffered, metrics=%o', metric);
|
||
if (!this.checkMediaOptionType(metric.fragmentType)) {
|
||
return;
|
||
}
|
||
const parseTime = metric.endDataAppend - metric.startDataAppend;
|
||
this.rtcStore.updateFragBuffered(this.rtcEventItemId(), { fragType: metric.fragmentType, bytes: metric.dataBytesAppend, parseTime: parseTime });
|
||
}
|
||
handleLevelLoaded(mediaOptionDetails, stats) {
|
||
if (!mediaOptionDetails) {
|
||
// This happens on Safari and FireFox, browsers that block autoplay.
|
||
// rdar://82312973
|
||
this.logger.warn(`handleLevelLoaded called with mediaOptionDetails as ${mediaOptionDetails}`);
|
||
return;
|
||
}
|
||
if (mediaOptionDetails.itemId !== this.rtcEventItemId()) {
|
||
this.logger.warn(loggerName$4, `media option id does not match current item id. media Id=${mediaOptionDetails.itemId}, current id=${this.rtcEventItemId}`);
|
||
}
|
||
if (mediaOptionDetails.mediaOptionType === MediaOptionType.Variant) {
|
||
const adt = stats.tload - stats.trequest;
|
||
this.rtcStore.updateLevelLoaded(this.rtcEventItemId(), {
|
||
url: mediaOptionDetails.url,
|
||
targetduration: mediaOptionDetails.targetduration,
|
||
adt: adt,
|
||
contentType: stats.contentType,
|
||
playType: mediaOptionDetails.type,
|
||
});
|
||
}
|
||
}
|
||
handleLevelSwitched(data) {
|
||
const levelData = { url: data.url, isSeeking: this.isSeeking, mediaDur: this.hls.bufferedDuration, currentTime: this.hls.realCurrentTime };
|
||
this.logger.trace(loggerName$4, `RTC level Switched, oldVariant: ${data.oldVariant}, newVariant: ${data.newVariant}`);
|
||
if (data.oldVariant) {
|
||
// Playing variant ended.
|
||
this.rtcStore.updateVariantEnd(this.rtcEventItemId(), { currentTime: this.hls.realCurrentTime });
|
||
this.sendAndFinalize(this.rtcEventItemId(), RTCEventGroup.VariantEnded);
|
||
}
|
||
// Playback switched to new variant.
|
||
this.rtcStore.updateLevelSwitched(this.rtcEventItemId(), levelData);
|
||
this.sendAndFinalize(this.rtcEventItemId(), RTCEventGroup.SwitchComplete);
|
||
}
|
||
handleLevelSwitching(levelUrl) {
|
||
this.levelSwitchingUrl = levelUrl;
|
||
}
|
||
handleLevelsChanged(variantList) {
|
||
this.logger.trace(loggerName$4, 'Levels Changed Levels=%o', variantList);
|
||
this.rtcStore.updateLevelsChanged(this.rtcEventItemId(), { levels: variantList });
|
||
}
|
||
handleManifestParsed(manifestParsedData) {
|
||
var _a;
|
||
this.logger.trace(loggerName$4, 'RTC Manifest Parsed, levels=%o, stats=%o', manifestParsedData.levels, manifestParsedData.stats);
|
||
const adt = manifestParsedData.stats.tload - manifestParsedData.stats.trequest;
|
||
this.rtcStore.updateManifestParsed(this.rtcEventItemId(), {
|
||
levels: manifestParsedData.levels,
|
||
adt: adt,
|
||
contentType: manifestParsedData.stats.contentType,
|
||
isAudioOnly: this.hls.inGaplessMode,
|
||
isGapless: this.hls.inGaplessMode,
|
||
isFirstItem: this.hls.isFirstItem,
|
||
itemID: (((_a = this.hls.reportingAgent) === null || _a === void 0 ? void 0 : _a.SessionID) || guid()) + '-' + this.rtcEventItemId(),
|
||
});
|
||
this.logger.qe({
|
||
critical: true,
|
||
name: 'gapless',
|
||
data: {
|
||
isGapless: this.hls.inGaplessMode,
|
||
isAudioOnly: this.hls.inGaplessMode,
|
||
isFirstItem: this.hls.isFirstItem,
|
||
sessionId: this.hls.sessionID,
|
||
itemId: this.rtcEventItemId(),
|
||
},
|
||
});
|
||
}
|
||
handleSeek(event) {
|
||
this.logger.trace(loggerName$4, `RTC seek event: ${event}`);
|
||
if (event === 'SEEKING') {
|
||
this.isSeeking = true;
|
||
this.seekStart = Date.now();
|
||
}
|
||
else if (event === 'SEEKED') {
|
||
// seeked update store
|
||
this.isSeeking = false;
|
||
let latency = 0;
|
||
if (this.seekStart) {
|
||
latency = Date.now() - this.seekStart;
|
||
}
|
||
this.seekStart = null;
|
||
this.logger.trace(loggerName$4, `RTC seeked, latency=${latency}`);
|
||
this.rtcStore.updateSeeked(this.rtcEventItemId(true), { latency: latency });
|
||
}
|
||
}
|
||
handleDesiredRateChanged(oldRate, newRate) {
|
||
if (newRate === 0 || (Math.abs(oldRate) > 1 && Math.abs(newRate) > 1)) {
|
||
/* Desired rate to actual rate transition is immediate for the following
|
||
- any rate to 0
|
||
- between any trick play rates (2x <-> 4x etc.)
|
||
So, update the store right away.
|
||
*/
|
||
this.logger.trace(loggerName$4, `RTC rate changed oldRate=${oldRate} newRate=${newRate}`);
|
||
this.logger.qe({ critical: true, name: 'rateChanged', data: { oldRate, newRate } });
|
||
this.rtcStore.updateRateChanged(this.rtcEventItemId(true), {
|
||
rate: newRate,
|
||
latency: 0,
|
||
mediaDur: this.hls.bufferedDuration,
|
||
currentTime: this.hls.realCurrentTime,
|
||
url: this.levelSwitchingUrl,
|
||
});
|
||
this.sendAndFinalize(this.rtcEventItemId(true), RTCEventGroup.PlayRateChanged);
|
||
}
|
||
else if (oldRate !== 1 && newRate === 1) {
|
||
// moving from pause/trickplay to playing rate
|
||
this.playStart = Date.now();
|
||
}
|
||
/* Save the desired rate and wait for playback to switch to that rate (store update happens later for those cases) */
|
||
this.oldRate = oldRate;
|
||
this.newRate = newRate;
|
||
}
|
||
handleVariantBufferAppended(timestampOffset, totalBytes) {
|
||
this.logger.trace(loggerName$4, 'RTC Variant Buffer Appended');
|
||
let latency = 0;
|
||
if (timestampOffset) {
|
||
latency = Date.now() - timestampOffset;
|
||
}
|
||
this.rtcStore.updateBufferAppended(this.rtcEventItemId(), { latency: latency, size: totalBytes });
|
||
}
|
||
handleStalled(stallInfo, bufferLen) {
|
||
this.logger.trace(loggerName$4, 'Stall Info data=%o', stallInfo);
|
||
const data = { type: stallInfo.type, stallDurationMs: stallInfo.stallDurationMs, bufferLen: bufferLen, mediaDur: this.hls.bufferedDuration };
|
||
const state = this.rtcQuery.getEntity(this.rtcEventItemId(true)).sessionControlRecord.state;
|
||
if (stallInfo.type === StallType.LowBuffer || (stallInfo.type === StallType.Seek && stallInfo.isLowBufferStall)) {
|
||
// Low Buffer Stall
|
||
if (state !== 'RTC_STATE_PLAY') {
|
||
// don't report stall events if we're not playing
|
||
this.logger.info(loggerName$4, `skipping low buffer stall event because we're not playing, state: ${state}`);
|
||
}
|
||
else {
|
||
this.rtcStore.updateBufferStalled(this.rtcEventItemId(true), data);
|
||
this.sendAndFinalize(this.rtcEventItemId(true), RTCEventGroup.PlayStalled);
|
||
}
|
||
}
|
||
else {
|
||
// High Buffer Stall
|
||
if (state !== 'RTC_STATE_PLAY') {
|
||
// don't report stall events if we're not playing
|
||
this.logger.info(loggerName$4, `skipping high buffer stall event because we're not playing, state: ${state}`);
|
||
}
|
||
else {
|
||
this.rtcStore.updateMediaEngineStalled(this.rtcEventItemId(true), data);
|
||
this.sendAndFinalize(this.rtcEventItemId(true), RTCEventGroup.MediaEngineStalled);
|
||
}
|
||
}
|
||
}
|
||
handlePlaybackInfo(droppedVideoFrames, decodedFrameCount) {
|
||
this.logger.trace(loggerName$4, `RTC Playback Info, droppedVideoFrames=${droppedVideoFrames}, decodedFrameCount=${decodedFrameCount}`);
|
||
this.rtcStore.updatePlaybackInfo(this.rtcEventItemId(true), { droppedVideoFrames: droppedVideoFrames, decodedFrameCount: decodedFrameCount });
|
||
this.accessLog.updatePlaybackInfo(this.rtcEventItemId(true), { droppedVideoFrames: droppedVideoFrames, decodedFrameCount: decodedFrameCount });
|
||
}
|
||
checkMediaOptionType(mediaOptionType) {
|
||
if (mediaOptionType === MediaOptionType.Variant || mediaOptionType === MediaOptionType.AltAudio) {
|
||
return true;
|
||
}
|
||
else {
|
||
this.logger.error(loggerName$4, 'Should not have media option type = "%s" in RTC', MediaOptionNames[mediaOptionType]);
|
||
return false;
|
||
}
|
||
}
|
||
// Return currently playing id or loading item id if preloading.
|
||
// Force return playing id if forcePlayingId is set to true.
|
||
rtcEventItemId(forcePlayingId = false) {
|
||
if (this.hls.isPreloading) {
|
||
if (forcePlayingId) {
|
||
return this.hls.playingItem.itemId;
|
||
}
|
||
return this.hls.loadingItem.itemId;
|
||
}
|
||
return this.hls.currentItem.itemId;
|
||
}
|
||
subscribeAndUpdateStore() {
|
||
this.hls.publicQueries$
|
||
.pipe(switchMap(([, mediaElementQuery]) => {
|
||
return this.mediaElementQueryListener(mediaElementQuery);
|
||
}), takeUntil(this.destroy$))
|
||
.subscribe();
|
||
this.hls.itemQueue.activeItemById$
|
||
.pipe(tap((item) => {
|
||
var _a, _b;
|
||
if (item) {
|
||
// if this is an internal build, always send RTC
|
||
// if D&U flag is true, use the configuration
|
||
// if there is no userInfo use the configuration
|
||
let enableRtc = false;
|
||
if (this.hls.userInfo) {
|
||
if (this.hls.userInfo.internalBuild) {
|
||
enableRtc = true;
|
||
}
|
||
else if (this.hls.userInfo.diagnosticsAndUsage) {
|
||
enableRtc = this.config.enableRtcReporting;
|
||
}
|
||
}
|
||
else {
|
||
enableRtc = this.config.enableRtcReporting;
|
||
}
|
||
if (enableRtc) {
|
||
// set reporting agent.
|
||
const reportingAgent = this.hls.reportingAgent;
|
||
if (reportingAgent) {
|
||
this.rtcComponent.setReportingAgent(reportingAgent);
|
||
this.logger.qe({ critical: true, name: 'rtcStart', data: { baseSessionId: reportingAgent.SessionID, itemId: (_a = this.hls.itemQueue.activeItem) === null || _a === void 0 ? void 0 : _a.itemId } });
|
||
}
|
||
else {
|
||
this.logger.warn(loggerName$4, '[RTCA] - Reporting is enabled but reportingAgent is null');
|
||
}
|
||
}
|
||
else {
|
||
// RTC is disabled.
|
||
this.rtcComponent.setReportingAgent(null);
|
||
this.logger.info(loggerName$4, '[RTCA] - Reporting is disabled');
|
||
}
|
||
this.serverInfoInstance = null;
|
||
// create new entity with item.itemId
|
||
const id = item.itemId;
|
||
this.rtcStore.createEntity(id);
|
||
// if this is the first item or we are not in gapless mode, start the periodic timer
|
||
if (this.hls.isFirstItem || !this.hls.inGaplessMode) {
|
||
this.setPeriodic(id);
|
||
}
|
||
this.logger.trace(`RTC Manifest loading: ${(_b = this.hls.itemQueue.activeItem) === null || _b === void 0 ? void 0 : _b.url}`);
|
||
}
|
||
}), takeUntil(this.destroy$))
|
||
.subscribe();
|
||
}
|
||
itemTransitioned(oldItem, newItem) {
|
||
// item transitioned
|
||
this.logger.trace(loggerName$4, `RTC Item transitioned oldItem=${oldItem} newItem=${newItem}`);
|
||
this.rtcStore.updateVariantEnd(oldItem, { currentTime: this.hls.realCurrentTime });
|
||
this.sendAndFinalize(oldItem, RTCEventGroup.VariantEnded);
|
||
this.rtcStore.updatePeriodic(oldItem, true);
|
||
this.sendAndFinalize(oldItem, RTCEventGroup.Periodic);
|
||
this.rtcStore.updateEnded(oldItem);
|
||
this.sendAndFinalize(oldItem, RTCEventGroup.PlayEnded);
|
||
// attach/subscribe to timer for periodic events. Note set periodic calls clearPeriodic
|
||
this.setPeriodic(newItem);
|
||
}
|
||
mediaElementQueryListener(mediaElementQuery) {
|
||
const playingSource$ = mediaElementQuery.gotPlaying$.pipe(tap((playing) => {
|
||
if (playing) {
|
||
const oldRate = this.oldRate;
|
||
const newRate = this.newRate || 1; // set 1 for default cases, e.g. autoplay
|
||
if (Math.abs(oldRate) > 1 && Math.abs(newRate) > 1) {
|
||
return; // Ignore gotPlaying notifications for transition between trick play rates
|
||
}
|
||
this.logger.info(loggerName$4, `RTC rate changed oldRate=${oldRate} newRate=${newRate}`);
|
||
this.logger.qe({ critical: true, name: 'rateChanged', data: { oldRate, newRate } });
|
||
this.rtcStore.updateCanPlay(this.rtcEventItemId(true), { mediaDur: this.hls.bufferedDuration });
|
||
this.sendAndFinalize(this.rtcEventItemId(true), RTCEventGroup.PlayLikelyToKeepUp);
|
||
this.rtcStore.updateRateChanged(this.rtcEventItemId(true), {
|
||
rate: newRate,
|
||
latency: isFiniteNumber(this.playStart) ? Date.now() - this.playStart : 0,
|
||
mediaDur: this.hls.bufferedDuration,
|
||
currentTime: this.hls.realCurrentTime,
|
||
url: this.levelSwitchingUrl,
|
||
});
|
||
this.sendAndFinalize(this.rtcEventItemId(true), RTCEventGroup.PlayRateChanged);
|
||
}
|
||
}));
|
||
return playingSource$;
|
||
}
|
||
// Event based
|
||
registerForEvents() {
|
||
const target = fromEventTarget(this.hls, this);
|
||
merge(target.event(HlsEvent.KEY_REQUEST_STARTED, this.keyRequestStarted, this), target.event(HlsEvent.KEY_LOADED, this.keyLoaded, this)).pipe(takeUntil(this.destroy$)).subscribe();
|
||
}
|
||
keyRequestStarted(data) {
|
||
this.logger.trace(loggerName$4, 'RTC key request started %o', data);
|
||
data.timestamp = Date.now();
|
||
this.rtcStore.updateLicenseChallengeRequested(this.rtcEventItemId(), data);
|
||
}
|
||
keyLoaded(data) {
|
||
this.logger.trace(loggerName$4, 'RTC key loaded, data: %o', data);
|
||
data.timestamp = Date.now();
|
||
data.currentTime = this.hls.realCurrentTime;
|
||
this.rtcStore.updateSegmentKeyLoaded(this.rtcEventItemId(), data);
|
||
}
|
||
// End of Event based
|
||
// Key session handler methods
|
||
licenseChallengeReceived(data) {
|
||
this.logger.trace(loggerName$4, 'RTC licenseChallenge received data: %o', data);
|
||
this.rtcStore.updateLicenseChallengeReceived(this.rtcEventItemId(), { timestamp: Date.now(), keyuri: data.keyuri });
|
||
}
|
||
licenseChallengeSubmitted(data) {
|
||
this.logger.trace(loggerName$4, 'RTC licenseChallenge submitted data: %o', data);
|
||
this.rtcStore.updateLicenseChallengeSubmitted(this.rtcEventItemId(), { timestamp: Date.now(), keyFormat: data.keyFormat, keyuri: data.keyuri });
|
||
}
|
||
licenseChallengeCreated(data) {
|
||
this.logger.trace(loggerName$4, 'RTC licenseChallenge created data: %o', data);
|
||
this.rtcStore.updateLicenseChallengeCreated(this.rtcEventItemId(), { timestamp: Date.now(), cdmVersion: data.cdmVersion, keyuri: data.keyuri });
|
||
this.rtcStore.updateLicenseResponseRequested(this.rtcEventItemId(), { timestamp: Date.now(), keyuri: data.keyuri }); // both licenseChallengeCreated and licenseResponseRequested happen simultaneouly
|
||
}
|
||
licenseResponseSubmitted(data) {
|
||
this.logger.trace(loggerName$4, 'RTC licenseResponse submitted data: %o', data);
|
||
this.rtcStore.updateLicenseResponseReceived(this.rtcEventItemId(), { timestamp: Date.now(), keyuri: data.keyuri }); // both licenseResponseReceived and licenseResponseSubmitted happen simultaneouly
|
||
this.rtcStore.updateLicenseResponseSubmitted(this.rtcEventItemId(), { timestamp: Date.now(), keyuri: data.keyuri });
|
||
}
|
||
licenseResponseProcessed(data) {
|
||
this.logger.trace(loggerName$4, 'RTC licenseResponse processed data: %o', data);
|
||
this.rtcStore.updateLicenseResponseProcessed(this.rtcEventItemId(), { timestamp: Date.now(), keyuri: data.keyuri, currentTime: this.hls.realCurrentTime });
|
||
this.sendAndFinalize(this.rtcEventItemId(), RTCEventGroup.KeySessionComplete);
|
||
}
|
||
licenseChallengeError(data) {
|
||
this.logger.trace(loggerName$4, 'RTC licenseChallenge error data: %o', data);
|
||
this.rtcStore.updateLicenseChallengeError(this.rtcEventItemId(), { timestamp: Date.now(), keyuri: data.keyuri });
|
||
this.sendAndFinalize(this.rtcEventItemId(), RTCEventGroup.KeySessionComplete);
|
||
}
|
||
licenseResponseError(data) {
|
||
this.logger.trace(loggerName$4, 'RTC licenseResponse error data: %o', data);
|
||
this.rtcStore.updateLicenseResponseError(this.rtcEventItemId(), { timestamp: Date.now(), keyuri: data.keyuri });
|
||
this.sendAndFinalize(this.rtcEventItemId(), RTCEventGroup.KeySessionComplete);
|
||
}
|
||
keyAborted(data) {
|
||
var _a;
|
||
this.logger.trace(loggerName$4, 'RTC key aborted data: %o', data);
|
||
if (((_a = this.rtcQuery.getEntity(this.rtcEventItemId())) === null || _a === void 0 ? void 0 : _a.sessionControlRecord.activeKeySessions[data.keyuri]) != null) {
|
||
this.rtcStore.updateKeyAborted(this.rtcEventItemId(), { timestamp: Date.now(), keyuri: data.keyuri });
|
||
this.sendAndFinalize(this.rtcEventItemId(), RTCEventGroup.KeySessionComplete);
|
||
}
|
||
else {
|
||
this.logger.warn(`keyAbort called without active key session ${redactUrl(data.keyuri)}`);
|
||
}
|
||
}
|
||
// End of key session handler methods
|
||
setPeriodic(id) {
|
||
this.clearPeriodic();
|
||
this.intervalFunc = setInterval(this.handlePeriodic.bind(this, id), this.periodicInterval);
|
||
}
|
||
handlePeriodic(id) {
|
||
this.logger.trace(loggerName$4, `sendPeriodic id=${id}`);
|
||
this.rtcStore.updatePeriodic(id, false);
|
||
this.sendAndFinalize(id, RTCEventGroup.Periodic);
|
||
}
|
||
clearPeriodic() {
|
||
if (this.intervalFunc) {
|
||
clearInterval(this.intervalFunc);
|
||
}
|
||
this.intervalFunc = null;
|
||
}
|
||
sendAndFinalize(id, rtcEvent) {
|
||
this.accessLog.addPlayTime(id);
|
||
switch (rtcEvent) {
|
||
case RTCEventGroup.PlayEnded:
|
||
this.rtcComponent.sendPlayEnded(id);
|
||
break;
|
||
case RTCEventGroup.Periodic:
|
||
this.rtcComponent.sendPeriodic(id);
|
||
break;
|
||
case RTCEventGroup.PlayStalled:
|
||
this.accessLog.updateStallCount(id);
|
||
this.rtcComponent.sendPlayStalled(id);
|
||
break;
|
||
case RTCEventGroup.KeySessionComplete:
|
||
this.rtcComponent.sendKeySessionComplete(id);
|
||
break;
|
||
case RTCEventGroup.PlayLikelyToKeepUp:
|
||
this.accessLog.updateCanPlay(id);
|
||
this.rtcComponent.sendPlayLikelyToKeepUp(id);
|
||
break;
|
||
case RTCEventGroup.PlayRateChanged:
|
||
this.rtcComponent.sendPlayRateChange(id);
|
||
break;
|
||
case RTCEventGroup.PlayError:
|
||
this.accessLog.addToErrorLog(id, 'mediaError');
|
||
this.rtcComponent.sendPlayError(id);
|
||
break;
|
||
case RTCEventGroup.MediaEngineStalled:
|
||
this.accessLog.updateMediaEngineStallCount(id);
|
||
this.rtcComponent.sendMediaEngineStalled(id);
|
||
break;
|
||
case RTCEventGroup.SwitchComplete:
|
||
this.accessLog.addToAccessLog(id);
|
||
this.rtcComponent.sendSwitchComplete(id);
|
||
break;
|
||
case RTCEventGroup.VariantEnded:
|
||
this.rtcComponent.sendVariantEnded(id);
|
||
break;
|
||
case RTCEventGroup.NwError:
|
||
this.accessLog.addToErrorLog(id, 'networkError');
|
||
this.rtcComponent.sendNwError(id);
|
||
break;
|
||
default:
|
||
this.logger.error(loggerName$4, `Unknown rtc event eventGroupId:${id}`);
|
||
return;
|
||
}
|
||
this.rtcStore.finalize(id, rtcEvent);
|
||
}
|
||
}
|
||
|
||
const genericTimeWeightedAggregation = (power) => {
|
||
return (entries, windowStartTimestamp) => {
|
||
let totalValue = 0;
|
||
let totalWeight = 0;
|
||
for (const { timestamp, value } of entries) {
|
||
// Math.max(0, ...) to guard against privacy.resistFingerprinting or privacy.reduceTimerPrecision,
|
||
// which may leave expired entries in the array.
|
||
const weight = Math.pow(Math.max(0, timestamp - windowStartTimestamp) / 1000, power);
|
||
totalValue += weight * value;
|
||
totalWeight += weight;
|
||
}
|
||
return totalValue / totalWeight;
|
||
};
|
||
};
|
||
const AggregationImplementations = {
|
||
'uniform-time-weighted': genericTimeWeightedAggregation(0),
|
||
'linear-time-weighted': genericTimeWeightedAggregation(1),
|
||
'quadratic-time-weighted': genericTimeWeightedAggregation(2),
|
||
};
|
||
|
||
/**
|
||
* Returns a new array of intervals where overlapping intervals are joined together
|
||
* into non-overlapping intervals.
|
||
* @param intervals An array of intervals
|
||
* @param join A function that takes in two overlapping intervals and joins them
|
||
*
|
||
* @note this function only joins strict overlaps, interval pairs such as ((1, 2), (2, 3))
|
||
* are not joined.
|
||
*/
|
||
function accumulateBW(intervals) {
|
||
const sorted_intervals = [...intervals].sort((a, b) => {
|
||
if (a.start !== b.start) {
|
||
return a.start - b.start;
|
||
}
|
||
else {
|
||
return a.end - b.end;
|
||
}
|
||
});
|
||
const result = [];
|
||
const insertEntry = function (arr, entry) {
|
||
if (!arr.length) {
|
||
arr.push(entry);
|
||
return;
|
||
}
|
||
for (let i = 0; i < arr.length; i++) {
|
||
if (arr[i].start > entry.start || (arr[i].start === entry.start && arr[i].end > entry.end)) {
|
||
arr.splice(i, 0, entry);
|
||
break;
|
||
}
|
||
}
|
||
};
|
||
while (sorted_intervals.length) {
|
||
const interval = sorted_intervals[0];
|
||
sorted_intervals.shift();
|
||
let lastInterval;
|
||
if (result.length) {
|
||
lastInterval = result[result.length - 1];
|
||
}
|
||
if (result.length === 0 || lastInterval.end <= interval.start) {
|
||
// No overlap
|
||
result.push(interval);
|
||
}
|
||
else if (interval.start === lastInterval.start) {
|
||
// Overlap with same start
|
||
if (interval.end === lastInterval.end) {
|
||
// same end also .. so just accumulate
|
||
lastInterval.bitsPerSec += interval.bitsPerSec;
|
||
}
|
||
else if (!(interval.end < lastInterval.end)) {
|
||
// End of interval in consideration is larger than last interval.
|
||
// Accumulate for the overlap. create an new entry for
|
||
// extended part and insert it back in sorted entries.
|
||
lastInterval.bitsPerSec += interval.bitsPerSec;
|
||
interval.start = lastInterval.end;
|
||
insertEntry(sorted_intervals, interval);
|
||
}
|
||
}
|
||
else {
|
||
// there is initial part that does not overlap
|
||
const prevend = lastInterval.end;
|
||
const prevBitsPerSec = lastInterval.bitsPerSec;
|
||
lastInterval.end = interval.start;
|
||
// make a new entry for the overlap
|
||
const entry = {
|
||
start: interval.start,
|
||
end: Math.min(prevend, interval.end),
|
||
bitsPerSec: interval.bitsPerSec + prevBitsPerSec,
|
||
};
|
||
result.push(entry);
|
||
if (prevend !== interval.end) {
|
||
// create new entry for the extended part and insert it back
|
||
// in sorted entries.
|
||
let start = 0, end = 0, bitsPerSec = 0;
|
||
if (prevend < interval.end) {
|
||
start = prevend;
|
||
end = interval.end;
|
||
bitsPerSec = interval.bitsPerSec;
|
||
}
|
||
else {
|
||
start = interval.end;
|
||
end = prevend;
|
||
bitsPerSec = prevBitsPerSec;
|
||
}
|
||
const newEntry = { start, end, bitsPerSec };
|
||
insertEntry(sorted_intervals, newEntry);
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
class HistoryBasedBandwidthEstimator {
|
||
constructor(windowSize, aggregationMethod = 'quadratic-time-weighted', initValue = {
|
||
avgLatencyMs: NaN,
|
||
avgBandwidth: NaN,
|
||
}) {
|
||
this.windowSize = windowSize;
|
||
this.aggregationMethod = aggregationMethod;
|
||
this.latencyEntries = [];
|
||
this.bandwidthEntries = [];
|
||
this.minEntries = 1;
|
||
this.cleanUpExpiredEntries = this.cleanUpExpiredEntries.bind(this);
|
||
this.bwSubject = new BehaviorSubject(initValue);
|
||
}
|
||
get estimate$() {
|
||
return this.bwSubject.asObservable();
|
||
}
|
||
record(details) {
|
||
// logger.debug(`Recording Bandwidth entry: ${JSON.stringify(details)}`);
|
||
const { trequest, tfirst, tload, bitsDownloaded } = details;
|
||
if (trequest === tload) {
|
||
return;
|
||
}
|
||
this.recordLatency(trequest, tfirst);
|
||
// tfirst tends to be delayed in the beginning of playback, which
|
||
// inflates bandwidth estimation (especially for small downloads)
|
||
// Thus we use trequest to get a more stable measurement of
|
||
// bandwidth. Note that this will decrease the bandwidth
|
||
// estimation by a little (~3% on a 40Mbps network at the office).
|
||
this.recordBandwidth(trequest, tload, (bitsDownloaded * 1000) / (tload - trequest));
|
||
if (this.bwSubject.closed) {
|
||
return;
|
||
}
|
||
const estimate = this.getEstimate();
|
||
this.bwSubject.next(estimate);
|
||
}
|
||
getEstimate() {
|
||
if (this.latencyEntries.length < this.minEntries) {
|
||
return {
|
||
avgLatencyMs: NaN,
|
||
avgBandwidth: NaN,
|
||
};
|
||
}
|
||
const windowStartTimestamp = performance.now() - this.windowSize;
|
||
const aggregationFn = AggregationImplementations[this.aggregationMethod];
|
||
const latencyEntries = this.latencyEntries.map(({ start, end }) => ({
|
||
timestamp: end,
|
||
value: end - start,
|
||
duration: 1,
|
||
}));
|
||
this.bandwidthEntries = accumulateBW(this.bandwidthEntries);
|
||
const bandwidthEntries = this.bandwidthEntries.map(({ start, end, bitsPerSec }) => ({
|
||
timestamp: end,
|
||
duration: 1,
|
||
value: bitsPerSec, // bits
|
||
}));
|
||
return {
|
||
avgLatencyMs: aggregationFn(latencyEntries, windowStartTimestamp),
|
||
avgBandwidth: aggregationFn(bandwidthEntries, windowStartTimestamp),
|
||
};
|
||
}
|
||
getLatest() {
|
||
if (this.latencyEntries.length === 0) {
|
||
return {
|
||
avgLatencyMs: NaN,
|
||
avgBandwidth: NaN,
|
||
};
|
||
}
|
||
const lastLatency = this.latencyEntries[this.latencyEntries.length - 1];
|
||
const lastBw = this.bandwidthEntries[this.bandwidthEntries.length - 1];
|
||
return {
|
||
avgLatencyMs: lastLatency.end - lastLatency.start,
|
||
avgBandwidth: lastBw.bitsPerSec,
|
||
};
|
||
}
|
||
recordLatency(start, end) {
|
||
this.latencyEntries.push({ start, end });
|
||
this.updateCleanupTimeout(end);
|
||
}
|
||
recordBandwidth(start, end, bitsPerSec) {
|
||
this.bandwidthEntries.push({ start, end, bitsPerSec });
|
||
this.updateCleanupTimeout(end);
|
||
}
|
||
setCleanupTimeout(cleanupTimestamp) {
|
||
this.cleanupTimeout = setTimeout(this.cleanUpExpiredEntries, Math.max(cleanupTimestamp - performance.now(), 0));
|
||
this.cleanupTimestamp = cleanupTimestamp;
|
||
}
|
||
clearCleanupTimeout() {
|
||
if (typeof this.cleanupTimeout !== 'undefined') {
|
||
clearTimeout(this.cleanupTimeout);
|
||
this.cleanupTimeout = undefined;
|
||
}
|
||
this.cleanupTimestamp = undefined;
|
||
}
|
||
updateCleanupTimeout(timestamp) {
|
||
const cleanupTimestamp = timestamp + this.windowSize;
|
||
if (!this.cleanupTimestamp || cleanupTimestamp < this.cleanupTimestamp) {
|
||
this.clearCleanupTimeout();
|
||
this.setCleanupTimeout(cleanupTimestamp);
|
||
}
|
||
}
|
||
cleanUpExpiredEntries() {
|
||
this.clearCleanupTimeout();
|
||
const windowStartTimestamp = performance.now() - this.windowSize;
|
||
this.latencyEntries = this.latencyEntries.filter((entry) => entry.end >= windowStartTimestamp);
|
||
this.bandwidthEntries = this.bandwidthEntries.filter((entry) => entry.end >= windowStartTimestamp);
|
||
// during cleanup
|
||
if (!this.bwSubject.closed) {
|
||
this.bwSubject.next(this.getEstimate());
|
||
}
|
||
if (this.latencyEntries.length > 0 || this.bandwidthEntries.length > 0) {
|
||
const timestampOfEarliestEntry = Math.min(...this.latencyEntries.map((entry) => entry.end), ...this.bandwidthEntries.map((entry) => entry.end));
|
||
this.updateCleanupTimeout(timestampOfEarliestEntry);
|
||
}
|
||
}
|
||
destroy() {
|
||
this.clearCleanupTimeout();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Helper functions that deal with storing data on device storage.
|
||
*/
|
||
const PersistStats = {
|
||
setCombinedEstimate: function (hlsStorage, estimate, serviceName) {
|
||
const logger = getLogger();
|
||
if (typeof hlsStorage.storage.set === 'undefined') {
|
||
logger.warn('storage.set is not supported! Not persisting bandwidth estimates');
|
||
return;
|
||
}
|
||
// splitting into two different records
|
||
const bwStorageKey = hlsStorage.bandwidthHistoryStorageKey;
|
||
const bwEstimate = { avgLatencyMs: estimate.avgLatencyMs, avgBandwidth: estimate.avgBandwidth };
|
||
const bwRecord = Object.assign({}, bwEstimate, { expires: Date.now() + hlsStorage.bandwidthHistoryTTL });
|
||
try {
|
||
hlsStorage.storage.set(bwStorageKey, JSON.stringify(bwRecord));
|
||
}
|
||
catch (err) {
|
||
logger.warn(`Error stringifying! Not persisting bandwidth estimates: ${err.message}`);
|
||
}
|
||
const serviceStats = {
|
||
maxDuration: estimate.maxDurationSec,
|
||
avgFragParseTimeMs: estimate.avgParseTimeMs,
|
||
avgFragBufferCreationDelayMs: estimate.avgBufferCreateMs,
|
||
avgPlaylistLoadTimeMs: estimate.avgPlaylistLoadTimeMs,
|
||
avgPlaylistParseTimeMs: estimate.avgPlaylistParseTimeMs,
|
||
avgInitFragAppendMs: estimate.avgInitFragAppendMs,
|
||
avgDataFragAppendMs: estimate.avgDataFragAppendMs,
|
||
};
|
||
let storageKey = hlsStorage.storageKeyPrefix;
|
||
if (serviceName) {
|
||
storageKey += serviceName;
|
||
}
|
||
try {
|
||
hlsStorage.storage.set(storageKey, JSON.stringify(serviceStats));
|
||
}
|
||
catch (err) {
|
||
logger.warn(`Error stringifying! Not persisting bandwidth estimates: ${err.message}`);
|
||
}
|
||
},
|
||
getCombinedEstimate: function (hlsStorage, serviceName) {
|
||
const logger = getLogger();
|
||
let combinedEstimate = {};
|
||
if (typeof hlsStorage.storage.get === 'undefined') {
|
||
logger.warn('storage.get is not supported! unable to retreive bandwidth estimates');
|
||
return this.convertStorageJsonToCombinedEstimate(combinedEstimate);
|
||
}
|
||
try {
|
||
let bwParsed = JSON.parse(hlsStorage.storage.get(hlsStorage.bandwidthHistoryStorageKey));
|
||
if ((bwParsed === null || bwParsed === void 0 ? void 0 : bwParsed.expires) && bwParsed.expires < Date.now()) {
|
||
bwParsed = null;
|
||
}
|
||
else {
|
||
bwParsed = { avgLatencyMs: bwParsed === null || bwParsed === void 0 ? void 0 : bwParsed.avgLatencyMs, avgBandwidth: bwParsed === null || bwParsed === void 0 ? void 0 : bwParsed.avgBandwidth };
|
||
}
|
||
combinedEstimate = Object.assign(Object.assign({}, combinedEstimate), bwParsed);
|
||
}
|
||
catch (err) {
|
||
logger.warn(`Unable to get persisted bandwidth history: ${err.message}`);
|
||
}
|
||
let storageKey = hlsStorage.storageKeyPrefix;
|
||
if (serviceName) {
|
||
storageKey += serviceName;
|
||
}
|
||
try {
|
||
const restEstimateParsed = JSON.parse(hlsStorage.storage.get(storageKey));
|
||
combinedEstimate = Object.assign(Object.assign({}, combinedEstimate), restEstimateParsed);
|
||
}
|
||
catch (err) {
|
||
logger.warn(`Unable to get persisted bandwidth history: ${err.message}`);
|
||
}
|
||
return this.convertStorageJsonToCombinedEstimate(combinedEstimate);
|
||
},
|
||
convertStorageJsonToCombinedEstimate: function (storageJson) {
|
||
const combinedEstimate = {
|
||
avgLatencyMs: (storageJson === null || storageJson === void 0 ? void 0 : storageJson.avgLatencyMs) || NaN,
|
||
avgBandwidth: (storageJson === null || storageJson === void 0 ? void 0 : storageJson.avgBandwidth) || NaN,
|
||
maxDurationSec: (storageJson === null || storageJson === void 0 ? void 0 : storageJson.maxDuration) || NaN,
|
||
avgParseTimeMs: (storageJson === null || storageJson === void 0 ? void 0 : storageJson.avgFragParseTimeMs) || NaN,
|
||
avgBufferCreateMs: (storageJson === null || storageJson === void 0 ? void 0 : storageJson.avgFragBufferCreationDelayMs) || NaN,
|
||
avgPlaylistLoadTimeMs: (storageJson === null || storageJson === void 0 ? void 0 : storageJson.avgPlaylistLoadTimeMs) || NaN,
|
||
avgPlaylistParseTimeMs: (storageJson === null || storageJson === void 0 ? void 0 : storageJson.avgPlaylistParseTimeMs) || NaN,
|
||
avgInitFragAppendMs: (storageJson === null || storageJson === void 0 ? void 0 : storageJson.avgInitFragAppendMs) || NaN,
|
||
avgDataFragAppendMs: (storageJson === null || storageJson === void 0 ? void 0 : storageJson.avgDataFragAppendMs) || NaN,
|
||
};
|
||
return combinedEstimate;
|
||
},
|
||
getBandwidthEstimate: function (hlsStorage, serviceName) {
|
||
const estimate = this.getCombinedEstimate(hlsStorage, serviceName);
|
||
const bwEstimate = { avgLatencyMs: estimate === null || estimate === void 0 ? void 0 : estimate.avgLatencyMs, avgBandwidth: estimate === null || estimate === void 0 ? void 0 : estimate.avgBandwidth };
|
||
if (!isFiniteNumber(bwEstimate.avgLatencyMs)) {
|
||
bwEstimate.avgLatencyMs = NaN;
|
||
}
|
||
if (!isFiniteNumber(bwEstimate.avgBandwidth)) {
|
||
bwEstimate.avgBandwidth = NaN;
|
||
}
|
||
return bwEstimate;
|
||
},
|
||
getPlaylistEstimate: function (hlsStorage, serviceName) {
|
||
const estimate = this.getCombinedEstimate(hlsStorage, serviceName);
|
||
const playlistEstimate = { avgPlaylistLoadTimeMs: estimate === null || estimate === void 0 ? void 0 : estimate.avgPlaylistLoadTimeMs, avgPlaylistParseTimeMs: estimate === null || estimate === void 0 ? void 0 : estimate.avgPlaylistParseTimeMs };
|
||
if (!isFiniteNumber(playlistEstimate.avgPlaylistLoadTimeMs)) {
|
||
playlistEstimate.avgPlaylistLoadTimeMs = NaN;
|
||
}
|
||
if (!isFiniteNumber(playlistEstimate.avgPlaylistParseTimeMs)) {
|
||
playlistEstimate.avgPlaylistParseTimeMs = NaN;
|
||
}
|
||
return playlistEstimate;
|
||
},
|
||
getFragEstimate: function (hlsStorage, serviceName) {
|
||
const estimate = this.getCombinedEstimate(hlsStorage, serviceName);
|
||
const fragEstimate = { maxDurationSec: estimate === null || estimate === void 0 ? void 0 : estimate.maxDurationSec, avgParseTimeMs: estimate === null || estimate === void 0 ? void 0 : estimate.avgParseTimeMs };
|
||
if (!isFiniteNumber(fragEstimate.maxDurationSec)) {
|
||
fragEstimate.maxDurationSec = NaN;
|
||
}
|
||
if (!isFiniteNumber(fragEstimate.avgParseTimeMs)) {
|
||
fragEstimate.avgParseTimeMs = NaN;
|
||
}
|
||
return fragEstimate;
|
||
},
|
||
getBufferEstimate: function (hlsStorage, serviceName) {
|
||
const estimate = this.getCombinedEstimate(hlsStorage, serviceName);
|
||
const bufferEstimate = {
|
||
avgBufferCreateMs: estimate === null || estimate === void 0 ? void 0 : estimate.avgBufferCreateMs,
|
||
avgInitFragAppendMs: estimate === null || estimate === void 0 ? void 0 : estimate.avgInitFragAppendMs,
|
||
avgDataFragAppendMs: estimate === null || estimate === void 0 ? void 0 : estimate.avgDataFragAppendMs,
|
||
};
|
||
if (!isFiniteNumber(bufferEstimate.avgBufferCreateMs)) {
|
||
bufferEstimate.avgBufferCreateMs = NaN;
|
||
}
|
||
if (!isFiniteNumber(bufferEstimate.avgInitFragAppendMs)) {
|
||
bufferEstimate.avgInitFragAppendMs = NaN;
|
||
}
|
||
if (!isFiniteNumber(bufferEstimate.avgDataFragAppendMs)) {
|
||
bufferEstimate.avgDataFragAppendMs = NaN;
|
||
}
|
||
return bufferEstimate;
|
||
},
|
||
};
|
||
var PersistStats$1 = PersistStats;
|
||
|
||
// Accumulator for doing simple stats
|
||
class SimpleAccumulator {
|
||
constructor(_minSamples = 0) {
|
||
this._minSamples = _minSamples;
|
||
this._sum = 0;
|
||
this._max = Number.NEGATIVE_INFINITY;
|
||
this._numSamples = 0;
|
||
}
|
||
get avg() {
|
||
if (this._numSamples < this._minSamples) {
|
||
return NaN;
|
||
}
|
||
return this._sum / this._numSamples;
|
||
}
|
||
get max() {
|
||
return this.count > 0 ? this._max : NaN;
|
||
}
|
||
get count() {
|
||
return this._numSamples;
|
||
}
|
||
reset() {
|
||
this._sum = 0;
|
||
this._numSamples = 0;
|
||
this._max = Number.NEGATIVE_INFINITY;
|
||
}
|
||
add(value) {
|
||
this._sum += value;
|
||
this._max = Math.max(this._max, value);
|
||
++this._numSamples;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief Query interface to the stats store
|
||
*/
|
||
class StatsQuery extends QueryEntity {
|
||
constructor(statsStore, id) {
|
||
super(statsStore);
|
||
this.id = id;
|
||
}
|
||
getBandwidthEstimate(hlsStorage, serviceName) {
|
||
var _a;
|
||
const bwEstimate = Object.assign({}, (_a = this.statsEntity) === null || _a === void 0 ? void 0 : _a.bandwidthEstimate);
|
||
if (isFiniteNumber(bwEstimate.avgBandwidth) && isFiniteNumber(bwEstimate.avgLatencyMs)) {
|
||
return bwEstimate;
|
||
}
|
||
// fallback: check in persistent storage
|
||
if (hlsStorage) {
|
||
const parsedEstimate = PersistStats.getBandwidthEstimate(hlsStorage, serviceName);
|
||
if (!isFiniteNumber(bwEstimate.avgBandwidth)) {
|
||
bwEstimate.avgBandwidth = parsedEstimate.avgBandwidth;
|
||
}
|
||
if (!isFiniteNumber(bwEstimate.avgLatencyMs)) {
|
||
bwEstimate.avgLatencyMs = parsedEstimate.avgLatencyMs;
|
||
}
|
||
}
|
||
return bwEstimate;
|
||
}
|
||
getPlaylistEstimate(hlsStorage, serviceName) {
|
||
var _a;
|
||
const playlistEstimate = Object.assign({}, (_a = this.statsEntity) === null || _a === void 0 ? void 0 : _a.playlistEstimate);
|
||
const checkValidity = (playlistEstimate) => {
|
||
return isFiniteNumber(playlistEstimate.avgPlaylistLoadTimeMs) && isFiniteNumber(playlistEstimate.avgPlaylistParseTimeMs);
|
||
};
|
||
if (checkValidity(playlistEstimate)) {
|
||
return playlistEstimate;
|
||
}
|
||
// fallback: check in persistent storage
|
||
if (hlsStorage) {
|
||
const parsedEstimate = PersistStats.getPlaylistEstimate(hlsStorage, serviceName);
|
||
if (!isFiniteNumber(playlistEstimate.avgPlaylistLoadTimeMs)) {
|
||
playlistEstimate.avgPlaylistLoadTimeMs = parsedEstimate.avgPlaylistLoadTimeMs;
|
||
}
|
||
if (!isFiniteNumber(playlistEstimate.avgPlaylistParseTimeMs)) {
|
||
playlistEstimate.avgPlaylistParseTimeMs = parsedEstimate.avgPlaylistParseTimeMs;
|
||
}
|
||
if (checkValidity(playlistEstimate)) {
|
||
return playlistEstimate;
|
||
}
|
||
//pick config default
|
||
if (!isFiniteNumber(playlistEstimate.avgPlaylistLoadTimeMs)) {
|
||
playlistEstimate.avgPlaylistLoadTimeMs = hlsStorage.statDefaults.playlistLoadTimeMs;
|
||
}
|
||
if (!isFiniteNumber(playlistEstimate.avgPlaylistParseTimeMs)) {
|
||
playlistEstimate.avgPlaylistParseTimeMs = hlsStorage.statDefaults.playlistParseTimeMs;
|
||
}
|
||
}
|
||
return playlistEstimate;
|
||
}
|
||
getBufferEstimate(hlsStorage, serviceName) {
|
||
var _a;
|
||
const bufferEstimate = Object.assign({}, (_a = this.statsEntity) === null || _a === void 0 ? void 0 : _a.bufferEstimate);
|
||
const checkValidity = (bufferEstimate) => {
|
||
return isFiniteNumber(bufferEstimate.avgBufferCreateMs) && isFiniteNumber(bufferEstimate.avgDataFragAppendMs) && isFiniteNumber(bufferEstimate.avgInitFragAppendMs);
|
||
};
|
||
if (checkValidity(bufferEstimate)) {
|
||
return bufferEstimate;
|
||
}
|
||
// fallback: check in persistent storage
|
||
if (hlsStorage) {
|
||
const parsedEstimate = PersistStats.getBufferEstimate(hlsStorage, serviceName);
|
||
if (!isFiniteNumber(bufferEstimate.avgBufferCreateMs)) {
|
||
bufferEstimate.avgBufferCreateMs = parsedEstimate.avgBufferCreateMs;
|
||
}
|
||
if (!isFiniteNumber(bufferEstimate.avgDataFragAppendMs)) {
|
||
bufferEstimate.avgDataFragAppendMs = parsedEstimate.avgDataFragAppendMs;
|
||
}
|
||
if (!isFiniteNumber(bufferEstimate.avgInitFragAppendMs)) {
|
||
bufferEstimate.avgInitFragAppendMs = parsedEstimate.avgInitFragAppendMs;
|
||
}
|
||
if (checkValidity(bufferEstimate)) {
|
||
return bufferEstimate;
|
||
}
|
||
//pick config default
|
||
if (!isFiniteNumber(bufferEstimate.avgBufferCreateMs)) {
|
||
bufferEstimate.avgBufferCreateMs = hlsStorage.statDefaults.fragBufferCreationDelayMs;
|
||
}
|
||
if (!isFiniteNumber(bufferEstimate.avgDataFragAppendMs)) {
|
||
bufferEstimate.avgDataFragAppendMs = hlsStorage.statDefaults.dataFragAppendMs;
|
||
}
|
||
if (!isFiniteNumber(bufferEstimate.avgInitFragAppendMs)) {
|
||
bufferEstimate.avgInitFragAppendMs = hlsStorage.statDefaults.initFragAppendMs;
|
||
}
|
||
}
|
||
return bufferEstimate;
|
||
}
|
||
getFragEstimate(hlsStorage, serviceName) {
|
||
var _a;
|
||
const fragEstimate = Object.assign({}, (_a = this.statsEntity) === null || _a === void 0 ? void 0 : _a.fragEstimate);
|
||
const checkValidity = (fragEstimate) => {
|
||
return isFiniteNumber(fragEstimate.maxDurationSec) && isFiniteNumber(fragEstimate.avgParseTimeMs);
|
||
};
|
||
if (checkValidity(fragEstimate)) {
|
||
return fragEstimate;
|
||
}
|
||
// fallback: check in persistent storage
|
||
if (hlsStorage) {
|
||
const parsedEstimate = PersistStats.getFragEstimate(hlsStorage, serviceName);
|
||
if (!isFiniteNumber(fragEstimate.maxDurationSec)) {
|
||
fragEstimate.maxDurationSec = parsedEstimate.maxDurationSec;
|
||
}
|
||
if (!isFiniteNumber(fragEstimate.avgParseTimeMs)) {
|
||
fragEstimate.avgParseTimeMs = parsedEstimate.avgParseTimeMs;
|
||
}
|
||
if (checkValidity(fragEstimate)) {
|
||
return fragEstimate;
|
||
}
|
||
//pick config default
|
||
if (!isFiniteNumber(fragEstimate.maxDurationSec)) {
|
||
fragEstimate.maxDurationSec = hlsStorage.defaultTargetDuration;
|
||
}
|
||
if (!isFiniteNumber(fragEstimate.avgParseTimeMs)) {
|
||
fragEstimate.avgParseTimeMs = hlsStorage.statDefaults.fragParseTimeMs;
|
||
}
|
||
}
|
||
return fragEstimate;
|
||
}
|
||
getCombinedEstimate() {
|
||
return Object.assign(Object.assign(Object.assign(Object.assign({}, this.getFragEstimate()), this.getPlaylistEstimate()), this.getBufferEstimate()), this.getBandwidthEstimate());
|
||
}
|
||
// getters
|
||
get statsEntity() {
|
||
return this.getEntity(this.id);
|
||
}
|
||
get bandwidthSample() {
|
||
var _a;
|
||
return (_a = this.statsEntity) === null || _a === void 0 ? void 0 : _a.bandwidthSample;
|
||
}
|
||
get bandwidthStatus() {
|
||
var _a;
|
||
return (_a = this.statsEntity) === null || _a === void 0 ? void 0 : _a.bandwidthStatus;
|
||
}
|
||
get fragSample() {
|
||
var _a;
|
||
return (_a = this.statsEntity) === null || _a === void 0 ? void 0 : _a.fragSample;
|
||
}
|
||
// Aggregated data
|
||
get bandwidthEstimate$() {
|
||
return this.selectEntity(this.id, 'bandwidthEstimate');
|
||
}
|
||
get fragEstimate$() {
|
||
return this.selectEntity(this.id, 'fragEstimate');
|
||
}
|
||
get playlistEstimate$() {
|
||
return this.selectEntity(this.id, 'playlistEstimate');
|
||
}
|
||
get bufferEstimate$() {
|
||
return this.selectEntity(this.id, 'bufferEstimate');
|
||
}
|
||
// Individual samples
|
||
get bandwidthSample$() {
|
||
return this.selectEntity(this.id, ({ bandwidthSample }) => bandwidthSample).pipe(filterNullOrUndefined());
|
||
}
|
||
get fragSample$() {
|
||
return this.selectEntity(this.id, ({ fragSample }) => fragSample).pipe(filterNullOrUndefined());
|
||
}
|
||
get playlistSample$() {
|
||
return this.selectEntity(this.id, ({ playlistSample }) => playlistSample).pipe(filterNullOrUndefined());
|
||
}
|
||
get bufferMetric$() {
|
||
return this.selectEntity(this.id, ({ bufferMetric }) => bufferMetric).pipe(filterNullOrUndefined());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief Backing store for stats
|
||
*/
|
||
/**
|
||
* @brief Store that keeps track of stats measured for network requests,
|
||
* demuxing, and buffering
|
||
*/
|
||
class StatsStore extends EntityStore {
|
||
constructor() {
|
||
super({}, { name: 'stats-store', producerFn: produce_1 });
|
||
}
|
||
set statsEntity(statsEntity) {
|
||
logAction('statsStore.set.stats');
|
||
applyTransaction(() => {
|
||
this.add(statsEntity);
|
||
this.setActive(statsEntity.id);
|
||
});
|
||
}
|
||
set playlistSample(playlistSample) {
|
||
logAction(`stats.set.playlistSample: ${playlistSample}`);
|
||
this.updateActive((statsEntity) => {
|
||
statsEntity.playlistSample = playlistSample;
|
||
});
|
||
}
|
||
set bandwidthSample(bandwidthSample) {
|
||
logAction(`stats.set.bandwidthSample: ${bandwidthSample}`);
|
||
this.updateActive((statsEntity) => {
|
||
statsEntity.bandwidthSample = bandwidthSample;
|
||
statsEntity.bandwidthStatus.bandwidthSampleCount += 1;
|
||
statsEntity.bandwidthStatus.instantBw = (bandwidthSample.loaded * 8000) / (bandwidthSample.tload - bandwidthSample.trequest);
|
||
});
|
||
}
|
||
set fragSample(fragSample) {
|
||
logAction(`stats.set.fragSample: ${fragSample}`);
|
||
this.updateActive((statsEntity) => {
|
||
statsEntity.fragSample = fragSample;
|
||
});
|
||
}
|
||
set bufferMetric(bufferMetric) {
|
||
logAction(`stats.set.bufferMetric: ${bufferMetric}`);
|
||
this.updateActive((statsEntity) => {
|
||
statsEntity.bufferMetric = bufferMetric;
|
||
});
|
||
}
|
||
set bandwidthEstimate(bandwidthEstimate) {
|
||
logAction(`stats.set.bandwidthEstimate: ${bandwidthEstimate}`);
|
||
this.updateActive((statsEntity) => {
|
||
statsEntity.bandwidthEstimate = bandwidthEstimate;
|
||
});
|
||
}
|
||
set fragEstimate(fragEstimate) {
|
||
logAction(`stats.set.fragEstimate: ${fragEstimate}`);
|
||
this.updateActive((statsEntity) => {
|
||
statsEntity.fragEstimate = fragEstimate;
|
||
});
|
||
}
|
||
set playlistEstimate(playlistEstimate) {
|
||
logAction(`stats.set.playlistEstimate: ${playlistEstimate}`);
|
||
this.updateActive((statsEntity) => {
|
||
statsEntity.playlistEstimate = playlistEstimate;
|
||
});
|
||
}
|
||
set bufferEstimate(bufferEstimate) {
|
||
logAction(`stats.set.bufferEstimate: ${bufferEstimate}`);
|
||
this.updateActive((statsEntity) => {
|
||
statsEntity.bufferEstimate = bufferEstimate;
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief Service for managing stats samples and estimates
|
||
*/
|
||
class StatsService {
|
||
constructor(statsStore) {
|
||
this.statsStore = statsStore;
|
||
}
|
||
getQuery() {
|
||
return new QueryEntity(this.statsStore);
|
||
}
|
||
getQueryForItem(itemId) {
|
||
return new StatsQuery(this.statsStore, itemId);
|
||
}
|
||
remove(itemId) {
|
||
this.statsStore.remove(itemId);
|
||
}
|
||
removeAll() {
|
||
this.statsStore.remove();
|
||
}
|
||
setBandwidthSample(bandwidthSample) {
|
||
this.statsStore.bandwidthSample = bandwidthSample;
|
||
}
|
||
setFragSample(fragSample) {
|
||
this.statsStore.fragSample = fragSample;
|
||
}
|
||
setPlaylistSample(playlistSample) {
|
||
this.statsStore.playlistSample = playlistSample;
|
||
}
|
||
setBufferMetric(bufferMetric) {
|
||
this.statsStore.bufferMetric = bufferMetric;
|
||
}
|
||
setBandwidthEstimate(bandwidthEstimate) {
|
||
this.statsStore.bandwidthEstimate = bandwidthEstimate;
|
||
}
|
||
setFragEstimate(fragEstimate) {
|
||
this.statsStore.fragEstimate = fragEstimate;
|
||
}
|
||
setPlaylistEstimate(playlistEstimate) {
|
||
this.statsStore.playlistEstimate = playlistEstimate;
|
||
}
|
||
setBufferEstimate(bufferEstimate) {
|
||
this.statsStore.bufferEstimate = bufferEstimate;
|
||
}
|
||
}
|
||
const statsStore = new StatsStore();
|
||
let statsService = null; //
|
||
const createStatsQuery = (id) => {
|
||
return new StatsQuery(statsStore, id);
|
||
};
|
||
function statsServiceSingleton() {
|
||
if (!statsService) {
|
||
statsService = new StatsService(statsStore);
|
||
}
|
||
return statsService;
|
||
}
|
||
const initializeStatsFunc = (itemId) => {
|
||
const statsQuery = createStatsQuery(itemId);
|
||
if (statsQuery.hasEntity(itemId)) {
|
||
return of(statsQuery);
|
||
}
|
||
return addStatsEntity(statsStore, itemId);
|
||
};
|
||
function addStatsEntity(statsStore, itemId) {
|
||
logAction('stats.loading');
|
||
statsStore.setLoading(true);
|
||
const statsEntity = {
|
||
id: itemId,
|
||
// bandwidthSample: { trequest: 0, tfirst: 0, tload: 0, loaded: 0, total: 0, complete: true },
|
||
bandwidthEstimate: { avgLatencyMs: NaN, avgBandwidth: NaN },
|
||
bandwidthStatus: { bandwidthSampleCount: 0, instantBw: NaN },
|
||
// fragSample: { duration: 0, parseTimeMs: 0 },
|
||
fragEstimate: { maxDurationSec: NaN, avgParseTimeMs: NaN },
|
||
// playlistSample: { playlistLoadTimeMs: 0, playlistParseTimeMs: 0 },
|
||
playlistEstimate: { avgPlaylistLoadTimeMs: NaN, avgPlaylistParseTimeMs: NaN },
|
||
// bufferMetric: {
|
||
// fragmentType: MediaOptionType.Variant,
|
||
// startInitAppend: 0,
|
||
// endInitAppend: 0,
|
||
// initBytesAppend: 0,
|
||
// startDataAppend: 0,
|
||
// endDataAppend: 0,
|
||
// dataBytesAppend: 0,
|
||
// bufferCreationStart: 0,
|
||
// bufferCreationEnd: 0,
|
||
// },
|
||
bufferEstimate: {
|
||
avgBufferCreateMs: NaN,
|
||
avgInitFragAppendMs: NaN,
|
||
avgDataFragAppendMs: NaN,
|
||
},
|
||
};
|
||
statsStore.statsEntity = statsEntity;
|
||
statsStore.setLoading(false);
|
||
logAction('stats.loaded');
|
||
}
|
||
/**
|
||
* @brief Service that retrieves past stats and seeds
|
||
* the current sample and add to the store
|
||
*/
|
||
/*
|
||
export const initializeStats = () => (pipelineActionSource$: Observable<[RootPlaylistQuery, MediaElementQuery]>) => {
|
||
return pipelineActionSource$.pipe(
|
||
tag('stats.initializing'),
|
||
switchMap(item => {
|
||
if (!item) return EMPTY;
|
||
|
||
const { itemId } = item;
|
||
// const configurationQuery = createConfigurationQuery();
|
||
const statsQuery = createStatsQuery(itemId);
|
||
if(statsQuery.hasEntity(itemId)) {
|
||
return of(statsQuery);
|
||
}
|
||
statsStore.setLoading(true);
|
||
return of(item).pipe(
|
||
map(() => {
|
||
logAction(`stats.loaded`);
|
||
// TODO get it from persistent store
|
||
// or rebuild from configs
|
||
const statsEntity: StatsEntity = {
|
||
id: itemId,
|
||
bandwidthSample: {trequest: 0, tparsed: 0, tfirst: 0, tload: 0, loaded: 0},
|
||
bandwidthEstimate: {avgLatencyMs: 0, avgBandwidth: 0},
|
||
fragSample: { duration: 0, parseTimeMs: 0, bufferCreationDelayMs: 0, bufferTimeMs: 0},
|
||
fragEstimate: { maxDuration: 0, maxParseTimeMs: 0, maxBufferCreationDelayMs: 0, maxBufferTimeMs: 0 },
|
||
playlistSample: { playlistLoadTimeMs: 10 },
|
||
playlistEstimate: { maxPlaylistLoadTimeMs: 10}
|
||
};
|
||
statsStore.statsEntity = statsEntity;
|
||
return statsQuery;
|
||
}),
|
||
finalize(() => {
|
||
statsStore.setLoading(false);
|
||
})
|
||
);
|
||
}),
|
||
tag('stats.initialized'),
|
||
);
|
||
}
|
||
*/
|
||
|
||
function objectIsEqual(a, b) {
|
||
if (a === b)
|
||
return true;
|
||
if (!a || !b)
|
||
return false;
|
||
let same = Object.keys(a).length === Object.keys(b).length;
|
||
for (const k of Object.keys(a)) {
|
||
same = same && ((isNaN(a[k]) && isNaN(b[k])) || a[k] === b[k]);
|
||
}
|
||
return same;
|
||
}
|
||
/**
|
||
Stats processing Epic. Listens to samples (bandwidth, playlist,
|
||
frag) and processes the data and sends back estimates to the store
|
||
*/
|
||
const statsBandwidthProcessingEpic = (config, statsService, logger) => (bandwidthSampleSource$) => {
|
||
return new Observable((subscriber) => {
|
||
let historyBasedBandwidthEstimator = new HistoryBasedBandwidthEstimator(config.bandwidthHistoryWindowSize, config.bandwidthHistoryAggregationMethod, { avgLatencyMs: NaN, avgBandwidth: NaN });
|
||
const estimate$ = historyBasedBandwidthEstimator.estimate$;
|
||
const sub = merge(
|
||
// feed the estimator
|
||
bandwidthSampleSource$.pipe(filter((sample) => sample.complete), tap((sample) => {
|
||
logger.qe({
|
||
critical: true,
|
||
name: 'bandwidthRecordDetails',
|
||
data: {
|
||
type: MediaOptionNames[sample.mediaOptionType],
|
||
observedBitrateExcludingLatency: ((sample.loaded * 8) / (sample.tload - sample.tfirst)) * 1000,
|
||
observedBitrate: ((sample.loaded * 8) / (sample.tload - sample.trequest)) * 1000,
|
||
},
|
||
});
|
||
}), map((bandwidthSample) => ({
|
||
trequest: bandwidthSample.trequest,
|
||
tfirst: bandwidthSample.tfirst,
|
||
tload: bandwidthSample.tload,
|
||
bitsDownloaded: bandwidthSample.loaded * 8,
|
||
})), tag('statsBandwidthProcessingEpic.in'), switchMap((value) => {
|
||
historyBasedBandwidthEstimator.record(value);
|
||
return EMPTY;
|
||
})), estimate$.pipe(distinctUntilChanged(), tag('statsBandwidthProcessingEpic.change'), tap((value) => {
|
||
if (statsService) {
|
||
statsService.setBandwidthEstimate(value);
|
||
logger.qe({ critical: true, name: 'bandwidthEstimate', data: { bandwidthEstimate: value } });
|
||
}
|
||
}))).subscribe(subscriber);
|
||
return () => {
|
||
sub.unsubscribe();
|
||
historyBasedBandwidthEstimator.destroy();
|
||
historyBasedBandwidthEstimator = undefined;
|
||
};
|
||
});
|
||
};
|
||
const statsFragProcessingEpic = (config, statsService) => (fragSampleSource$) => {
|
||
return fragSampleSource$.pipe(tag('statsFragProcessingEpic.in'), scan((acc, sample) => {
|
||
acc.durationSec.add(sample.durationSec);
|
||
acc.fragParseMs.add(sample.parseTimeMs);
|
||
return acc;
|
||
}, {
|
||
durationSec: new SimpleAccumulator(),
|
||
fragParseMs: new SimpleAccumulator(config.minFragmentCount),
|
||
}), map((value) => ({
|
||
maxDurationSec: value.durationSec.max,
|
||
avgParseTimeMs: value.fragParseMs.avg,
|
||
})), distinctUntilChanged(objectIsEqual), tap((estimate) => statsService.setFragEstimate(estimate)));
|
||
};
|
||
const statsPlaylistProcessingEpic = (config, statsService) => (playlistSampleSource$) => {
|
||
return playlistSampleSource$.pipe(tag('statsPlaylistProcessingEpic.in'), scan((acc, sample) => {
|
||
acc.playlistLoadTimeMs.add(sample.playlistLoadTimeMs);
|
||
acc.playlistParseTimeMs.add(sample.playlistParseTimeMs);
|
||
return acc;
|
||
}, {
|
||
playlistLoadTimeMs: new SimpleAccumulator(config.minPlaylistCount),
|
||
playlistParseTimeMs: new SimpleAccumulator(config.minPlaylistCount),
|
||
}), map((value) => ({
|
||
avgPlaylistLoadTimeMs: value.playlistLoadTimeMs.avg,
|
||
avgPlaylistParseTimeMs: value.playlistParseTimeMs.avg,
|
||
})), distinctUntilChanged(objectIsEqual), tap((estimate) => {
|
||
statsService.setPlaylistEstimate(estimate);
|
||
}));
|
||
};
|
||
const statsBufferProcessingEpic = (config, statsService) => (bufferMetricSource$) => {
|
||
return bufferMetricSource$.pipe(tag('statsBufferMetricProcessingEpic.in'), scan((acc, metric) => {
|
||
if (isFiniteNumber(metric.bufferCreationStart) && isFiniteNumber(metric.bufferCreationEnd)) {
|
||
acc.bufferCreateMs.add(metric.bufferCreationEnd - metric.bufferCreationStart);
|
||
}
|
||
if (isFiniteNumber(metric.startInitAppend) && isFiniteNumber(metric.endInitAppend)) {
|
||
acc.initFragAppendMs.add(metric.endInitAppend - metric.startInitAppend);
|
||
}
|
||
if (isFiniteNumber(metric.startDataAppend) && isFiniteNumber(metric.endDataAppend)) {
|
||
acc.dataFragAppendMs.add(metric.endDataAppend - metric.startDataAppend);
|
||
}
|
||
return acc;
|
||
}, {
|
||
// Seed value
|
||
bufferCreateMs: new SimpleAccumulator(),
|
||
initFragAppendMs: new SimpleAccumulator(),
|
||
dataFragAppendMs: new SimpleAccumulator(config.minFragmentCount),
|
||
}), map((value) => ({
|
||
avgBufferCreateMs: value.bufferCreateMs.avg,
|
||
avgInitFragAppendMs: value.initFragAppendMs.avg,
|
||
avgDataFragAppendMs: value.dataFragAppendMs.avg,
|
||
})), distinctUntilChanged(objectIsEqual), tap((estimate) => {
|
||
statsService.setBufferEstimate(estimate);
|
||
}));
|
||
};
|
||
function statsProcessor(config, statsService, item, logger) {
|
||
return new Observable((subscriber) => {
|
||
initializeStatsFunc(item.itemId);
|
||
const statsQuery = createStatsQuery(item.itemId);
|
||
const { fragSample$, playlistSample$, bandwidthSample$, bufferMetric$ } = statsQuery;
|
||
merge(playlistSample$.pipe(observeOn(asyncScheduler), statsPlaylistProcessingEpic(config, statsService)), bandwidthSample$.pipe(observeOn(asyncScheduler), statsBandwidthProcessingEpic(config, statsService, logger)), fragSample$.pipe(observeOn(asyncScheduler), statsFragProcessingEpic(config, statsService)), bufferMetric$.pipe(observeOn(asyncScheduler), statsBufferProcessingEpic(config, statsService)))
|
||
.pipe(switchMapTo(EMPTY))
|
||
.subscribe(subscriber);
|
||
return () => {
|
||
PersistStats$1.setCombinedEstimate(config, Object.assign(Object.assign(Object.assign(Object.assign({}, statsQuery.getFragEstimate()), statsQuery.getPlaylistEstimate()), statsQuery.getBufferEstimate()), statsQuery.getBandwidthEstimate()), item.serviceName);
|
||
statsService.remove(item.itemId);
|
||
};
|
||
});
|
||
}
|
||
|
||
/**
|
||
* MediaSource helper
|
||
*/
|
||
function getMediaSource() {
|
||
return window.MediaSource || window.WebKitMediaSource;
|
||
}
|
||
|
||
/**
|
||
* MediaSource helper
|
||
*/
|
||
function isSupported() {
|
||
try {
|
||
const mediaSource = getMediaSource();
|
||
const sourceBuffer = window.SourceBuffer || window.WebKitSourceBuffer;
|
||
const isTypeSupported = mediaSource && typeof mediaSource.isTypeSupported === 'function' && mediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"');
|
||
// if SourceBuffer is exposed ensure its API is valid
|
||
// safari and old version of Chrome do not expose SourceBuffer globally so checking SourceBuffer.prototype is impossible
|
||
const sourceBufferValidAPI = !sourceBuffer || (sourceBuffer.prototype && typeof sourceBuffer.prototype.appendBuffer === 'function' && typeof sourceBuffer.prototype.remove === 'function');
|
||
return !!isTypeSupported && !!sourceBufferValidAPI;
|
||
}
|
||
catch (_a) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/*
|
||
* Media Element Helper: Contains wrapper class information for HTMLMediaElement
|
||
*
|
||
*
|
||
*/
|
||
const MediaElementHelper = {
|
||
isWebkitMediaElement(media) {
|
||
return 'webkitDroppedFrameCount' in media;
|
||
},
|
||
isHtmlVideoElement(media) {
|
||
return 'getVideoPlaybackQuality' in media;
|
||
},
|
||
timeRangeToArray(timeRange) {
|
||
const rangeArr = [];
|
||
for (let i = 0; i < timeRange.length; i++) {
|
||
rangeArr.push([timeRange.start(i), timeRange.end(i)]);
|
||
}
|
||
return rangeArr;
|
||
},
|
||
};
|
||
|
||
/*
|
||
* Access Log Types
|
||
*
|
||
*
|
||
*/
|
||
const loggerName$3 = { name: 'access-log' };
|
||
class AccessLog {
|
||
constructor(hls, sessionID) {
|
||
this.hls = hls;
|
||
this.sessionID = sessionID;
|
||
this.rtcQuery = null;
|
||
this.accessLogData = this.createAccessLogEntry();
|
||
this.accesslog = [];
|
||
this.errorlog = [];
|
||
}
|
||
destroy() {
|
||
this.rtcQuery = null;
|
||
this.accesslog = [];
|
||
this.errorlog = [];
|
||
this.accessLogData = undefined;
|
||
this.accessLogReporter = undefined;
|
||
}
|
||
setRTCQuery(query) {
|
||
this.rtcQuery = query;
|
||
}
|
||
setupReporter(appData) {
|
||
this.accessLogReporter = { SessionID: this.sessionID, ClientName: appData === null || appData === void 0 ? void 0 : appData.clientName, ServiceName: appData === null || appData === void 0 ? void 0 : appData.serviceName };
|
||
}
|
||
addPlayTime(itemId) {
|
||
var _a;
|
||
const entity = (_a = this.rtcQuery) === null || _a === void 0 ? void 0 : _a.getEntity(itemId);
|
||
if (!entity) {
|
||
return;
|
||
}
|
||
const sessionControlRecord = entity.sessionControlRecord;
|
||
if (sessionControlRecord.state === 'RTC_STATE_PLAY') {
|
||
this.accessLogData.PlayTimeWC = (this.accessLogData.PlayTimeWC || 0) + sessionControlRecord.eventStartTime;
|
||
}
|
||
getLogger().trace(loggerName$3, 'accessLogData playtime: %d', this.accessLogData.PlayTimeWC);
|
||
}
|
||
updatePlaybackInfo(itemId, data) {
|
||
this.accessLogData.ViFrDr = this.rtcQuery.getEntity(itemId).sessionControlRecord.droppedVideoFrames || 0;
|
||
getLogger().trace(loggerName$3, 'accessLogData Framedrop: %d', this.accessLogData.ViFrDr);
|
||
}
|
||
updateStallCount(itemId) {
|
||
const state = this.rtcQuery.getEntity(itemId).sessionControlRecord.state;
|
||
if (state !== 'RTC_STATE_PLAY') {
|
||
// don't report stall events if we're not playing
|
||
getLogger().info(loggerName$3, `skipping low buffer stall event because we're not playing, state: ${state}`);
|
||
return;
|
||
}
|
||
this.accessLogData.StallCount++;
|
||
}
|
||
updateMediaEngineStallCount(itemId) {
|
||
const state = this.rtcQuery.getEntity(itemId).sessionControlRecord.state;
|
||
if (state !== 'RTC_STATE_PLAY') {
|
||
// don't report stall events if we're not playing
|
||
getLogger().info(loggerName$3, `skipping high buffer stall event because we're not playing, state: ${state}`);
|
||
return;
|
||
}
|
||
this.accessLogData.MediaEngineStallCount++;
|
||
}
|
||
updateCanPlay(itemId) {
|
||
this.accessLogData.StartupTime = this.rtcQuery.getEntity(itemId).sessionControlRecord.eventStartTime;
|
||
}
|
||
updateFragLoaded(itemId, isSeeking, data) {
|
||
// use only 'main' (muxed) / 'video' (unmuxed) data to calculate below keys
|
||
if (data.fragType === MediaOptionType.Variant) {
|
||
this.accessLogData.NetBytes += data.bytes;
|
||
this.accessLogData.ADT += data.adt;
|
||
//this._accessLogData.SegmentProcessTime += data.processTime;
|
||
const accessLogBitrateStats = this.aggregateFragObserverdBitrate(data, ++this.accessLogData.fragmentCnt, this.accessLogData.NetBytes, this.accessLogData.ADT);
|
||
this.accessLogData.OBRLast = accessLogBitrateStats.obrLast;
|
||
this.accessLogData.OBRMean = accessLogBitrateStats.obrMean;
|
||
this.aggregateFragMinMaxBitrate(this.accessLogData, accessLogBitrateStats.obr);
|
||
const currentTime = this.hls.realCurrentTime;
|
||
if (currentTime > data.startPTS && !isSeeking) {
|
||
this.accessLogData.overdue++;
|
||
}
|
||
if (this.hasGap(data.startPTS, data.endPTS, this.accessLogData.lastStartPTS, this.accessLogData.lastEndPTS)) {
|
||
// New continuous playback stream due to timejump
|
||
getLogger().info(loggerName$3, 'summarize stats for seek to startPTS: ' + data.startPTS + 's');
|
||
this.addToAccessLog(itemId);
|
||
}
|
||
if (!this.accessLogData.startPTS) {
|
||
// mark first ever fragment start time for fresh access log entry
|
||
this.accessLogData.startPTS = data.startPTS;
|
||
}
|
||
// Remember last loaded fragment start and end time to detect new seek later
|
||
this.accessLogData.lastStartPTS = data.startPTS;
|
||
this.accessLogData.lastEndPTS = data.endPTS;
|
||
// accumulate the 'main' (muxed) / 'video' (unmuxed) bytes & duration to calculate AvgVideoBitrate
|
||
this.accessLogData.videoBytes += data.bytes;
|
||
this.accessLogData.videoDuration += data.duration;
|
||
}
|
||
else if (data.fragType === MediaOptionType.AltAudio) {
|
||
// accumulate the 'audio' bytes & duration to calculate AvgAudioBitrate
|
||
this.accessLogData.audioBytes += data.bytes;
|
||
this.accessLogData.audioDuration += data.duration;
|
||
}
|
||
getLogger().trace(loggerName$3, 'Fragloaded, accessLogData=%o', this.accessLogData);
|
||
}
|
||
addToAccessLog(itemId) {
|
||
const varInfo = this.getVariantInfo(itemId);
|
||
const url = this.rtcQuery.getEntity(itemId).sessionControlRecord.curLevelUrl;
|
||
const playType = this.rtcQuery.getEntity(itemId).playEndedRecord.PlayType;
|
||
if (!url || url === '') {
|
||
// Too early, fields may be undefined.
|
||
return;
|
||
}
|
||
const translatedData = this.translateToAccessLogItem(itemId, url, varInfo, playType);
|
||
if (translatedData) {
|
||
const overage = this.accesslog.length - 20;
|
||
if (overage > 0) {
|
||
this.accesslog.splice(0, overage); // trim access log entries
|
||
}
|
||
this.accesslog.push(translatedData);
|
||
getLogger().trace(loggerName$3, 'accesslog=%o', this.accesslog);
|
||
}
|
||
// reset accessLogData after we added an entry to the log
|
||
this.accessLogData = this.createAccessLogEntry();
|
||
const recordMediaDur = this.rtcQuery.getEntity(itemId).switchCompleteRecord.MediaDur;
|
||
this.accessLogData.lastMediaDur = recordMediaDur ? recordMediaDur : this.hls.bufferedDuration;
|
||
}
|
||
addToErrorLog(itemId, domain) {
|
||
var _a;
|
||
const entity = (_a = this.rtcQuery) === null || _a === void 0 ? void 0 : _a.getEntity(itemId);
|
||
if (!entity) {
|
||
return;
|
||
}
|
||
let code;
|
||
if (domain === 'mediaError') {
|
||
code = Number(entity.playErrorRecord.ErrCode);
|
||
}
|
||
else {
|
||
// else if (networkError)
|
||
code = Number(entity.nwErrorRecord.ErrCode);
|
||
}
|
||
const errorInfo = { domain, code };
|
||
const url = entity.sessionControlRecord.curLevelUrl;
|
||
const translatedData = this.translateToErrorLogItem(itemId, url, errorInfo);
|
||
if (translatedData) {
|
||
const overage = this.errorlog.length - 20;
|
||
if (overage > 0) {
|
||
this.errorlog.splice(0, overage); // trim error log entries
|
||
}
|
||
this.errorlog.push(translatedData);
|
||
getLogger().trace(loggerName$3, 'errorlog=%o', this.errorlog);
|
||
}
|
||
}
|
||
getAccessLog(itemId) {
|
||
var _a;
|
||
const finalAccessLog = this.accesslog.slice(0); // Make a copy of the access log array
|
||
const entity = (_a = this.rtcQuery) === null || _a === void 0 ? void 0 : _a.getEntity(itemId);
|
||
if (finalAccessLog && entity) {
|
||
// Add the current "open" variant entry
|
||
const url = entity.sessionControlRecord.curLevelUrl;
|
||
if (url && url !== '') {
|
||
const varInfo = this.getVariantInfo(itemId);
|
||
const translatedData = this.translateToAccessLogItem(itemId, url, varInfo, this.rtcQuery.getEntity(itemId).playEndedRecord.PlayType);
|
||
if (translatedData) {
|
||
translatedData['c-provisional-entry'] = true; // mark item as incomplete
|
||
finalAccessLog.push(translatedData);
|
||
}
|
||
}
|
||
}
|
||
return finalAccessLog;
|
||
}
|
||
get errorLog() {
|
||
return this.errorlog;
|
||
}
|
||
createAccessLogEntry() {
|
||
const entry = {
|
||
fragmentCnt: 0,
|
||
overdue: 0,
|
||
startPTS: 0,
|
||
obrMax: 0,
|
||
obrMin: 0,
|
||
audioBytes: 0,
|
||
audioDuration: 0,
|
||
videoBytes: 0,
|
||
videoDuration: 0,
|
||
svrAddrChanged: 0,
|
||
svrAddr: '',
|
||
PlayTimeWC: 0,
|
||
ViFrDr: 0,
|
||
StallCount: 0,
|
||
MediaEngineStallCount: 0,
|
||
ADT: 0,
|
||
NetBytes: 0,
|
||
StartupTime: 0,
|
||
OBRMean: 0,
|
||
OBRLast: 0,
|
||
};
|
||
return entry;
|
||
}
|
||
convertStringObjectToPrimitive(str) {
|
||
// Prevent crashing Netflix because it assumes url is always a valid string
|
||
let output;
|
||
if (!str) {
|
||
output = '';
|
||
}
|
||
else if (typeof str === 'object') {
|
||
output = str.toString();
|
||
}
|
||
else {
|
||
output = str;
|
||
}
|
||
return output;
|
||
}
|
||
updateSvrAddrStats(url) {
|
||
const urlParts = URLToolkit$1.parseURL(url);
|
||
if (urlParts && urlParts.netLoc) {
|
||
const portLoc = urlParts.netLoc.indexOf(':');
|
||
let svrAddr = portLoc >= 0 ? urlParts.netLoc.slice(0, portLoc) : urlParts.netLoc;
|
||
if (svrAddr.startsWith('//')) {
|
||
svrAddr = svrAddr.slice(2);
|
||
}
|
||
if (!this.accessLogData.svrAddr) {
|
||
this.accessLogData.svrAddrChanged = 0;
|
||
}
|
||
else if (svrAddr !== this.accessLogData.svrAddr) {
|
||
this.accessLogData.svrAddrChanged++;
|
||
}
|
||
// server address
|
||
this.accessLogData.svrAddr = svrAddr;
|
||
}
|
||
}
|
||
translateToAccessLogItem(itemId, url, varInfo, playType) {
|
||
const uri = this.convertStringObjectToPrimitive(url);
|
||
this.updateSvrAddrStats(uri);
|
||
// accumulated duration of the media downloaded, in seconds
|
||
let currentMediaDur = this.rtcQuery.getEntity(itemId).switchCompleteRecord.MediaDur;
|
||
if (!currentMediaDur) {
|
||
// total data downloaded after seek, or live/last access log entry
|
||
currentMediaDur = this.hls.bufferedDuration;
|
||
}
|
||
if (!currentMediaDur) {
|
||
currentMediaDur = 0;
|
||
}
|
||
const item = {
|
||
// User info -- Internal use only. Commented out to prevent leaking project via access log serviceName (com.apple.hlsjs.airplay)
|
||
// item['c-client-name'] = this._accessLogReporter.ClientName;
|
||
// item['c-service-identifier'] = this._accessLogReporter.ServiceName;
|
||
// Server related
|
||
uri: uri,
|
||
's-ip': this.accessLogData.svrAddr,
|
||
's-ip-changes': this.accessLogData.svrAddrChanged,
|
||
// number of network read requests over WWAN, mediaRequestsWWAN. Not sure if TV is connected via WiFi.
|
||
'sc-wwan-count': -1,
|
||
// transfer duration
|
||
'c-transfer-duration': this.accessLogData.ADT,
|
||
// number of bytes transferred
|
||
bytes: this.accessLogData.NetBytes,
|
||
// number of media requests
|
||
'c-total-media-requests': this.accessLogData.fragmentCnt,
|
||
// Playback related
|
||
// GUID identifies playback session
|
||
'cs-guid': this.accessLogReporter.SessionID,
|
||
// start time
|
||
'c-start-time': this.accessLogData.startPTS,
|
||
// startup time
|
||
'c-startup-time': this.accessLogData.StartupTime,
|
||
// duration watched (in seconds)
|
||
'c-duration-watched': this.accessLogData.PlayTimeWC / 1000,
|
||
// number of dropped video frames
|
||
'c-frames-dropped': this.accessLogData.ViFrDr,
|
||
// total number of playback stalls encountered
|
||
'c-stalls': this.accessLogData.StallCount + this.accessLogData.MediaEngineStallCount,
|
||
// subtract total data downloaded since last access log entry, if available
|
||
'c-duration-downloaded': this.accessLogData.lastMediaDur ? currentMediaDur - this.accessLogData.lastMediaDur : currentMediaDur,
|
||
// total number of times the download of the segments took too long
|
||
'c-overdue': this.accessLogData.overdue,
|
||
// video track’s average bit rate, in bits per second 'c-avg-video-bitrate'
|
||
'c-avg-video-bitrate': (this.accessLogData.videoBytes * 8) / (this.accessLogData.videoDuration || 1),
|
||
// maximum observed segment download bit rate 'c-observed-max-bitrate'
|
||
'c-observed-max-bitrate': this.accessLogData.obrMax,
|
||
// minimum observed segment download bit rate 'c-observed-min-bitrate'
|
||
'c-observed-min-bitrate': this.accessLogData.obrMin,
|
||
// throughput, in bits per second, required to play the stream, as advertised by the server.
|
||
'sc-indicated-bitrate': varInfo.bandwidth ? varInfo.bandwidth : 0,
|
||
// average throughput, in bits per second, required to play the stream, as advertised by the server
|
||
'sc-indicated-avg-bitrate': varInfo.avgBandwidth ? varInfo.avgBandwidth : 0,
|
||
// empirical throughput, in bits per second, across all media downloaded.
|
||
'c-observed-bitrate': this.accessLogData.OBRMean,
|
||
// bandwidth that caused a switch (up or down) 'c-switch-bitrate'
|
||
'c-switch-bitrate': this.accessLogData.OBRLast,
|
||
// mark item as complete
|
||
'c-provisional-entry': false,
|
||
};
|
||
// playback type
|
||
item['s-playback-type'] = playType;
|
||
// audio track’s average bit rate, in bits per second 'c-avg-audio-bitrate'
|
||
if (this.accessLogData.audioBytes) {
|
||
// don't show this for muxed content
|
||
item['c-avg-audio-bitrate'] = (this.accessLogData.audioBytes * 8) / (this.accessLogData.audioDuration || 1);
|
||
}
|
||
return item;
|
||
}
|
||
translateToErrorLogItem(itemId, url, errorInfo) {
|
||
const uri = this.convertStringObjectToPrimitive(url);
|
||
this.updateSvrAddrStats(uri); // extract URL host name to svrAddr attribute
|
||
const item = {
|
||
date: new Date(),
|
||
'cs-guid': this.accessLogReporter.SessionID + '-' + itemId,
|
||
uri: uri,
|
||
's-ip': this.accessLogData.svrAddr,
|
||
status: '' + errorInfo.code,
|
||
domain: errorInfo.domain,
|
||
};
|
||
return item;
|
||
}
|
||
hasGap(newStartPTS, newEndPTS, oldStartPTS, oldEndPTS) {
|
||
if (typeof newStartPTS === 'undefined' || typeof oldStartPTS === 'undefined') {
|
||
// ignore initSegment and newly create access log entry
|
||
return false;
|
||
}
|
||
// seek gap is at least 1 second
|
||
return newStartPTS - oldEndPTS > 1 || oldStartPTS - newEndPTS > 1;
|
||
}
|
||
aggregateFragObserverdBitrate(data, fragments, bytes, adt) {
|
||
const obr = (bytes * 8) / (adt / 1000);
|
||
const obrLast = (data.bytes * 8) / (data.adt / 1000);
|
||
const obrMean = obr / fragments;
|
||
return { obr: obr, obrLast: obrLast, obrMean: obrMean };
|
||
}
|
||
aggregateFragMinMaxBitrate(target, obr) {
|
||
if (!target.obrMax || obr > target.obrMax) {
|
||
target.obrMax = obr;
|
||
}
|
||
if (!target.obrMin || obr < target.obrMin) {
|
||
target.obrMin = obr;
|
||
}
|
||
}
|
||
getVariantInfo(itemId) {
|
||
var _a;
|
||
const url = this.rtcQuery.getEntity(itemId).sessionControlRecord.curLevelUrl;
|
||
const variantList = (_a = this.rtcQuery.getEntity(itemId).sessionControlRecord.manifestData) === null || _a === void 0 ? void 0 : _a.variantList;
|
||
return url && variantList && variantList[url] ? variantList[url] : {};
|
||
}
|
||
}
|
||
|
||
const loadMediaFragment = (mediaFragment, config, loadPolicy, onProgress, requestServerInfo, extendMaxTTFB) => {
|
||
var _a;
|
||
const { absoluteUrl, byteRangeOffset, discoSeqNum, keyTagInfo, iframe, isInitSegment } = mediaFragment;
|
||
const url = absoluteUrl;
|
||
const { method } = keyTagInfo;
|
||
const { start, end } = byteRangeOffset;
|
||
const loadConfig = getLoadConfig({ url }, loadPolicy);
|
||
let byteRangeStart = start;
|
||
let byteRangeEnd = end;
|
||
let resetIV = false;
|
||
let byterange = !isFiniteNumber(start) && !isFiniteNumber(end) ? undefined : byteRangeOffset;
|
||
if (method === 'AES-128' && end && (iframe || isInitSegment)) {
|
||
// Map fragment encrypted with method 'AES-128', when served with HTTP Range, has the unencrypted size specified in the range. Modify the range to ensure we read the padding bytes as well.
|
||
/**
|
||
* Ref: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-6.3.6
|
||
*
|
||
* If the encryption METHOD is AES-128 and the Media Segment is part of
|
||
* an I-frame Playlist (Section 4.4.3.6) and it has an EXT-X-BYTERANGE
|
||
* tag applied to it, special care needs to be taken in loading and
|
||
* decrypting the segment, because the resource identified by the URI is
|
||
* encrypted in 16-byte blocks from the start of the resource.
|
||
*
|
||
* The decrypted I-frame can be recovered by first widening its byte
|
||
* range, as specified by the EXT-X-BYTERANGE tag, so that it starts and
|
||
* ends on 16-byte boundaries from the start of the resource.
|
||
* */
|
||
const fragmentLen = end - start;
|
||
if (fragmentLen % 16) {
|
||
byteRangeEnd = end + (16 - (fragmentLen % 16));
|
||
}
|
||
/**
|
||
* Next, the byte range is widened further to include a 16-byte block at
|
||
* the beginning of the range. This 16-byte block allows the correct IV
|
||
* for the following block to be calculated.
|
||
* */
|
||
if (start !== 0) {
|
||
resetIV = true;
|
||
byteRangeStart = start - 16;
|
||
}
|
||
byterange = { start: byteRangeStart, end: byteRangeEnd };
|
||
}
|
||
let collectServerInstanceInfo = undefined;
|
||
if (requestServerInfo && isFiniteNumber(mediaFragment.mediaSeqNum) && mediaFragment.mediaOptionType === MediaOptionType.Variant) {
|
||
// Sanitize the list
|
||
collectServerInstanceInfo = [];
|
||
(_a = loadConfig.reportHTTPResponseHeaders) === null || _a === void 0 ? void 0 : _a.forEach(function (header) {
|
||
if (privacyAllowedLoadConfigHeaders.includes(header)) {
|
||
collectServerInstanceInfo.push(header);
|
||
}
|
||
else {
|
||
getLogger().warn({ name: 'load-media-fragment' }, `${header} is not in approved privacy list. Actions required.`);
|
||
}
|
||
});
|
||
if (collectServerInstanceInfo.length === 0) {
|
||
collectServerInstanceInfo = undefined;
|
||
}
|
||
}
|
||
const context = {
|
||
url: url,
|
||
byteRangeOffset: byterange,
|
||
checkContentLength: true,
|
||
extendMaxTTFB: extendMaxTTFB,
|
||
collectServerInstanceInfo: collectServerInstanceInfo,
|
||
onProgress,
|
||
xhrSetup: config.xhrSetup,
|
||
};
|
||
return fromUrlArrayBuffer(context, loadConfig).pipe(map(([loadedData, bwSample, serverInfo]) => {
|
||
if (resetIV) {
|
||
const buf = loadedData;
|
||
mediaFragment.keyTagInfo.iv = new Uint8Array(buf.slice(0, 16)); // First 16 bytes -> IV
|
||
loadedData = buf.slice(16); // rest is the actual fragment
|
||
}
|
||
return [mediaFragment, loadedData, bwSample, serverInfo];
|
||
}), convertToFragmentNetworkError(mediaFragment, false));
|
||
};
|
||
|
||
const kKeySystemIdToPropertiesMap = {
|
||
clearkey: ClearKeySystemProperties,
|
||
fairplaystreaming: FairPlayStreamingKeySystemProperties,
|
||
playready: PlayReadyKeySystemProperties,
|
||
widevine: WidevineKeySystemProperties,
|
||
};
|
||
/**
|
||
* Get the EXT-X-KEY:FORMAT value associated with the key system
|
||
* @param keySystemId Identifier string to request keyFormatString property for
|
||
*/
|
||
const KeySystemFactory = {
|
||
getKeySystemFormat(keySystemId) {
|
||
const properties = kKeySystemIdToPropertiesMap[keySystemId];
|
||
return properties ? properties.keyFormatString : '';
|
||
},
|
||
getKeySystemSecurityLevel(keySystemId) {
|
||
const properties = kKeySystemIdToPropertiesMap[keySystemId];
|
||
return properties ? properties.securityLevels : null;
|
||
},
|
||
};
|
||
|
||
const decryptmethods = { NONE: '', 'AES-128': '', 'ISO-23001-7': '', 'SAMPLE-AES': '', 'SAMPLE-AES-CTR': '' };
|
||
function isDecryptMethod(tagValue) {
|
||
if (!tagValue) {
|
||
return false;
|
||
}
|
||
return tagValue in decryptmethods;
|
||
}
|
||
|
||
const HdcpLevels = {
|
||
NONE: 0,
|
||
'TYPE-0': 1,
|
||
'TYPE-1': 2,
|
||
'TYPE-2': 3,
|
||
};
|
||
function isHdcpLevel(x) {
|
||
return x in HdcpLevels;
|
||
}
|
||
function hdcpLevelToInt(hdcpLevel) {
|
||
if (hdcpLevel == null) {
|
||
return 4;
|
||
}
|
||
return HdcpLevels[hdcpLevel];
|
||
}
|
||
|
||
const SessionDataKey = {
|
||
CHAPTER: 'com.apple.hls.chapters',
|
||
TITLE: 'com.apple.hls.title',
|
||
TITLE_DESCRIPTIONS: 'com.apple.hls.description',
|
||
EPISODE_TITLE: 'com.apple.hls.episode-title',
|
||
ARTWORK: 'com.apple.hls.poster',
|
||
ROTTEN_TOMATOES_RATING: 'com.apple.hls.rt-rating',
|
||
ROTTEN_TOMATOES_AUDIENCE_SCORE: 'com.apple.hls.rt-audience-score',
|
||
GENRE: 'com.apple.hls.genre',
|
||
RELEASE_DATE: 'com.apple.hls.release-date',
|
||
CONTENT_RATING_BADGE: 'com.apple.hls.rating-tag',
|
||
OTHER_TEXT_BADGES: 'com.apple.hls.other-tags',
|
||
FORMAT: 'com.apple.hls.format',
|
||
QUALITY: 'com.apple.hls.quality',
|
||
ACCESSIBILITY: 'com.apple.hls.accessibility',
|
||
};
|
||
const isSessionDataItem = (item) => 'DATA-ID' in item;
|
||
|
||
const VideoRangeValues = ['SDR', 'PQ', 'HLG'];
|
||
function isVideoRange(vr) {
|
||
return vr != null && VideoRangeValues.includes(vr);
|
||
}
|
||
|
||
/*
|
||
* 2018 Apple Inc. All rights reserved.
|
||
*/
|
||
const bcp47ShortestLangCode = {
|
||
afr: 'af',
|
||
aka: 'ak',
|
||
amh: 'am',
|
||
ara: 'ar',
|
||
arg: 'an',
|
||
asm: 'as',
|
||
ava: 'av',
|
||
ave: 'ae',
|
||
aym: 'ay',
|
||
aze: 'az',
|
||
bam: 'bm',
|
||
bel: 'be',
|
||
ben: 'bn',
|
||
bih: 'bh',
|
||
bod: 'bo',
|
||
bos: 'bs',
|
||
bre: 'br',
|
||
bul: 'bg',
|
||
cat: 'ca',
|
||
ces: 'cs',
|
||
cha: 'ch',
|
||
che: 'ce',
|
||
chu: 'cu',
|
||
chv: 'cv',
|
||
cor: 'kw',
|
||
cos: 'co',
|
||
cre: 'cr',
|
||
cym: 'cy',
|
||
dan: 'da',
|
||
deu: 'de',
|
||
div: 'dv',
|
||
dzo: 'dz',
|
||
ell: 'el',
|
||
eng: 'en',
|
||
epo: 'eo',
|
||
est: 'et',
|
||
eus: 'eu',
|
||
ewe: 'ee',
|
||
fao: 'fo',
|
||
fas: 'fa',
|
||
fin: 'fi',
|
||
fra: 'fr',
|
||
fry: 'fy',
|
||
ful: 'ff',
|
||
gla: 'gd',
|
||
gle: 'ga',
|
||
glg: 'gl',
|
||
glv: 'gv',
|
||
grn: 'gn',
|
||
guj: 'gu',
|
||
hat: 'ht',
|
||
heb: 'he',
|
||
her: 'hz',
|
||
hin: 'hi',
|
||
hmo: 'ho',
|
||
hrv: 'hr',
|
||
hun: 'hu',
|
||
hye: 'hy',
|
||
ibo: 'ig',
|
||
ido: 'io',
|
||
iii: 'ii',
|
||
iku: 'iu',
|
||
ile: 'ie',
|
||
ina: 'ia',
|
||
ind: 'id',
|
||
isl: 'is',
|
||
ita: 'it',
|
||
jav: 'jv',
|
||
jpn: 'ja',
|
||
kal: 'kl',
|
||
kan: 'kn',
|
||
kas: 'ks',
|
||
kat: 'ka',
|
||
kau: 'kr',
|
||
kaz: 'kk',
|
||
khm: 'km',
|
||
kik: 'ki',
|
||
kin: 'rw',
|
||
kir: 'ky',
|
||
kom: 'kv',
|
||
kon: 'kg',
|
||
kor: 'ko',
|
||
kua: 'kj',
|
||
kur: 'ku',
|
||
lao: 'lo',
|
||
lat: 'la',
|
||
lav: 'lv',
|
||
lim: 'li',
|
||
lit: 'lt',
|
||
ltz: 'lb',
|
||
lub: 'lu',
|
||
lug: 'lg',
|
||
mah: 'mh',
|
||
mal: 'ml',
|
||
mar: 'mr',
|
||
mkd: 'mk',
|
||
mlg: 'mg',
|
||
mlt: 'mt',
|
||
mol: 'mo',
|
||
mon: 'mn',
|
||
mri: 'mi',
|
||
msa: 'ms',
|
||
mya: 'my',
|
||
nav: 'nv',
|
||
nbl: 'nr',
|
||
nde: 'nd',
|
||
ndo: 'ng',
|
||
nep: 'ne',
|
||
nld: 'nl',
|
||
nno: 'nn',
|
||
nob: 'nb',
|
||
nya: 'ny',
|
||
oci: 'oc',
|
||
oji: 'oj',
|
||
ori: 'or',
|
||
orm: 'om',
|
||
oss: 'os',
|
||
pan: 'pa',
|
||
pli: 'pi',
|
||
pol: 'pl',
|
||
por: 'pt',
|
||
pus: 'ps',
|
||
que: 'qu',
|
||
roh: 'rm',
|
||
ron: 'ro',
|
||
run: 'rn',
|
||
rus: 'ru',
|
||
san: 'sa',
|
||
sin: 'si',
|
||
slk: 'sk',
|
||
slv: 'sl',
|
||
sme: 'se',
|
||
snd: 'sd',
|
||
som: 'so',
|
||
spa: 'es',
|
||
sqi: 'sq',
|
||
srd: 'sc',
|
||
srp: 'sr',
|
||
sun: 'su',
|
||
swa: 'sw',
|
||
swe: 'sv',
|
||
tah: 'ty',
|
||
tam: 'ta',
|
||
tat: 'tt',
|
||
tel: 'te',
|
||
tgk: 'tg',
|
||
tgl: 'tl',
|
||
tha: 'th',
|
||
tir: 'ti',
|
||
ton: 'to',
|
||
tuk: 'tk',
|
||
tur: 'tr',
|
||
uig: 'ug',
|
||
ukr: 'uk',
|
||
urd: 'ur',
|
||
uzb: 'uz',
|
||
ven: 've',
|
||
vie: 'vi',
|
||
wln: 'wa',
|
||
yid: 'yi',
|
||
zha: 'za',
|
||
zho: 'zh',
|
||
};
|
||
const bcp47Utils = {
|
||
isLanguageCode(value) {
|
||
return value in bcp47ShortestLangCode;
|
||
},
|
||
shortenLanguageCode(langTag) {
|
||
let shortenedLangTag;
|
||
if (langTag) {
|
||
const langCodePos = langTag.indexOf('-');
|
||
const langCode = langCodePos >= 0 ? langTag.slice(0, langCodePos) : langTag;
|
||
if (bcp47Utils.isLanguageCode(langCode)) {
|
||
shortenedLangTag = bcp47ShortestLangCode[langCode];
|
||
}
|
||
if (!shortenedLangTag) {
|
||
shortenedLangTag = langCode;
|
||
}
|
||
if (langCodePos > 0) {
|
||
const theRest = langTag.slice(langCodePos + 1);
|
||
shortenedLangTag += '-' + theRest;
|
||
}
|
||
}
|
||
return shortenedLangTag;
|
||
},
|
||
};
|
||
|
||
const RichestMedia = {
|
||
getRichestVideoCodec(videoCodecList) {
|
||
if (!videoCodecList || !videoCodecList.length) {
|
||
return undefined;
|
||
}
|
||
const sortedCodecs = videoCodecList.sort((codec1, codec2) => getVideoCodecRanking(codec2) - getVideoCodecRanking(codec1));
|
||
return sortedCodecs && sortedCodecs.length ? sortedCodecs[0] : undefined;
|
||
},
|
||
getRichestAudioCodec(audioCodecList) {
|
||
if (!audioCodecList || !audioCodecList.length) {
|
||
return undefined;
|
||
}
|
||
const sortedCodecs = audioCodecList.sort((codec1, codec2) => getAudioCodecRanking(codec2) - getAudioCodecRanking(codec1));
|
||
return sortedCodecs && sortedCodecs.length ? sortedCodecs[0] : undefined;
|
||
},
|
||
getRichestChannelLayoutForGroupId(audioGroupId, audioMediaOptions) {
|
||
if (!audioGroupId || !audioMediaOptions || !audioMediaOptions.length) {
|
||
return undefined;
|
||
}
|
||
let channels = undefined;
|
||
const tracksforGroupdId = audioMediaOptions.filter((track) => track.groupId === audioGroupId);
|
||
if (tracksforGroupdId && tracksforGroupdId.length) {
|
||
const sortedTracksForGroupId = tracksforGroupdId.sort((track1, track2) => MediaUtil.getChannelCount(track2.channels) - MediaUtil.getChannelCount(track1.channels));
|
||
if (sortedTracksForGroupId && sortedTracksForGroupId.length) {
|
||
channels = sortedTracksForGroupId[0].channels;
|
||
}
|
||
}
|
||
return channels;
|
||
},
|
||
};
|
||
|
||
function _formatError(msg) {
|
||
return new PlaylistParsingError(ErrorTypes.MEDIA_ERROR, ErrorDetails.STEERING_MANIFEST_PARSING_ERROR, false, msg, ErrorResponses.FormatError);
|
||
}
|
||
function validatePathwayID(pathwayID) {
|
||
if (typeof pathwayID !== 'string') {
|
||
return _formatError('invalid steering manifest PATHWAY-PRIORITY list item data type');
|
||
}
|
||
if (!/^[\w\-\.]+$/.test(pathwayID)) {
|
||
return _formatError('steering manifest contains invalid pathway ID: ' + pathwayID);
|
||
}
|
||
return void 0;
|
||
}
|
||
|
||
/*
|
||
* Fragment
|
||
*
|
||
* 2018 Apple Inc. All rights reserved.
|
||
*/
|
||
class FragmentBuilder {
|
||
// public backtracked?: boolean;
|
||
// public dropped?: number;
|
||
// public startDTS?: number;
|
||
// public startPTS?: number;
|
||
// public endDTS?: number;
|
||
// public endPTS?: number;
|
||
// public deltaPTS?: number;
|
||
// public autoLevel?: boolean;
|
||
// public bitrateTest?: boolean;
|
||
// public loader?: any; // TODO: fix any
|
||
// public loadSuccess?: boolean;
|
||
// public data?: Uint8Array;
|
||
// public iframeMediaStart?: number;
|
||
// public iframeMediaDuration?: number;
|
||
// public trackId?: number; // used by subtitle-stream-controller, should be moved
|
||
// public loaded: number; // used by fragment loader
|
||
// public itemId: string;
|
||
constructor(inheritQuery) {
|
||
this._url = null;
|
||
this._programDateTime = null;
|
||
this._byteRange = null;
|
||
this.relurl = null;
|
||
this.baseurl = null;
|
||
this.isInitSegment = false;
|
||
this.mediaSeqNum = NaN;
|
||
this.cc = NaN;
|
||
this.iframe = false;
|
||
this.bitrate = NaN;
|
||
this.start = NaN;
|
||
this.duration = NaN;
|
||
this.lastByteRangeEndOffset = NaN;
|
||
this.inheritQuery = inheritQuery;
|
||
this.tagList = new Array();
|
||
this.iframe = false;
|
||
}
|
||
getMediaFragment(itemId, mediaOptionId, mediaOptionType) {
|
||
var _a;
|
||
const fragment = {
|
||
mediaOptionType,
|
||
absoluteUrl: this.url,
|
||
start: this.start,
|
||
duration: this.duration,
|
||
mediaSeqNum: this.mediaSeqNum,
|
||
discoSeqNum: this.cc,
|
||
mediaOptionId,
|
||
itemId,
|
||
isLastFragment: false,
|
||
isInitSegment: this.isInitSegment,
|
||
};
|
||
if ((_a = this.byteRange) === null || _a === void 0 ? void 0 : _a.length) {
|
||
fragment.byteRangeOffset = { start: this.byteRangeStartOffset, end: this.byteRangeEndOffset };
|
||
}
|
||
if (this.iframe) {
|
||
fragment.iframe = this.iframe;
|
||
}
|
||
if (this.levelkey) {
|
||
fragment.keyTagInfo = this.levelkey;
|
||
}
|
||
if (this.programDateTime) {
|
||
fragment.programDateTime = this.programDateTime;
|
||
}
|
||
return fragment;
|
||
}
|
||
// static getStartEndString(frag: Fragment) {
|
||
// return `[${toFixed(frag.startDTS || frag.start, 3)},${toFixed(frag.endPTS || frag.start + frag.duration, 3)}]`;
|
||
// }
|
||
/*
|
||
* @returns whether this fragment is equivalent to another fragment
|
||
*/
|
||
// equals(fragInfo: Fragment) {
|
||
// return fragInfo.mediaSeqNum === this.mediaSeqNum && fragInfo.levelId === this.levelId;
|
||
// }
|
||
get url() {
|
||
if (!this._url && this.relurl && this.baseurl) {
|
||
this._url = URLToolkit$1.buildAbsoluteURL(this.baseurl, this.relurl, { alwaysNormalize: true, inheritQuery: this.inheritQuery });
|
||
}
|
||
return this._url;
|
||
}
|
||
set url(value) {
|
||
this._url = value;
|
||
}
|
||
get programDateTime() {
|
||
if (!this._programDateTime && this.rawProgramDateTime) {
|
||
this._programDateTime = new Date(Date.parse(this.rawProgramDateTime));
|
||
}
|
||
return this._programDateTime;
|
||
}
|
||
get byteRange() {
|
||
if (!this._byteRange) {
|
||
const byteRange = new Array(2);
|
||
if (this.rawByteRange) {
|
||
const params = this.rawByteRange.split('@', 2);
|
||
if (params.length === 1) {
|
||
const { lastByteRangeEndOffset } = this;
|
||
byteRange[0] = lastByteRangeEndOffset ? lastByteRangeEndOffset : 0;
|
||
}
|
||
else {
|
||
byteRange[0] = parseInt(params[1]);
|
||
}
|
||
byteRange[1] = parseInt(params[0]) + byteRange[0];
|
||
}
|
||
this._byteRange = byteRange;
|
||
}
|
||
return this._byteRange;
|
||
}
|
||
get byteRangeStartOffset() {
|
||
return this.byteRange[0];
|
||
}
|
||
get byteRangeEndOffset() {
|
||
return this.byteRange[1];
|
||
}
|
||
get rangeString() {
|
||
if (this.start >= 0 && this.duration >= 0) {
|
||
return `${this.start.toFixed(2)}-${(this.start + this.duration).toFixed(2)}`;
|
||
}
|
||
return 'N/A';
|
||
}
|
||
// useful for a consistent logging message tag
|
||
get fragTag() {
|
||
return `sn/cc/levelId: ${this.mediaSeqNum}/${this.cc}`;
|
||
}
|
||
}
|
||
|
||
const MediaSelectionHelper = {
|
||
parseMediaCharacteristics(mediaCharacteristics) {
|
||
return mediaCharacteristics ? mediaCharacteristics.split(/\s*,\s*/) : new Array();
|
||
},
|
||
addMediaToSelectionArray(mediaSelectionGroup, mediaEntry, nextPersistentID) {
|
||
if (typeof mediaSelectionGroup === 'undefined') {
|
||
return -1;
|
||
}
|
||
const mediaSelectionGroupOptionArray = mediaSelectionGroup.MediaSelectionGroupOptions;
|
||
let mediaSelectionOption = mediaSelectionGroupOptionArray.find((mediaSelectionOption) => mediaSelectionOption.MediaSelectionOptionsMediaType === mediaEntry.mediaType &&
|
||
mediaSelectionOption.MediaSelectionOptionsName === mediaEntry.name &&
|
||
mediaSelectionOption.MediaSelectionOptionsExtendedLanguageTag === mediaEntry.lang
|
||
// TODO: need ISO 639 code - MediaSelectionOptionsLanguageCode
|
||
);
|
||
if (!mediaSelectionOption) {
|
||
mediaSelectionOption = {
|
||
MediaSelectionOptionsMediaType: mediaEntry.mediaType,
|
||
MediaSelectionOptionsExtendedLanguageTag: mediaEntry.lang,
|
||
MediaSelectionOptionsIsDefault: mediaEntry.default,
|
||
MediaSelectionOptionsName: mediaEntry.name,
|
||
MediaSelectionOptionsPersistentID: nextPersistentID,
|
||
MediaSelectionOptionsTaggedMediaCharacteristics: mediaEntry.characteristics,
|
||
};
|
||
if (mediaEntry.mediaType === MediaTypeFourCC.SUBTITLE) {
|
||
mediaSelectionOption.MediaSelectionOptionsDisplaysNonForcedSubtitles = mediaEntry.forced ? Allowed.NO : Allowed.YES;
|
||
}
|
||
nextPersistentID++; // created a new group, increment the next ID
|
||
mediaSelectionGroupOptionArray.push(mediaSelectionOption);
|
||
}
|
||
mediaEntry.persistentID = mediaSelectionOption.MediaSelectionOptionsPersistentID;
|
||
return nextPersistentID;
|
||
},
|
||
addDefaultClosedCaptionOption(queueItemId, playlistSubtitleTracks, subtitleMediaSelectionGroup, nextPersistentID) {
|
||
const mediaEntry = {
|
||
// only 1 text track
|
||
itemId: queueItemId,
|
||
mediaOptionType: MediaOptionType.Subtitle,
|
||
id: 0,
|
||
mediaOptionId: 'cc1_' + guid(),
|
||
mediaType: MediaTypeFourCC.CLOSEDCAPTION,
|
||
inStreamID: 'CC1',
|
||
groupId: 'cc',
|
||
name: 'English-CC',
|
||
type: 'CLOSED-CAPTIONS',
|
||
default: false,
|
||
autoselect: false,
|
||
forced: false,
|
||
lang: 'en',
|
||
// 'public.easy-to-read' is not default in CoreMedia
|
||
characteristics: ['public.accessibility.transcribes-spoken-dialog', 'public.accessibility.describes-music-and-sound'],
|
||
persistentID: nextPersistentID,
|
||
};
|
||
playlistSubtitleTracks.push(mediaEntry);
|
||
MediaSelectionHelper.addMediaToSelectionArray(subtitleMediaSelectionGroup, mediaEntry, nextPersistentID);
|
||
},
|
||
};
|
||
|
||
const StringTags = {
|
||
NAME: '',
|
||
TYPE: '',
|
||
DEFAULT: '',
|
||
AUTOSELECT: '',
|
||
FORCED: '',
|
||
LANGUAGE: '',
|
||
URI: '',
|
||
AUDIO: '',
|
||
'VIDEO-RANGE': '',
|
||
'CLOSED-CAPTIONS': '',
|
||
CODECS: '',
|
||
BYTERANGE: '',
|
||
'INSTREAM-ID': '',
|
||
'GROUP-ID': '',
|
||
CHANNELS: '',
|
||
CHARACTERISTICS: '',
|
||
KEYFORMAT: '',
|
||
KEYFORMATVERSIONS: '',
|
||
'DATA-ID': '',
|
||
VALUE: '',
|
||
METHOD: '',
|
||
'HDCP-LEVEL': '',
|
||
'ALLOWED-CPC': '',
|
||
SUBTITLES: '',
|
||
// ext-daterange
|
||
ID: '',
|
||
CLASS: '',
|
||
'START-DATE': '',
|
||
'END-DATE': '',
|
||
'END-ON-NEXT': '',
|
||
// content-steering
|
||
'SERVER-URI': '',
|
||
'PATHWAY-ID': '',
|
||
};
|
||
const IntegerTags = {
|
||
BANDWIDTH: NaN,
|
||
'AVERAGE-BANDWIDTH': NaN,
|
||
};
|
||
const FloatTags = {
|
||
'TIME-OFFSET': NaN,
|
||
'FRAME-RATE': NaN,
|
||
SCORE: NaN,
|
||
'PLANNED-DURATION': NaN,
|
||
DURATION: NaN,
|
||
};
|
||
const HexTags = {
|
||
IV: null,
|
||
};
|
||
const ResolutionTags = {
|
||
RESOLUTION: null,
|
||
};
|
||
|
||
/*
|
||
* 2019 Apple Inc. All rights reserved.
|
||
*/
|
||
const DECIMAL_RESOLUTION_REGEX = /^(\d+)x(\d+)$/;
|
||
const ATTR_LIST_REGEX = /\s*(.+?)\s*=((?:\".*?\")|.*?)(?:,|$)/g;
|
||
// T is a string union type of keys
|
||
// K is the value type
|
||
class TagParser {
|
||
constructor(validTags) {
|
||
this.validTags = validTags;
|
||
}
|
||
isKey(tag) {
|
||
return tag in this.validTags;
|
||
}
|
||
trySetValue(tag, tagValue, outBag) {
|
||
if (this.isKey(tag)) {
|
||
outBag[tag] = this.parseFunc(tagValue);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
class StringTagParser extends TagParser {
|
||
parseFunc(tag) {
|
||
return tag;
|
||
}
|
||
}
|
||
class IntegerTagParser extends TagParser {
|
||
parseFunc(tagValue) {
|
||
const parsedValue = parseInt(tagValue);
|
||
if (parsedValue > Number.MAX_SAFE_INTEGER) {
|
||
return Infinity;
|
||
}
|
||
return parsedValue;
|
||
}
|
||
}
|
||
class FloatTagParser extends TagParser {
|
||
constructor() {
|
||
super(...arguments);
|
||
this.parseFunc = parseFloat;
|
||
}
|
||
}
|
||
class HexTagParser extends TagParser {
|
||
parseFunc(tagValue) {
|
||
let stringValue = (tagValue || '0x').slice(2);
|
||
stringValue = (stringValue.length & 1 ? '0' : '') + stringValue;
|
||
const value = new Uint8Array(stringValue.length / 2);
|
||
for (let i = 0; i < stringValue.length / 2; i++) {
|
||
const val = parseInt(stringValue.slice(i * 2, i * 2 + 2), 16);
|
||
if (!isFiniteNumber(val)) {
|
||
return undefined;
|
||
}
|
||
value[i] = val;
|
||
}
|
||
return value;
|
||
}
|
||
}
|
||
class ResolutionTagParser extends TagParser {
|
||
parseFunc(tagValue) {
|
||
const res = DECIMAL_RESOLUTION_REGEX.exec(tagValue);
|
||
let resolution;
|
||
if (res !== null) {
|
||
resolution = {
|
||
width: parseInt(res[1], 10),
|
||
height: parseInt(res[2], 10),
|
||
};
|
||
}
|
||
return resolution;
|
||
}
|
||
}
|
||
class PlaylistTagParser {
|
||
static parseTags(input) {
|
||
let match;
|
||
const parsedTags = {};
|
||
if (!input) {
|
||
return parsedTags;
|
||
}
|
||
ATTR_LIST_REGEX.lastIndex = 0;
|
||
while ((match = ATTR_LIST_REGEX.exec(input)) !== null) {
|
||
const tag = match[1].toUpperCase(), quote = '"';
|
||
let value = match[2];
|
||
if (value.indexOf(quote) === 0 && value.lastIndexOf(quote) === value.length - 1) {
|
||
value = value.slice(1, -1);
|
||
}
|
||
for (const tagParser of PlaylistTagParser.tagParsers) {
|
||
if (tagParser.trySetValue(tag, value, parsedTags)) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return parsedTags;
|
||
}
|
||
}
|
||
PlaylistTagParser.tagParsers = [new StringTagParser(StringTags), new IntegerTagParser(IntegerTags), new FloatTagParser(FloatTags), new HexTagParser(HexTags), new ResolutionTagParser(ResolutionTags)];
|
||
|
||
/*
|
||
* Playlist Parser
|
||
*
|
||
* 2018 Apple Inc. All rights reserved.
|
||
*/
|
||
// https://regex101.com is your friend
|
||
const HlsRegex = {
|
||
ExtractVariableParameter: /{\$(.*?)}/g,
|
||
LevelPlaylistFast: /#EXTINF:(\d*(?:\.\d+)?)(?:,(.*))?|(?!#)(\S.+)|#EXT-X-BYTERANGE: *(.+)|#EXT-X-PROGRAM-DATE-TIME:(.+)|#EXT-X-BITRATE:(.+)|#EXT-X-DATERANGE:(.+)|#.*/g,
|
||
LevelPlaylistSlow: /(?:(?:#(EXTM3U))|(?:#EXT-X-(PLAYLIST-TYPE):(.+))|(?:#EXT-X-(MEDIA-SEQUENCE): *(\d+))|(?:#EXT-X-(TARGETDURATION): *(\d+))|(?:#EXT-X-(KEY):(.+))|(?:#EXT-X-(START):(.+))|(?:#EXT-X-(ENDLIST))|(?:#EXT-X-(DISCONTINUITY-SEQ)UENCE:(\d+))|(?:#EXT-X-(DIS)CONTINUITY))|(?:#EXT-X-(VERSION):(\d+))|(?:#EXT-X-(MAP):(.+))|(?:#EXT-X-(I-FRAMES)-ONLY)|(?:#EXT-X-(DEFINE):(.+))|(?:(#)(.*):(.*))|(?:(#)(.*))(?:.*)\r?\n?/,
|
||
MasterPlaylist: /#EXT-X-STREAM-INF:([^\n\r]*)[\r\n]+([^\r\n]+)|#EXT-X-I-FRAME-STREAM-INF:([^\r\n]+)|#EXT-X-DEFINE:([^\n\r]*)|#EXT-X-CONTENT-STEERING:([^\n\r]*)/g,
|
||
MasterPlaylistAlternateMedia: /#EXT-X-MEDIA:(.*)/g,
|
||
SessionData: /#EXT-X-SESSION-DATA[^:]*:(.*)/g,
|
||
SessionKeys: /#EXT-X-SESSION-KEY:([^\n\r]*)/g,
|
||
VARIABLE_PLAYLIST_REGEX: /(NAME|VALUE)=\"(.*)\",(NAME|VALUE)=\"(.*)\"|(IMPORT)=\"(.*)\"/,
|
||
};
|
||
function resolve(url, baseUrl, inheritQuery) {
|
||
return URLToolkit.buildAbsoluteURL(baseUrl, url, { alwaysNormalize: true, inheritQuery });
|
||
}
|
||
function tryDecodeBase64Metadata(data) {
|
||
let decodedObj;
|
||
try {
|
||
decodedObj = JSON.parse(NumericEncodingUtils$1.base64DecodeToStr(data));
|
||
}
|
||
catch (err) {
|
||
// logger.error(`Error ${err} decoding metadata value: '${data}'`);
|
||
decodedObj = data;
|
||
}
|
||
return decodedObj;
|
||
}
|
||
class PlaylistParser {
|
||
static isValidPlaylist(playlist) {
|
||
return playlist.indexOf('#EXTM3U') === 0;
|
||
}
|
||
static isMediaPlaylist(playlist) {
|
||
return playlist.indexOf('#EXTINF:') > 0 || playlist.indexOf('#EXT-X-PLAYLIST-TYPE:') > 0;
|
||
}
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
static replaceVariables(input, variableMap) {
|
||
let updatedString, error = false;
|
||
if (input && variableMap) {
|
||
updatedString = input.replace(HlsRegex.ExtractVariableParameter, (result) => {
|
||
HlsRegex.ExtractVariableParameter.lastIndex = 0;
|
||
const variable = HlsRegex.ExtractVariableParameter.exec(result);
|
||
const property = variable[1];
|
||
// eslint-disable-next-line no-prototype-builtins
|
||
if (property && variableMap.hasOwnProperty(property)) {
|
||
return variableMap[property];
|
||
}
|
||
error = true;
|
||
});
|
||
}
|
||
return { updatedString, error };
|
||
}
|
||
static parseDecryptData(decryptparams, baseurl, keySystemPreference) {
|
||
var _a;
|
||
const keyAttrs = PlaylistTagParser.parseTags(decryptparams);
|
||
const decryptmethod = isDecryptMethod(keyAttrs.METHOD) ? keyAttrs.METHOD : null;
|
||
const decryptformat = (_a = keyAttrs.KEYFORMAT) !== null && _a !== void 0 ? _a : null;
|
||
if (decryptmethod && PlaylistParser.shouldSelectKeyTag(decryptformat, decryptmethod, keySystemPreference)) {
|
||
const decrypturi = keyAttrs.URI;
|
||
const decryptiv = keyAttrs.IV ? keyAttrs.IV : null;
|
||
if (decrypturi) {
|
||
// if there's an IV specified, make sure it's valid
|
||
if (keyAttrs.IV && !decryptiv) {
|
||
const playlistParsingError = new PlaylistParsingError(ErrorTypes.MEDIA_ERROR, ErrorDetails.MANIFEST_PARSING_ERROR, true, `Invalid IV: ${keyAttrs.IV}`, ErrorResponses.PlaylistErrorInvalidEntry);
|
||
playlistParsingError.url = baseurl;
|
||
throw playlistParsingError;
|
||
}
|
||
}
|
||
// do not use inheritQuery on key URIs
|
||
const resolvedUri = decrypturi ? URLToolkit.buildAbsoluteURL(baseurl, decrypturi, { alwaysNormalize: true }) : baseurl;
|
||
const decryptformatversions = (keyAttrs.KEYFORMATVERSIONS ? keyAttrs.KEYFORMATVERSIONS : '1').split('/').map(Number).filter(isFinite);
|
||
return new DecryptData(decryptmethod, resolvedUri, decryptiv, decryptformat, decryptformatversions);
|
||
}
|
||
}
|
||
/*
|
||
* Utility method to pick a desired key system based on the app preference, EXT-X-KEY tag values.
|
||
*/
|
||
static shouldSelectKeyTag(decryptformat, decryptmethod, keySystemPreference) {
|
||
// If it's Segment encryption or No encryption, just select that key system
|
||
// There is no app preference, so select the key system from the manifest
|
||
// Now try to match the app preference with the key system in the manifest
|
||
return 'AES-128' === decryptmethod || 'NONE' === decryptmethod || null == keySystemPreference || decryptformat === KeySystemFactory.getKeySystemFormat(keySystemPreference);
|
||
}
|
||
static optOutClosedCaption(levels) {
|
||
let noClosedCaption = false;
|
||
let hasVideo = false;
|
||
if (levels) {
|
||
// may be null for playlist with 1 audio only track
|
||
for (let i = 0; i < levels.length; ++i) {
|
||
const levelInfo = levels[i];
|
||
if (levelInfo.videoCodec) {
|
||
hasVideo = true;
|
||
if (levelInfo.iframes !== true && levelInfo.closedcaption && levelInfo.closedcaption.toLowerCase() === 'none') {
|
||
noClosedCaption = true; // 'CLOSED-CAPTION=NONE' must be present in all levels if it exists in one.
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return !hasVideo || noClosedCaption;
|
||
}
|
||
static parseRootPlaylistAlternateMediaOptions(queueItemId, playlist, baseurl, levels, inheritQuery, masterVariableList) {
|
||
let result;
|
||
let playlistParsingError;
|
||
const audioMediaSelectionGroup = {
|
||
MediaSelectionGroupAllowEmptySelection: 1,
|
||
MediaSelectionGroupMediaCharacteristics: ['public.audible'],
|
||
MediaSelectionGroupMediaType: MediaTypeFourCC.AUDIO,
|
||
MediaSelectionGroupOptions: [],
|
||
};
|
||
const subtitleMediaSelectionGroup = {
|
||
MediaSelectionGroupAllowEmptySelection: 1,
|
||
MediaSelectionGroupMediaCharacteristics: ['public.legible'],
|
||
MediaSelectionGroupMediaType: MediaTypeFourCC.SUBTITLE,
|
||
MediaSelectionGroupOptions: [],
|
||
};
|
||
const alternateMediaInfo = {
|
||
videoAlternateOptions: [],
|
||
audioAlternateOptions: [],
|
||
subtitleAlternateOptions: [],
|
||
audioMediaSelectionGroup,
|
||
subtitleMediaSelectionGroup,
|
||
};
|
||
let nextPersistentID = 0;
|
||
HlsRegex.MasterPlaylistAlternateMedia.lastIndex = 0;
|
||
while ((result = HlsRegex.MasterPlaylistAlternateMedia.exec(playlist)) != null) {
|
||
const updatedAttrList = PlaylistParser.replaceVariables(result[1], masterVariableList);
|
||
if (updatedAttrList.error) {
|
||
playlistParsingError = new PlaylistParsingError(ErrorTypes.MEDIA_ERROR, ErrorDetails.LEVEL_LOAD_ERROR, true, ErrorResponses.PlaylistErrorInvalidEXTXDEFINE.text, ErrorResponses.PlaylistErrorInvalidEXTXDEFINE);
|
||
break;
|
||
}
|
||
const attrs = PlaylistTagParser.parseTags(updatedAttrList.updatedString);
|
||
let mediaType = MediaTypeFourCC.UNKNOWN;
|
||
let inStreamID;
|
||
let trackArray;
|
||
let mediaSelectionGroup;
|
||
const characteristics = MediaSelectionHelper.parseMediaCharacteristics(attrs.CHARACTERISTICS);
|
||
const groupId = attrs['GROUP-ID'];
|
||
const channels = attrs.CHANNELS;
|
||
let groupCodecList;
|
||
let mediaOptionType = null;
|
||
switch (attrs.TYPE) {
|
||
case 'VIDEO':
|
||
mediaType = MediaTypeFourCC.VIDEO;
|
||
// mediaOptionType = TODO
|
||
trackArray = alternateMediaInfo.videoAlternateOptions;
|
||
// no selection options yet
|
||
break;
|
||
case 'AUDIO':
|
||
mediaType = MediaTypeFourCC.AUDIO;
|
||
mediaOptionType = MediaOptionType.AltAudio;
|
||
trackArray = alternateMediaInfo.audioAlternateOptions;
|
||
mediaSelectionGroup = audioMediaSelectionGroup;
|
||
const matchingLevel = levels.find((level) => level.audioGroupId === groupId);
|
||
groupCodecList = matchingLevel ? matchingLevel.audioCodecList : [];
|
||
break;
|
||
case 'SUBTITLES':
|
||
mediaType = MediaTypeFourCC.SUBTITLE;
|
||
mediaOptionType = MediaOptionType.Subtitle;
|
||
trackArray = alternateMediaInfo.subtitleAlternateOptions;
|
||
mediaSelectionGroup = subtitleMediaSelectionGroup;
|
||
break;
|
||
case 'CLOSED-CAPTIONS':
|
||
mediaType = MediaTypeFourCC.CLOSEDCAPTION;
|
||
mediaOptionType = MediaOptionType.Subtitle;
|
||
inStreamID = attrs['INSTREAM-ID'];
|
||
trackArray = alternateMediaInfo.subtitleAlternateOptions;
|
||
mediaSelectionGroup = subtitleMediaSelectionGroup;
|
||
break;
|
||
}
|
||
const mediaEntry = {
|
||
itemId: queueItemId,
|
||
mediaOptionType,
|
||
mediaType,
|
||
groupId,
|
||
channels,
|
||
groupCodecList,
|
||
name: attrs.NAME,
|
||
type: attrs.TYPE,
|
||
default: attrs.DEFAULT === 'YES',
|
||
autoselect: attrs.AUTOSELECT === 'YES',
|
||
forced: attrs.FORCED === 'YES',
|
||
characteristics,
|
||
persistentID: nextPersistentID,
|
||
id: trackArray ? trackArray.length : 0,
|
||
mediaOptionId: `${attrs.NAME}_${groupId}_${nextPersistentID}`,
|
||
lang: bcp47Utils.shortenLanguageCode(attrs.LANGUAGE),
|
||
};
|
||
if (attrs.URI) {
|
||
mediaEntry.url = resolve(attrs.URI, baseurl, inheritQuery);
|
||
}
|
||
if (!mediaEntry.name) {
|
||
mediaEntry.name = mediaEntry.lang;
|
||
if (mediaEntry.mediaType === MediaTypeFourCC.CLOSEDCAPTION) {
|
||
mediaEntry.name += ' CC';
|
||
}
|
||
}
|
||
if (mediaEntry.mediaType === MediaTypeFourCC.CLOSEDCAPTION && inStreamID) {
|
||
mediaEntry.inStreamID = inStreamID;
|
||
}
|
||
if (trackArray) {
|
||
mediaEntry.id = trackArray.length;
|
||
trackArray.push(mediaEntry);
|
||
}
|
||
nextPersistentID = MediaSelectionHelper.addMediaToSelectionArray(mediaSelectionGroup, mediaEntry, nextPersistentID);
|
||
}
|
||
if (alternateMediaInfo.subtitleAlternateOptions.length === 0 && !PlaylistParser.optOutClosedCaption(levels)) {
|
||
MediaSelectionHelper.addDefaultClosedCaptionOption(queueItemId, alternateMediaInfo.subtitleAlternateOptions, subtitleMediaSelectionGroup, nextPersistentID);
|
||
}
|
||
return { alternateMediaInfo, playlistParsingError };
|
||
}
|
||
static parseMediaOptionPlaylist(playlist, baseurl, inheritQuery = true, keySystemPreference, masterVariableList, itemId, mediaOptionId, mediaOptionType, logger, fragStartTime = 0, // itemStartOffset
|
||
iFrameStreamInf = false) {
|
||
var _a, _b;
|
||
let currentSN = 0;
|
||
let totalduration = 0;
|
||
const levelDetails = {
|
||
itemId,
|
||
mediaOptionId,
|
||
mediaOptionType,
|
||
type: '',
|
||
version: 0,
|
||
url: baseurl,
|
||
initSegments: {},
|
||
fragments: [],
|
||
liveOrEvent: true,
|
||
startSN: 0,
|
||
endSN: 0,
|
||
iframesOnly: iFrameStreamInf,
|
||
targetduration: 0,
|
||
totalduration: 0,
|
||
averagetargetduration: 0,
|
||
ptsKnown: false,
|
||
};
|
||
let decryptData = new DecryptData('NONE', baseurl, null, null, null);
|
||
let isKeySystemSelected = false;
|
||
let isEncrypted = false;
|
||
let cc = 0;
|
||
let prevFrag = null;
|
||
let frag = new FragmentBuilder(inheritQuery);
|
||
let result;
|
||
let ix;
|
||
let activeBitrate = 0;
|
||
let lastProgramDateTime; // Latest unmatched PROGRAM-DATE-TIME tag
|
||
const tempVariableList = {};
|
||
let unprocessedRelURL;
|
||
let playlistParsingError;
|
||
let prevInitSegment;
|
||
let ensureMapForDiscontinuity = true;
|
||
let ensureMapRefresh = true;
|
||
HlsRegex.LevelPlaylistFast.lastIndex = 0;
|
||
const incompatibleAssetError = () => new PlaylistParsingError(ErrorTypes.MEDIA_ERROR, ErrorDetails.MANIFEST_PARSING_ERROR, true, 'Invalid key system preference for the playlist', ErrorResponses.IncompatibleAsset);
|
||
while ((result = HlsRegex.LevelPlaylistFast.exec(playlist)) !== null) {
|
||
const duration = result[1];
|
||
if (duration) {
|
||
// INF
|
||
frag.duration = parseFloat(duration);
|
||
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
|
||
const title = (' ' + result[2]).slice(1);
|
||
frag.title = title ? title : null;
|
||
frag.tagList.push(title ? ['INF', duration, title] : ['INF', duration]);
|
||
}
|
||
else if (result[3]) {
|
||
// url
|
||
if (isFiniteNumber(frag.duration)) {
|
||
const sn = currentSN++;
|
||
// frag.type = type;
|
||
frag.start = totalduration + fragStartTime;
|
||
frag.levelkey = decryptData;
|
||
if (isEncrypted && !isKeySystemSelected) {
|
||
playlistParsingError = incompatibleAssetError();
|
||
break;
|
||
}
|
||
// The decrypt data has been associated with the fragment, reset the key system selected flag.
|
||
// So that if the key changes for the future segments, we can select it.
|
||
isKeySystemSelected = false;
|
||
isEncrypted = false;
|
||
frag.mediaSeqNum = sn;
|
||
// frag.levelId = levelId; // persistent level id
|
||
frag.cc = cc;
|
||
frag.iframe = levelDetails.iframesOnly;
|
||
frag.baseurl = baseurl;
|
||
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
|
||
unprocessedRelURL = PlaylistParser.replaceVariables((' ' + result[3]).slice(1), tempVariableList);
|
||
if (unprocessedRelURL.error) {
|
||
playlistParsingError = new PlaylistParsingError(ErrorTypes.MEDIA_ERROR, ErrorDetails.LEVEL_LOAD_ERROR, true, ErrorResponses.PlaylistErrorInvalidEXTXDEFINE.text, ErrorResponses.PlaylistErrorInvalidEXTXDEFINE);
|
||
break;
|
||
}
|
||
frag.relurl = unprocessedRelURL.updatedString;
|
||
// bitrate for fragment
|
||
frag.bitrate = isFiniteNumber(frag.byteRangeEndOffset) ? ((frag.byteRangeEndOffset - frag.byteRangeStartOffset) * 8) / frag.duration : activeBitrate;
|
||
if (lastProgramDateTime != null) {
|
||
frag.rawProgramDateTime = lastProgramDateTime;
|
||
frag.tagList.push(['PROGRAM-DATE-TIME', frag.rawProgramDateTime]);
|
||
const dt = frag.programDateTime.getTime();
|
||
levelDetails.programDateTimeMap = (_a = levelDetails.programDateTimeMap) !== null && _a !== void 0 ? _a : {};
|
||
levelDetails.programDateTimeMap[dt] = frag.mediaSeqNum;
|
||
levelDetails.dateMediaTimePairs = (_b = levelDetails.dateMediaTimePairs) !== null && _b !== void 0 ? _b : [];
|
||
levelDetails.dateMediaTimePairs.push([dt, frag.start]);
|
||
lastProgramDateTime = undefined;
|
||
}
|
||
levelDetails.fragments.push(frag.getMediaFragment(itemId, mediaOptionId, mediaOptionType));
|
||
prevFrag = frag;
|
||
totalduration += frag.duration;
|
||
// implicit init segment for iframes or after a discontinuity
|
||
if (ensureMapForDiscontinuity || !levelDetails.initSegments[cc] || ensureMapRefresh) {
|
||
if (levelDetails.iframesOnly && frag.byteRangeStartOffset > 0 && !levelDetails.initSegments[cc] && !ensureMapRefresh) {
|
||
const ifrag = new FragmentBuilder(inheritQuery);
|
||
ifrag.url = frag.url;
|
||
// allow the init segment to be up to 7 * 188 (must be multiples of 188)
|
||
ifrag.rawByteRange = Math.min(frag.byteRangeStartOffset, 1316) + '@0';
|
||
ifrag.baseurl = baseurl;
|
||
// ifrag.levelId = levelId;
|
||
// ifrag.type = type;
|
||
ifrag.isInitSegment = true;
|
||
ifrag.cc = cc;
|
||
ifrag.levelkey = decryptData;
|
||
ifrag.iframe = true;
|
||
if (isEncrypted && !isKeySystemSelected) {
|
||
playlistParsingError = incompatibleAssetError();
|
||
break;
|
||
}
|
||
// The decrypt data has been associated with the fragment, reset the key system selected flag.
|
||
// So that if the key changes for the future segments, we can select it.
|
||
isKeySystemSelected = false;
|
||
isEncrypted = false;
|
||
levelDetails.initSegments[cc] = ifrag.getMediaFragment(itemId, mediaOptionId, mediaOptionType);
|
||
logger.info(`generated implicit initSegment in cc ${cc}`);
|
||
}
|
||
else if (prevInitSegment) {
|
||
if (isFiniteNumber(prevInitSegment.discoSeqNum)) {
|
||
// use the last cc's *declared* initSegment (for fmp4 when MAP for a discontinuity is not defined)
|
||
logger.info(`assume initSegment from ${prevInitSegment.discoSeqNum} in cc ${cc}`);
|
||
}
|
||
// deferred assignment of cc
|
||
prevInitSegment.discoSeqNum = cc;
|
||
levelDetails.initSegments[cc] = prevInitSegment;
|
||
}
|
||
}
|
||
ensureMapForDiscontinuity = false;
|
||
ensureMapRefresh = false;
|
||
frag = new FragmentBuilder(inheritQuery);
|
||
}
|
||
}
|
||
else if (result[4]) {
|
||
// X-BYTERANGE
|
||
frag.rawByteRange = (' ' + result[4]).slice(1);
|
||
if (prevFrag) {
|
||
const lastByteRangeEndOffset = prevFrag.byteRangeEndOffset;
|
||
if (lastByteRangeEndOffset) {
|
||
frag.lastByteRangeEndOffset = lastByteRangeEndOffset;
|
||
}
|
||
}
|
||
}
|
||
else if (result[5]) {
|
||
// PROGRAM-DATE-TIME
|
||
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
|
||
lastProgramDateTime = (' ' + result[5]).slice(1);
|
||
}
|
||
else if (result[6]) {
|
||
// X-BITRATE
|
||
// Note that we assume that if this tag exists in the playlist then it should exist for every fragment
|
||
const parsedBitrate = parseInt(result[6]);
|
||
if (isFiniteNumber(parsedBitrate)) {
|
||
activeBitrate = parsedBitrate * 1000;
|
||
}
|
||
}
|
||
else if (result[7]) {
|
||
// DATERANGE
|
||
const rawDaterange = result[7];
|
||
const daterangeTags = PlaylistTagParser.parseTags(rawDaterange);
|
||
if (daterangeTags.ID) {
|
||
if (!levelDetails.daterangeTags) {
|
||
levelDetails.daterangeTags = {};
|
||
}
|
||
levelDetails.daterangeTags[daterangeTags.ID] = daterangeTags;
|
||
}
|
||
}
|
||
else {
|
||
result = result[0].match(HlsRegex.LevelPlaylistSlow);
|
||
for (ix = 1; ix < result.length; ix++) {
|
||
if (result[ix] !== undefined) {
|
||
break;
|
||
}
|
||
}
|
||
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
|
||
const unprocessedValue1 = PlaylistParser.replaceVariables((' ' + result[ix + 1]).slice(1), tempVariableList);
|
||
const unprocessedValue2 = PlaylistParser.replaceVariables((' ' + result[ix + 2]).slice(1), tempVariableList);
|
||
if (unprocessedValue1.error || unprocessedValue2.error) {
|
||
playlistParsingError = new PlaylistParsingError(ErrorTypes.MEDIA_ERROR, ErrorDetails.LEVEL_LOAD_ERROR, true, ErrorResponses.PlaylistErrorInvalidEXTXDEFINE.text, ErrorResponses.PlaylistErrorInvalidEXTXDEFINE);
|
||
break;
|
||
}
|
||
const value1 = unprocessedValue1.updatedString;
|
||
const value2 = unprocessedValue2.updatedString;
|
||
switch (result[ix]) {
|
||
case '#':
|
||
frag.tagList.push(value2 ? [value1, value2] : [value1]);
|
||
break;
|
||
case 'PLAYLIST-TYPE':
|
||
levelDetails.type = value1.toUpperCase();
|
||
if (levelDetails.type === 'VOD') {
|
||
levelDetails.liveOrEvent = false;
|
||
}
|
||
break;
|
||
case 'MEDIA-SEQUENCE':
|
||
if (levelDetails.fragments.length === 0) {
|
||
currentSN = levelDetails.startSN = parseInt(value1);
|
||
}
|
||
break;
|
||
case 'TARGETDURATION':
|
||
levelDetails.targetduration = parseFloat(value1);
|
||
break;
|
||
case 'VERSION':
|
||
levelDetails.version = parseInt(value1);
|
||
break;
|
||
case 'EXTM3U':
|
||
break;
|
||
case 'ENDLIST':
|
||
levelDetails.liveOrEvent = false;
|
||
break;
|
||
case 'DIS':
|
||
cc++;
|
||
frag.tagList.push(['DIS']);
|
||
ensureMapForDiscontinuity = true;
|
||
break;
|
||
case 'DISCONTINUITY-SEQ':
|
||
cc = parseInt(value1);
|
||
break;
|
||
case 'KEY':
|
||
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.4.4
|
||
const decryptparams = value1;
|
||
isEncrypted = true;
|
||
if (!isKeySystemSelected) {
|
||
try {
|
||
decryptData = PlaylistParser.parseDecryptData(decryptparams, baseurl, keySystemPreference);
|
||
}
|
||
catch (error) {
|
||
playlistParsingError = error;
|
||
}
|
||
if (decryptData) {
|
||
// Now mark that the key system has been selected. No more playing around with decryptData
|
||
isKeySystemSelected = true;
|
||
}
|
||
}
|
||
// if (!isKeySystemSelected || !decryptData) {
|
||
// logger.warn(`[Keys] Not selecting EXT-X-KEY tag : ${decryptparams}, IsKeySystemSelected: ${isKeySystemSelected}`);
|
||
// }
|
||
break;
|
||
case 'START':
|
||
const startParams = value1;
|
||
const startAttrs = PlaylistTagParser.parseTags(startParams);
|
||
const startTimeOffset = startAttrs['TIME-OFFSET'];
|
||
// TIME-OFFSET can be 0
|
||
if (isFiniteNumber(startTimeOffset)) {
|
||
levelDetails.startTimeOffset = startTimeOffset;
|
||
}
|
||
break;
|
||
case 'I-FRAMES':
|
||
levelDetails.iframesOnly = true;
|
||
break;
|
||
case 'MAP':
|
||
const mapAttrs = PlaylistTagParser.parseTags(value1);
|
||
frag.relurl = mapAttrs.URI;
|
||
frag.rawByteRange = mapAttrs.BYTERANGE;
|
||
frag.baseurl = baseurl;
|
||
// frag.levelId = levelId;
|
||
frag.isInitSegment = true;
|
||
frag.levelkey = decryptData; // TODO: handle MAP before KEY
|
||
if (isEncrypted && !isKeySystemSelected) {
|
||
playlistParsingError = incompatibleAssetError();
|
||
break;
|
||
}
|
||
// The decrypt data has been associated with the fragment, reset the key system selected flag.
|
||
// So that if the key changes for the future segments, we can select it.
|
||
isKeySystemSelected = false;
|
||
isEncrypted = false;
|
||
// The occurence of map indicates that it applies to all
|
||
// media fragments that follows until the next map. The
|
||
// DISCONTINUITY tag can come before or after or never
|
||
// relative to MAP tag.
|
||
// Common Scenario DISCO -> MAP -> FRAGMENT1 -> Fragment 2
|
||
// Uncommon Scenario Map -> Disco -> FRAGMENTS (live case)
|
||
// Defer insertion into initSegments[cc] until media
|
||
// fragments are seen
|
||
prevInitSegment = frag.getMediaFragment(itemId, mediaOptionId, mediaOptionType);
|
||
ensureMapRefresh = true;
|
||
// logger.info(`creating new initSegment`);
|
||
frag = new FragmentBuilder(inheritQuery);
|
||
break;
|
||
case 'DEFINE':
|
||
const defineList = HlsRegex.VARIABLE_PLAYLIST_REGEX.exec(value1);
|
||
const name = 'NAME' === defineList[1] ? defineList[2] : defineList[4], value = 'VALUE' === defineList[1] ? defineList[2] : defineList[4], importName = defineList[5], importValue = defineList[6];
|
||
// eslint-disable-next-line no-prototype-builtins
|
||
if (!name && !value && importName === 'IMPORT' && masterVariableList.hasOwnProperty(importValue)) {
|
||
tempVariableList[importValue] = masterVariableList[importValue];
|
||
// eslint-disable-next-line no-prototype-builtins
|
||
}
|
||
else if (name && !importName && defineList[1] !== defineList[3] && !tempVariableList.hasOwnProperty(name)) {
|
||
tempVariableList[name] = value;
|
||
}
|
||
else {
|
||
playlistParsingError = new PlaylistParsingError(ErrorTypes.MEDIA_ERROR, ErrorDetails.MANIFEST_PARSING_ERROR, true, ErrorResponses.PlaylistErrorMissingImportReference.text, ErrorResponses.PlaylistErrorMissingImportReference);
|
||
break;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
// frag.itemId = this.itemId;
|
||
}
|
||
frag = prevFrag;
|
||
// logger.info('found ' + levelDetails.fragments.length + ' fragments');
|
||
if (frag && !frag.relurl) {
|
||
levelDetails.fragments.pop();
|
||
totalduration -= frag.duration;
|
||
}
|
||
if (!levelDetails.liveOrEvent && levelDetails.fragments.length > 0) {
|
||
levelDetails.fragments[levelDetails.fragments.length - 1].isLastFragment = true;
|
||
}
|
||
levelDetails.totalduration = totalduration;
|
||
levelDetails.averagetargetduration = totalduration / levelDetails.fragments.length;
|
||
levelDetails.endSN = currentSN - 1;
|
||
return { mediaOptionDetails: levelDetails, playlistParsingError };
|
||
}
|
||
static parseRootPlaylist(queueItemId, playlist, baseurl, inheritQuery) {
|
||
var _a;
|
||
const levelArray = [];
|
||
const masterVariableList = {};
|
||
let contentSteeringOption = null;
|
||
let result;
|
||
let updatedURI;
|
||
let updatedAttrList;
|
||
let playlistParsingError;
|
||
let scoreAvailable = true;
|
||
HlsRegex.MasterPlaylist.lastIndex = 0;
|
||
while ((result = HlsRegex.MasterPlaylist.exec(playlist)) != null) {
|
||
// result[4] -> regex capture group 4, i.e. EXT-X-DEFINE
|
||
if (result[4]) {
|
||
// result[1] -> NAME/VALUE keyword, result[2] -> value of result[1], result[3] -> NAME/VALUE keyword,
|
||
// result[4] -> value of result[3], result[5] -> IMPORT keyword, result[6] -> value of IMPORT
|
||
result = HlsRegex.VARIABLE_PLAYLIST_REGEX.exec(result[4]);
|
||
const name = result[1] === 'NAME' ? result[2] : result[4], value = result[1] === 'VALUE' ? result[2] : result[4], importName = result[5];
|
||
// eslint-disable-next-line no-prototype-builtins
|
||
if (name && !masterVariableList.hasOwnProperty(name) && !importName && result[1] !== result[3]) {
|
||
masterVariableList[name] = value;
|
||
}
|
||
else {
|
||
playlistParsingError = new PlaylistParsingError(ErrorTypes.MEDIA_ERROR, ErrorDetails.MANIFEST_PARSING_ERROR, true, ErrorResponses.PlaylistErrorInvalidEXTXDEFINE.text, ErrorResponses.PlaylistErrorInvalidEXTXDEFINE);
|
||
break;
|
||
}
|
||
}
|
||
else if (result[5]) {
|
||
// result[5] -> regex capture group 5, i.e. EXT-X-CONTENT-STEERING
|
||
const updatedAttrList = PlaylistParser.replaceVariables(result[5], masterVariableList);
|
||
if (updatedAttrList.error) {
|
||
playlistParsingError = new PlaylistParsingError(ErrorTypes.MEDIA_ERROR, ErrorDetails.MANIFEST_PARSING_ERROR, true, ErrorResponses.PlaylistErrorInvalidEXTXDEFINE.text, ErrorResponses.PlaylistErrorInvalidEXTXDEFINE);
|
||
break;
|
||
}
|
||
const attrs = PlaylistTagParser.parseTags(updatedAttrList.updatedString);
|
||
if (typeof attrs['SERVER-URI'] !== 'string') {
|
||
playlistParsingError = new PlaylistParsingError(ErrorTypes.MEDIA_ERROR, ErrorDetails.MANIFEST_PARSING_ERROR, true, ErrorResponses.PlaylistErrorInvalidSERVERURI.text, ErrorResponses.PlaylistErrorInvalidSERVERURI);
|
||
break;
|
||
}
|
||
if (attrs['PATHWAY-ID'] != null && typeof attrs['PATHWAY-ID'] !== 'string') {
|
||
playlistParsingError = new PlaylistParsingError(ErrorTypes.MEDIA_ERROR, ErrorDetails.MANIFEST_PARSING_ERROR, true, ErrorResponses.PlaylistErrorInvalidPATHWAYID.text, ErrorResponses.PlaylistErrorInvalidPATHWAYID);
|
||
break;
|
||
}
|
||
contentSteeringOption = {
|
||
serverURI: resolve(attrs['SERVER-URI'], baseurl, false),
|
||
initPathwayID: attrs['PATHWAY-ID'] || '.',
|
||
};
|
||
}
|
||
else {
|
||
// result[1] -> regex capture group 1, i.e. EXT-X-STREAM-INF
|
||
// otherwise, result[3] -> regex capture group 3, i.e. EXT-X-I-FRAME-STREAM-INF
|
||
updatedAttrList = PlaylistParser.replaceVariables(result[1] || result[3], masterVariableList);
|
||
const attrs = PlaylistTagParser.parseTags(updatedAttrList.updatedString);
|
||
// result[2] -> regex capture group 2, i.e. URI to be read from the new line (for EXT-X-STREAM-INF)
|
||
// otherwise, arrts.URI -> URI to be read from the attributes (for EXT-X-I-FRAME-STREAM-INF)
|
||
updatedURI = PlaylistParser.replaceVariables(result[2] || attrs.URI, masterVariableList);
|
||
if (updatedAttrList.error || updatedURI.error) {
|
||
playlistParsingError = new PlaylistParsingError(ErrorTypes.MEDIA_ERROR, ErrorDetails.MANIFEST_PARSING_ERROR, true, ErrorResponses.PlaylistErrorInvalidEXTXDEFINE.text, ErrorResponses.PlaylistErrorInvalidEXTXDEFINE);
|
||
break;
|
||
}
|
||
if ((attrs.SCORE !== undefined && !isFiniteNumber(attrs.SCORE)) || attrs.SCORE < 0) {
|
||
playlistParsingError = new PlaylistParsingError(ErrorTypes.MEDIA_ERROR, ErrorDetails.MANIFEST_PARSING_ERROR, true, ErrorResponses.PlaylistErrorInvalidSCORE.text, ErrorResponses.PlaylistErrorInvalidSCORE);
|
||
scoreAvailable = false;
|
||
break;
|
||
// if SCORE attribute is missing in any level, default to bandwidth
|
||
}
|
||
else if (scoreAvailable && attrs.SCORE === undefined) {
|
||
// logger.warn('SCORE attribute missing in one of the levels, defaulting to bandwidth selection');
|
||
scoreAvailable = false;
|
||
}
|
||
const bandwidth = attrs.BANDWIDTH;
|
||
const avgBandwidth = attrs['AVERAGE-BANDWIDTH'];
|
||
const bitrate = avgBandwidth || bandwidth;
|
||
const videoRange = (_a = attrs['VIDEO-RANGE']) !== null && _a !== void 0 ? _a : 'SDR';
|
||
if (!isVideoRange(videoRange)) {
|
||
// Skip this one.
|
||
continue;
|
||
}
|
||
// Note this is different from audio persistentId; this is an arbitrarily calculated value based on bitrate and index
|
||
const mediaOptionId = `level_${(bitrate || 0) + (levelArray.length % 1000) / 1000}`;
|
||
const levelInfo = {
|
||
itemId: queueItemId,
|
||
mediaOptionId,
|
||
mediaOptionType: MediaOptionType.Variant,
|
||
attrs,
|
||
url: resolve(updatedURI.updatedString, baseurl, inheritQuery),
|
||
name: attrs.NAME,
|
||
audioGroupId: attrs.AUDIO,
|
||
subtitleGroupId: attrs.SUBTITLES,
|
||
iframes: !!result[3],
|
||
bandwidth,
|
||
avgBandwidth,
|
||
bitrate,
|
||
videoRange,
|
||
frameRate: attrs['FRAME-RATE'],
|
||
allowedCPCMap: PlaylistParser.parseAllowedCPC(attrs['ALLOWED-CPC']),
|
||
closedcaption: attrs['CLOSED-CAPTIONS'],
|
||
levelCodec: attrs.CODECS,
|
||
score: attrs.SCORE,
|
||
pathwayID: attrs['PATHWAY-ID'] || '.',
|
||
};
|
||
const hdcpString = attrs['HDCP-LEVEL'];
|
||
if (isHdcpLevel(hdcpString)) {
|
||
levelInfo.hdcpLevel = hdcpString;
|
||
}
|
||
const resolution = attrs.RESOLUTION;
|
||
if (resolution) {
|
||
levelInfo.width = resolution.width;
|
||
levelInfo.height = resolution.height;
|
||
}
|
||
if (attrs.CODECS) {
|
||
levelInfo.videoCodecList = new Array();
|
||
levelInfo.audioCodecList = new Array();
|
||
const codecs = attrs.CODECS.split(/[ ,]+/);
|
||
const { length } = codecs;
|
||
for (let i = 0; i < length; i++) {
|
||
const codec = codecs[i];
|
||
const prefix = codec.slice(0, 4);
|
||
switch (prefix) {
|
||
case 'avc1':
|
||
levelInfo.videoCodec = MediaUtil.avc1toavcoti(codec);
|
||
levelInfo.videoCodecList.push(levelInfo.videoCodec);
|
||
break;
|
||
case 'avc3':
|
||
case 'dvav':
|
||
case 'dva1':
|
||
// HEVC
|
||
case 'hev1': // eslint-disable-line
|
||
case 'hvc1':
|
||
case 'dvh1':
|
||
case 'dvhe':
|
||
case 'vp09':
|
||
levelInfo.videoCodec = codec;
|
||
levelInfo.videoCodecList.push(levelInfo.videoCodec);
|
||
break;
|
||
case 'mp4a':
|
||
case 'ec-3':
|
||
case 'ac-3':
|
||
levelInfo.audioCodec = codec;
|
||
levelInfo.audioCodecList.push(levelInfo.audioCodec);
|
||
break;
|
||
default:
|
||
// logger.warn(`Unrecognized codec ${codec}`);
|
||
levelInfo.audioCodec = codec; // Just set it to whatever, maybe it will work.
|
||
levelInfo.audioCodecList.push(levelInfo.audioCodec);
|
||
break;
|
||
}
|
||
}
|
||
if (levelInfo.audioCodecList.length > 1) {
|
||
levelInfo.audioCodec = RichestMedia.getRichestAudioCodec(levelInfo.audioCodecList);
|
||
}
|
||
if (levelInfo.videoCodecList.length > 1) {
|
||
levelInfo.videoCodec = RichestMedia.getRichestVideoCodec(levelInfo.videoCodecList);
|
||
}
|
||
}
|
||
if ((playlistParsingError = validatePathwayID(levelInfo.pathwayID)) != null) {
|
||
break;
|
||
}
|
||
levelArray.push(levelInfo);
|
||
}
|
||
}
|
||
return { variantMediaOptions: levelArray, contentSteeringOption, masterVariableList, playlistParsingError, scoreAvailable };
|
||
}
|
||
/**
|
||
* Encryption Stuff
|
||
*/
|
||
static parseAllowedCPC(allowedCPCString) {
|
||
if (typeof allowedCPCString !== 'string') {
|
||
return null;
|
||
}
|
||
const allowedCPCMap = {};
|
||
allowedCPCString.split(',').forEach((keyFormatToCPCListStr) => {
|
||
const splitList = keyFormatToCPCListStr.split(':');
|
||
let keyFormat, cpcStr;
|
||
if (splitList.length === 2) {
|
||
// The keyformat
|
||
keyFormat = splitList[0].trim();
|
||
cpcStr = splitList[1].trim();
|
||
}
|
||
else if (splitList.length > 2) {
|
||
cpcStr = splitList[splitList.length - 1].trim();
|
||
splitList.pop();
|
||
keyFormat = splitList.join(':');
|
||
}
|
||
else {
|
||
return;
|
||
}
|
||
if (keyFormat in allowedCPCMap) {
|
||
return;
|
||
}
|
||
let cpcLabels = new Array();
|
||
if (cpcStr !== '') {
|
||
cpcLabels = cpcStr.split('/').map((cpcLabel) => cpcLabel.trim());
|
||
}
|
||
allowedCPCMap[keyFormat] = cpcLabels;
|
||
});
|
||
return allowedCPCMap;
|
||
}
|
||
static parseSessionKeys(playlist, baseurl, keySystemPreference) {
|
||
let result;
|
||
const sessionKeys = [];
|
||
HlsRegex.SessionData.lastIndex = 0;
|
||
while ((result = HlsRegex.SessionKeys.exec(playlist))) {
|
||
try {
|
||
const decryptData = PlaylistParser.parseDecryptData(result[1], baseurl, keySystemPreference);
|
||
if (decryptData && decryptData.isEncrypted) {
|
||
sessionKeys.push(decryptData);
|
||
}
|
||
}
|
||
catch (error) {
|
||
// PlaylistParsingError: "Invalid IV..."
|
||
}
|
||
}
|
||
return sessionKeys;
|
||
}
|
||
//#region session data
|
||
static parseSessionData(sessionDataString, baseurl) {
|
||
let result;
|
||
const sessionDataItemList = [];
|
||
const existingItems = new Set();
|
||
HlsRegex.SessionData.lastIndex = 0;
|
||
while ((result = HlsRegex.SessionData.exec(sessionDataString)) != null) {
|
||
const attrs = PlaylistTagParser.parseTags(result[1]);
|
||
attrs.LANGUAGE = bcp47Utils.shortenLanguageCode(attrs.LANGUAGE);
|
||
const uniqueKey = attrs.LANGUAGE ? attrs['DATA-ID'] + '|' + attrs.LANGUAGE : undefined;
|
||
if (isSessionDataItem(attrs)) {
|
||
if (!uniqueKey || !existingItems.has(uniqueKey)) {
|
||
// Prevent duplicated DATA-ID & LANGUAGE pair.
|
||
// If LANGUAGE is absent, duplicated DATA-ID items may be okay.
|
||
if (attrs['DATA-ID'] === SessionDataKey.OTHER_TEXT_BADGES) {
|
||
attrs.VALUE = tryDecodeBase64Metadata(attrs.VALUE);
|
||
}
|
||
sessionDataItemList.push(attrs);
|
||
if (uniqueKey) {
|
||
existingItems.add(uniqueKey);
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
const logger = getLogger();
|
||
logger.error(`Error processing DATA-ID ${attrs['DATA-ID']} and LANGUAGE ${attrs.LANGUAGE}`);
|
||
}
|
||
}
|
||
const sessionData = {
|
||
itemList: sessionDataItemList,
|
||
baseUrl: baseurl,
|
||
};
|
||
return sessionData;
|
||
}
|
||
}
|
||
//#endregion session data
|
||
var PlaylistParser$1 = PlaylistParser;
|
||
|
||
const fromUrlText = (requestCtx, loadConfig, extendMaxTTFB) => {
|
||
const ctx = Object.assign(Object.assign({}, requestCtx), { method: 'GET', responseType: 'text', extendMaxTTFB: extendMaxTTFB });
|
||
const url = ctx.url;
|
||
if (isCustomUrl(url)) {
|
||
const customUrlLoader = getCustomUrlLoader();
|
||
return customUrlLoader.load(ctx, loadConfig).pipe(map((res) => {
|
||
const responseText = res.data.response.data.toString();
|
||
return {
|
||
responseText,
|
||
responseURL: res.data.response.uri,
|
||
stats: res.stats,
|
||
};
|
||
}));
|
||
}
|
||
else {
|
||
return fromXMLHttpRequest(ctx, loadConfig).pipe(map(([xhr, stats]) => ({
|
||
responseText: xhr.responseText,
|
||
responseURL: xhr.responseURL,
|
||
stats: stats,
|
||
})));
|
||
}
|
||
};
|
||
|
||
const loadMediaOptionDetails = (mediaOption, itemStartOffset, config, loadPolicy, logger, keySystemPreference, statsService, masterVariableList, extendMaxTTFB) => {
|
||
const { url, itemId, mediaOptionId, mediaOptionType, iframes = false } = mediaOption;
|
||
const loadConfig = getLoadConfig(mediaOption, loadPolicy);
|
||
return fromUrlText({ url, xhrSetup: config.xhrSetup }, loadConfig, extendMaxTTFB).pipe(map(({ responseText: rawPlaylist, responseURL: rURL, stats }) => {
|
||
logger.debug(`media playlist url: ${url}, baseUrl: ${rURL}`);
|
||
if (!rURL) {
|
||
logger.warn('Missing response url. Reusing request url as base url');
|
||
rURL = url;
|
||
}
|
||
const t1 = performance.now();
|
||
const parsed = PlaylistParser.parseMediaOptionPlaylist(rawPlaylist, rURL, true, keySystemPreference, masterVariableList, itemId, mediaOptionId, mediaOptionType, logger, itemStartOffset, iframes);
|
||
updatePlaylistAttributes(parsed.mediaOptionDetails);
|
||
const t2 = performance.now();
|
||
const { mediaOptionDetails } = parsed;
|
||
const playlistSample = {
|
||
playlistLoadTimeMs: stats.tload - stats.trequest,
|
||
playlistParseTimeMs: t2 - t1,
|
||
};
|
||
statsService.setPlaylistSample(playlistSample);
|
||
const results = { mediaOptionDetails, stats };
|
||
return results;
|
||
}), convertToPlaylistNetworkError(mediaOptionType, mediaOptionId, false, url));
|
||
};
|
||
|
||
const decryptMediaFragment = (mediaFragment, data, config, logger, crypto) => {
|
||
return of(data).pipe(switchMap((data) => {
|
||
const { keyTagInfo, isInitSegment, iframe, byteRangeOffset } = mediaFragment;
|
||
const { method } = keyTagInfo;
|
||
const { start, end } = byteRangeOffset;
|
||
if (method === 'AES-128') {
|
||
if (keyTagInfo.uri && !keyTagInfo.iv && (!keyTagInfo.format || keyTagInfo.format === 'identity')) {
|
||
keyTagInfo.iv = createInitializationVector(mediaFragment.mediaSeqNum);
|
||
}
|
||
const cipherText = data;
|
||
const key = keyTagInfo.key.buffer;
|
||
const iv = keyTagInfo.iv.buffer;
|
||
// For Iframes and Initsegments, byterange indicates the plainText length - ref: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-6.3.6
|
||
// For media segments, byterange indicates the length of the cipherText, and the decrypt method will remove the padding to recover the plainText length
|
||
const plainTextLength = end && (iframe || isInitSegment) ? end - start : undefined;
|
||
/* WebCrypto is the default choice. However, we use JSCrypto in the following scenarios.
|
||
1. WebCrypto disabled via config.
|
||
2. Due to [rdar://73772133], Iframes or Initsegment served via byte-ranges.
|
||
*/
|
||
const useJSCrypto = !config.enableWebCrypto || !!plainTextLength;
|
||
const keyCopy = key.slice(0); // Copy the Key and IV for the webworker.
|
||
const ivCopy = iv.slice(0); // Copy the Key and IV for the webworker.
|
||
const options = { useJSCrypto, plainTextLength };
|
||
return crypto.decrypt(keyCopy, ivCopy, 'AES-CBC', cipherText, options);
|
||
}
|
||
else {
|
||
// If method is not AES-128, just return the original buffer
|
||
return of(data);
|
||
}
|
||
}));
|
||
};
|
||
/**
|
||
* Utility method to create an initialization vector from integer
|
||
* @param ivValue 128-bit unsigned int representing initialization vector
|
||
* @returns {Uint8Array} Buffer representation of the IV
|
||
*/
|
||
function createInitializationVector(ivValue) {
|
||
const uint8View = new Uint8Array(16);
|
||
for (let i = 12; i < 16; i++) {
|
||
uint8View[i] = (ivValue >> (8 * (15 - i))) & 255;
|
||
}
|
||
return uint8View;
|
||
}
|
||
|
||
function fragIsInDetails(details, parsedFrag) {
|
||
const fragments = details.fragments;
|
||
const fragIdx = parsedFrag.mediaSeqNum - details.startSN;
|
||
return fragIdx >= 0 && fragIdx < details.fragments.length && fragEqual(parsedFrag, fragments[fragIdx]);
|
||
}
|
||
/**
|
||
* Update fragment lookup table
|
||
*/
|
||
function updateFragPTSDTS(details, parsedFrag, startOffset, iframeMode, cpyModified = false, forceUpdateAll = false) {
|
||
if (!fragIsInDetails(details, parsedFrag)) {
|
||
return;
|
||
}
|
||
const fragIdx = parsedFrag.mediaSeqNum - details.startSN;
|
||
let fragments = details.fragments;
|
||
let frag = fragments[fragIdx];
|
||
const { startDtsTs, startPts, endPts } = parsedFrag;
|
||
if (cpyModified) {
|
||
fragments = details.fragments = details.fragments.slice(); // shallow cpy
|
||
frag = fragments[fragIdx] = Object.assign({}, frag);
|
||
}
|
||
frag.startDtsTs = startDtsTs;
|
||
frag.startPts = startPts;
|
||
frag.endPts = endPts;
|
||
if (iframeMode || frag.isIframeStart !== undefined) {
|
||
frag.isIframeStart = iframeMode;
|
||
}
|
||
frag.start = startOffset;
|
||
frag.duration = diffSeconds(endPts, startPts);
|
||
// adjust fragment PTS/duration from seqnum-1 to frag 0
|
||
for (let i = fragIdx, canContinue = true; i > 0 && (forceUpdateAll || canContinue); i--) {
|
||
canContinue = updatePTS(fragments, i, i - 1, iframeMode, startPts.timescale, cpyModified);
|
||
}
|
||
// adjust fragment PTS/duration from seqnum to last frag
|
||
for (let i = fragIdx, canContinue = true; i < fragments.length - 1 && (forceUpdateAll || canContinue); i++) {
|
||
canContinue = updatePTS(fragments, i, i + 1, iframeMode, startPts.timescale, cpyModified);
|
||
}
|
||
const endFrag = fragments[fragments.length - 1];
|
||
details.totalduration = endFrag.start + endFrag.duration - fragments[0].start;
|
||
details.ptsKnown = true;
|
||
}
|
||
/**
|
||
*
|
||
* @param fragments fragment list
|
||
* @param fromIdx index inside fragments of the reference fragment
|
||
* @param toIdx index inside fragments of the fragment to modify
|
||
* @param iframeMode (optional) the iframeMode when this was called
|
||
* @param timescale (optional) use timescale for deciding if value is changed enough to update
|
||
* @param cpyModified (optional) whether we should make a copy of the fragment rather than modify in place
|
||
* @returns whether it should continue iterating
|
||
*/
|
||
function updatePTS(fragments, fromIdx, toIdx, iframeMode, timescale, cpyModified = false) {
|
||
const fragFrom = fragments[fromIdx];
|
||
let fragTo = fragments[toIdx];
|
||
const forceUpdateStartTime = iframeMode != null && fragTo.isIframeStart != null && fragTo.isIframeStart !== iframeMode && fragTo.discoSeqNum === fragFrom.discoSeqNum;
|
||
let newStart = fragTo.start;
|
||
// Update all frag start times in disco after iframe transition or unknown pts
|
||
if (forceUpdateStartTime || fragTo.startPts == null) {
|
||
if (toIdx > fromIdx) {
|
||
newStart = fragFrom.start + fragFrom.duration;
|
||
}
|
||
else {
|
||
newStart = Math.max(fragFrom.start - fragTo.duration, 0);
|
||
}
|
||
}
|
||
const precision = isFiniteNumber(timescale) ? 1 / timescale : Number.EPSILON;
|
||
const timeChanged = Math.abs(fragTo.start - newStart) > precision;
|
||
if (forceUpdateStartTime || timeChanged) {
|
||
// Never update duration. These are parsed values.
|
||
if (cpyModified) {
|
||
fragTo = fragments[toIdx] = Object.assign({}, fragTo);
|
||
}
|
||
if (timeChanged) {
|
||
fragTo.start = newStart;
|
||
}
|
||
if (forceUpdateStartTime) {
|
||
fragTo.isIframeStart = iframeMode;
|
||
}
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function updateDateToMediaTimeMap(details) {
|
||
if (details.programDateTimeMap) {
|
||
details.dateMediaTimePairs = [];
|
||
for (const [dtStr, sn] of Object.entries(details.programDateTimeMap)) {
|
||
const dt = Number(dtStr);
|
||
const frag = details.fragments[sn - details.startSN];
|
||
if (frag) {
|
||
const startTime = frag.start;
|
||
details.dateMediaTimePairs.push([dt, startTime]);
|
||
}
|
||
}
|
||
details.dateMediaTimePairs.sort((a, b) => {
|
||
return a[0] - b[0];
|
||
});
|
||
}
|
||
}
|
||
|
||
class SimpleListFilter {
|
||
constructor(option) {
|
||
this.option = option;
|
||
}
|
||
get name() {
|
||
return this.option.name;
|
||
}
|
||
get priority() {
|
||
return this.option.priority;
|
||
}
|
||
get expiry() {
|
||
return this.option.expiry;
|
||
}
|
||
filter(originalList, options) {
|
||
const context = (this.option.initFn && this.option.initFn(originalList, options)) || (options ? Object.assign({}, options) : {});
|
||
let filteredList = originalList;
|
||
if (this.option.firstPassFn) {
|
||
originalList.forEach((value, index) => this.option.firstPassFn(value, index, context, originalList));
|
||
}
|
||
if (this.option.filterFn) {
|
||
filteredList = originalList.filter((value, index) => this.option.filterFn(value, index, context, originalList));
|
||
}
|
||
if ((this.option.filterFn == null || filteredList.length === 0) && this.option.minSortingFn) {
|
||
filteredList = originalList.sort((a, b) => this.option.minSortingFn(a, b, context, originalList));
|
||
}
|
||
if (this.option.finalFn) {
|
||
this.option.finalFn(filteredList, context, originalList);
|
||
}
|
||
return filteredList;
|
||
}
|
||
}
|
||
function applyFilters(originalList, filters, options) {
|
||
return (filters || []).reduce((list, filter) => filter.filter(list, options), Array.from(originalList));
|
||
}
|
||
function isExpired(expiry, now) {
|
||
return now >= expiry;
|
||
}
|
||
function sortFilters(filters) {
|
||
const cpy = [...filters];
|
||
return cpy.sort((a, b) => {
|
||
var _a, _b;
|
||
const priorityA = (_a = a.priority) !== null && _a !== void 0 ? _a : Number.MAX_SAFE_INTEGER;
|
||
const priorityB = (_b = b.priority) !== null && _b !== void 0 ? _b : Number.MAX_SAFE_INTEGER;
|
||
return priorityA - priorityB;
|
||
});
|
||
}
|
||
|
||
function getPreferredList(preferredHost, filteredMediaOptionList) {
|
||
return filteredMediaOptionList.filter((x) => { var _a; return hasMatchingHost(preferredHost, (_a = x.url) !== null && _a !== void 0 ? _a : null); });
|
||
}
|
||
function newRemoveFilter() {
|
||
return new SimpleListFilter({
|
||
name: 'Remove Filter',
|
||
priority: 0,
|
||
filterFn: (value, index, options) => {
|
||
return !options || options.removed.every((mediaOptionId) => value.mediaOptionId !== mediaOptionId);
|
||
},
|
||
});
|
||
}
|
||
function newPenaltyBoxFilter() {
|
||
return new SimpleListFilter({
|
||
name: 'Penalty Box Filter',
|
||
priority: 1,
|
||
filterFn: (value, index, options) => {
|
||
const now = performance.now();
|
||
return !options || options.penaltyBoxQueue.every((info) => isExpired(info.expiry, now) || value.mediaOptionId !== info.mediaOptionId);
|
||
},
|
||
});
|
||
}
|
||
function newCompatibleIdsFilter() {
|
||
return new SimpleListFilter({
|
||
name: 'Compatible IDs Filter',
|
||
priority: 1,
|
||
filterFn: (value, index, options) => {
|
||
return !options || options.compatibleIds == null || options.compatibleIds.some((id) => id === value.mediaOptionId);
|
||
},
|
||
});
|
||
}
|
||
function makeCommonFilters() {
|
||
// prettier-ignore
|
||
return [
|
||
newRemoveFilter(),
|
||
newPenaltyBoxFilter(),
|
||
newCompatibleIdsFilter(),
|
||
];
|
||
}
|
||
/**
|
||
* @brief Query used to get the filtered media option list for a given media option type.
|
||
*/
|
||
class MediaOptionListQuery extends QueryEntity {
|
||
constructor(store, itemId, mediaOptionType) {
|
||
super(store);
|
||
this.itemId = itemId;
|
||
this.mediaOptionType = mediaOptionType;
|
||
this.allowFilters = this._initFilters();
|
||
}
|
||
/**
|
||
* Get the unfiltered media option list
|
||
*/
|
||
get mediaOptionList() {
|
||
var _a;
|
||
return ((_a = this.mediaOptionListInfo) === null || _a === void 0 ? void 0 : _a.mediaOptions) || null;
|
||
}
|
||
get mediaOptionList$() {
|
||
return this.mediaOptionListInfo$.pipe(map(({ mediaOptions }) => mediaOptions));
|
||
}
|
||
mediaOptionFromId(mediaOptionId) {
|
||
var _a, _b;
|
||
const list = (_a = this.mediaOptionList) !== null && _a !== void 0 ? _a : [];
|
||
return (_b = list.find((option) => option.mediaOptionId === mediaOptionId)) !== null && _b !== void 0 ? _b : null;
|
||
}
|
||
_getFilteredList(mediaOptionListInfo) {
|
||
const mediaOptionList = mediaOptionListInfo.mediaOptions;
|
||
return applyFilters(mediaOptionList, this.allowFilters, mediaOptionListInfo);
|
||
}
|
||
/**
|
||
* Get the filtered media option list
|
||
*/
|
||
get filteredMediaOptionList() {
|
||
return this.mediaOptionListInfo ? this._getFilteredList(this.mediaOptionListInfo) : null;
|
||
}
|
||
get filteredMediaOptionList$() {
|
||
return this.mediaOptionListInfo$.pipe(switchMap((mediaOptionListInfo) => {
|
||
const fire$ = [VOID];
|
||
const now = performance.now();
|
||
for (const penalty of mediaOptionListInfo.penaltyBoxQueue) {
|
||
if (isFiniteNumber(penalty.expiry) && penalty.expiry > now) {
|
||
fire$.push(timer(penalty.expiry - now));
|
||
}
|
||
}
|
||
return merge(...fire$).pipe(map(() => {
|
||
return this._getFilteredList(mediaOptionListInfo);
|
||
}));
|
||
}), distinctUntilArrayItemChanged());
|
||
}
|
||
/**
|
||
* Get the filtered media option list that also has a hostname matching the preferred host
|
||
*/
|
||
get preferredMediaOptionList() {
|
||
return this.filteredMediaOptionList ? getPreferredList(this.preferredHost, this.filteredMediaOptionList) : [];
|
||
}
|
||
get preferredMediaOptionList$() {
|
||
return combineQueries([this.preferredHost$, this.filteredMediaOptionList$]).pipe(map(([preferredHost, filteredMediaOptionList]) => {
|
||
return getPreferredList(preferredHost, filteredMediaOptionList);
|
||
}));
|
||
}
|
||
/**
|
||
* @param fromId Media option id used as reference for the switch
|
||
* @returns The new host to switch to, or the current host if not switching
|
||
*/
|
||
getNewHost(fromId) {
|
||
const fallback = this.getFallbackVariant(fromId, false, true);
|
||
if (fallback === null || fallback === void 0 ? void 0 : fallback.url) {
|
||
return getHostName(fallback.url);
|
||
}
|
||
return this.preferredHost;
|
||
}
|
||
}
|
||
|
||
function isHDRLevel(levelInfo) {
|
||
return levelInfo.videoRange === 'PQ' || levelInfo.videoRange === 'HLG';
|
||
}
|
||
function isIframeLevel(option) {
|
||
return isMatchingIframeLevel(true, option);
|
||
}
|
||
function isMatchingIframeLevel(iframeMode, option) {
|
||
return option.iframes === iframeMode;
|
||
}
|
||
/**
|
||
* Sort function for variants. Ordering determines how ABR mediaOptionList is stored.
|
||
* Order will be preserved even after filtering
|
||
*
|
||
* @param variantList The list to sort
|
||
* @param hasScore Use SCORE to sort the list
|
||
* @returns Sorted list
|
||
*/
|
||
function sortVariants(variantList, hasScore) {
|
||
const cpy = [...variantList];
|
||
if (hasScore) {
|
||
// ascending order by score, then descending order by bitrate
|
||
cpy.sort((a, b) => {
|
||
return a.score - b.score || b.bitrate - a.bitrate;
|
||
});
|
||
}
|
||
else {
|
||
// bitrate in ascending order
|
||
cpy.sort((a, b) => {
|
||
return a.bitrate - b.bitrate;
|
||
});
|
||
}
|
||
return cpy;
|
||
}
|
||
var Rank;
|
||
(function (Rank) {
|
||
Rank[Rank["Better"] = 1] = "Better";
|
||
Rank[Rank["Same"] = 0] = "Same";
|
||
Rank[Rank["Worse"] = -1] = "Worse";
|
||
})(Rank || (Rank = {}));
|
||
/**
|
||
* @param fromVariant The variant we're switching from. We will try to choose something with similar bitrate in all cases
|
||
* @param hasScore Whether we should consider SCORE attribute when comparing candidates
|
||
* @param candidate Candidate to consider
|
||
* @param currentBest Current best candidate
|
||
* @returns Better if candidate is better than currentBest, Same if candidate is same as currentBest, else Worse
|
||
*/
|
||
function compareCandidate(fromVariant, hasScore, candidate, currentBest) {
|
||
// TODO: handle SCORE
|
||
if (!currentBest || (candidate.bitrate > currentBest.bitrate && candidate.bitrate <= fromVariant.bitrate)) {
|
||
return Rank.Better;
|
||
}
|
||
else if (candidate.bitrate === currentBest.bitrate) {
|
||
return Rank.Same;
|
||
}
|
||
return Rank.Worse;
|
||
}
|
||
function rankComparison(matchA, matchB) {
|
||
if (matchA && !matchB) {
|
||
return -1;
|
||
}
|
||
else if (!matchA && matchB) {
|
||
return 1;
|
||
}
|
||
return 0;
|
||
}
|
||
function sortFallbackVariants(variantList, fromVariant, hasScore) {
|
||
const cpy = [...variantList];
|
||
const fromHost = getHostName(fromVariant.url);
|
||
const fromAudioGroup = fromVariant.audioGroupId;
|
||
// sort score or bitrate in descending order
|
||
// tie breaker: hostname, then audioGroup
|
||
cpy.sort((a, b) => {
|
||
let rank = 0;
|
||
const useScore = hasScore && isFiniteNumber(a.score) && isFiniteNumber(b.score);
|
||
const ABetterThanB = useScore ? a.score > b.score && a.score <= fromVariant.score : a.bitrate > b.bitrate && a.bitrate <= fromVariant.bitrate;
|
||
const AEqualB = useScore ? a.score === b.score : a.bitrate === b.bitrate;
|
||
if (ABetterThanB) {
|
||
rank = -1;
|
||
}
|
||
else if (AEqualB) {
|
||
const AmatchHost = hasMatchingHost(fromHost, a.url);
|
||
const BmatchHost = hasMatchingHost(fromHost, b.url);
|
||
rank = rankComparison(AmatchHost, BmatchHost);
|
||
if (rank === 0) {
|
||
const AmatchGroup = !fromAudioGroup || a.audioGroupId === fromAudioGroup;
|
||
const BmatchGroup = !fromAudioGroup || b.audioGroupId === fromAudioGroup;
|
||
rank = rankComparison(AmatchGroup, BmatchGroup);
|
||
}
|
||
}
|
||
else {
|
||
rank = 1;
|
||
}
|
||
return rank;
|
||
});
|
||
return cpy;
|
||
}
|
||
function newHDRFilter() {
|
||
return new SimpleListFilter({
|
||
name: 'HDR Filter',
|
||
priority: 1,
|
||
filterFn: (value, index, options) => {
|
||
return !options || (options.hasHdrLevels && options.preferHDR) === isHDRLevel(value);
|
||
},
|
||
});
|
||
}
|
||
function isSizeWithinTolerance(variant, viewportInfo) {
|
||
const TOLERANCE_FACTOR = 1.35; // Value inherited from native stack
|
||
return (variant.width < viewportInfo.width * TOLERANCE_FACTOR &&
|
||
variant.height < viewportInfo.height * TOLERANCE_FACTOR &&
|
||
variant.width * variant.height < viewportInfo.width * viewportInfo.height * TOLERANCE_FACTOR);
|
||
}
|
||
function newViewportFilter() {
|
||
return new SimpleListFilter({
|
||
name: 'Viewport Filter',
|
||
priority: 1,
|
||
firstPassFn: (value, index, context) => {
|
||
if (context && value && !value.iframes && value.videoCodec) {
|
||
// Find the lowest non-iframe level with video
|
||
const lowestBitrate = !context.lowestBitrate || value.bitrate < context.lowestBitrate ? value.bitrate : context.lowestBitrate;
|
||
context.lowestBitrate = lowestBitrate;
|
||
}
|
||
},
|
||
filterFn: (value, index, context) => {
|
||
if (!value || !context || !context.viewportInfo || !value.videoCodec || !context.lowestBitrate) {
|
||
// Don't apply this filter
|
||
return true;
|
||
}
|
||
// Allow only the sizes within tolerance or if it's the lowest bitrate
|
||
return isSizeWithinTolerance({ width: value.width, height: value.height }, context.viewportInfo) || value.bitrate === context.lowestBitrate;
|
||
},
|
||
});
|
||
}
|
||
function newHDCPFilter() {
|
||
return new SimpleListFilter({
|
||
name: 'HDCP Filter',
|
||
priority: 2,
|
||
filterFn: (value, index, options) => {
|
||
return !options || !isHdcpLevel(options.maxHdcpLevel) || hdcpLevelToInt(value.hdcpLevel) < hdcpLevelToInt(options.maxHdcpLevel);
|
||
},
|
||
});
|
||
}
|
||
class VariantMediaOptionListQuery extends MediaOptionListQuery {
|
||
constructor(store, itemId) {
|
||
super(store, itemId, MediaOptionType.Variant);
|
||
}
|
||
static makeFilters() {
|
||
// prettier-ignore
|
||
return sortFilters(makeCommonFilters().concat([
|
||
newHDRFilter(),
|
||
newViewportFilter(),
|
||
newHDCPFilter(),
|
||
// TODO: Fix rdar://81171922 and re-enable content steering
|
||
// newPathwayFilter(),
|
||
]));
|
||
}
|
||
_initFilters() {
|
||
return VariantMediaOptionListQuery.kAllowFilters;
|
||
}
|
||
get preferredHost() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.mediaOptionListInfo) === null || _a === void 0 ? void 0 : _a.preferredHost) !== null && _b !== void 0 ? _b : null;
|
||
}
|
||
get preferredHost$() {
|
||
return this.selectEntity(this.itemId, (entity) => {
|
||
var _a;
|
||
return (_a = entity === null || entity === void 0 ? void 0 : entity.mediaOptionListTuple[MediaOptionType.Variant].preferredHost) !== null && _a !== void 0 ? _a : null;
|
||
});
|
||
}
|
||
get mediaOptionListInfo() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.getEntity(this.itemId)) === null || _a === void 0 ? void 0 : _a.mediaOptionListTuple[MediaOptionType.Variant]) !== null && _b !== void 0 ? _b : null;
|
||
}
|
||
get mediaOptionListInfo$() {
|
||
return this.selectEntity(this.itemId, (entity) => { var _a; return (_a = entity === null || entity === void 0 ? void 0 : entity.mediaOptionListTuple) === null || _a === void 0 ? void 0 : _a[MediaOptionType.Variant]; }).pipe(filterNullOrUndefined());
|
||
}
|
||
get hdrMode$() {
|
||
return this.mediaOptionListInfo$.pipe(map((info) => {
|
||
return info.preferHDR === true && info.hasHdrLevels;
|
||
}), distinctUntilChanged());
|
||
}
|
||
get maxHdcpLevel$() {
|
||
return this.selectEntity(this.itemId, (entity) => {
|
||
var _a;
|
||
const info = (_a = entity === null || entity === void 0 ? void 0 : entity.mediaOptionListTuple) === null || _a === void 0 ? void 0 : _a[MediaOptionType.Variant];
|
||
return info === null || info === void 0 ? void 0 : info.maxHdcpLevel;
|
||
}).pipe(distinctUntilChanged());
|
||
}
|
||
// excludingOptions is useful for skipping visited variants in successive getFallbackVariant
|
||
listFallbackVariants(fromId, sdrOnly, shouldSwitchHosts, shouldDownswitch, excludingOptions = undefined) {
|
||
var _a;
|
||
const mediaOptionListInfo = this.mediaOptionListInfo;
|
||
const fromVariant = (_a = this.mediaOptionList) === null || _a === void 0 ? void 0 : _a.find((x) => x.mediaOptionId === fromId);
|
||
if (!fromVariant || !mediaOptionListInfo) {
|
||
return null;
|
||
}
|
||
const filteredList = this.makeFilteredListFromVariant(fromVariant, sdrOnly, excludingOptions);
|
||
if (!filteredList) {
|
||
return null;
|
||
}
|
||
const fromHost = getHostName(fromVariant.url);
|
||
const hasScore = mediaOptionListInfo.hasScore;
|
||
return VariantMediaOptionListQuery._listFallbackVariants(filteredList, fromVariant, fromHost, hasScore, shouldSwitchHosts, shouldDownswitch, excludingOptions);
|
||
}
|
||
// excludingOptions is useful for skipping visited variants in successive getFallbackVariant
|
||
getFallbackVariant(fromId, sdrOnly, shouldSwitchHosts, excludingOptions = undefined) {
|
||
var _a;
|
||
const mediaOptionListInfo = this.mediaOptionListInfo;
|
||
const fromVariant = (_a = this.mediaOptionList) === null || _a === void 0 ? void 0 : _a.find((x) => x.mediaOptionId === fromId);
|
||
if (!fromVariant || !mediaOptionListInfo) {
|
||
return null;
|
||
}
|
||
const filteredList = this.makeFilteredListFromVariant(fromVariant, sdrOnly, excludingOptions);
|
||
if (!filteredList) {
|
||
return null;
|
||
}
|
||
const fromHost = getHostName(fromVariant.url);
|
||
const hasScore = mediaOptionListInfo.hasScore;
|
||
return VariantMediaOptionListQuery._getFallbackVariant(filteredList, fromVariant, fromHost, hasScore, shouldSwitchHosts);
|
||
}
|
||
makeFilteredListFromVariant(fromVariant, sdrOnly, excludingOptions = undefined) {
|
||
let mediaOptionListInfo = this.mediaOptionListInfo;
|
||
if (!fromVariant || !this.mediaOptionList || !mediaOptionListInfo) {
|
||
return null;
|
||
}
|
||
mediaOptionListInfo = Object.assign(Object.assign({}, mediaOptionListInfo), { includeAllEligiblePathways: true });
|
||
const originalList = Array.from(this.mediaOptionList);
|
||
let filteredList = sdrOnly ? applyFilters(originalList, this.allowFilters, Object.assign(Object.assign({}, mediaOptionListInfo), { preferHDR: false, compatibleIds: null })) : this._getFilteredList(mediaOptionListInfo);
|
||
if (!filteredList) {
|
||
return null;
|
||
}
|
||
if (excludingOptions && excludingOptions.length > 0) {
|
||
filteredList = filteredList.filter((option) => !excludingOptions.includes(option.mediaOptionId));
|
||
}
|
||
return filteredList;
|
||
}
|
||
get hasIframes() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.mediaOptionListInfo) === null || _a === void 0 ? void 0 : _a.hasIframeLevels) !== null && _b !== void 0 ? _b : false;
|
||
}
|
||
/**
|
||
* @returns true if fromId is an HDR level and we can switch to a SDR level
|
||
*/
|
||
canSwitchToSDR(fromId, shouldSwitchHosts, shouldDownswitch = false) {
|
||
const mediaOptionListInfo = this.mediaOptionListInfo;
|
||
if (!this.mediaOptionList || !mediaOptionListInfo) {
|
||
return false;
|
||
}
|
||
const fromVariant = this.mediaOptionFromId(fromId);
|
||
if (!fromVariant) {
|
||
return false;
|
||
}
|
||
if (!isHDRLevel(fromVariant)) {
|
||
return false;
|
||
}
|
||
const fromHost = getHostName(fromVariant.url);
|
||
// Get filtered list but override hdr preference to false
|
||
const originalList = Array.from(this.mediaOptionList);
|
||
const filteredList = applyFilters(originalList, this.allowFilters, Object.assign(Object.assign({}, mediaOptionListInfo), { preferHDR: false, compatibleIds: null }));
|
||
const hasScore = mediaOptionListInfo.hasScore;
|
||
return VariantMediaOptionListQuery._getFallbackVariant(filteredList, fromVariant, fromHost, hasScore, shouldSwitchHosts, shouldDownswitch) != null;
|
||
}
|
||
// Generic helper function for finding a fallback given a filtered list and the failing variant
|
||
static _listFallbackVariants(filteredList, fromVariant, fromHost, hasScore, shouldSwitchHosts, shouldDownswitch = false, excludingOptions = null) {
|
||
// shouldDownswitch: On timeout we want to pick variant with bitrate lower than currently being downloaded
|
||
let needFromVariant = false;
|
||
const candidateList = filteredList.filter((candidate) => {
|
||
const validCandidate = candidate.iframes === fromVariant.iframes && (!shouldSwitchHosts || !hasMatchingHost(fromHost, candidate.url));
|
||
const lesserTier = shouldDownswitch ? candidate.bitrate < fromVariant.bitrate : candidate.bitrate <= fromVariant.bitrate;
|
||
const success = validCandidate && lesserTier;
|
||
if (fromVariant.mediaOptionId === candidate.mediaOptionId) {
|
||
needFromVariant = success; // if needed, add fromVariant to the top of candidateList later
|
||
return false;
|
||
}
|
||
return success;
|
||
});
|
||
const result = sortFallbackVariants(candidateList, fromVariant, hasScore);
|
||
if (needFromVariant && (!excludingOptions || !excludingOptions.includes(fromVariant.mediaOptionId))) {
|
||
result.unshift(fromVariant); // put fromVariant at the top if not excluded
|
||
}
|
||
return result;
|
||
}
|
||
// Generic helper function for finding a fallback given a filtered list and the failing variant
|
||
static _getFallbackVariant(filteredList, fromVariant, fromHost, hasScore, shouldSwitchHosts, shouldDownswitch = false) {
|
||
// Choose best option between these two
|
||
let newVariant = null;
|
||
// On timeout we want to pick variant with bitrate lower than currently being downloaded
|
||
if (shouldDownswitch) {
|
||
filteredList = filteredList.filter((option) => option.bitrate < fromVariant.bitrate);
|
||
}
|
||
const candidateList = filteredList.filter((candidate) => candidate.mediaOptionId !== fromVariant.mediaOptionId && candidate.iframes === fromVariant.iframes);
|
||
if (shouldSwitchHosts && fromHost != null) {
|
||
// Choose from different host
|
||
for (const candidate of candidateList) {
|
||
if (compareCandidate(fromVariant, hasScore, candidate, newVariant) === Rank.Better && !hasMatchingHost(fromHost, candidate.url)) {
|
||
newVariant = candidate;
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
// Choose best one from any host. Tie breaker favors current host
|
||
// If hosts are same between candidates, favor same audio group
|
||
for (const candidate of candidateList) {
|
||
const rank = compareCandidate(fromVariant, hasScore, candidate, newVariant);
|
||
if (rank === Rank.Better ||
|
||
(rank === Rank.Same && hasMatchingHost(fromHost, candidate.url) && (!hasMatchingHost(fromHost, newVariant.url) || candidate.audioGroupId === fromVariant.audioGroupId))) {
|
||
newVariant = candidate;
|
||
}
|
||
}
|
||
}
|
||
return newVariant;
|
||
}
|
||
// Get media option matching the input mediaOption groupID
|
||
getMatchingVariant(fromId, mediaOption) {
|
||
const fromVariant = this.mediaOptionFromId(fromId);
|
||
const fromHost = getHostName(fromVariant === null || fromVariant === void 0 ? void 0 : fromVariant.url);
|
||
const whichGroup = mediaOption.mediaOptionType === MediaOptionType.AltAudio ? 'audioGroupId' : 'subtitleGroupId';
|
||
let bestCandidate = null;
|
||
const hasScore = this.mediaOptionListInfo.hasScore;
|
||
for (const candidate of this.filteredMediaOptionList) {
|
||
if (candidate[whichGroup] !== mediaOption.groupId) {
|
||
continue;
|
||
}
|
||
if (!fromVariant) {
|
||
bestCandidate = candidate; // First mediaOption with matching groupId
|
||
break;
|
||
}
|
||
const rank = compareCandidate(fromVariant, hasScore, candidate, bestCandidate);
|
||
if (rank === Rank.Better ||
|
||
(rank === Rank.Same && bestCandidate.mediaOptionId !== fromVariant.mediaOptionId && (candidate.mediaOptionId === fromVariant.mediaOptionId || hasMatchingHost(fromHost, candidate.url)))) {
|
||
bestCandidate = candidate;
|
||
}
|
||
}
|
||
return bestCandidate;
|
||
}
|
||
get currentPathwayID() {
|
||
var _a;
|
||
return (_a = this.mediaOptionListInfo) === null || _a === void 0 ? void 0 : _a.currentPathwayID;
|
||
}
|
||
}
|
||
VariantMediaOptionListQuery.kAllowFilters = VariantMediaOptionListQuery.makeFilters();
|
||
|
||
/**
|
||
* Error handling policies. This file handles general error handling
|
||
*
|
||
* @copyright 2018 Apple Inc. All rights reserved.
|
||
*/
|
||
/**
|
||
* @brief Common error actions
|
||
*/
|
||
var NetworkErrorAction;
|
||
(function (NetworkErrorAction) {
|
||
NetworkErrorAction[NetworkErrorAction["DoNothing"] = 0] = "DoNothing";
|
||
NetworkErrorAction[NetworkErrorAction["SendEndCallback"] = 1] = "SendEndCallback";
|
||
NetworkErrorAction[NetworkErrorAction["SendAlternateToPenaltyBox"] = 2] = "SendAlternateToPenaltyBox";
|
||
NetworkErrorAction[NetworkErrorAction["RemoveAlternatePermanently"] = 3] = "RemoveAlternatePermanently";
|
||
NetworkErrorAction[NetworkErrorAction["InsertDiscontinuity"] = 4] = "InsertDiscontinuity";
|
||
NetworkErrorAction[NetworkErrorAction["RetryRequest"] = 5] = "RetryRequest";
|
||
})(NetworkErrorAction || (NetworkErrorAction = {}));
|
||
/**
|
||
* PenaltyBox or Remove only. Additional flags for determining what we should do
|
||
*/
|
||
var ErrorActionFlags;
|
||
(function (ErrorActionFlags) {
|
||
ErrorActionFlags[ErrorActionFlags["MoveAllAlternatesMatchingHost"] = 1] = "MoveAllAlternatesMatchingHost";
|
||
ErrorActionFlags[ErrorActionFlags["MoveAllAlternatesMatchingHDCP"] = 2] = "MoveAllAlternatesMatchingHDCP";
|
||
ErrorActionFlags[ErrorActionFlags["SwitchToSDR"] = 4] = "SwitchToSDR";
|
||
})(ErrorActionFlags || (ErrorActionFlags = {}));
|
||
/**
|
||
* Modify the error action if we have no fallbacks to switch to
|
||
*
|
||
* @param errorAction Original error action
|
||
* @param errorCode The error code
|
||
*/
|
||
function _modifyErrorActionIfLastValidVariantInternal(errorAction, errorCode) {
|
||
switch (errorAction) {
|
||
case NetworkErrorAction.SendAlternateToPenaltyBox:
|
||
errorAction = NetworkErrorAction.RetryRequest;
|
||
if (errorCode === 401 ||
|
||
errorCode === 403 ||
|
||
errorCode === 407 ||
|
||
errorCode === ErrorResponses.CorruptStream.code ||
|
||
errorCode === ErrorResponses.LivePlaylistUpdateError.code // We've already retried up to max at this point
|
||
) {
|
||
errorAction = NetworkErrorAction.SendEndCallback;
|
||
}
|
||
break;
|
||
case NetworkErrorAction.RemoveAlternatePermanently:
|
||
errorAction = NetworkErrorAction.SendEndCallback;
|
||
break;
|
||
}
|
||
return errorAction;
|
||
}
|
||
/**
|
||
* Common code to modify if it's the last variant of its kind in the controller
|
||
* @param originalAction Original action from getActionForCommonError
|
||
* @param isPrefetch Was a prefetch (or not needed immediately for playback)
|
||
* @param errorCode The error code
|
||
* @param mediaOptionId Option id
|
||
* @param mediaOptionType The type
|
||
* @param rootQuery Root playlist query
|
||
*/
|
||
function modifyErrorActionIfCurrentLevelIsLastValidLevel(originalAction, isPrefetch, errorCode, mediaOptionId, mediaOptionType, rootQuery, rootService, isTimeout = false) {
|
||
const mediaListQuery = rootQuery.mediaOptionListQueries[mediaOptionType];
|
||
const shouldSwitchHosts = (originalAction.errorActionFlags & ErrorActionFlags.MoveAllAlternatesMatchingHost) != 0;
|
||
const mediaOption = rootQuery.mediaOptionListQueries[mediaOptionType].mediaOptionFromId(mediaOptionId);
|
||
const fallbackMediaOptions = rootService.getFallbackMediaOptionTupleFromMediaOptionId(rootQuery, mediaOptionType, mediaOptionId, mediaOption.backingMediaOptionId, false, shouldSwitchHosts, isTimeout);
|
||
let { errorAction, errorActionFlags } = originalAction;
|
||
if (!rootQuery.isValidMediaOptionTuple(fallbackMediaOptions)) {
|
||
if (!isPrefetch) {
|
||
errorAction = _modifyErrorActionIfLastValidVariantInternal(errorAction, errorCode);
|
||
errorActionFlags = 0;
|
||
}
|
||
if (mediaListQuery instanceof VariantMediaOptionListQuery) {
|
||
const mediaOption = mediaListQuery.mediaOptionFromId(mediaOptionId);
|
||
if (mediaOption.iframes === true) {
|
||
errorAction = NetworkErrorAction.DoNothing;
|
||
errorActionFlags = 0;
|
||
rootService.logger.debug(`[modifyErrorAction] skip error handling for iframes; isPrefetch ${isPrefetch} fallback ${fallbackMediaOptions}`);
|
||
}
|
||
else if (!isPrefetch && rootService.canSwitchToSDR(rootQuery, mediaOptionId, shouldSwitchHosts, isTimeout)) {
|
||
errorAction = originalAction.errorAction;
|
||
errorActionFlags = ErrorActionFlags.SwitchToSDR;
|
||
rootService.logger.debug(`[modifyErrorAction] switchToSDR; isPrefetch ${isPrefetch} fallback ${fallbackMediaOptions} isTimeout ${isTimeout}`);
|
||
}
|
||
}
|
||
}
|
||
else if (hasMatchingHost(mediaListQuery.preferredHost, fallbackMediaOptions[mediaOptionType].url)) {
|
||
errorActionFlags &= ~ErrorActionFlags.MoveAllAlternatesMatchingHost;
|
||
rootService.logger.debug(`[modifyErrorAction] matched preferredHost ${mediaListQuery.preferredHost}; isPrefetch ${isPrefetch} fallback ${fallbackMediaOptions}`);
|
||
}
|
||
return { errorAction, errorActionFlags };
|
||
}
|
||
// Get default error action based on error codes
|
||
function _getActionForCommonNetworkError(errorCode) {
|
||
let errorAction;
|
||
let errorActionFlags = 0;
|
||
switch (errorCode) {
|
||
case 0:
|
||
// 0 is sometimes returned as a CORS error or a "muted" error when "--disable-web-security --ignore-certificate-errors" are used in Chrome.
|
||
errorAction = NetworkErrorAction.SendAlternateToPenaltyBox;
|
||
errorActionFlags = ErrorActionFlags.MoveAllAlternatesMatchingHost;
|
||
break;
|
||
case 410:
|
||
errorAction = NetworkErrorAction.RemoveAlternatePermanently;
|
||
break;
|
||
case 500: // Internal Server error
|
||
case 502: // Bad gateway
|
||
case 503: // Service unavailable
|
||
case 504: // Gateway timeout
|
||
case 404: // NotFound
|
||
case 409: // Conflict
|
||
case 401: // Unauthorized
|
||
case 403: // Forbidden
|
||
case 407: // Proxy error
|
||
case ErrorResponses.LivePlaylistUpdateError.code: // -12888
|
||
case ErrorResponses.PlaylistNotReceived.code: // -12884
|
||
default:
|
||
errorAction = NetworkErrorAction.SendAlternateToPenaltyBox;
|
||
errorActionFlags = 0;
|
||
break;
|
||
}
|
||
return { errorAction, errorActionFlags };
|
||
}
|
||
/**
|
||
* Modify the error action if we think we're going to take too long....
|
||
*
|
||
* @param error The error
|
||
* @param originalAction The original action
|
||
* @param isPrefetch true if this is not for playback but optimistic load
|
||
* @param maxTimeouts The maximum number of consecutive timeouts allowed
|
||
* @param rootQuery The root playlist query
|
||
* @return modified error action
|
||
*/
|
||
function _modifyActionOnTimeout(error, originalAction, isPrefetch, maxTimeouts, rootQuery) {
|
||
var _a, _b;
|
||
let { errorAction, errorActionFlags } = originalAction;
|
||
if (error.isTimeout) {
|
||
const { mediaOptionType } = error;
|
||
const consecutiveTimeouts = (_b = (_a = rootQuery.getErrorInfoByType(mediaOptionType)) === null || _a === void 0 ? void 0 : _a.timeouts['load']) !== null && _b !== void 0 ? _b : 0;
|
||
if (!isPrefetch && consecutiveTimeouts >= maxTimeouts) {
|
||
// this.logger.warn(`Hit max consecutive penalty due to timeout count: ${error.details}`);
|
||
errorAction = NetworkErrorAction.DoNothing; // don't treat timeouts as fatal, if the NW conditions don't improve, low buffer will lead to end playback
|
||
errorActionFlags = 0;
|
||
}
|
||
}
|
||
return { errorAction, errorActionFlags };
|
||
}
|
||
/**
|
||
* Get the policy for a manifest load error
|
||
* @param error The error
|
||
*/
|
||
function getActionForManifestError(error) {
|
||
let { errorAction, errorActionFlags } = _getActionForCommonNetworkError(error.response.code);
|
||
errorAction = _modifyErrorActionIfLastValidVariantInternal(errorAction, error.response.code);
|
||
errorActionFlags = 0; // cannot default to a backup CDN for manifest
|
||
// this.logger.info(`Manifest error ${errorAction}`);
|
||
return { errorAction, errorActionFlags };
|
||
}
|
||
/**
|
||
* Get the policy for a playlist load error
|
||
* @param error The error
|
||
* @param isPrefetch true if this is not critical for playback. For instance i-frame
|
||
* @param maxTimeouts The maximum number of consecutive timeouts allowed
|
||
* @param rootQuery The root playlist query
|
||
* @returns The action to perform
|
||
*/
|
||
function getActionForPlaylistOrFragError(error, isPrefetch, maxTimeouts, rootQuery, rootService) {
|
||
const errorCode = error.response.code;
|
||
let result = _getActionForCommonNetworkError(errorCode);
|
||
const { mediaOptionId, mediaOptionType } = error;
|
||
if (isPrefetch) {
|
||
// Do not modify on last level in prefetch mode
|
||
result.errorActionFlags &= ~ErrorActionFlags.MoveAllAlternatesMatchingHost;
|
||
}
|
||
else {
|
||
result = _modifyActionOnTimeout(error, result, isPrefetch, maxTimeouts, rootQuery);
|
||
}
|
||
result = modifyErrorActionIfCurrentLevelIsLastValidLevel(result, isPrefetch, errorCode, mediaOptionId, mediaOptionType, rootQuery, rootService, error.isTimeout);
|
||
// rootService.logger.info(`[${error.type}] Got playlist error ${errorCode} ${error.message} ${JSON.stringify(result)}`);
|
||
return result;
|
||
}
|
||
/**
|
||
* Handle a network error due to crypt key failure
|
||
* @param error KeyError
|
||
* @param mediaOptionId One of the media options associated with this error
|
||
* @param mediaOptionType The media option type associated with the media option id
|
||
* @param rootQuery The root query to use
|
||
*/
|
||
function getActionForCryptKeyNetworkError(error, mediaOptionId, mediaOptionType, rootQuery, rootService) {
|
||
let action = {
|
||
errorAction: NetworkErrorAction.SendAlternateToPenaltyBox,
|
||
errorActionFlags: 0,
|
||
};
|
||
if (error instanceof KeyRequestTimeoutError) {
|
||
action.errorAction = NetworkErrorAction.SendAlternateToPenaltyBox;
|
||
}
|
||
else {
|
||
const isOkToRetry = error.isOkToRetry;
|
||
const isHDCPError = error.keyErrorReason === KeyRequestErrorReason.OutputRestricted;
|
||
if (isHDCPError) {
|
||
action.errorAction = NetworkErrorAction.RemoveAlternatePermanently;
|
||
action.errorActionFlags |= ErrorActionFlags.MoveAllAlternatesMatchingHDCP;
|
||
}
|
||
else if (!isOkToRetry) {
|
||
action.errorAction = NetworkErrorAction.RemoveAlternatePermanently;
|
||
}
|
||
else {
|
||
action = _getActionForCommonNetworkError(error.code);
|
||
}
|
||
}
|
||
// Don't force host switch for key errors
|
||
action.errorActionFlags &= ~ErrorActionFlags.MoveAllAlternatesMatchingHost;
|
||
const enabledOptionId = rootQuery.enabledMediaOptionIdByType(mediaOptionType);
|
||
if (mediaOptionId === enabledOptionId) {
|
||
return modifyErrorActionIfCurrentLevelIsLastValidLevel(action, false, error.code, enabledOptionId, mediaOptionType, rootQuery, rootService, error.isTimeout);
|
||
}
|
||
else {
|
||
return action;
|
||
}
|
||
}
|
||
/**
|
||
* Common error action handling. use if error can't be handled automatically by handleErrorCommon.
|
||
* Note that this will modify the store multiple times so it should be wrapped in a transaction
|
||
*
|
||
* @param action Error action to handle
|
||
* @param rootQuery The root playlist query
|
||
* @param rootService The root playlist service
|
||
* @param mediaOptionType The media option type that the error originates from. Required for penalty box
|
||
* @param mediaOptionId The media option id that the error originates from. Required for penalty box
|
||
* @returns true if error was handled (we did something in reaction to it)
|
||
*/
|
||
function handleErrorAction(action, error, rootQuery, rootService, mediaOptionType, mediaOptionId, isTimeOut) {
|
||
const { errorAction, errorActionFlags } = action;
|
||
let errorHandled = true;
|
||
switch (errorAction) {
|
||
case NetworkErrorAction.RemoveAlternatePermanently:
|
||
case NetworkErrorAction.SendAlternateToPenaltyBox: {
|
||
if (mediaOptionType == null || mediaOptionId == null) {
|
||
error.handled = false;
|
||
return false;
|
||
}
|
||
const itemId = rootQuery.itemId;
|
||
let failedMediaOptionId = mediaOptionId;
|
||
let failedMediaOption = rootQuery.mediaOptionListQueries[mediaOptionType].mediaOptionFromId(mediaOptionId);
|
||
if (failedMediaOption.backingMediaOptionId) {
|
||
failedMediaOptionId = failedMediaOption.backingMediaOptionId;
|
||
failedMediaOption = rootQuery.mediaOptionListQueries[mediaOptionType].mediaOptionFromId(failedMediaOptionId);
|
||
}
|
||
const shouldSwitchHosts = (errorActionFlags & ErrorActionFlags.MoveAllAlternatesMatchingHost) != 0;
|
||
const shouldMoveMatchingHDCP = (errorActionFlags & ErrorActionFlags.MoveAllAlternatesMatchingHDCP) != 0;
|
||
const shouldSwitchToSDR = (errorActionFlags & ErrorActionFlags.SwitchToSDR) != 0;
|
||
const shouldRemove = errorAction === NetworkErrorAction.RemoveAlternatePermanently;
|
||
rootService.logger.debug(`[handleErrorAction] mediaOptionId ${mediaOptionId} shouldSwitchHosts ${shouldSwitchHosts} shouldMoveMatchingHDCP ${shouldMoveMatchingHDCP} shouldSwitchToSDR ${shouldSwitchToSDR} shouldRemove ${shouldRemove} fatal ${error.fatal}`);
|
||
if (shouldMoveMatchingHDCP && 'hdcpLevel' in failedMediaOption) {
|
||
const maxHdcpLevel = failedMediaOption.hdcpLevel;
|
||
rootService.setMaxHdcpLevel(itemId, maxHdcpLevel);
|
||
}
|
||
if (shouldSwitchToSDR) {
|
||
rootService.switchToSDROnly(itemId);
|
||
}
|
||
if (shouldSwitchHosts) {
|
||
const hostName = getHostName(failedMediaOption.url);
|
||
rootService.moveAllWithMatchingHosts(itemId, mediaOptionType, hostName, shouldRemove);
|
||
}
|
||
else {
|
||
if (shouldRemove) {
|
||
rootService.removePermanently(itemId, mediaOptionType, failedMediaOptionId);
|
||
}
|
||
else {
|
||
rootService.addToPenaltyBox(itemId, mediaOptionType, failedMediaOptionId);
|
||
}
|
||
}
|
||
// Switch enabled option only if we're the enabled option
|
||
const currentMediaOptionId = rootQuery.enabledMediaOptionIdByType(mediaOptionType);
|
||
if (currentMediaOptionId === mediaOptionId) {
|
||
let newOptions = [NoMediaOption, NoMediaOption, NoMediaOption];
|
||
newOptions = rootService.getFallbackMediaOptionTupleFromMediaOptionId(rootQuery, mediaOptionType, mediaOptionId, null, false, shouldSwitchHosts, isTimeOut);
|
||
if (rootQuery.isValidMediaOptionTuple(newOptions)) {
|
||
rootService.setPreferredHost(itemId, getHostName(newOptions[MediaOptionType.Variant].url));
|
||
}
|
||
else {
|
||
error.fatal = true; // Couldn't find matching variant
|
||
}
|
||
rootService.logger.debug(`[handleErrorAction] ${error.fatal ? 'rejected' : 'picked'} ${JSON.stringify(newOptions)}`);
|
||
if (error.fatal) {
|
||
newOptions = [NoMediaOption, NoMediaOption, NoMediaOption];
|
||
}
|
||
rootService.setNextMediaOptions(rootQuery.itemId, newOptions);
|
||
}
|
||
break;
|
||
}
|
||
case NetworkErrorAction.SendEndCallback:
|
||
error.fatal = true;
|
||
break;
|
||
case NetworkErrorAction.RetryRequest: // Retry should be handled outside of this using retryWhen
|
||
case NetworkErrorAction.DoNothing:
|
||
default:
|
||
errorHandled = false;
|
||
break;
|
||
}
|
||
error.handled = errorHandled;
|
||
return errorHandled;
|
||
}
|
||
function logError(logger, error, action, retryCount, nextRetry) {
|
||
var _a, _b;
|
||
let requestID;
|
||
if (error instanceof ManifestNetworkError) {
|
||
requestID = 'manifest';
|
||
}
|
||
else if (error instanceof PlaylistNetworkError || error instanceof FragmentNetworkError) {
|
||
requestID = `${MediaOptionNames[error.mediaOptionType]}:${error.mediaOptionId}`;
|
||
}
|
||
else if (error instanceof KeyRequestError || error instanceof KeyRequestTimeoutError) {
|
||
requestID = `key:${redactUrl(error.keyuri)}`;
|
||
}
|
||
const data = {
|
||
requestID,
|
||
type: error.type,
|
||
details: error.details,
|
||
fatal: error.fatal,
|
||
code: (_b = (_a = error.response) === null || _a === void 0 ? void 0 : _a.code) !== null && _b !== void 0 ? _b : NaN,
|
||
errorAction: action === null || action === void 0 ? void 0 : action.errorAction,
|
||
errorActionFlags: action === null || action === void 0 ? void 0 : action.errorActionFlags,
|
||
retryCount,
|
||
nextRetry,
|
||
};
|
||
logger.qe({
|
||
critical: true,
|
||
name: 'internalError',
|
||
data,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Common helpers / operators for handling errors
|
||
*/
|
||
function handleRetryAction(error, retryCount, retryConfig, logger) {
|
||
error.handled = true;
|
||
if (retryConfig && retryCount < retryConfig.maxNumRetry && isFiniteNumber(retryConfig.retryDelayMs)) {
|
||
let timeoutMs;
|
||
switch (retryConfig.backoff) {
|
||
case 'linear':
|
||
timeoutMs = (retryCount + 1) * retryConfig.retryDelayMs;
|
||
break;
|
||
default:
|
||
// exponential
|
||
timeoutMs = Math.pow(2, retryCount) * retryConfig.retryDelayMs;
|
||
break;
|
||
}
|
||
timeoutMs = Math.min(retryConfig.maxRetryDelayMs, timeoutMs);
|
||
logError(logger, error, { errorAction: NetworkErrorAction.RetryRequest, errorActionFlags: 0 }, retryCount, timeoutMs);
|
||
return timer(timeoutMs);
|
||
}
|
||
error.fatal = true;
|
||
logError(logger, error, { errorAction: NetworkErrorAction.SendEndCallback, errorActionFlags: 0 }, retryCount);
|
||
return throwError(error);
|
||
}
|
||
/**
|
||
* For use in retryWhen. This is a generic helper just in case we don't want to go
|
||
* through getActionAndHandleError.
|
||
*
|
||
* @returns timer() if we should retry, else throws if we should stop / abort the request
|
||
*/
|
||
function handleErrorWithRetry(error, retryCount, retryConfig, action, rootQuery, rootService, mediaOptionType, mediaOptionId, isTimeOut = false) {
|
||
if ((action === null || action === void 0 ? void 0 : action.errorAction) === NetworkErrorAction.RetryRequest) {
|
||
return handleRetryAction(error, retryCount, retryConfig, rootService.logger);
|
||
}
|
||
return handleErrorActionCommon(error, retryCount, action, rootQuery, rootService, mediaOptionType, mediaOptionId, isTimeOut);
|
||
}
|
||
function handleErrorActionCommon(error, retryCount, action, rootQuery, rootService, mediaOptionType, mediaOptionId, isTimeOut = false) {
|
||
const async$ = new AsyncSubject();
|
||
// Wrap handleErrorAction in transaction because it will update the store a few times
|
||
applyTransaction(() => {
|
||
if (action) {
|
||
logError(rootService.logger, error, action, retryCount);
|
||
handleErrorAction(action, error, rootQuery, rootService, mediaOptionType, mediaOptionId, isTimeOut);
|
||
}
|
||
async$.error(error);
|
||
});
|
||
return async$;
|
||
}
|
||
|
||
/**
|
||
* Handle errors coming from demuxers
|
||
*/
|
||
/**
|
||
* An operator for handling demux errors
|
||
*/
|
||
function addDemuxErrorHandlingPolicy(rootService, rootQuery, mediaOptionType, mediaOptionId) {
|
||
return (source$) => source$.pipe(catchError((error) => {
|
||
rootService.logger.error(`Got demux error ${error.message}`);
|
||
if (error instanceof FragParsingError || error instanceof RemuxAllocError) {
|
||
let errorAction = NetworkErrorAction.SendAlternateToPenaltyBox;
|
||
if (error.fatal) {
|
||
errorAction = NetworkErrorAction.SendEndCallback;
|
||
}
|
||
else if (error instanceof RemuxAllocError) {
|
||
errorAction = NetworkErrorAction.SendAlternateToPenaltyBox;
|
||
}
|
||
else {
|
||
errorAction = NetworkErrorAction.RemoveAlternatePermanently;
|
||
}
|
||
const action = {
|
||
errorAction,
|
||
errorActionFlags: 0,
|
||
};
|
||
// No retry allowed
|
||
return handleErrorActionCommon(error, 0, action, rootQuery, rootService, mediaOptionType, mediaOptionId);
|
||
}
|
||
throw error;
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* Handle errors that occur while fetching keys or from keysystem
|
||
*/
|
||
function updateKeyTimeouts(keyUri, increment, rootQuery, rootService, ksQuery) {
|
||
var _a, _b;
|
||
const mediaOptionIds = (_b = (_a = ksQuery.getKeyInfo(keyUri)) === null || _a === void 0 ? void 0 : _a.mediaOptionIds) !== null && _b !== void 0 ? _b : [];
|
||
for (const mediaOptionId of mediaOptionIds) {
|
||
for (const query of rootQuery.mediaOptionListQueries) {
|
||
if (query.mediaOptionFromId(mediaOptionId) != null) {
|
||
rootService.updateConsecutiveTimeouts(rootQuery.itemId, query.mediaOptionType, increment, 'key');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
function addKeyErrorHandlingPolicy(keyUri, loadConfig, rootQuery, rootService, ksQuery) {
|
||
return (source) => source.pipe(withTransaction(() => {
|
||
updateKeyTimeouts(keyUri, false, rootQuery, rootService, ksQuery);
|
||
}), retryWhen((errors) => errors.pipe(mergeMap((err, retryCount) => {
|
||
if (err instanceof KeyRequestTimeoutError || err instanceof KeyRequestError) {
|
||
return handleKeyError(err, retryCount, getRetryConfig(err, loadConfig), rootService, rootQuery, ksQuery);
|
||
}
|
||
throw err;
|
||
}))));
|
||
}
|
||
function handleKeyError(error, retryCount, retryConfig, rootService, rootQuery, ksQuery) {
|
||
const logger = rootService.logger;
|
||
const enabledOptions = rootQuery.enabledMediaOptionKeys;
|
||
const mediaOptions = [];
|
||
for (const mediaOptionId of error.mediaOptionIds) {
|
||
const isEnabled = enabledOptions.some((key) => key.mediaOptionId === mediaOptionId);
|
||
const query = rootQuery.mediaOptionListQueries.find((query) => query.mediaOptionFromId(mediaOptionId) != null);
|
||
if (!query) {
|
||
logger.warn(`Couldn't find query for ${mediaOptionId}`);
|
||
continue;
|
||
}
|
||
const mediaOptionType = query.mediaOptionType;
|
||
const obj = { mediaOptionId, mediaOptionType };
|
||
if (isEnabled) {
|
||
mediaOptions.push(obj); // Handle after all other options because of fallback handling
|
||
}
|
||
else {
|
||
mediaOptions.unshift(obj);
|
||
}
|
||
}
|
||
const async$ = new AsyncSubject();
|
||
applyTransaction(() => {
|
||
const isTimeout = error instanceof KeyRequestTimeoutError;
|
||
updateKeyTimeouts(error.keyuri, isTimeout, rootQuery, rootService, ksQuery);
|
||
let shouldRetry = false;
|
||
let action;
|
||
for (const { mediaOptionId, mediaOptionType } of mediaOptions) {
|
||
action = getActionForCryptKeyNetworkError(error, mediaOptionId, mediaOptionType, rootQuery, rootService);
|
||
logger.error(`[Keys] handleNetworkError uri=${redactUrl(error.keyuri)} mediaOptionId=${mediaOptionId} mediaOptionType=${mediaOptionType} action=${JSON.stringify(action)}`);
|
||
if (action.errorAction === NetworkErrorAction.RetryRequest) {
|
||
// This means there's no fallback!
|
||
shouldRetry = true;
|
||
}
|
||
handleErrorAction(action, error, rootQuery, rootService, mediaOptionType, mediaOptionId);
|
||
}
|
||
if (shouldRetry) {
|
||
async$.next();
|
||
async$.complete();
|
||
}
|
||
else {
|
||
logError(logger, error, action, retryCount); // Log final action
|
||
async$.error(error);
|
||
}
|
||
});
|
||
return async$.pipe(switchMap(() => {
|
||
return handleRetryAction(error, retryCount, retryConfig, rootService.logger);
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* Handle root playlist, media playlist and fragment load errors
|
||
*/
|
||
/**
|
||
* An operator for handling Playlist and Fragment load errors. It will either retry or abort the observable by throwing error
|
||
* Additional error handling may happen by putting something into penalty box
|
||
*/
|
||
function addLoadErrorHandlingPolicy(itemId, mediaOptionType, loadConfig, maxTimeouts, isPrefetch, rootQuery, rootPlaylistService, statsService) {
|
||
maxTimeouts = Math.max(0, maxTimeouts);
|
||
return (source) => source.pipe(
|
||
// Expected that the source will throw appropriate HlsError.
|
||
tap(() => {
|
||
if (mediaOptionType != null) {
|
||
rootPlaylistService.updateConsecutiveTimeouts(itemId, mediaOptionType, false, 'load');
|
||
}
|
||
}), retryWhen((errors) => errors.pipe(mergeMap((error, retryCount) => {
|
||
return getActionAndHandleError(error, retryCount, getRetryConfig(error, loadConfig), isPrefetch, maxTimeouts, rootQuery, rootPlaylistService, statsService);
|
||
}))));
|
||
}
|
||
/**
|
||
* Meant to be used within retryWhen for an Observable
|
||
* @param retryCount How many times we have retried this observable. 0 means this was the original request
|
||
* @param retryConfig Retry config to use
|
||
* @param isPrefetch true if this is a prefetch (not needed for playback)
|
||
* @param maxTimeouts Maximum number of consecutive timeouts allowed before failing
|
||
* @param rootQuery The root playlist query
|
||
* @param rootService The root playlist service
|
||
* @returns Retry timer if retrying, else throws error
|
||
*/
|
||
function getActionAndHandleError(error, retryCount, retryConfig, isPrefetch, maxTimeouts, rootQuery, rootService, statsService) {
|
||
var _a;
|
||
if (!(error instanceof HlsError)) {
|
||
return throwError(error);
|
||
}
|
||
let mediaOptionId;
|
||
let mediaOptionType;
|
||
let action;
|
||
let isTimeout = false;
|
||
if (error instanceof ManifestNetworkError) {
|
||
action = getActionForManifestError(error);
|
||
}
|
||
else if (error instanceof PlaylistNetworkError || error instanceof FragmentNetworkError) {
|
||
({ mediaOptionType, mediaOptionId, isTimeout } = error);
|
||
const mediaOption = (_a = rootQuery.mediaOptionListQueries[mediaOptionType]) === null || _a === void 0 ? void 0 : _a.mediaOptionFromId(mediaOptionId);
|
||
if (!isPrefetch && error.isTimeout && mediaOption != null && !('iframes' in mediaOption && mediaOption.iframes === true)) {
|
||
rootService.updateConsecutiveTimeouts(rootQuery.itemId, error.mediaOptionType, true, 'load');
|
||
// record only for fragments
|
||
if (error instanceof FragmentNetworkError && error.stats) {
|
||
const now = performance.now();
|
||
statsService.setBandwidthSample(Object.assign(Object.assign({}, error.stats), { tfirst: error.stats.tfirst || now, tload: error.stats.tload || now, complete: true, mediaOptionType: mediaOptionType }));
|
||
}
|
||
}
|
||
action = getActionForPlaylistOrFragError(error, isPrefetch, maxTimeouts, rootQuery, rootService);
|
||
}
|
||
// TODO: add more error types
|
||
return handleErrorWithRetry(error, retryCount, retryConfig, action, rootQuery, rootService, mediaOptionType, mediaOptionId, isTimeout);
|
||
}
|
||
|
||
class HlsQuery extends QueryEntity {
|
||
constructor(store) {
|
||
super(store);
|
||
}
|
||
get currentConfig() {
|
||
var _a;
|
||
return (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.config;
|
||
}
|
||
get extendMaxTTFB() {
|
||
var _a;
|
||
return (_a = this.getActive()) === null || _a === void 0 ? void 0 : _a.extendMaxTTFB;
|
||
}
|
||
get config$() {
|
||
return this.selectActive((entity) => entity === null || entity === void 0 ? void 0 : entity.config);
|
||
}
|
||
get userSeek$() {
|
||
return this.selectActive((entity) => entity === null || entity === void 0 ? void 0 : entity.userSeek);
|
||
}
|
||
}
|
||
|
||
// rdar://84941644 ([HLS JS 2.1a beta] [DoW] The plugin for 'MapSet' has not been loaded into Immer)
|
||
// hls.js uses Set and/or Map in highestVideoCodecs and various entities.
|
||
// dev-app occasionally throws "The plugin for 'MapSet' has not been loaded into Immer." error during error handling (e.g., switchToSDROnly).
|
||
// need to call enableMapSet() after and including immer 6.
|
||
// reference: https://immerjs.github.io/immer/map-set/
|
||
enableMapSet_1();
|
||
/**
|
||
* @brief Store for keeping track of things that are associated with Hls lifetime
|
||
*/
|
||
class HlsStore extends EntityStore {
|
||
constructor() {
|
||
super({}, { name: 'hls-store', producerFn: produce_1 });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief Service for HlsStore
|
||
*/
|
||
class HlsService {
|
||
constructor(store) {
|
||
this.store = store;
|
||
}
|
||
getQuery() {
|
||
return new HlsQuery(this.store);
|
||
}
|
||
/**
|
||
* Add and set active HlsEntity
|
||
*/
|
||
setHlsEntity(entity) {
|
||
const id = entity.id;
|
||
logAction(`hls.set.entity ${id}`);
|
||
applyTransaction(() => {
|
||
this.store.add(deepCpy(entity));
|
||
this.store.setActive(id);
|
||
});
|
||
}
|
||
removeEntity(id) {
|
||
logAction(`hls.remove ${id}`);
|
||
this.store.remove(id);
|
||
}
|
||
// legacy hack to get airplay to work
|
||
setStartTime(startTimeSec) {
|
||
this.store.updateActive((active) => {
|
||
active.config.startPosition = startTimeSec;
|
||
});
|
||
}
|
||
// If we got a user seek
|
||
setUserSeek(position) {
|
||
this.store.updateActive((active) => {
|
||
active.userSeek = position;
|
||
});
|
||
}
|
||
setExtendMaxTTFB(value) {
|
||
this.store.updateActive((active) => {
|
||
active.extendMaxTTFB = value;
|
||
});
|
||
}
|
||
}
|
||
let service;
|
||
function globalHlsService() {
|
||
if (!service) {
|
||
service = new HlsService(new HlsStore());
|
||
}
|
||
return service;
|
||
}
|
||
function createHlsQuery() {
|
||
return globalHlsService().getQuery();
|
||
}
|
||
/**
|
||
* Config for active hls entity
|
||
*/
|
||
function getCurrentConfig() {
|
||
return globalHlsService().getQuery().currentConfig;
|
||
}
|
||
|
||
const detailFields = ['mediaOptionId', 'startSN', 'endSN', 'ptsKnown'];
|
||
/**
|
||
* Used for live, merge the two playlists together
|
||
* @param oldDetails
|
||
* @param newDetails Newly parsed variant info from PlaylistParser. Assumption is that fragments[0].start === 0
|
||
*/
|
||
function mergeDetails(oldDetails, newDetails, logger) {
|
||
var _a;
|
||
// no merging required in VOD or if not matching in iframe mode
|
||
if (oldDetails.type === 'VOD' || newDetails.type === 'VOD' || oldDetails.iframesOnly !== newDetails.iframesOnly) {
|
||
return;
|
||
}
|
||
logger.info(`[live] merging ${JSON.stringify(oldDetails, detailFields)}+${JSON.stringify(newDetails, detailFields)}`);
|
||
const matchingMediaOption = oldDetails.mediaOptionId === newDetails.mediaOptionId;
|
||
const startIdx = Math.max(oldDetails.startSN, newDetails.startSN) - newDetails.startSN; // start of overlap inside newfragments
|
||
const endIdx = Math.min(oldDetails.endSN, newDetails.endSN) - newDetails.startSN; // end of overlap inside newfragments
|
||
const idxDelta = newDetails.startSN - oldDetails.startSN;
|
||
const oldfragments = oldDetails.fragments;
|
||
const newfragments = newDetails.fragments;
|
||
let discoSeqNumOffset = 0; // oldCC - newCC
|
||
for (let i = startIdx; i <= endIdx; ++i) {
|
||
// Expect this to only be O(1) usually...
|
||
if (oldfragments[idxDelta + i] && newfragments[i]) {
|
||
discoSeqNumOffset = oldfragments[idxDelta + i].discoSeqNum - newfragments[i].discoSeqNum;
|
||
// non zero value means the content is in violation of authoring guidelines
|
||
logger.debug(`[live] merging details for mediaOptionId: ${newDetails.mediaOptionId} using discoSeqNumOffset: ${discoSeqNumOffset}`);
|
||
break;
|
||
}
|
||
}
|
||
// loop through overlapping SN and update startPTS , cc, and duration if any found
|
||
const mergedInitSegments = {};
|
||
let lastPtsFrag = null;
|
||
for (let i = 0; i < newfragments.length; i++) {
|
||
// logger.info(`[live] merging details for media sequence number: ${newfragments[i].mediaSeqNum}`);
|
||
const oldFrag = oldfragments[idxDelta + i];
|
||
const newFrag = newfragments[i];
|
||
if (discoSeqNumOffset) {
|
||
// This block is to handle publisher side issue, where the
|
||
// authoring guideline below is not handled: 8.17. If
|
||
// live/linear content will ever contain an EXT-X-DISCONTINUITY
|
||
// tag, the EXT-X-DISCONTINUITY-SEQUENCE tag MUST always be
|
||
// present.
|
||
const discoSeqNum = newFrag.discoSeqNum + discoSeqNumOffset;
|
||
if (newDetails.initSegments[newFrag.discoSeqNum]) {
|
||
// logger.info(`[live] merging details offseting discoSeqNum from ${newFrag.discoSeqNum} to ${discoSeqNum}`);
|
||
newDetails.initSegments[newFrag.discoSeqNum].discoSeqNum = discoSeqNum; // update the frag details
|
||
mergedInitSegments[discoSeqNum] = newDetails.initSegments[newFrag.discoSeqNum]; // update the record
|
||
delete newDetails.initSegments[newFrag.discoSeqNum]; // delete from the old record.
|
||
}
|
||
newFrag.discoSeqNum = discoSeqNum;
|
||
}
|
||
if (matchingMediaOption && newFrag.mediaSeqNum === (oldFrag === null || oldFrag === void 0 ? void 0 : oldFrag.mediaSeqNum) && oldFrag.startPts != null) {
|
||
// logger.info(`[live] merging timestamps for media sequence number: ${newFrag.mediaSeqNum} using start PTS: ${oldFrag.startPTS}`);
|
||
newFrag.start = oldFrag.start;
|
||
newFrag.duration = oldFrag.duration;
|
||
newFrag.startDtsTs = oldFrag.startDtsTs;
|
||
newFrag.endDtsTs = oldFrag.endDtsTs;
|
||
newFrag.startPts = oldFrag.startPts;
|
||
newFrag.endPts = oldFrag.endPts;
|
||
lastPtsFrag = newFrag;
|
||
}
|
||
}
|
||
if (Object.keys(mergedInitSegments).length) {
|
||
newDetails.initSegments = mergedInitSegments;
|
||
}
|
||
if (lastPtsFrag) {
|
||
// Force update entire list to ensure correct sliding window
|
||
updateFragPTSDTS(newDetails, lastPtsFrag, lastPtsFrag.start, undefined, false, true);
|
||
}
|
||
else {
|
||
// ensure that delta is within oldfragments range
|
||
// also adjust sliding in case delta is 0 (we could have old=[50-60] and new=old=[50-61])
|
||
// in that case we also need to adjust start offset of all fragments
|
||
if (idxDelta >= 0 && idxDelta < oldfragments.length) {
|
||
// adjust start by sliding offset
|
||
const sliding = oldfragments[idxDelta].start;
|
||
logger.info(`[live] merging details using sliding ${sliding}`);
|
||
const mergedFragments = newDetails.fragments;
|
||
for (let i = 0; i < newfragments.length; i++) {
|
||
mergedFragments[i].start += sliding;
|
||
}
|
||
}
|
||
}
|
||
// If overlapping use oldDetails.PTSKnown
|
||
newDetails.ptsKnown = newDetails.ptsKnown || (matchingMediaOption && oldDetails.ptsKnown === true && oldDetails.endSN >= newDetails.startSN);
|
||
updateDateToMediaTimeMap(newDetails);
|
||
const startPos = (_a = newDetails.fragments[0]) === null || _a === void 0 ? void 0 : _a.start;
|
||
const endPos = startPos + newDetails.totalduration;
|
||
logger.info(`[live] merged ${JSON.stringify(oldDetails, detailFields)}+${JSON.stringify(newDetails, detailFields)}=[${startPos === null || startPos === void 0 ? void 0 : startPos.toFixed(3)},${endPos === null || endPos === void 0 ? void 0 : endPos.toFixed(3)}]`);
|
||
}
|
||
|
||
function computeLivePosition(sliding, mediaOptionDetails, config, logger) {
|
||
let targetLatency = mediaOptionDetails.targetduration;
|
||
if (isFiniteNumber(config.liveSyncDuration)) {
|
||
targetLatency = config.liveSyncDuration;
|
||
}
|
||
else if (isFiniteNumber(config.liveSyncDurationCount)) {
|
||
targetLatency = config.liveSyncDurationCount * mediaOptionDetails.targetduration;
|
||
}
|
||
const result = sliding + Math.max(0, mediaOptionDetails.totalduration - targetLatency);
|
||
logger.info(`[live] computeLivePosition: ${toFixed(result, 3)}`);
|
||
return result;
|
||
}
|
||
function sanitizeLiveSeek(seekTo, details, config, logger) {
|
||
let santizedSeek = seekTo;
|
||
const liveWindowStart = details.fragments[0].start;
|
||
const liveWindowEnd = details.fragments[details.fragments.length - 1].start + details.fragments[details.fragments.length - 1].duration;
|
||
if (seekTo < liveWindowStart) {
|
||
santizedSeek = liveWindowStart;
|
||
}
|
||
else if (seekTo > liveWindowEnd) {
|
||
santizedSeek = computeLivePosition(0, details, config, logger);
|
||
}
|
||
if (seekTo < liveWindowStart || seekTo > liveWindowEnd) {
|
||
logger.warn(`[live] sanitizeLiveSeek seekTo:${toFixed(seekTo, 3)}, sanitizedSeek:${toFixed(santizedSeek, 3)}, liveWindowStart:${toFixed(liveWindowStart, 3)}, liveWindowEnd:${toFixed(liveWindowEnd, 3)}`);
|
||
}
|
||
return santizedSeek;
|
||
}
|
||
function getMinPlayablePosition(pos, details, lastUpdateMillis, maxBufferHole, mediaQuery) {
|
||
if (!details.ptsKnown) {
|
||
return 0;
|
||
}
|
||
const targetDuration = details.targetduration;
|
||
const liveWindowStart = details.fragments[0].start;
|
||
const playlistEstimate = { avgPlaylistLoadTimeMs: 0, avgPlaylistParseTimeMs: 0 };
|
||
const canPlayThrough = mediaQuery.canContinuePlaybackWithoutGap(details, lastUpdateMillis, playlistEstimate, maxBufferHole);
|
||
let minPosition = Math.max(0, pos - targetDuration);
|
||
if (pos < liveWindowStart && !canPlayThrough) {
|
||
minPosition = liveWindowStart;
|
||
}
|
||
return minPosition;
|
||
}
|
||
/**
|
||
* @returns An observable that will ensure playback stays within the live window on playlist refresh
|
||
* emits the seek position
|
||
*/
|
||
function ensurePlaybackWithinWindow(context) {
|
||
const { config, mediaSink, rootPlaylistQuery, mediaLibraryService } = context;
|
||
const logger = context.logger.child({ name: 'live' });
|
||
const mediaQuery = mediaSink.mediaQuery;
|
||
return rootPlaylistQuery.enabledMediaOptionByType$(MediaOptionType.Variant).pipe(filter(isEnabledMediaOption), switchMap((option) => {
|
||
const libQuery = mediaLibraryService.getQueryForOption(option);
|
||
return libQuery.mediaOptionDetailsEntity$.pipe(filter((entity) => { var _a; return ((_a = entity === null || entity === void 0 ? void 0 : entity.mediaOptionDetails) === null || _a === void 0 ? void 0 : _a.ptsKnown) && entity.mediaOptionDetails.liveOrEvent; }), distinctUntilChanged((a, b) => (a === null || a === void 0 ? void 0 : a.lastUpdateMillis) === (b === null || b === void 0 ? void 0 : b.lastUpdateMillis)));
|
||
}), map((entity) => {
|
||
const details = entity.mediaOptionDetails;
|
||
const pos = mediaQuery.currentTime;
|
||
const duration = mediaQuery.msDuration;
|
||
if (duration < entity.playlistDuration) {
|
||
logger.info(`msDuration < playlistDuration, updating ${duration}->${entity.playlistDuration}`);
|
||
mediaSink.msDuration = entity.playlistDuration;
|
||
}
|
||
else if (isFiniteNumber(mediaSink.msDuration)) {
|
||
// audio could have been behind video and stagnated, and missed out on last opportunity
|
||
// and video playlist could stagnate subsequently
|
||
// doing this will trigger the needData, giving a chance to survive low buffer stall
|
||
mediaSink.msDuration = mediaSink.msDuration + config.livePlaylistDurationNudge;
|
||
}
|
||
const minPosition = getMinPlayablePosition(pos, details, entity.lastUpdateMillis, config.maxBufferHole, mediaQuery);
|
||
let seekTo = NaN;
|
||
if (pos < minPosition) {
|
||
seekTo = computeLivePosition(details.fragments[0].start, details, config, logger);
|
||
logger.info(`${pos.toFixed(3)} too far behind window start:${minPosition} seek to live=${seekTo}`);
|
||
mediaSink.seekTo = seekTo;
|
||
}
|
||
return seekTo;
|
||
}));
|
||
}
|
||
|
||
/*
|
||
* deals with live/event refresh related aspects
|
||
*
|
||
*
|
||
*
|
||
*/
|
||
// in milliseconds
|
||
function getLiveRefreshInterval(mediaOptionDetails) {
|
||
return 1000 * (mediaOptionDetails.averagetargetduration ? mediaOptionDetails.averagetargetduration : mediaOptionDetails.targetduration);
|
||
}
|
||
function needToRefreshLevel(mediaOptionDetails, lastUpdateMillis) {
|
||
const refreshInterval = getLiveRefreshInterval(mediaOptionDetails);
|
||
const timeSinceLastLevelRequest = performance.now() - lastUpdateMillis;
|
||
return mediaOptionDetails.liveOrEvent && timeSinceLastLevelRequest >= refreshInterval;
|
||
}
|
||
/**
|
||
*
|
||
* @param curDetails
|
||
* @param newDetails
|
||
* @param logger
|
||
* @returns Whether there has been a change between curDetails && newDetails
|
||
*/
|
||
function mediaOptionDetailsHasChanged(curDetails, newDetails) {
|
||
return curDetails == null || newDetails.endSN !== curDetails.endSN || newDetails.liveOrEvent !== curDetails.liveOrEvent;
|
||
}
|
||
function getReloadTimer(mediaOptionDetailsEntity, slowDownTimeInMillis, logger) {
|
||
// In gapless mode it is possible the mediaOptionDetailsEntity is null right after an item eviction
|
||
if (!mediaOptionDetailsEntity) {
|
||
logger.info('mediaOptionDetailsEntity is null');
|
||
return EMPTY;
|
||
}
|
||
const { mediaOptionDetails, lastUpdateMillis, unchangedCount } = mediaOptionDetailsEntity;
|
||
if (!(mediaOptionDetails === null || mediaOptionDetails === void 0 ? void 0 : mediaOptionDetails.liveOrEvent)) {
|
||
logger.info(`End of event refresh for mediaOptionId: ${mediaOptionDetails.mediaOptionId}`);
|
||
return EMPTY;
|
||
}
|
||
if (needToRefreshLevel(mediaOptionDetails, lastUpdateMillis)) {
|
||
return timer(0).pipe(tap(() => logger.info(`[live] immediate live refresh for mediaOptionId: ${mediaOptionDetails.mediaOptionId}`)));
|
||
}
|
||
let reloadInterval = getLiveRefreshInterval(mediaOptionDetails);
|
||
if (unchangedCount > 0) {
|
||
reloadInterval /= 2;
|
||
reloadInterval = Math.max(reloadInterval, 5000); // Have a minumum 5 seconds gap
|
||
}
|
||
// decrement reloadInterval with level loading delay
|
||
const now = performance.now();
|
||
const sinceLastUpdate = now - lastUpdateMillis;
|
||
reloadInterval -= sinceLastUpdate;
|
||
reloadInterval += slowDownTimeInMillis;
|
||
// in any case, don't reload more than every second
|
||
reloadInterval = Math.max(1000, Math.round(reloadInterval));
|
||
return timer(reloadInterval).pipe(tap(() => logger.info(`[live] live refresh after ${reloadInterval} for mediaOptionId: ${mediaOptionDetails.mediaOptionId}`)));
|
||
}
|
||
/**
|
||
* @returns whether the live playlist is too far in the past. This means the sliding window end is too
|
||
* far behind the minimum playback position
|
||
*/
|
||
function livePlaylistExpired(details, lastUpdateMillis, maxBufferHole, mediaQuery) {
|
||
const minPosition = getMinPlayablePosition(mediaQuery.currentTime, details, lastUpdateMillis, maxBufferHole, mediaQuery);
|
||
const lastFrag = details.fragments[details.fragments.length - 1];
|
||
const windowEnd = (lastFrag === null || lastFrag === void 0 ? void 0 : lastFrag.start) + (lastFrag === null || lastFrag === void 0 ? void 0 : lastFrag.duration);
|
||
const expired = lastFrag != null && details.liveOrEvent && details.ptsKnown && windowEnd < minPosition;
|
||
return { expired, windowEnd, minPosition };
|
||
}
|
||
|
||
function findFragForCriteria(details, fn, startSN = NaN) {
|
||
const fragList = details.fragments;
|
||
const startIdx = startSN > details.startSN ? startSN - details.startSN : 0;
|
||
for (let idx = startIdx; idx < fragList.length; ++idx) {
|
||
const mediaFragment = fragList[idx];
|
||
const { start: timelineOffset } = mediaFragment;
|
||
if (fn(mediaFragment)) {
|
||
return { timelineOffset, mediaFragment };
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function discoSeqNumForTime(details, position) {
|
||
var _a;
|
||
const foundFrag = (_a = findFragForCriteria(details, (f) => {
|
||
const positiveDuration = f.duration > 0;
|
||
const fragEnd = f.start + f.duration;
|
||
const validPosition = fragEnd > position || (position - fragEnd < 1 && f.isLastFragment);
|
||
return positiveDuration && validPosition;
|
||
})) !== null && _a !== void 0 ? _a : null;
|
||
return foundFrag === null || foundFrag === void 0 ? void 0 : foundFrag.mediaFragment.discoSeqNum;
|
||
}
|
||
function validUnbufferedFragment(bufferedSeg, position, f) {
|
||
const positiveDuration = f.duration > 0;
|
||
const fragEnd = f.start + f.duration;
|
||
// edge case : <rdar://82696656> position is after last segment, but its very close, use the last fragment.
|
||
const validPosition = position == null || fragEnd > position || (position - fragEnd < 1 && f.isLastFragment);
|
||
const fragNotAlreadyBuffered = bufferedSeg.every((seg) => !fragEqual(seg.frag, f));
|
||
return positiveDuration && fragNotAlreadyBuffered && validPosition;
|
||
}
|
||
function findFragment(position, activeDiscoSeqNum, anchorMSN, details, bufferedSeg) {
|
||
var _a, _b, _c;
|
||
const firstUnbufferedFrag = (_a = findFragForCriteria(details, validUnbufferedFragment.bind(null, bufferedSeg, undefined), anchorMSN)) !== null && _a !== void 0 ? _a : null;
|
||
let foundFrag = null;
|
||
if (firstUnbufferedFrag) {
|
||
// Try to find fragment with valid position using previous result as hint
|
||
foundFrag = (_b = findFragForCriteria(details, validUnbufferedFragment.bind(null, bufferedSeg, position), firstUnbufferedFrag.mediaFragment.mediaSeqNum)) !== null && _b !== void 0 ? _b : null;
|
||
if (!foundFrag && details.liveOrEvent && !details.ptsKnown) {
|
||
// this is fall back for fairly rare case
|
||
foundFrag = firstUnbufferedFrag;
|
||
}
|
||
}
|
||
let nextDisco = NaN;
|
||
if (foundFrag != null && isFiniteNumber(activeDiscoSeqNum) && foundFrag.mediaFragment.discoSeqNum !== activeDiscoSeqNum) {
|
||
nextDisco = foundFrag.mediaFragment.discoSeqNum;
|
||
foundFrag = null;
|
||
}
|
||
// When switching variants and segments have the potential to drop frames, return mediaSeqNum -1
|
||
// to retieve the start of the GOP needed to buffer the start of the segment time range.
|
||
const canDropFrames = bufferedSeg.some((seg) => seg.frag.framesWithoutIDR > 0);
|
||
if (foundFrag && canDropFrames) {
|
||
const levelSwitch = bufferedSeg[bufferedSeg.length - 1].frag.mediaOptionId !== details.mediaOptionId;
|
||
if (levelSwitch) {
|
||
foundFrag = (_c = findFragForCriteria(details, (f) => f.mediaSeqNum === foundFrag.mediaFragment.mediaSeqNum - 1)) !== null && _c !== void 0 ? _c : foundFrag;
|
||
}
|
||
}
|
||
return { foundFrag, nextDisco };
|
||
}
|
||
function findIframeFragmentForPosition(position, details, audioDetails, rate, iframeMachine) {
|
||
const nextFragResult = iframeMachine.nextFragment(details.fragments, (audioDetails === null || audioDetails === void 0 ? void 0 : audioDetails.fragments) || [], rate, position);
|
||
if (!nextFragResult) {
|
||
return null;
|
||
}
|
||
const { frag, newMediaRootTime } = nextFragResult;
|
||
const foundFrag = { timelineOffset: frag.iframeMediaStart, mediaFragment: frag };
|
||
return { foundFrag, nextDisco: NaN, newMediaRootTime };
|
||
}
|
||
function calculatePlaylistEnd(details) {
|
||
const fragEnd = details.fragments[details.fragments.length - 1];
|
||
return fragEnd ? fragEnd.start + fragEnd.duration : 0;
|
||
}
|
||
|
||
/**
|
||
* @brief Query interface to the media library. Holds the details and init segments
|
||
*/
|
||
class MediaLibraryQuery extends QueryEntity {
|
||
constructor(mediaLibraryStore, mediaOption) {
|
||
super(mediaLibraryStore);
|
||
this.mediaOption = mediaOption;
|
||
}
|
||
get itemId() {
|
||
return this.mediaOption.itemId;
|
||
}
|
||
get mediaOptionId() {
|
||
return this.mediaOption.mediaOptionId;
|
||
}
|
||
get initSegmentEntities() {
|
||
var _a;
|
||
return (_a = this.mediaOptionDetailsEntity) === null || _a === void 0 ? void 0 : _a.initSegmentCacheEntities;
|
||
}
|
||
get mediaLibraryEntity() {
|
||
return this.getEntity(this.itemId);
|
||
}
|
||
get mediaOptionDetailsEntityRecord() {
|
||
var _a;
|
||
return (_a = this.mediaLibraryEntity) === null || _a === void 0 ? void 0 : _a.mediaOptionDetailsEntityRecord;
|
||
}
|
||
get mediaOptionDetailsEntity() {
|
||
if (!this.mediaOptionDetailsEntityRecord)
|
||
return null;
|
||
return this.mediaOptionDetailsEntityRecord[this.mediaOptionId];
|
||
}
|
||
get mediaOptionDetails() {
|
||
var _a;
|
||
return (_a = this.mediaOptionDetailsEntity) === null || _a === void 0 ? void 0 : _a.mediaOptionDetails;
|
||
}
|
||
get playlistDuration() {
|
||
var _a;
|
||
return (_a = this.mediaOptionDetailsEntity) === null || _a === void 0 ? void 0 : _a.playlistDuration;
|
||
}
|
||
get mediaOptionDetailsEntity$() {
|
||
const { itemId, mediaOptionId } = this;
|
||
return this.selectEntity(itemId, (libraryEntity) => {
|
||
if (libraryEntity === null || libraryEntity === void 0 ? void 0 : libraryEntity.mediaOptionDetailsEntityRecord) {
|
||
return libraryEntity === null || libraryEntity === void 0 ? void 0 : libraryEntity.mediaOptionDetailsEntityRecord[mediaOptionId];
|
||
}
|
||
});
|
||
}
|
||
get mediaOptionDetails$() {
|
||
return this.selectEntity(this.itemId, (entity) => { var _a; return (_a = entity === null || entity === void 0 ? void 0 : entity.mediaOptionDetailsEntityRecord[this.mediaOptionId]) === null || _a === void 0 ? void 0 : _a.mediaOptionDetails; }).pipe(filterNullOrUndefined());
|
||
}
|
||
get playlistDuration$() {
|
||
return this.mediaOptionDetailsEntity$.pipe(map((mediaOptionDetailsEntity) => mediaOptionDetailsEntity === null || mediaOptionDetailsEntity === void 0 ? void 0 : mediaOptionDetailsEntity.playlistDuration), filterNullOrUndefined(), distinctUntilChanged());
|
||
}
|
||
get live$() {
|
||
return this.mediaOptionDetails$.pipe(map((mediaOptionDetails) => mediaOptionDetails === null || mediaOptionDetails === void 0 ? void 0 : mediaOptionDetails.liveOrEvent), distinctUntilChanged());
|
||
}
|
||
}
|
||
|
||
class MediaLibraryStore extends EntityStore {
|
||
constructor() {
|
||
super({}, { name: 'media-library-store', idKey: 'itemId', producerFn: produce_1 });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief Service interface to the media library. Will fetch and catch media options details and init segments, and find
|
||
* the right fragments to load.
|
||
*
|
||
* The retrieve functions are rxjs pipeable operators:
|
||
* https://rxjs.dev/guide/v6/pipeable-operators
|
||
*/
|
||
class MediaLibraryService {
|
||
constructor(store) {
|
||
this.store = store;
|
||
}
|
||
getQuery() {
|
||
return new QueryEntity(this.store);
|
||
}
|
||
getQueryForOption(mediaOption) {
|
||
return new MediaLibraryQuery(this.store, mediaOption);
|
||
}
|
||
createMediaLibraryEntity(itemId) {
|
||
const libraryEntity = { itemId, mediaOptionDetailsEntityRecord: {} };
|
||
logAction(`library.entity.create: ${itemId}`);
|
||
this.store.add(libraryEntity);
|
||
}
|
||
setDetailsLoading(mediaOption) {
|
||
const { itemId, mediaOptionId } = mediaOption;
|
||
logAction(`library.details.loading: ${mediaOptionId}`);
|
||
this.store.update(itemId, ({ mediaOptionDetailsEntityRecord }) => {
|
||
if (!mediaOptionDetailsEntityRecord[mediaOptionId]) {
|
||
mediaOptionDetailsEntityRecord[mediaOptionId] = { initSegmentCacheEntities: {}, unchangedCount: 0 };
|
||
}
|
||
mediaOptionDetailsEntityRecord[mediaOptionId].detailsLoading = true;
|
||
});
|
||
}
|
||
archiveMediaOptionDetails(mediaOptionDetails, stats, changed) {
|
||
const { itemId, mediaOptionId } = mediaOptionDetails;
|
||
const lastUpdateMillis = performance.now();
|
||
const playlistDuration = calculatePlaylistEnd(mediaOptionDetails);
|
||
logAction(`library.details.loaded: ${mediaOptionId}`);
|
||
this.store.update(itemId, (libraryEntity) => {
|
||
const detailsEntity = libraryEntity.mediaOptionDetailsEntityRecord[mediaOptionId];
|
||
detailsEntity.detailsLoading = false;
|
||
detailsEntity.mediaOptionDetails = mediaOptionDetails;
|
||
detailsEntity.lastUpdateMillis = lastUpdateMillis;
|
||
if (changed) {
|
||
detailsEntity.unchangedCount = 0;
|
||
}
|
||
else {
|
||
++detailsEntity.unchangedCount;
|
||
}
|
||
detailsEntity.playlistDuration = playlistDuration;
|
||
detailsEntity.stats = stats;
|
||
libraryEntity.liveOrEvent = mediaOptionDetails.liveOrEvent;
|
||
});
|
||
}
|
||
setInitSegmentLoading(initSegment) {
|
||
const { itemId, mediaOptionId, discoSeqNum } = initSegment;
|
||
logAction(`library.initsegs.loading: ${mediaOptionId}/${discoSeqNum}`);
|
||
this.store.update(itemId, (libraryEntity) => {
|
||
libraryEntity.mediaOptionDetailsEntityRecord[mediaOptionId].initSegLoading = discoSeqNum;
|
||
});
|
||
}
|
||
archiveInitSegmentEntity(original, generated) {
|
||
const { itemId, mediaOptionId, discoSeqNum } = original;
|
||
logAction(`library.initseg.loaded: ${mediaOptionId}/${discoSeqNum}`);
|
||
this.store.update(itemId, ({ mediaOptionDetailsEntityRecord }) => {
|
||
const detailsEntity = mediaOptionDetailsEntityRecord[mediaOptionId];
|
||
detailsEntity.initSegmentCacheEntities[discoSeqNum] = [original, generated];
|
||
detailsEntity.initSegLoading = null;
|
||
});
|
||
}
|
||
updatePTSDTS(itemId, mediaOptionId, initPTSInfo, parsedFrag) {
|
||
var _a;
|
||
const origDetails = (_a = this.getQueryForOption({ itemId, mediaOptionId })) === null || _a === void 0 ? void 0 : _a.mediaOptionDetails;
|
||
if (!origDetails || !fragIsInDetails(origDetails, parsedFrag)) {
|
||
return;
|
||
}
|
||
// <rdar://90651041> immer read/write is slow, modify details cpy outside of store update
|
||
const { startDtsTs } = parsedFrag;
|
||
const { variantDTS, timelineOffset, iframeMode } = initPTSInfo;
|
||
const startOffset = diffSeconds(startDtsTs, variantDTS) + timelineOffset;
|
||
const newDetails = Object.assign({}, origDetails); // shallow copy
|
||
updateFragPTSDTS(newDetails, parsedFrag, startOffset, iframeMode, true);
|
||
updateDateToMediaTimeMap(newDetails);
|
||
const newPlaylistDuration = calculatePlaylistEnd(newDetails);
|
||
this.store.update(itemId, ({ mediaOptionDetailsEntityRecord }) => {
|
||
const entity = mediaOptionDetailsEntityRecord[mediaOptionId];
|
||
if (!(entity === null || entity === void 0 ? void 0 : entity.mediaOptionDetails)) {
|
||
return;
|
||
}
|
||
entity.mediaOptionDetails = newDetails;
|
||
entity.playlistDuration = newPlaylistDuration;
|
||
});
|
||
}
|
||
remove(ids) {
|
||
this.store.remove(ids);
|
||
}
|
||
clear() {
|
||
this.store.remove();
|
||
}
|
||
}
|
||
/**
|
||
* Global state store for the service
|
||
*/
|
||
let libraryService;
|
||
function mediaLibraryService() {
|
||
if (!libraryService) {
|
||
libraryService = new MediaLibraryService(new MediaLibraryStore());
|
||
}
|
||
return libraryService;
|
||
}
|
||
/**
|
||
* Creates a new media library query from any media option info (MediaOption, MediaOptionDetails, etc)
|
||
* Uses the global store for read only query, but protects the store access
|
||
*
|
||
* @param {MediaOptionDetails} mediaOptionDetails The details to store
|
||
*/
|
||
const createMediaLibraryQuery = (mediaOption) => {
|
||
return mediaLibraryService().getQueryForOption(mediaOption);
|
||
};
|
||
/**
|
||
* Upserts a MediaOptionDetails to the store
|
||
*
|
||
* @param {MediaOptionDetails} mediaOptionDetails The details to store
|
||
*/
|
||
const archiveMediaOptionDetails = (mediaOptionDetails, stats, changed) => {
|
||
mediaLibraryService().archiveMediaOptionDetails(mediaOptionDetails, stats, changed);
|
||
};
|
||
/**
|
||
* Retrieve the details (a mediaplaylist.m3u8) for a media option (variant or alternate)
|
||
* Uses the global store for read only query, but protects the store access
|
||
*
|
||
* @param {MediaOption} mediaOption media option to load the details for
|
||
* @param {LoadPolicy} loadPolicy the policy determining how to load the details
|
||
* @param {number} maxTimeouts Maximum number of consecutive timeouts allowed
|
||
* @returns {Observable<MediaOptionDetails>} retrieved (cached or loaded) details for the source media option
|
||
*/
|
||
const retrieveMediaOptionDetails = (libContext, mediaOption, isPrefetch = false, force = false) => {
|
||
if (mediaOption == null || !isEnabledMediaOption(mediaOption)) {
|
||
return of(null);
|
||
}
|
||
const { itemId } = mediaOption;
|
||
const { mediaLibraryService: libraryService } = libContext;
|
||
const libraryQuery = libraryService.getQueryForOption(mediaOption);
|
||
if (!libraryQuery.hasEntity(itemId)) {
|
||
libraryService.createMediaLibraryEntity(itemId);
|
||
}
|
||
const entity = libraryQuery.mediaOptionDetailsEntity;
|
||
const details = libraryQuery.mediaOptionDetails;
|
||
if (details != null && !force && (details.type === 'VOD' || (details.liveOrEvent && !needToRefreshLevel(details, entity.lastUpdateMillis)))) {
|
||
return of(details).pipe(tag('retrieveMediaOptionDetails.emit.cached')).pipe(take(1));
|
||
}
|
||
libraryService.setDetailsLoading(mediaOption);
|
||
return getMediaOptionDetailsCommon(libContext, mediaOption, isPrefetch);
|
||
};
|
||
/**
|
||
* Schedules playlist refresh if needed
|
||
*/
|
||
function refreshMediaOptionDetails(libContext, mediaOption) {
|
||
const { mediaLibraryService, logger } = libContext;
|
||
const mediaLibraryQuery = mediaLibraryService.getQueryForOption(mediaOption);
|
||
// getReloadTimer returns EMPTY if non-live so it shouldn't emit
|
||
return getReloadTimer(mediaLibraryQuery.mediaOptionDetailsEntity, 0, logger).pipe(switchMap(() => retrieveMediaOptionDetails(libContext, mediaOption, false, true)), switchMap(() => refreshMediaOptionDetails(libContext, mediaOption)));
|
||
}
|
||
// Load media option details with error handling
|
||
function getMediaOptionDetailsCommon(libContext, mediaOption, isPrefetch) {
|
||
var _a, _b;
|
||
const { logger, config, rootPlaylistQuery: rootQuery, rootPlaylistService: rootService, statsService, mediaLibraryService, mediaSink } = libContext;
|
||
const mediaQuery = mediaSink.mediaQuery;
|
||
const loadPolicy = config.playlistLoadPolicy;
|
||
const keySystemPreference = config.keySystemPreference;
|
||
const masterVariableList = rootQuery.masterVariableList;
|
||
const extendMaxTTFB = (_b = (_a = globalHlsService()) === null || _a === void 0 ? void 0 : _a.getQuery()) === null || _b === void 0 ? void 0 : _b.extendMaxTTFB;
|
||
return loadMediaOptionDetails(mediaOption, rootQuery.itemStartOffset, config, loadPolicy, logger, keySystemPreference, statsService, masterVariableList, extendMaxTTFB)
|
||
.pipe(map((loadMediaOptionDetailsResult) => {
|
||
var _a;
|
||
// Handle merging with previous level for live/event
|
||
const curLibraryQuery = mediaLibraryService.getQueryForOption(mediaOption);
|
||
const thisMediaOptionDetails = curLibraryQuery.mediaOptionDetails;
|
||
const { mediaOptionDetails: newMediaOptionDetails } = loadMediaOptionDetailsResult;
|
||
const { stats } = loadMediaOptionDetailsResult;
|
||
let changed = true;
|
||
if (newMediaOptionDetails.liveOrEvent) {
|
||
const { mediaOptionType } = newMediaOptionDetails;
|
||
changed = mediaOptionDetailsHasChanged(thisMediaOptionDetails, newMediaOptionDetails);
|
||
// Try merging with self first, then last loaded
|
||
if (thisMediaOptionDetails) {
|
||
mergeDetails(thisMediaOptionDetails, newMediaOptionDetails, logger);
|
||
}
|
||
const lastLoadedOption = rootQuery.lastLoadedMediaOptionByType(mediaOptionType);
|
||
const lastMediaOptionDetails = lastLoadedOption ? (_a = mediaLibraryService.getQueryForOption(lastLoadedOption)) === null || _a === void 0 ? void 0 : _a.mediaOptionDetails : null;
|
||
if (!newMediaOptionDetails.ptsKnown && lastMediaOptionDetails && lastMediaOptionDetails.mediaOptionId !== (thisMediaOptionDetails === null || thisMediaOptionDetails === void 0 ? void 0 : thisMediaOptionDetails.mediaOptionId)) {
|
||
mergeDetails(lastMediaOptionDetails, newMediaOptionDetails, logger);
|
||
}
|
||
}
|
||
if (newMediaOptionDetails) {
|
||
applyTransaction(() => {
|
||
mediaLibraryService.archiveMediaOptionDetails(newMediaOptionDetails, stats, changed);
|
||
rootService.setLastLoadedMediaOptionByType(rootQuery.itemId, mediaOption.mediaOptionType, mediaOption);
|
||
});
|
||
}
|
||
const hitUnchangedMaxCount = !changed && curLibraryQuery.mediaOptionDetailsEntity.unchangedCount >= config.liveMaxUnchangedPlaylistRefresh;
|
||
const isExpiredInfo = livePlaylistExpired(newMediaOptionDetails, stats.tload, config.maxBufferHole, mediaQuery);
|
||
if (hitUnchangedMaxCount || isExpiredInfo.expired) {
|
||
let response;
|
||
if (hitUnchangedMaxCount) {
|
||
response = ErrorResponses.LivePlaylistUpdateError;
|
||
}
|
||
else {
|
||
response = {
|
||
text: `Live window too far in the past end:${isExpiredInfo.windowEnd.toFixed(3)} minPosition:${isExpiredInfo.minPosition}`,
|
||
code: 0,
|
||
};
|
||
}
|
||
throw new PlaylistNetworkError(false, response.text, response.code, response, false, mediaOption.mediaOptionType, mediaOption.mediaOptionId, mediaOption.url);
|
||
}
|
||
return newMediaOptionDetails;
|
||
}), tag('getMediaOptionDetailsCommon.emit.loaded'), addLoadErrorHandlingPolicy(mediaOption.itemId, mediaOption.mediaOptionType, getLoadConfig(mediaOption, loadPolicy), config.maxNumAddLevelToPenaltyBox, isPrefetch, rootQuery, rootService, statsService))
|
||
.pipe(take(1));
|
||
}
|
||
/**
|
||
* Retrieve the corresponding init segment (#EXT-X-MAP) for a media fragment
|
||
*
|
||
* @param {MediaFragment} mediaFragment get init segment for this media fragment
|
||
* @returns {Observable<InitSegmentCacheEntity>} retrieved (cached or loaded) init segment
|
||
*/
|
||
const retrieveInitSegmentCacheEntity = (libContext, mediaFragment) => {
|
||
if (!mediaFragment)
|
||
return of(null);
|
||
const { logger } = libContext;
|
||
const { mediaLibraryService, mediaParser } = libContext;
|
||
const mediaLibraryQuery = mediaLibraryService.getQueryForOption(mediaFragment);
|
||
const { mediaOption, mediaOptionDetailsEntityRecord, mediaOptionDetails } = mediaLibraryQuery;
|
||
const { mediaOptionId } = mediaOption;
|
||
if (!(mediaOptionDetailsEntityRecord === null || mediaOptionDetailsEntityRecord === void 0 ? void 0 : mediaOptionDetailsEntityRecord[mediaOptionId]))
|
||
throw new Error('retrieveInitSegmentCacheEntity no details entity');
|
||
if (!mediaOptionDetails)
|
||
throw new Error('retrieveInitSegmentCacheEntity no details');
|
||
const { initSegmentCacheEntities } = mediaOptionDetailsEntityRecord[mediaOptionId];
|
||
const { initSegments } = mediaOptionDetails;
|
||
const { mediaSeqNum, discoSeqNum } = mediaFragment;
|
||
if (initSegmentCacheEntities[discoSeqNum]) {
|
||
logger.debug({ mediaOptionId, mediaSeqNum, discoSeqNum }, 'found cached init segment');
|
||
const [original, generated] = initSegmentCacheEntities[discoSeqNum];
|
||
let entity = original;
|
||
if (generated) {
|
||
const trackSwitch = mediaParser.willBeTrackSwitch(mediaFragment);
|
||
entity = trackSwitch ? original : generated;
|
||
}
|
||
return of(entity);
|
||
}
|
||
const initFrag = initSegments[discoSeqNum];
|
||
if (!initFrag) {
|
||
logger.debug({ mediaOptionId, mediaSeqNum, discoSeqNum }, 'no init segment entry');
|
||
return of(null);
|
||
}
|
||
mediaLibraryService.setInitSegmentLoading(initFrag);
|
||
logger.info({ mediaSeqNum, discoSeqNum }, 'loading init segment');
|
||
const timingObj = {
|
||
mediaOptionId,
|
||
mediaSeqNum: 'initSegment',
|
||
discoSeqNum,
|
||
name: MediaOptionNames[mediaFragment.mediaOptionType],
|
||
state: 'loading',
|
||
};
|
||
const tlog = logger.child({ name: 'timing' });
|
||
tlog.info(`${JSON.stringify(timingObj)}`);
|
||
return getMediaFragmentCommon(libContext, initFrag, false, false).pipe(tap(() => {
|
||
tlog.info(`${JSON.stringify(Object.assign(Object.assign({}, timingObj), { state: 'loaded' }))}`);
|
||
}), observeOn(asyncScheduler), switchMap((data) => {
|
||
tlog.info(`${JSON.stringify(Object.assign(Object.assign({}, timingObj), { state: 'parsing' }))}`);
|
||
return parseInitSegment(data, initFrag, libContext);
|
||
}), tap(() => {
|
||
tlog.info(`${JSON.stringify(Object.assign(Object.assign({}, timingObj), { state: 'parsed' }))}`);
|
||
}));
|
||
};
|
||
function parseInitSegment(data, frag, libContext) {
|
||
var _a;
|
||
const { logger, mediaSink, rootPlaylistService, rootPlaylistQuery, mediaParser, mediaLibraryService, gaplessInstance } = libContext;
|
||
const { mediaQuery } = mediaSink;
|
||
const mediaLibraryQuery = mediaLibraryService.getQueryForOption(frag);
|
||
const { mediaOption, mediaOptionDetails } = mediaLibraryQuery;
|
||
const { itemId, mediaOptionId } = mediaOption;
|
||
const { keyTagInfo, discoSeqNum, mediaSeqNum, mediaOptionType } = frag;
|
||
const seeking = mediaQuery.seeking;
|
||
const live = mediaOptionDetails.liveOrEvent;
|
||
const ptsKnown = mediaOptionType === MediaOptionType.Variant ? mediaOptionDetails.ptsKnown : false;
|
||
let segment;
|
||
let initSegment;
|
||
if (frag.isInitSegment) {
|
||
initSegment = new Uint8Array(data);
|
||
}
|
||
else {
|
||
segment = new Uint8Array(data);
|
||
}
|
||
const parserContext = {
|
||
segment,
|
||
initSegment,
|
||
frag,
|
||
ptsKnown,
|
||
seeking,
|
||
live,
|
||
totalDuration: mediaOptionDetails.totalduration,
|
||
};
|
||
return mediaParser.parseInitSegment(parserContext, (_a = navigator === null || navigator === void 0 ? void 0 : navigator.vendor) !== null && _a !== void 0 ? _a : '').pipe(map((parsedInitSegment) => {
|
||
const { track, moovData, mimeType } = parsedInitSegment;
|
||
const { initSegment } = track;
|
||
if (gaplessInstance.inGaplessMode && MediaUtil.isVideoCodec(track.codec)) {
|
||
logger.warn(`Video codec discovered in gapless mode codec:${track.codec}`);
|
||
gaplessInstance.dequeueSource('InvalidFormat');
|
||
}
|
||
const initSegmentCacheEntity = { itemId, mediaOptionId, discoSeqNum, initParsedData: moovData, data: initSegment, mimeType, keyTagInfo, fragment: frag };
|
||
mediaLibraryService.archiveInitSegmentEntity(initSegmentCacheEntity);
|
||
logger.info({ mediaOptionId, mediaSeqNum, discoSeqNum }, 'loaded init segment');
|
||
return initSegmentCacheEntity;
|
||
}), addDemuxErrorHandlingPolicy(rootPlaylistService, rootPlaylistQuery, mediaOptionType, mediaOptionId));
|
||
}
|
||
function parseSegment(data, defaultInitPTS, initSegmentCacheEntity, frag, timelineOffset, libContext) {
|
||
var _a, _b;
|
||
const segment = new Uint8Array(data);
|
||
const { legibleSystemAdapter, rootPlaylistService, mediaSink, mediaParser, rootPlaylistQuery, mediaLibraryService } = libContext;
|
||
const { mediaQuery } = mediaSink;
|
||
const mediaLibraryQuery = mediaLibraryService.getQueryForOption(frag);
|
||
const { mediaOption, mediaOptionDetails } = mediaLibraryQuery;
|
||
const { initSegments } = mediaOptionDetails;
|
||
const { itemId, mediaOptionId } = mediaOption;
|
||
const { discoSeqNum, mediaSeqNum, mediaOptionType, isLastFragment } = frag;
|
||
const seeking = mediaQuery.seeking;
|
||
const live = mediaOptionDetails.liveOrEvent;
|
||
const initSeg = initSegments[discoSeqNum];
|
||
const ptsKnown = mediaOptionType === MediaOptionType.Variant ? mediaOptionDetails.ptsKnown : false;
|
||
const parserContext = {
|
||
segment,
|
||
frag,
|
||
seeking,
|
||
live,
|
||
ptsKnown,
|
||
totalDuration: mediaOptionDetails.totalduration,
|
||
defaultInitPTS,
|
||
iframeMediaStart: isIframeMediaFragment(frag) ? frag.iframeMediaStart : undefined,
|
||
iframeDuration: isIframeMediaFragment(frag) ? frag.iframeMediaDuration : undefined,
|
||
iframeOriginalStart: isIframeMediaFragment(frag) ? frag.iframeOriginalStart : undefined,
|
||
};
|
||
let source;
|
||
if (initSegmentCacheEntity != null && ((_a = frag.keyTagInfo) === null || _a === void 0 ? void 0 : _a.uri) === ((_b = initSegmentCacheEntity.keyTagInfo) === null || _b === void 0 ? void 0 : _b.uri)) {
|
||
source = of(initSegmentCacheEntity);
|
||
}
|
||
else if (initSegmentCacheEntity != null) {
|
||
// key rotated, update cached init segment with current frag keyTagInfo, pass to demuxer for parsing
|
||
const updatedFrag = Object.assign(Object.assign({}, initSegmentCacheEntity.fragment), { keyTagInfo: frag.keyTagInfo });
|
||
const initData = initSeg ? initSegmentCacheEntity.data : data;
|
||
source = parseInitSegment(initData, updatedFrag, libContext);
|
||
}
|
||
else {
|
||
source = parseInitSegment(data, frag, libContext);
|
||
}
|
||
return source.pipe(switchMap((initSegmentCacheEntity) => {
|
||
const parseStartTime = performance.now();
|
||
if (initSegmentCacheEntity != null) {
|
||
const { data: is } = initSegmentCacheEntity;
|
||
const initSegment = new Uint8Array(is);
|
||
parserContext.initSegment = initSegment;
|
||
}
|
||
if (frag.mediaOptionType === MediaOptionType.Variant) {
|
||
legibleSystemAdapter === null || legibleSystemAdapter === void 0 ? void 0 : legibleSystemAdapter.setupForFrag(frag);
|
||
}
|
||
return mediaParser.parseSegment(parserContext, '').pipe(map((parsedSegment) => {
|
||
var _a;
|
||
const parseEndTime = performance.now();
|
||
const { startPTS, startDTS: startDtsTs, endPTS, endDTS: endDtsTs, firstKeyframePts, framesWithoutIDR, dropped, data1, data2, captionData, id3Samples, parsedInitSegment } = parsedSegment;
|
||
const fragSample = {
|
||
durationSec: endPTS.baseTime / endPTS.timescale - startPTS.baseTime / startPTS.timescale,
|
||
parseTimeMs: parseEndTime - parseStartTime,
|
||
};
|
||
libContext.statsService.setFragSample(fragSample);
|
||
// parser updated initSegment, possibly due to silent audio insertion
|
||
let generatedInitSegmentCacheEntity = Object.assign({}, initSegmentCacheEntity);
|
||
if (parsedInitSegment) {
|
||
const { track, moovData, mimeType } = parsedInitSegment;
|
||
const { initSegment } = track;
|
||
generatedInitSegmentCacheEntity = { itemId, mediaOptionId, discoSeqNum, initParsedData: moovData, data: initSegment, mimeType, keyTagInfo: frag.keyTagInfo, fragment: frag };
|
||
mediaLibraryService.archiveInitSegmentEntity(initSegmentCacheEntity, generatedInitSegmentCacheEntity);
|
||
}
|
||
const keyTagInfo = frag.keyTagInfo;
|
||
const mediaFragmentCacheEntity = {
|
||
itemId,
|
||
mediaOptionId,
|
||
mediaSeqNum,
|
||
discoSeqNum,
|
||
startDtsTs,
|
||
endDtsTs,
|
||
timelineOffset,
|
||
firstKeyframePts,
|
||
framesWithoutIDR,
|
||
dropped,
|
||
data1,
|
||
data2,
|
||
startPts: startPTS,
|
||
endPts: endPTS,
|
||
keyTagInfo,
|
||
isLastFragment,
|
||
iframe: (_a = frag.iframe) !== null && _a !== void 0 ? _a : false,
|
||
duration: frag.duration,
|
||
iframeMediaDuration: isIframeMediaFragment(frag) ? frag.iframeMediaDuration : undefined,
|
||
iframeOriginalStart: isIframeMediaFragment(frag) ? frag.iframeOriginalStart : undefined,
|
||
captionData,
|
||
id3Samples,
|
||
};
|
||
const appendDataTuple = [generatedInitSegmentCacheEntity, mediaFragmentCacheEntity];
|
||
return appendDataTuple;
|
||
}));
|
||
}), addDemuxErrorHandlingPolicy(rootPlaylistService, rootPlaylistQuery, mediaOptionType, mediaOptionId));
|
||
}
|
||
// Fragment loading with error handling
|
||
function getMediaFragmentCommon(libContext, frag, updateStats, updateRtc) {
|
||
var _a, _b, _c;
|
||
const { rootPlaylistQuery: rootQuery, rootPlaylistService: rootService, config, rtcService, statsService } = libContext;
|
||
const { itemId, mediaOptionType } = frag;
|
||
const loadPolicy = config.fragLoadPolicy;
|
||
const isMediaFragment = isFiniteNumber(frag.mediaSeqNum);
|
||
let onProgress;
|
||
if (isMediaFragment) {
|
||
const updateSample = (_url, _status, stats, _data) => {
|
||
rootService.updateInflightFrag(itemId, frag.mediaOptionType, frag, 'loading', stats);
|
||
return false;
|
||
};
|
||
onProgress = { getData: false, cb: updateSample };
|
||
}
|
||
let requestServerInfo = false;
|
||
if (updateRtc) {
|
||
if (rtcService.serverInfoInstance === null) {
|
||
requestServerInfo = true;
|
||
}
|
||
}
|
||
const fetchSegment$ = loadMediaFragment(frag, config, loadPolicy, onProgress, requestServerInfo, (_b = (_a = globalHlsService()) === null || _a === void 0 ? void 0 : _a.getQuery()) === null || _b === void 0 ? void 0 : _b.extendMaxTTFB).pipe(tap(([, , stats, serverInfo]) => {
|
||
libContext.logger.qe({
|
||
critical: true,
|
||
name: 'resourceTimingMonitor',
|
||
data: {
|
||
type: MediaOptionNames[frag.mediaOptionType],
|
||
trequest: stats.trequest,
|
||
tfirst: stats.tfirst,
|
||
tload: stats.tload,
|
||
bitsDownloaded: stats.total,
|
||
mediaSeqNum: frag.mediaSeqNum,
|
||
mediaOpitonId: frag.mediaOptionId,
|
||
},
|
||
});
|
||
if (updateStats) {
|
||
statsService.setBandwidthSample(Object.assign(Object.assign({}, stats), { mediaOptionType: frag.mediaOptionType }));
|
||
}
|
||
if (updateRtc && requestServerInfo) {
|
||
rtcService.serverInfoInstance = serverInfo;
|
||
}
|
||
if (isMediaFragment) {
|
||
rootService.updateInflightFrag(itemId, frag.mediaOptionType, frag, 'loaded', stats);
|
||
}
|
||
}), tap(([mediaFragment, , stats]) => {
|
||
if (updateRtc) {
|
||
const { logger } = rootService;
|
||
logger.qe({
|
||
critical: true,
|
||
name: 'fragLoaded',
|
||
data: { mediaOptionType: frag.mediaOptionType, fragLoadingProcessingMs: stats.tload - stats.tfirst, loaded: stats.loaded, duration: frag.duration, fragLoadMs: stats.tload - stats.trequest },
|
||
});
|
||
rtcService.handleFragLoaded(mediaFragment, stats);
|
||
}
|
||
}), addLoadErrorHandlingPolicy(itemId, mediaOptionType, getLoadConfig(frag, loadPolicy), config.maxNumAddLevelToPenaltyBox, false, rootQuery, rootService, statsService));
|
||
// <rdar://89444975> Don't need to wait for key if segment is not encrypted!
|
||
const shouldWaitForKey = ((_c = frag.keyTagInfo) === null || _c === void 0 ? void 0 : _c.method) === 'AES-128';
|
||
if (shouldWaitForKey) {
|
||
return forkJoin([loadKey(libContext, frag.keyTagInfo, { itemId: frag.itemId, mediaOptionId: frag.mediaOptionId }), fetchSegment$]).pipe(switchMap(([keyTagInfo, fragInfo]) => {
|
||
const [mediaFragment, fragDataBuf] = fragInfo;
|
||
mediaFragment.keyTagInfo.key = keyTagInfo.key;
|
||
return decryptMediaFragment(mediaFragment, fragDataBuf, config, libContext.logger, libContext.rpcClients.crypto);
|
||
}));
|
||
}
|
||
return fetchSegment$.pipe(map((fragInfo) => fragInfo[1]));
|
||
}
|
||
/**
|
||
* Retrieve the data for a media fragment
|
||
*
|
||
* @param {MediaFragment} retrieveFragAction the media fragment to retrieve data for
|
||
* @returns {Observable<SbDataTuple>} the init segment data and fragment data
|
||
*/
|
||
const retrieveSubtitleFragmentCacheEntity = (libContext, initPTS, mediaFragment) => {
|
||
const { logger } = libContext;
|
||
const { mediaOptionType, mediaOptionId, discoSeqNum, mediaSeqNum } = mediaFragment;
|
||
logger.info(`[${MediaOptionNames[mediaOptionType]}] loading media fragment ${JSON.stringify({ mediaOptionId, discoSeqNum, mediaSeqNum })}`);
|
||
return getMediaFragmentCommon(libContext, mediaFragment, false, false).pipe(map((data) => {
|
||
logger.info(`[${MediaOptionNames[mediaOptionType]}] loaded media fragment ${JSON.stringify({ mediaOptionId, discoSeqNum, mediaSeqNum })}`);
|
||
return { initPTS, data, mediaFragment };
|
||
}), tag('retrieveSubtitleFragmentCacheEntity.emit'));
|
||
};
|
||
/**
|
||
* Retrieve the data for a media fragment
|
||
*
|
||
* @param {MediaFragment} retrieveFragAction the media fragment to retrieve data for
|
||
* @returns {Observable<SbDataTuple>} the init segment data and fragment data
|
||
*/
|
||
const retrieveMediaFragmentCacheEntity = (libContext, mediaOptionType, retrieveFragAction) => {
|
||
const { rootPlaylistService: rootService, rootPlaylistQuery: rootQuery } = libContext;
|
||
const { logger } = rootService;
|
||
const { timelineOffset, mediaFragment } = retrieveFragAction.foundFrag;
|
||
const { itemId, mediaOptionId, discoSeqNum, mediaSeqNum } = mediaFragment;
|
||
logger.info(`[${MediaOptionNames[mediaOptionType]}] loading media fragment ${JSON.stringify({ mediaOptionId, discoSeqNum, mediaSeqNum })}`);
|
||
return retrieveInitSegmentCacheEntity(libContext, mediaFragment).pipe(switchMap((initSegmentCacheEntity) => {
|
||
rootService.updateInflightFrag(itemId, mediaFragment.mediaOptionType, mediaFragment, 'loading', null);
|
||
return getMediaFragmentCommon(libContext, mediaFragment, true, true).pipe(switchMap((data) => {
|
||
var _a;
|
||
rootService.updateInflightFrag(itemId, mediaOptionType, mediaFragment, 'parsing', null);
|
||
return parseSegment(data, (_a = rootQuery.getInitPTS(discoSeqNum)) === null || _a === void 0 ? void 0 : _a.offsetTimestamp, initSegmentCacheEntity, mediaFragment, timelineOffset, libContext);
|
||
}), tap((parsedData) => {
|
||
rootService.updateInflightFrag(itemId, mediaOptionType, mediaFragment, 'parsed', null);
|
||
const { startPts, endPts, startDtsTs, endDtsTs } = parsedData[1];
|
||
logger.info(`[${MediaOptionNames[mediaOptionType]}] ${JSON.stringify({ mediaOptionId, discoSeqNum, mediaSeqNum })} parsed: ${JSON.stringify({ startPts, endPts, startDtsTs, endDtsTs })}`);
|
||
}), tag(`retrieveMediaFragmentCacheEntity.${mediaOptionType}.emit`));
|
||
}), take(1));
|
||
};
|
||
function loadKey(context, keyTagInfo, mediaOptionKey) {
|
||
const { keySystemAdapter, rootPlaylistQuery, rootPlaylistService, config } = context;
|
||
return keySystemAdapter
|
||
.getKeyFromDecryptData(keyTagInfo, mediaOptionKey)
|
||
.pipe(addKeyErrorHandlingPolicy(keyTagInfo.uri, getLoadConfig({ url: keyTagInfo.uri }, config.keyLoadPolicy), rootPlaylistQuery, rootPlaylistService, keySystemAdapter.ksQuery));
|
||
}
|
||
function mediaLibraryRemove(ids) {
|
||
mediaLibraryService().remove(ids);
|
||
}
|
||
function mediaLibraryClear() {
|
||
mediaLibraryService().clear();
|
||
}
|
||
|
||
/*
|
||
* HLS Player Events
|
||
*
|
||
*
|
||
*/
|
||
/*
|
||
* @brief subscribes to hls public queries and sends out events.
|
||
*/
|
||
class HlsPlayerEvents {
|
||
constructor(hls, logger, rtcService) {
|
||
this.hls = hls;
|
||
this.destroy$ = new Subject();
|
||
this.iframeSwitchStart = 0;
|
||
this.logger = logger.child({ name: 'hls-player-events' });
|
||
this.rtc = rtcService;
|
||
this.subscribeAndEmit();
|
||
}
|
||
destroy() {
|
||
this.destroy$.next();
|
||
}
|
||
subscribeAndEmit() {
|
||
const loaderQuery$ = this.loaderQueryListener(createLoaderQuery());
|
||
const publicQueries$ = this.hls.publicQueries$.pipe(switchMap(([rootPlaylistQuery, mediaElementQuery]) => {
|
||
return merge(this.rootPlaylistQueryListener(rootPlaylistQuery, mediaElementQuery), this.mediaElementQueryListener(mediaElementQuery, rootPlaylistQuery));
|
||
}));
|
||
const activeItemQuery$ = this.activeItemListener(this.hls.itemQueue);
|
||
merge(loaderQuery$, publicQueries$, activeItemQuery$)
|
||
.pipe(catchError((err) => {
|
||
let errMessage = err.message;
|
||
{
|
||
errMessage = err.stack;
|
||
}
|
||
this.logger.error(`Got error in HlsPlayerEvents ${errMessage}`, err);
|
||
return EMPTY;
|
||
}), takeUntil(this.destroy$), finalize$1(() => {
|
||
this.logger.info('HlsPlayerEvents finalized');
|
||
}))
|
||
.subscribe();
|
||
}
|
||
activeItemListener(itemQueue) {
|
||
const manifestLoadSource$ = itemQueue.activeItemById$.pipe(filterNullOrUndefined(), switchMap((item) => {
|
||
var _a;
|
||
const url = item.url;
|
||
this.logger.debug(`Manifest loading: ${(_a = this.hls.itemQueue.activeItem) === null || _a === void 0 ? void 0 : _a.url}`);
|
||
this.hls.trigger(HlsEvent.MANIFEST_LOADING, { url });
|
||
return EMPTY;
|
||
}));
|
||
return manifestLoadSource$;
|
||
}
|
||
rootPlaylistQueryListener(rootPlaylistQuery, mediaElementQuery) {
|
||
const variantSource$ = rootPlaylistQuery.enabledMediaOptionByType$(MediaOptionType.Variant).pipe(filter((mediaOption) => !!mediaOption), switchMap((mediaOption) => {
|
||
var _a;
|
||
this.logger.debug(`Switching to level: ${mediaOption.mediaOptionId}`);
|
||
this.hls.trigger(HlsEvent.LEVEL_SWITCHING, mediaOption);
|
||
(_a = this.rtc) === null || _a === void 0 ? void 0 : _a.handleLevelSwitching(mediaOption.url);
|
||
return EMPTY;
|
||
}));
|
||
const levelLoading$ = rootPlaylistQuery.enabledMediaOptionByType$(MediaOptionType.Variant).pipe(switchMap((mediaOption) => {
|
||
const mediaLibQuery = createMediaLibraryQuery(mediaOption);
|
||
return mediaLibQuery.mediaOptionDetailsEntity$.pipe(filter((entity) => (entity === null || entity === void 0 ? void 0 : entity.detailsLoading) === true), tap((_) => {
|
||
const levelLoading = {
|
||
url: redactUrl(mediaOption === null || mediaOption === void 0 ? void 0 : mediaOption.url),
|
||
level: mediaOption.mediaOptionId,
|
||
type: MediaOptionNames[mediaOption.mediaOptionType],
|
||
};
|
||
this.logger.qe({ critical: true, name: 'levelLoading', data: levelLoading });
|
||
this.hls.trigger(HlsEvent.LEVEL_LOADING, levelLoading);
|
||
return EMPTY;
|
||
}));
|
||
}));
|
||
const levelLoaded$ = rootPlaylistQuery.enabledMediaOptionByType$(MediaOptionType.Variant).pipe(switchMap((mediaOption) => {
|
||
const query = createMediaLibraryQuery(mediaOption);
|
||
let lastUpdate = 0;
|
||
// filter on entities that have loading set to false and have stats filled and have been updated
|
||
return query.mediaOptionDetailsEntity$.pipe(filterNullOrUndefined(), filter((entity) => {
|
||
var _a;
|
||
const retValue = entity.stats !== null && entity.detailsLoading === false && entity.lastUpdateMillis > lastUpdate;
|
||
lastUpdate = (_a = entity.lastUpdateMillis) !== null && _a !== void 0 ? _a : 0;
|
||
return retValue;
|
||
}));
|
||
}), switchMap((mediaDetailsEntity) => {
|
||
var _a;
|
||
const mediaOptionDetails = mediaDetailsEntity.mediaOptionDetails;
|
||
const stats = mediaDetailsEntity.stats;
|
||
const levelLoadedData = {
|
||
mediaOptionId: mediaOptionDetails.mediaOptionId,
|
||
details: mediaOptionDetails,
|
||
playlistType: mediaOptionDetails.type,
|
||
stats: stats,
|
||
};
|
||
// this.logger.debug('Level loaded %o', levelLoadedData);
|
||
this.logger.qe({
|
||
critical: true,
|
||
name: 'levelLoaded',
|
||
data: { url: redactUrl(mediaOptionDetails === null || mediaOptionDetails === void 0 ? void 0 : mediaOptionDetails.url), level: mediaOptionDetails.mediaOptionId, type: MediaOptionNames[mediaOptionDetails.mediaOptionType], adt: stats.tload - stats.trequest },
|
||
});
|
||
(_a = this.rtc) === null || _a === void 0 ? void 0 : _a.handleLevelLoaded(mediaOptionDetails, levelLoadedData.stats);
|
||
this.hls.trigger(HlsEvent.LEVEL_LOADED, levelLoadedData);
|
||
if (mediaDetailsEntity.unchangedCount === 0) {
|
||
const levelUpdatedData = {
|
||
level: 0,
|
||
details: mediaOptionDetails,
|
||
};
|
||
// This is critical for gapless. Muze uses this to set media duration
|
||
this.hls.trigger(HlsEvent.LEVEL_UPDATED, levelUpdatedData);
|
||
}
|
||
if (mediaOptionDetails === null || mediaOptionDetails === void 0 ? void 0 : mediaOptionDetails.daterangeTags) {
|
||
const dateRangeTags = { daterangeTags: mediaOptionDetails.daterangeTags };
|
||
// this.logger.debug('Date range tags parsed: %o', dateRangeTags);
|
||
this.logger.qe({ critical: true, name: 'dateRangeTags', data: { dateRangeTags } });
|
||
this.hls.trigger(HlsEvent.DATERANGE_UPDATED, dateRangeTags);
|
||
}
|
||
return EMPTY;
|
||
}));
|
||
const audioSource$ = rootPlaylistQuery.enableMediaOptionSwitchedForType$(MediaOptionType.AltAudio).pipe(switchMap((mediaOption) => {
|
||
const altOption = rootPlaylistQuery.alternateMediaOptionById(MediaOptionType.AltAudio, mediaOption.mediaOptionId);
|
||
if (altOption) {
|
||
this.triggerAudioSwitch(altOption);
|
||
}
|
||
return EMPTY;
|
||
}));
|
||
// The first audio switch occurs before the rootPlaylistQuery is ready. As a result, we'll monitor the
|
||
// rootPlaylistEntity to trigger the first audio switch
|
||
const firstAudioSwitch$ = rootPlaylistQuery.rootPlaylistEntity$.pipe(filter((rootEntity) => rootEntity.enabledMediaOptionKeys[MediaOptionType.AltAudio].mediaOptionId !== null), take(1), switchMap((rootEntity) => {
|
||
const mediaOptionId = rootEntity.enabledMediaOptionKeys[MediaOptionType.AltAudio].mediaOptionId;
|
||
if (mediaOptionId) {
|
||
const altOption = rootEntity.mediaOptionListTuple[MediaOptionType.AltAudio].mediaOptions.find((option) => option.mediaOptionId === mediaOptionId);
|
||
this.triggerAudioSwitch(Object.assign(Object.assign({}, altOption), { url: redactUrl(altOption === null || altOption === void 0 ? void 0 : altOption.url) }));
|
||
}
|
||
return EMPTY;
|
||
}));
|
||
// Trigger SUBTITLE_TRACK_SWITCH only when the HTML5 texttracks have been created.
|
||
// MatchPoint will call addCueChange on the enabled texttrack.
|
||
const subtitleSource$ = waitFor(mediaElementQuery.textTracksCreated$, (created) => created).pipe(switchMap(() => {
|
||
return rootPlaylistQuery.enabledMediaOptionByType$(MediaOptionType.Subtitle).pipe(switchMap((mediaOption) => {
|
||
const altOption = rootPlaylistQuery.alternateMediaOptionById(MediaOptionType.Subtitle, mediaOption.mediaOptionId);
|
||
if (altOption) {
|
||
const data = {
|
||
trackId: altOption.id,
|
||
mediaOptionId: altOption.mediaOptionId,
|
||
groupId: altOption.groupId,
|
||
persistentId: altOption.persistentID,
|
||
name: altOption.name,
|
||
};
|
||
this.logger.qe({ critical: true, name: 'textTrackSwitch', data });
|
||
this.logger.debug(`Subtitle track switch track: ${altOption.id}`);
|
||
this.hls.trigger(HlsEvent.SUBTITLE_TRACK_SWITCH, {
|
||
track: Object.assign({}, altOption),
|
||
hidden: false,
|
||
});
|
||
}
|
||
else {
|
||
this.hls.trigger(HlsEvent.SUBTITLE_TRACK_SWITCH, {
|
||
track: undefined,
|
||
hidden: false,
|
||
});
|
||
this.logger.qe({ critical: true, name: 'textTrackSwitch', data: { unselected: true } });
|
||
}
|
||
return EMPTY;
|
||
}));
|
||
}));
|
||
const sessionData$ = rootPlaylistQuery.sessionData$.pipe(filter((d) => d.complete != undefined), take(1), tap((sd) => this.logger.debug('Session Data Complete: %o', sd)), map((sd) => {
|
||
this.hls.trigger(HlsEvent.SESSION_DATA_COMPLETE, sd);
|
||
}));
|
||
const prefferedLevels$ = rootPlaylistQuery.getPreferredMediaOptionsByType$(MediaOptionType.Variant).pipe(skip(1), map((prefLevels) => {
|
||
var _a;
|
||
const variantList = prefLevels;
|
||
this.logger.debug('Levels changed %o', variantList);
|
||
(_a = this.rtc) === null || _a === void 0 ? void 0 : _a.handleLevelsChanged(variantList);
|
||
this.logger.qe({ critical: true, name: 'levelsChanged', data: { numLevels: variantList.length } });
|
||
variantList.forEach((level) => {
|
||
const { mediaOptionId, bandwidth, bitrate, videoCodec, audioCodec, height, width, videoRange, iframes } = level;
|
||
this.logger.qe({ critical: true, name: 'manifestLevel', data: { mediaOptionId, bandwidth, bitrate, videoCodec, audioCodec, height, width, videoRange, iframes } });
|
||
});
|
||
this.hls.trigger(HlsEvent.LEVELS_CHANGED, { requiresReset: false, levels: variantList });
|
||
//TODO fix require reset
|
||
}));
|
||
const altPrefferedLevels$ = rootPlaylistQuery.getPreferredMediaOptionsByType$(MediaOptionType.AltAudio).pipe(map((prefLevels) => {
|
||
const altList = prefLevels;
|
||
this.logger.debug('Audio tracks updated %o', altList);
|
||
this.hls.trigger(HlsEvent.AUDIO_TRACKS_UPDATED, { audioTracks: altList });
|
||
//TODO fix require reset
|
||
}));
|
||
// Trigger SUBTITLE_TRACKS_UPDATED only when the HTML5 texttracks have been created.
|
||
// MatchPoint may query the available media option list and relate them to the HTML5 texttracks
|
||
const subPrefferedLevels$ = waitFor(mediaElementQuery.textTracksCreated$, (created) => created).pipe(switchMap(() => {
|
||
return rootPlaylistQuery.getPreferredMediaOptionsByType$(MediaOptionType.Subtitle).pipe(take(1), map((prefLevels) => {
|
||
const altList = prefLevels;
|
||
// Potentially large object log
|
||
// this.logger.info('Subtitle tracks updated %o', altList);
|
||
this.hls.trigger(HlsEvent.SUBTITLE_TRACKS_UPDATED, { subtitleTracks: altList });
|
||
this.logger.info('Subtitle tracks created');
|
||
this.hls.trigger(HlsEvent.SUBTITLE_TRACKS_CREATED);
|
||
//TODO fix require reset
|
||
}));
|
||
}));
|
||
return merge(variantSource$, audioSource$, subtitleSource$, sessionData$, prefferedLevels$, altPrefferedLevels$, subPrefferedLevels$, levelLoaded$, levelLoading$, firstAudioSwitch$);
|
||
}
|
||
mediaElementQueryListener(mediaElementQuery, rootPlaylistQuery) {
|
||
const seekSource$ = mediaElementQuery.seekTo$.pipe(map((seekTo) => {
|
||
var _a, _b;
|
||
if (seekTo && isFiniteNumber(seekTo.pos)) {
|
||
this.logger.debug(`Seeking pos:${seekTo.pos}`);
|
||
(_a = this.rtc) === null || _a === void 0 ? void 0 : _a.handleSeek('SEEKING');
|
||
this.hls.trigger(HlsEvent.SEEKING, { seekToPos: seekTo.pos });
|
||
}
|
||
else if (seekTo === null) {
|
||
(_b = this.rtc) === null || _b === void 0 ? void 0 : _b.handleSeek('SEEKED');
|
||
this.logger.debug('Seeked');
|
||
this.hls.trigger(HlsEvent.SEEKED);
|
||
}
|
||
}));
|
||
const desiredRateSource$ = mediaElementQuery.desiredRate$.pipe(startWith(0), pairwise(), map((pair) => {
|
||
var _a;
|
||
const oldRate = pair[0];
|
||
const newRate = pair[1];
|
||
if (isIframeRate(newRate)) {
|
||
if (this.iframeSwitchStart == 0) {
|
||
this.iframeSwitchStart = performance.now();
|
||
}
|
||
}
|
||
else {
|
||
this.iframeSwitchStart = 0;
|
||
}
|
||
this.logger.debug(`Rate changed oldRate: ${oldRate}, newRate: ${newRate}`);
|
||
this.hls.trigger(HlsEvent.DESIRED_RATE_CHANGED, { oldRate, newRate });
|
||
(_a = this.rtc) === null || _a === void 0 ? void 0 : _a.handleDesiredRateChanged(oldRate, newRate);
|
||
}));
|
||
const audioBufferSource$ = mediaElementQuery.sourceBufferEntityByType$(SourceBufferType.AltAudio).pipe(filter((buffer) => !!buffer), distinctUntilChanged((prev, cur) => prev.totalBytes === cur.totalBytes), map((buffer) => {
|
||
this.logger.trace('Audio Buffer Appended. Buffer= %o', buffer);
|
||
this.hls.trigger(HlsEvent.BUFFER_APPENDED);
|
||
}));
|
||
const variantBufferSource$ = mediaElementQuery.sourceBufferEntityByType$(SourceBufferType.Variant).pipe(filter((buffer) => !!buffer), distinctUntilChanged((prev, cur) => prev.totalBytes === cur.totalBytes), map((buffer) => {
|
||
var _a;
|
||
this.logger.trace('Variant Buffer Appended. Buffer= %o', buffer);
|
||
(_a = this.rtc) === null || _a === void 0 ? void 0 : _a.handleVariantBufferAppended(buffer.timestampOffset, buffer.totalBytes);
|
||
this.hls.trigger(HlsEvent.BUFFER_APPENDED);
|
||
}));
|
||
const stallInfo$ = mediaElementQuery.stallInfo$.pipe(filterNullOrUndefined(), withLatestFrom(mediaElementQuery.combinedBuffer$), map(([stallInfo, buffered]) => {
|
||
var _a;
|
||
this.logger.qe({ critical: true, name: 'stall', data: Object.assign(Object.assign({}, stallInfo), { buffered }) });
|
||
(_a = this.rtc) === null || _a === void 0 ? void 0 : _a.handleStalled(stallInfo, mediaElementQuery.getCombinedBufferInfo(stallInfo.currentTime, 0).len);
|
||
this.hls.trigger(HlsEvent.STALLED, stallInfo);
|
||
}));
|
||
const fragChangeMonitor$ = combineLatest([
|
||
of(rootPlaylistQuery),
|
||
combineQueries([mediaElementQuery.timeupdate$, mediaElementQuery.bufferedSegmentsByType$(SourceBufferType.Variant)]).pipe(throttleTime(1000), map(([currentTime, bufferedSegments]) => {
|
||
const playingFrag = bufferedSegments === null || bufferedSegments === void 0 ? void 0 : bufferedSegments.find((seg) => seg.startPTS <= currentTime && seg.endPTS > currentTime);
|
||
return playingFrag;
|
||
}), filter((playingFrag) => !!playingFrag), startWith(null), pairwise()),
|
||
]).pipe(switchMap(([rootPlaylistQuery, [a, b]]) => {
|
||
var _a;
|
||
const playingFrag = b === null || b === void 0 ? void 0 : b.frag;
|
||
const previousFrag = a === null || a === void 0 ? void 0 : a.frag;
|
||
if (playingFrag && !fragEqual(previousFrag, playingFrag)) {
|
||
this.hls.trigger(HlsEvent.FRAG_CHANGED, b);
|
||
if (this.hls.inGaplessMode) {
|
||
this.checkAndTriggerReadyForNext(mediaElementQuery, b);
|
||
}
|
||
if (!previousFrag || playingFrag.mediaOptionId !== previousFrag.mediaOptionId) {
|
||
const variantOption = rootPlaylistQuery.mediaOptionListQueries[MediaOptionType.Variant].mediaOptionFromId(playingFrag.mediaOptionId);
|
||
if (!variantOption) {
|
||
// TODO investigate this.. sometimes happens in gapless mode
|
||
this.logger.warn('variantInfo is undefined in fragChangeMonitor');
|
||
return EMPTY;
|
||
}
|
||
const oldVariant = previousFrag ? previousFrag.mediaOptionId : '';
|
||
const newVariant = playingFrag.mediaOptionId;
|
||
const data = {
|
||
oldVariant,
|
||
newVariant,
|
||
bitrate: variantOption.bitrate,
|
||
bandwidth: variantOption.bandwidth,
|
||
avgBandwidth: variantOption.avgBandwidth,
|
||
width: variantOption.width,
|
||
height: variantOption.height,
|
||
levelCodec: variantOption.levelCodec,
|
||
frameRate: variantOption.frameRate,
|
||
};
|
||
if (variantOption.iframes) {
|
||
const rate = mediaElementQuery.desiredRate;
|
||
const state = mediaElementQuery.isIframeRate;
|
||
const startupTime = performance.now() - this.iframeSwitchStart;
|
||
this.logger.qe({ critical: true, name: 'iframes', data: { state, rate, oldVariant, newVariant, startupTime } });
|
||
}
|
||
this.logger.qe({ critical: true, name: 'variantSwitched', data });
|
||
(_a = this.rtc) === null || _a === void 0 ? void 0 : _a.handleLevelSwitched({ url: variantOption.url, mediaOptionId: variantOption.mediaOptionId, oldVariant: oldVariant !== '' ? oldVariant : undefined, newVariant: newVariant });
|
||
this.hls.trigger(HlsEvent.LEVEL_SWITCHED, variantOption);
|
||
}
|
||
}
|
||
return EMPTY;
|
||
}));
|
||
const fullyBuffered$ = mediaElementQuery.isBufferedToEnd$(this.hls.config.maxBufferHole, false).pipe(filter((isBuffered) => isBuffered === true),
|
||
// Use asyncScheduler as the READY_FOR_NEXT_ITEM event could cause an item queue change
|
||
observeOn(asyncScheduler), map((isBuffered) => {
|
||
if (isBuffered && !this.hls.itemQueue.isPreloading() && this.hls.inGaplessMode) {
|
||
const combinedBuffer = mediaElementQuery.getCombinedBufferInfo(mediaElementQuery.currentTime, 0);
|
||
let duration = 0;
|
||
if (combinedBuffer) {
|
||
duration = combinedBuffer.end;
|
||
}
|
||
else {
|
||
this.logger.info('Combined Buffer is not ready, do not trigger READY_FOR_NEXT_ITEM');
|
||
return;
|
||
}
|
||
const mediaDuration = mediaElementQuery.mediaElementDuration;
|
||
// Only trigger READY_FOR_NEXT with less than 10 seconds to play. This is a Muze hack so that
|
||
// short songs are not skipped
|
||
if (duration > 0 && mediaDuration - mediaElementQuery.currentTime < 10) {
|
||
this.logger.info(`Trigger READY_FOR_NEXT_ITEM BufferDuration=${duration}, mediaDuration=${mediaDuration}`);
|
||
this.hls.trigger(HlsEvent.READY_FOR_NEXT_ITEM, { duration });
|
||
}
|
||
}
|
||
}));
|
||
return merge(seekSource$, desiredRateSource$, audioBufferSource$, variantBufferSource$, stallInfo$, fragChangeMonitor$, fullyBuffered$);
|
||
}
|
||
checkAndTriggerReadyForNext(mediaElementQuery, currentFrag) {
|
||
if (!currentFrag || !currentFrag.frag) {
|
||
return;
|
||
}
|
||
this.logger.trace('currentFrag %o', currentFrag);
|
||
const pos = mediaElementQuery.currentTime;
|
||
const combinedBuffer = mediaElementQuery.getCombinedBufferInfo(pos, 0);
|
||
if (!combinedBuffer) {
|
||
this.logger.info('Combined Buffer is not ready, do not trigger READY_FOR_NEXT_ITEM');
|
||
return;
|
||
}
|
||
const duration = combinedBuffer.end;
|
||
const mediaDuration = mediaElementQuery.mediaElementDuration;
|
||
const bufInfoMonitor = mediaElementQuery.bufferMonitorInfo;
|
||
// The closeToEndThreashold is typically half of the target duration. This differs from 2.0 that uses the full target duration.
|
||
// This leaves short time to buffer the next item. Good for audio but will not work for video.
|
||
// If we ever want to do gapless video we'll need to re-visit the using the fullyBuffered$ method above
|
||
const closeToEndThreshold = Math.max(bufInfoMonitor.almostDryWaterLevelSeconds, bufInfoMonitor.lowWaterLevelSeconds / 2);
|
||
if (mediaDuration - currentFrag.endPTS <= closeToEndThreshold || currentFrag.frag.isLastFragment) {
|
||
if (this.hls.inGaplessMode && !this.hls.isPreloading) {
|
||
this.logger.info(`Trigger READY_FOR_NEXT_ITEM BufferDuration=${duration}, mediaDuration=${mediaDuration}`);
|
||
this.hls.trigger(HlsEvent.READY_FOR_NEXT_ITEM, { duration });
|
||
}
|
||
}
|
||
}
|
||
loaderQueryListener(loaderQuery) {
|
||
const unresolvedUriLoadingSource$ = loaderQuery.unresolvedUriLoading$.pipe(map((entities) => {
|
||
return entities.map((entity) => {
|
||
this.logger.debug('Unresolved Uri Loading, data= %o', entity);
|
||
const data = { uri: entity.uri, responseType: entity.responseType, userAgent: entity.userAgent };
|
||
this.hls.trigger(HlsEvent.UNRESOLVED_URI_LOADING, data);
|
||
});
|
||
}));
|
||
return merge(unresolvedUriLoadingSource$);
|
||
}
|
||
triggerAudioSwitch(altOption) {
|
||
if (altOption) {
|
||
this.logger.info(`Audio track switched id: ${altOption.id} ${altOption.mediaOptionId}`);
|
||
this.logger.qe({ critical: true, name: 'audioTrackSwitched', data: { altOption } });
|
||
this.hls.trigger(HlsEvent.AUDIO_TRACK_SWITCHED, { id: altOption.id });
|
||
}
|
||
}
|
||
triggerManifestLoaded(loadRootMediaOptionsResult) {
|
||
var _a;
|
||
const payload = {
|
||
levels: loadRootMediaOptionsResult.rootMediaOptionsTuple[MediaOptionType.Variant],
|
||
audioTracks: loadRootMediaOptionsResult.rootMediaOptionsTuple[MediaOptionType.AltAudio],
|
||
subtitleTracks: loadRootMediaOptionsResult.rootMediaOptionsTuple[MediaOptionType.Subtitle],
|
||
url: loadRootMediaOptionsResult.baseUrl,
|
||
audioMediaSelectionGroup: loadRootMediaOptionsResult.audioMediaSelectionGroup,
|
||
subtitleMediaSelectionGroup: loadRootMediaOptionsResult.subtitleMediaSelectionGroup,
|
||
stats: loadRootMediaOptionsResult.stats,
|
||
isMediaPlaylist: loadRootMediaOptionsResult.isMediaPlaylist,
|
||
};
|
||
this.logger.debug('Manifest loaded');
|
||
this.logger.qe({ critical: true, name: 'manifestLoaded', data: { numLevels: (_a = payload.levels) === null || _a === void 0 ? void 0 : _a.length } });
|
||
this.hls.trigger(HlsEvent.MANIFEST_LOADED, payload);
|
||
}
|
||
triggerManifestParsed(rootQuery) {
|
||
var _a, _b;
|
||
// TODO Fix firstlevel, audio, video and altAudio
|
||
const manifestParsedData = {
|
||
levels: rootQuery.mediaOptionListQueries[MediaOptionType.Variant].filteredMediaOptionList,
|
||
firstLevel: 0,
|
||
audio: false,
|
||
video: true,
|
||
altAudio: false,
|
||
audioTracks: rootQuery.mediaOptionListQueries[MediaOptionType.AltAudio].filteredMediaOptionList,
|
||
audioMediaSelectionGroup: rootQuery.audioMediaSelectionGroup,
|
||
subtitleMediaSelectionGroup: rootQuery.subtitleMediaSelectionGroup,
|
||
stats: rootQuery.loadStats,
|
||
};
|
||
this.logger.debug('Manifest parsed %o', manifestParsedData);
|
||
this.logger.qe({ critical: true, name: 'manifestParsed', data: { numLevels: (_a = manifestParsedData.levels) === null || _a === void 0 ? void 0 : _a.length } });
|
||
(_b = this.rtc) === null || _b === void 0 ? void 0 : _b.handleManifestParsed(manifestParsedData);
|
||
this.hls.trigger(HlsEvent.MANIFEST_PARSED, manifestParsedData);
|
||
}
|
||
urlRedactedManifestLoaded(indata) {
|
||
const outdata = Object.assign({}, indata);
|
||
outdata.url = redactUrl(outdata.url);
|
||
outdata.levels = urlRedactedLevelInfo(outdata.levels);
|
||
outdata.audioTracks = urlRedactedAltMediaOption(outdata.audioTracks);
|
||
outdata.subtitleTracks = urlRedactedAltMediaOption(outdata.subtitleTracks);
|
||
return outdata;
|
||
}
|
||
urlRedactedManifestParsed(indata) {
|
||
const outdata = Object.assign({}, indata);
|
||
outdata.levels = urlRedactedLevelInfo(outdata.levels);
|
||
outdata.audioTracks = urlRedactedAltMediaOption(outdata.audioTracks);
|
||
return outdata;
|
||
}
|
||
}
|
||
|
||
var SwitchReason;
|
||
(function (SwitchReason) {
|
||
SwitchReason["LowBandwidth"] = "LowBandwidth";
|
||
SwitchReason["HighBandwidth"] = "HighBandwidth";
|
||
SwitchReason["PreferredListChanged"] = "PreferredListChanged";
|
||
SwitchReason["IframeModeChange"] = "IframeModeChange";
|
||
SwitchReason["None"] = "";
|
||
})(SwitchReason || (SwitchReason = {}));
|
||
|
||
const firstMediaOptionSelectionMetrics = {
|
||
minValidBitrate: 2000000,
|
||
maxValidBitrate: 5000000,
|
||
maxPreferredBitrate: 3000000,
|
||
minValidHeight: 480,
|
||
maxValidHeight: 720,
|
||
};
|
||
// Filter media options that are not compatible with the starting tier
|
||
// Currently we hit an issue when switching between audio codecs: ac-3 / ec-3 / mp4a.40.*
|
||
// and video codecs: dolby, hevc, avc
|
||
const isCompatible = (mediaOption1, mediaOption2) => {
|
||
let isVideoCompatible = true;
|
||
if (mediaOption1.videoCodec && mediaOption2.videoCodec) {
|
||
isVideoCompatible = MediaUtil.isCompatibleVideoCodec(mediaOption1.videoCodec, mediaOption2.videoCodec);
|
||
}
|
||
let isVideoRangeSame = false;
|
||
if (mediaOption1.videoRange && mediaOption2.videoRange) {
|
||
isVideoRangeSame = mediaOption1.videoRange == mediaOption2.videoRange;
|
||
}
|
||
else if (!mediaOption1.videoRange && !mediaOption2.videoRange) {
|
||
isVideoRangeSame = true;
|
||
}
|
||
let isAudioCompatible = true;
|
||
if (mediaOption1.audioCodec && mediaOption2.audioCodec) {
|
||
// Both have valid audio codecs, check with MediaUtil.isAudioCompatible
|
||
isAudioCompatible = MediaUtil.isCompatibleAudioCodec(mediaOption1.audioCodec, mediaOption2.audioCodec);
|
||
}
|
||
return isVideoCompatible && isVideoRangeSame && isAudioCompatible;
|
||
};
|
||
function filterMediaOptionsBasedOnFirstMediaOptions(mediaOptions, firstMediaOptionInfo) {
|
||
// Filter out again according to starting level, and save audioGroups of filtered levels if present
|
||
return mediaOptions.reduce((prev, cur) => {
|
||
const validMediaOption = isCompatible(firstMediaOptionInfo, cur);
|
||
if (validMediaOption) {
|
||
const audioGroup = cur.audioGroupId;
|
||
if (audioGroup) {
|
||
prev.audioGroups.add(audioGroup);
|
||
}
|
||
prev.mediaOptions.add(cur);
|
||
}
|
||
const subtitleGroup = cur.subtitleGroupId;
|
||
if (subtitleGroup) {
|
||
prev.subtitleGroups.add(subtitleGroup);
|
||
}
|
||
const closedCaptionGroup = cur.closedcaption;
|
||
if (closedCaptionGroup) {
|
||
prev.closedCaptionGroups.add(closedCaptionGroup);
|
||
}
|
||
return prev;
|
||
}, { mediaOptions: new Set(), audioGroups: new Set(), subtitleGroups: new Set(), closedCaptionGroups: new Set() });
|
||
}
|
||
function chooseFirstMediaOptionBasedOnScore(mediaOptions, bandwidthHistory, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate) {
|
||
const ranking = mediaOptions.reduce((prev, cur) => {
|
||
if (cur.iframes) {
|
||
return prev;
|
||
}
|
||
let result = prev;
|
||
const rank = getScoreRank(cur, bandwidthHistory, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate);
|
||
if (!prev || rank.isGreaterThan(prev.bestRank) || (rank.isEqualTo(prev.bestRank) && cur.bandwidth < prev.selected.bandwidth)) {
|
||
result = { selected: cur, bestRank: rank };
|
||
}
|
||
return result;
|
||
}, null);
|
||
return ranking.selected;
|
||
}
|
||
// Determine the highest ranking MediaOption to be used as first MediaOption (breaking ties by prioritizing the higher bitrate if two MediaOptions have the same rank)
|
||
function chooseFirstMediaOptionBasedOnRanking(mediaOptions, metrics, bandwidthHistory, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate) {
|
||
const ranking = mediaOptions.reduce((prev, cur) => {
|
||
if (cur.iframes) {
|
||
return prev;
|
||
}
|
||
let result = prev;
|
||
const rank = getMediaOptionRank(cur, metrics, bandwidthHistory, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate);
|
||
if (!prev || rank.isGreaterThan(prev.bestRank) || (rank.isEqualTo(prev.bestRank) && cur.bitrate > prev.selected.bitrate)) {
|
||
result = { selected: cur, bestRank: rank };
|
||
}
|
||
return result;
|
||
}, null);
|
||
return ranking.selected;
|
||
}
|
||
// A MediaOption SCORE ranking is determined by a multisort comparison of 2 properties in order of importance:
|
||
// (1) within bandwidth cap
|
||
// (2) SCORE (determined by the playlist)
|
||
function getScoreRank(mediaInfo, bandwidthHistory, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate) {
|
||
const score = mediaInfo.score;
|
||
const isWithinCap = bandwidthHistory &&
|
||
adaptiveStartupConfig &&
|
||
playlistEstimate &&
|
||
fragEstimate &&
|
||
!isWithinBandwidthCap(mediaInfo, bandwidthHistory, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate)
|
||
? MatchRanking.INVALID
|
||
: MatchRanking.VALID; // If no bandwidthHistory or adaptiveStartupConfig specified, then don't mark the media option as INVALID;
|
||
return new MediaOptionRank(isWithinCap, score);
|
||
}
|
||
// A MediaOption's ranking is determined by a multisort comparison on 8 properties in order of importance:
|
||
// (1) validMetrics (between 2-5 mbps, height between 480-720p)
|
||
// (2) videoRangeRank (PQ/HLG/SDR)
|
||
// (3) videoRank (video codec rank)
|
||
// (4) audioChannelRank (if available and supported, then a 5.1 ac-3 tier takes precedence over stereo ec-3 tier)
|
||
// (5) audioCodecRank (audio codec rank)
|
||
// (6) within bandwidth cap
|
||
// (7) preferred max bitrate (less than 3 mbps)
|
||
// (8) MediaOption's height
|
||
function getMediaOptionRank(mediaInfo, firstMediaOptionSelectionMetrics, bandwidthHistory, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate) {
|
||
const validMetrics = hasValidMetrics(mediaInfo.bitrate, mediaInfo.height, firstMediaOptionSelectionMetrics) ? MatchRanking.VALID : MatchRanking.INVALID;
|
||
const videoRangeRank = getVideoRangeRanking(mediaInfo.videoRange);
|
||
const { videoCodecRank, audioCodecRank } = getAudioVideoCodecRanks(mediaInfo);
|
||
const lessThanMaxPreferredBitrate = mediaInfo.bitrate < firstMediaOptionSelectionMetrics.maxPreferredBitrate ? MatchRanking.VALID : MatchRanking.INVALID;
|
||
const DEFAULT_AUDIO_CHANNEL_COUNT = 1;
|
||
const audioChannelCount = mediaInfo.audioChannelCount || DEFAULT_AUDIO_CHANNEL_COUNT; // use the channel count for ranking;
|
||
const isWithinCap = bandwidthHistory &&
|
||
adaptiveStartupConfig &&
|
||
playlistEstimate &&
|
||
fragEstimate &&
|
||
!isWithinBandwidthCap(mediaInfo, bandwidthHistory, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate)
|
||
? MatchRanking.INVALID
|
||
: MatchRanking.VALID; // If no bandwidthHistory or adaptiveStartupConfig specified, then don't mark the MediaOption as INVALID
|
||
return new MediaOptionRank(validMetrics, videoRangeRank, videoCodecRank, audioChannelCount, audioCodecRank, isWithinCap, lessThanMaxPreferredBitrate, mediaInfo.height);
|
||
}
|
||
function isWithinBandwidthCap(mediaInfo, bandwidthHistory, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate) {
|
||
const { targetDuration, targetStartupMs } = adaptiveStartupConfig;
|
||
const { avgPlaylistLoadTimeMs } = playlistEstimate;
|
||
const { avgParseTimeMs } = fragEstimate;
|
||
const { avgBufferCreateMs, avgInitFragAppendMs, avgDataFragAppendMs } = bufferEstimate;
|
||
const avgBufferTimeMs = avgInitFragAppendMs + avgDataFragAppendMs;
|
||
const { avgBandwidth, avgLatencyMs } = bandwidthHistory;
|
||
const fragCount = 1; // Math.ceil(targetDuration / maxDuration);
|
||
return (mediaInfo.bandwidth <= avgBandwidth && // has sufficient bandwidth
|
||
(((mediaInfo.avgBandwidth || mediaInfo.bandwidth) * targetDuration) / avgBandwidth) * 1000 +
|
||
avgPlaylistLoadTimeMs +
|
||
avgBufferCreateMs +
|
||
(avgLatencyMs + avgParseTimeMs + avgBufferTimeMs) * fragCount <=
|
||
targetStartupMs); // can be loaded within targetStartupMs
|
||
}
|
||
function onlyIFrames(mediaOptions) {
|
||
return mediaOptions.every((opt) => opt.iframes);
|
||
}
|
||
function hasValidMetrics(bitrate, height, metrics) {
|
||
const inRange = (input, minVal, maxVal) => (input - minVal) * (input - maxVal) <= 0;
|
||
return inRange(bitrate, metrics.minValidBitrate, metrics.maxValidBitrate) && inRange(height, metrics.minValidHeight, metrics.maxValidHeight);
|
||
}
|
||
/**
|
||
* Choose first MediaOption from MediaOption array
|
||
*
|
||
* @param mediaOptions Set of MediaOptions to choose from
|
||
* @param hasScoreAvailable If SCORE metric is available
|
||
* @param metrics Metrics to use for selection
|
||
* @param bandwidthHistory Bandwidth history
|
||
* @param adaptiveStartupConfig Override configs
|
||
*/
|
||
function chooseFirstMediaOption(mediaOptions, metrics, hasScoreAvailable, logger, bandwidthHistory, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate) {
|
||
let firstMediaOption;
|
||
if (!mediaOptions || mediaOptions.length < 1 || onlyIFrames(mediaOptions)) {
|
||
logger.warn('no non-iframe media option found');
|
||
return;
|
||
}
|
||
if (hasScoreAvailable) {
|
||
firstMediaOption = chooseFirstMediaOptionBasedOnScore(mediaOptions, bandwidthHistory, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate);
|
||
}
|
||
else {
|
||
firstMediaOption = chooseFirstMediaOptionBasedOnRanking(mediaOptions, metrics, bandwidthHistory, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate);
|
||
}
|
||
if (!firstMediaOption) {
|
||
logger.warn('no valid first media option found');
|
||
}
|
||
return firstMediaOption;
|
||
}
|
||
function filterRegularLevelsBasedOnCapOn1080p(levels) {
|
||
// standard 1080p resolution with a slight tolerance of 1.2
|
||
const PIXELS_CAP = 2488320;
|
||
const capedLevels = levels.filter((levelInfo) => !levelInfo.iframes && (!levelInfo.width || !levelInfo.height || levelInfo.width * levelInfo.height <= PIXELS_CAP));
|
||
return capedLevels;
|
||
}
|
||
|
||
/*
|
||
* simple ABR level switcher
|
||
* - compute next level based on last fragment bw heuristics
|
||
* - implement an abandon rules triggered if we have less than 2 frag buffered and if computed bw shows that we risk buffer stalling
|
||
*
|
||
* 2018 Apple Inc. All rights reserved.
|
||
*/
|
||
const abrLogName = { name: 'abr' };
|
||
function getMaxStarvationDelaySec(fragDuration, maxBufStarvationSec) {
|
||
return isFiniteNumber(fragDuration) ? Math.min(fragDuration, maxBufStarvationSec) : maxBufStarvationSec;
|
||
}
|
||
function instantBwTooSlow(avgBw, abrStatus, bwStatus) {
|
||
const instantBw = bwStatus.instantBw;
|
||
return abrStatus.fragDownloadSlow || abrStatus.fragDownloadTooSlow || (isFiniteNumber(instantBw) && instantBw < avgBw);
|
||
}
|
||
function gotSlowMedia(abrStatus) {
|
||
return abrStatus.fragDownloadSlow || abrStatus.fragDownloadTooSlow;
|
||
}
|
||
function hasReliableBandwidthEstimate(bandwidthEstimate) {
|
||
return isFiniteNumber(bandwidthEstimate === null || bandwidthEstimate === void 0 ? void 0 : bandwidthEstimate.avgBandwidth);
|
||
}
|
||
function nextAutoMediaOption(abrConfig, rootPlaylistQuery, rootPlaylistService, mediaLibraryQuery, mediaElementQuery, statsQuery, logger) {
|
||
let defaultAutoVariantOption = rootPlaylistQuery.nextMaxAutoOptionId;
|
||
// In 2.0 arch, There was check to see if the nextMaxAutoOptionId was
|
||
// in penalty box. Don't see a possibility of that, here in 2.1 as
|
||
// the nexMaxAutoOptionId is set from the preferrdList.
|
||
if (defaultAutoVariantOption !== NoMediaOption.mediaOptionId && !hasReliableBandwidthEstimate(statsQuery.getBandwidthEstimate())) {
|
||
if (mediaElementQuery.isIframeRate) {
|
||
// no group matching for iframe mode
|
||
return { variantMediaOption: defaultAutoVariantOption, holdOffDuration: 0, lowestCandidate: null };
|
||
}
|
||
else {
|
||
// defaultAutoVariantOption (rootPlaylistQuery.nextMaxAutoOptionId) gets set in likelyToStall.
|
||
// Even though it has matching altAudio and subtitle mediaOptions at that time.
|
||
// It might not at this moment due to penalized alternates.
|
||
// getBestMediaOptionTupleFromVariantAndPersistentId will look for a
|
||
// fallback variant that has matching alternates.
|
||
// If it comes up short, then continue to use the current variant and alternates.
|
||
const variantMediaOption = rootPlaylistQuery.variantMediaOptionById(defaultAutoVariantOption);
|
||
const subtitleAltOption = rootPlaylistQuery.enabledAlternateMediaOptionByType(MediaOptionType.Subtitle);
|
||
const audioAltOption = rootPlaylistQuery.enabledAlternateMediaOptionByType(MediaOptionType.AltAudio);
|
||
const audioPersistentId = audioAltOption === null || audioAltOption === void 0 ? void 0 : audioAltOption.persistentID;
|
||
const subtitlePersistentId = subtitleAltOption === null || subtitleAltOption === void 0 ? void 0 : subtitleAltOption.persistentID;
|
||
const sdrOnly = !rootPlaylistQuery.preferHDR;
|
||
const mediaOptionTuple = rootPlaylistService.getBestMediaOptionTupleFromVariantAndPersistentId(rootPlaylistQuery, variantMediaOption, audioPersistentId, subtitlePersistentId, undefined, [], sdrOnly, false, false);
|
||
logger.info(`capped at sdrOnly ${sdrOnly} ${JSON.stringify(defaultAutoVariantOption)} with matching alternates: ${JSON.stringify(mediaOptionTuple)}`);
|
||
if (rootPlaylistQuery.isValidMediaOptionTuple(mediaOptionTuple)) {
|
||
defaultAutoVariantOption = mediaOptionTuple[MediaOptionType.Variant].mediaOptionId;
|
||
const alternates = {
|
||
altAudio: mediaOptionTuple[MediaOptionType.AltAudio],
|
||
subtitle: mediaOptionTuple[MediaOptionType.Subtitle],
|
||
};
|
||
return { variantMediaOption: defaultAutoVariantOption, holdOffDuration: 0, alternates, lowestCandidate: null };
|
||
}
|
||
else {
|
||
logger.info(`No fallback variant with matching alternates for (capped) level ${defaultAutoVariantOption}: use currentVariant instead.`);
|
||
}
|
||
const currentVariant = rootPlaylistQuery.enabledMediaOptionKeys[MediaOptionType.Variant];
|
||
return { variantMediaOption: currentVariant.mediaOptionId, holdOffDuration: 0, lowestCandidate: null }; // by using currentVariant, switch will be skipped.
|
||
}
|
||
}
|
||
return nextABRAutoMediaOption(abrConfig, rootPlaylistQuery, mediaLibraryQuery, mediaElementQuery, statsQuery, logger);
|
||
}
|
||
function nextABRAutoMediaOption(abrConfig, rootPlaylistQuery, mediaLibraryQuery, mediaElementQuery, statsQuery, logger) {
|
||
const loggerChild = logger.child({ name: 'abr' });
|
||
const iframeMode = mediaElementQuery.isIframeRate;
|
||
const enabledVariantOptionId = rootPlaylistQuery.enabledMediaOptionIdByType(MediaOptionType.Variant);
|
||
const autoVariantOption = findNextABRAutoVariantOptionInMode(iframeMode, abrConfig, rootPlaylistQuery, mediaLibraryQuery, mediaElementQuery, statsQuery, loggerChild);
|
||
loggerChild.info(`nextABRAutoMediaOption ${enabledVariantOptionId}->${autoVariantOption.variantMediaOption}`);
|
||
return autoVariantOption;
|
||
}
|
||
function getABRPlaybackRate(mediaElementQuery) {
|
||
// playbackRate is the absolute value of the playback rate; if v.playbackRate is 0, we use 1 to load as
|
||
// if we're playing back at the normal rate.
|
||
return mediaElementQuery.playbackRate !== 0 ? Math.abs(mediaElementQuery.playbackRate) : 1;
|
||
}
|
||
// Scaled buffer ahead in seconds for video buffer
|
||
function getBufferAheadSec(mediaElementQuery, maxBufferHole) {
|
||
return mediaElementQuery.getCurrentWaterLevelByType(SourceBufferType.Variant, maxBufferHole) / getABRPlaybackRate(mediaElementQuery);
|
||
}
|
||
function findLowestValidVariantIndex(minAutoVariantIndex, availableVariantOptions, iframeMode, rootPlaylistQuery) {
|
||
if (iframeMode) {
|
||
return minAutoVariantIndex;
|
||
}
|
||
let lowestValidVariantId = -1;
|
||
if (minAutoVariantIndex < 0) {
|
||
return lowestValidVariantId;
|
||
}
|
||
for (let i = minAutoVariantIndex; i < availableVariantOptions.length; ++i) {
|
||
const variantMediaOption = availableVariantOptions[i];
|
||
const alternates = getMatchingAlternates(variantMediaOption, rootPlaylistQuery);
|
||
if (alternates.altAudio && alternates.subtitle) {
|
||
lowestValidVariantId = i;
|
||
break;
|
||
}
|
||
}
|
||
return lowestValidVariantId;
|
||
}
|
||
function findNextABRAutoVariantOptionInMode(iframeMode, abrConfig, rootPlaylistQuery, curOptionQuery, mediaElementQuery, statsQuery, logger) {
|
||
const variantOptions = rootPlaylistQuery.preferredMediaOptions[MediaOptionType.Variant].filter((variantOption) => variantOption.iframes === iframeMode);
|
||
if (!variantOptions.length) {
|
||
return { variantMediaOption: NoMediaOption.mediaOptionId, holdOffDuration: 0, lowestCandidate: null };
|
||
}
|
||
// Find minAutoVariant if it was set because of highBWTrigger
|
||
let minAutoVariantOption = 0;
|
||
const minAutoVariantOptionId = rootPlaylistQuery.nextMinAutoOptionId;
|
||
if (minAutoVariantOptionId !== NoMediaOption.mediaOptionId) {
|
||
const found = variantOptions.findIndex((variantOption) => variantOption.mediaOptionId === minAutoVariantOptionId);
|
||
if (found >= 0) {
|
||
minAutoVariantOption = found;
|
||
logger.info(`minAutoVariantOptionId set=${minAutoVariantOptionId}, minAutoVariantOption=${minAutoVariantOption}/${variantOptions.length}`);
|
||
}
|
||
}
|
||
// Ensure the minAutoVariant is valid
|
||
minAutoVariantOption = findLowestValidVariantIndex(minAutoVariantOption, variantOptions, iframeMode, rootPlaylistQuery);
|
||
if (minAutoVariantOption < 0) {
|
||
return { variantMediaOption: NoMediaOption.mediaOptionId, holdOffDuration: 0, lowestCandidate: null };
|
||
}
|
||
let maxAutoVariantOption = variantOptions.length - 1;
|
||
// if forced auto level has been defined, use it to cap ABR computed quality level
|
||
const maxAutoVariantOptionId = rootPlaylistQuery.nextMaxAutoOptionId;
|
||
if (maxAutoVariantOptionId !== NoMediaOption.mediaOptionId) {
|
||
const found = variantOptions.findIndex((variantOption) => variantOption.mediaOptionId === maxAutoVariantOptionId);
|
||
if (found >= 0) {
|
||
maxAutoVariantOption = found;
|
||
logger.info(`maxAutoVariantOptionId set=${maxAutoVariantOptionId}, maxAutoVariantOption=${maxAutoVariantOption}/${variantOptions.length}`);
|
||
}
|
||
}
|
||
const currentVariant = rootPlaylistQuery.variantMediaOptionById(curOptionQuery.mediaOptionId);
|
||
const currentVariantOptionDetails = curOptionQuery.mediaOptionDetails; // Get info even if in penalty box TODO check here
|
||
const bufferedDuration = (currentVariant === null || currentVariant === void 0 ? void 0 : currentVariant.iframes) !== iframeMode ? 0 : getBufferAheadSec(mediaElementQuery, abrConfig.maxBufferHole);
|
||
let bestLevelHeuristics, currentFragDuration;
|
||
if (!iframeMode) {
|
||
currentFragDuration = currentVariantOptionDetails ? currentVariantOptionDetails.targetduration : 0;
|
||
// TODO where is the current Frag and get duration, be careful not to use init segment
|
||
}
|
||
else {
|
||
const desiredIframeFPS = abrConfig.desiredIframeFPS;
|
||
currentFragDuration = currentVariantOptionDetails ? currentVariantOptionDetails.targetduration / desiredIframeFPS : 0;
|
||
// lower limit on iframe duration - dense tracks likely have a target duration that would achieve more than 8 fps if we displayed everything
|
||
currentFragDuration = Math.max(1 / desiredIframeFPS, currentFragDuration);
|
||
}
|
||
const bwUpFactor = abrConfig.abrBandWidthUpFactor;
|
||
const bwFactor = abrConfig.abrBandWidthFactor;
|
||
// First, look to see if we can find a level matching with our avg bandwidth AND that could also guarantee no rebuffering at all
|
||
bestLevelHeuristics = findBestVariantOption(variantOptions, currentFragDuration, minAutoVariantOption, maxAutoVariantOption, bufferedDuration, iframeMode, statsQuery.getCombinedEstimate(), statsQuery.bandwidthStatus, bwFactor, bwUpFactor, abrConfig, rootPlaylistQuery, curOptionQuery, mediaElementQuery, logger, true);
|
||
if (bestLevelHeuristics.variantMediaOption === NoMediaOption.mediaOptionId) {
|
||
logger.trace('rebuffering expected to happen, lets try to find a quality level minimizing the rebuffering');
|
||
// not possible to get rid of rebuffering ... let's try to find
|
||
// level that will guarantee less than maxStarvationDelay of
|
||
// rebuffering if no matching level found, logic will return 0
|
||
const maxStarvationDelay = getMaxStarvationDelaySec(currentFragDuration, abrConfig.maxStarvationDelay);
|
||
bestLevelHeuristics = findBestVariantOption(variantOptions, currentFragDuration, minAutoVariantOption, maxAutoVariantOption, bufferedDuration + maxStarvationDelay, iframeMode, statsQuery.getCombinedEstimate(), statsQuery.bandwidthStatus, bwFactor, bwUpFactor, abrConfig, rootPlaylistQuery, curOptionQuery, mediaElementQuery, logger, true);
|
||
if (bestLevelHeuristics.variantMediaOption === NoMediaOption.mediaOptionId && minAutoVariantOption >= 0) {
|
||
bestLevelHeuristics.variantMediaOption = variantOptions[minAutoVariantOption].mediaOptionId;
|
||
bestLevelHeuristics.alternates = iframeMode ? null : getMatchingAlternates(variantOptions[minAutoVariantOption], rootPlaylistQuery);
|
||
}
|
||
}
|
||
logger.debug(`[ findNextABRAutoVariantOptionInMode] Returning ${JSON.stringify(bestLevelHeuristics)}`);
|
||
return bestLevelHeuristics;
|
||
}
|
||
function getMatchingAlternates(variant, rootPlaylistQuery) {
|
||
const enabledMediaOptions = rootPlaylistQuery.enabledMediaOptionKeys;
|
||
const altAudioKey = enabledMediaOptions[MediaOptionType.AltAudio];
|
||
const altAudio = isEnabledMediaOption(altAudioKey) ? rootPlaylistQuery.mediaOptionListQueries[MediaOptionType.AltAudio].getMatchingAlternate(altAudioKey.mediaOptionId, variant) : NoMediaOption;
|
||
const subtitleKey = enabledMediaOptions[MediaOptionType.Subtitle];
|
||
const subtitle = isEnabledMediaOption(subtitleKey) ? rootPlaylistQuery.mediaOptionListQueries[MediaOptionType.Subtitle].getMatchingAlternate(subtitleKey.mediaOptionId, variant) : NoMediaOption;
|
||
return { altAudio, subtitle };
|
||
}
|
||
const minItemsInBandwidthAverage = 4;
|
||
const shortTermBandwidthFactor = 1.8;
|
||
/**
|
||
* @param variantOptionList The filtered list of candidate variants
|
||
*/
|
||
function findBestVariantOption(variantOptionList, currentFragDuration, minAutoVariant, maxAutoVariant, maxFetchDuration, iframeMode, estimate, bwStatus, bwFactor, bwUpFactor, abrConfig, rootPlaylistQuery, mediaLibraryQuery, mediaElementQuery, logger, dumpParams = false) {
|
||
if (abrConfig.abrBandwidthEstimator !== 'bandwidth-history-controller') {
|
||
logger.warn(`Unsupported configuration: ${abrConfig.abrBandwidthEstimator} for ABR bandwidth estimator`);
|
||
}
|
||
const bandwidthSamples = bwStatus.bandwidthSampleCount;
|
||
const abrStatus = rootPlaylistQuery.abrStatus;
|
||
const maxBufferSize = mediaElementQuery.maxBufferSize;
|
||
const minTargetDurations = abrConfig.minTargetDurations || 1;
|
||
const mediaOptionListInfo = rootPlaylistQuery.mediaOptionListQueries[MediaOptionType.Variant].mediaOptionListInfo;
|
||
const hasScore = mediaOptionListInfo.hasScore;
|
||
if (!variantOptionList.length) {
|
||
return { variantMediaOption: NoMediaOption.mediaOptionId, holdOffDuration: -1, lowestCandidate: null };
|
||
}
|
||
const enabledVariantOptionId = rootPlaylistQuery.enabledMediaOptionIdByType(MediaOptionType.Variant);
|
||
const enabledVariantOption = rootPlaylistQuery.variantMediaOptionById(enabledVariantOptionId);
|
||
const enabledVariantOptionIsValid = variantOptionList.find((variantOption) => variantOption.mediaOptionId === enabledVariantOptionId) != null;
|
||
const isSameMode = enabledVariantOption && enabledVariantOption.iframes === iframeMode;
|
||
const currentScore = enabledVariantOption && isSameMode ? enabledVariantOption.score : undefined;
|
||
const currentFrameRate = enabledVariantOption && isSameMode ? enabledVariantOption.frameRate : undefined;
|
||
const currentHeight = enabledVariantOption && isSameMode ? enabledVariantOption.height : undefined;
|
||
const bufferedDuration = isSameMode ? getBufferAheadSec(mediaElementQuery, abrConfig.maxBufferHole) : 0;
|
||
// Sanity capping
|
||
maxAutoVariant = Math.max(0, Math.min(variantOptionList.length - 1, maxAutoVariant));
|
||
minAutoVariant = Math.max(0, Math.min(variantOptionList.length - 1, minAutoVariant));
|
||
if (dumpParams) {
|
||
logger.debug(`[abr] findBestLevel params=${stringifyWithPrecision({
|
||
enabledVariantOptionId,
|
||
currentFragDuration,
|
||
estimate,
|
||
minAutoVariant,
|
||
maxAutoVariant,
|
||
bwFactor,
|
||
bwUpFactor,
|
||
maxFetchDuration,
|
||
iframeMode,
|
||
maxBufferSize,
|
||
bufferedDuration,
|
||
currentScore,
|
||
currentFrameRate,
|
||
currentHeight,
|
||
})}`);
|
||
}
|
||
const mediaOptionDetailsEntityRecord = mediaLibraryQuery.mediaOptionDetailsEntityRecord;
|
||
const adjustedBwObj = getAdjustedBW(estimate.avgBandwidth, bwUpFactor, bwFactor, bandwidthSamples, logger);
|
||
let lowestCandidate;
|
||
for (let i = maxAutoVariant; i >= minAutoVariant; i--) {
|
||
const variantOption = variantOptionList[i];
|
||
let variantOptionId = variantOption.mediaOptionId;
|
||
const variantOptionScore = variantOption.score;
|
||
const variantOptionDetails = mediaOptionDetailsEntityRecord && mediaOptionDetailsEntityRecord[variantOptionId] ? mediaOptionDetailsEntityRecord[variantOptionId].mediaOptionDetails : undefined;
|
||
const lastUpdateMs = mediaOptionDetailsEntityRecord && mediaOptionDetailsEntityRecord[variantOptionId] ? mediaOptionDetailsEntityRecord[variantOptionId].lastUpdateMillis : null;
|
||
const avgDuration = variantOptionDetails ? variantOptionDetails.totalduration / variantOptionDetails.fragments.length : currentFragDuration;
|
||
const isUpSwitch = enabledVariantOption != null && variantOption.bitrate > enabledVariantOption.bitrate;
|
||
const isDownSwitch = enabledVariantOption != null && variantOption.bitrate < enabledVariantOption.bitrate;
|
||
// don't switch up to a lower frame rate or switch down to a higher frame rate
|
||
const validFrameRate = !(currentFrameRate != null && ((isUpSwitch && variantOption.frameRate < currentFrameRate) || (isDownSwitch && variantOption.frameRate > currentFrameRate)));
|
||
// pick only ascending levels that have greater than/equal height than the current level
|
||
const validHeight = !(isUpSwitch && currentHeight != null && currentHeight > variantOption.height);
|
||
/*
|
||
Note: AMP playlists are set up in such a way that
|
||
- fragments are served from a primary host (e.g. Apple CDN, vod-ap1-aoc.tv.apple.com) and alternate host (e.g. Akamai, vod-ak-aoc.tv.apple.com)
|
||
- for both of these, the playlists are served from a single host (e.g. play.itunes.apple.com)
|
||
- the primary and alternate hosts are identified by a bitrate difference of 1 bit (primary bitrate > alternate bitrate by 1)
|
||
*/
|
||
// Don't use same bitrate on alternate CDN for downswitch.
|
||
const validCDN = !(isDownSwitch && (variantOption.bitrate === enabledVariantOption.bitrate - 1 || variantOption.bitrate === enabledVariantOption.bitrate + 1));
|
||
// if SCORE attribute is available, pick ascending levels that have greater/equal SCORE than the current level
|
||
// if SCORE attributes are the same, ignore levels that have higher bitrate than current selected level
|
||
const validScore = !(hasScore &&
|
||
isUpSwitch &&
|
||
currentScore != null &&
|
||
(variantOptionScore < currentScore || (variantOptionScore === currentScore && enabledVariantOption && variantOption.bitrate >= enabledVariantOption.bitrate)));
|
||
const validMode = variantOption.iframes === iframeMode;
|
||
if (!isFiniteNumber(avgDuration) || !validMode || !validFrameRate || !validHeight || !validCDN || !validScore) {
|
||
logger.debug(`variant ${variantOptionId} ineligible ${stringifyWithPrecision({ avgDuration, validMode, validFrameRate, validHeight, validCDN, validScore })}`);
|
||
continue;
|
||
}
|
||
const { adjustedbw, bitrate, fetchDuration, rejectLevelDueToPeakBW, canFitMultipleSegments, requireAlternates, alternates } = getVariantOptionMetrics(variantOption, variantOptionDetails, bufferedDuration, minTargetDurations, isUpSwitch, estimate, adjustedBwObj, currentFragDuration, maxBufferSize, lastUpdateMs, iframeMode, rootPlaylistQuery, logger);
|
||
if (dumpParams) {
|
||
logger.debug(`getVariantOptionMetrics in=${stringifyWithPrecision({
|
||
bufferedDuration,
|
||
minTargetDurations,
|
||
isUpSwitch,
|
||
estimate,
|
||
bwUpFactor,
|
||
bwFactor,
|
||
currentFragDuration,
|
||
maxBufferSize,
|
||
bandwidthSamples,
|
||
lastUpdateMs,
|
||
iframeMode,
|
||
})} out=${stringifyWithPrecision({ adjustedbw, bitrate, fetchDuration, rejectLevelDueToPeakBW, canFitMultipleSegments, requireAlternates })} alternates=${JSON.stringify(alternates)}`);
|
||
}
|
||
// lowestCandidate is also guaranteed to be delivering fragments from same host.
|
||
if (requireAlternates && Boolean(alternates)) {
|
||
lowestCandidate = variantOptionId;
|
||
}
|
||
// if adjusted bw is greater than level bitrate AND
|
||
if (adjustedbw > bitrate &&
|
||
canFitMultipleSegments &&
|
||
!rejectLevelDueToPeakBW &&
|
||
(!requireAlternates || Boolean(alternates)) &&
|
||
// fragment fetchDuration unknown OR fragment fetchDuration less than max allowed fetch duration, then this level matches
|
||
(iframeMode || !fetchDuration || fetchDuration < maxFetchDuration)) {
|
||
// as we are looping from highest to lowest, this will return the best achievable quality level
|
||
if (instantBwTooSlow(adjustedbw, abrStatus, bwStatus) && enabledVariantOptionIsValid && isSameMode) {
|
||
// low inst bw and a switchdown imminent, only switch up if we have enough buffer
|
||
if (bufferedDuration <= 2 * avgDuration && isUpSwitch) {
|
||
// do not switch up
|
||
logger.info(`Last fragment too slow, not switching up instantBw=${bwStatus.instantBw}, ${enabledVariantOptionId}->${variantOptionId}, ${stringifyWithPrecision({
|
||
bufferedDuration,
|
||
avgDuration,
|
||
isUpSwitch,
|
||
})}`);
|
||
variantOptionId = enabledVariantOptionId;
|
||
}
|
||
else if (isUpSwitch && bwUpFactor * bwStatus.instantBw < bitrate) {
|
||
// instantBw might be too slow for this level, but we could upswitch to a lower level
|
||
continue;
|
||
}
|
||
}
|
||
return { variantMediaOption: variantOptionId, holdOffDuration: fetchDuration, alternates: alternates, lowestCandidate: lowestCandidate };
|
||
}
|
||
}
|
||
// not enough time budget even with quality level 0 ... rebuffering might happen
|
||
return { variantMediaOption: NoMediaOption.mediaOptionId, holdOffDuration: -1, lowestCandidate: lowestCandidate };
|
||
}
|
||
// Get metrics used for switching
|
||
function getVariantOptionMetrics(variantOption, variantOptionDetails, bufferedDuration, minTargetDurations, isUpSwitch, estimate, adjustedBwObj, defaultFragDuration, maxBufferSize, lastUpdateMs, iframeMode, rootPlaylistQuery, logger) {
|
||
const adjustedbw = isUpSwitch ? adjustedBwObj.bwUp : adjustedBwObj.bwDown;
|
||
const bitrate = variantOption.bitrate;
|
||
// const live = levelDetails != null && levelDetails.live;
|
||
const avgDuration = variantOptionDetails ? variantOptionDetails.totalduration / variantOptionDetails.fragments.length : defaultFragDuration;
|
||
const playlistFetchMs = isFiniteNumber(estimate.avgPlaylistLoadTimeMs) ? estimate.avgPlaylistLoadTimeMs : estimate.avgLatencyMs;
|
||
const fetchDuration = calcFetchDuration(variantOption, variantOptionDetails, defaultFragDuration, adjustedbw, playlistFetchMs, lastUpdateMs, logger);
|
||
const peakBandwidth = variantOption.bandwidth;
|
||
// have atleast 2 target-durations buffered to switch if peak exceeds measured bandwidth
|
||
const rejectLevelDueToPeakBW = adjustedbw > bitrate && adjustedbw < peakBandwidth && bufferedDuration <= 2 * avgDuration;
|
||
const peakSegSizeBits = (peakBandwidth || bitrate || 0) * ((variantOptionDetails === null || variantOptionDetails === void 0 ? void 0 : variantOptionDetails.targetduration) || avgDuration);
|
||
// can fit at least this.minTargetDurations * targetDuration seconds into the buffer
|
||
const canFitMultipleSegments = (minTargetDurations * peakSegSizeBits) / 8 <= maxBufferSize;
|
||
// must have matching alternates
|
||
let alternates = null;
|
||
const requireAlternates = !iframeMode;
|
||
if (requireAlternates) {
|
||
alternates = getMatchingAlternates(variantOption, rootPlaylistQuery);
|
||
logger.debug(`${variantOption.mediaOptionId} matched alternate audio ${JSON.stringify(alternates.altAudio)} subtitle ${JSON.stringify(alternates.subtitle)}`);
|
||
if (!alternates.altAudio || !alternates.subtitle) {
|
||
logger.info(`no matching alternates ${alternates.altAudio} or ${alternates.subtitle}, skipping ${variantOption.mediaOptionId}`);
|
||
alternates = null;
|
||
}
|
||
}
|
||
return {
|
||
adjustedbw,
|
||
bitrate,
|
||
fetchDuration,
|
||
rejectLevelDueToPeakBW,
|
||
canFitMultipleSegments,
|
||
requireAlternates,
|
||
alternates,
|
||
};
|
||
}
|
||
function getAdjustedBW(bw, switchUpFactor, switchDownFactor, numBandwidthSamples, logger) {
|
||
let bwUp;
|
||
let bwDown;
|
||
if (numBandwidthSamples >= minItemsInBandwidthAverage) {
|
||
bwUp = bw * switchUpFactor;
|
||
bwDown = bw * switchDownFactor;
|
||
}
|
||
else {
|
||
bwUp = bwDown = bw / shortTermBandwidthFactor;
|
||
}
|
||
logger.trace(`[getAdjustedBW] numSamples: ${numBandwidthSamples}, minSamples: ${minItemsInBandwidthAverage} - returning up: ${bwUp}, down: ${bwDown}`);
|
||
return { bwUp, bwDown };
|
||
}
|
||
function calcFetchDuration(variantOption, variantOptionDetails, defaultFragDuration, bw, playlistFetchMs, lastUpdateMs, logger) {
|
||
const bitrate = variantOption.bitrate;
|
||
const avgDuration = variantOptionDetails ? variantOptionDetails.totalduration / variantOptionDetails.fragments.length : defaultFragDuration;
|
||
const avgSegSizeBits = bitrate * avgDuration;
|
||
let fetchDuration = avgSegSizeBits / bw;
|
||
// if live and !ptsKnown or outOfDate
|
||
if ((variantOptionDetails === null || variantOptionDetails === void 0 ? void 0 : variantOptionDetails.liveOrEvent) && (!variantOptionDetails.ptsKnown || likelyOutOfDate(variantOptionDetails.totalduration, playlistFetchMs, lastUpdateMs))) {
|
||
// live fetch duration could be up to two segments if it doesn't have time info
|
||
// First segment could be the wrong segment if we don't have PTS since we choose the middle frag
|
||
logger.debug(`PTS unknown / expired, using 2x fetchDuration for ${variantOption.mediaOptionId}`);
|
||
fetchDuration *= 2;
|
||
}
|
||
// if this is first update for this option or if live
|
||
if (isFiniteNumber(playlistFetchMs) && (!isFiniteNumber(lastUpdateMs) || (variantOptionDetails === null || variantOptionDetails === void 0 ? void 0 : variantOptionDetails.liveOrEvent))) {
|
||
fetchDuration += playlistFetchMs / 1000;
|
||
}
|
||
return fetchDuration;
|
||
}
|
||
function minSwitchBufferAheadSec(from, to, toDetails, currentFragDuration, abrStatus, statsQuery, abrConfig, lastUpdateMs, logger) {
|
||
const forcedAutoOptionId = abrStatus.nextMaxAutoOptionId;
|
||
// in case next auto level has been forced, and bw not available or not reliable, return false
|
||
if (forcedAutoOptionId !== NoMediaOption.mediaOptionId && !hasReliableBandwidthEstimate(statsQuery.getBandwidthEstimate())) {
|
||
return Number.POSITIVE_INFINITY;
|
||
}
|
||
const bw = getAdjustedBW(statsQuery.getBandwidthEstimate().avgBandwidth, abrConfig.abrBandWidthUpFactor, abrConfig.abrBandWidthFactor, statsQuery.bandwidthStatus.bandwidthSampleCount, logger);
|
||
const playlistFetchMs = isFiniteNumber(statsQuery.getPlaylistEstimate().avgPlaylistLoadTimeMs)
|
||
? statsQuery.getPlaylistEstimate().avgPlaylistLoadTimeMs
|
||
: statsQuery.getBandwidthEstimate().avgLatencyMs;
|
||
// How much buffer ahead required to switch
|
||
// duration from playhead pos that is necessary to stay at this level
|
||
const isUpSwitch = to.bitrate > from.bitrate;
|
||
const bwValue = isUpSwitch ? bw.bwUp : bw.bwDown;
|
||
if ((toDetails === null || toDetails === void 0 ? void 0 : toDetails.liveOrEvent) && (!toDetails.ptsKnown || likelyOutOfDate(toDetails.totalduration, playlistFetchMs, lastUpdateMs))) {
|
||
// Don't allow flush if we won't have overlapping window
|
||
return Number.POSITIVE_INFINITY;
|
||
}
|
||
return calcFetchDuration(to, toDetails, currentFragDuration, bwValue, playlistFetchMs, lastUpdateMs, logger);
|
||
}
|
||
function getLowestSuperiorBW(mediaOptionId, variantOptions) {
|
||
let lowestSuperiorBW = Infinity;
|
||
if (mediaOptionId === NoMediaOption.mediaOptionId) {
|
||
return lowestSuperiorBW;
|
||
}
|
||
const givenMediaOption = variantOptions.find((mediaOption) => mediaOption.mediaOptionId === mediaOptionId);
|
||
if (!givenMediaOption) {
|
||
return Infinity;
|
||
}
|
||
const iframeMode = givenMediaOption.iframes;
|
||
const bitrate = givenMediaOption.bitrate;
|
||
const frameRate = givenMediaOption.frameRate;
|
||
const nextMediaOption = variantOptions.find((mediaOption) => mediaOption.iframes === iframeMode && (frameRate === undefined || mediaOption.frameRate >= frameRate) && mediaOption.bitrate > bitrate);
|
||
if (nextMediaOption) {
|
||
lowestSuperiorBW = nextMediaOption.bitrate;
|
||
}
|
||
return lowestSuperiorBW;
|
||
}
|
||
/**
|
||
* Returns whether it's likely that the refreshed level will end up with a non-overlapping playlist (causing ptsKnown == false)
|
||
* @param totalDurationSecs Details about the level
|
||
* @param playlistFetchMs time in MS to get playlist refresh
|
||
* @param lastUpdateMillis timeStamp of the last update
|
||
*/
|
||
function likelyOutOfDate(totalDurationSecs, playlistFetchMs, lastUpdateMillis) {
|
||
playlistFetchMs = isFiniteNumber(playlistFetchMs) ? playlistFetchMs : 0;
|
||
return !isFiniteNumber(lastUpdateMillis) || performance.now() - lastUpdateMillis + playlistFetchMs > totalDurationSecs * 1000;
|
||
}
|
||
/**
|
||
* Choose option to resume at when coming out of trickplay
|
||
*/
|
||
function getTrickPlayMaxResumeOption(preIframeOptionId, config, logger, rootPlaylistQuery, statsQuery) {
|
||
const vQuery = rootPlaylistQuery.mediaOptionListQueries[MediaOptionType.Variant];
|
||
const vOptions = vQuery.preferredMediaOptionList;
|
||
const targetStartupMs = config.targetStartupMs;
|
||
const playlistEstimate = {
|
||
// statsQuery.playlistEstimate
|
||
avgPlaylistParseTimeMs: 0,
|
||
avgPlaylistLoadTimeMs: 0,
|
||
}; // <rdar://75273462> Do not use max values for this estimate
|
||
const fragEstimate = statsQuery.getFragEstimate();
|
||
const bufferEstimate = {
|
||
avgBufferCreateMs: 0,
|
||
avgInitFragAppendMs: 0,
|
||
avgDataFragAppendMs: 0,
|
||
};
|
||
const fragDuration = fragEstimate.maxDurationSec;
|
||
// <rdar://75273462> Use 0 latency since it is already wrapped into bw estimate(See BandwidthHistoryController.record). Change back if we use tload - tfirst instead
|
||
const bw = {
|
||
avgLatencyMs: 0,
|
||
avgBandwidth: statsQuery.getBandwidthEstimate().avgBandwidth,
|
||
};
|
||
const startConfig = {
|
||
targetDuration: fragDuration,
|
||
targetStartupMs: targetStartupMs,
|
||
metricsOverride: {
|
||
maxValidHeight: 0,
|
||
maxValidBitrate: 0,
|
||
maxPreferredBitrate: 0,
|
||
},
|
||
};
|
||
logger.info(`trickplay resume selection params=${stringifyWithPrecision({ startConfig, bw, playlistEstimate, fragEstimate, bufferEstimate })}`);
|
||
const regOptions = filterRegularLevelsBasedOnCapOn1080p(vOptions);
|
||
let resumeId = preIframeOptionId;
|
||
if (regOptions.length > 0) {
|
||
resumeId = regOptions[0].mediaOptionId;
|
||
}
|
||
const candidates = regOptions.filter((option) => isWithinBandwidthCap(option, bw, startConfig, playlistEstimate, fragEstimate, bufferEstimate));
|
||
logger.debug(`Picking resume level from validLevels/regLevels/resumeLevels ${vOptions.length}/${regOptions.length}/${candidates.length}`);
|
||
if (candidates.length > 0) {
|
||
resumeId = candidates[candidates.length - 1].mediaOptionId;
|
||
}
|
||
logger.info(`trickplay start/max resume:${preIframeOptionId}/${resumeId}`);
|
||
return resumeId;
|
||
}
|
||
function fragAbrStatusDidChange(a, b) {
|
||
return (a === null || a === void 0 ? void 0 : a.fragDownloadSlow) === (b === null || b === void 0 ? void 0 : b.fragDownloadSlow) && a.fragDownloadTooSlow === (b === null || b === void 0 ? void 0 : b.fragDownloadTooSlow);
|
||
}
|
||
/**
|
||
* @returns Returns whether we should abort the fragment and also updates AbrStatus
|
||
*/
|
||
function shouldAbortFrag(context) {
|
||
// Slow fragment download:
|
||
const { mediaSink, rootPlaylistQuery, rootPlaylistService } = context;
|
||
const mediaQuery = mediaSink.mediaQuery;
|
||
const logger = context.logger.child(abrLogName);
|
||
return merge(slowFragmentCheck(rootPlaylistQuery, mediaQuery, logger), likelyToStallCheck(context)).pipe(startWith({ fragDownloadSlow: false, fragDownloadTooSlow: false }), scan((curState, newState) => {
|
||
return {
|
||
fragDownloadSlow: curState.fragDownloadSlow || newState.fragDownloadSlow,
|
||
fragDownloadTooSlow: curState.fragDownloadTooSlow || newState.fragDownloadTooSlow,
|
||
};
|
||
}), distinctUntilChanged(fragAbrStatusDidChange), map((status) => {
|
||
logger.debug(`abrStatus update=${JSON.stringify(status)}`);
|
||
rootPlaylistService.setFragLoadSlow(rootPlaylistQuery.itemId, status);
|
||
return false;
|
||
}), catchError((err) => {
|
||
if (err instanceof FragmentAbortError) {
|
||
const wayTooSlow = { fragDownloadSlow: true, fragDownloadTooSlow: true };
|
||
rootPlaylistService.setFragLoadSlow(rootPlaylistQuery.itemId, wayTooSlow);
|
||
return of(true);
|
||
}
|
||
return throwError(err);
|
||
}));
|
||
}
|
||
function slowFragmentCheck(rootPlaylistQuery, mediaQuery, logger) {
|
||
return combineQueries([mediaQuery.fellBelowLowWater$, rootPlaylistQuery.getInFlightFragByType$(MediaOptionType.Variant)]).pipe(switchMap((value) => {
|
||
const [, inFlight] = value;
|
||
if (!hasValidSample(inFlight)) {
|
||
return EMPTY;
|
||
}
|
||
const elapsedMs = performance.now() - inFlight.bwSample.trequest;
|
||
const flowDeadline = kFlowMeasurementPeriodMS - elapsedMs;
|
||
const durationDeadline = inFlight.duration * 1000 - elapsedMs;
|
||
const timers$ = [VOID];
|
||
if (flowDeadline > 0) {
|
||
timers$.push(timer(flowDeadline));
|
||
}
|
||
if (durationDeadline > 0) {
|
||
timers$.push(timer(durationDeadline));
|
||
}
|
||
return merge(...timers$).pipe(mapTo(value));
|
||
}), scan((curState, [fellBelowLowWater, inFlight]) => {
|
||
const newState = Object.assign({}, curState);
|
||
if (fellBelowLowWater) {
|
||
newState.fragDownloadSlow = true;
|
||
}
|
||
return gotSlowFlow(inFlight, newState, rootPlaylistQuery, logger);
|
||
}, { fragDownloadSlow: false, fragDownloadTooSlow: false }), startWith({ fragDownloadSlow: false, fragDownloadTooSlow: false }), distinctUntilChanged(fragAbrStatusDidChange));
|
||
}
|
||
function hasValidSample(inFlight) {
|
||
var _a;
|
||
return (inFlight === null || inFlight === void 0 ? void 0 : inFlight.state) === 'loading' && isFiniteNumber((_a = inFlight.bwSample) === null || _a === void 0 ? void 0 : _a.trequest);
|
||
}
|
||
const kFlowMeasurementPeriodMS = 2000;
|
||
const kUnderflowAllowanceMS = 1000;
|
||
function gotSlowFlow(inFlight, curStatus, rootQuery, logger) {
|
||
let { fragDownloadSlow, fragDownloadTooSlow } = curStatus;
|
||
const variant = rootQuery.variantMediaOptionById(inFlight.mediaOptionId);
|
||
const bitrate = variant.bitrate;
|
||
const stats = inFlight.bwSample;
|
||
logger = logger.child(abrLogName);
|
||
const totalBytes = stats.total ? stats.total : Math.max(stats.loaded, Math.round((inFlight.duration * bitrate) / 8)), timeSpentMS = performance.now() - stats.tfirst, // time spent downloading the fragment
|
||
durationDownloadedMS = (stats.loaded * inFlight.duration * 1000) / totalBytes;
|
||
if (timeSpentMS >= kFlowMeasurementPeriodMS && timeSpentMS - durationDownloadedMS >= kUnderflowAllowanceMS) {
|
||
if (!fragDownloadSlow) {
|
||
logger.warn(`flow indicates low bandwidth, after time/duration behind real time: ${timeSpentMS}/${timeSpentMS - durationDownloadedMS}`);
|
||
}
|
||
fragDownloadSlow = true;
|
||
}
|
||
if (timeSpentMS >= inFlight.duration * 1000) {
|
||
if (!fragDownloadTooSlow) {
|
||
logger.warn(`too much time spent downloading fragment, likely to switch down ${timeSpentMS} > ${inFlight.duration * 1000}`);
|
||
}
|
||
fragDownloadTooSlow = true;
|
||
}
|
||
return { fragDownloadSlow, fragDownloadTooSlow };
|
||
}
|
||
function likelyToStallCheck(context) {
|
||
const mediaQuery = context.mediaSink.mediaQuery;
|
||
const { rootPlaylistQuery: rootQuery, config } = context;
|
||
return mediaQuery.desiredRate$
|
||
.pipe(switchMap((desiredRate) => {
|
||
// Don't emit if about to pause or if we are currently paused
|
||
if (desiredRate === 0) {
|
||
return EMPTY;
|
||
}
|
||
return combineQueries([
|
||
rootQuery.getInFlightFragByType$(MediaOptionType.Variant),
|
||
rootQuery.mediaOptionListQueries[MediaOptionType.Variant].preferredMediaOptionList$.pipe(map((list) => list.filter(isMatchingIframeLevel.bind(null, isIframeRate(desiredRate))))),
|
||
]);
|
||
}), throttleTime(100), switchMap((value) => {
|
||
const [inFlight, preferredList] = value;
|
||
// Make sure inFlight fragment in correct mode && not the lowest bitrate variant
|
||
if (!hasValidSample(inFlight) || preferredList.findIndex((o) => o.mediaOptionId === inFlight.mediaOptionId) <= 0) {
|
||
return EMPTY;
|
||
}
|
||
// Wait some min threshold for stable bitrate calculation then check every 100ms
|
||
const now = performance.now();
|
||
const elapsedMs = now - inFlight.bwSample.trequest;
|
||
const maxStarvationDelaySec = getMaxStarvationDelaySec(inFlight.duration, config.maxStarvationDelay);
|
||
const minThresholdMs = Math.min(maxStarvationDelaySec * 1000, (inFlight.duration * 500) / mediaQuery.playbackRate);
|
||
const delayMs = Math.max(0, minThresholdMs - elapsedMs);
|
||
return timer(delayMs, 100).pipe(mapTo(value));
|
||
}))
|
||
.pipe(scan((curStatus, [inFlight, preferredList]) => {
|
||
return likelyToStall(curStatus, inFlight, preferredList, context);
|
||
}, { fragDownloadSlow: false, fragDownloadTooSlow: false }), startWith({ fragDownloadSlow: false, fragDownloadTooSlow: false }), distinctUntilChanged(fragAbrStatusDidChange));
|
||
}
|
||
/**
|
||
* Determine whether we are going to stall based on the remaining time to download the inflight fragment and buffer then switch fragments if needed.
|
||
* Can initiate abort of inflight fragment and switch down through limiting the nexMaxAutoOptiondId to either a level that can download in time or the lowest option.
|
||
* Alternatively, can just initiate a switch down without abort by limiting nextMaxAutoOptionId to one level down if the current fragment can download quickly enough.
|
||
*/
|
||
function likelyToStall(curStatus, inFlight, preferredList, // filtered list in the correct mode
|
||
context) {
|
||
var _a, _b;
|
||
let { fragDownloadSlow, fragDownloadTooSlow } = curStatus;
|
||
const { config, rootPlaylistService, rootPlaylistQuery: rootQuery, mediaSink, statsService, mediaLibraryService } = context;
|
||
const logger = context.logger.child(abrLogName);
|
||
const mediaQuery = mediaSink.mediaQuery;
|
||
// Prevent switch down if we are not in play state
|
||
if (mediaQuery.paused) {
|
||
return curStatus;
|
||
}
|
||
const stats = inFlight.bwSample;
|
||
// likelyToStall not required to check until download has started
|
||
if (!isFiniteNumber(stats.tfirst)) {
|
||
return curStatus;
|
||
}
|
||
const now = performance.now();
|
||
const elapsedMs = now - stats.trequest;
|
||
const maxStarvationDelaySec = getMaxStarvationDelaySec(inFlight.duration, config.maxStarvationDelay);
|
||
const mediaOptionType = MediaOptionType.Variant;
|
||
const mediaOptionId = inFlight.mediaOptionId;
|
||
const curVariant = rootQuery.variantMediaOptionById(mediaOptionId);
|
||
const curLibQuery = mediaLibraryService.getQueryForOption(curVariant);
|
||
const variantBitrate = curVariant.bitrate;
|
||
const instantBw = Math.max(1, (stats.loaded * 8000) / elapsedMs); // instant load rate in bits per second
|
||
const totalBytes = isFiniteNumber(stats.total) ? stats.total : Math.max(stats.loaded, Math.round((inFlight.duration * variantBitrate) / 8)); // fragment size in bytes
|
||
const remainingTimeSec = ((totalBytes - stats.loaded) * 8) / instantBw; // how much longer until this fragment will be loaded in seconds
|
||
const bufferAheadSec = getBufferAheadSec(mediaQuery, config.maxBufferHole);
|
||
let maxTimeToLoadSec; // Max time to load the next fragment
|
||
if (isFiniteNumber(bufferAheadSec) && bufferAheadSec > 0 && !isFiniteNumber((_a = mediaQuery.seekTo) === null || _a === void 0 ? void 0 : _a.pos)) {
|
||
maxTimeToLoadSec = bufferAheadSec;
|
||
}
|
||
else {
|
||
// bufferAheadSec === 0 or NaN or seeking
|
||
const elapsedSec = elapsedMs / 1000;
|
||
if (elapsedSec < maxStarvationDelaySec) {
|
||
// Already starving. Try to complete load by maxStarvationDelaySec relative to current load
|
||
maxTimeToLoadSec = maxStarvationDelaySec - elapsedSec;
|
||
}
|
||
else {
|
||
// Could happen if we have high target duration. Start over with maxStarvationDelay
|
||
maxTimeToLoadSec = maxStarvationDelaySec;
|
||
}
|
||
}
|
||
const wasSlow = fragDownloadSlow; // To make logs less chatty
|
||
({ fragDownloadSlow, fragDownloadTooSlow } = gotSlowFlow(inFlight, curStatus, rootQuery, logger));
|
||
// consider emergency switch down only if we have less than 2 frag buffered AND
|
||
// time to finish loading current fragment is bigger than buffer starvation delay
|
||
// ie if we risk buffer starvation if bw does not increase quickly.
|
||
const minSwitchDuration = 2 * (((_b = curLibQuery.mediaOptionDetails) === null || _b === void 0 ? void 0 : _b.targetduration) || inFlight.duration);
|
||
const likelyToStall = bufferAheadSec <= minSwitchDuration && (remainingTimeSec >= maxTimeToLoadSec || fragDownloadSlow);
|
||
if (!likelyToStall) {
|
||
if (globalHlsService().getQuery().extendMaxTTFB) {
|
||
logger.info('[abr] re-enabling TTFB');
|
||
globalHlsService().setExtendMaxTTFB(0);
|
||
}
|
||
return { fragDownloadSlow, fragDownloadTooSlow };
|
||
}
|
||
if (!wasSlow) {
|
||
logger.warn(`likely to stall ${stringifyWithPrecision({
|
||
maxTimeToLoadSec,
|
||
minSwitchDuration,
|
||
stats,
|
||
elapsedMs,
|
||
remainingTimeSec,
|
||
instantBw,
|
||
bufferAheadSec,
|
||
fragDownloadSlow,
|
||
})}`);
|
||
}
|
||
fragDownloadSlow = true;
|
||
if (!globalHlsService().getQuery().extendMaxTTFB) {
|
||
logger.info('[abr] temporarily disabling TTFB');
|
||
globalHlsService().setExtendMaxTTFB(600000); // set to 10mins
|
||
}
|
||
// TODO: in native stack we only downswitch if our predictedBitrate < levelBitrate. However
|
||
// seems like we need to investigate if bandwidth estimate is reacting fast enough to bandwidth changes
|
||
let nextOptionId = undefined;
|
||
// Find something that will take less than bufferStarvationDelay (avoids rebuffering)
|
||
const itemId = inFlight.itemId;
|
||
const statsQuery = statsService.getQueryForItem(itemId);
|
||
const avgEstimate = statsQuery.getCombinedEstimate();
|
||
const instantEstimate = Object.assign(Object.assign({}, avgEstimate), { avgBandwidth: instantBw });
|
||
const bwStatus = statsQuery.bandwidthStatus;
|
||
const bwFactor = 1;
|
||
const bwUpFactor = 1;
|
||
const iframeMode = curVariant.iframes;
|
||
// We should probably switch down either way after this fragment, but whether we abort depends on whether we can avoid rebuffering
|
||
// <rdar://87250808> Never abort in iframe mode. It kills the av-pipe when handleVariantSwitch does not switch
|
||
const shouldAbort = remainingTimeSec >= maxTimeToLoadSec && !iframeMode;
|
||
// Find lowest index with matching group or 0 if none
|
||
const minLevelIdx = findLowestValidVariantIndex(0, preferredList, iframeMode, rootQuery);
|
||
// There is no variant with audio/subtitle group matching, so there is nothing to switch to
|
||
if (minLevelIdx < 0) {
|
||
logger.info('findLowestValidVariantIndex could not find a valid level');
|
||
return { fragDownloadSlow, fragDownloadTooSlow };
|
||
}
|
||
// Set max level to current level or the lowest valid level, whichever is greater
|
||
const maxLevelIdx = Math.max(minLevelIdx, preferredList.findIndex((v) => v && v.mediaOptionId === curVariant.mediaOptionId));
|
||
if (shouldAbort) {
|
||
// We will stall if we do not abort and switch down
|
||
// Check if we can find a variant that will load within maxTimeToLoadSec
|
||
let res = findBestVariantOption(preferredList, inFlight.duration, minLevelIdx, maxLevelIdx, maxTimeToLoadSec, iframeMode, instantEstimate, bwStatus, bwFactor, bwUpFactor, config, rootQuery, curLibQuery, mediaQuery, logger);
|
||
const badOptionId = NoMediaOption.mediaOptionId;
|
||
if (res.variantMediaOption !== badOptionId) {
|
||
logger.info(`buffered: ${bufferAheadSec} got option ${res.variantMediaOption} that will load in conditions ${stringifyWithPrecision({ maxTimeToLoadSec, instantBw })}`);
|
||
nextOptionId = res.variantMediaOption;
|
||
}
|
||
else if ((res = findBestVariantOption(preferredList, inFlight.duration, minLevelIdx, maxLevelIdx, remainingTimeSec, iframeMode, instantEstimate, bwStatus, bwFactor, bwUpFactor, config, rootQuery, curLibQuery, mediaQuery, logger)).variantMediaOption !== badOptionId) {
|
||
// Found variant that will load before current frag is done
|
||
nextOptionId = res.variantMediaOption;
|
||
logger.info(`buffered: ${bufferAheadSec} got option ${nextOptionId} that will load in conditions ${stringifyWithPrecision({ remainingTimeSec, instantBw })}`);
|
||
}
|
||
else {
|
||
// Waiting for current frag to finish will result in rebuffering, fall back to minimum option and abort
|
||
nextOptionId = res.lowestCandidate;
|
||
logger.info(`buffered: ${bufferAheadSec} got fallback option ${nextOptionId}, could not find variant that will load in conditions ${stringifyWithPrecision({ remainingTimeSec, instantBw })}`);
|
||
}
|
||
}
|
||
else {
|
||
// Can finish current frag download and then switch down by one level, matching alternates.
|
||
// Reverse preferredList in range [minLevelIdx, maxLevelIdx-1] inclusive then look for the lowest idx with valid variant audio/subtitle matching.
|
||
// This will return -1 on failure, maxLevelIdx-1 if iframeMode, or the offset from the tail of the highest idx below maxLevelIdx with matching
|
||
const offsetFromTail = findLowestValidVariantIndex(0, preferredList.slice(minLevelIdx, maxLevelIdx).reverse(), iframeMode, rootQuery);
|
||
const nextMatchingGroupIdx = maxLevelIdx - 1 - offsetFromTail; // maxLevelIdx-1 since slice(start, end) excludes the end idx
|
||
// Cap to next highest level with audio/subtitle group matching. If there is no lower group, stay at the currently enabled option and log it.
|
||
if (offsetFromTail >= 0 || maxLevelIdx === minLevelIdx) {
|
||
logger.info('cap to next highest level with audio/subtitle matching');
|
||
nextOptionId = preferredList[nextMatchingGroupIdx].mediaOptionId;
|
||
}
|
||
else {
|
||
logger.info(`no lower level with matching audio/subtitle to switch to level=${curVariant.mediaOptionId}`);
|
||
}
|
||
}
|
||
if (nextOptionId != null && nextOptionId !== rootQuery.abrStatus.nextMaxAutoOptionId) {
|
||
logger.info(`capping level to ${nextOptionId} because likelyToStall`);
|
||
rootPlaylistService.setNextMaxAutoOptionId(itemId, nextOptionId);
|
||
}
|
||
// only emergency switch down if it takes less time to load new fragment at lowest level instead
|
||
// of finishing loading current one ...
|
||
if (shouldAbort) {
|
||
logger.warn(`loading too slow, abort fragment loading and switch to level ${nextOptionId}`);
|
||
logger.debug(`abort stats=${stringifyWithPrecision({
|
||
instantBw,
|
||
loadedBytes: stats.loaded,
|
||
totalBytes,
|
||
elapsedMs,
|
||
remainingTimeSec,
|
||
maxTimeToLoadSec,
|
||
fragDownloadSlow,
|
||
})}`);
|
||
// force next load level in auto mode
|
||
// update bw estimate for this fragment before cancelling load (this will help reducing the bw)
|
||
logger.info(`[${MediaOptionNames[mediaOptionType]}] fragLoadingProcessingMs/stats.loaded: ${elapsedMs}/${stats.loaded}`); // also log when too slow
|
||
statsService.setBandwidthSample(Object.assign(Object.assign({}, stats), { tfirst: stats.tfirst || now, tload: stats.tload || now, complete: true, mediaOptionType: mediaOptionType }));
|
||
fragDownloadTooSlow = true;
|
||
// Abort current sequence
|
||
throw new FragmentAbortError({ mediaOptionType, mediaOptionId }, nextOptionId, ErrorResponses.FragmentAbortError);
|
||
}
|
||
return { fragDownloadSlow, fragDownloadTooSlow };
|
||
}
|
||
function initializeAbrStatus(mediaOptionId, variantOptions) {
|
||
const highBWTrigger = getLowestSuperiorBW(mediaOptionId, variantOptions);
|
||
return {
|
||
fragDownloadSlow: false,
|
||
fragDownloadTooSlow: false,
|
||
nextMinAutoOptionId: NoMediaOption.mediaOptionId,
|
||
nextMaxAutoOptionId: NoMediaOption.mediaOptionId,
|
||
highBWTrigger: highBWTrigger,
|
||
};
|
||
}
|
||
/**
|
||
* @brief Use ABR logic to switch VariantMediaOption if needed
|
||
*
|
||
* We should not switch levels unless we hit certain triggers:
|
||
* 1. Bandwidth High / Low change
|
||
* 2. Crossing low water level threshold
|
||
* 3. Hitting end of buffer
|
||
* 4. Slow fragment or level loading
|
||
* 5. iframe mode change
|
||
* 6. gapless item transition
|
||
* Exceptions are when we are at a discontinuity or seeking, when we should only switch if we have
|
||
* slow media (timeout) current level was sent to penalty box.
|
||
* When switching variant, handleVariantSwitch will ensure altAudio and subtitle mediaOptions follow
|
||
* the same groups specified by the chosen variant.
|
||
*/
|
||
function handleVariantSwitch(originalReason, config, rootQuery, mediaElementQuery, rootService) {
|
||
var _a, _b;
|
||
const logger = rootService.logger.child(abrLogName);
|
||
const iframeMode = mediaElementQuery.isIframeRate;
|
||
const optionListQuery = rootQuery.mediaOptionListQueries[MediaOptionType.Variant];
|
||
const preferredList = optionListQuery.preferredMediaOptionList;
|
||
const curVariant = rootQuery.enabledMediaOptionKeys[MediaOptionType.Variant];
|
||
let switchReason = originalReason;
|
||
if (switchReason === SwitchReason.None && !preferredList.some((option) => option.mediaOptionId === curVariant.mediaOptionId)) {
|
||
switchReason = SwitchReason.PreferredListChanged;
|
||
logger.info(`overriding switchReason: ${originalReason}->${switchReason}`);
|
||
}
|
||
const shouldSwitchVariant = switchReason !== SwitchReason.None;
|
||
if (!shouldSwitchVariant) {
|
||
logger.info(`Skipping variant switch as switchReason: ${switchReason}`);
|
||
return false;
|
||
}
|
||
if (iframeMode && switchReason !== SwitchReason.IframeModeChange) {
|
||
const bufferedIframes = mediaElementQuery.getBufferedSegmentsByType(SourceBufferType.Variant).filter((seg) => seg.frag.iframe);
|
||
if (bufferedIframes.length < config.minFramesBeforeSwitchingLevel) {
|
||
logger.info(`[iframes] only ${bufferedIframes.length} iframes, stay at current mediaOption ${curVariant.mediaOptionId}`);
|
||
// TODO: <rdar://87250808> Force needData$. If fragment was aborted and water level is almost/dry, av-pipe will die. <@robert-walch 3/8/2022>
|
||
return false;
|
||
}
|
||
}
|
||
if (iframeMode && switchReason === SwitchReason.IframeModeChange) {
|
||
rootService.setEnabledVariantMediaOptionIdBeforeTrickplaySwitch(rootQuery.itemId, curVariant.mediaOptionId);
|
||
}
|
||
const statsQuery = createStatsQuery(rootQuery.itemId);
|
||
const mediaLibraryQuery = createMediaLibraryQuery(curVariant);
|
||
const newOptions = [NoMediaOption, NoMediaOption, NoMediaOption];
|
||
// rdar://85517745 (Resume from trickplay level calculation needs functional parity with 2.15)
|
||
// On leaving trickplay, set the nextMaxAutioOptionId, this will in turn be used in nextAutoMediaOption
|
||
if (!iframeMode && originalReason === SwitchReason.IframeModeChange) {
|
||
const maxResumeOption = getTrickPlayMaxResumeOption(rootQuery.enabledVariantMediaOptionIdBeforeTrickplaySwitch, config, logger, rootQuery, statsQuery);
|
||
logger.info(`Capping variant selection to ${maxResumeOption} because switchReason: ${originalReason}`);
|
||
rootService.setNextMaxAutoOptionId(rootQuery.itemId, maxResumeOption);
|
||
rootService.setEnabledVariantMediaOptionIdBeforeTrickplaySwitch(rootQuery.itemId, undefined);
|
||
}
|
||
const nextVariant = nextAutoMediaOption(config, rootQuery, rootService, mediaLibraryQuery, mediaElementQuery, statsQuery, logger);
|
||
if (!iframeMode && originalReason === SwitchReason.IframeModeChange) {
|
||
// reset nextMaxAutioOptionId that was set above
|
||
rootService.setNextMaxAutoOptionId(rootQuery.itemId, NoMediaOption.mediaOptionId);
|
||
}
|
||
if (nextVariant.variantMediaOption === curVariant.mediaOptionId) {
|
||
logger.info(`Skipping variant switch as no better candidate than ${curVariant.mediaOptionId}`);
|
||
// TODO: <rdar://87250808> Force needData$. If fragment was aborted and water level is almost/dry, av-pipe will die. <@robert-walch 3/8/2022>
|
||
return false;
|
||
}
|
||
logger.qe({ critical: true, name: 'shouldSwitchVariant', data: { value: shouldSwitchVariant, reason: switchReason } });
|
||
newOptions[MediaOptionType.Variant] = { itemId: rootQuery.itemId, mediaOptionId: nextVariant.variantMediaOption };
|
||
for (const i of [MediaOptionType.AltAudio, MediaOptionType.Subtitle]) {
|
||
const curOptionId = rootQuery.enabledMediaOptionIdByType(i);
|
||
if (curOptionId !== NoMediaOption.mediaOptionId) {
|
||
// newAlternate may be null for iframe, NoMediaOption, or reuse existing variant (no switch): reuse existing alternates
|
||
const newAlternate = i === MediaOptionType.AltAudio ? (_a = nextVariant.alternates) === null || _a === void 0 ? void 0 : _a.altAudio : (_b = nextVariant.alternates) === null || _b === void 0 ? void 0 : _b.subtitle;
|
||
newOptions[i] = newAlternate ? newAlternate : { itemId: rootQuery.itemId, mediaOptionId: curOptionId };
|
||
}
|
||
}
|
||
rootService.setEnabledMediaOptions(rootQuery.itemId, newOptions);
|
||
logger.info(`Switching variant ${switchReason}: ${curVariant.mediaOptionId} variant/audio/subtitle ${newOptions[0].mediaOptionId}/${newOptions[1].mediaOptionId}/${newOptions[2].mediaOptionId}`);
|
||
return true;
|
||
}
|
||
/**
|
||
* @returns whether we should check for downswitch
|
||
*/
|
||
function gotLowBw(rootQuery, mediaElementQuery, rootService) {
|
||
var _a;
|
||
const logger = rootService.logger.child(abrLogName);
|
||
const abrStatus = rootQuery.abrStatus;
|
||
const slowMedia = gotSlowMedia(abrStatus);
|
||
const seeking = isFiniteNumber((_a = mediaElementQuery.seekTo) === null || _a === void 0 ? void 0 : _a.pos);
|
||
if (slowMedia && !abrStatus.fragDownloadTooSlow && seeking) {
|
||
logger.warn('could be ignoring low bandwidth due to seek');
|
||
return false;
|
||
}
|
||
return slowMedia;
|
||
}
|
||
/**
|
||
* @returns whether we should check for upswitch
|
||
*/
|
||
function gotHighBw(config, rootQuery, rootService) {
|
||
var _a;
|
||
const logger = rootService.logger;
|
||
const statsQuery = createStatsQuery(rootQuery.itemId);
|
||
const bwe = statsQuery.getBandwidthEstimate();
|
||
const abrStatus = rootQuery.abrStatus;
|
||
if (!hasReliableBandwidthEstimate(bwe)) {
|
||
return false;
|
||
}
|
||
const bandwidthSampleCount = ((_a = statsQuery.bandwidthStatus) === null || _a === void 0 ? void 0 : _a.bandwidthSampleCount) || 0;
|
||
const { bwUp } = getAdjustedBW(bwe.avgBandwidth, config.abrBandWidthUpFactor, config.abrBandWidthFactor, bandwidthSampleCount, logger);
|
||
if (bwUp > abrStatus.highBWTrigger) {
|
||
logger.info(`[abr] bandwidth high ${Math.round(bwe.avgBandwidth / 1000)}kbps trigger=${abrStatus.highBWTrigger / 1000}kbps factor=${config.abrBandWidthUpFactor}`);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
const loggerName$2 = { name: 'iframes' };
|
||
var PrefetchResult;
|
||
(function (PrefetchResult) {
|
||
PrefetchResult[PrefetchResult["DISABLED"] = 0] = "DISABLED";
|
||
PrefetchResult[PrefetchResult["ERRORED"] = 1] = "ERRORED";
|
||
PrefetchResult[PrefetchResult["SUCCESS"] = 2] = "SUCCESS";
|
||
})(PrefetchResult || (PrefetchResult = {}));
|
||
const checkForIframePrefetch = (context) => {
|
||
const { config, logger } = context;
|
||
if (!config.enableIFramePreloading) {
|
||
logger.info(loggerName$2, 'Iframe prefetch disabled');
|
||
return of(PrefetchResult.DISABLED);
|
||
}
|
||
return waitTillAllowedPrefetch(context);
|
||
};
|
||
function waitTillAllowedPrefetch(context) {
|
||
const { mediaSink, rootPlaylistQuery, mediaLibraryService, logger } = context;
|
||
const { mediaQuery } = mediaSink;
|
||
return combineQueries([mediaQuery.desiredRate$, mediaQuery.waterLevelChangedForType$(SourceBufferType.Variant)]).pipe(
|
||
// startup: don't subscribe to root playlist query until needed
|
||
switchMap(([desiredRate, waterLevel]) => {
|
||
if (isIframeRate(desiredRate) || waterLevel !== BufferWaterLevel.AboveHighWater) {
|
||
return EMPTY;
|
||
}
|
||
return rootPlaylistQuery.enabledMediaOptionByType$(MediaOptionType.Variant).pipe(map((enabledVariantMediaOption) => {
|
||
var _a, _b;
|
||
const variantQuery = rootPlaylistQuery.mediaOptionListQueries[MediaOptionType.Variant];
|
||
const hasIframes = variantQuery.hasIframes;
|
||
const libQuery = mediaLibraryService.getQuery();
|
||
const isLiveOrEvent = (_b = (_a = libQuery.getEntity(rootPlaylistQuery.itemId)) === null || _a === void 0 ? void 0 : _a.liveOrEvent) !== null && _b !== void 0 ? _b : false;
|
||
if (!hasIframes || isLiveOrEvent) {
|
||
logger.info(loggerName$2, `Skipping iframe prefetch hasIframes=${hasIframes}, liveOrEvent=${isLiveOrEvent}`);
|
||
return PrefetchResult.DISABLED;
|
||
}
|
||
return enabledVariantMediaOption;
|
||
}));
|
||
}), take(1), exhaustMap((variantOption) => beginPrefetch(variantOption, context)), finalize$1(() => {
|
||
logger.trace(loggerName$2, 'finalizing iframe prefetch');
|
||
}));
|
||
}
|
||
function beginPrefetch(variantOption, context) {
|
||
const { logger } = context;
|
||
if (variantOption === PrefetchResult.DISABLED) {
|
||
return of(variantOption);
|
||
}
|
||
return prefetchPlaylist(variantOption, context).pipe(exhaustMap((mediaOptionDetails) => prefetchInitSegmentAndKey(mediaOptionDetails, context)), mapTo(PrefetchResult.SUCCESS), catchError((err) => {
|
||
logger.error(loggerName$2, `got error ${err.message} in prefetch`);
|
||
return of(PrefetchResult.ERRORED);
|
||
}));
|
||
}
|
||
function prefetchPlaylist(variantOption, context) {
|
||
const { rootPlaylistQuery, logger, config, mediaSink, statsService } = context;
|
||
const { mediaQuery } = mediaSink;
|
||
const statsQuery = statsService.getQueryForItem(rootPlaylistQuery.itemId);
|
||
const mediaLibraryQuery = createMediaLibraryQuery(variantOption);
|
||
const iframeMediaChoice = findNextABRAutoVariantOptionInMode(true, config, rootPlaylistQuery, mediaLibraryQuery, mediaQuery, statsQuery, logger);
|
||
const iframeMediaOption = rootPlaylistQuery.variantMediaOptionById(iframeMediaChoice.variantMediaOption);
|
||
logger.info(loggerName$2, `prefetching variant ${iframeMediaChoice.variantMediaOption}`);
|
||
return retrieveMediaOptionDetails(context, iframeMediaOption, true);
|
||
}
|
||
function prefetchInitSegmentAndKey(mediaOptionDetails, context) {
|
||
var _a;
|
||
const { logger, mediaSink, rootPlaylistQuery } = context;
|
||
const { mediaQuery } = mediaSink;
|
||
const fragResult = findFragment(mediaQuery.currentTime, rootPlaylistQuery.discoSeqNum, 0, mediaOptionDetails, []);
|
||
if (!((_a = fragResult === null || fragResult === void 0 ? void 0 : fragResult.foundFrag) === null || _a === void 0 ? void 0 : _a.mediaFragment)) {
|
||
return throwError('Unable to find fragment for iframe prefetch');
|
||
}
|
||
const mediaFragment = fragResult.foundFrag.mediaFragment;
|
||
const prefetchKey$ = loadKey(context, mediaFragment.keyTagInfo, { itemId: mediaFragment.itemId, mediaOptionId: mediaFragment.mediaOptionId });
|
||
const prefetchInitSeg$ = retrieveInitSegmentCacheEntity(context, mediaFragment);
|
||
return forkJoin([prefetchKey$, prefetchInitSeg$]).pipe(tap(() => logger.info(loggerName$2, `prefetched variant ${mediaOptionDetails.mediaOptionId}`)), mapTo(PrefetchResult.SUCCESS));
|
||
}
|
||
function checkForIframeAutoPause(context) {
|
||
const { config, logger, iframeMachine, mediaSink } = context;
|
||
const { mediaQuery } = mediaSink;
|
||
return mediaQuery.desiredRate$.pipe(switchMap((rate) => {
|
||
if (!isIframeRate(rate)) {
|
||
return EMPTY;
|
||
}
|
||
const period = Math.abs(1000 / rate);
|
||
return timer(0, period).pipe(map(() => {
|
||
let result = null;
|
||
const seekable = mediaQuery.seekable;
|
||
if (!iframeMachine.isStarted || seekable.length < 1) {
|
||
return result;
|
||
}
|
||
const iframeReferenceClockTime = iframeMachine.iframeClockTimeSeconds;
|
||
const { leftMediaTimeToAutoPause } = config;
|
||
const minTime = seekable.start(0);
|
||
const maxTime = seekable.end(seekable.length - 1);
|
||
if (rate > 1 && maxTime - iframeReferenceClockTime < leftMediaTimeToAutoPause) {
|
||
logger.info({ name: 'iframes' }, `near the end of media, pausing normal playback, maxTime ${maxTime}, ifrct ${iframeReferenceClockTime}`);
|
||
result = { newRate: 0, postFlushSeek: maxTime - leftMediaTimeToAutoPause };
|
||
iframeMachine.pause();
|
||
}
|
||
else if (rate < 0 && iframeReferenceClockTime - minTime < rate / -2) {
|
||
logger.info({ name: 'iframes' }, `near the start of media, resuming playback, minTime ${minTime}, ifrct: ${iframeReferenceClockTime}`);
|
||
result = { newRate: 1, postFlushSeek: minTime };
|
||
}
|
||
return result;
|
||
}), filterNullOrUndefined(), tap(({ newRate, postFlushSeek }) => {
|
||
mediaSink.postFlushSeek = postFlushSeek;
|
||
mediaSink.desiredRate = newRate;
|
||
}), switchMapTo(EMPTY));
|
||
}));
|
||
}
|
||
function desiredRateChange(context) {
|
||
const mediaQuery = context.mediaSink.mediaQuery;
|
||
return combineLatest([of(context), mediaQuery.desiredRate$.pipe(pairwise())]).pipe(switchMap(([context, [oldRate, newRate]]) => {
|
||
const { rootPlaylistQuery, rootPlaylistService, config, logger, mediaSink, mediaLibraryService, statsService } = context;
|
||
const mediaQuery = mediaSink.mediaQuery;
|
||
const wasIframeRate = isIframeRate(oldRate);
|
||
const iframeRate = isIframeRate(newRate);
|
||
if (wasIframeRate !== iframeRate) {
|
||
logger.qe({ critical: true, name: 'iframes', data: { state: iframeRate, prevRate: oldRate, newRate } });
|
||
handleVariantSwitch(SwitchReason.IframeModeChange, config, rootPlaylistQuery, mediaQuery, rootPlaylistService);
|
||
}
|
||
else if (oldRate === 0 && newRate === 1) {
|
||
const canPlayWithoutGap = AVMediaOptionTypes.every((type) => {
|
||
var _a;
|
||
const enabledOption = rootPlaylistQuery.enabledMediaOptionKeys[type];
|
||
const libQuery = mediaLibraryService.getQueryForOption(enabledOption);
|
||
const statsQuery = statsService.getQueryForItem(rootPlaylistQuery.itemId);
|
||
const entity = libQuery.mediaOptionDetailsEntity;
|
||
return (!((_a = entity === null || entity === void 0 ? void 0 : entity.mediaOptionDetails) === null || _a === void 0 ? void 0 : _a.ptsKnown) ||
|
||
mediaQuery.canContinuePlaybackWithoutGap(entity.mediaOptionDetails, entity.lastUpdateMillis, statsQuery.getPlaylistEstimate(), config.maxBufferHole));
|
||
});
|
||
if (!canPlayWithoutGap) {
|
||
logger.info('flush due to live gap [0, Infinity]');
|
||
mediaSink.pause();
|
||
return mediaSink.flushAll(0, Infinity, true);
|
||
}
|
||
}
|
||
return EMPTY;
|
||
}), switchMapTo(EMPTY));
|
||
}
|
||
function capToEnabledIframeOption(context) {
|
||
const rootQuery = context.rootPlaylistQuery;
|
||
const mediaQuery = context.mediaSink.mediaQuery;
|
||
const enabledOption$ = rootQuery.enabledMediaOptionByType$(MediaOptionType.Variant);
|
||
return combineLatest([of(context), mediaQuery.desiredRate$.pipe(pairwise())]).pipe(
|
||
// distinct until desiredRate changed
|
||
distinctUntilChanged((prev, cur) => prev[1] === cur[1]), withLatestFrom(enabledOption$), // Startup: don't subscribe to root playlist query until needed
|
||
switchMap(([[context, [oldRate, newRate]], enabledMediaOption]) => {
|
||
const wasIframeRate = isIframeRate(oldRate);
|
||
const iframeRate = isIframeRate(newRate);
|
||
// Early return if trickplay was not toggled
|
||
if (wasIframeRate === iframeRate) {
|
||
return EMPTY;
|
||
}
|
||
const { rootPlaylistService, logger } = context;
|
||
if (iframeRate) {
|
||
// Check if level uncapped
|
||
if (context.rootPlaylistQuery.nextMaxAutoOptionId === NoMediaOption.mediaOptionId) {
|
||
// Attach to rootPlaylistQuery so it lasts throughout trickplay
|
||
rootPlaylistService.setNextMaxAutoOptionId(context.rootPlaylistQuery.itemId, enabledMediaOption.mediaOptionId);
|
||
logger.info(`Capped level to ${enabledMediaOption.mediaOptionId} on entering trickplay`);
|
||
}
|
||
}
|
||
return EMPTY;
|
||
}));
|
||
}
|
||
|
||
class QueueItemQuery extends QueryEntity {
|
||
}
|
||
const queueItemStore = new EntityStore({}, { name: 'item-queue', producerFn: produce_1, idKey: 'itemId', resettable: true });
|
||
const queueItemQuery = new QueueItemQuery(queueItemStore);
|
||
/**
|
||
* @brief The playback queue
|
||
*/
|
||
class ItemQueue {
|
||
constructor() {
|
||
this.firstItem = true;
|
||
this.playingEntity = null;
|
||
this.loadingEntity = null;
|
||
}
|
||
static createItem(name, url, initialSeekTime = NaN, platformInfo, serviceName) {
|
||
const today = new Date();
|
||
const createTime = `${today.getHours()}:${today.getMinutes()}:${today.getSeconds()}`;
|
||
// The MediaFragment URI start time we get from URL will take precedence over:
|
||
// hls.config.startPosition > TIME-OFFSET in playlist > initialSeekTime parameter in this function.
|
||
const logger = getLogger();
|
||
const timeOffset = getTimeOffsetParameter(url);
|
||
logger.info(`timeOffset parsed from URL ${timeOffset}`);
|
||
if (isFiniteNumber(timeOffset)) {
|
||
initialSeekTime = timeOffset;
|
||
}
|
||
else {
|
||
const hlsConfig = getCurrentConfig();
|
||
if (isFiniteNumber(hlsConfig === null || hlsConfig === void 0 ? void 0 : hlsConfig.startPosition)) {
|
||
// override only if finite value
|
||
initialSeekTime = hlsConfig.startPosition;
|
||
logger.info(`override initialSeekTime with startPosition ${initialSeekTime}`);
|
||
}
|
||
}
|
||
const queueItem = {
|
||
itemId: `${name}_${createTime}`,
|
||
name,
|
||
url,
|
||
serviceName,
|
||
createTime,
|
||
initialSeekTime,
|
||
itemStartOffset: 0,
|
||
platformInfo,
|
||
config: {},
|
||
};
|
||
return queueItem;
|
||
}
|
||
/**
|
||
* @returns obervable for whenever active item changes
|
||
*/
|
||
get activeItemById$() {
|
||
return queueItemQuery.selectActiveId().pipe(map((_) => queueItemQuery.getActive()));
|
||
}
|
||
get removedItems$() {
|
||
return queueItemQuery.selectEntityAction(EntityActions.Remove).pipe(map((ids) => ids));
|
||
}
|
||
/**
|
||
* @returns the active item. The active item is the currently downloading item
|
||
* which could be different from the playing item.
|
||
*/
|
||
get activeItem() {
|
||
return queueItemQuery.getActive();
|
||
}
|
||
/**
|
||
* @returns the entire play queue
|
||
*/
|
||
get queueItems$() {
|
||
return queueItemQuery.selectAll().pipe(map((queueItems) => queueItems !== null && queueItems !== void 0 ? queueItems : []));
|
||
}
|
||
/*
|
||
* @returns whether this is the first item
|
||
*/
|
||
get isFirstItem() {
|
||
return this.firstItem;
|
||
}
|
||
get playingItem() {
|
||
return this.playingEntity;
|
||
}
|
||
get loadingItem() {
|
||
return this.loadingEntity;
|
||
}
|
||
/**
|
||
* Add item to the queue and makes it active but keeps the former active item.
|
||
*
|
||
* @param name Identifier for the item
|
||
* @param url URL for the root playlist
|
||
*/
|
||
addQueueItem(name, url, initialSeekTime, platformInfo, itemStartOffset, serviceName) {
|
||
// TODO: Kola to investigate not exposing itemStartOffset in the public API of addQueueItem
|
||
queueItemQuery.getCount();
|
||
const queueItem = ItemQueue.createItem(name, url, initialSeekTime, platformInfo, serviceName);
|
||
if (this.playingEntity != null) {
|
||
// clear initialSeekTime if it's not the playing item
|
||
queueItem.initialSeekTime = undefined;
|
||
}
|
||
if (itemStartOffset) {
|
||
queueItem.itemStartOffset = itemStartOffset;
|
||
getLogger().debug(`itemStartOffset=${queueItem.itemStartOffset}`);
|
||
this.firstItem = false;
|
||
// set playing and loading item ids
|
||
this.playingEntity = this.activeItem;
|
||
this.loadingEntity = queueItem;
|
||
}
|
||
logAction(`queue.add.item: ${name}`);
|
||
applyTransaction(() => {
|
||
queueItemStore.add(queueItem);
|
||
queueItemStore.setActive(queueItem.itemId);
|
||
});
|
||
}
|
||
/**
|
||
* Update the playing item id from loading item id and clear loading item id
|
||
*/
|
||
updatePlayingItemId() {
|
||
this.playingEntity = this.loadingEntity;
|
||
this.loadingEntity = null;
|
||
this.clearAllButActive();
|
||
}
|
||
/**
|
||
* Reset loading item
|
||
*/
|
||
resetLoadingItem() {
|
||
this.removeQueueItem(this.loadingEntity.itemId);
|
||
this.loadingEntity = null;
|
||
applyTransaction(() => {
|
||
queueItemStore.setActive(this.playingEntity.itemId);
|
||
});
|
||
}
|
||
/**
|
||
* Returns whether a track is currently preloading but not yet playing
|
||
*/
|
||
isPreloading() {
|
||
// If both loading and playing items are not undefined then a track is currently loading
|
||
return this.playingEntity !== null && this.loadingEntity !== null;
|
||
}
|
||
setQueueItem(name, url, initialSeekTime, platformInfo, serviceName) {
|
||
logAction('queue.set.item');
|
||
this.loadingEntity = null;
|
||
applyTransaction(() => {
|
||
queueItemStore.reset();
|
||
const queueItem = ItemQueue.createItem(name, url, initialSeekTime, platformInfo, serviceName);
|
||
queueItemStore.add(queueItem);
|
||
queueItemStore.setActive(queueItem.itemId);
|
||
});
|
||
this.playingEntity = this.activeItem;
|
||
}
|
||
/**
|
||
* Remove an item from the queue
|
||
*/
|
||
removeQueueItem(itemId) {
|
||
queueItemStore.remove(itemId);
|
||
}
|
||
/**
|
||
* Clear the queue
|
||
*/
|
||
clearQueue() {
|
||
queueItemStore.reset();
|
||
}
|
||
/**
|
||
* Clear all items but the active item
|
||
*/
|
||
clearAllButActive() {
|
||
var _a;
|
||
const activeId = (_a = this.activeItem) === null || _a === void 0 ? void 0 : _a.itemId;
|
||
applyTransaction(() => {
|
||
const items = queueItemQuery.getAll();
|
||
items.forEach((item) => {
|
||
if (item.itemId !== activeId) {
|
||
queueItemStore.remove(item.itemId);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
set earlyAudioSelection(audioPersistentId) {
|
||
queueItemStore.updateActive((activeItem) => {
|
||
if (!activeItem.earlySelection) {
|
||
activeItem.earlySelection = {};
|
||
}
|
||
activeItem.earlySelection.audioPersistentId = audioPersistentId;
|
||
});
|
||
}
|
||
get earlyAudioSelection() {
|
||
var _a;
|
||
return (_a = this.activeItem.earlySelection) === null || _a === void 0 ? void 0 : _a.audioPersistentId;
|
||
}
|
||
set earlySubtitleSelection(subtitlePersistentId) {
|
||
queueItemStore.updateActive((activeItem) => {
|
||
if (!activeItem.earlySelection) {
|
||
activeItem.earlySelection = {};
|
||
}
|
||
activeItem.earlySelection.subtitlePersistentId = subtitlePersistentId;
|
||
});
|
||
}
|
||
get earlySubtitleSelection() {
|
||
var _a;
|
||
return (_a = this.activeItem.earlySelection) === null || _a === void 0 ? void 0 : _a.subtitlePersistentId;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handle MSE related errors: sourcebuffer creation, appending, etc.
|
||
*/
|
||
// Append error handling
|
||
function addAppendErrorHandlingPolicy(mediaSink, sbAdapter, mediaOptionType, mediaOptionId, targetDuration, config, rootQuery, rootService, meQuery) {
|
||
return (source) => source.pipe(tap(() => {
|
||
rootService.updateConsecutiveTimeouts(rootQuery.itemId, mediaOptionType, false, 'append');
|
||
}), retryWhen((errors) => errors.pipe(mergeMap((err, retryCount) => {
|
||
const isTimeout = err instanceof AppendBufferError && err.isTimeout;
|
||
rootService.updateConsecutiveTimeouts(rootQuery.itemId, mediaOptionType, isTimeout, 'append');
|
||
if (isTimeout) {
|
||
return handleAppendTimeout(err, retryCount, config, mediaOptionType, mediaOptionId, meQuery, rootQuery, rootService);
|
||
}
|
||
if (err instanceof BufferFullError) {
|
||
return handleBufferFullError(err, sbAdapter, retryCount, targetDuration, config, mediaOptionType, mediaOptionId, meQuery, rootQuery, rootService);
|
||
}
|
||
if (err instanceof MediaDecodeError) {
|
||
const { mediaOptionType, mediaOptionId } = err;
|
||
return handleFormatError(err, mediaOptionType, mediaOptionId, mediaSink, rootService, rootQuery);
|
||
}
|
||
throw err;
|
||
}))));
|
||
}
|
||
function handleBufferFullError(error, sb, retryCount, targetDuration, config, mediaOptionType, mediaOptionId, meQuery, rootQuery, rootService) {
|
||
const sbType = sb.type;
|
||
const currentWaterLevel = meQuery.getCurrentWaterLevelByType(sbType, config.maxBufferHole); // Always use real position for buffer full errors
|
||
const mediaBuffered = currentWaterLevel >= config.almostDryBufferSec;
|
||
// <rdar://87250808> Ejecting media from the buffer in iframe-mode will allow trick-play to recover when the forward audio buffer is full
|
||
// TODO: This should be considered for normal playback. Retries buy time, but they don't help reduce buffer usage.
|
||
// Flushing back and forward buffer is a better long-term solution for reclaiming memory.
|
||
if (mediaBuffered && !meQuery.isIframeRate) {
|
||
// this.logger.debug(`${self} bufferFullError waterLevel:${currentWaterLevel} targetDur:${this.targetDuration}`);
|
||
const retryDelayMs = targetDuration * 1000;
|
||
const action = {
|
||
errorAction: NetworkErrorAction.RetryRequest,
|
||
errorActionFlags: 0,
|
||
};
|
||
if (currentWaterLevel * 1000 < retryDelayMs) {
|
||
if (rootService.hasFallbackMediaOptionTuple(rootQuery, mediaOptionType, mediaOptionId, false)) {
|
||
// Put into penalty box if we might run out of buffer before we can append again
|
||
action.errorAction = NetworkErrorAction.SendAlternateToPenaltyBox;
|
||
}
|
||
else {
|
||
// can only fit 1 target duration so likely to stall. Probably pointless to keep going
|
||
// this.logger.warn(`${self} buffer too small to sustain playback @${pos.toFixed(3)} buffered:${TimeRangesExt.toString(this.buffered)}`);
|
||
action.errorAction = NetworkErrorAction.SendEndCallback;
|
||
}
|
||
}
|
||
// this.logger.info(`${self} handleBufferFullError errorAction:${errorAction} ${retryDelayMs}ms`);
|
||
const retryConfig = {
|
||
retryDelayMs,
|
||
maxNumRetry: Infinity,
|
||
maxRetryDelayMs: retryDelayMs,
|
||
};
|
||
return handleErrorWithRetry(error, retryCount, retryConfig, action, rootQuery, rootService, mediaOptionType, mediaOptionId);
|
||
}
|
||
else if (retryCount < config.appendErrorMaxRetry) {
|
||
// current position is not buffered, but browser is still complaining about buffer full error
|
||
// this happens on IE/Edge, refer to https://github.com/video-dev/hls.js/pull/708
|
||
// in that case flush the whole buffer to recover and retry
|
||
// this.logger.warn(`${self} buffer full and not buffered. flush everything and retry`);
|
||
return sb.remove(0, Number.POSITIVE_INFINITY);
|
||
}
|
||
else {
|
||
error.fatal = true;
|
||
}
|
||
return throwError(error); // Rethrow
|
||
}
|
||
function handleAppendTimeout(err, retryCount, config, mediaOptionType, mediaOptionId, meQuery, rootQuery, rootService) {
|
||
// Should be rare. If append timeout occurs possibly there is some platform or media issue so safer to
|
||
// reset and switch
|
||
let errorAction = {
|
||
errorAction: NetworkErrorAction.SendAlternateToPenaltyBox,
|
||
errorActionFlags: 0,
|
||
};
|
||
const waterLevel = meQuery.getCurrentWaterLevel(config.maxBufferHole);
|
||
const bufferEmpty = waterLevel < config.almostDryBufferSec;
|
||
let retryDelayMs = NaN;
|
||
const maxAppendFails = config.appendErrorMaxRetry;
|
||
const appendTimeouts = rootQuery.rootPlaylistEntity.errorsByType[mediaOptionType].timeouts.append;
|
||
if ((bufferEmpty && appendTimeouts >= maxAppendFails) || retryCount >= maxAppendFails) {
|
||
errorAction.errorAction = NetworkErrorAction.SendEndCallback;
|
||
}
|
||
else {
|
||
retryDelayMs = waterLevel * 1000; // should only trigger on last variant
|
||
}
|
||
const retryConfig = {
|
||
retryDelayMs,
|
||
maxNumRetry: maxAppendFails,
|
||
maxRetryDelayMs: retryDelayMs,
|
||
};
|
||
errorAction = modifyErrorActionIfCurrentLevelIsLastValidLevel(errorAction, false, err.response.code, mediaOptionId, mediaOptionType, rootQuery, rootService);
|
||
return handleErrorWithRetry(err, retryCount, retryConfig, errorAction, rootQuery, rootService, mediaOptionType, mediaOptionId)
|
||
.pipe();
|
||
}
|
||
function handleFormatError(err, mediaOptionType, mediaOptionId, mediaSink, rootService, rootQuery) {
|
||
let action = {
|
||
errorAction: NetworkErrorAction.RemoveAlternatePermanently,
|
||
errorActionFlags: 0,
|
||
};
|
||
action = modifyErrorActionIfCurrentLevelIsLastValidLevel(action, false, err.response.code, mediaOptionId, mediaOptionType, rootQuery, rootService);
|
||
// No retry
|
||
return handleErrorActionCommon(err, 0, action, rootQuery, rootService, mediaOptionType, mediaOptionId).pipe(catchError((err) => {
|
||
if (err.fatal === false) {
|
||
mediaSink.resetMediaSource();
|
||
}
|
||
throw err;
|
||
}));
|
||
}
|
||
// Handle CreateSourceBufferError. For combined audio video append, handle the error here so that
|
||
// reset does not destroy the other SourceBuffer too early
|
||
function addCreateSourceBufferErrorHandlingPolicy(mediaSink, rootService, rootQuery) {
|
||
return (source$) => source$.pipe(catchError((err) => {
|
||
if (err instanceof CreateSourceBufferError) {
|
||
const { mediaOptionType, mediaOptionId } = err;
|
||
return handleFormatError(err, mediaOptionType, mediaOptionId, mediaSink, rootService, rootQuery);
|
||
}
|
||
throw err;
|
||
}));
|
||
}
|
||
|
||
class FragmentPicker {
|
||
constructor(logger, _rootPlaylistService, _rootQuery, _mediaQuery, _iframeMachine, config) {
|
||
this.logger = logger;
|
||
this._rootPlaylistService = _rootPlaylistService;
|
||
this._rootQuery = _rootQuery;
|
||
this._mediaQuery = _mediaQuery;
|
||
this._iframeMachine = _iframeMachine;
|
||
this._anchorMSNs = [NaN, NaN]; // First fragment in current discontinuity
|
||
this._avDetails = [null, null]; // enabled AV options latest details (just the relevant info)
|
||
this.logger = logger.child({ name: 'fpicker' });
|
||
this._discoSeqNum = NaN;
|
||
this.lookUpTolerance = Math.max(config.maxBufferHole, config.maxFragLookUpTolerance);
|
||
this.firstAudioMustOverlapVideoStart = config.firstAudioMustOverlapVideoStart;
|
||
this.lookUpToleranceAlt = config.firstAudioMustOverlapVideoStart ? 0 : config.maxFragLookUpTolerance;
|
||
this.logger.info(`new item ${this._rootQuery.itemId}`);
|
||
}
|
||
destroy() {
|
||
this._anchorMSNs = [NaN, NaN];
|
||
this._avDetails = [null, null];
|
||
this._rootQuery = null;
|
||
this._mediaQuery = null;
|
||
this._rootPlaylistService = null;
|
||
this._iframeMachine = null;
|
||
}
|
||
get discoSeqNum() {
|
||
return this._discoSeqNum;
|
||
}
|
||
get _discoSeqNum() {
|
||
return this._rootQuery.discoSeqNum;
|
||
}
|
||
set _discoSeqNum(cc) {
|
||
// Should we just have a separate store for fragment selection?
|
||
this._rootPlaylistService.setDiscoSeqNum(this._rootQuery.itemId, cc);
|
||
}
|
||
get anchorMSNs() {
|
||
return this._anchorMSNs;
|
||
}
|
||
_resolvePosition(position, type, newDetails) {
|
||
var _a;
|
||
let resolvedPosition = position;
|
||
const curDetails = this._avDetails[type];
|
||
// LIVE:
|
||
// 1. try choosing position based on program date time @ the switch position
|
||
// 2. if ptsknown, we can reasonably choose @ switch position
|
||
// 3. else, choose at live offset from end
|
||
// Has END-TAG:
|
||
// Simple case. just use position.
|
||
if ((curDetails === null || curDetails === void 0 ? void 0 : curDetails.mediaOptionId) !== (newDetails === null || newDetails === void 0 ? void 0 : newDetails.mediaOptionId) &&
|
||
newDetails.liveOrEvent &&
|
||
newDetails.ptsKnown === false &&
|
||
(!curDetails || Math.max(curDetails.startSN, newDetails.startSN) > Math.min(curDetails.endSN, newDetails.endSN)) // no overlap
|
||
) {
|
||
// Commenting out PDT based revisal as it needs new position as opposed to old position, before rebasing to newDetails.
|
||
// if (curDetails?.dateMediaTimePairs != null && newDetails.dateMediaTimePairs) {
|
||
// const date = resolvePTSToDate(curDetails.dateMediaTimePairs, position);
|
||
// resolvedPosition = resolveDateToPTS(newDetails.dateMediaTimePairs, date);
|
||
// this.logger.info(`Use program date time ${date.toISOString()} ${position.toFixed(3)}->${resolvedPosition.toFixed(3)}`);
|
||
// } else {
|
||
const liveOffsetSec = 3 * newDetails.targetduration; // HLS native chooses 3 * targetDuration, or HOLD-BACK property
|
||
const liveEdge = ((_a = newDetails.fragments[0]) === null || _a === void 0 ? void 0 : _a.start) + newDetails.totalduration;
|
||
resolvedPosition = Math.max(0, liveEdge - liveOffsetSec);
|
||
this.logger.info(`Use live offset ${liveOffsetSec.toFixed(3)} ${position.toFixed(3)}->${resolvedPosition.toFixed(3)}`);
|
||
// }
|
||
}
|
||
return resolvedPosition;
|
||
}
|
||
getDiscoSeqNumForTime(details, position) {
|
||
if (this._mediaQuery.isIframeRate && details.iframesOnly) {
|
||
return DiscoHelper$1.discoSeqNumForTime(details.fragments, position); // from iframeMachine startClocksAndGetFirstFragment
|
||
}
|
||
else {
|
||
return discoSeqNumForTime(details, position);
|
||
}
|
||
}
|
||
/**
|
||
* After a seek, need to re-anchor using latest details
|
||
* @param position Position for re-anchor
|
||
* @param detailsTuple details
|
||
*/
|
||
_updateAnchorByPosition(position, detailsTuple) {
|
||
let newCC = NaN;
|
||
const vDetails = detailsTuple[SourceBufferType.Variant];
|
||
let resolvedPosition = position;
|
||
if (vDetails) {
|
||
const fragments = vDetails.fragments;
|
||
resolvedPosition = this._resolvePosition(position, SourceBufferType.Variant, vDetails);
|
||
newCC = this.getDiscoSeqNumForTime(vDetails, resolvedPosition); // used resolved position to get CC
|
||
if (!isFiniteNumber(newCC)) {
|
||
const firstFrag = fragments[0];
|
||
const lastFrag = fragments[fragments.length - 1];
|
||
const startPTSSec = firstFrag === null || firstFrag === void 0 ? void 0 : firstFrag.start;
|
||
const endPTSSec = (lastFrag === null || lastFrag === void 0 ? void 0 : lastFrag.start) + (lastFrag === null || lastFrag === void 0 ? void 0 : lastFrag.duration);
|
||
this.logger.warn(`${position.toFixed(3)} out of range [${startPTSSec === null || startPTSSec === void 0 ? void 0 : startPTSSec.toFixed(3)},${endPTSSec === null || endPTSSec === void 0 ? void 0 : endPTSSec.toFixed(3)}]`);
|
||
if (resolvedPosition <= startPTSSec) {
|
||
// Gapless preloading
|
||
newCC = firstFrag.discoSeqNum;
|
||
}
|
||
else if (resolvedPosition >= endPTSSec) {
|
||
newCC = lastFrag.discoSeqNum;
|
||
}
|
||
else {
|
||
this.logger.warn(`Unable to determine newCC. fragFirst: ${JSON.stringify(firstFrag)} fragLast: ${JSON.stringify(lastFrag)}`);
|
||
}
|
||
if (!isFiniteNumber(newCC)) {
|
||
this.logger.warn(`Unable to determine newCC. fragFirst: ${JSON.stringify(firstFrag)} fragLast: ${JSON.stringify(lastFrag)}`);
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
this.logger.warn('No variant details for anchoring');
|
||
}
|
||
if (newCC !== this._discoSeqNum) {
|
||
this.logger.info(`Update anchor by position:${resolvedPosition.toFixed(3)} cc:${this._discoSeqNum}->${newCC}`);
|
||
}
|
||
this._updateAnchor(newCC, detailsTuple);
|
||
return resolvedPosition;
|
||
}
|
||
_updateAnchor(newCC, detailsTuple) {
|
||
const ccDidChange = newCC !== this._discoSeqNum;
|
||
if (ccDidChange) {
|
||
this.logger.info(`Update anchor cc:${this._discoSeqNum}->${newCC}`);
|
||
this._discoSeqNum = newCC;
|
||
}
|
||
AVMediaOptionTypes.forEach((type) => {
|
||
const curDetails = this._avDetails[type];
|
||
const newDetails = detailsTuple[type];
|
||
const detailsChanged = (curDetails === null || curDetails === void 0 ? void 0 : curDetails.mediaOptionId) !== (newDetails === null || newDetails === void 0 ? void 0 : newDetails.mediaOptionId);
|
||
if (ccDidChange || detailsChanged) {
|
||
this._updateAnchorForType(mediaOptionTypeToSourceBufferType(type), newDetails);
|
||
}
|
||
else if (newDetails) {
|
||
const { mediaOptionId, ptsKnown, dateMediaTimePairs, startSN, endSN } = newDetails;
|
||
this._avDetails[type] = { mediaOptionId, ptsKnown, dateMediaTimePairs, startSN, endSN };
|
||
}
|
||
});
|
||
}
|
||
/**
|
||
* Get next set of fragments to choose.
|
||
* @param pos The playback position (media.currentTime)
|
||
* @param detailsTuple The latest details for variant and alt audio
|
||
* @param usePosition if true, use position for selection. else, use buffer end
|
||
*/
|
||
getNextFragments(action, detailsTuple, logger) {
|
||
const { position, bufferInfoTuple, switchContexts } = action;
|
||
const searchPositions = bufferInfoTuple.map((bufferInfo, type) => {
|
||
return getSearchPosition(position, detailsTuple[type], switchContexts[type], bufferInfo === null || bufferInfo === void 0 ? void 0 : bufferInfo.buffered, type === MediaOptionType.AltAudio ? this.lookUpToleranceAlt : this.lookUpTolerance);
|
||
});
|
||
let anchorPosition = searchPositions.reduce((lastMin, position) => Math.min(position, lastMin), Number.POSITIVE_INFINITY);
|
||
if (isFiniteNumber(action.discoSeqNum)) {
|
||
// if anchorTime specifies a discontinuity sequence number, find only fragments in the specified cc.
|
||
// happens right after resetting mediaSource when crossing an incompat. discontinuity
|
||
const combinedRange = detailsTuple.reduce((finalRange, details) => {
|
||
if (!details) {
|
||
return finalRange;
|
||
}
|
||
const thisRange = DiscoHelper$1.getTimeRangeForCC(details.fragments, action.discoSeqNum, logger); // adjust anchorPosition in case the anchor cc is a bit off
|
||
return [Math.max(finalRange[0], thisRange[0]), Math.min(finalRange[1], thisRange[1])];
|
||
}, [Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY]);
|
||
anchorPosition = Math.min(Math.max(anchorPosition, combinedRange[0]), combinedRange[1]);
|
||
this.logger.info(`adjusted anchorPosition ${anchorPosition} to fit in cc ${action.discoSeqNum} [${combinedRange[0]}-${combinedRange[1]}]`);
|
||
}
|
||
action.position = this._updateAnchorByPosition(anchorPosition, detailsTuple);
|
||
return this._getNextFragmentsInternal(action, detailsTuple);
|
||
}
|
||
_getNextFragmentsInternal(action, detailsTuple) {
|
||
var _a, _b;
|
||
const chosenFrags = [null, null];
|
||
let origPosition;
|
||
detailsTuple.forEach((_, type) => {
|
||
var _a;
|
||
if (this.firstAudioMustOverlapVideoStart && type === MediaOptionType.AltAudio && this._mediaQuery.seeking) {
|
||
// this ensures the audio is not ahead of the video;
|
||
// especially, decode from iframes could further pull back the
|
||
// seek position, and we should ensure that the corresponding audio is
|
||
// also available
|
||
if ((_a = chosenFrags[MediaOptionType.Variant]) === null || _a === void 0 ? void 0 : _a.foundFrag) {
|
||
// retain original position in case of next discontinuity
|
||
origPosition = action.position;
|
||
action.position = chosenFrags[MediaOptionType.Variant].foundFrag.mediaFragment.start;
|
||
this.logger.info(`revised altAudio frag search position to ${action.position} based on variant frag start`);
|
||
}
|
||
}
|
||
chosenFrags[type] = this._getNextFragmentForType(action, detailsTuple, type);
|
||
});
|
||
const vResult = chosenFrags[SourceBufferType.Variant];
|
||
const aResult = chosenFrags[SourceBufferType.AltAudio];
|
||
// One buffer is too far ahead of the other one
|
||
// 1. Could happen in i-frame mode where audio fragments are much longer
|
||
// 2. Audio switch that causes complete flush
|
||
const vFrag = (_a = vResult === null || vResult === void 0 ? void 0 : vResult.foundFrag) === null || _a === void 0 ? void 0 : _a.mediaFragment;
|
||
const aFrag = (_b = aResult === null || aResult === void 0 ? void 0 : aResult.foundFrag) === null || _b === void 0 ? void 0 : _b.mediaFragment;
|
||
if (vFrag && aFrag) {
|
||
if (aFrag.start > vFrag.start + vFrag.duration) {
|
||
// must not load audio too far ahead in iframe mode, cause unrecoverable bufferFull error in AirPlay
|
||
this.logger.warn('Audio too far ahead');
|
||
chosenFrags[SourceBufferType.AltAudio] = FragmentPicker.noopResult;
|
||
}
|
||
else if (vFrag.start > aFrag.start + aFrag.duration && !this._mediaQuery.isIframeRate) {
|
||
this.logger.warn('Video too far ahead');
|
||
chosenFrags[SourceBufferType.Variant] = FragmentPicker.noopResult;
|
||
}
|
||
}
|
||
// All fragments in next discontinuity. Advance and try selecting again
|
||
if (isFinite(vResult === null || vResult === void 0 ? void 0 : vResult.nextDisco) && (aResult == null || isFiniteNumber(aResult.nextDisco))) {
|
||
const newDisco = chosenFrags[SourceBufferType.Variant].nextDisco;
|
||
this.logger.info(`Advance anchor cc: ${this._discoSeqNum}->${newDisco}`);
|
||
this._updateAnchor(newDisco, detailsTuple);
|
||
if (isFiniteNumber(origPosition)) {
|
||
// reinstate the original position due to previous firstAudioMustOverlapVideoStart override
|
||
action.position = origPosition;
|
||
origPosition = NaN;
|
||
}
|
||
return this._getNextFragmentsInternal(action, detailsTuple);
|
||
}
|
||
return chosenFrags;
|
||
}
|
||
/**
|
||
* Get fragment for position for particular buffer
|
||
* @param pos position that fragment should overlap
|
||
* @param usePos if true, choose fragment overlapping pos (e.g. immediate switch w/o flush). else, use bufferEnd (default)
|
||
*/
|
||
_getNextFragmentForType(action, detailsTuple, type) {
|
||
var _a, _b, _c, _d, _e, _f, _g, _h;
|
||
const { position: pos, bufferInfoTuple, switchContexts } = action;
|
||
const newDetails = detailsTuple[type];
|
||
const bufferInfo = (_b = (_a = bufferInfoTuple[type]) === null || _a === void 0 ? void 0 : _a.buffered) !== null && _b !== void 0 ? _b : { start: pos, end: pos, len: 0 };
|
||
const bufferedSeg = this._mediaQuery.getBufferedSegmentsByType(type);
|
||
const willFlush = (_d = (_c = switchContexts[type]) === null || _c === void 0 ? void 0 : _c.userInitiated) !== null && _d !== void 0 ? _d : false; // Full flush of this buffer will happen
|
||
const searchPos = getSearchPosition(pos, newDetails, switchContexts[type], bufferInfo, this.lookUpTolerance);
|
||
if (!newDetails) {
|
||
return null;
|
||
}
|
||
const { highWaterLevelSeconds, lowWaterLevelSeconds } = this._mediaQuery.bufferMonitorInfo;
|
||
const bufferAhead = bufferInfo.len; // Future bufferAhead
|
||
if (!willFlush && bufferAhead >= highWaterLevelSeconds) {
|
||
this.logger.info(`[${MediaOptionNames[type]}] got high buffer @${pos.toFixed(3)} ${bufferInfo.len.toFixed(3)} > ${highWaterLevelSeconds}`);
|
||
return FragmentPicker.noopResult;
|
||
}
|
||
// Special case where audio is starving but variant is ok
|
||
const otherType = type === SourceBufferType.Variant ? SourceBufferType.AltAudio : SourceBufferType.Variant;
|
||
const otherBuffer = (_e = bufferInfoTuple[otherType]) === null || _e === void 0 ? void 0 : _e.buffered;
|
||
const otherWillFlush = (_g = (_f = switchContexts[otherType]) === null || _f === void 0 ? void 0 : _f.userInitiated) !== null && _g !== void 0 ? _g : false;
|
||
let wantToDelayVariant = false;
|
||
if (type === SourceBufferType.Variant &&
|
||
bufferAhead >= lowWaterLevelSeconds &&
|
||
// Yield to altaudio (but ensure audio load-n-append does not punt back to variant, causing an infinite loop):
|
||
// A) Audio buffer ends earlier than video buffer.
|
||
// If buffered audio ends later than video, audio append may abort & wait for video append to catch up in mediaSink.getMatchingInfo
|
||
this._mediaQuery.expectedSbCount > 1 &&
|
||
otherBuffer != null &&
|
||
otherBuffer.end < bufferInfo.end &&
|
||
// B) Audio was starting from an empty buffer (true starvation after audio track switch): _total_ audio buffer < 1 target duration
|
||
// Use (otherBuffer.end - otherBuffer.start) instead of otherBuffer.len (which begins from currentTime, not start of buffer)
|
||
(otherWillFlush || otherBuffer.end - otherBuffer.start < lowWaterLevelSeconds)) {
|
||
this.logger.info(`[${MediaOptionNames[type]}] want to delay variant due to audio starvation @${pos.toFixed(3)} ${bufferInfo.len.toFixed(3)} > ${lowWaterLevelSeconds}`);
|
||
wantToDelayVariant = true;
|
||
}
|
||
let foundFrag = null;
|
||
let nextDisco = NaN;
|
||
let newMediaRootTime = undefined;
|
||
if (this._mediaQuery.isIframeRate && type === SourceBufferType.Variant && newDetails.iframesOnly) {
|
||
const result = findIframeFragmentForPosition(searchPos, newDetails, detailsTuple[SourceBufferType.AltAudio], this._mediaQuery.desiredRate, this._iframeMachine);
|
||
if (result) {
|
||
({ foundFrag, nextDisco, newMediaRootTime } = result);
|
||
const frag = foundFrag.mediaFragment;
|
||
if (frag.discoSeqNum !== this._discoSeqNum) {
|
||
this.logger.info(`[iframes] overrride cc:${frag.discoSeqNum}`);
|
||
// OK to advance here since Variant will run before AltAudio.
|
||
this._updateAnchor(frag.discoSeqNum, detailsTuple);
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
// Find eligible segment in the specified discontinuity
|
||
const anchorMSN = this._anchorMSNs[type];
|
||
({ foundFrag, nextDisco, newMediaRootTime } = findFragment(searchPos, this._discoSeqNum, anchorMSN, newDetails, bufferedSeg));
|
||
}
|
||
if (wantToDelayVariant && this._rootQuery.getInitPTS(foundFrag === null || foundFrag === void 0 ? void 0 : foundFrag.mediaFragment.discoSeqNum)) {
|
||
this.logger.info(`[${MediaOptionNames[type]}] delay variant since initPTS[${foundFrag === null || foundFrag === void 0 ? void 0 : foundFrag.mediaFragment.discoSeqNum}] is available`);
|
||
return FragmentPicker.noopResult;
|
||
}
|
||
this.logger.info(`[${MediaOptionNames[type]}] getNextFragmentForType searchPos:${searchPos.toFixed(3)} pos:${pos.toFixed(3)} bufferEnd:${bufferInfo === null || bufferInfo === void 0 ? void 0 : bufferInfo.end.toFixed(3)} willFlush: ${willFlush} cc:${this._discoSeqNum} foundFrag:${fragPrint(foundFrag === null || foundFrag === void 0 ? void 0 : foundFrag.mediaFragment)} timelineOffset:${(_h = foundFrag === null || foundFrag === void 0 ? void 0 : foundFrag.timelineOffset) === null || _h === void 0 ? void 0 : _h.toFixed(3)} nextDisco:${nextDisco} newMediaRootTime:${newMediaRootTime}`);
|
||
return { foundFrag, nextDisco, newMediaRootTime };
|
||
}
|
||
_updateAnchorForType(type, newDetails) {
|
||
var _a;
|
||
if (!newDetails) {
|
||
this._anchorMSNs[type] = NaN;
|
||
this._avDetails[type] = null;
|
||
return;
|
||
}
|
||
if (!isFiniteNumber(this._discoSeqNum)) {
|
||
this.logger.warn('Trying to anchor with non-finite discoSeqNum');
|
||
return;
|
||
}
|
||
const cc = this._discoSeqNum;
|
||
const anchorFrag = getAnchorFrag(newDetails.fragments, cc);
|
||
const mediaSeqNum = (_a = anchorFrag === null || anchorFrag === void 0 ? void 0 : anchorFrag.mediaSeqNum) !== null && _a !== void 0 ? _a : newDetails.startSN; // No anchor, use first fragment as fallback
|
||
this._anchorMSNs[type] = mediaSeqNum;
|
||
const { mediaOptionId, ptsKnown, dateMediaTimePairs, startSN, endSN } = newDetails;
|
||
this._avDetails[type] = { mediaOptionId, ptsKnown, dateMediaTimePairs, startSN, endSN };
|
||
this.logger.info(`[${MediaOptionNames[type]}] Anchor update id:${mediaOptionId} sn:${this._anchorMSNs[type]}->${mediaSeqNum} discoSeqNum:${this._discoSeqNum}`);
|
||
}
|
||
}
|
||
FragmentPicker.noopResult = { foundFrag: null, nextDisco: NaN }; // prevents us from skipping ahead in disco
|
||
/**
|
||
* Get first fragment in discontinuity cc
|
||
*/
|
||
function getAnchorFrag(fragments, cc) {
|
||
return fragments.find((f) => f.discoSeqNum === cc);
|
||
}
|
||
function getSearchPosition(pos, details, switchContext, bufferInfo, defaultTolerance) {
|
||
var _a;
|
||
bufferInfo = bufferInfo !== null && bufferInfo !== void 0 ? bufferInfo : { start: pos, end: pos, len: 0 };
|
||
const willFlush = (_a = switchContext === null || switchContext === void 0 ? void 0 : switchContext.userInitiated) !== null && _a !== void 0 ? _a : false; // Full flush of this buffer will happen
|
||
const tolerance = (details === null || details === void 0 ? void 0 : details.iframesOnly) ? 0 : defaultTolerance;
|
||
return willFlush || bufferInfo.len === 0 ? pos : bufferInfo.end + tolerance;
|
||
}
|
||
|
||
const avPipeLogName = { name: 'avpipe' };
|
||
// AVPipe
|
||
function makeAVPipe(context) {
|
||
const { config, rootPlaylistService, rootPlaylistQuery, mediaSink, gaplessInstance } = context;
|
||
const mediaQuery = mediaSink.mediaQuery;
|
||
const mediaOptionSwitching$ = combineQueries(AVMediaOptionTypes.map((type) => {
|
||
return rootPlaylistQuery.enabledMediaOptionSwitchForType$(type).pipe(tap((switchInfo) => logger.info(`${MediaOptionNames[type]} mediaOptionSwitch: ${JSON.stringify(switchInfo)}`)));
|
||
})).pipe(switchMap((switchInfo) => {
|
||
// Teardown if no enabled option for at least main
|
||
if (!isEnabledMediaOption({ itemId: rootPlaylistQuery.itemId, mediaOptionId: switchInfo[MediaOptionType.Variant].toId })) {
|
||
throw new ExceptionError(true, `No valid variant enabled id:${switchInfo[MediaOptionType.Variant].toId}`, ErrorResponses.NoValidAlternates);
|
||
}
|
||
// Handle switch
|
||
const switchActions$ = switchInfo.map(({ fromId, toId }, mediaOptionType) => {
|
||
return pipelineHandleOptionSwitch(context, statsQuery, mediaOptionType, fromId, toId);
|
||
});
|
||
return concat(of(true), // Emit on switch start
|
||
forkJoin(switchActions$).pipe(mapTo(false)) // Emit when we're ready to select fragments
|
||
);
|
||
}), tag('mediaOptionSwitch.audiovideo.out'));
|
||
const statsQuery = createStatsQuery(rootPlaylistQuery.itemId);
|
||
const logger = context.logger.child(avPipeLogName);
|
||
const fragmentPicker = new FragmentPicker(logger, rootPlaylistService, rootPlaylistQuery, mediaQuery, context.iframeMachine, config);
|
||
const anchorTimeChange$ = rootPlaylistQuery.anchorTime$.pipe(tag('anchorTime.audiovideo.in'));
|
||
// Switch subscriptions when:
|
||
// 1. anchor time change
|
||
// 2. enabled option change
|
||
// Exhaust / wait for pipeline to finalize when:
|
||
// 1. Need data changes
|
||
return combineQueries([anchorTimeChange$, mediaOptionSwitching$]).pipe(switchMap(([anchorTime, handlingSwitch]) => {
|
||
logger.info(`anchorTime=${anchorTime === null || anchorTime === void 0 ? void 0 : anchorTime.pos.toFixed(3)} cc=${anchorTime === null || anchorTime === void 0 ? void 0 : anchorTime.discoSeqNum} handlingSwitch=${handlingSwitch}`);
|
||
if (handlingSwitch) {
|
||
return EMPTY; // Just cancel any loads in progress. This is to stop stuff from loading while we're flushing
|
||
}
|
||
return mediaQuery.needData$(config.maxBufferHole, gaplessInstance.inGaplessMode).pipe(map((needData) => {
|
||
const switchContexts = [
|
||
rootPlaylistQuery.enabledMediaOptionSwitchContexts[MediaOptionType.Variant],
|
||
rootPlaylistQuery.enabledMediaOptionSwitchContexts[MediaOptionType.AltAudio],
|
||
];
|
||
return mediaQuery.getSourceBufferInfoAction(needData, anchorTime, switchContexts, config.maxBufferHole);
|
||
}), exhaustMap((sbAction) => {
|
||
if (!sbAction) {
|
||
return EMPTY; // Don't emit if we didn't do anything
|
||
}
|
||
logger.debug(`start hoses ${JSON.stringify(sbAction)}`);
|
||
const producerConsumer$ = of(sbAction).pipe(mediaProducerEpic(context, fragmentPicker), mediaConsumerEpic(context));
|
||
return race(producerConsumer$, waitFor(shouldAbortFrag(context), (abort) => abort) // If this emits, we'll abort the whole chain
|
||
).pipe(take(1), finalize$1(() => {
|
||
AVMediaOptionTypes.forEach((type) => {
|
||
rootPlaylistService.updateInflightFrag(rootPlaylistQuery.itemId, type, null, null, null);
|
||
});
|
||
}));
|
||
}));
|
||
}), map(() => {
|
||
if (rootPlaylistQuery.getEntity(rootPlaylistQuery.itemId).manualMode) {
|
||
return;
|
||
}
|
||
// Check for automatic / lazy switch
|
||
let switchReason = SwitchReason.None;
|
||
if (checkAndUpdateViewportInfo(platformService(), mediaQuery, config, logger)) {
|
||
switchReason = SwitchReason.PreferredListChanged;
|
||
}
|
||
let cappedForLowBandwidth = false;
|
||
const enabledOption = rootPlaylistQuery.enabledVariantMediaOption;
|
||
if (gotLowBw(rootPlaylistQuery, mediaQuery, rootPlaylistService)) {
|
||
switchReason = SwitchReason.LowBandwidth;
|
||
// Only add a cap if there is not already one in place
|
||
if (rootPlaylistQuery.nextMaxAutoOptionId === NoMediaOption.mediaOptionId) {
|
||
// Should uncap automatically if we select a different variant, ensure this is released after handling switch
|
||
rootPlaylistService.setNextMaxAutoOptionId(enabledOption.itemId, enabledOption.mediaOptionId);
|
||
logger.info(`Capping variant selection to ${enabledOption.mediaOptionId} because switchReason: ${switchReason}`);
|
||
cappedForLowBandwidth = true;
|
||
}
|
||
}
|
||
else if (mediaQuery.playbackStarted && gotHighBw(config, rootPlaylistQuery, rootPlaylistService)) {
|
||
switchReason = SwitchReason.HighBandwidth;
|
||
rootPlaylistService.setNextMinAutoOptionId(enabledOption.itemId, enabledOption.mediaOptionId);
|
||
logger.info(`Capping variant selection to above ${enabledOption.mediaOptionId} because switchReason: ${switchReason}`);
|
||
}
|
||
handleVariantSwitch(switchReason, config, rootPlaylistQuery, mediaQuery, rootPlaylistService);
|
||
// Remove any max/min level caps we placed before level selection
|
||
if (cappedForLowBandwidth) {
|
||
rootPlaylistService.setNextMaxAutoOptionId(enabledOption.itemId, NoMediaOption.mediaOptionId);
|
||
}
|
||
else if (switchReason === SwitchReason.HighBandwidth) {
|
||
rootPlaylistService.setNextMinAutoOptionId(enabledOption.itemId, NoMediaOption.mediaOptionId);
|
||
}
|
||
}), finalize$1(() => {
|
||
// on (i) active item change, or (ii) after handleErrorActionCommon which will reuse fragmentPicker.
|
||
// let fragmentPicker garbage collect automatically.
|
||
logger.debug('teardown');
|
||
}));
|
||
}
|
||
function audioMediaOptionRemoved(rootPlaylistQuery, fromId, toId) {
|
||
const fromAlternate = fromId === 'Nah' ? null : rootPlaylistQuery.alternateMediaOptionById(MediaOptionType.AltAudio, fromId);
|
||
const fromIdHasAudio = Boolean(fromAlternate && fromAlternate.url);
|
||
const toAlternate = fromId === 'Nah' ? null : rootPlaylistQuery.alternateMediaOptionById(MediaOptionType.AltAudio, toId);
|
||
const toIdHasAudio = Boolean(toAlternate && toAlternate.url);
|
||
// rename function to audioMediaOptionRemovedOrAdded if uncommenting following line.
|
||
// return fromIdHasAudio !== toIdHasAudio;
|
||
return fromIdHasAudio && !toIdHasAudio;
|
||
}
|
||
/**
|
||
* @brief Handle option switch at start of pipeline
|
||
* Called within pipeline as an action before fetch, as part of switching the enabled option
|
||
* May be called before or after destination option has actually fully loaded
|
||
*/
|
||
function pipelineHandleOptionSwitch(libContext, statsQuery, mediaOptionType, fromId, toId) {
|
||
var _a, _b;
|
||
const { rootPlaylistQuery, rootPlaylistService, mediaSink, mediaParser, logger, config, iframeMachine } = libContext;
|
||
const meQuery = mediaSink.mediaQuery;
|
||
if (!fromId || !toId || (fromId === toId && (mediaOptionType === MediaOptionType.AltAudio || !iframeMachine.isStarted))) {
|
||
// <rdar://88230974> if iframeMachine.isStarted, must check for trickplay exit condition
|
||
return VOID;
|
||
}
|
||
switch (mediaOptionType) {
|
||
case MediaOptionType.Variant:
|
||
{
|
||
if (fromId !== toId) {
|
||
mediaParser.reset(MediaOptionType.Variant);
|
||
}
|
||
const sbType = mediaOptionTypeToSourceBufferType(mediaOptionType);
|
||
const fromVariant = rootPlaylistQuery.variantMediaOptionById(fromId);
|
||
const toVariant = rootPlaylistQuery.variantMediaOptionById(toId);
|
||
const itemId = rootPlaylistQuery.itemId;
|
||
if (toVariant == null || fromVariant == null) {
|
||
return VOID;
|
||
}
|
||
const resumingFromTrickPlay = !toVariant.iframes && iframeMachine.isStarted;
|
||
if (fromVariant.iframes !== toVariant.iframes || resumingFromTrickPlay) {
|
||
mediaSink.toggleTrickPlaybackMode(toVariant.iframes);
|
||
if (resumingFromTrickPlay) {
|
||
if (!isFiniteNumber(mediaSink.mediaQuery.postFlushSeek)) {
|
||
mediaSink.postFlushSeek = iframeMachine.iframeClockTimeSeconds;
|
||
}
|
||
iframeMachine.stop();
|
||
}
|
||
logger.info(`pause & flush for iframeMode transition [0, Infinity] postFlushSeek=${mediaSink.mediaQuery.postFlushSeek} resumingFromTrickPlay=${resumingFromTrickPlay}`);
|
||
mediaSink.pause();
|
||
// <rdar://88320795> (2.1a) Flush audio and video when resuming from trickplay to avoid fpicker getting stuck with audio gaps after rewind
|
||
const postTrickPlayFlush$ = resumingFromTrickPlay ? mediaSink.flushAll(0, Infinity, true) : mediaSink.flushData(sbType, 0, Infinity, true);
|
||
return postTrickPlayFlush$.pipe(tap(() => {
|
||
const postFlushSeek = mediaSink.mediaQuery.postFlushSeek;
|
||
if (isFiniteNumber(postFlushSeek)) {
|
||
rootPlaylistService.setPendingSeek(itemId, postFlushSeek);
|
||
}
|
||
}));
|
||
}
|
||
// From stream-controller.ts attemptSafeNextLevelSwitch
|
||
if (!config.allowFastSwitchUp || toVariant.iframes) {
|
||
return VOID;
|
||
}
|
||
const fromQuery = createMediaLibraryQuery(fromVariant);
|
||
const fromDetails = fromQuery.mediaOptionDetails;
|
||
if (fromDetails != null && toVariant != null && fromVariant.bitrate < toVariant.bitrate) {
|
||
const fromTargetDuration = fromDetails.targetduration;
|
||
const mediaLibraryQuery = createMediaLibraryQuery(toVariant);
|
||
const toDetails = mediaLibraryQuery.mediaOptionDetails;
|
||
const lastUpdateMs = mediaLibraryQuery.mediaOptionDetailsEntity.lastUpdateMillis;
|
||
const currentWaterLevel = meQuery.getCurrentWaterLevelByType(sbType, config.maxBufferHole);
|
||
const minBufferAhead = minSwitchBufferAheadSec(fromVariant, toVariant, toDetails, fromTargetDuration, rootPlaylistQuery.abrStatus, statsQuery, config, lastUpdateMs, logger) + config.maxStarvationDelay; // add a safety delay of maxStarvationDelay
|
||
logger.info(`fastSwitchUp: currentWaterLevel:${currentWaterLevel} minBufferAhead:${minBufferAhead}`);
|
||
// Flush from somewhere past midpoint of first segment after minFlushStart position
|
||
const minFlushStart = meQuery.currentTime + minBufferAhead;
|
||
const segment = (_b = (_a = meQuery.sourceBufferEntityByType(sbType)) === null || _a === void 0 ? void 0 : _a.bufferedSegments) === null || _b === void 0 ? void 0 : _b.find((seg) => seg.startPTS >= minFlushStart);
|
||
// Flush position will remove half to a quarter (at least maxFragLookUpTolerance) of the buffered segment
|
||
// to avoid gaps left by appening new segments with dropped samples up to the first IDR frame
|
||
let flushStart;
|
||
if (segment) {
|
||
const duration = segment.endPTS - segment.startPTS;
|
||
flushStart = segment.startPTS + Math.min(Math.max(duration - config.maxFragLookUpTolerance, duration * 0.5), duration * 0.75);
|
||
}
|
||
if (isFiniteNumber(flushStart) && currentWaterLevel >= minBufferAhead) {
|
||
// Note: old code had special handling for live to preserve fragPrevious
|
||
logger.info(`flush for fast switchup [${flushStart.toFixed(3)}, Infinity]`);
|
||
return mediaSink.flushData(sbType, flushStart, Infinity);
|
||
}
|
||
}
|
||
}
|
||
break;
|
||
case MediaOptionType.AltAudio:
|
||
if (audioMediaOptionRemoved(rootPlaylistQuery, fromId, toId)) {
|
||
// altAudio -> muxed handling is tricky because:
|
||
// when hls switches audio, the new altAudio mediaOption (toId) has no url; audio is muxed with video.
|
||
// During switching, getDetailsAndSwitchContext(altAudio) will ignore toId because it has no mediaOptionDetails too.
|
||
// As a result, chooseAndFetchFragment will never sees toId's switchContext to trigger any audio switching logic.
|
||
// Cleaner to shortcircuit the process: reset the mediaSource now, reload and seek with only the muxed mediaOption.
|
||
logger.info(`alt audio removed/added while switching fromId ${fromId} to toId ${toId} @ ${meQuery.currentTime}`);
|
||
rootPlaylistService.setEnabledMediaOptionSwitchContextByType(rootPlaylistQuery.itemId, MediaOptionType.AltAudio, toId, undefined);
|
||
mediaSink.resetMediaSource(meQuery.currentTime);
|
||
}
|
||
mediaParser.reset(MediaOptionType.AltAudio);
|
||
break;
|
||
}
|
||
return VOID;
|
||
}
|
||
function checkAndUpdateViewportInfo(platformService, mediaQuery, config, logger) {
|
||
var _a;
|
||
const clientWidth = mediaQuery === null || mediaQuery === void 0 ? void 0 : mediaQuery.clientWidth;
|
||
const clientHeight = mediaQuery === null || mediaQuery === void 0 ? void 0 : mediaQuery.clientHeight;
|
||
const devicePixelRatio = typeof window === 'object' && window.devicePixelRatio ? window.devicePixelRatio : 1;
|
||
const newViewportInfo = clientWidth && clientHeight ? { width: clientWidth * devicePixelRatio, height: clientHeight * devicePixelRatio } : undefined;
|
||
const curViewportInfo = ((_a = platformService.getQuery()) === null || _a === void 0 ? void 0 : _a.viewportInfo) || {};
|
||
const isViewportSizeChanged = curViewportInfo && newViewportInfo && (curViewportInfo.width !== newViewportInfo.width || curViewportInfo.height !== newViewportInfo.height);
|
||
if (config.useViewportSizeForLevelCap && isViewportSizeChanged) {
|
||
platformService.updateViewportInfo(newViewportInfo);
|
||
logger.info(`ViewportSize changed, old: ${JSON.stringify(curViewportInfo)}, new: ${JSON.stringify(newViewportInfo)}`);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
// AV Producer should work as follows:
|
||
// 1. wait for details for enabled variant & alt audio
|
||
// 2. choose fragment(s) for the position if < highWaterLevelSeconds
|
||
// 3. fetch & parse candidate fragments(s)
|
||
// 4. on parse, check if we need to re-select any fragment (ptsKnown changed)
|
||
// 5. advance discontinuity if needed
|
||
// 6. fetch & parse any new candidate fragment(s)
|
||
const mediaProducerEpic = (pipelineContext, fragmentPicker) => (bufferInfoAction$) => {
|
||
const { rootPlaylistQuery: rootQuery, mediaSink } = pipelineContext;
|
||
const logger = pipelineContext.logger.child(avPipeLogName);
|
||
return bufferInfoAction$.pipe(observeOn(asyncScheduler), withLatestFrom(rootQuery.enabledMediaOptionKeys$),
|
||
// Wait for details and switch context
|
||
switchMap(([action, enabledOptionKeys]) => {
|
||
return zip(getDetailsAndSwitchContext(action, MediaOptionType.Variant, pipelineContext, enabledOptionKeys).pipe(tap((detailsAndContext) => {
|
||
var _a, _b;
|
||
const varEntity = detailsAndContext.detailsEntity;
|
||
if (!varEntity.mediaOptionDetails.liveOrEvent || varEntity.mediaOptionDetails.ptsKnown) {
|
||
const newDuration = varEntity.playlistDuration;
|
||
// When AltAudio buffer is ahead of Variant and AltAudio buffer end is slightly greater than playlistDuration
|
||
// then setting mediaSink.msDuration based on variant alone will lead to mediaSource buffer trucation to set duration.
|
||
// This behavior is seen on Chrome browser, Roku & LG
|
||
const variantBufferEnd = ((_a = action.bufferInfoTuple[0]) === null || _a === void 0 ? void 0 : _a.buffered.end) || 0;
|
||
const altAudioBufferEnd = ((_b = action.bufferInfoTuple[1]) === null || _b === void 0 ? void 0 : _b.buffered.end) || 0;
|
||
const maxBufferEnd = Math.max(variantBufferEnd, altAudioBufferEnd);
|
||
mediaSink.msDuration = isFiniteNumber(mediaSink.msDuration) ? Math.max(mediaSink.msDuration, newDuration, maxBufferEnd) : newDuration;
|
||
}
|
||
})), getDetailsAndSwitchContext(action, MediaOptionType.AltAudio, pipelineContext, enabledOptionKeys)).pipe(map((detailsAndContext) => ({ action, detailsAndContext })));
|
||
}),
|
||
// Choose, fetch, & parse fragments:
|
||
switchMap(({ action, detailsAndContext }) => chooseAndFetchFragments(logger, fragmentPicker, pipelineContext, action, detailsAndContext)), tag('mediaProducerEpic.emit'));
|
||
};
|
||
function playlistIsValid(mediaOptionDetails, lastUpdateMillis) {
|
||
return !mediaOptionDetails.liveOrEvent || !mediaOptionDetails.ptsKnown || !likelyOutOfDate(mediaOptionDetails === null || mediaOptionDetails === void 0 ? void 0 : mediaOptionDetails.totalduration, 0, lastUpdateMillis);
|
||
}
|
||
function getDetailsAndSwitchContext(bufferInfoAction, type, pipelineContext, enabledMediaOptionKeys) {
|
||
const { rootPlaylistQuery, mediaLibraryService, gaplessInstance, config } = pipelineContext;
|
||
// Note we don't expect enabled option to change while we're in this hose because of makeAVPipe.
|
||
const option = enabledMediaOptionKeys[type];
|
||
const logger = pipelineContext.logger.child({ name: MediaOptionNames[type] });
|
||
if (!option || option.mediaOptionId === 'Nah') {
|
||
return of({ detailsEntity: null, switchContext: null });
|
||
}
|
||
const playlistQuery = mediaLibraryService.getQueryForOption(option);
|
||
return combineLatest([
|
||
of(bufferInfoAction),
|
||
playlistQuery.mediaOptionDetailsEntity$.pipe(distinctUntilChanged((a, b) => (a === null || a === void 0 ? void 0 : a.lastUpdateMillis) === (b === null || b === void 0 ? void 0 : b.lastUpdateMillis)), filter((mediaOptionDetailsEntity) => {
|
||
const mediaOptionDetails = mediaOptionDetailsEntity === null || mediaOptionDetailsEntity === void 0 ? void 0 : mediaOptionDetailsEntity.mediaOptionDetails;
|
||
if (!mediaOptionDetails) {
|
||
return true; // audio/muxed content
|
||
}
|
||
const now = performance.now();
|
||
const lastUpdateMillis = mediaOptionDetailsEntity.lastUpdateMillis || now;
|
||
const liveOrEvent = mediaOptionDetails.liveOrEvent;
|
||
const targetDuration = mediaOptionDetails.targetduration;
|
||
// logger.info(`playlist updated at ${lastUpdateMillis}, current time is ${now}, delta is ${(now - lastUpdateMillis) / 1000} gt/lt ${2 * targetDuration}`);
|
||
// Ensure live mediaOptionDetails is not stale from a previous use of this level.
|
||
return !liveOrEvent || now - lastUpdateMillis < config.livePlaylistUpdateStaleness * targetDuration * 1000;
|
||
})),
|
||
]).pipe(filter(([bufferInfoAction, detailsEntity]) => {
|
||
var _a, _b;
|
||
logger.info(`details entity ${(_a = detailsEntity === null || detailsEntity === void 0 ? void 0 : detailsEntity.mediaOptionDetails) === null || _a === void 0 ? void 0 : _a.mediaOptionId} updated`);
|
||
if (type === MediaOptionType.AltAudio && !rootPlaylistQuery.altMediaOptionHasValidUrl(type, option.mediaOptionId)) {
|
||
logger.info('manifest may have unused alternate audio options');
|
||
return true;
|
||
}
|
||
const mediaOptionDetails = detailsEntity === null || detailsEntity === void 0 ? void 0 : detailsEntity.mediaOptionDetails;
|
||
// Old code tries to save effort by blocking variant if it has already loaded the last segment.
|
||
// but altAudio may backtrack to media.currentTime when user switches audio.
|
||
// In which case, hls needs both variant and altaudio to emit getDetailsAndSwitchContext @ media.currentTime
|
||
// const bufferedSegments = bufferInfoAction.bufferInfoTuple[type]?.bufferedSegments ?? [];
|
||
return (mediaOptionDetails != null && playlistIsValid(mediaOptionDetails, (_b = detailsEntity.lastUpdateMillis) !== null && _b !== void 0 ? _b : 0)
|
||
// && (!lastFragmentBuffered(mediaOptionDetails, bufferedSegments, rootPlaylistQuery.itemStartOffset, gaplessInstance.inGaplessMode))
|
||
);
|
||
}), take(1), withLatestFrom(rootPlaylistQuery.enabledMediaOptionSwitchContextsByType$(type)), map(([[, detailsEntity], switchContext]) => {
|
||
var _a, _b, _c;
|
||
const details = detailsEntity === null || detailsEntity === void 0 ? void 0 : detailsEntity.mediaOptionDetails;
|
||
if (details) {
|
||
const lastFrag = details.fragments[details.fragments.length - 1];
|
||
logger.info(`${details.mediaOptionId} got details MSNs:[${details.startSN},${details.endSN}] range=[${(_b = (_a = details.fragments[0]) === null || _a === void 0 ? void 0 : _a.start) === null || _b === void 0 ? void 0 : _b.toFixed(3)},${(_c = ((lastFrag === null || lastFrag === void 0 ? void 0 : lastFrag.start) + (lastFrag === null || lastFrag === void 0 ? void 0 : lastFrag.duration))) === null || _c === void 0 ? void 0 : _c.toFixed(3)}]`);
|
||
}
|
||
return { detailsEntity, switchContext };
|
||
}));
|
||
}
|
||
function chooseAndFetchFragments(logger, fragmentPicker, pipelineContext, action, detailsAndContext) {
|
||
var _a, _b;
|
||
const { mediaSink, iframeMachine, rootPlaylistQuery } = pipelineContext;
|
||
const detailsTuple = [detailsAndContext[MediaOptionType.Variant].detailsEntity.mediaOptionDetails, (_b = (_a = detailsAndContext[MediaOptionType.AltAudio]) === null || _a === void 0 ? void 0 : _a.detailsEntity) === null || _b === void 0 ? void 0 : _b.mediaOptionDetails];
|
||
let chosenFrags = fragmentPicker.getNextFragments(action, detailsTuple, logger);
|
||
logger.info(`chosen frags=${JSON.stringify(chosenFrags.map((x) => {
|
||
var _a;
|
||
return ({
|
||
foundFrag: fragPrint((_a = x === null || x === void 0 ? void 0 : x.foundFrag) === null || _a === void 0 ? void 0 : _a.mediaFragment),
|
||
nextDisco: x === null || x === void 0 ? void 0 : x.nextDisco,
|
||
newMediaRootTime: x === null || x === void 0 ? void 0 : x.newMediaRootTime,
|
||
});
|
||
}))}`);
|
||
const newMediaRootTime = chosenFrags.reduce((prev, cur) => {
|
||
return Math.max(prev, isFiniteNumber(cur === null || cur === void 0 ? void 0 : cur.newMediaRootTime) ? cur.newMediaRootTime : -Infinity);
|
||
}, -Infinity);
|
||
if (isFiniteNumber(newMediaRootTime)) {
|
||
logger.info(`seek to newMediaRootTime ${newMediaRootTime}`);
|
||
mediaSink.seekTo = newMediaRootTime;
|
||
chosenFrags = [null, null];
|
||
}
|
||
if (chosenFrags.every((f) => (f === null || f === void 0 ? void 0 : f.foundFrag) == null)) {
|
||
return of(null); // no more to produce
|
||
}
|
||
return zip(...chosenFrags.map((f, type) => {
|
||
if (!f || f.foundFrag == null) {
|
||
return of(null);
|
||
}
|
||
else {
|
||
const { mediaFragment } = f.foundFrag;
|
||
const prefetchKey$ = loadKey(pipelineContext, mediaFragment.keyTagInfo, { itemId: mediaFragment.itemId, mediaOptionId: mediaFragment.mediaOptionId });
|
||
let fetchAndParse$ = retrieveMediaFragmentCacheEntity(pipelineContext, type, f).pipe(tap((sbDataTuple) => {
|
||
var _a;
|
||
const mediaFragmentCacheEntity = sbDataTuple[1];
|
||
const switchContext = detailsAndContext[type].switchContext;
|
||
mediaFragmentCacheEntity.switchPosition = switchContext === null || switchContext === void 0 ? void 0 : switchContext.switchPosition;
|
||
// if user initiated or rewind rate change
|
||
const userInitiated = (_a = switchContext === null || switchContext === void 0 ? void 0 : switchContext.userInitiated) !== null && _a !== void 0 ? _a : false;
|
||
const { mediaQuery } = mediaSink;
|
||
const { desiredRate, isIframeRate } = mediaQuery;
|
||
const rewindRateChange = isIframeRate && iframeMachine.isStarted && desiredRate && desiredRate < 0 && desiredRate !== iframeMachine.iframeRate;
|
||
if (userInitiated || rewindRateChange) {
|
||
logger.info(`creating flushBeforeAppend > userInitiated=${userInitiated}, rewindRateChange=${rewindRateChange} :: iframeMachine.isStarted=${iframeMachine.isStarted}, iframeMachine.iframeRate=${iframeMachine.iframeRate}, desiredRate=${desiredRate}`);
|
||
mediaFragmentCacheEntity.flushBeforeAppend = { start: 0, end: Number.POSITIVE_INFINITY };
|
||
}
|
||
}));
|
||
// UpdatePTSDTS is expensive on startup
|
||
// try to parallelize with key request and/or fragment request
|
||
if (type === MediaOptionType.Variant) {
|
||
fetchAndParse$ = fetchAndParse$.pipe(tap((sbData) => {
|
||
const initPTSInfo = updateInitPTS(logger, pipelineContext, sbData, fragmentPicker.discoSeqNum);
|
||
updatePTSDTS(logger, pipelineContext, sbData, initPTSInfo);
|
||
}));
|
||
}
|
||
else {
|
||
const initPTSInfo$ = waitFor(rootPlaylistQuery.initPTS$(fragmentPicker.discoSeqNum), (initPTSInfo) => {
|
||
const iframeMode = mediaSink.mediaQuery.isIframeRate;
|
||
return initPTSInfo != null && (iframeMode || !initPTSInfo.iframeMode);
|
||
});
|
||
fetchAndParse$ = forkJoin([fetchAndParse$, initPTSInfo$]).pipe(map(([sbData, initPTSInfo]) => {
|
||
updatePTSDTS(logger, pipelineContext, sbData, initPTSInfo);
|
||
return sbData;
|
||
}));
|
||
}
|
||
return forkJoin([prefetchKey$, fetchAndParse$]).pipe(map((res) => res[1]));
|
||
}
|
||
})).pipe(map((sbDataTuple) => getConsumerInput(logger, pipelineContext, fragmentPicker.discoSeqNum, sbDataTuple)), switchMap((consumerInput) => {
|
||
if (!consumerInput) {
|
||
// Potentially dangerous (unbounded loop) if we keep selecting the same fragment
|
||
logger.info('Attempting fragment reselection because of complete overlap with updated details');
|
||
const updatedDetailsAndContext = getUpdatedDetailsAndContext(rootPlaylistQuery, detailsAndContext, logger);
|
||
return chooseAndFetchFragments(logger, fragmentPicker, pipelineContext, action, updatedDetailsAndContext);
|
||
}
|
||
return of(consumerInput);
|
||
}));
|
||
}
|
||
function getUpdatedDetailsAndContext(rootPlaylistQuery, detailsAndContext, logger) {
|
||
const enabledMediaKeys = rootPlaylistQuery.enabledMediaOptionKeys;
|
||
const updatedDetailsAndContext = [null, null];
|
||
const detailsInfo = [null, null];
|
||
AVMediaOptionTypes.map((type) => {
|
||
var _a, _b;
|
||
if (isEnabledMediaOption(enabledMediaKeys[type])) {
|
||
const libraryQuery = createMediaLibraryQuery(enabledMediaKeys[type]);
|
||
const detailsEntity = libraryQuery.mediaOptionDetailsEntity;
|
||
detailsInfo[type] = (_a = detailsEntity.mediaOptionDetails) === null || _a === void 0 ? void 0 : _a.ptsKnown;
|
||
updatedDetailsAndContext[type] = { detailsEntity, switchContext: (_b = detailsAndContext[type]) === null || _b === void 0 ? void 0 : _b.switchContext };
|
||
}
|
||
});
|
||
logger.info(`retrieve updated details entity ${JSON.stringify(detailsInfo)}`);
|
||
return updatedDetailsAndContext;
|
||
}
|
||
/**
|
||
* Update initPTS info
|
||
* @param vTuple the parsed fragment information for the variant
|
||
*/
|
||
function updateInitPTS(logger, pipelineContext, vTuple, activeDiscoSeqNum) {
|
||
var _a, _b, _c;
|
||
if (!vTuple) {
|
||
return null;
|
||
}
|
||
const { rootPlaylistService: rootService, rootPlaylistQuery: rootQuery } = pipelineContext;
|
||
const itemId = rootQuery.itemId;
|
||
const variantData = vTuple[1];
|
||
const fragIsIframe = variantData.iframe;
|
||
// Favor non-iframe initPTS if available
|
||
let initPTSInfo = rootQuery.getInitPTS(activeDiscoSeqNum);
|
||
if (initPTSInfo == null || (!fragIsIframe && initPTSInfo.iframeMode)) {
|
||
const variantDTS = (_a = variantData.startDtsTs) !== null && _a !== void 0 ? _a : null;
|
||
if (variantDTS == null) {
|
||
logger.warn('updateInitPTS: Variant data missing.');
|
||
return null;
|
||
}
|
||
let timelineOffset = (_b = variantData.timelineOffset) !== null && _b !== void 0 ? _b : 0;
|
||
if (fragIsIframe) {
|
||
logger.info(`updateInitPTS: initPTS timelineOffset is appendData iframeOriginalStart ${vTuple[1].iframeOriginalStart}`);
|
||
timelineOffset = (_c = variantData.iframeOriginalStart) !== null && _c !== void 0 ? _c : 0;
|
||
}
|
||
else {
|
||
logger.info(`updateInitPTS: initPTS timelineOffset is appendData timelineOffset ${vTuple[1].timelineOffset}`);
|
||
}
|
||
if (variantDTS.timescale < 1000) {
|
||
// this scaleup is especially useful when traversing 23.976/24 fps discos
|
||
variantDTS.timescale = variantDTS.timescale * 1000;
|
||
variantDTS.baseTime = variantDTS.baseTime * 1000;
|
||
}
|
||
const playlistTS = convertSecondsToTimestamp(timelineOffset, variantDTS.timescale);
|
||
const offsetTimestamp = { baseTime: variantDTS.baseTime - playlistTS.baseTime, timescale: variantDTS.timescale };
|
||
logger.info(`initPTS[${activeDiscoSeqNum}] ${JSON.stringify({ variantDTS, timelineOffset, offsetTimestamp, fragIsIframe })}`);
|
||
rootService.setInitPTS(itemId, activeDiscoSeqNum, variantDTS, timelineOffset, offsetTimestamp, fragIsIframe);
|
||
initPTSInfo = { variantDTS, timelineOffset, offsetTimestamp, iframeMode: fragIsIframe };
|
||
}
|
||
return initPTSInfo;
|
||
}
|
||
function updatePTSDTS(logger, pipelineContext, sbData, initPTSInfo) {
|
||
const { mediaLibraryService, rootPlaylistQuery, mediaSink } = pipelineContext;
|
||
const itemId = rootPlaylistQuery.itemId;
|
||
if (initPTSInfo == null) {
|
||
return;
|
||
}
|
||
// Don't update if we're in normal playback and we have an i-frame initPTS
|
||
const curIframeMode = mediaSink.mediaQuery.isIframeRate;
|
||
if (!curIframeMode && initPTSInfo.iframeMode) {
|
||
logger.warn('updatePTSDTS iframeMode mismatch');
|
||
return;
|
||
}
|
||
// Update playlist offsets. Never update for main in i-frame mode
|
||
if (sbData && !isFiniteNumber(sbData[1].iframeMediaDuration)) {
|
||
const tstart = performance.now();
|
||
mediaLibraryService.updatePTSDTS(itemId, sbData[1].mediaOptionId, initPTSInfo, sbData[1]);
|
||
logger.info(`updatePTSDTS took ${Math.round(performance.now() - tstart)}ms`);
|
||
}
|
||
}
|
||
function getConsumerInput(logger, pipelineContext, activeDiscoSeqNum, sbDataTuple) {
|
||
const { rootPlaylistQuery: rootQuery, mediaSink, config } = pipelineContext;
|
||
const mediaQuery = mediaSink.mediaQuery;
|
||
const iframeMode = mediaQuery.isIframeRate;
|
||
const initPTSInfo = rootQuery.getInitPTS(activeDiscoSeqNum);
|
||
if (initPTSInfo == null) {
|
||
logger.warn('No initPTS info found');
|
||
return null;
|
||
}
|
||
const variantData = sbDataTuple[MediaOptionType.Variant];
|
||
const variantFragData = variantData === null || variantData === void 0 ? void 0 : variantData[1];
|
||
if (variantFragData && variantFragData.iframe !== iframeMode) {
|
||
logger.warn(`frag mediaSeqNum ${variantFragData.mediaSeqNum} discoSeqNum ${variantFragData.discoSeqNum} mediaOptionId ${variantFragData.mediaOptionId} doesn't match mediaSink's iframeMode ${iframeMode}; discard`);
|
||
return null; // repick frag since the parsed one doesn't match mediaSink's iframe mode
|
||
}
|
||
// Build output
|
||
const appendDataTuple = [null, null];
|
||
if (variantData) {
|
||
const [initSeg, dataSeg] = variantData;
|
||
let offsetTimestamp = initPTSInfo.offsetTimestamp;
|
||
if (iframeMode) {
|
||
// Always offset video
|
||
const dts = dataSeg.startDtsTs;
|
||
const timelineOffset = dataSeg.timelineOffset;
|
||
const playlistTS = convertSecondsToTimestamp(timelineOffset, dts.timescale);
|
||
offsetTimestamp = { baseTime: dts.baseTime - playlistTS.baseTime, timescale: dts.timescale };
|
||
}
|
||
appendDataTuple[SourceBufferType.Variant] = { initSeg, dataSeg, offsetTimestamp };
|
||
}
|
||
const audioData = sbDataTuple[MediaOptionType.AltAudio];
|
||
if (audioData != null) {
|
||
const [initSeg, dataSeg] = audioData;
|
||
appendDataTuple[SourceBufferType.AltAudio] = { initSeg, dataSeg, offsetTimestamp: initPTSInfo.offsetTimestamp };
|
||
}
|
||
// Update in flight fragments
|
||
const inFlightFrags = appendDataTuple.map((data, type) => {
|
||
var _a, _b;
|
||
const dataSeg = data === null || data === void 0 ? void 0 : data.dataSeg;
|
||
if (dataSeg) {
|
||
const { itemId, mediaOptionId, mediaSeqNum, discoSeqNum, startPts, endPts, duration, iframe } = dataSeg;
|
||
const { offsetTimestamp } = data;
|
||
const startPTSSec = diffSeconds(startPts, offsetTimestamp);
|
||
const endPTSSec = diffSeconds(endPts, offsetTimestamp);
|
||
const libraryQuery = createMediaLibraryQuery(dataSeg);
|
||
logger.info(`${MediaOptionNames[type]} aboutToAppend iframe:${dataSeg.iframeMediaDuration != null} ${stringifyWithPrecision({
|
||
mediaOptionId,
|
||
mediaSeqNum,
|
||
discoSeqNum,
|
||
offsetTimestamp,
|
||
startPTSSec,
|
||
endPTSSec,
|
||
startPts,
|
||
endPts,
|
||
})} buffered:${stringifyWithPrecision(mediaQuery.getBufferedRangeByType(type))}`);
|
||
const videoSegment = appendDataTuple[0];
|
||
const noDroppedFrames = !videoSegment || !videoSegment.dataSeg.dropped;
|
||
if (noDroppedFrames && !dataSeg.flushBeforeAppend && ((_b = (_a = mediaQuery.getBufferInfo(startPTSSec, config.maxBufferHole)[type]) === null || _a === void 0 ? void 0 : _a.buffered) === null || _b === void 0 ? void 0 : _b.len) >= endPTSSec - startPTSSec) {
|
||
logger.warn(`${MediaOptionNames[type]} Discarding append due to complete overlap with existing buffer`);
|
||
appendDataTuple[type] = null;
|
||
return null;
|
||
}
|
||
return {
|
||
start: startPTSSec,
|
||
duration: iframe ? duration : endPTSSec - startPTSSec,
|
||
itemId,
|
||
mediaOptionId,
|
||
mediaSeqNum,
|
||
discoSeqNum,
|
||
targetDuration: libraryQuery.mediaOptionDetails.targetduration,
|
||
};
|
||
}
|
||
return null;
|
||
});
|
||
if (inFlightFrags.every((f) => !f)) {
|
||
return null;
|
||
}
|
||
return { appendDataTuple, inFlightFrags, initPTSInfo };
|
||
}
|
||
/**
|
||
* @brief The media consumer side
|
||
* transform consumerInput to boolean indicating whether we actually appended anything
|
||
*/
|
||
const mediaConsumerEpic = (context) => (consumerInput) => {
|
||
const { rootPlaylistQuery: rootQuery, rootPlaylistService: rootService, mediaSink, legibleSystemAdapter, statsService, rtcService } = context;
|
||
return consumerInput.pipe(tag('mediaConsumerEpic.in'),
|
||
// Do Append
|
||
switchMap((appendData) => {
|
||
if (!appendData) {
|
||
return of(false);
|
||
}
|
||
const { appendDataTuple, inFlightFrags, initPTSInfo } = appendData;
|
||
const { offsetTimestamp } = initPTSInfo;
|
||
inFlightFrags.forEach((frag, type) => {
|
||
if (frag) {
|
||
rootService.updateInflightFrag(frag.itemId, type, frag, 'appending', null);
|
||
}
|
||
});
|
||
// Pass rootQuery.itemStartOffset into appendData.
|
||
// add samples based on captiondata
|
||
appendDataTuple.forEach((sbAppendData) => {
|
||
if (sbAppendData) {
|
||
const dataSeg = sbAppendData.dataSeg;
|
||
legibleSystemAdapter.addLegibleSamples(offsetTimestamp, dataSeg.captionData, dataSeg.id3Samples, dataSeg.endPts);
|
||
}
|
||
});
|
||
return mediaSink
|
||
.appendData(appendDataTuple, (sbAdapter, mediaOptionType, mediaOptionId, config, meQuery) => {
|
||
var _a;
|
||
const targetDuration = (_a = inFlightFrags[mediaOptionType].targetDuration) !== null && _a !== void 0 ? _a : 10;
|
||
return addAppendErrorHandlingPolicy(mediaSink, sbAdapter, mediaOptionType, mediaOptionId, targetDuration, config, rootQuery, rootService, meQuery);
|
||
}, rootQuery.highestVideoCodec)
|
||
.pipe(map((metrics) => {
|
||
var _a, _b;
|
||
inFlightFrags.forEach((frag, type) => {
|
||
if (frag) {
|
||
rootService.updateInflightFrag(frag.itemId, type, frag, 'appended', null);
|
||
}
|
||
});
|
||
const metric = metrics.filter((metric) => (metric === null || metric === void 0 ? void 0 : metric.fragmentType) === MediaOptionType.Variant);
|
||
if (metric.length) {
|
||
statsService.setBufferMetric(metric[0]);
|
||
rtcService === null || rtcService === void 0 ? void 0 : rtcService.handleFragBuffered(metric[0]);
|
||
}
|
||
const audioAppendData = appendDataTuple[SourceBufferType.AltAudio];
|
||
if (((_a = audioAppendData === null || audioAppendData === void 0 ? void 0 : audioAppendData.dataSeg) === null || _a === void 0 ? void 0 : _a.flushBeforeAppend) || isFiniteNumber((_b = audioAppendData === null || audioAppendData === void 0 ? void 0 : audioAppendData.dataSeg) === null || _b === void 0 ? void 0 : _b.switchPosition)) {
|
||
const { itemId, mediaOptionId } = audioAppendData.dataSeg;
|
||
rootService.setEnabledMediaOptionSwitchContextByType(itemId, MediaOptionType.AltAudio, mediaOptionId, undefined);
|
||
}
|
||
return true;
|
||
}), addCreateSourceBufferErrorHandlingPolicy(mediaSink, rootService, rootQuery));
|
||
}));
|
||
};
|
||
|
||
/**
|
||
* Convert seek to date values into media position seeks
|
||
*/
|
||
/**
|
||
* @brief Get the media time for a given date
|
||
* @param dateMediaTimeTuple DateTimeMs, mediaTimeSec pairs sorted in ascending order
|
||
* @param searchDate date to resolve
|
||
* @returns media position. 0 if earlier than beginning of playlist
|
||
*/
|
||
function resolveDateToPTS(dateMediaTimeTuple, searchDate) {
|
||
var _a;
|
||
if (!dateMediaTimeTuple || dateMediaTimeTuple.length === 0) {
|
||
return 0;
|
||
}
|
||
// Scan for the max startDateTime that is smaller than the searchDate
|
||
const descendingTuple = [...dateMediaTimeTuple].sort((a, b) => b[0] - a[0]);
|
||
const searchDt = searchDate.getTime();
|
||
const closest = (_a = descendingTuple.find(([dt]) => searchDt >= dt)) !== null && _a !== void 0 ? _a : descendingTuple[descendingTuple.length - 1];
|
||
const [closestDt, startPTSSec] = closest;
|
||
const resolvedTime = startPTSSec + (searchDt - closestDt) / 1000;
|
||
return Math.max(0, resolvedTime);
|
||
}
|
||
/**
|
||
* @brief Get the date for a given position in media
|
||
* @param dateMediaTimeTuple DateTimeMs, mediaTimeSec pairs sorted in ascending order
|
||
* @param ptsSeconds time in media seconds
|
||
* @returns Date associated with this time. undefined if no date available
|
||
*/
|
||
function resolvePTSToDate(dateMediaTimeTuple, ptsSeconds) {
|
||
var _a;
|
||
if (!dateMediaTimeTuple || dateMediaTimeTuple.length === 0) {
|
||
return undefined;
|
||
}
|
||
const descendingTuple = [...dateMediaTimeTuple].sort((a, b) => b[1] - a[1]);
|
||
const closest = (_a = descendingTuple.find(([_, mediaTime]) => ptsSeconds >= mediaTime)) !== null && _a !== void 0 ? _a : descendingTuple[descendingTuple.length - 1];
|
||
const [closestDt, startPTSSec] = closest;
|
||
const resolvedDate = new Date(closestDt + (ptsSeconds - startPTSSec) * 1000);
|
||
return resolvedDate;
|
||
}
|
||
function resolveSeekToDateAsync(seekTo, rootQuery, libService) {
|
||
return rootQuery.enabledMediaOptionByType$(MediaOptionType.Variant).pipe(switchMap((option) => {
|
||
return libService.getQueryForOption(option).mediaOptionDetails$;
|
||
}), map((details) => resolveDateToPTS(details.dateMediaTimePairs, seekTo)), take(1));
|
||
}
|
||
/**
|
||
* Return seek position within media
|
||
* @param seekTo
|
||
* @param rootPlaylistQuery
|
||
* @returns
|
||
*/
|
||
function resolveSeekToAsync(seekTo, isFirstSeek, config, rootPlaylistQuery, libService, logger) {
|
||
// seekTo value meanings:
|
||
// non-finite = auto seek position. LIVE: live edge, VOD: startTimeOffset if available or start of item
|
||
// 0 = if (LIVE && liveEdgeForZeroStartPositon), live edge, else 0
|
||
// positive = position in media timeline
|
||
// negative = LIVE: live edge, VOD: position relative to end of playlist
|
||
let resolvedSeekTo$ = rootPlaylistQuery.enabledMediaOptionByType$(MediaOptionType.Variant).pipe(switchMap((variant) => libService.getQueryForOption(variant).mediaOptionDetails$), filter((details) => isFiniteNumber(details === null || details === void 0 ? void 0 : details.totalduration)), take(1), map((details) => {
|
||
const hasEndTag = !details.liveOrEvent;
|
||
const totalDuration = details.totalduration;
|
||
const itemStartOffset = rootPlaylistQuery.itemStartOffset;
|
||
if (hasEndTag) {
|
||
if (!isFiniteNumber(seekTo)) {
|
||
const offset = isFiniteNumber(details.startTimeOffset) ? details.startTimeOffset : 0;
|
||
return itemStartOffset + offset;
|
||
}
|
||
else if (seekTo >= 0) {
|
||
// seekTo positive values are never pending
|
||
// seekTo 0 is pending only when liveEdgeForZeroStartPositon is opted in
|
||
return seekTo;
|
||
}
|
||
else {
|
||
// seekTo negative values
|
||
const offset = totalDuration + seekTo;
|
||
return itemStartOffset + offset;
|
||
}
|
||
}
|
||
else {
|
||
// live and event (no EXT-X-END tag)
|
||
if (!isFiniteNumber(seekTo) || seekTo < 0 || (seekTo === 0 && config.liveEdgeForZeroStartPositon)) {
|
||
// catch up to live edge
|
||
return computeLivePosition(0, details, config, logger);
|
||
}
|
||
else {
|
||
// isFinite(seekTo) && seekTo >= 0 && (seekTo !== 0 || !liveEdgeForZeroStartPosition)
|
||
// sanitize seek value
|
||
return sanitizeLiveSeek(seekTo, details, config, logger);
|
||
}
|
||
}
|
||
}));
|
||
if (isFirstSeek) {
|
||
resolvedSeekTo$ = resolvedSeekTo$.pipe(tap((seekTo) => {
|
||
logger.qe({ critical: true, name: 'initialSeekSet', data: { seekTo, source: 'rootPlaylistQuery.pendingSeek' } });
|
||
}));
|
||
}
|
||
return resolvedSeekTo$;
|
||
}
|
||
/**
|
||
* Epic that just updates pending seek in active item from hls service.
|
||
* @param rootQuery
|
||
* @param libService
|
||
* @returns
|
||
*/
|
||
function userSeekEpic(itemQueue, rootService) {
|
||
return (userSeek$) => userSeek$.pipe(filterNullOrUndefined(), switchMap((seekTo) => forkJoin([
|
||
of(seekTo),
|
||
itemQueue.activeItemById$.pipe(filterNullOrUndefined(), switchMap((entity) => {
|
||
return rootService.getQueryForId(entity.itemId).rootPlaylistEntity$;
|
||
}), take(1)),
|
||
])), map(([seekTo, activeItem]) => {
|
||
rootService.setPendingSeek(activeItem.itemId, seekTo);
|
||
return seekTo;
|
||
}));
|
||
}
|
||
/**
|
||
* Epic that handles requested seeks made to the current active item and
|
||
* then updates mediaSink position. Expected to be subscribed on active item change
|
||
*
|
||
* @param config configuration for seek resolution
|
||
* @param logger logger object
|
||
* @param mediaSink MediaSink object
|
||
* @param rootQuery Query for the active item
|
||
* @param libService Media library service
|
||
* @returns
|
||
*/
|
||
function pendingSeekEpic(config, logger, mediaSink, rootService, rootQuery, libService) {
|
||
logger = logger.child({ name: 'seek' });
|
||
return (pendingSeek$) => pendingSeek$.pipe(switchMap((pendingSeek, index) => {
|
||
if (pendingSeek == null) {
|
||
return EMPTY;
|
||
}
|
||
logger.info(`got pendingSeek=${pendingSeek}`);
|
||
const gotNewerSeek$ = mediaSink.mediaQuery.seekTo$.pipe(skip(1), // seekTo$ always emits the active seekTo value to new subscribers (which is populated while seeking, null otherwise)
|
||
filterNullOrUndefined());
|
||
return resolveSeek(pendingSeek, index === 0, config, logger, rootQuery, libService).pipe(finalize$1(() => {
|
||
rootService.setPendingSeek(rootQuery.itemId, undefined);
|
||
}), takeUntil(gotNewerSeek$));
|
||
}), tap((pendingSeek) => {
|
||
logger.info(`resolved pendingSeek=${pendingSeek}`);
|
||
if (isFiniteNumber(pendingSeek)) {
|
||
mediaSink.seekTo = pendingSeek;
|
||
}
|
||
}));
|
||
}
|
||
function resolveSeek(seekTo, isFirst, config, logger, rootQuery, libService) {
|
||
if (seekTo == null) {
|
||
return EMPTY;
|
||
}
|
||
if (seekTo instanceof Date) {
|
||
return resolveSeekToDateAsync(seekTo, rootQuery, libService);
|
||
}
|
||
else {
|
||
return resolveSeekToAsync(seekTo, isFirst, config, rootQuery, libService, logger);
|
||
}
|
||
}
|
||
|
||
function updateEnabledMediaOptionsAsync(rootPlaylistService, itemId) {
|
||
return timer(0).pipe(map(() => {
|
||
rootPlaylistService.logger.debug(`[updateEnabledMediaOptions] itemId ${itemId}`);
|
||
rootPlaylistService.updateEnabledMediaOptions(itemId);
|
||
}));
|
||
}
|
||
/**
|
||
* Handle end of the pipeline error. Update the media options as needed based
|
||
* on nextMediaOptionsKeys (updated by ErrorHandlingPolicies)
|
||
*
|
||
* Should always be the last operator.
|
||
*/
|
||
function mediaPipelineHandleError(pipelineContext) {
|
||
const { logger, rootPlaylistService, rootPlaylistQuery } = pipelineContext;
|
||
const itemId = rootPlaylistQuery.itemId;
|
||
return (source) => source.pipe(retryWhen((errors) => errors.pipe(mergeMap((err) => {
|
||
logger.error(`Got error in pipeline ${err.message} fatal:${err === null || err === void 0 ? void 0 : err.fatal} handled:${err === null || err === void 0 ? void 0 : err.handled}`);
|
||
if (!(err instanceof HlsError) || err.fatal) {
|
||
throw err;
|
||
}
|
||
if (!err.handled) {
|
||
return EMPTY; // Do nothing, wait for next emit
|
||
}
|
||
// Not fatal, we can re-subscribe to source after updating media options
|
||
return updateEnabledMediaOptionsAsync(rootPlaylistService, itemId);
|
||
}))));
|
||
}
|
||
/**
|
||
* Core fetch and append epic. Listens to what details are available from the playlist service,
|
||
* and the playback and buffering state from the mediaPlaybackService. It can respond to either
|
||
* by changing downloading fragments or initiating track switching.
|
||
*
|
||
* store with these selections
|
||
* @param mediaPlaybackService
|
||
* @param config Configuration needed load and playback
|
||
*/
|
||
const mediaFragmentPipelineEpic = () => (pipelineActionSource$) => {
|
||
return pipelineActionSource$.pipe(tag('mediaFragmentPipelineEpic.in'), switchMap((pipelineContext) => {
|
||
if (!pipelineContext) {
|
||
return EMPTY;
|
||
}
|
||
const { logger, config, platformService, rootPlaylistService, rootPlaylistQuery, keySystemAdapter, mediaSink, mediaParser, gaplessInstance, mediaLibraryService } = pipelineContext;
|
||
const { itemId } = rootPlaylistQuery;
|
||
const { mediaQuery } = mediaSink;
|
||
logger.info(`mediaFragmentPipelineEpic start ${itemId}`);
|
||
const keyStatusChange$ = keySystemAdapter.keyStatusChange$.pipe(keyStatusChangeEpic(pipelineContext));
|
||
const platformQuery = platformService.getQuery();
|
||
const displayChange$ = platformQuery.displaySupportsHdr$.pipe(distinctUntilChanged(), switchMap((capable) => {
|
||
logger.info(`hdr display change=${capable}`);
|
||
rootPlaylistService.setHDRPreference(itemId, capable, true);
|
||
return EMPTY;
|
||
}));
|
||
const viewPortChange$ = platformQuery.viewportInfo$.pipe(distinctUntilChanged((a, b) => a && b && a.width === b.width && a.height === b.height), tap((viewportInfo) => {
|
||
logger.info(`viewport change=${JSON.stringify(viewportInfo)}, useViewportSizeForLevelCap: ${config.useViewportSizeForLevelCap}`);
|
||
if (config.useViewportSizeForLevelCap) {
|
||
rootPlaylistService.setViewportInfo(itemId, viewportInfo);
|
||
}
|
||
}), switchMapTo(EMPTY));
|
||
const needsMediaReset$ = combineQueries([rootPlaylistQuery.hdrMode$.pipe(distinctUntilChanged()), rootPlaylistQuery.maxHdcpLevel$.pipe(distinctUntilChanged())]).pipe(switchMap(([hdrMode, maxHdcpLevel]) => {
|
||
logger.info(`hdrMode=${hdrMode} maxHdcpLevel=${maxHdcpLevel}`);
|
||
// if not in gapless mode and the current item is not offset on the timeline, reset the media source
|
||
if (!gaplessInstance.inGaplessMode && rootPlaylistQuery.itemStartOffset === 0) {
|
||
mediaSink.resetMediaSource();
|
||
mediaParser.reset();
|
||
}
|
||
return EMPTY;
|
||
}));
|
||
// Register this separately so errors here that may be recovered don't cause us to re-subscribe to the platform updates
|
||
const mediaPipes$ = merge(
|
||
// prettier-ignore
|
||
handleEnabledMediaOptions(pipelineContext), makeAVPipe(pipelineContext), makeSubtitlePipe(pipelineContext), keyStatusChange$).pipe(
|
||
// This updates enabled media options whenever an emit happens
|
||
mapTo(undefined), mediaPipelineHandleError(pipelineContext));
|
||
const anchor$ = mediaQuery.seekTo$.pipe(filter((seekTo) => isFiniteNumber(seekTo === null || seekTo === void 0 ? void 0 : seekTo.pos)), distinctUntilChanged((a, b) => Math.abs(a.pos - b.pos) < Number.EPSILON), // Can choose different threshold
|
||
switchMap((anchorTime) => {
|
||
rootPlaylistService.setAnchorTime(itemId, anchorTime);
|
||
return EMPTY;
|
||
}));
|
||
// Non-functional, Logs compatible levels after playback starts
|
||
const logCompatibleLevels$ = mediaQuery.gotPlaying$.pipe(filter((playing) => playing), tap((_) => {
|
||
const mediaOptionQuery = rootPlaylistQuery.mediaOptionListQueries[MediaOptionType.Variant];
|
||
const compatibleMediaLevels = mediaOptionQuery.filteredMediaOptionList;
|
||
logger.qe({ critical: true, name: 'compatibleLevels', data: { numLevels: compatibleMediaLevels.length } });
|
||
compatibleMediaLevels.forEach((level) => {
|
||
const { mediaOptionId, bandwidth, bitrate, videoCodec, audioCodec, height, width, videoRange, iframes } = level;
|
||
logger.qe({ critical: true, name: 'manifestLevel', data: { mediaOptionId, bandwidth, bitrate, videoCodec, audioCodec, height, width, videoRange, iframes } });
|
||
});
|
||
}), take(1), switchMapTo(EMPTY));
|
||
return merge(
|
||
// Seeking / position adjustments
|
||
rootPlaylistQuery.pendingSeek$.pipe(pendingSeekEpic(config, logger, mediaSink, rootPlaylistService, rootPlaylistQuery, mediaLibraryService)), ensurePlaybackWithinWindow(pipelineContext), // Adjust position within live window
|
||
anchor$,
|
||
// Download pipe
|
||
mediaPipes$,
|
||
// Following observables monitor state changes that may modify enabled media options
|
||
displayChange$, viewPortChange$, needsMediaReset$, desiredRateChange(pipelineContext), capToEnabledIframeOption(pipelineContext), logCompatibleLevels$).pipe(tag('mediaFragmentPiplineEpic.emit'), mapTo(undefined));
|
||
}));
|
||
};
|
||
/**
|
||
* update expected SbCount and load all media playlists
|
||
*/
|
||
function handleEnabledMediaOptions(pipelineContext) {
|
||
const { logger, rootPlaylistQuery, mediaSink } = pipelineContext;
|
||
const updateExpectedSbCount$ = rootPlaylistQuery.enabledMediaOptions$.pipe(filterNullOrUndefined(), tap((enabledOptions) => {
|
||
const altAudio = enabledOptions[MediaOptionType.AltAudio];
|
||
const expectedSbCount = isEnabledMediaOption(altAudio) && (altAudio === null || altAudio === void 0 ? void 0 : altAudio.url) != null ? 2 : 1;
|
||
mediaSink.setExpectedSbCount(expectedSbCount);
|
||
}));
|
||
const setBufferTargetDuration$ = forkJoin([
|
||
rootPlaylistQuery.enabledMediaOptionByType$(MediaOptionType.Variant).pipe(filter((option) => isEnabledMediaOption(option)), switchMap((option) => {
|
||
const query = createMediaLibraryQuery(option);
|
||
return query.mediaOptionDetails$;
|
||
}), take(1) // Just update on first load. Target duration should be consistent across variants
|
||
),
|
||
waitFor(mediaSink.mediaQuery.updating$, (x) => x), // On startup don't need to do this immediately
|
||
]).pipe(observeOn(asyncScheduler), tap(([details]) => {
|
||
mediaSink.bufferMonitorTargetDuration = details.targetduration;
|
||
}));
|
||
const loadMediaOptions = MediaOptionTypes.map((mediaOptionType) => {
|
||
return rootPlaylistQuery.enabledMediaOptionByType$(mediaOptionType).pipe(tag('mediaOptionRetrieve.switch'), switchMap((option) => {
|
||
if (!option || !option.url || !isEnabledMediaOption(option)) {
|
||
return EMPTY;
|
||
}
|
||
const allowRefresh$ = mediaSink.mediaQuery.desiredRate$.pipe(map((rate) => rate !== 0), distinctUntilChanged());
|
||
// always request first copy. this will either return cached value or request from server
|
||
return retrieveMediaOptionDetails(pipelineContext, option).pipe(tag('mediaOptionRetrieve.first'), switchMapTo(allowRefresh$), switchMap((allowRefresh) => {
|
||
if (!allowRefresh) {
|
||
return EMPTY; // Don't refresh while in pause
|
||
}
|
||
// refresh
|
||
return refreshMediaOptionDetails(pipelineContext, option);
|
||
}));
|
||
}));
|
||
});
|
||
return merge(updateExpectedSbCount$, setBufferTargetDuration$, merge(...loadMediaOptions)).pipe(switchMapTo(EMPTY));
|
||
}
|
||
function makeSubtitlePipe(pipelineContext) {
|
||
const { rootPlaylistQuery, mediaSink } = pipelineContext;
|
||
const mediaQuery = mediaSink.mediaQuery;
|
||
return waitFor(mediaQuery.mediaElementEntity$, (entity) => entity).pipe(switchMap((_) => {
|
||
return rootPlaylistQuery.anchorTime$.pipe(tag('anchorTime.subtitle.in'), filter((anchorTime) => isFiniteNumber(anchorTime === null || anchorTime === void 0 ? void 0 : anchorTime.pos)), subtitleSelectionEpic(pipelineContext), subtitleLoadAndProcessEpic(pipelineContext));
|
||
}));
|
||
}
|
||
function keyStatusChangeEpic(keyLoadContext) {
|
||
return (source) => source.pipe(switchMap((event) => {
|
||
const { decryptdata, status, error } = event;
|
||
const { rootPlaylistQuery } = keyLoadContext;
|
||
if (status === 'needs-renewal') {
|
||
return loadKey(keyLoadContext, decryptdata, null);
|
||
}
|
||
else if (status === 'error' && (error instanceof KeyRequestError || error instanceof KeyRequestTimeoutError) && !error.handled) {
|
||
// Don't actually retry, we're just doing penalty box stuff and updating enabled options
|
||
const { rootPlaylistService, keySystemAdapter } = keyLoadContext;
|
||
return handleKeyError(error, 0, null, rootPlaylistService, rootPlaylistQuery, keySystemAdapter.ksQuery);
|
||
}
|
||
return EMPTY;
|
||
}), switchMap(() => EMPTY));
|
||
}
|
||
/**
|
||
* @brief subtitle selection
|
||
*/
|
||
const subtitleSelectionEpic = (pipelineContext) => (anchorTimeSource$) => {
|
||
const { rootPlaylistQuery, rootPlaylistService, legibleSystemAdapter } = pipelineContext;
|
||
// setup HTML5 texttracks
|
||
const enabledMediaOption = rootPlaylistQuery.enabledAlternateMediaOptionByType(MediaOptionType.Subtitle);
|
||
if (!legibleSystemAdapter.gotTracks) {
|
||
const availableAlternateMediaOptions = rootPlaylistQuery.preferredMediaOptions[MediaOptionType.Subtitle];
|
||
legibleSystemAdapter.setTracks(availableAlternateMediaOptions, enabledMediaOption, rootPlaylistQuery.getDisabledMediaOption(MediaOptionType.Subtitle));
|
||
}
|
||
else {
|
||
legibleSystemAdapter.selectedTrack = enabledMediaOption;
|
||
}
|
||
return anchorTimeSource$.pipe(tag('subtitleEpic.select.in'), switchMap(() => {
|
||
return merge(legibleSystemAdapter.nativeSubtitleTrackChange$.pipe(
|
||
// mediaOption selected via native media element video controller UI
|
||
switchMap((mediaOption) => {
|
||
if (mediaOption.mediaOptionId !== legibleSystemAdapter.selectedMediaOption.mediaOptionId) {
|
||
rootPlaylistService.setEnabledMediaOptionByType(mediaOption.itemId, MediaOptionType.Subtitle, mediaOption);
|
||
}
|
||
return EMPTY;
|
||
})), rootPlaylistQuery
|
||
.enabledMediaOptionByType$(MediaOptionType.Subtitle)
|
||
.pipe(
|
||
// mediaOption selected via hls.js API
|
||
map((option) => {
|
||
// option may be null initially
|
||
const altMediaOption = isEnabledMediaOption(option) ? rootPlaylistQuery.alternateMediaOptionById(MediaOptionType.Subtitle, option.mediaOptionId) : option;
|
||
legibleSystemAdapter.selectedMediaOption = altMediaOption;
|
||
return altMediaOption;
|
||
}))
|
||
.pipe(distinctUntilChanged((a, b) => {
|
||
return (a === null || a === void 0 ? void 0 : a.mediaOptionId) === (b === null || b === void 0 ? void 0 : b.mediaOptionId);
|
||
})));
|
||
}), tag('subtitleEpic.select.emit'));
|
||
};
|
||
const loadOneSubtitleFrag = (pipelineContext, initPTS, loadingFrag) => {
|
||
const { rootPlaylistQuery, legibleSystemAdapter } = pipelineContext;
|
||
return defer(() => {
|
||
return retrieveSubtitleFragmentCacheEntity(pipelineContext, initPTS, loadingFrag).pipe(map(({ initPTS, data, mediaFragment }) => {
|
||
const enabledAltMediaOption = rootPlaylistQuery.enabledAlternateMediaOptionByType(MediaOptionType.Subtitle);
|
||
const parsedCueRecord = processSubtitlePayload(enabledAltMediaOption, mediaFragment, initPTS, data, legibleSystemAdapter);
|
||
return { frag: mediaFragment, cueRange: parsedCueRecord };
|
||
}));
|
||
});
|
||
};
|
||
const loadSubtitleBatch = (pipelineContext, initPTS, findFragsResult, details) => {
|
||
const legibleServiceAdapter = pipelineContext.legibleSystemAdapter;
|
||
const nextFrags = findFragsResult.foundFrags;
|
||
return from(nextFrags).pipe(
|
||
// emit next frags to load in a sequence
|
||
map((frag) => {
|
||
return loadOneSubtitleFrag(pipelineContext, initPTS, frag).pipe(delayWhen((parsedFragResult) => {
|
||
return legibleServiceAdapter.checkReadyToLoadNextSubtitleFragment$(frag, nextFrags).pipe(filter((x) => x));
|
||
}));
|
||
}), mergeAll(pipelineContext.config.vttConcurrentLoadCount), tap((parsedFragResult) => {
|
||
if (legibleServiceAdapter.reviewParsedFrag(parsedFragResult, findFragsResult, details) !== ParsedFragQuality.CloseEnough) {
|
||
// may be too far from target frag. find another frag based on newly parsed cues
|
||
pipelineContext.legibleSystemAdapter.tryAgain$.next(true);
|
||
}
|
||
}));
|
||
};
|
||
const loadSubtitleInDiscontinuity = (pipelineContext, details, activeSeqNum) => {
|
||
const { legibleSystemAdapter, rootPlaylistQuery } = pipelineContext;
|
||
return rootPlaylistQuery.initPTS$(activeSeqNum).pipe(switchMap((initPTS) => {
|
||
if (!initPTS || initPTS.iframeMode) {
|
||
return NEVER;
|
||
}
|
||
return legibleSystemAdapter.findFrags$(details, activeSeqNum).pipe(switchMap((findFragsResult) => {
|
||
if (!details || !(findFragsResult === null || findFragsResult === void 0 ? void 0 : findFragsResult.foundFrags))
|
||
return VOID;
|
||
return loadSubtitleBatch(pipelineContext, initPTS.offsetTimestamp, findFragsResult, details);
|
||
}));
|
||
}));
|
||
};
|
||
/**
|
||
* @brief subtitle loading & processing
|
||
*/
|
||
const subtitleLoadAndProcessEpic = (pipelineContext) => (bufferInfoSource$) => {
|
||
const { mediaSink, rootPlaylistQuery, legibleSystemAdapter, logger } = pipelineContext;
|
||
return bufferInfoSource$.pipe(tag('subtitleEpic.process.in'), switchMap((option) => {
|
||
if (!option || !option.url || !isEnabledMediaOption(option)) {
|
||
return of([null, null, null]);
|
||
}
|
||
const mediaLibraryQuery = createMediaLibraryQuery(option);
|
||
const details$ = mediaLibraryQuery.mediaOptionDetails$;
|
||
const activeDiscoSeqNum$ = rootPlaylistQuery.discoSeqNum$.pipe(filter((sn) => isFiniteNumber(sn)));
|
||
return combineQueries([details$, activeDiscoSeqNum$]).pipe(switchMap(([details, activeSeqNum]) => {
|
||
return loadSubtitleInDiscontinuity(pipelineContext, details, activeSeqNum);
|
||
}));
|
||
}), tag('subtitleEpic.process.emit'));
|
||
};
|
||
function processSubtitlePayload(enabledMediaOption, mediaFragment, initPTS, data, legibleSystemAdapter) {
|
||
if (enabledMediaOption) {
|
||
return legibleSystemAdapter.processSubtitleFrag(enabledMediaOption, mediaFragment, initPTS, data);
|
||
}
|
||
return;
|
||
}
|
||
|
||
const getMimeInfo = (initParsedData, container) => {
|
||
var _a;
|
||
let mimeType = '';
|
||
let codec = '';
|
||
let type;
|
||
if (initParsedData.videoCodec && initParsedData.audioCodec) {
|
||
codec = `${initParsedData.videoCodec}, ${initParsedData.audioCodec}`;
|
||
container = container !== null && container !== void 0 ? container : 'video/mp4';
|
||
type = 'audiovideo';
|
||
}
|
||
else if (initParsedData.videoCodec) {
|
||
codec = `${initParsedData.videoCodec}`;
|
||
container = container !== null && container !== void 0 ? container : 'video/mp4';
|
||
type = 'video';
|
||
}
|
||
else {
|
||
codec = `${(_a = initParsedData.audioCodec) !== null && _a !== void 0 ? _a : ''}`;
|
||
container = container !== null && container !== void 0 ? container : 'audio/mp4';
|
||
type = 'audio';
|
||
}
|
||
mimeType = `${container};codecs=${codec}`;
|
||
return { mimeType, codec, container, type };
|
||
};
|
||
|
||
class MediaParser {
|
||
constructor(config, logger, demuxClient) {
|
||
this.config = config;
|
||
this.logger = logger;
|
||
this.demuxClient = demuxClient;
|
||
this.typeSupported = {
|
||
mp4: MediaSource.isTypeSupported('video/mp4'),
|
||
mpeg: MediaSource.isTypeSupported('audio/mpeg'),
|
||
mp3: MediaSource.isTypeSupported('audio/mp4; codecs="mp3"'),
|
||
ac3: MediaSource.isTypeSupported('audio/mp4; codecs="ac-3"'),
|
||
ec3: MediaSource.isTypeSupported('audio/mp4; codecs="ec-3"'),
|
||
};
|
||
this.demuxers = [];
|
||
this.lastInitFrags = [];
|
||
this.lastFrags = [];
|
||
}
|
||
parseInitSegment(context, vendor) {
|
||
return this.getDemuxerInfo(context, this.lastInitFrags, vendor, this.demuxClient).pipe(switchMap(({ demuxer, contiguous, trackSwitch, discontinuity, accurateTimeOffset }) => {
|
||
const { frag } = context;
|
||
const { keyTagInfo, start: timeOffset, mediaOptionType } = frag;
|
||
this.lastInitFrags[mediaOptionType] = frag;
|
||
if (context.initSegment) {
|
||
const remuxedInitSegment = MP4EncryptionRemuxer.remuxInitSegment(new Uint8Array(context.initSegment), this.logger, keyTagInfo);
|
||
const initParsedData = MP4Demuxer.parseInitSegment(remuxedInitSegment);
|
||
const { mimeType, type, codec, container } = getMimeInfo(initParsedData);
|
||
return of({ moovData: initParsedData, mimeType, track: { type, codec, initSegment: remuxedInitSegment, container } });
|
||
}
|
||
const segmentData = context.segment ? context.segment : context.initSegment;
|
||
const initSegmentData = segmentData ? context.initSegment : undefined;
|
||
const demuxerTarget = fromEventTarget(demuxer.observer);
|
||
return of(demuxerTarget.event(DemuxerEvent.FRAG_PARSING_INIT_SEGMENT).pipe(map(this.handleInitSegmentData)), demuxerTarget.event(HlsEvent$1.INTERNAL_ERROR).pipe(switchMap(this.handleError)),
|
||
// Because we're sending the same data to demuxers multiple times, we cannot transfer to worker here.
|
||
demuxer
|
||
.pushWithoutTransfer(segmentData, keyTagInfo, initSegmentData, timeOffset, discontinuity, trackSwitch, contiguous, context.totalDuration, accurateTimeOffset, undefined, // context.defaultInitPTS,
|
||
context.iframeMediaStart, context.iframeDuration)
|
||
.pipe(switchMapTo(EMPTY))).pipe(mergeAll(), take(1));
|
||
}));
|
||
}
|
||
parseSegment(context, vendor) {
|
||
return this.getDemuxerInfo(context, this.lastFrags, vendor, this.demuxClient).pipe(switchMap(({ demuxer, contiguous, trackSwitch, discontinuity, accurateTimeOffset }) => {
|
||
const { frag, defaultInitPTS } = context;
|
||
const { keyTagInfo, start: timeOffset, duration, mediaOptionType } = frag;
|
||
this.lastFrags[mediaOptionType] = frag;
|
||
let parsedInitSegment;
|
||
const demuxerTarget = fromEventTarget(demuxer.observer);
|
||
return of(demuxerTarget.event(DemuxerEvent.FRAG_PARSING_INIT_SEGMENT).pipe(switchMap((data) => {
|
||
var _a;
|
||
if (data.track.initSegment.byteLength !== ((_a = context.initSegment) === null || _a === void 0 ? void 0 : _a.byteLength)) {
|
||
parsedInitSegment = this.handleInitSegmentData(data);
|
||
}
|
||
return EMPTY;
|
||
})), demuxerTarget.event(DemuxerEvent.FRAG_PARSING_DATA).pipe(map((parsedData) => {
|
||
const { startPTS, startDTS, firstKeyframePts, framesWithoutIDR, dropped, data1, data2, captionData, id3Samples } = parsedData;
|
||
let { endPTS, endDTS } = parsedData;
|
||
if (endPTS == null) {
|
||
this.logger.warn(`${MediaOptionNames[mediaOptionType]} ${fragPrint(frag)}: null endPTS parsed, using duration ${duration}`);
|
||
endPTS = Object.assign(Object.assign({}, startPTS), { baseTime: startPTS.baseTime + convertSecondsToTimestamp(duration, startPTS.timescale).baseTime });
|
||
}
|
||
if (endDTS == null) {
|
||
this.logger.warn(`${MediaOptionNames[mediaOptionType]} ${fragPrint(frag)}: null endDTS parsed, using duration ${duration}`);
|
||
endDTS = Object.assign(Object.assign({}, startDTS), { baseTime: startDTS.baseTime + convertSecondsToTimestamp(duration, startDTS.timescale).baseTime });
|
||
}
|
||
// <rdar://problem/54533536> Don't post sanity check error event for trickplay until we fix the rounding problem (due to small timescale)
|
||
if (!isFiniteNumber(context.iframeMediaStart)) {
|
||
// Just do duration check for now (accurateTimeOffset == false)
|
||
checkIfValidFragParsingData(frag, parsedData, defaultInitPTS, false, context.iframeMediaStart, this.config.audioPrimingDelay);
|
||
}
|
||
return { startPTS, endPTS, startDTS, endDTS, firstKeyframePts, framesWithoutIDR, dropped, data1, data2, captionData, id3Samples, parsedInitSegment };
|
||
})), demuxerTarget.event(HlsEvent$1.INTERNAL_ERROR).pipe(switchMap(this.handleError)), demuxer
|
||
.push(context.segment, keyTagInfo, context.initSegment, timeOffset, discontinuity, trackSwitch, contiguous, context.totalDuration, accurateTimeOffset, defaultInitPTS, context.iframeMediaStart, context.iframeDuration)
|
||
.pipe(switchMapTo(EMPTY))).pipe(mergeAll(), take(1));
|
||
}));
|
||
}
|
||
reset(mediaOptionType = undefined) {
|
||
if (mediaOptionType == undefined) {
|
||
this.demuxers.forEach((demuxer) => {
|
||
if (demuxer)
|
||
demuxer.destroy();
|
||
});
|
||
this.demuxers = [];
|
||
return;
|
||
}
|
||
const demuxer = this.demuxers[mediaOptionType];
|
||
demuxer === null || demuxer === void 0 ? void 0 : demuxer.destroy();
|
||
this.demuxers[mediaOptionType] = null;
|
||
}
|
||
destroy(mediaOptionType = undefined) {
|
||
if (mediaOptionType == undefined) {
|
||
this.reset();
|
||
return;
|
||
}
|
||
this.reset(mediaOptionType);
|
||
}
|
||
willBeTrackSwitch(mediaFragment, frags) {
|
||
const { mediaOptionType, mediaOptionId } = mediaFragment;
|
||
const lastFrag = frags ? frags[mediaOptionType] : this.lastFrags[mediaOptionType];
|
||
return !(lastFrag && lastFrag.mediaOptionId === mediaOptionId);
|
||
}
|
||
getDemuxerInfo(context, frags, vendor, demuxClient) {
|
||
const { frag, ptsKnown, seeking, live } = context;
|
||
const { discoSeqNum, mediaSeqNum, mediaOptionType } = frag;
|
||
return defer(() => {
|
||
const demuxer = this.demuxers[mediaOptionType];
|
||
if (demuxer) {
|
||
return of(demuxer);
|
||
}
|
||
return demuxClient.init(this.typeSupported, this.config, vendor).pipe(tap((demuxer) => (this.demuxers[mediaOptionType] = demuxer)));
|
||
}).pipe(map((demuxer) => {
|
||
const lastFrag = frags[mediaOptionType];
|
||
const trackSwitch = this.willBeTrackSwitch(frag, frags);
|
||
const discontinuity = !(lastFrag && discoSeqNum === lastFrag.discoSeqNum);
|
||
const contiguous = (lastFrag || false) && !trackSwitch && lastFrag.mediaSeqNum + 1 === mediaSeqNum;
|
||
const accurateTimeOffset = !seeking && (ptsKnown || !live);
|
||
return { demuxer, trackSwitch, discontinuity, contiguous, accurateTimeOffset };
|
||
}));
|
||
}
|
||
handleInitSegmentData(data) {
|
||
const { track } = data;
|
||
const { initSegment } = track;
|
||
const moovData = MP4Demuxer.parseInitSegment(initSegment);
|
||
const { mimeType, type, codec, container } = getMimeInfo(moovData, track.container);
|
||
return { moovData, mimeType, track: Object.assign(Object.assign({}, track), { type, codec, initSegment, container }) };
|
||
}
|
||
handleError(err) {
|
||
return throwError(err);
|
||
}
|
||
}
|
||
// Basic sanity check. Returns true if this is valid data, false if PTS/DTS values seem wrong
|
||
function checkIfValidFragParsingData(frag, data, initPTS, accurateTimeOffset, iframeMediaStart, audioPrimingDelay) {
|
||
let fudge = NaN; // max diff in seconds between parsed startDTS & expectedStartDTS
|
||
let expectedStartDTS = NaN; // expected value of parsed startDTS in seconds
|
||
if (isFiniteNumber(iframeMediaStart)) {
|
||
expectedStartDTS = iframeMediaStart;
|
||
// Most we should be off is one tick of the timescale due to rounding. iframe tracks can have small timescales, so allow up to 10 ms
|
||
fudge = 0.01;
|
||
if (isFinite(expectedStartDTS) && isFinite(audioPrimingDelay)) {
|
||
expectedStartDTS += audioPrimingDelay;
|
||
}
|
||
}
|
||
else if (accurateTimeOffset && initPTS) {
|
||
const timestampOffset = convertTimestampToSeconds(initPTS);
|
||
expectedStartDTS = frag.start + timestampOffset;
|
||
fudge = frag.duration;
|
||
}
|
||
const { startPTS, startDTS, endPTS, endDTS } = data;
|
||
// Technically we could have negative numbers if value > Number.MAX_SAFE_INTEGER
|
||
// but right now we would barf in LevelHelper adjustment
|
||
const success = startPTS.baseTime >= 0 &&
|
||
startDTS.baseTime >= 0 &&
|
||
frag.duration > 0 &&
|
||
(endPTS == null || diffSeconds(endPTS, startPTS) > 0) &&
|
||
(endDTS == null || diffSeconds(endDTS, startDTS) > 0) &&
|
||
(!isFiniteNumber(fudge) || !isFiniteNumber(expectedStartDTS) || Math.abs(convertTimestampToSeconds(startDTS) - expectedStartDTS) <= fudge);
|
||
if (!success) {
|
||
throw new FragParsingError(false, `Failed demuxer sanity check frag=${fragPrint(frag)} parsed=${JSON.stringify({ startPTS, endPTS, startDTS, endDTS })} ${stringifyWithPrecision({ expectedStartDTS, fudge })}`, ErrorResponses.FailedDemuxerSanityCheck);
|
||
}
|
||
return success;
|
||
}
|
||
function createMediaParser(config, logger, demuxClient) {
|
||
return new Observable((subscriber) => {
|
||
const parser = new MediaParser(config, logger, demuxClient);
|
||
subscriber.next(parser);
|
||
return () => {
|
||
parser.destroy();
|
||
};
|
||
});
|
||
}
|
||
|
||
function getMinBufferAhead(config, details, bufferInfo) {
|
||
var _a, _b;
|
||
// Allow if we have a whole segment ahead
|
||
let playingFragDuration = Infinity;
|
||
if (bufferInfo.playingFrag) {
|
||
playingFragDuration = (_b = (_a = details.fragments[bufferInfo.playingFrag.mediaSeqNum - details.startSN]) === null || _a === void 0 ? void 0 : _a.duration) !== null && _b !== void 0 ? _b : Infinity;
|
||
}
|
||
const { minRequiredStartDuration, maxRequiredStartDuration, startTargetDurationFactor } = config;
|
||
const { targetduration, averagetargetduration } = details;
|
||
const requiredStartDuration = startTargetDurationFactor * Math.min(playingFragDuration, averagetargetduration, targetduration, maxRequiredStartDuration);
|
||
return Math.max(minRequiredStartDuration, requiredStartDuration);
|
||
}
|
||
/**
|
||
* @returns whether we probably have enough
|
||
*/
|
||
function probablyHaveEnough(config, fragInfo, playlistInfo, bufferInfo, desiredRate, bwEstimate, fragEstimate, bufferEstimate, logger) {
|
||
const { combined } = bufferInfo;
|
||
const minBufferAhead = getMinBufferAhead(config, playlistInfo.details, bufferInfo);
|
||
let haveEnough = haveEnoughBuffer(fragInfo, playlistInfo, bufferInfo, desiredRate, minBufferAhead);
|
||
if (!haveEnough && combined.len > 0 && (fragInfo === null || fragInfo === void 0 ? void 0 : fragInfo.state)) {
|
||
const estTimeToHaveEnough = getEstTimeToHaveEnough(fragInfo, playlistInfo.variant.bitrate, minBufferAhead, bufferInfo, bwEstimate, fragEstimate, bufferEstimate, config.maxBufferHole);
|
||
haveEnough = estTimeToHaveEnough <= combined.len;
|
||
logger === null || logger === void 0 ? void 0 : logger.info(`haveEnough (probably): ${haveEnough}, ${stringifyWithPrecision({
|
||
bufferLen: combined.len,
|
||
pos: bufferInfo.pos,
|
||
estTimeToHaveEnough,
|
||
minBufferAhead,
|
||
state: fragInfo.state,
|
||
tstart: fragInfo.tstart,
|
||
avgParseMs: fragEstimate.avgParseTimeMs,
|
||
avgAppendMs: bufferEstimate.avgDataFragAppendMs,
|
||
})}`);
|
||
}
|
||
else if (!haveEnough && fragInfo == null) {
|
||
logger === null || logger === void 0 ? void 0 : logger.info(`haveEnough: ${haveEnough}, bufferLen: ${combined.len.toFixed(3)}`);
|
||
}
|
||
return haveEnough;
|
||
}
|
||
/**
|
||
* @returns whether we have enough buffer without using any estimation
|
||
*/
|
||
function haveEnoughBuffer(fragInfo, playlistInfo, bufferInfo, desiredRate, requiredStartDuration) {
|
||
const { pos, combined, playingFrag } = bufferInfo;
|
||
if (combined.len === 0) {
|
||
return false;
|
||
}
|
||
const iframeMode = desiredRate != 0 && desiredRate != 1;
|
||
const details = playlistInfo.details;
|
||
const fragments = details.fragments;
|
||
let haveEnough = iframeMode || combined.len >= requiredStartDuration;
|
||
const lastFrag = fragments[details.fragments.length - 1];
|
||
const playlistStart = fragments[0].start;
|
||
const playlistEnd = playlistStart + details.totalduration;
|
||
let gotDiscontinuity = false;
|
||
if (playingFrag) {
|
||
const lastFragForCC = BinarySearch$1.search(fragments, (frag) => playingFrag.discoSeqNum - frag.discoSeqNum);
|
||
gotDiscontinuity =
|
||
(fragInfo && playingFrag.discoSeqNum !== fragInfo.discoSeqNum) ||
|
||
lastFragForCC == null || // buffered cc does not appear in the refreshed playlist anymore
|
||
fragEqual(lastFragForCC, playingFrag); // buffered cc appears to end in refreshed playlist
|
||
}
|
||
// Also ensure that live is behind furthest live position
|
||
if (haveEnough && details.liveOrEvent) {
|
||
const behindLivePosition = playlistEnd - lastFrag.duration;
|
||
haveEnough = pos <= behindLivePosition;
|
||
}
|
||
else if (!details.liveOrEvent) {
|
||
// Allow play through if we're close to the end
|
||
haveEnough = haveEnough || pos >= playlistEnd - requiredStartDuration;
|
||
}
|
||
haveEnough = haveEnough || gotDiscontinuity;
|
||
return haveEnough;
|
||
}
|
||
/**
|
||
* @returns the duration relative to now when we think that the fragment will be
|
||
* buffered / complete
|
||
*/
|
||
function getEstTimeToHaveEnough(fragInfo, bitrate, minBufferAhead, bufferInfo, bwEstimate, fragEstimate, bufferEstimate, maxBufferHole) {
|
||
var _a, _b;
|
||
const mainBufferInfo = (_a = bufferInfo.sbTuple[MediaOptionType.Variant]) === null || _a === void 0 ? void 0 : _a.buffered;
|
||
const audioBufferInfo = (_b = bufferInfo.sbTuple[MediaOptionType.AltAudio]) === null || _b === void 0 ? void 0 : _b.buffered;
|
||
if ((mainBufferInfo === null || mainBufferInfo === void 0 ? void 0 : mainBufferInfo.len) >= minBufferAhead && (!audioBufferInfo || audioBufferInfo.len >= minBufferAhead)) {
|
||
// Already have enough buffered in main/audio sb. Its
|
||
// already assumed that if main suffices, audio will as well - but
|
||
// still checking for sanity
|
||
return 0;
|
||
}
|
||
else if (!mainBufferInfo || !fragInfo || !fragOverlapsEndAndSufficient(fragInfo, mainBufferInfo, maxBufferHole, minBufferAhead, bufferInfo.pos)) {
|
||
// Not enough fragment info
|
||
return Infinity;
|
||
}
|
||
const state = fragInfo.state;
|
||
let tStartOfState = fragInfo.tstart;
|
||
let timeToComplete = 0;
|
||
switch (state) {
|
||
case 'loading':
|
||
timeToComplete += getTimeToLoad(fragInfo, bitrate, bwEstimate);
|
||
tStartOfState = fragInfo.tstart + timeToComplete * 1000;
|
||
// fallthrough
|
||
case 'loaded':
|
||
case 'parsing':
|
||
timeToComplete += getTimeToParse(tStartOfState, fragEstimate);
|
||
tStartOfState = fragInfo.tstart + timeToComplete * 1000;
|
||
// fallthrough
|
||
case 'parsed':
|
||
case 'appending':
|
||
timeToComplete += getTimeToAppendBuffer(tStartOfState, bufferEstimate);
|
||
break;
|
||
case 'appended':
|
||
// 0 triggers a false positive for haveEnough; If appended, it
|
||
// should be factored in combined.len.
|
||
timeToComplete = Infinity;
|
||
break;
|
||
default:
|
||
timeToComplete = Infinity;
|
||
break;
|
||
}
|
||
return timeToComplete;
|
||
}
|
||
/**
|
||
* @returns the estimated remaining amount of time to load the fragment
|
||
*/
|
||
function getTimeToLoad(fragInfo, bitrate, bwEstimate) {
|
||
const { bwSample: fragStats, duration } = fragInfo;
|
||
if (!fragStats) {
|
||
return Infinity;
|
||
}
|
||
// Calculate based on current bw when we think the next frag will be finished
|
||
// Assume audio will take less time
|
||
const totalBits = isFiniteNumber(fragStats.total) ? fragStats.total * 8 : Math.ceil(duration * bitrate); // est bytes using bitrate & duration
|
||
const loadedBits = fragStats.loaded * 8;
|
||
const remainingBits = totalBits - loadedBits;
|
||
const instantBW = (loadedBits / (performance.now() - fragStats.tfirst)) * 1000; // bps
|
||
if (!isFiniteNumber(instantBW)) {
|
||
return Infinity;
|
||
}
|
||
const avgBW = bwEstimate.avgBandwidth; // bps
|
||
const bandwidth = Math.min(avgBW, instantBW);
|
||
return remainingBits / bandwidth;
|
||
}
|
||
/**
|
||
* @returns the estimated remaining amount of time to parse the fragment
|
||
*/
|
||
function getTimeToParse(tParseStart, fragEstimate) {
|
||
const avgParseTimeMs = isFiniteNumber(fragEstimate.avgParseTimeMs) ? fragEstimate.avgParseTimeMs : 0;
|
||
if (performance.now() < tParseStart) {
|
||
return avgParseTimeMs / 1000;
|
||
}
|
||
return Math.max(0, avgParseTimeMs - (performance.now() - tParseStart)) / 1000;
|
||
}
|
||
/**
|
||
* @returns the estimated remaining amount of time to append the fragment
|
||
*/
|
||
function getTimeToAppendBuffer(tAppendStart, bufferEstimate) {
|
||
const avgDataFragAppendMs = isFiniteNumber(bufferEstimate.avgDataFragAppendMs) ? bufferEstimate.avgDataFragAppendMs : 0;
|
||
if (performance.now() < tAppendStart) {
|
||
return avgDataFragAppendMs / 1000;
|
||
}
|
||
return Math.max(0, avgDataFragAppendMs - (performance.now() - tAppendStart)) / 1000;
|
||
}
|
||
/**
|
||
*@returns true if to frag overlaps end
|
||
*/
|
||
function fragOverlapsEndAndSufficient(fragInfo, bufferInfo, maxBufferHole, minBufferAhead, currentPosition) {
|
||
const fragend = fragInfo.start + fragInfo.duration;
|
||
return fragend > bufferInfo.end && (fragInfo.start - bufferInfo.end <= maxBufferHole || fragInfo.start <= bufferInfo.end) && fragend - currentPosition >= minBufferAhead;
|
||
}
|
||
|
||
/**
|
||
* Observables that keep track of pipeline and media state to update playback state
|
||
*/
|
||
function checkForHaveEnough(config, logger, rootQuery, mediaSink, mediaLibraryService, statsQuery) {
|
||
const mediaQuery = mediaSink.mediaQuery;
|
||
// Startup perf: avoid subscribing to stuff on startup if we don't /need/ it
|
||
// Have enough won't ever be true until something gets buffered so don't listen to any observables until then.
|
||
return waitFor(mediaQuery.combinedBuffer$, (combinedBuffer) => (combinedBuffer === null || combinedBuffer === void 0 ? void 0 : combinedBuffer.length) > 0).pipe(switchMap(() => {
|
||
const mediaBufferInfo$ = combineQueries([mediaQuery.seekTo$, mediaQuery.bufferedSegmentsByType$(SourceBufferType.Variant)]).pipe(map(([seekTo, bufferedSeg]) => {
|
||
var _a;
|
||
const pos = isFiniteNumber(seekTo === null || seekTo === void 0 ? void 0 : seekTo.pos) ? seekTo.pos : mediaQuery.currentTime;
|
||
const playingSeg = bufferedSeg.find((seg) => seg.startPTS <= pos && seg.endPTS > pos);
|
||
const sbTuple = mediaQuery.getBufferInfo(pos, config.maxBufferHole);
|
||
const combined = mediaQuery.getCombinedBufferInfo(pos, config.maxBufferHole);
|
||
return {
|
||
pos,
|
||
sbTuple,
|
||
combined,
|
||
playingFrag: (_a = playingSeg === null || playingSeg === void 0 ? void 0 : playingSeg.frag) !== null && _a !== void 0 ? _a : null,
|
||
};
|
||
}));
|
||
const inFlightInfo$ = combineQueries([
|
||
rootQuery.getInFlightFragByType$(MediaOptionType.Variant),
|
||
rootQuery.enabledMediaOptionByType$(MediaOptionType.Variant),
|
||
]).pipe(switchMap(([inFlightFrag, mediaOption]) => {
|
||
const playlistQuery = mediaLibraryService.getQueryForOption(mediaOption);
|
||
return zip(of(inFlightFrag), of(mediaOption), playlistQuery.mediaOptionDetails$);
|
||
}), map(([currentFrag, variant, details]) => {
|
||
return [currentFrag, { variant, details }];
|
||
}));
|
||
const bwEstimates$ = combineQueries([statsQuery.bandwidthEstimate$, statsQuery.fragEstimate$, statsQuery.bufferEstimate$]);
|
||
return combineQueries([mediaQuery.readyState$, inFlightInfo$, mediaBufferInfo$, mediaQuery.desiredRate$, bwEstimates$]);
|
||
}), throttleTime(100, asyncScheduler, { leading: true, trailing: true }), map(([_readyState, inFlightInfo, bufferInfo, desiredRate, bwEstimates]) => {
|
||
const [fragInfo, playlistInfo] = inFlightInfo;
|
||
const [bwEstimate, fragEstimate, bufferEstimate] = bwEstimates;
|
||
let overrideFragEstimate = fragEstimate;
|
||
let overrideBufferEsimate = bufferEstimate;
|
||
if (fragEstimate) {
|
||
const maxDurationSec = isFiniteNumber(fragEstimate.maxDurationSec) ? fragEstimate.maxDurationSec : config.defaultTargetDuration;
|
||
const avgParseTimeMs = isFiniteNumber(fragEstimate.avgParseTimeMs) ? fragEstimate.avgParseTimeMs : config.statDefaults.fragParseTimeMs;
|
||
overrideFragEstimate = { maxDurationSec, avgParseTimeMs };
|
||
}
|
||
if (bufferEstimate) {
|
||
const avgBufferCreateMs = isFiniteNumber(bufferEstimate.avgBufferCreateMs) ? bufferEstimate.avgBufferCreateMs : config.statDefaults.fragBufferCreationDelayMs;
|
||
const avgInitFragAppendMs = isFiniteNumber(bufferEstimate.avgInitFragAppendMs) ? bufferEstimate.avgInitFragAppendMs : config.statDefaults.initFragAppendMs;
|
||
const avgDataFragAppendMs = isFiniteNumber(bufferEstimate.avgDataFragAppendMs) ? bufferEstimate.avgDataFragAppendMs : config.statDefaults.dataFragAppendMs;
|
||
overrideBufferEsimate = { avgBufferCreateMs, avgInitFragAppendMs, avgDataFragAppendMs };
|
||
}
|
||
const haveEnough = probablyHaveEnough(config, fragInfo, playlistInfo, bufferInfo, desiredRate, bwEstimate, overrideFragEstimate, overrideBufferEsimate, logger);
|
||
mediaSink.haveEnough = haveEnough;
|
||
return haveEnough;
|
||
}), distinctUntilChanged(), tap((haveEnough) => {
|
||
logger.info(`[seek] haveEnough=${haveEnough}`);
|
||
}), switchMapTo(EMPTY));
|
||
}
|
||
function checkForEndOfStream(context) {
|
||
const { config, logger, mediaSink, rootPlaylistQuery: rootQuery, mediaLibraryService, gaplessInstance } = context;
|
||
const { mediaQuery } = mediaSink;
|
||
const currentEnabledDetails$ = rootQuery.enabledAVOptions$.pipe(switchMap((options) => zip(...options.map((option) => {
|
||
if (!isEnabledMediaOption(option)) {
|
||
return of(null);
|
||
}
|
||
return mediaLibraryService.getQueryForOption(option).mediaOptionDetails$;
|
||
}))));
|
||
// Startup: wait for something to be buffered before subscribe to enabledAVOptions & details
|
||
return waitFor(mediaQuery.combinedBuffer$, (combined) => {
|
||
return (combined === null || combined === void 0 ? void 0 : combined.length) > 0;
|
||
}).pipe(switchMapTo(combineQueries([
|
||
currentEnabledDetails$,
|
||
mediaQuery.msReadyState$,
|
||
mediaQuery.updating$,
|
||
mediaQuery.isIframeRate$,
|
||
mediaQuery.isBufferedToEnd$(config.maxBufferHole, !gaplessInstance.inGaplessMode),
|
||
])), tag('checkForEndOfStream'), filter(([_, readyState, updating, isIframeRate, bufferedToEnd]) => {
|
||
return readyState === 'open' && updating === false && !isIframeRate && bufferedToEnd;
|
||
}), tap(([details]) => {
|
||
logger.info('Reached end of stream');
|
||
const isVOD = details[0] != null && details.every((x) => x == null || (x.liveOrEvent === false && x.iframesOnly === false));
|
||
if (isVOD && !gaplessInstance.inGaplessMode) {
|
||
logger.info('Calling MediaSink endStream');
|
||
mediaSink.endStream(); // May only be called when updating == false
|
||
}
|
||
}), switchMapTo(EMPTY));
|
||
}
|
||
|
||
function loadRootMediaOptions(item, loadPolicy, logger, config, statsService, extendMaxTTFB) {
|
||
const { itemId, url, itemStartOffset } = item;
|
||
const loadConfig = getLoadConfig(item, loadPolicy);
|
||
return fromUrlText({
|
||
url,
|
||
onProgress: { getData: true, cb: loadProgress },
|
||
xhrSetup: config.xhrSetup,
|
||
}, loadConfig, extendMaxTTFB).pipe(map(({ responseText: rawPlaylist, responseURL: rURL, stats }) => {
|
||
const { keySystemPreference } = config;
|
||
logger.debug(`root playlist url: ${url}, baseUrl: ${rURL}`);
|
||
if (!rURL) {
|
||
logger.warn('Missing response url. Reusing request url as base url');
|
||
rURL = url;
|
||
}
|
||
if (PlaylistParser$1.isMediaPlaylist(rawPlaylist)) {
|
||
const mediaOptionId = 'media-pl-' + guid();
|
||
const parsed = PlaylistParser$1.parseMediaOptionPlaylist(rawPlaylist, rURL, true, keySystemPreference, {}, itemId, mediaOptionId, MediaOptionType.Variant, logger, itemStartOffset);
|
||
updatePlaylistAttributes(parsed.mediaOptionDetails);
|
||
const variantOption = {
|
||
itemId,
|
||
mediaOptionId,
|
||
mediaOptionType: MediaOptionType.Variant,
|
||
url,
|
||
bandwidth: 0,
|
||
bitrate: 0,
|
||
iframes: parsed.mediaOptionDetails.iframesOnly,
|
||
pathwayID: '.',
|
||
};
|
||
const rootMediaOptionsTuple = [[variantOption], [], []];
|
||
const loadResult = {
|
||
itemId,
|
||
itemStartOffset,
|
||
rootMediaOptionsTuple,
|
||
stats,
|
||
baseUrl: rURL,
|
||
initialDetails: parsed.mediaOptionDetails,
|
||
isMediaPlaylist: true,
|
||
};
|
||
return loadResult;
|
||
}
|
||
else {
|
||
const sessionData = PlaylistParser$1.parseSessionData(rawPlaylist, rURL);
|
||
const sessionKeys = PlaylistParser$1.parseSessionKeys(rawPlaylist, rURL, keySystemPreference);
|
||
const parsed = PlaylistParser$1.parseRootPlaylist(itemId, rawPlaylist, rURL, true);
|
||
if (parsed.playlistParsingError) {
|
||
throw parsed.playlistParsingError;
|
||
}
|
||
const { variantMediaOptions, contentSteeringOption, masterVariableList } = parsed;
|
||
const parsingResult = PlaylistParser$1.parseRootPlaylistAlternateMediaOptions(itemId, rawPlaylist, rURL, parsed.variantMediaOptions, true, masterVariableList);
|
||
if (parsingResult.playlistParsingError) {
|
||
throw parsingResult.playlistParsingError;
|
||
}
|
||
const { audioAlternateOptions, subtitleAlternateOptions, audioMediaSelectionGroup, subtitleMediaSelectionGroup } = parsingResult.alternateMediaInfo;
|
||
const rootMediaOptionsTuple = [variantMediaOptions, audioAlternateOptions, subtitleAlternateOptions];
|
||
const loadResult = {
|
||
itemId,
|
||
itemStartOffset,
|
||
rootMediaOptionsTuple,
|
||
stats,
|
||
baseUrl: rURL,
|
||
audioMediaSelectionGroup,
|
||
subtitleMediaSelectionGroup,
|
||
contentSteeringOption,
|
||
sessionData,
|
||
sessionKeys,
|
||
masterVariableList,
|
||
};
|
||
return loadResult;
|
||
}
|
||
}), convertToManifestNetworkError(false));
|
||
}
|
||
function loadProgress(url, status, _stats, data) {
|
||
if (status === 200 && data && data.length > 10) {
|
||
// only look at responses which have status 200 and is at least 10 chars long
|
||
if (!PlaylistParser$1.isValidPlaylist(data)) {
|
||
const playlistParsingError = new PlaylistParsingError(ErrorTypes.NETWORK_ERROR, ErrorDetails.MANIFEST_PARSING_ERROR, true, 'response doesnt have #EXTM3U tag', ErrorResponses.PlaylistErrorMissingEXTM3U);
|
||
playlistParsingError.url = url;
|
||
throw playlistParsingError;
|
||
}
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
const loggerName$1 = { name: 'pltfrm' };
|
||
var HdrMetadataType;
|
||
(function (HdrMetadataType) {
|
||
HdrMetadataType["HDR10"] = "smpteSt2086";
|
||
HdrMetadataType["DoVi"] = "smpteSt2094-10";
|
||
HdrMetadataType["HDR10Plus"] = "smpteSt2094-40";
|
||
})(HdrMetadataType || (HdrMetadataType = {}));
|
||
function isValidSecurityLevel(securityLevel, keySystemId) {
|
||
const securityLevels = KeySystemFactory$1.getKeySystemSecurityLevel(keySystemId);
|
||
return securityLevel != null && securityLevels[securityLevel] !== undefined;
|
||
}
|
||
function onlyIframes(mediaOptionList) {
|
||
return mediaOptionList.every((mediaOption) => mediaOption.iframes);
|
||
}
|
||
function isWithinCap(val, cap) {
|
||
// If there is no value or if there is no cap, then the value is vacuously within the cap
|
||
return !isFiniteNumber(val) || !isFiniteNumber(cap) || val <= cap; // TODO revert or fix <- <jgainfort> this is not my todo, inherited from 2.0
|
||
}
|
||
function makeVideoConfiguration(codec, mediaOption) {
|
||
const vConfig = {
|
||
contentType: `video/mp4;codecs=${codec}`,
|
||
width: mediaOption.width,
|
||
height: mediaOption.height,
|
||
bitrate: mediaOption.bandwidth || mediaOption.avgBandwidth,
|
||
framerate: mediaOption.iframes ? 8 : mediaOption.frameRate,
|
||
};
|
||
if (mediaOption.videoRange) {
|
||
switch (mediaOption.videoRange) {
|
||
case 'PQ':
|
||
if (MediaUtil.isDolby(codec)) {
|
||
vConfig.hdrMetadataType = HdrMetadataType.DoVi;
|
||
vConfig.colorGamut = 'rec2020';
|
||
}
|
||
else if (MediaUtil.isHEVC(codec) || MediaUtil.isVP09(codec)) {
|
||
// Assuming HDR10
|
||
vConfig.hdrMetadataType = HdrMetadataType.HDR10;
|
||
vConfig.colorGamut = 'rec2020';
|
||
}
|
||
vConfig.transferFunction = 'pq';
|
||
break;
|
||
case 'HLG':
|
||
// no metadata
|
||
vConfig.colorGamut = 'rec2020';
|
||
vConfig.transferFunction = 'hlg';
|
||
break;
|
||
}
|
||
}
|
||
return vConfig;
|
||
}
|
||
function makeAudioConfiguration(codec, mediaOption, audioMediaOptions) {
|
||
const aConfig = {
|
||
contentType: `audio/mp4;codecs=${codec}`,
|
||
};
|
||
// Optional fields
|
||
const channels = RichestMedia.getRichestChannelLayoutForGroupId(mediaOption.audioGroupId, audioMediaOptions);
|
||
if (channels) {
|
||
aConfig.channels = MediaUtil.getChannelCount(channels).toString();
|
||
aConfig.spatialRendering = MediaUtil.isDolbyAtmos(codec, channels);
|
||
}
|
||
return aConfig;
|
||
}
|
||
function createHandleCodec(logger) {
|
||
// can reduce the number of calls by caching the results
|
||
const cachedDecodingInfo = new Map();
|
||
const mediaCapabilities = navigator && navigator.mediaCapabilities;
|
||
return (mediaOption, audioMediaOptions, codec, isVideo, isTypeSupportedObservables) => {
|
||
const config = {
|
||
type: 'media-source',
|
||
};
|
||
if (isVideo) {
|
||
config.video = makeVideoConfiguration(codec, mediaOption);
|
||
}
|
||
else {
|
||
config.audio = makeAudioConfiguration(codec, mediaOption, audioMediaOptions);
|
||
}
|
||
const configStr = JSON.stringify(config);
|
||
let decodingInfoObservable;
|
||
if (cachedDecodingInfo.has(configStr)) {
|
||
decodingInfoObservable = cachedDecodingInfo.get(configStr);
|
||
}
|
||
else {
|
||
decodingInfoObservable = from(mediaCapabilities.decodingInfo(config)).pipe(map((decodingInfo) => {
|
||
const supportedConfig = decodingInfo.configuration || decodingInfo.supportedConfiguration;
|
||
const hasRequestedFields = supportedConfig instanceof Object &&
|
||
(!config.video || Object.keys(config.video).find((k) => !(k in supportedConfig.video)) == undefined) &&
|
||
(!config.audio || Object.keys(config.audio).find((k) => !(k in supportedConfig.audio)) == undefined);
|
||
const isTypeSupported = decodingInfo.supported && (!isVideo || decodingInfo.powerEfficient) && hasRequestedFields;
|
||
if (!isTypeSupported) {
|
||
logger.warn(loggerName$1, `Unsupported config ${decodingInfo.supported}/${decodingInfo.powerEfficient}/${hasRequestedFields} ${JSON.stringify(config)} supportedConfig=${JSON.stringify(supportedConfig)}`);
|
||
}
|
||
return isTypeSupported;
|
||
}));
|
||
cachedDecodingInfo.set(configStr, decodingInfoObservable);
|
||
}
|
||
return [...isTypeSupportedObservables, decodingInfoObservable];
|
||
};
|
||
}
|
||
function createIsSupportedInternal() {
|
||
const supportedCodecs = new Set();
|
||
const unsupportedCodecs = new Set();
|
||
return (mediaOption) => {
|
||
const checkSupported = (type, codec) => {
|
||
let isSupported = MediaSource.isTypeSupported(`${type}/mp4;codecs=${codec}`);
|
||
if (codec === 'mp4a.40.34' && !isSupported) {
|
||
isSupported = MediaSource.isTypeSupported(`${type}/mpeg`);
|
||
}
|
||
return isSupported;
|
||
};
|
||
const updateCachedCodecsSetsIfNecessary = (codec, isAudioCodec) => {
|
||
const type = isAudioCodec ? 'audio' : 'video';
|
||
// Minimizes calls to chromium by caching supported/unsupported audio video codecs
|
||
if (!supportedCodecs.has(codec) && !unsupportedCodecs.has(codec)) {
|
||
if (checkSupported(type, codec)) {
|
||
supportedCodecs.add(codec);
|
||
}
|
||
else {
|
||
unsupportedCodecs.add(codec);
|
||
}
|
||
}
|
||
};
|
||
// Update the codec support/notSupport cache based on MediaSource support.
|
||
// After that verify if that codec falls into unsupported category.
|
||
const checkUnsupportedCodec = (codec, isAudioCodec) => {
|
||
updateCachedCodecsSetsIfNecessary(codec, isAudioCodec);
|
||
return unsupportedCodecs.has(codec);
|
||
};
|
||
let foundUnsupportedCodec = false;
|
||
if (mediaOption.audioCodecList) {
|
||
foundUnsupportedCodec = mediaOption.audioCodecList.some((codec) => checkUnsupportedCodec(codec, true));
|
||
}
|
||
if (!foundUnsupportedCodec && mediaOption.videoCodecList) {
|
||
foundUnsupportedCodec = mediaOption.videoCodecList.some((codec) => checkUnsupportedCodec(codec, false));
|
||
}
|
||
return !foundUnsupportedCodec;
|
||
};
|
||
}
|
||
function useFallbackCapsIfMissing(result, fallback) {
|
||
const keys = Object.keys(fallback);
|
||
keys.forEach((key) => {
|
||
if (!result[key]) {
|
||
result[key] = fallback[key];
|
||
}
|
||
});
|
||
return result;
|
||
}
|
||
/**
|
||
* @param cappingInfoList Usually platformInfo.videoCodecs or platformInfo.videoDynamicRangeFormats
|
||
* @param type to use for lookup
|
||
* @returns Capping info with matching type
|
||
*/
|
||
function getCappingInfoFromType(cappingInfoList, type) {
|
||
for (const id in cappingInfoList) {
|
||
if (cappingInfoList[id].type === type) {
|
||
return cappingInfoList[id];
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
/**
|
||
* @param format DynamicRangeFormat
|
||
* @param compressionType CompressionType
|
||
* @param videoCodecs List of capping info [ { type: DynamicRangeFormat } ]
|
||
* @param videoDynamicRangeFormats List of capping info [ {type, CompressionType } ]
|
||
* @returns The capping information to use
|
||
*/
|
||
function getCappingInfo(vdrType, compressionType, videoCodecs, videoDynamicRangeFormats) {
|
||
if (!videoDynamicRangeFormats && !videoCodecs) {
|
||
return {};
|
||
}
|
||
const cappingFromCompressionInfo = videoCodecs ? getCappingInfoFromType(videoCodecs, compressionType) : {};
|
||
const cappingFromFormat = videoDynamicRangeFormats ? getCappingInfoFromType(videoDynamicRangeFormats, vdrType) : {};
|
||
let preferredCaps;
|
||
let fallbackCaps;
|
||
switch (vdrType) {
|
||
case VideoDynamicRangeType.SDR:
|
||
// Prioritize using compressionType > dynamic range
|
||
preferredCaps = cappingFromCompressionInfo;
|
||
fallbackCaps = cappingFromFormat;
|
||
break;
|
||
default:
|
||
// Prioritize using dynamic range > compressionType
|
||
preferredCaps = cappingFromFormat;
|
||
fallbackCaps = cappingFromCompressionInfo;
|
||
}
|
||
const result = Object.assign({}, preferredCaps);
|
||
return useFallbackCapsIfMissing(result, fallbackCaps);
|
||
}
|
||
function getSupportedVideoFormats(videoDynamicRangeFormats) {
|
||
return videoDynamicRangeFormats.reduce((prev, cur) => {
|
||
switch (cur.type) {
|
||
case VideoDynamicRangeType.DolbyVision:
|
||
prev.doViSupported = true;
|
||
break;
|
||
case VideoDynamicRangeType.HDR10:
|
||
prev.hdr10Supported = true;
|
||
break;
|
||
case VideoDynamicRangeType.HLG:
|
||
prev.hlgSupported = true;
|
||
break;
|
||
}
|
||
return prev;
|
||
}, { doViSupported: false, hdr10Supported: false, hlgSupported: false });
|
||
}
|
||
function filterMediaOptionInfoPrint(filterName, unfilteredMediaOptionInfo, filteredMediaOptionInfo, logger) {
|
||
const unfilteredMediaOptionCount = unfilteredMediaOptionInfo.length;
|
||
const filteredMediaOptionCount = filteredMediaOptionInfo.length;
|
||
const unchanged = unfilteredMediaOptionCount === filteredMediaOptionCount;
|
||
const removedMediaOptionIds = unfilteredMediaOptionInfo.filter((mediaOption) => !filteredMediaOptionInfo.includes(mediaOption)).map((mediaOption) => mediaOption.mediaOptionId);
|
||
logger.qe({ critical: true, name: 'mediaFiltering', data: { filterName, unchanged, remaining: filteredMediaOptionCount, filteredIds: removedMediaOptionIds } });
|
||
}
|
||
function printMediaOptionInfo(mediaOptionInfo, logger) {
|
||
logger.info(loggerName$1, `Unsupported level ${JSON.stringify(mediaOptionInfo, ['avgBandwidth', 'audioCodecList', 'videoCodecList', 'videoRange', 'frameRate', 'width', 'height', 'iframes'])}`);
|
||
}
|
||
function filterMediaOptionsBasedOnHlsConfig(mediaOptions, disableVideoCodecList, disableAudioCodecList, logger) {
|
||
let filteredMediaOptions = mediaOptions.filter((mediaOption) => {
|
||
if (mediaOption.videoCodec) {
|
||
return mediaOption.videoCodecList.every((codec) => {
|
||
const videoCodecRank = getVideoCodecRanking(codec);
|
||
return !disableVideoCodecList.has(videoCodecRank);
|
||
});
|
||
}
|
||
return true;
|
||
});
|
||
filteredMediaOptions = filteredMediaOptions.filter((mediaOption) => {
|
||
if (!mediaOption.iframes && mediaOption.audioCodec) {
|
||
return mediaOption.audioCodecList.every((codec) => {
|
||
const audioCodecRank = getAudioCodecRanking(codec);
|
||
return !disableAudioCodecList.has(audioCodecRank);
|
||
});
|
||
}
|
||
return true;
|
||
});
|
||
filterMediaOptionInfoPrint('Disable Codec config', mediaOptions, filteredMediaOptions, logger);
|
||
return filteredMediaOptions;
|
||
}
|
||
function filterMediaOptionsBasedOnMaxAllowedHdcpLevel(mediaOptions, maxHdcpLevel, logger) {
|
||
const maxHdcpRanking = hdcpLevelToInt(maxHdcpLevel);
|
||
const filteredMediaOptions = mediaOptions.filter((mediaOption) => {
|
||
const curHdcpLevel = mediaOption.hdcpLevel;
|
||
return !curHdcpLevel || hdcpLevelToInt(curHdcpLevel) <= maxHdcpRanking;
|
||
});
|
||
filterMediaOptionInfoPrint('Hdcp', mediaOptions, filteredMediaOptions, logger);
|
||
return filteredMediaOptions;
|
||
}
|
||
function filterMediaOptionsBasedOnSecurityLevel(mediaOptions, maxSecurityLevel, keySystemId, logger) {
|
||
const securityLevels = KeySystemFactory$1.getKeySystemSecurityLevel(keySystemId);
|
||
const keyFormat = KeySystemFactory$1.getKeySystemFormat(keySystemId);
|
||
const getSecurityLevelRanking = function (curSecurityLevel) {
|
||
return isValidSecurityLevel(curSecurityLevel, keySystemId) ? securityLevels[curSecurityLevel] : -1;
|
||
};
|
||
const maxSecurityLevelRanking = getSecurityLevelRanking(maxSecurityLevel);
|
||
const filteredMediaOptions = mediaOptions.filter((mediaOption) => {
|
||
var _a, _b;
|
||
const cpcLabels = (_b = (_a = mediaOption.allowedCPCMap) === null || _a === void 0 ? void 0 : _a[keyFormat]) !== null && _b !== void 0 ? _b : [];
|
||
let selectLevel = true;
|
||
for (const currentLabelSecurityLevel of cpcLabels) {
|
||
const currentLabelSecurityLevelRanking = getSecurityLevelRanking(currentLabelSecurityLevel);
|
||
selectLevel = currentLabelSecurityLevelRanking <= maxSecurityLevelRanking;
|
||
if (!selectLevel) {
|
||
break;
|
||
}
|
||
}
|
||
return selectLevel;
|
||
});
|
||
filterMediaOptionInfoPrint('Security Level', mediaOptions, filteredMediaOptions, logger);
|
||
return [...filteredMediaOptions];
|
||
}
|
||
function filterMediaOptionsBasedOnMediaKeySystemAccess(mediaOptions, keySystemId, logger) {
|
||
const cachedKeySystemConfig = new Map();
|
||
function handleKeySystem(keySystemId, levelInfo, isKeySystemSupportedPromises) {
|
||
const mediaCapabilities = keysystemutil.getCapabilities(levelInfo.videoCodecList, levelInfo.audioCodecList);
|
||
const configStr = JSON.stringify(mediaCapabilities);
|
||
let requestKeySystemPromise;
|
||
if (cachedKeySystemConfig.has(configStr)) {
|
||
requestKeySystemPromise = cachedKeySystemConfig.get(configStr);
|
||
}
|
||
else {
|
||
requestKeySystemPromise = KeySystemFactory$1.requestKeySystemAccess(keySystemId, mediaCapabilities, undefined, logger).pipe(map(() => {
|
||
return true;
|
||
}), catchError((error) => {
|
||
logger.warn(`Request key system error: ${error.message}`);
|
||
return of(false);
|
||
}), shareReplay({ bufferSize: 1, refCount: true }));
|
||
cachedKeySystemConfig.set(configStr, requestKeySystemPromise);
|
||
}
|
||
isKeySystemSupportedPromises.push(requestKeySystemPromise);
|
||
}
|
||
const isSupported = new Array();
|
||
mediaOptions.forEach((mediaOption) => {
|
||
const isKeySystemSupportedPromises = Array();
|
||
handleKeySystem(keySystemId, mediaOption, isKeySystemSupportedPromises);
|
||
const p = forkJoin(isKeySystemSupportedPromises).pipe(map((results) => {
|
||
// If no unsupported config found then we're good
|
||
if (results.find((supported) => supported === false) === undefined) {
|
||
return mediaOption;
|
||
}
|
||
}));
|
||
isSupported.push(p);
|
||
});
|
||
return forkJoin(isSupported).pipe(map((results) => {
|
||
return results.filter((r) => Boolean(r));
|
||
}));
|
||
}
|
||
/**
|
||
* Use MediaCapabilities API to determine whether we can play this.
|
||
* @param mediaOptions
|
||
* @param audioMediaOptions
|
||
*/
|
||
function filterBasedOnMediaCapabilities(mediaOptions, audioMediaOptions, logger) {
|
||
logger.debug(loggerName$1, 'Using MediaCapabilities');
|
||
const observables = [];
|
||
const isSupportedInternal = createIsSupportedInternal();
|
||
const handleCodec = createHandleCodec(logger);
|
||
mediaOptions.forEach((mediaOption) => {
|
||
var _a, _b;
|
||
let isTypeSupportedObservables = [];
|
||
(_a = mediaOption.videoCodecList) === null || _a === void 0 ? void 0 : _a.forEach((codec) => {
|
||
isTypeSupportedObservables = handleCodec(mediaOption, audioMediaOptions, codec, true, isTypeSupportedObservables);
|
||
});
|
||
if (((_b = mediaOption.audioCodecList) === null || _b === void 0 ? void 0 : _b.length) > 0) {
|
||
const codec = RichestMedia.getRichestAudioCodec(mediaOption.audioCodecList);
|
||
isTypeSupportedObservables = handleCodec(mediaOption, audioMediaOptions, codec, false, isTypeSupportedObservables);
|
||
}
|
||
let o = of(mediaOption);
|
||
if (isTypeSupportedObservables.length > 0) {
|
||
o = forkJoin(isTypeSupportedObservables).pipe(map((results) => {
|
||
const supported = results.find((supported) => supported === false) == undefined;
|
||
if (!supported) {
|
||
printMediaOptionInfo(mediaOption, logger);
|
||
}
|
||
return supported ? mediaOption : null;
|
||
}), catchError((error) => {
|
||
logger.warn(loggerName$1, `decodingInfo errror: ${error.message}`); // Missing field probably
|
||
const supported = isSupportedInternal(mediaOption);
|
||
if (!supported) {
|
||
printMediaOptionInfo(mediaOption, logger);
|
||
}
|
||
return of(supported ? mediaOption : null);
|
||
}));
|
||
}
|
||
observables.push(o);
|
||
});
|
||
return forkJoin(observables).pipe(map((list) => list.filter((item) => Boolean(item))));
|
||
}
|
||
function filterMediaOptionsBasedOnAudioAndVideoCodecs(mediaOptions, logger) {
|
||
const isSupportedInternal = createIsSupportedInternal();
|
||
const filteredMediaOptions = mediaOptions.filter(isSupportedInternal);
|
||
filterMediaOptionInfoPrint('Platform Supported Media Codec', mediaOptions, filteredMediaOptions, logger);
|
||
return filteredMediaOptions;
|
||
}
|
||
function filterMediaOptionsBasedOnExtendedAudioParameters(mediaOptions, audioMediaOptions, logger) {
|
||
const supportedMimeCodecs = new Set();
|
||
const unsupportedMimeCodecs = new Set();
|
||
/* Intentional negative tests to identify platform support for 'channels' and 'features' attributes via isTypeSupported().
|
||
- If the platform supports 'channels' & 'features' attributes, then the below calls will return false.
|
||
- Otherwise, if we get true, that means the platform ignores the 'channels' and/or 'features' attribute and we cannot rely on this API for testing them.
|
||
*/
|
||
const shouldCheckChannels = !MediaSource.isTypeSupported('audio/mp4; codecs="mp4a.40.2"; channels="-1"');
|
||
const shouldCheckFeatures = shouldCheckChannels && !MediaSource.isTypeSupported('audio/mp4; codecs="mp4a.40.2"; channels="2"; features="INVALID"');
|
||
logger.info(loggerName$1, `Platform support for channels: ${shouldCheckChannels}; features: ${shouldCheckFeatures}`);
|
||
const checkSupported = (codec, channels) => {
|
||
const parameters = channels.split('/');
|
||
const channelCount = parseInt(parameters[0]);
|
||
let mimeCodec;
|
||
let mimeCodecAlternate;
|
||
if (parameters.length > 1) {
|
||
const features = parameters[1].split(',')[0]; // 'features' is the first in the comma separated list.
|
||
mimeCodec = `audio/mp4;codecs="${codec}";channels="${channelCount}";features="${features}"`;
|
||
mimeCodecAlternate = `audio/mp4;codecs="${codec}";channels="8";features="${features}"`;
|
||
}
|
||
else {
|
||
mimeCodec = `audio/mp4;codecs="${codec}";channels="${channelCount}"`;
|
||
}
|
||
let isSupported = MediaSource.isTypeSupported(mimeCodec);
|
||
// LG doesn't return isTypeSupported correctly for 16ch ATMOS. This workaround uses 8ch test for ATMOS support.
|
||
// This code should be removed when LG firmware fix is in place.
|
||
if (!isSupported && mimeCodecAlternate) {
|
||
isSupported = MediaSource.isTypeSupported(mimeCodecAlternate);
|
||
logger.info(loggerName$1, `Using mimeCodecAlternate, isSupported: ${isSupported}; mimeCodecAlternate: ${mimeCodecAlternate}`);
|
||
}
|
||
return isSupported;
|
||
};
|
||
const updateCachedMimeCodecSetsIfNecessary = (codec, channels) => {
|
||
const mimeCodec = `${codec}/${channels}`;
|
||
if (!supportedMimeCodecs.has(mimeCodec) && !unsupportedMimeCodecs.has(mimeCodec)) {
|
||
if (checkSupported(codec, channels)) {
|
||
supportedMimeCodecs.add(mimeCodec);
|
||
}
|
||
else {
|
||
unsupportedMimeCodecs.add(mimeCodec);
|
||
}
|
||
}
|
||
};
|
||
// Update the mimeCodec support/notSupport cache based on MediaSource support.
|
||
// After that verify if that mimeCodec falls into unsupported category.
|
||
const checkUnsupportedMimeCodec = (codec, channels, logger) => {
|
||
const isAtmos = MediaUtil.isDolbyAtmos(codec, channels);
|
||
if (shouldCheckFeatures || (shouldCheckChannels && !isAtmos)) {
|
||
// check for platform support
|
||
updateCachedMimeCodecSetsIfNecessary(codec, channels);
|
||
const mimeCodec = `${codec}/${channels}`;
|
||
return unsupportedMimeCodecs.has(mimeCodec);
|
||
}
|
||
else {
|
||
// Don't allow ATMOS if we cannot reliably check support via MediaSource.isTypeSupported()
|
||
// All other cases are already handled via codec support
|
||
return isAtmos ? true : false;
|
||
}
|
||
};
|
||
const filteredMediaOptions = mediaOptions.filter((mediaOption) => {
|
||
let foundUnsupportedMimeCodec = false;
|
||
// Extended parameters are valid only if there is an audio group
|
||
if (mediaOption.audioCodecList && mediaOption.audioGroupId) {
|
||
const channels = RichestMedia.getRichestChannelLayoutForGroupId(mediaOption.audioGroupId, audioMediaOptions);
|
||
if (mediaOption.audioCodecList.length > 0 && channels) {
|
||
// pick the highest ranking audio codec to match with the channels tag
|
||
const richestCodec = RichestMedia.getRichestAudioCodec(mediaOption.audioCodecList);
|
||
// check only if channels attribute is present, otherwise, the case is already handled through audio codecs
|
||
foundUnsupportedMimeCodec = checkUnsupportedMimeCodec(richestCodec, channels);
|
||
}
|
||
}
|
||
return !foundUnsupportedMimeCodec;
|
||
});
|
||
filterMediaOptionInfoPrint('Richest Audio Codec Selection', mediaOptions, filteredMediaOptions, logger);
|
||
return filteredMediaOptions;
|
||
}
|
||
function getCapsForMediaOption(mediaOption, platformInfo) {
|
||
if (!platformInfo) {
|
||
return {
|
||
highestPlayableAverageBitRate: undefined,
|
||
highestPlayablePeakBitRate: undefined,
|
||
highestPlayableWidth: undefined,
|
||
highestPlayableHeight: undefined,
|
||
highestPlayableFrameRate: undefined,
|
||
};
|
||
}
|
||
const firstMediaOptionCodec = mediaOption.videoCodec;
|
||
const firstMediaOptionVideoRange = mediaOption.videoRange;
|
||
const videoDynamicRangeFormats = platformInfo.videoDynamicRangeFormats;
|
||
const videoCodecs = platformInfo.videoCodecs;
|
||
const vdrType = MediaUtil.getDynamicRangeType(firstMediaOptionVideoRange, firstMediaOptionCodec);
|
||
const compressionType = MediaUtil.getCompressionType(firstMediaOptionCodec);
|
||
const caps = getCappingInfo(vdrType, compressionType, videoCodecs, videoDynamicRangeFormats);
|
||
// Ignore highestPlayablePeakBitRateForClearContent for all codecs and formats except VP9
|
||
if (compressionType !== CompressionType.VP09) {
|
||
caps.highestPlayablePeakBitRateForClearContent = undefined;
|
||
}
|
||
return caps;
|
||
}
|
||
function filterMediaOptionsByCaps(mediaOptions, sessionKeys, platformInfo, logger) {
|
||
const hasSessionKeys = (sessionKeys === null || sessionKeys === void 0 ? void 0 : sessionKeys.length) > 0;
|
||
const filteredMediaOptions = mediaOptions.filter((mediaOption) => {
|
||
const caps = getCapsForMediaOption(mediaOption, platformInfo);
|
||
const { highestPlayablePeakBitRateForClearContent } = caps;
|
||
// Use highestPlayablePeakBitRate|highestPlayablePeakBitRateForClearContent based on the presence of sessionKeys or allowedCPCMap which we assume means contents are encrypted
|
||
const assumeEncryptedContent = mediaOption.allowedCPCMap || hasSessionKeys;
|
||
const isWithinSafePeekBitRate = isWithinCap(mediaOption.bandwidth, caps.highestPlayablePeakBitRate);
|
||
const isWithinPeekBitRate = assumeEncryptedContent || !highestPlayablePeakBitRateForClearContent
|
||
? isWithinSafePeekBitRate
|
||
: isWithinSafePeekBitRate || isWithinCap(mediaOption.bandwidth, highestPlayablePeakBitRateForClearContent);
|
||
return (isWithinPeekBitRate &&
|
||
isWithinCap(mediaOption.avgBandwidth, caps.highestPlayableAverageBitRate) &&
|
||
isWithinCap(mediaOption.width, caps.highestPlayableWidth) &&
|
||
isWithinCap(mediaOption.height, caps.highestPlayableHeight) &&
|
||
isWithinCap(mediaOption.frameRate, caps.highestPlayableFrameRate));
|
||
});
|
||
filterMediaOptionInfoPrint('Highest Playable Platform Caps', mediaOptions, filteredMediaOptions, logger);
|
||
return filteredMediaOptions;
|
||
}
|
||
function filterIframeMediaOptionsBasedOnCapOn1080p(mediaOptions, logger) {
|
||
// standard 1080p resolution with a slight tolerance of 1.2
|
||
const PIXELS_CAP = 2488320;
|
||
const filteredMediaOptions = mediaOptions.filter((mediaOption) => !mediaOption.iframes || !mediaOption.width || !mediaOption.height || mediaOption.width * mediaOption.height <= PIXELS_CAP);
|
||
filterMediaOptionInfoPrint('1080p iFrame', mediaOptions, filteredMediaOptions, logger);
|
||
return filteredMediaOptions;
|
||
}
|
||
function filterMediaOptionsBasedOnSupportedVideoFormats(mediaOptions, videoDynamicRangeFormats, logger) {
|
||
const supportedFormats = getSupportedVideoFormats(videoDynamicRangeFormats);
|
||
const { doViSupported, hdr10Supported, hlgSupported } = supportedFormats;
|
||
logger.info(loggerName$1, `Supported videoDynamicRangeFormats DoVi/HDR10/HLG: ${doViSupported}/${hdr10Supported}/${hlgSupported}`);
|
||
// filter based on supported videoDynamicRangeFormats
|
||
// only keep level with correct codec for video-range
|
||
const validMediaOptions = mediaOptions.reduce((prev, option) => {
|
||
var _a;
|
||
const vdr = MediaUtil.getDynamicRangeType(option.videoRange, (_a = option.videoCodec) !== null && _a !== void 0 ? _a : '');
|
||
switch (vdr) {
|
||
case VideoDynamicRangeType.HDR:
|
||
case VideoDynamicRangeType.HDR10:
|
||
if (hdr10Supported) {
|
||
prev.hdrMediaOptions.push(option);
|
||
}
|
||
break;
|
||
case VideoDynamicRangeType.DolbyVision:
|
||
if (doViSupported) {
|
||
prev.hdrMediaOptions.push(option);
|
||
}
|
||
break;
|
||
case VideoDynamicRangeType.HLG:
|
||
if (hlgSupported) {
|
||
prev.hdrMediaOptions.push(option);
|
||
}
|
||
break;
|
||
default:
|
||
// default is SDR
|
||
if (option.videoRange === 'SDR' || option.videoRange == null) {
|
||
prev.sdrMediaOptions.push(option);
|
||
}
|
||
}
|
||
return prev;
|
||
}, { hdrMediaOptions: new Array(), sdrMediaOptions: new Array() });
|
||
logger.debug(loggerName$1, `HDR/SDR mediaOptions: ${validMediaOptions.hdrMediaOptions.length}/${validMediaOptions.sdrMediaOptions.length}`);
|
||
return validMediaOptions;
|
||
}
|
||
/**
|
||
* Filter root playlist. Remove all media options that should not ever be used on this platform based on
|
||
* capabilities (platformInfo and MediaCapabilities)
|
||
*
|
||
* @param mediaOptions Original mediaOption list
|
||
* @param audioMediaOptions Alternate mediaOption info
|
||
* @param platformInfo Platform information for filtering out media options
|
||
* @param config HLS Config option
|
||
* @returns { hdrMediaOptions: valid hdr media options, sdrMediaOptions: valid sdr media options }
|
||
*/
|
||
function filterByPlatformCapabilities(mediaOptions, audioMediaOptions, sessionKeys, platformInfo, config, logger) {
|
||
const maxHdcpLevel = (platformInfo === null || platformInfo === void 0 ? void 0 : platformInfo.maxHdcpLevel) || undefined;
|
||
let filteredMediaOptions = [...mediaOptions];
|
||
if (config.disableVideoCodecList.size > 0 || config.disableAudioCodecList.size > 0) {
|
||
filteredMediaOptions = filterMediaOptionsBasedOnHlsConfig(filteredMediaOptions, config.disableVideoCodecList, config.disableAudioCodecList, logger);
|
||
}
|
||
if (maxHdcpLevel && isHdcpLevel(maxHdcpLevel)) {
|
||
filteredMediaOptions = filterMediaOptionsBasedOnMaxAllowedHdcpLevel(filteredMediaOptions, maxHdcpLevel, logger);
|
||
}
|
||
// Filter based on ALLOWED-CPC
|
||
const maxSecurityLevel = platformInfo === null || platformInfo === void 0 ? void 0 : platformInfo.maxSecurityLevel;
|
||
const keySystemId = config === null || config === void 0 ? void 0 : config.keySystemPreference;
|
||
if (maxSecurityLevel && keySystemId && isValidSecurityLevel(maxSecurityLevel, keySystemId)) {
|
||
filteredMediaOptions = filterMediaOptionsBasedOnSecurityLevel(filteredMediaOptions, maxSecurityLevel, keySystemId, logger);
|
||
}
|
||
// parse and set audioChannelCount property
|
||
filteredMediaOptions = filteredMediaOptions.map((mediaOption) => {
|
||
if (mediaOption.audioCodecList && mediaOption.audioGroupId) {
|
||
const matchingTrack = audioMediaOptions.find((audio) => audio.groupId === mediaOption.audioGroupId);
|
||
const channels = matchingTrack === null || matchingTrack === void 0 ? void 0 : matchingTrack.channels;
|
||
if (channels) {
|
||
mediaOption.audioChannelCount = parseInt(channels);
|
||
}
|
||
}
|
||
return mediaOption;
|
||
});
|
||
const useMediaKeySystemAccessFilter = (config === null || config === void 0 ? void 0 : config.useMediaKeySystemAccessFilter) || false;
|
||
const keySystemAccessEnabled = useMediaKeySystemAccessFilter && keySystemId && navigator && typeof navigator.requestMediaKeySystemAccess === 'function';
|
||
const source = keySystemAccessEnabled ? filterMediaOptionsBasedOnMediaKeySystemAccess(filteredMediaOptions, keySystemId, logger) : of(filteredMediaOptions);
|
||
return source.pipe(switchMap((keySystemMediaOptions) => {
|
||
if (keySystemMediaOptions.length === 0 || onlyIframes(keySystemMediaOptions)) {
|
||
throw new PlaylistParsingError(ErrorTypes.MEDIA_ERROR, ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR, undefined, 'no media option with compatible codecs found in playlist', undefined);
|
||
}
|
||
if (keySystemAccessEnabled) {
|
||
filterMediaOptionInfoPrint('Key System Support', filteredMediaOptions, keySystemMediaOptions, logger);
|
||
}
|
||
const mediaCapabilities = navigator && navigator.mediaCapabilities;
|
||
const useMediaCapabilities = (config === null || config === void 0 ? void 0 : config.useMediaCapabilities) || false;
|
||
const mediaCapabilitiesEnabled = useMediaCapabilities && mediaCapabilities && typeof mediaCapabilities.decodingInfo === 'function';
|
||
let source;
|
||
if (mediaCapabilitiesEnabled) {
|
||
source = filterBasedOnMediaCapabilities(keySystemMediaOptions, audioMediaOptions, logger);
|
||
}
|
||
else {
|
||
keySystemMediaOptions = filterMediaOptionsBasedOnAudioAndVideoCodecs(keySystemMediaOptions, logger);
|
||
keySystemMediaOptions = filterMediaOptionsBasedOnExtendedAudioParameters(keySystemMediaOptions, audioMediaOptions, logger);
|
||
source = of(keySystemMediaOptions);
|
||
}
|
||
return source.pipe(map((compatibleMediaOptions) => {
|
||
if (compatibleMediaOptions.length === 0 || onlyIframes(compatibleMediaOptions)) {
|
||
throw new PlaylistParsingError(ErrorTypes.MEDIA_ERROR, ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR, undefined, 'no media option with compatible codecs found in manifest', undefined);
|
||
}
|
||
compatibleMediaOptions = filterMediaOptionsByCaps(compatibleMediaOptions, sessionKeys, platformInfo, logger);
|
||
compatibleMediaOptions = filterIframeMediaOptionsBasedOnCapOn1080p(compatibleMediaOptions, logger);
|
||
if (compatibleMediaOptions.length === 0 || onlyIframes(compatibleMediaOptions)) {
|
||
throw new PlaylistParsingError(ErrorTypes.MEDIA_ERROR, ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR, undefined, 'no media option with compatible codecs found in manifest', undefined);
|
||
}
|
||
// Seperate out fallback mediaOptions if needed
|
||
let videoDynamicRangeFormats = (platformInfo === null || platformInfo === void 0 ? void 0 : platformInfo.videoDynamicRangeFormats) || [];
|
||
if (mediaCapabilitiesEnabled && videoDynamicRangeFormats.length === 0) {
|
||
// Just allow all types; MediaCapabilities API should have filtered out anything that was unsupported
|
||
videoDynamicRangeFormats = [
|
||
{ type: VideoDynamicRangeType.SDR },
|
||
{ type: VideoDynamicRangeType.HDR },
|
||
{ type: VideoDynamicRangeType.HDR10 },
|
||
{ type: VideoDynamicRangeType.DolbyVision },
|
||
{ type: VideoDynamicRangeType.HLG },
|
||
];
|
||
}
|
||
const { hdrMediaOptions, sdrMediaOptions } = filterMediaOptionsBasedOnSupportedVideoFormats(compatibleMediaOptions, videoDynamicRangeFormats, logger);
|
||
if ((hdrMediaOptions.length === 0 && sdrMediaOptions.length === 0) || (onlyIframes(hdrMediaOptions) && onlyIframes(sdrMediaOptions))) {
|
||
throw new PlaylistParsingError(ErrorTypes.MEDIA_ERROR, ErrorDetails.MANIFEST_INCOMPATIBLE_VIDEO_RANGE_ERROR, undefined, 'mediaOption with compatible VIDEO-RANGE not found in manifest', undefined);
|
||
}
|
||
return { hdrMediaOptions, sdrMediaOptions };
|
||
}), catchError((err) => {
|
||
if (err instanceof PlaylistParsingError) {
|
||
err.fatal = true;
|
||
err.response = ErrorResponses.IncompatibleAsset;
|
||
}
|
||
throw err;
|
||
}));
|
||
}));
|
||
}
|
||
|
||
function mediaOptionToString(mediaOption) {
|
||
const MediaOptionLogAttributes = [
|
||
'persistentId',
|
||
'bitrate',
|
||
'bandwidth',
|
||
'avg-bandwidth',
|
||
'width',
|
||
'height',
|
||
'videoCodec',
|
||
'audioCodec',
|
||
'videoRange',
|
||
'iframes',
|
||
'frameRate',
|
||
'audioGroupId',
|
||
'score',
|
||
];
|
||
return JSON.stringify(mediaOption, MediaOptionLogAttributes);
|
||
}
|
||
|
||
function isValidCandidate(fromTrack, candidate) {
|
||
// Same persistentID but different rendition group
|
||
return candidate.mediaOptionId !== fromTrack.mediaOptionId && candidate.persistentID === fromTrack.persistentID && candidate.groupId !== fromTrack.groupId;
|
||
}
|
||
class AlternateMediaOptionListQuery extends MediaOptionListQuery {
|
||
constructor(store, itemId, mediaOptionType) {
|
||
super(store, itemId, mediaOptionType);
|
||
}
|
||
static makeFilters() {
|
||
return makeCommonFilters();
|
||
}
|
||
_initFilters() {
|
||
return AlternateMediaOptionListQuery.kAllowFilters;
|
||
}
|
||
// For convenience
|
||
get _mediaOptionType() {
|
||
return this.mediaOptionType;
|
||
}
|
||
get preferredHost() {
|
||
return null;
|
||
}
|
||
get preferredHost$() {
|
||
return of(null);
|
||
}
|
||
get mediaOptionListInfo() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.getEntity(this.itemId)) === null || _a === void 0 ? void 0 : _a.mediaOptionListTuple[this._mediaOptionType]) !== null && _b !== void 0 ? _b : null;
|
||
}
|
||
get mediaOptionListInfo$() {
|
||
return this.selectEntity(this.itemId, (entity) => (entity && entity.mediaOptionListTuple ? entity.mediaOptionListTuple[this._mediaOptionType] : null)).pipe(filterNullOrUndefined());
|
||
}
|
||
// excludingVariants for skipping variants visited in successive getFallbackVariant; not applicable to alternates
|
||
// since we are looking for a *variant* fallback
|
||
getFallbackVariant(fromId, sdrOnly, shouldSwitchHosts, excludingVariants = undefined) {
|
||
var _a, _b;
|
||
const fromTrack = (_a = this.mediaOptionList) === null || _a === void 0 ? void 0 : _a.find((track) => track.mediaOptionId === fromId);
|
||
if (!fromTrack) {
|
||
return null;
|
||
}
|
||
const filteredList = this.filteredMediaOptionList;
|
||
if (!filteredList) {
|
||
return null;
|
||
}
|
||
// If shouldSwitchHosts is true, we must switch hosts
|
||
const fromHost = getHostName(fromTrack.url);
|
||
if (shouldSwitchHosts) {
|
||
return (_b = filteredList.find((candidate) => isValidCandidate(fromTrack, candidate) && !hasMatchingHost(fromHost, candidate.url))) !== null && _b !== void 0 ? _b : null;
|
||
}
|
||
// else, allow any host but favor current if available
|
||
let newTrack = null;
|
||
for (const candidate of filteredList) {
|
||
if (isValidCandidate(fromTrack, candidate) && (!newTrack || hasMatchingHost(fromHost, candidate.url))) {
|
||
newTrack = candidate;
|
||
}
|
||
}
|
||
return newTrack;
|
||
}
|
||
// excludingOptions for skipping soon-to-be-penalized alternates
|
||
getMatchingAlternateWithPersistentId(persistentId, variant, excludingOptions) {
|
||
var _a;
|
||
return ((_a = this.preferredMediaOptionList.find((option) => {
|
||
if ((excludingOptions === null || excludingOptions === void 0 ? void 0 : excludingOptions.length) > 0 && excludingOptions.includes(option.mediaOptionId)) {
|
||
return false;
|
||
}
|
||
if (!isFiniteNumber(persistentId) || option.persistentID === persistentId) {
|
||
if (variant) {
|
||
// match group and persistent id
|
||
return this.matchGroup(option, variant.audioGroupId, variant.subtitleGroupId, variant.closedcaption);
|
||
}
|
||
else {
|
||
// match only persistent id
|
||
return true;
|
||
}
|
||
}
|
||
else {
|
||
return false;
|
||
}
|
||
})) !== null && _a !== void 0 ? _a : null);
|
||
}
|
||
matchGroup(mediaOption, audioGroup, subtitleGroup, closedCaptionGroup) {
|
||
let result = false;
|
||
switch (mediaOption.type) {
|
||
case 'CLOSED-CAPTIONS':
|
||
result = !closedCaptionGroup || mediaOption.groupId === closedCaptionGroup;
|
||
break;
|
||
case 'SUBTITLES':
|
||
result = !subtitleGroup || mediaOption.groupId === subtitleGroup;
|
||
break;
|
||
case 'AUDIO':
|
||
result = !audioGroup || mediaOption.groupId === audioGroup;
|
||
break;
|
||
}
|
||
return result;
|
||
}
|
||
getMatchingAlternate(fromId, variant) {
|
||
const fromAlternate = this.mediaOptionFromId(fromId);
|
||
return this.getMatchingAlternateWithPersistentId(fromAlternate === null || fromAlternate === void 0 ? void 0 : fromAlternate.persistentID, variant, []);
|
||
}
|
||
/**
|
||
* Package Alternate media Option
|
||
* Augment closed-captions mediaOption with the playlist URL of a supporting subtitle mediaOption.
|
||
* This allows Apple-style closed-caption track to include forced subtitles from the supporting WebVTT playlist.
|
||
*
|
||
* @param variantOption The variantMediaOption that is to be augmented.
|
||
* @param altOption The alternate media option (has to be of type closed caption) to be augmented
|
||
* @param useFilteredList boolean if true use a filtered list for packaging, hence do not include media in penalty Box
|
||
* if false use an unfiltered raw list as seen from the manifest.
|
||
* @return AlternateMediaOption A packaged alternate media option or altOption if packaging was not possible
|
||
*/
|
||
packageAlternateMediaOption(variantOption, altOption, useFilteredList) {
|
||
return altOption.mediaType === MediaTypeFourCC.CLOSEDCAPTION ? this.augmentClosedCaptionsWithForcedSubtitles(variantOption === null || variantOption === void 0 ? void 0 : variantOption.subtitleGroupId, altOption, useFilteredList) : altOption;
|
||
}
|
||
augmentClosedCaptionsWithForcedSubtitles(subtitleGroup, closedCaptionMediaOption, useFilteredList) {
|
||
const forcedMediaOption = this.pairForcedSubtitleMediaOptionWithClosedCaption(subtitleGroup, closedCaptionMediaOption, useFilteredList);
|
||
return forcedMediaOption ? Object.assign(Object.assign({}, closedCaptionMediaOption), { url: forcedMediaOption.url, backingMediaOptionId: forcedMediaOption.mediaOptionId }) : closedCaptionMediaOption;
|
||
}
|
||
pairForcedSubtitleMediaOptionWithClosedCaption(subtitleGroup, selectedMediaOption, useFilteredList) {
|
||
let supportingOption;
|
||
if (selectedMediaOption && selectedMediaOption.mediaType === MediaTypeFourCC.CLOSEDCAPTION) {
|
||
let list = this.mediaOptionList;
|
||
if (useFilteredList) {
|
||
list = this.preferredMediaOptionList;
|
||
}
|
||
supportingOption = AlternateMediaOptionListQuery.pairForcedSubtitleMediaOptionWithClosedCaptionInList(subtitleGroup, selectedMediaOption, list);
|
||
}
|
||
return supportingOption;
|
||
}
|
||
static pairForcedSubtitleMediaOptionWithClosedCaptionInList(subtitleGroup, selectedMediaOption, mediaOptionList) {
|
||
return mediaOptionList.find(function (option) {
|
||
const match = option.mediaType === MediaTypeFourCC.SUBTITLE && option.lang === selectedMediaOption.lang && option.forced && option.autoselect && (!subtitleGroup || option.groupId === subtitleGroup);
|
||
if (match) {
|
||
getLogger().debug(`[subtitle] pick side track persistent id ${option.persistentID} option id ${option.id} option mediaOptionId ${option.mediaOptionId} group id ${option.groupId} (=${subtitleGroup})`);
|
||
}
|
||
return match;
|
||
});
|
||
}
|
||
}
|
||
AlternateMediaOptionListQuery.kAllowFilters = AlternateMediaOptionListQuery.makeFilters();
|
||
|
||
/**
|
||
* @brief Query interface to the root playlist store
|
||
*/
|
||
class RootPlaylistQuery extends QueryEntity {
|
||
constructor(rootPlaylistStore, itemId) {
|
||
super(rootPlaylistStore);
|
||
this.itemId = itemId;
|
||
this.mediaOptionListQueries = [
|
||
new VariantMediaOptionListQuery(rootPlaylistStore, this.itemId),
|
||
new AlternateMediaOptionListQuery(rootPlaylistStore, this.itemId, MediaOptionType.AltAudio),
|
||
new AlternateMediaOptionListQuery(rootPlaylistStore, this.itemId, MediaOptionType.Subtitle),
|
||
];
|
||
}
|
||
// getters
|
||
get rootPlaylistEntity() {
|
||
return this.getEntity(this.itemId);
|
||
}
|
||
/**
|
||
* @returns the unmodified original media options without filters applied
|
||
*/
|
||
get rootMediaOptionsTuple() {
|
||
var _a;
|
||
const mediaOptionListTuple = (_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.mediaOptionListTuple;
|
||
if (mediaOptionListTuple) {
|
||
return [mediaOptionListTuple[0].mediaOptions, mediaOptionListTuple[1].mediaOptions, mediaOptionListTuple[2].mediaOptions];
|
||
}
|
||
return [[], [], []];
|
||
}
|
||
/**
|
||
* @returns the position in media corresponding to the start of this item
|
||
*/
|
||
get itemStartOffset() {
|
||
var _a, _b, _c;
|
||
if (((_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.itemStartOffset) && isFiniteNumber((_b = this.rootPlaylistEntity) === null || _b === void 0 ? void 0 : _b.itemStartOffset)) {
|
||
return (_c = this.rootPlaylistEntity) === null || _c === void 0 ? void 0 : _c.itemStartOffset;
|
||
}
|
||
return 0;
|
||
}
|
||
get highestVideoCodec() {
|
||
var _a;
|
||
return (_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.highestVideoCodec;
|
||
}
|
||
get baseUrl() {
|
||
var _a;
|
||
return (_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.baseUrl;
|
||
}
|
||
get anchorTime() {
|
||
var _a;
|
||
return (_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.anchorTime;
|
||
}
|
||
get discoSeqNum() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.discoSeqNum) !== null && _b !== void 0 ? _b : NaN;
|
||
}
|
||
get discoSeqNum$() {
|
||
return this.selectEntity(this.itemId, 'discoSeqNum');
|
||
}
|
||
get audioMediaSelectionGroup() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.audioMediaSelectionGroup) !== null && _b !== void 0 ? _b : null;
|
||
}
|
||
get subtitleMediaSelectionGroup() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.subtitleMediaSelectionGroup) !== null && _b !== void 0 ? _b : null;
|
||
}
|
||
get audioMediaSelectionOptions() {
|
||
var _a, _b, _c;
|
||
return (_c = (_b = (_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.audioMediaSelectionGroup) === null || _b === void 0 ? void 0 : _b.MediaSelectionGroupOptions) !== null && _c !== void 0 ? _c : [];
|
||
}
|
||
get subtitleMediaSelectionOptions() {
|
||
var _a, _b, _c;
|
||
return (_c = (_b = (_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.subtitleMediaSelectionGroup) === null || _b === void 0 ? void 0 : _b.MediaSelectionGroupOptions) !== null && _c !== void 0 ? _c : [];
|
||
}
|
||
get contentSteeringOption() {
|
||
var _a;
|
||
return (_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.contentSteeringOption;
|
||
}
|
||
get masterVariableList() {
|
||
var _a;
|
||
return (_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.masterVariableList;
|
||
}
|
||
get loadStats() {
|
||
var _a;
|
||
return (_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.loadStats;
|
||
}
|
||
get isMediaPlaylist() {
|
||
var _a;
|
||
return (_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.isMediaPlaylist;
|
||
}
|
||
getInitPTS(discoSeqNum) {
|
||
var _a;
|
||
return (_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.initPtsRecord[discoSeqNum];
|
||
}
|
||
get abrStatus$() {
|
||
return this.selectEntity(this.itemId, (entity) => entity === null || entity === void 0 ? void 0 : entity.abrStatus);
|
||
}
|
||
get abrStatus() {
|
||
var _a;
|
||
return (_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.abrStatus;
|
||
}
|
||
get nextMaxAutoOptionId() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.abrStatus) === null || _b === void 0 ? void 0 : _b.nextMaxAutoOptionId;
|
||
}
|
||
get nextMinAutoOptionId() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.abrStatus) === null || _b === void 0 ? void 0 : _b.nextMinAutoOptionId;
|
||
}
|
||
initPTS$(discoSeqNum) {
|
||
return this.selectEntity(this.itemId, ({ initPtsRecord }) => initPtsRecord[discoSeqNum]);
|
||
}
|
||
// observables
|
||
get rootPlaylistEntity$() {
|
||
return this.selectEntity(this.itemId).pipe(filter((rootEntity) => Boolean(rootEntity)), map((rootEntity) => rootEntity));
|
||
}
|
||
get rootPlaylistEntityAdded$() {
|
||
return this.selectEntityAction(EntityActions.Add).pipe(map((itemIds) => itemIds.map((itemId) => this.getEntity(itemId))));
|
||
}
|
||
/**
|
||
* @returns the unmodified original media options without filters applied
|
||
*/
|
||
get rootMediaOptionsTuple$() {
|
||
return combineQueries([
|
||
this.selectEntity(this.itemId, (entity) => entity.mediaOptionListTuple[0].mediaOptions),
|
||
this.selectEntity(this.itemId, (entity) => entity.mediaOptionListTuple[1].mediaOptions),
|
||
this.selectEntity(this.itemId, (entity) => entity.mediaOptionListTuple[2].mediaOptions),
|
||
]);
|
||
}
|
||
get sessionData() {
|
||
var _a;
|
||
return (_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.sessionData;
|
||
}
|
||
get sessionData$() {
|
||
return this.selectEntity(this.itemId, ({ sessionData }) => sessionData).pipe(filterNullOrUndefined());
|
||
}
|
||
get anchorTime$() {
|
||
return this.selectEntity(this.itemId, 'anchorTime').pipe(switchMap((anchorTime) => {
|
||
var _a, _b;
|
||
if (!isFiniteNumber(anchorTime === null || anchorTime === void 0 ? void 0 : anchorTime.pos)) {
|
||
return EMPTY;
|
||
}
|
||
if ((anchorTime === null || anchorTime === void 0 ? void 0 : anchorTime.pos) !== ((_a = this.anchorTime) === null || _a === void 0 ? void 0 : _a.pos)) {
|
||
// Kind of hacky but sometimes this happens????
|
||
getLogger().warn(`anchorTime doesn't match stored value! ${anchorTime === null || anchorTime === void 0 ? void 0 : anchorTime.pos} !== ${(_b = this.anchorTime) === null || _b === void 0 ? void 0 : _b.pos}`);
|
||
return EMPTY;
|
||
}
|
||
return of(anchorTime);
|
||
}));
|
||
}
|
||
get pendingSeek$() {
|
||
return this.selectEntity(this.itemId, ({ pendingSeek }) => pendingSeek).pipe(distinctUntilChanged((a, b) => a === b || (typeof a === 'number' && typeof b === 'number' && isNaN(a) && isNaN(b))));
|
||
}
|
||
get enabledMediaOptionKeys$() {
|
||
return this.selectEntity(this.itemId, 'enabledMediaOptionKeys').pipe(filter((keys) => Boolean(keys)));
|
||
}
|
||
get enabledMediaOptionKeys() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.getEntity(this.itemId)) === null || _a === void 0 ? void 0 : _a.enabledMediaOptionKeys) !== null && _b !== void 0 ? _b : [NoMediaOption, NoMediaOption, NoMediaOption];
|
||
}
|
||
get enabledMediaOptionSwitchContexts() {
|
||
var _a, _b;
|
||
return (_b = (_a = this.getEntity(this.itemId)) === null || _a === void 0 ? void 0 : _a.mediaOptionSwitchContexts) !== null && _b !== void 0 ? _b : [null, null, null];
|
||
}
|
||
enabledMediaOptionSwitchContextsByType$(mediaOptionType) {
|
||
return this.selectEntity(this.itemId, 'mediaOptionSwitchContexts').pipe(map((contexts) => contexts === null || contexts === void 0 ? void 0 : contexts[mediaOptionType]));
|
||
}
|
||
get enabledMediaOptions$() {
|
||
return combineQueries([
|
||
this.enabledMediaOptionByType$(MediaOptionType.Variant),
|
||
this.enabledMediaOptionByType$(MediaOptionType.AltAudio),
|
||
this.enabledMediaOptionByType$(MediaOptionType.Subtitle),
|
||
]);
|
||
}
|
||
// Convenience method for just A/V
|
||
get enabledAVOptions$() {
|
||
return combineQueries([this.enabledMediaOptionByType$(MediaOptionType.Variant), this.enabledMediaOptionByType$(MediaOptionType.AltAudio)]);
|
||
}
|
||
rawEnabledMediaOptionByType$(mediaOptionType) {
|
||
return this.enabledMediaOptionKeys$.pipe(map((enabledMediaOptionKeys) => {
|
||
const mediaOptionKey = enabledMediaOptionKeys[mediaOptionType];
|
||
if (!isEnabledMediaOption(mediaOptionKey)) {
|
||
// return NoMediaOption even if mediaOptionKey may have a valid itemId.
|
||
// callers typically check for NoMediaOption only to see if a track is disabled.
|
||
return NoMediaOption;
|
||
}
|
||
const options = this.rootMediaOptionsTuple[mediaOptionType];
|
||
const foundOption = options.find((option) => mediaOptionKeyEquals(option, mediaOptionKey));
|
||
return foundOption ? foundOption : NoMediaOption;
|
||
}));
|
||
}
|
||
enabledMediaOptionByType$(mediaOptionType) {
|
||
return this.rawEnabledMediaOptionByType$(mediaOptionType).pipe(distinctUntilChanged((a, b) => a.mediaOptionId === b.mediaOptionId && a.url === b.url));
|
||
}
|
||
/**
|
||
* Emits when selected option changes and returns switch information
|
||
*/
|
||
enabledMediaOptionSwitchForType$(mediaOptionType) {
|
||
// Only trigger on option change, not when context is set
|
||
// rawEnabledMediaOptionByType does not de-dupe mediaOption changes
|
||
// so it's possible to snapshot/represent a switch to the same mediaOption { fromId: sameMediaOptionId, toId: sameMediaOptionId }
|
||
// when this happens, the observer will know no switching took place and it won't trigger any change event.
|
||
// enabledMediaOptionByType will always return a stale mediaOption switch from previous activities,
|
||
// potentially causing bogus emits and event triggers.
|
||
return this.rawEnabledMediaOptionByType$(mediaOptionType).pipe(withLatestFrom(this.enabledMediaOptionSwitchContextsByType$(mediaOptionType)), startWith(null), pairwise(), map(([a, b]) => {
|
||
return { fromId: a === null || a === void 0 ? void 0 : a[0].mediaOptionId, toId: b === null || b === void 0 ? void 0 : b[0].mediaOptionId, switchContext: b === null || b === void 0 ? void 0 : b[1] };
|
||
}), distinctUntilChanged((x, y) => x.fromId === y.fromId && x.toId === y.toId));
|
||
}
|
||
enableMediaOptionSwitchedForType$(mediaOptionType) {
|
||
return this.enabledMediaOptionByType$(mediaOptionType).pipe(switchMap((mediaOption) => waitFor(combineLatest([of(mediaOption), this.enabledMediaOptionSwitchContextsByType$(mediaOptionType).pipe(pairwise())]), ([option, contexts]) => contexts[0] && !contexts[1])),
|
||
// drop the contexts because contexts[0] may be wrong in this sequence:
|
||
// 1. Request 1: setEnabledMediaOptions( englishMediaOption, { switchPosition: 0.0 });
|
||
// 2. Request 2: setEnabledMediaOptions( englishMediaOption, { switchPosition: 1.0 });
|
||
// 3. Request 2 succeeded: setEnabledMediaOptions( englishMediaOption, null);
|
||
// But this function will return [englishMediaOption, [{ switchPosition: 0.0 }, null]]
|
||
// Nonetheless, englishMediaOption did switch successfully.
|
||
map(([option]) => option));
|
||
}
|
||
enabledMediaOptionIdByType(mediaOptionType) {
|
||
return this.getEntity(this.itemId).enabledMediaOptionKeys[mediaOptionType].mediaOptionId;
|
||
}
|
||
get enabledVariantMediaOptionIdBeforeTrickplaySwitch() {
|
||
return this.getEntity(this.itemId).enabledVariantMediaOptionIdBeforeTrickplaySwitch;
|
||
}
|
||
variantMediaOptionById(mediaOptionId) {
|
||
return this.mediaOptionListQueries[MediaOptionType.Variant].mediaOptionFromId(mediaOptionId);
|
||
}
|
||
alternateMediaOptionById(mediaOptionType, mediaOptionId) {
|
||
return this.mediaOptionListQueries[mediaOptionType].mediaOptionFromId(mediaOptionId);
|
||
}
|
||
enabledAlternateMediaOptionByType(mediaOptionType) {
|
||
const enabledOptionId = this.enabledMediaOptionIdByType(mediaOptionType);
|
||
return this.alternateMediaOptionById(mediaOptionType, enabledOptionId);
|
||
}
|
||
get enabledVariantMediaOption() {
|
||
const enabledOptionId = this.enabledMediaOptionIdByType(MediaOptionType.Variant);
|
||
return this.variantMediaOptionById(enabledOptionId);
|
||
}
|
||
lastLoadedMediaOptionByType(mediaOptionType) {
|
||
var _a;
|
||
return (_a = this.getEntity(this.itemId).lastLoadedMediaOptionKeys) === null || _a === void 0 ? void 0 : _a[mediaOptionType];
|
||
}
|
||
/**
|
||
* Set by ABR and error handling. Next enabled media options to be used next time we are allowed to switch
|
||
*/
|
||
get nextMediaOptionsKeys$() {
|
||
return this.selectEntity(this.itemId, 'nextMediaOptionKeys');
|
||
}
|
||
/**
|
||
* Return the filtered list with preferred host filtering
|
||
*/
|
||
get preferredMediaOptions() {
|
||
return [this.mediaOptionListQueries[0].preferredMediaOptionList, this.mediaOptionListQueries[1].preferredMediaOptionList, this.mediaOptionListQueries[2].preferredMediaOptionList];
|
||
}
|
||
get preferredMediaOptions$() {
|
||
return combineQueries([
|
||
this.mediaOptionListQueries[0].preferredMediaOptionList$,
|
||
this.mediaOptionListQueries[1].preferredMediaOptionList$,
|
||
this.mediaOptionListQueries[2].preferredMediaOptionList$,
|
||
]);
|
||
}
|
||
get filteredMediaOptions() {
|
||
return [this.mediaOptionListQueries[0].filteredMediaOptionList, this.mediaOptionListQueries[1].filteredMediaOptionList, this.mediaOptionListQueries[2].filteredMediaOptionList];
|
||
}
|
||
getDisabledMediaOption(mediaOptionType) {
|
||
return { itemId: this.itemId, mediaOptionType, mediaOptionId: 'Nah' };
|
||
}
|
||
getEnabledMediaOptionMask() {
|
||
return this.enabledMediaOptionKeys.map((key) => isEnabledMediaOption(key));
|
||
}
|
||
/**
|
||
* Get filtered list with preferred host filtering by type
|
||
* @param mediaOptionType The type to get info about
|
||
*/
|
||
getPreferredMediaOptionsByType$(mediaOptionType) {
|
||
return this.mediaOptionListQueries[mediaOptionType].preferredMediaOptionList$;
|
||
}
|
||
altMediaOptionHasValidUrl(mediaOptionType, mediaOptionId) {
|
||
const altOption = this.alternateMediaOptionById(mediaOptionType, mediaOptionId);
|
||
return Boolean(altOption === null || altOption === void 0 ? void 0 : altOption.url);
|
||
}
|
||
/**
|
||
* Return whether the preferred variant set is HDR
|
||
*/
|
||
get hdrMode$() {
|
||
return this.mediaOptionListQueries[MediaOptionType.Variant].hdrMode$;
|
||
}
|
||
get maxHdcpLevel$() {
|
||
return this.mediaOptionListQueries[MediaOptionType.Variant].maxHdcpLevel$;
|
||
}
|
||
get currentPathwayID() {
|
||
return this.mediaOptionListQueries[MediaOptionType.Variant].currentPathwayID;
|
||
}
|
||
get preferredHost() {
|
||
return this.mediaOptionListQueries[MediaOptionType.Variant].preferredHost;
|
||
}
|
||
/**
|
||
* Get error info by type
|
||
* @param mediaOptionType The type to get info about
|
||
*/
|
||
getErrorInfoByType(mediaOptionType) {
|
||
var _a;
|
||
if (((_a = this.rootPlaylistEntity) === null || _a === void 0 ? void 0 : _a.errorsByType) != null) {
|
||
return this.rootPlaylistEntity.errorsByType[mediaOptionType];
|
||
}
|
||
return null;
|
||
}
|
||
getInFlightFragByType(mediaOptionType) {
|
||
var _a, _b, _c;
|
||
return (_c = (_b = (_a = this.getEntity(this.itemId)) === null || _a === void 0 ? void 0 : _a.inFlightFrags) === null || _b === void 0 ? void 0 : _b[mediaOptionType]) !== null && _c !== void 0 ? _c : null;
|
||
}
|
||
getInFlightFragByType$(mediaOptionType) {
|
||
return this.selectEntity(this.itemId, (entity) => { var _a; return (_a = entity === null || entity === void 0 ? void 0 : entity.inFlightFrags) === null || _a === void 0 ? void 0 : _a[mediaOptionType]; });
|
||
}
|
||
matchAlternates(currentVariant, audioPersistentId, subtitlePersistentId, excludingOptions) {
|
||
const audioAltMediaOption = isFiniteNumber(audioPersistentId)
|
||
? this.mediaOptionListQueries[MediaOptionType.AltAudio].getMatchingAlternateWithPersistentId(audioPersistentId, currentVariant, excludingOptions)
|
||
: undefined;
|
||
const subtitleAltMediaOption = isFiniteNumber(subtitlePersistentId)
|
||
? this.mediaOptionListQueries[MediaOptionType.Subtitle].getMatchingAlternateWithPersistentId(subtitlePersistentId, currentVariant, excludingOptions)
|
||
: undefined;
|
||
return [audioAltMediaOption ? audioAltMediaOption : NoMediaOption, subtitleAltMediaOption ? subtitleAltMediaOption : NoMediaOption];
|
||
}
|
||
getLegacyMatchingAlternateWithPersistentId(mediaOptionType, persistentId, currentVariant) {
|
||
let altMediaOption = this.mediaOptionListQueries[mediaOptionType].getMatchingAlternateWithPersistentId(persistentId, currentVariant, []);
|
||
if (!altMediaOption) {
|
||
altMediaOption = this.mediaOptionListQueries[mediaOptionType].getMatchingAlternateWithPersistentId(persistentId, undefined, []); // just match persistent id
|
||
}
|
||
return altMediaOption;
|
||
}
|
||
isValidMediaOptionTuple(tuple, mediaOptionMask = undefined) {
|
||
const expectedEnabledMediaOptionMask = mediaOptionMask ? mediaOptionMask : this.getEnabledMediaOptionMask();
|
||
const result = [MediaOptionType.Variant, MediaOptionType.AltAudio, MediaOptionType.Subtitle].reduce((prev, cur) => {
|
||
return prev && expectedEnabledMediaOptionMask[cur] === isEnabledMediaOption(tuple[cur]);
|
||
}, true);
|
||
return result;
|
||
}
|
||
matchGroup(mediaOption, audioGroup, subtitleGroup, closedCaptionGroup) {
|
||
const mediaType = mediaOption.mediaOptionType;
|
||
const altOptionQuery = this.mediaOptionListQueries[mediaType];
|
||
return altOptionQuery.matchGroup(mediaOption, audioGroup, subtitleGroup, closedCaptionGroup);
|
||
}
|
||
get preferHDR() {
|
||
return this.mediaOptionListQueries[MediaOptionType.Variant].mediaOptionListInfo.preferHDR;
|
||
}
|
||
}
|
||
|
||
class RootPlaylistStore extends EntityStore {
|
||
constructor() {
|
||
super({}, { name: 'root-playlist-store', idKey: 'itemId', producerFn: produce_1 });
|
||
}
|
||
akitaPreAddEntity(newEntity) {
|
||
if (newEntity.errorsByType == null) {
|
||
return Object.assign(Object.assign({}, newEntity), { errorsByType: [{ timeouts: { load: 0, append: 0, key: 0 } }, { timeouts: { load: 0, append: 0, key: 0 } }, { timeouts: { load: 0, append: 0, key: 0 } }] });
|
||
}
|
||
return newEntity;
|
||
}
|
||
}
|
||
|
||
const loggerName = { name: 'rps' };
|
||
/**
|
||
* @brief Service for managing root playlist state. Will fetch the root playlist and manage selected media options
|
||
*/
|
||
const PENALTYBOX_TIMEOUT_MS = 120000; // 2 minutes
|
||
class RootPlaylistService {
|
||
constructor(store, logger) {
|
||
this.store = store;
|
||
this.logger = logger;
|
||
}
|
||
getQuery() {
|
||
return new QueryEntity(this.store);
|
||
}
|
||
getQueryForId(itemId) {
|
||
return new RootPlaylistQuery(this.store, itemId);
|
||
}
|
||
set rootPlaylistEntity(rootPlaylistEntity) {
|
||
logAction('root.add.rootPlaylist');
|
||
this.store.add(rootPlaylistEntity);
|
||
}
|
||
// Remove entity by item id
|
||
removeItems(itemIds) {
|
||
logAction(`root.add.remove ${JSON.stringify(itemIds)}`);
|
||
this.store.remove(itemIds);
|
||
}
|
||
// Remove all entities
|
||
removeAll() {
|
||
logAction('root.add.clear');
|
||
this.store.remove();
|
||
}
|
||
setRootPlaylistEntity(itemId, rootPlaylistEntity) {
|
||
logAction('root.set.rootPlaylistEntity');
|
||
this.store.update(itemId, (entity) => {
|
||
// Must return this so that entire entity is updated
|
||
return (rootPlaylistEntity);
|
||
});
|
||
}
|
||
setSessionData(itemId, sessionData) {
|
||
logAction('root.set.sessionData');
|
||
this.store.update(itemId, (rootPlaylistEntity) => {
|
||
rootPlaylistEntity.sessionData = sessionData;
|
||
});
|
||
}
|
||
/**
|
||
* Update the position and discontinuity sequence number used for choosing fragments
|
||
* @param anchorTime The anchor position
|
||
* @param discoSeqNum The discontinuity sequence number
|
||
*/
|
||
setAnchorTime(itemId, anchorTime) {
|
||
logAction(`root.set.anchorTime: ${anchorTime === null || anchorTime === void 0 ? void 0 : anchorTime.pos} ${anchorTime === null || anchorTime === void 0 ? void 0 : anchorTime.discoSeqNum}`);
|
||
this.store.update(itemId, (rootPlaylistEntity) => {
|
||
rootPlaylistEntity.anchorTime = anchorTime;
|
||
});
|
||
}
|
||
setDiscoSeqNum(itemId, cc) {
|
||
logAction(`root.set.discoSeqNum: ${cc}`);
|
||
this.store.update(itemId, (rootPlaylistEntity) => {
|
||
rootPlaylistEntity.discoSeqNum = cc;
|
||
});
|
||
}
|
||
setPendingSeek(itemId, pendingSeek) {
|
||
logAction('root.set.pendingSeek');
|
||
this.store.update(itemId, (rootPlaylistEntity) => {
|
||
rootPlaylistEntity.pendingSeek = pendingSeek;
|
||
});
|
||
if (pendingSeek === undefined) {
|
||
globalHlsService().setUserSeek(pendingSeek);
|
||
}
|
||
}
|
||
setEnabledMediaOptionSwitchContextByType(itemId, mediaOptionType, mediaOptionId, context) {
|
||
this.logger.info(`root.set.mediaOptionSwitchContextByType: ${mediaOptionType} ${mediaOptionId} ${context === null || context === void 0 ? void 0 : context.userInitiated}`);
|
||
this.store.update(itemId, (entity) => {
|
||
var _a;
|
||
if (entity.enabledMediaOptionKeys[mediaOptionType].mediaOptionId === mediaOptionId) {
|
||
const mediaOptionContextTuple = (_a = entity.mediaOptionSwitchContexts) !== null && _a !== void 0 ? _a : [null, null, null];
|
||
mediaOptionContextTuple[mediaOptionType] = context ? { userInitiated: context.userInitiated, switchPosition: context.switchPosition } : null;
|
||
entity.mediaOptionSwitchContexts = mediaOptionContextTuple;
|
||
}
|
||
else {
|
||
// Don't update switch context for a different mediaOption
|
||
logAction(`root.set.mediaOptionSwitchContextByType ${mediaOptionId} doesn't match existing mediaOption ${entity.enabledMediaOptionKeys[mediaOptionType].mediaOptionId}`);
|
||
}
|
||
});
|
||
}
|
||
setEnabledVariantMediaOptionIdBeforeTrickplaySwitch(itemId, id) {
|
||
this.logger.info(`root.set.enabledVariantMediaOptionIdBeforeTrickplaySwitch: ${id}`);
|
||
this.store.update(itemId, (entity) => {
|
||
entity.enabledVariantMediaOptionIdBeforeTrickplaySwitch = id;
|
||
});
|
||
}
|
||
setEnabledMediaOptionByType(itemId, mediaOptionType, mediaOption, replaceContext = false, context = undefined) {
|
||
if (!mediaOption) {
|
||
mediaOption = { itemId, mediaOptionType, mediaOptionId: 'Nah' };
|
||
}
|
||
this.logger.info(`root.set.mediaOptionByType: ${mediaOptionType} ${mediaOption.mediaOptionId} replaceCtxt ${replaceContext} ctxt ${JSON.stringify(context === null || context === void 0 ? void 0 : context.userInitiated)}`);
|
||
this.store.update(itemId, (entity) => {
|
||
var _a, _b;
|
||
const mediaOptionKeyTuple = (_a = [...entity.enabledMediaOptionKeys]) !== null && _a !== void 0 ? _a : [NoMediaOption, NoMediaOption, NoMediaOption];
|
||
mediaOptionKeyTuple[mediaOptionType] = { itemId, mediaOptionId: mediaOption.mediaOptionId };
|
||
this._updateEnabledMediaOptionKeys(entity, mediaOptionKeyTuple);
|
||
if (replaceContext) {
|
||
// consider this sequence of events:
|
||
// (1) user switches audio mediaOption. Such an alt audio switch will include a switchContext.
|
||
// (2) While audio mediaOption is being handled, user switches subtitle mediaOption, hls.js *might* also switch the audio mediaOption to match the latest group-ids.
|
||
// The "silent" audio track switch in (2) should not override the userInitiated switchContext in (1)
|
||
// replaceContext should be true in (1) and false in (2)
|
||
const mediaOptionContextTuple = (_b = entity.mediaOptionSwitchContexts) !== null && _b !== void 0 ? _b : [null, null, null];
|
||
mediaOptionContextTuple[mediaOptionType] = context ? { userInitiated: context.userInitiated, switchPosition: context.switchPosition } : null;
|
||
entity.mediaOptionSwitchContexts = mediaOptionContextTuple;
|
||
}
|
||
});
|
||
}
|
||
_associateForcedSubtitleWithClosedCaption(entity, variantMediaOptionId, closedCaption, rootQuery) {
|
||
if ((closedCaption === null || closedCaption === void 0 ? void 0 : closedCaption.mediaType) === MediaTypeFourCC.CLOSEDCAPTION) {
|
||
const variantOption = rootQuery.variantMediaOptionById(variantMediaOptionId);
|
||
const newClosedCaption = rootQuery.mediaOptionListQueries[MediaOptionType.Subtitle].packageAlternateMediaOption(variantOption, closedCaption, true);
|
||
getLogger().info(`[subtitle] new closedCaption ${redactUrl(newClosedCaption.url)} vs old ${redactUrl(closedCaption.url)}`);
|
||
if (newClosedCaption.url !== closedCaption.url) {
|
||
const newMediaOptions = replaceClosedCaptionInMediaOptionListIfNecessary(variantOption, newClosedCaption, entity.mediaOptionListTuple[MediaOptionType.Subtitle].mediaOptions, getLogger());
|
||
entity.mediaOptionListTuple[MediaOptionType.Subtitle].mediaOptions = newMediaOptions;
|
||
}
|
||
}
|
||
}
|
||
// Stuff that must be done always when setting enabledMediaOptionKeys
|
||
_updateEnabledMediaOptionKeys(entity, mediaOptionKeyTuple) {
|
||
var _a;
|
||
const enabledMediaOptionKeys = (_a = entity.enabledMediaOptionKeys) !== null && _a !== void 0 ? _a : [NoMediaOption, NoMediaOption, NoMediaOption];
|
||
let enabledVariantKey;
|
||
for (let type = 0; type < mediaOptionKeyTuple.length; ++type) {
|
||
const newOption = mediaOptionKeyTuple[type];
|
||
const didChange = enabledMediaOptionKeys[type].mediaOptionId !== newOption.mediaOptionId;
|
||
if (didChange) {
|
||
enabledMediaOptionKeys[type] = Object.assign({}, newOption);
|
||
}
|
||
if (type === MediaOptionType.Variant) {
|
||
// Using all mediaOptions so that mediaOptions in PenaltyBox are also considered when setting highBWTrigger
|
||
const unfilteredList = this.getQueryForId(newOption.itemId).mediaOptionListQueries[type].mediaOptionList;
|
||
if (didChange) {
|
||
entity.abrStatus = initializeAbrStatus(newOption.mediaOptionId, unfilteredList);
|
||
}
|
||
else {
|
||
// Just update highBW trigger
|
||
entity.abrStatus.highBWTrigger = getLowestSuperiorBW(newOption.mediaOptionId, unfilteredList);
|
||
}
|
||
enabledVariantKey = newOption;
|
||
}
|
||
else if (type === MediaOptionType.Subtitle && isEnabledMediaOption(newOption)) {
|
||
const rootQuery = this.getQueryForId(newOption.itemId);
|
||
const subtitleOption = rootQuery.alternateMediaOptionById(type, newOption.mediaOptionId);
|
||
this._associateForcedSubtitleWithClosedCaption(entity, enabledVariantKey.mediaOptionId, subtitleOption, rootQuery);
|
||
}
|
||
}
|
||
entity.enabledMediaOptionKeys = enabledMediaOptionKeys;
|
||
entity.nextMediaOptionKeys = undefined;
|
||
}
|
||
// Disables ABR. for development
|
||
setManualMode(itemId, manualMode) {
|
||
this.store.update(itemId, (entity) => {
|
||
entity.manualMode = manualMode;
|
||
});
|
||
}
|
||
setEnabledMediaOptions(itemId, mediaOptions) {
|
||
this.logger.info(`root.set.enabledMediaOptions: ${JSON.stringify(mediaOptions.map((x) => x.mediaOptionId))}`);
|
||
this.store.update(itemId, (entity) => {
|
||
const mediaOptionKeyTuple = mediaOptions.map(({ mediaOptionId, itemId }) => {
|
||
const result = { mediaOptionId, itemId };
|
||
return result;
|
||
});
|
||
this._updateEnabledMediaOptionKeys(entity, mediaOptionKeyTuple);
|
||
});
|
||
}
|
||
setEnabledMediaOptionsAndSwitchContexts(itemId, mediaOptions, switchContexts) {
|
||
this.logger.info(`root.set.enabledMediaOptionsAndSwitchContexts: ${JSON.stringify(mediaOptions.map((x) => x.mediaOptionId))} ctxt ${JSON.stringify(switchContexts)}`);
|
||
this.store.update(itemId, (entity) => {
|
||
const mediaOptionKeyTuple = mediaOptions.map(({ mediaOptionId, itemId }) => {
|
||
const result = { mediaOptionId, itemId };
|
||
return result;
|
||
});
|
||
this._updateEnabledMediaOptionKeys(entity, mediaOptionKeyTuple);
|
||
entity.mediaOptionSwitchContexts = switchContexts;
|
||
});
|
||
}
|
||
setNextMediaOptions(itemId, mediaOptions) {
|
||
logAction(`root.set.nextMediaOptions: ${JSON.stringify(mediaOptions === null || mediaOptions === void 0 ? void 0 : mediaOptions.map((x) => x.mediaOptionId))}`);
|
||
this.store.update(itemId, (entity) => {
|
||
const keys = mediaOptions
|
||
? mediaOptions.map(({ itemId, mediaOptionId }) => {
|
||
const result = { itemId, mediaOptionId };
|
||
return result;
|
||
})
|
||
: null;
|
||
entity.nextMediaOptionKeys = keys;
|
||
});
|
||
}
|
||
// updateEnabledMediaOptions should not touch the switch context
|
||
updateEnabledMediaOptions(itemId) {
|
||
logAction('root.set.updateEnabledMediaOptions');
|
||
this.store.update(itemId, (entity) => {
|
||
if (entity.nextMediaOptionKeys && entity.manualMode !== true) {
|
||
logAction(`root.set.updateEnabledMediaOptions ${JSON.stringify(entity.nextMediaOptionKeys)}`);
|
||
this._updateEnabledMediaOptionKeys(entity, [...entity.nextMediaOptionKeys]);
|
||
}
|
||
entity.nextMediaOptionKeys = undefined;
|
||
});
|
||
}
|
||
setLastLoadedMediaOptionByType(itemId, mediaOptionType, mediaOption) {
|
||
if (!mediaOption) {
|
||
mediaOption = { itemId, mediaOptionType, mediaOptionId: 'Nah' };
|
||
}
|
||
logAction(`root.set.lastLoadedMediaOptionByType: ${mediaOptionType} ${mediaOption.mediaOptionId}`);
|
||
this.store.update(itemId, (entity) => {
|
||
var _a;
|
||
const mediaOptionKeyTuple = (_a = entity.lastLoadedMediaOptionKeys) !== null && _a !== void 0 ? _a : [NoMediaOption, NoMediaOption, NoMediaOption];
|
||
mediaOptionKeyTuple[mediaOptionType] = { itemId, mediaOptionId: mediaOption.mediaOptionId };
|
||
entity.lastLoadedMediaOptionKeys = mediaOptionKeyTuple;
|
||
});
|
||
}
|
||
setPreferredHost(itemId, newHost) {
|
||
logAction(`root.set.preferredHost: ${newHost}`);
|
||
this.store.update(itemId, (entity) => {
|
||
if (!entity) {
|
||
return;
|
||
}
|
||
const optionListInfo = entity.mediaOptionListTuple[MediaOptionType.Variant];
|
||
optionListInfo.preferredHost = newHost;
|
||
});
|
||
}
|
||
setViewportInfo(itemId, viewportInfo) {
|
||
logAction(`root.set.viewportInfo: ${JSON.stringify(viewportInfo)}`);
|
||
this.store.update(itemId, (entity) => {
|
||
if (!entity) {
|
||
return;
|
||
}
|
||
const optionListInfo = entity.mediaOptionListTuple[MediaOptionType.Variant];
|
||
optionListInfo.viewportInfo = viewportInfo;
|
||
});
|
||
}
|
||
static getExistingPersistentIds(entity) {
|
||
var _a, _b;
|
||
const existingSelection = {};
|
||
const audioOptionId = (_a = entity.enabledMediaOptionKeys[MediaOptionType.AltAudio]) === null || _a === void 0 ? void 0 : _a.mediaOptionId;
|
||
if (audioOptionId !== 'Nah') {
|
||
const audioOptionListInfo = entity.mediaOptionListTuple[MediaOptionType.AltAudio];
|
||
const filteredAudio = applyFilters(audioOptionListInfo.mediaOptions, AlternateMediaOptionListQuery.kAllowFilters, audioOptionListInfo);
|
||
const altAudioOption = filteredAudio.find((altOption) => altOption.mediaOptionId === audioOptionId);
|
||
existingSelection.audioPersistentId = altAudioOption === null || altAudioOption === void 0 ? void 0 : altAudioOption.persistentID;
|
||
}
|
||
const subtitleOptionId = (_b = entity.enabledMediaOptionKeys[MediaOptionType.Subtitle]) === null || _b === void 0 ? void 0 : _b.mediaOptionId;
|
||
if (subtitleOptionId !== 'Nah') {
|
||
const subtitleOptionListInfo = entity.mediaOptionListTuple[MediaOptionType.Subtitle];
|
||
const filteredSubtitles = applyFilters(subtitleOptionListInfo.mediaOptions, AlternateMediaOptionListQuery.kAllowFilters, subtitleOptionListInfo);
|
||
const subtitleOption = filteredSubtitles.find((altOption) => altOption.mediaOptionId === subtitleOptionId);
|
||
existingSelection.subtitlePersistentId = subtitleOption === null || subtitleOption === void 0 ? void 0 : subtitleOption.persistentID;
|
||
}
|
||
return existingSelection;
|
||
}
|
||
static doUpdateRootHDRSwitch(entity, preferHDR, hasHdrLevels, logger) {
|
||
// immer complains if you try to modify and update the object in one go so make deep copy of mediaOptionListTuple
|
||
const mediaOptionListTuple = entity.mediaOptionListTuple.map((info) => (Object.assign({}, info)));
|
||
mediaOptionListTuple[MediaOptionType.Variant].preferHDR = preferHDR;
|
||
mediaOptionListTuple[MediaOptionType.Variant].hasHdrLevels = hasHdrLevels;
|
||
const hlsConf = getCurrentConfig();
|
||
const curItem = queueItemQuery.getEntity(entity.itemId);
|
||
const statsQuery = createStatsQuery(entity.itemId);
|
||
const bandwidthEstimate = statsQuery.getBandwidthEstimate(hlsConf, curItem === null || curItem === void 0 ? void 0 : curItem.serviceName);
|
||
const playlistEstimate = statsQuery.getPlaylistEstimate(hlsConf, curItem === null || curItem === void 0 ? void 0 : curItem.serviceName);
|
||
const fragEstimate = statsQuery.getFragEstimate(hlsConf, curItem === null || curItem === void 0 ? void 0 : curItem.serviceName);
|
||
const bufferEstimate = statsQuery.getBufferEstimate(hlsConf, curItem === null || curItem === void 0 ? void 0 : curItem.serviceName);
|
||
const adaptiveStartupConfig = {
|
||
targetDuration: fragEstimate.maxDurationSec || (hlsConf === null || hlsConf === void 0 ? void 0 : hlsConf.defaultTargetDuration),
|
||
targetStartupMs: hlsConf === null || hlsConf === void 0 ? void 0 : hlsConf.targetStartupMs,
|
||
};
|
||
const existingSelection = RootPlaylistService.getExistingPersistentIds(entity);
|
||
return updateRootPlaylistEntityWithEnabledMediaOptionKeys(Object.assign(Object.assign({}, entity), { mediaOptionListTuple, nextMediaOptionKeys: null }), existingSelection, logger, bandwidthEstimate, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate);
|
||
}
|
||
// For error handling. disable HDR playback but don't re-calculate enabled media options
|
||
switchToSDROnly(itemId) {
|
||
logAction('root.switchToSDROnly');
|
||
this.store.update(itemId, (entity) => {
|
||
const { mediaOptionListTuple } = RootPlaylistService.doUpdateRootHDRSwitch(entity, false, false, this.logger);
|
||
entity.mediaOptionListTuple = mediaOptionListTuple;
|
||
});
|
||
}
|
||
/**
|
||
* Update HDR preference and set enabled media options based on the preference
|
||
*/
|
||
setHDRPreference(itemId, preferHDR, updateEnabledOptions) {
|
||
logAction(`root.set.HDRPreference: ${preferHDR}`);
|
||
this.store.update(itemId, (entity) => {
|
||
const optionListInfo = entity.mediaOptionListTuple[MediaOptionType.Variant];
|
||
if (optionListInfo.preferHDR === preferHDR || (preferHDR && !optionListInfo.hasHdrLevels)) {
|
||
return;
|
||
}
|
||
const newEntity = RootPlaylistService.doUpdateRootHDRSwitch(entity, preferHDR, optionListInfo.hasHdrLevels, this.logger);
|
||
if (!updateEnabledOptions) {
|
||
entity.mediaOptionListTuple = newEntity.mediaOptionListTuple;
|
||
}
|
||
else {
|
||
// Must return this so that entire entity is updated
|
||
return (entity = newEntity);
|
||
}
|
||
});
|
||
}
|
||
setPathwayPriority(itemId, pathwayPriority) {
|
||
logAction(`root.set.PathwayPriority: [ ${pathwayPriority.join(', ')} ]`);
|
||
this.store.update(itemId, (entity) => {
|
||
if (!entity) {
|
||
return;
|
||
}
|
||
const optionListInfo = entity.mediaOptionListTuple[MediaOptionType.Variant];
|
||
optionListInfo.pathwayPriority = pathwayPriority;
|
||
optionListInfo.preferredHost = null;
|
||
});
|
||
}
|
||
setCurrentPathwayID(itemId, currentPathwayID) {
|
||
logAction(`root.set.currentPathwayID: ${currentPathwayID}`);
|
||
this.store.update(itemId, (entity) => {
|
||
if (!entity) {
|
||
return;
|
||
}
|
||
const optionListInfo = entity.mediaOptionListTuple[MediaOptionType.Variant];
|
||
optionListInfo.currentPathwayID = currentPathwayID;
|
||
});
|
||
}
|
||
setInitPTS(itemId, discoSeqNum, variantDTS, timelineOffset, offsetTimestamp, iframeMode) {
|
||
logAction(`root.set.initPTS: ${itemId} ${discoSeqNum} variantDTS:${JSON.stringify(variantDTS)} timelineOffset: ${timelineOffset}`);
|
||
this.store.update(itemId, (entity) => {
|
||
entity.initPtsRecord[discoSeqNum] = { variantDTS, timelineOffset, offsetTimestamp, iframeMode };
|
||
});
|
||
}
|
||
/**
|
||
* Modify penaltyBoxQueue to remove all expired entries in place
|
||
* @param penaltyBoxQueue
|
||
* @param now
|
||
*/
|
||
static prunePenaltyBox(penaltyBoxQueue, now) {
|
||
return penaltyBoxQueue.filter((x) => !isExpired(x.expiry, now));
|
||
}
|
||
/**
|
||
* Add entry to penaltyBoxQueue in place
|
||
* @param penaltyBoxQueue
|
||
* @param now
|
||
* @param mediaOptionId
|
||
*/
|
||
static addToPenaltyBox(penaltyBoxQueue, now, mediaOptionId) {
|
||
return penaltyBoxQueue.push({ mediaOptionId, expiry: now + PENALTYBOX_TIMEOUT_MS });
|
||
}
|
||
/**
|
||
* Put a media option into the penalty box. It will stay there for 2 minutes
|
||
* @param filterListId The filter list to modify
|
||
* @param mediaOptionType The media option type
|
||
* @param mediaOptionId The media option to put into penalty box
|
||
*/
|
||
addToPenaltyBox(itemId, mediaOptionType, mediaOptionId) {
|
||
logAction(`root.set.penaltyBox: ${mediaOptionType}: ${mediaOptionId}`);
|
||
this.store.update(itemId, ({ mediaOptionListTuple: mediaOptionsStates }) => {
|
||
const optionListInfo = mediaOptionsStates[mediaOptionType];
|
||
// TODO: dedup
|
||
const now = performance.now();
|
||
optionListInfo.penaltyBoxQueue = RootPlaylistService.prunePenaltyBox(optionListInfo.penaltyBoxQueue, now);
|
||
RootPlaylistService.addToPenaltyBox(optionListInfo.penaltyBoxQueue, now, mediaOptionId);
|
||
});
|
||
}
|
||
/**
|
||
* Remove all expired entries from penalty box
|
||
* @param itemId The queue item id
|
||
* @param mediaOptionType which media option type to prune penalty box for. if null, prune all
|
||
*/
|
||
prunePenaltyBox(itemId, mediaOptionType = null) {
|
||
logAction(`root.set.prunePenaltyBox: ${mediaOptionType}`);
|
||
this.store.update(itemId, ({ mediaOptionListTuple }) => {
|
||
const infoList = mediaOptionType ? [mediaOptionListTuple[mediaOptionType]] : mediaOptionListTuple;
|
||
const now = performance.now();
|
||
for (const info of infoList) {
|
||
info.penaltyBoxQueue = RootPlaylistService.prunePenaltyBox(info.penaltyBoxQueue, now);
|
||
}
|
||
});
|
||
}
|
||
/**
|
||
* Put a media option in the remove list. It will stay there permanently.
|
||
* @param itemId The filter list to modify
|
||
* @param mediaOptionType The media option type
|
||
* @param mediaOptionId The media option to put in remove list
|
||
*/
|
||
removePermanently(itemId, mediaOptionType, mediaOptionId) {
|
||
logAction(`root.set.removePermanently: ${mediaOptionType}: ${mediaOptionId}`);
|
||
this.store.update(itemId, ({ mediaOptionListTuple: mediaOptionsStates }) => {
|
||
const optionListInfo = mediaOptionsStates[mediaOptionType];
|
||
// dedup. uncommon to remove so this should hopefully be small
|
||
const removeSet = new Set(optionListInfo.removed);
|
||
removeSet.add(mediaOptionId);
|
||
optionListInfo.removed = Array.from(removeSet);
|
||
});
|
||
}
|
||
/**
|
||
*
|
||
* @param itemId The filter list to modify
|
||
* @param mediaOptionType Which media option list to update
|
||
* @param hostName The hostname used for matching
|
||
* @param remove if true, remove this host permanently from valid list. if false, put into penalty box
|
||
*/
|
||
moveAllWithMatchingHosts(itemId, mediaOptionType, hostName, remove) {
|
||
logAction(`root.set.moveAllMatchingHosts: ${mediaOptionType}:${hostName} remove:${remove}`);
|
||
this.store.update(itemId, ({ mediaOptionListTuple: mediaOptionsStates }) => {
|
||
const optionListInfo = mediaOptionsStates[mediaOptionType];
|
||
const mediaOptionIdsToModify = [...optionListInfo.mediaOptions].filter((mo) => hasMatchingHost(hostName, mo.url)).map((x) => x.mediaOptionId);
|
||
if (remove) {
|
||
const merged = new Set([...optionListInfo.removed, ...mediaOptionIdsToModify]);
|
||
optionListInfo.removed = Array.from(merged);
|
||
}
|
||
else {
|
||
const now = performance.now();
|
||
optionListInfo.penaltyBoxQueue = RootPlaylistService.prunePenaltyBox(optionListInfo.penaltyBoxQueue, now);
|
||
for (const mediaOptionId of mediaOptionIdsToModify) {
|
||
RootPlaylistService.addToPenaltyBox(optionListInfo.penaltyBoxQueue, now, mediaOptionId);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
/**
|
||
* @param itemId the item id to modify
|
||
* @param maxHdcpLevel the maximum hdcp level allowed for this asset (exclusive). Only allow variants with hdcpLevel < maxHdcpLevel
|
||
* @param force if true, always update. if false, only update if maxHdcpLevel is < current maxHdcpLevel
|
||
*/
|
||
setMaxHdcpLevel(itemId, maxHdcpLevel, force = false) {
|
||
logAction(`root.set.maxHdcpLevel: ${maxHdcpLevel}`);
|
||
this.store.update(itemId, ({ mediaOptionListTuple: mediaOptionsStates }) => {
|
||
const optionListInfo = mediaOptionsStates[MediaOptionType.Variant];
|
||
// Only can lower max hdcp level unless forced
|
||
if (force || hdcpLevelToInt(maxHdcpLevel) < hdcpLevelToInt(optionListInfo.maxHdcpLevel)) {
|
||
optionListInfo.maxHdcpLevel = maxHdcpLevel;
|
||
}
|
||
});
|
||
}
|
||
/**
|
||
* @param itemId The queue item id
|
||
* @param mediaOptionType the mediaOptionType
|
||
* @param increment if true, increment else reset
|
||
* @param timeout Which type of timeout it was
|
||
*/
|
||
updateConsecutiveTimeouts(itemId, mediaOptionType, increment, type) {
|
||
this.store.update(itemId, (entity) => {
|
||
const errorHandlingInfo = entity.errorsByType || [
|
||
{ timeouts: { load: 0, append: 0, key: 0 } },
|
||
{ timeouts: { load: 0, append: 0, key: 0 } },
|
||
{ timeouts: { load: 0, append: 0, key: 0 } },
|
||
];
|
||
if (increment) {
|
||
++errorHandlingInfo[mediaOptionType].timeouts[type];
|
||
}
|
||
else {
|
||
errorHandlingInfo[mediaOptionType].timeouts[type] = 0;
|
||
}
|
||
entity.errorsByType = errorHandlingInfo;
|
||
});
|
||
}
|
||
// Update inflight fragment information. For now just hooking up Variant & AltAudio
|
||
updateInflightFrag(itemId, type, frag, state, sample) {
|
||
logAction('root.set.updateInflightFrag');
|
||
this.store.update(itemId, (entity) => {
|
||
if (!entity.inFlightFrags) {
|
||
entity.inFlightFrags = [null, null];
|
||
}
|
||
if (type === MediaOptionType.Subtitle || (frag && frag.itemId !== itemId)) {
|
||
return;
|
||
}
|
||
if (!frag) {
|
||
entity.inFlightFrags[type] = null;
|
||
return;
|
||
}
|
||
let { start, duration } = frag;
|
||
const { mediaOptionId, mediaSeqNum, discoSeqNum } = frag;
|
||
const inFlight = entity.inFlightFrags[type];
|
||
// Update tstart if state changed
|
||
let tstart = inFlight === null || inFlight === void 0 ? void 0 : inFlight.tstart;
|
||
if (state !== (inFlight === null || inFlight === void 0 ? void 0 : inFlight.state)) {
|
||
tstart = performance.now();
|
||
}
|
||
if (fragEqual(inFlight, frag)) {
|
||
start = inFlight.start;
|
||
duration = inFlight.duration;
|
||
}
|
||
entity.inFlightFrags[type] = {
|
||
itemId,
|
||
mediaOptionId,
|
||
mediaSeqNum,
|
||
discoSeqNum,
|
||
start,
|
||
duration,
|
||
tstart,
|
||
state,
|
||
bwSample: Object.assign({}, sample),
|
||
};
|
||
});
|
||
}
|
||
setNextMaxAutoOptionId(itemId, nextMaxAutoOptionId) {
|
||
logAction(`root.set.nextMaxAutoOptionId: ${nextMaxAutoOptionId}`);
|
||
this.store.update(itemId, ({ abrStatus: abrs }) => {
|
||
abrs.nextMaxAutoOptionId = nextMaxAutoOptionId;
|
||
});
|
||
}
|
||
setNextMinAutoOptionId(itemId, nextMinAutoOptionId) {
|
||
logAction(`root.set.nextMinAutoOptionId: ${nextMinAutoOptionId}`);
|
||
this.store.update(itemId, ({ abrStatus: abrs }) => {
|
||
abrs.nextMinAutoOptionId = nextMinAutoOptionId;
|
||
});
|
||
}
|
||
setHighBWTrigger(itemId, value) {
|
||
logAction(`root.set.setHighBWTrigger: ${value}`);
|
||
this.store.update(itemId, ({ abrStatus: abrs }) => {
|
||
abrs.highBWTrigger = value;
|
||
});
|
||
}
|
||
setFragLoadSlow(itemId, value) {
|
||
logAction(`root.set.setFragLoadSlow ${itemId} ${JSON.stringify(value)}`);
|
||
this.store.update(itemId, ({ abrStatus: abrs }) => {
|
||
abrs.fragDownloadSlow = value.fragDownloadSlow;
|
||
abrs.fragDownloadTooSlow = value.fragDownloadTooSlow;
|
||
});
|
||
}
|
||
pickMediaOptionTupleByPersistentId(rootQuery, mediaOptionType, persistentId, sdrOnly = false, shouldSwitchHost = false) {
|
||
const currentVariantId = rootQuery.enabledMediaOptionIdByType(MediaOptionType.Variant);
|
||
const variant = rootQuery.variantMediaOptionById(currentVariantId);
|
||
let audioPersistentId;
|
||
let subtitlePersistentId;
|
||
if (mediaOptionType === MediaOptionType.AltAudio) {
|
||
const subtitleAltOption = rootQuery.enabledAlternateMediaOptionByType(MediaOptionType.Subtitle);
|
||
subtitlePersistentId = subtitleAltOption === null || subtitleAltOption === void 0 ? void 0 : subtitleAltOption.persistentID;
|
||
audioPersistentId = persistentId;
|
||
}
|
||
else {
|
||
const audioAltOption = rootQuery.enabledAlternateMediaOptionByType(MediaOptionType.AltAudio);
|
||
audioPersistentId = audioAltOption === null || audioAltOption === void 0 ? void 0 : audioAltOption.persistentID;
|
||
subtitlePersistentId = persistentId;
|
||
}
|
||
const expectedEnabledMediaOptionMask = rootQuery.getEnabledMediaOptionMask();
|
||
expectedEnabledMediaOptionMask[mediaOptionType] = isFiniteNumber(persistentId) && persistentId >= 0 ? true : false;
|
||
return variant
|
||
? this.getBestMediaOptionTupleFromVariantAndPersistentId(rootQuery, variant, audioPersistentId, subtitlePersistentId, expectedEnabledMediaOptionMask, undefined, sdrOnly, shouldSwitchHost, false)
|
||
: [NoMediaOption, NoMediaOption, NoMediaOption];
|
||
}
|
||
getFallbackMediaOptionTupleFromMediaOptionId(rootQuery, mediaOptionType, mediaOptionId, backingMediaOptionId, sdrOnly = false, shouldSwitchHosts = false, shouldDownswitch = false) {
|
||
const excludingOptions = backingMediaOptionId ? [backingMediaOptionId] : [mediaOptionId]; // always exclude mediaOptionId or backingMediaOptionId from result list since we are looking for its fallback
|
||
const currentVariantId = rootQuery.enabledMediaOptionIdByType(MediaOptionType.Variant); // getBestMediaOptionTupleFromVariantAndPersistentId will skip currentVariantId if it's mediaOptionId (excluded above)
|
||
const variant = rootQuery.variantMediaOptionById(currentVariantId);
|
||
const altAudio = mediaOptionType === MediaOptionType.AltAudio
|
||
? rootQuery.alternateMediaOptionById(MediaOptionType.AltAudio, mediaOptionId)
|
||
: rootQuery.enabledAlternateMediaOptionByType(MediaOptionType.AltAudio);
|
||
const audioPersistentId = altAudio === null || altAudio === void 0 ? void 0 : altAudio.persistentID;
|
||
const subtitle = mediaOptionType === MediaOptionType.Subtitle
|
||
? rootQuery.alternateMediaOptionById(MediaOptionType.Subtitle, mediaOptionId)
|
||
: rootQuery.enabledAlternateMediaOptionByType(MediaOptionType.Subtitle);
|
||
const subtitlePersistentId = subtitle === null || subtitle === void 0 ? void 0 : subtitle.persistentID;
|
||
return variant
|
||
? this.getBestMediaOptionTupleFromVariantAndPersistentId(rootQuery, variant, audioPersistentId, subtitlePersistentId, undefined, excludingOptions, sdrOnly, shouldSwitchHosts, shouldDownswitch)
|
||
: [NoMediaOption, NoMediaOption, NoMediaOption];
|
||
}
|
||
hasFallbackMediaOptionTuple(rootQuery, mediaOptionType, fromId, shouldSwitchHosts) {
|
||
const fromOption = rootQuery.mediaOptionListQueries[mediaOptionType].mediaOptionFromId(fromId);
|
||
return rootQuery.isValidMediaOptionTuple(this.getFallbackMediaOptionTupleFromMediaOptionId(rootQuery, mediaOptionType, fromId, fromOption.backingMediaOptionId, false, shouldSwitchHosts));
|
||
}
|
||
setLegacyAlternateMediaOption(rootQuery, itemId, mediaOptionType, persistentId, switchContext = undefined) {
|
||
const currentVariantId = rootQuery.enabledMediaOptionIdByType(MediaOptionType.Variant);
|
||
const currentVariant = rootQuery.variantMediaOptionById(currentVariantId);
|
||
const newAltOption = rootQuery.getLegacyMatchingAlternateWithPersistentId(mediaOptionType, persistentId, currentVariant);
|
||
if (newAltOption) {
|
||
this.setEnabledMediaOptionByType(itemId, mediaOptionType, newAltOption, true, switchContext);
|
||
}
|
||
else {
|
||
this.logger.warn(`${MediaOptionNames[mediaOptionType]} can't find matching mediaOption for persistent id ${persistentId}`);
|
||
}
|
||
}
|
||
setEnabledMediaOptionTupleWithMatchedGroups(itemId, mediaOptionType, persistentId, switchContext = undefined) {
|
||
const rootQuery = createRootPlaylistQuery(itemId);
|
||
const newOptions = this.pickMediaOptionTupleByPersistentId(rootQuery, mediaOptionType, persistentId);
|
||
if (!rootQuery.isValidMediaOptionTuple(newOptions)) {
|
||
// can't find matching variant and alternates combo,
|
||
// just switch the alternate media option of mediaOptionType, potentially ignore preferred host and group
|
||
return this.setLegacyAlternateMediaOption(rootQuery, itemId, mediaOptionType, persistentId, switchContext);
|
||
}
|
||
applyTransaction(() => {
|
||
this.setEnabledMediaOptionByType(itemId, mediaOptionType, newOptions[mediaOptionType], true, switchContext);
|
||
if (newOptions[MediaOptionType.Variant].mediaOptionId !== rootQuery.enabledMediaOptionIdByType(MediaOptionType.Variant)) {
|
||
this.setPreferredHost(itemId, getHostName(newOptions[MediaOptionType.Variant].url));
|
||
}
|
||
// always update the variant mediaOption even if it has not changed. This will allow AVPipe's enabledMediaOptionSwitchForType call
|
||
// to reflect the latest fromId->toId mediaOption switching.
|
||
this.setEnabledMediaOptionByType(itemId, MediaOptionType.Variant, newOptions[MediaOptionType.Variant]);
|
||
const otherType = mediaOptionType === MediaOptionType.AltAudio ? MediaOptionType.Subtitle : MediaOptionType.AltAudio;
|
||
if (newOptions[otherType].mediaOptionId !== rootQuery.enabledMediaOptionIdByType(otherType)) {
|
||
this.setEnabledMediaOptionByType(itemId, otherType, newOptions[otherType], false); // don't override switchContext of the other alternate
|
||
}
|
||
});
|
||
}
|
||
canSwitchToSDR(rootQuery, mediaOptionId, shouldSwitchHosts, shouldDownswitch = false) {
|
||
const mediaOption = rootQuery.mediaOptionListQueries[MediaOptionType.Variant].mediaOptionFromId(mediaOptionId);
|
||
const newMediaOptions = this.getFallbackMediaOptionTupleFromMediaOptionId(rootQuery, MediaOptionType.Variant, mediaOptionId, mediaOption.backingMediaOptionId, true, shouldSwitchHosts, shouldDownswitch);
|
||
return rootQuery.isValidMediaOptionTuple(newMediaOptions);
|
||
}
|
||
getBestMediaOptionTupleFromVariantAndPersistentId(rootQuery, currentVariant, audioPersistentId, subtitlePersistentId, expectedEnabledMediaOptionMask, excludingOptions, sdrOnly, shouldSwitchHosts, shouldDownswitch) {
|
||
let alternates;
|
||
const fallbackVariantList = rootQuery.mediaOptionListQueries[MediaOptionType.Variant].listFallbackVariants(currentVariant.mediaOptionId, sdrOnly, shouldSwitchHosts, shouldDownswitch, excludingOptions);
|
||
let preferredFallback = [NoMediaOption, NoMediaOption, NoMediaOption];
|
||
for (let i = 0; i < fallbackVariantList.length; ++i) {
|
||
const nextVariant = fallbackVariantList[i];
|
||
alternates = rootQuery.matchAlternates(nextVariant, audioPersistentId, subtitlePersistentId, excludingOptions);
|
||
if (rootQuery.isValidMediaOptionTuple([nextVariant, ...alternates], expectedEnabledMediaOptionMask)) {
|
||
preferredFallback = [nextVariant, ...alternates];
|
||
break; // found variant with valid alternates
|
||
}
|
||
}
|
||
return preferredFallback;
|
||
}
|
||
}
|
||
const rootPlaylistStore = new RootPlaylistStore();
|
||
new QueryEntity(rootPlaylistStore);
|
||
let rootService = null; // To be instantiated in rootPlaylistService
|
||
/***********************************************
|
||
* Static helper functions that specifically use the above singletons
|
||
*/
|
||
function createRootPlaylistQuery(itemId) {
|
||
return new RootPlaylistQuery(rootPlaylistStore, itemId);
|
||
}
|
||
/**
|
||
* @returns The global instance of RootPlaylistService that operates on global RootPlaylistStore
|
||
*/
|
||
function rootPlaylistService(logger) {
|
||
rootService = new RootPlaylistService(rootPlaylistStore, logger);
|
||
return rootService;
|
||
}
|
||
/**
|
||
* Choose the audio track given the selection group and options
|
||
* @param groupId GroupID from variant
|
||
* @param audioMediaSelectionGroup
|
||
* @param audioAlternateOptions
|
||
*/
|
||
const getAutoAudio = (persistentId, groupId, audioMediaSelectionGroup, audioAlternateOptions) => {
|
||
if (audioMediaSelectionGroup) {
|
||
let audioMediaOption;
|
||
if (isFiniteNumber(persistentId)) {
|
||
audioMediaOption = audioMediaSelectionGroup.MediaSelectionGroupOptions.find(function (mediaOption) {
|
||
return mediaOption.MediaSelectionOptionsPersistentID === persistentId;
|
||
});
|
||
}
|
||
else {
|
||
audioMediaOption = audioMediaSelectionGroup.MediaSelectionGroupOptions.find(function (mediaOption) {
|
||
return mediaOption.MediaSelectionOptionsIsDefault;
|
||
});
|
||
}
|
||
if (!audioMediaOption) {
|
||
audioMediaOption = audioMediaSelectionGroup.MediaSelectionGroupOptions[0];
|
||
getLogger().info(`no default audio: pick the first persistentId ${audioMediaOption.MediaSelectionOptionsPersistentID}`);
|
||
}
|
||
const audioTrackInfo = audioAlternateOptions.find((audioTrack) => {
|
||
return (!groupId || audioTrack.groupId === groupId) && audioTrack.persistentID === (audioMediaOption === null || audioMediaOption === void 0 ? void 0 : audioMediaOption.MediaSelectionOptionsPersistentID);
|
||
});
|
||
return audioTrackInfo;
|
||
}
|
||
};
|
||
function logPlatformCapabilitiesFilterResults(hdrMediaOptions, sdrMediaOptions, el, config, displaySupportsHdr, logger) {
|
||
hdrMediaOptions.concat(sdrMediaOptions);
|
||
const devicePixelRatio = typeof window === 'object' && window.devicePixelRatio ? window.devicePixelRatio : 1;
|
||
const displaySize = config.useViewportSizeForLevelCap && el ? { width: el.clientWidth * devicePixelRatio, height: el.clientHeight * devicePixelRatio } : undefined;
|
||
logger.info(loggerName, `valid media options: hdr=${hdrMediaOptions.length} sdr=${sdrMediaOptions.length}, supportsHdr=${displaySupportsHdr}, displaySize=${displaySize ? JSON.stringify(displaySize) : null}`);
|
||
}
|
||
const updateBasedOnPlatformCapabilities = (loadRootMediaOptionsResult, platformInfo, config, displaySupportsHdr, logger) => {
|
||
const { rootMediaOptionsTuple, sessionKeys } = loadRootMediaOptionsResult;
|
||
const variantMediaOptions = Array.from(rootMediaOptionsTuple[MediaOptionType.Variant]);
|
||
const audioAlternateOptions = Array.from(rootMediaOptionsTuple[MediaOptionType.AltAudio]);
|
||
let videoCodecFound = false;
|
||
let audioCodecFound = false;
|
||
// regroup redundant variants together
|
||
let regroupedVariantMediaOptions = variantMediaOptions.map((mediaOption) => {
|
||
videoCodecFound = videoCodecFound || Boolean(mediaOption.videoCodec);
|
||
audioCodecFound = audioCodecFound || Boolean(mediaOption.audioCodec) || Boolean(mediaOption.audioGroupId);
|
||
return mediaOption;
|
||
});
|
||
// remove audio-only media options if we also have media options with audio+video codecs signaled
|
||
if (videoCodecFound && audioCodecFound) {
|
||
regroupedVariantMediaOptions = regroupedVariantMediaOptions.filter(({ videoCodec }) => Boolean(videoCodec));
|
||
}
|
||
logger.info(loggerName, `playlist has ${regroupedVariantMediaOptions.length} variantMediaOptions`);
|
||
return filterByPlatformCapabilities(variantMediaOptions, audioAlternateOptions, sessionKeys, platformInfo, config, logger).pipe(map(({ hdrMediaOptions, sdrMediaOptions }) => {
|
||
const allMediaOptions = hdrMediaOptions.concat(sdrMediaOptions);
|
||
const hasHdrMediaOptions = hdrMediaOptions.length > 0;
|
||
logPlatformCapabilitiesFilterResults(hdrMediaOptions, sdrMediaOptions, undefined, config, displaySupportsHdr, logger);
|
||
return makeRootPlaylistEntity(loadRootMediaOptionsResult, allMediaOptions, hasHdrMediaOptions, displaySupportsHdr);
|
||
}));
|
||
};
|
||
/**
|
||
* Choose the audio track given the selection group and options
|
||
* @param subtitleMediaSelectionGroup
|
||
* @param subtitleAlternateOptions
|
||
*/
|
||
const getAutoSubtitle = (persistentId, closedCaptionGroup, subtitleGroup, subtitleMediaSelectionGroup, subtitleAlternateOptions, logger) => {
|
||
if (subtitleMediaSelectionGroup) {
|
||
let subtitleMediaOption;
|
||
if (isFiniteNumber(persistentId)) {
|
||
subtitleMediaOption = subtitleMediaSelectionGroup.MediaSelectionGroupOptions.find(function (mediaOption) {
|
||
return mediaOption.MediaSelectionOptionsPersistentID === persistentId;
|
||
});
|
||
}
|
||
else {
|
||
subtitleMediaOption = subtitleMediaSelectionGroup.MediaSelectionGroupOptions.find(function (mediaOption) {
|
||
return mediaOption.MediaSelectionOptionsIsDefault;
|
||
});
|
||
}
|
||
let subtitleTrackInfo;
|
||
if (subtitleMediaOption) {
|
||
subtitleTrackInfo = subtitleAlternateOptions.find((subtitleTrack) => {
|
||
if (subtitleTrack.mediaType === MediaTypeFourCC.CLOSEDCAPTION) {
|
||
return (!closedCaptionGroup || subtitleTrack.groupId === closedCaptionGroup) && subtitleTrack.persistentID === subtitleMediaOption.MediaSelectionOptionsPersistentID;
|
||
}
|
||
else if (subtitleTrack.mediaType === MediaTypeFourCC.SUBTITLE) {
|
||
return (!subtitleGroup || subtitleTrack.groupId === subtitleGroup) && subtitleTrack.persistentID === subtitleMediaOption.MediaSelectionOptionsPersistentID;
|
||
}
|
||
else {
|
||
logger.warn(loggerName, `subtitle media option has unknown type ${subtitleTrack.mediaType}`);
|
||
}
|
||
});
|
||
}
|
||
return subtitleTrackInfo;
|
||
}
|
||
};
|
||
function makeRootPlaylistEntity(loadRootMediaOptionsResult, variantMediaOptions, hasHdrLevels, preferHDR) {
|
||
var _a;
|
||
const { itemId, itemStartOffset, rootMediaOptionsTuple, audioMediaSelectionGroup, subtitleMediaSelectionGroup } = loadRootMediaOptionsResult;
|
||
const audioAlternateOptions = Array.from(rootMediaOptionsTuple[MediaOptionType.AltAudio]);
|
||
const subtitleAlternateOptions = Array.from(rootMediaOptionsTuple[MediaOptionType.Subtitle]);
|
||
const hasScore = variantMediaOptions.every((x) => isFiniteNumber(x.score));
|
||
const hasIframeLevels = variantMediaOptions.some((x) => isIframeLevel(x));
|
||
const sortedVariantMediaOptions = sortVariants(variantMediaOptions, hasScore);
|
||
const compatibleVariants = null;
|
||
const compatibleAudioAlternates = null;
|
||
const baseUrl = loadRootMediaOptionsResult.baseUrl;
|
||
const initPathwayID = (_a = loadRootMediaOptionsResult.contentSteeringOption) === null || _a === void 0 ? void 0 : _a.initPathwayID;
|
||
const sessionData = loadRootMediaOptionsResult.sessionData;
|
||
const rootPlaylistEntity = {
|
||
itemId,
|
||
baseUrl,
|
||
mediaOptionListTuple: [
|
||
{
|
||
mediaOptions: sortedVariantMediaOptions,
|
||
hasHdrLevels,
|
||
hasIframeLevels,
|
||
hasScore,
|
||
preferHDR,
|
||
compatibleIds: compatibleVariants,
|
||
penaltyBoxQueue: [],
|
||
removed: [],
|
||
currentPathwayID: initPathwayID,
|
||
},
|
||
{
|
||
mediaOptions: audioAlternateOptions,
|
||
compatibleIds: compatibleAudioAlternates,
|
||
penaltyBoxQueue: [],
|
||
removed: [],
|
||
},
|
||
{
|
||
mediaOptions: subtitleAlternateOptions,
|
||
penaltyBoxQueue: [],
|
||
removed: [],
|
||
},
|
||
],
|
||
audioMediaSelectionGroup,
|
||
subtitleMediaSelectionGroup,
|
||
enabledMediaOptionKeys: [NoMediaOption, NoMediaOption, NoMediaOption],
|
||
mediaOptionSwitchContexts: [null, null, null],
|
||
anchorTime: { pos: 0 },
|
||
discoSeqNum: NaN,
|
||
pendingSeek: undefined,
|
||
itemStartOffset,
|
||
initPtsRecord: {},
|
||
contentSteeringOption: loadRootMediaOptionsResult.contentSteeringOption,
|
||
masterVariableList: loadRootMediaOptionsResult.masterVariableList,
|
||
loadStats: loadRootMediaOptionsResult.stats,
|
||
isMediaPlaylist: loadRootMediaOptionsResult.isMediaPlaylist,
|
||
abrStatus: {
|
||
fragDownloadSlow: false,
|
||
fragDownloadTooSlow: false,
|
||
nextMinAutoOptionId: NoMediaOption.mediaOptionId,
|
||
nextMaxAutoOptionId: NoMediaOption.mediaOptionId,
|
||
highBWTrigger: NaN,
|
||
},
|
||
sessionData,
|
||
};
|
||
return rootPlaylistEntity;
|
||
}
|
||
function selectStartingVariant(rootPlaylistEntity, logger, bandwidthEstimate, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate) {
|
||
const mediaOptionListInfo = rootPlaylistEntity.mediaOptionListTuple[MediaOptionType.Variant];
|
||
const filteredVariants = applyFilters(mediaOptionListInfo.mediaOptions, VariantMediaOptionListQuery.kAllowFilters, Object.assign(Object.assign({}, mediaOptionListInfo), { compatibleIds: null }));
|
||
const preferredVariants = getPreferredList(mediaOptionListInfo.preferredHost, filteredVariants);
|
||
const firstVariant = chooseFirstMediaOption(preferredVariants, firstMediaOptionSelectionMetrics, mediaOptionListInfo.hasScore, logger, bandwidthEstimate, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate);
|
||
return { firstVariant, filteredVariants, preferredVariants };
|
||
}
|
||
function replaceClosedCaptionInMediaOptionListIfNecessary(variant, closedCaption, subtitleMediaOptions, logger) {
|
||
if ((closedCaption === null || closedCaption === void 0 ? void 0 : closedCaption.mediaType) === MediaTypeFourCC.CLOSEDCAPTION) {
|
||
const forcedMediaOption = AlternateMediaOptionListQuery.pairForcedSubtitleMediaOptionWithClosedCaptionInList(variant.subtitleGroupId, closedCaption, subtitleMediaOptions);
|
||
if (forcedMediaOption) {
|
||
closedCaption = Object.assign(Object.assign({}, closedCaption), { url: forcedMediaOption.url, backingMediaOptionId: forcedMediaOption.mediaOptionId });
|
||
const newSubtitles = subtitleMediaOptions.map((option) => {
|
||
if (option.mediaOptionId === closedCaption.mediaOptionId) {
|
||
logger.info(`[subtitle] use closedCaption ${JSON.stringify(closedCaption)}`);
|
||
return closedCaption;
|
||
}
|
||
return option;
|
||
});
|
||
return newSubtitles;
|
||
}
|
||
}
|
||
return subtitleMediaOptions; // no change
|
||
}
|
||
/**
|
||
* Modify rootPlaylistEntity (in-place), updating media options list and selecting a new set of enabled media options
|
||
*/
|
||
function updateRootPlaylistEntityWithEnabledMediaOptionKeys(rootPlaylistEntity, existingSelection, logger, bandwidthEstimate, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate) {
|
||
var _a, _b;
|
||
const itemId = rootPlaylistEntity.itemId;
|
||
const mediaOptionListInfo = rootPlaylistEntity.mediaOptionListTuple[MediaOptionType.Variant];
|
||
const audioOptionListInfo = rootPlaylistEntity.mediaOptionListTuple[MediaOptionType.AltAudio];
|
||
const subtitleOptionListInfo = rootPlaylistEntity.mediaOptionListTuple[MediaOptionType.Subtitle];
|
||
const filteredAudio = applyFilters(audioOptionListInfo.mediaOptions, AlternateMediaOptionListQuery.kAllowFilters, audioOptionListInfo);
|
||
const filteredSubtitles = applyFilters(subtitleOptionListInfo.mediaOptions, AlternateMediaOptionListQuery.kAllowFilters, subtitleOptionListInfo);
|
||
let { firstVariant, filteredVariants } = selectStartingVariant(rootPlaylistEntity, logger, bandwidthEstimate, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate);
|
||
if (!firstVariant) {
|
||
const preferHDR = mediaOptionListInfo.preferHDR;
|
||
if (preferHDR) {
|
||
mediaOptionListInfo.preferHDR = false;
|
||
}
|
||
else {
|
||
mediaOptionListInfo.preferHDR = mediaOptionListInfo.hasHdrLevels;
|
||
}
|
||
if (mediaOptionListInfo.preferHDR !== preferHDR) {
|
||
logger.warn(`No valid first variant found, toggling hdr preference=${preferHDR}->${mediaOptionListInfo.preferHDR}`);
|
||
({ firstVariant, filteredVariants } = selectStartingVariant(rootPlaylistEntity, logger, bandwidthEstimate, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate));
|
||
}
|
||
}
|
||
if (!firstVariant) {
|
||
throw new ExceptionError(true, 'No valid first variant found', ErrorResponses.NoValidAlternates);
|
||
}
|
||
const preferredHost = getHostName(firstVariant.url);
|
||
logger.info(loggerName, `First level ${itemId}: ${mediaOptionToString(firstVariant)}`);
|
||
const variantId = (_a = firstVariant === null || firstVariant === void 0 ? void 0 : firstVariant.mediaOptionId) !== null && _a !== void 0 ? _a : null;
|
||
const variantKey = { itemId, mediaOptionId: variantId };
|
||
const altAudioId = (filteredAudio === null || filteredAudio === void 0 ? void 0 : filteredAudio.length)
|
||
? (_b = getAutoAudio(existingSelection === null || existingSelection === void 0 ? void 0 : existingSelection.audioPersistentId, firstVariant.audioGroupId, rootPlaylistEntity.audioMediaSelectionGroup, filteredAudio)) === null || _b === void 0 ? void 0 : _b.mediaOptionId
|
||
: null;
|
||
const altAudioKey = altAudioId ? { itemId, mediaOptionId: altAudioId } : NoMediaOption;
|
||
const firstSubtitle = getAutoSubtitle(existingSelection === null || existingSelection === void 0 ? void 0 : existingSelection.subtitlePersistentId, firstVariant.closedcaption, firstVariant.subtitleGroupId, rootPlaylistEntity.subtitleMediaSelectionGroup, filteredSubtitles, logger);
|
||
const subtitleId = (filteredSubtitles === null || filteredSubtitles === void 0 ? void 0 : filteredSubtitles.length) ? firstSubtitle === null || firstSubtitle === void 0 ? void 0 : firstSubtitle.mediaOptionId : null;
|
||
const subtitleKey = subtitleId ? { itemId, mediaOptionId: subtitleId, mediaOptionType: MediaOptionType.Subtitle } : NoMediaOption;
|
||
const { mediaOptions, audioGroups, subtitleGroups, closedCaptionGroups } = filterMediaOptionsBasedOnFirstMediaOptions(filteredVariants, firstVariant);
|
||
const compatibleVariants = Array.from(mediaOptions).map((mediaOption) => mediaOption.mediaOptionId);
|
||
const currentPathwayID = firstVariant.pathwayID;
|
||
const updatedMediaOptionListInfo = Object.assign(Object.assign({}, mediaOptionListInfo), { compatibleIds: compatibleVariants, preferredHost,
|
||
currentPathwayID });
|
||
// audio
|
||
const compatibleAudioAlternates = [];
|
||
const audioMediaOptions = audioOptionListInfo.mediaOptions;
|
||
const filteredAudioResults = audioMediaOptions.reduce((prev, cur) => {
|
||
if (audioGroups.has(cur.groupId)) {
|
||
prev.persistentIds.add(cur.persistentID);
|
||
compatibleAudioAlternates.push(cur.mediaOptionId);
|
||
prev.filteredAudioMediaOptions.push(cur);
|
||
if (!prev.altAudio) {
|
||
prev.altAudio = !!cur.url;
|
||
}
|
||
}
|
||
return prev;
|
||
}, { filteredAudioMediaOptions: [], persistentIds: new Set(), altAudio: false });
|
||
const updatedAudioOptionListInfo = Object.assign(Object.assign({}, audioOptionListInfo), { compatibleIds: compatibleAudioAlternates });
|
||
let audioMediaSelectionGroup = rootPlaylistEntity.audioMediaSelectionGroup;
|
||
const audioMediaSelectionOptions = audioMediaSelectionGroup === null || audioMediaSelectionGroup === void 0 ? void 0 : audioMediaSelectionGroup.MediaSelectionGroupOptions;
|
||
if (audioMediaSelectionOptions) {
|
||
const filteredAudioSelectionOptions = audioMediaSelectionOptions.reduce((prev, cur) => {
|
||
if (filteredAudioResults.persistentIds.has(cur.MediaSelectionOptionsPersistentID)) {
|
||
prev.push(cur);
|
||
}
|
||
return prev;
|
||
}, new Array());
|
||
audioMediaSelectionGroup = Object.assign(Object.assign({}, audioMediaSelectionGroup), { MediaSelectionGroupOptions: filteredAudioSelectionOptions });
|
||
}
|
||
// subtitle
|
||
subtitleOptionListInfo.mediaOptions = replaceClosedCaptionInMediaOptionListIfNecessary(firstVariant, firstSubtitle, subtitleOptionListInfo.mediaOptions, logger);
|
||
const subtitleMediaOptions = subtitleOptionListInfo.mediaOptions;
|
||
const filteredSubtitleResults = subtitleMediaOptions.reduce((prev, cur) => {
|
||
if (subtitleGroups.has(cur.groupId)) {
|
||
// do NOT re-assign track id. They must be unique for the entire collection of subtitle and caption tracks
|
||
// cur.id = prev.filteredSubtitleMediaOptions.length;
|
||
prev.persistentIds.add(cur.persistentID);
|
||
prev.filteredSubtitleMediaOptions.push(cur);
|
||
}
|
||
return prev;
|
||
}, { filteredSubtitleMediaOptions: [], persistentIds: new Set() });
|
||
let subtitleMediaSelectionGroup = rootPlaylistEntity.subtitleMediaSelectionGroup;
|
||
const subtitleMediaSelectionOptions = subtitleMediaSelectionGroup === null || subtitleMediaSelectionGroup === void 0 ? void 0 : subtitleMediaSelectionGroup.MediaSelectionGroupOptions;
|
||
if (subtitleMediaSelectionOptions) {
|
||
const filteredSubtitleSelectionOptions = subtitleMediaSelectionOptions.reduce((prev, cur) => {
|
||
if (filteredSubtitleResults.persistentIds.has(cur.MediaSelectionOptionsPersistentID)) {
|
||
prev.push(cur);
|
||
}
|
||
return prev;
|
||
}, new Array());
|
||
subtitleMediaSelectionGroup = Object.assign(Object.assign({}, subtitleMediaSelectionGroup), { MediaSelectionGroupOptions: filteredSubtitleSelectionOptions });
|
||
}
|
||
const mediaOptionListTuple = [updatedMediaOptionListInfo, updatedAudioOptionListInfo, subtitleOptionListInfo];
|
||
// Find the highest codec string per codec family
|
||
let highestVideoCodec = new Map();
|
||
const hlsConf = getCurrentConfig();
|
||
if (hlsConf.useHighestVideoCodecPrivate) {
|
||
highestVideoCodec = updatedMediaOptionListInfo === null || updatedMediaOptionListInfo === void 0 ? void 0 : updatedMediaOptionListInfo.mediaOptions.reduce((codecStringMap, mediaOption) => {
|
||
const codecStringList = mediaOption.videoCodecList;
|
||
if (codecStringList) {
|
||
for (const codec of codecStringList) {
|
||
const key = getVideoCodecFamily(codec);
|
||
const currentCodec = codecStringMap.get(key);
|
||
if (MediaUtil.isHigherCodecByFamily(currentCodec, codec)) {
|
||
codecStringMap.set(key, codec);
|
||
}
|
||
}
|
||
}
|
||
return codecStringMap;
|
||
}, highestVideoCodec);
|
||
}
|
||
if (highestVideoCodec.size) {
|
||
highestVideoCodec.forEach((value, key) => logger.info(`override for ${key} family with codec ${value}`));
|
||
}
|
||
const abrStatus = {
|
||
fragDownloadSlow: false,
|
||
fragDownloadTooSlow: false,
|
||
nextMinAutoOptionId: NoMediaOption.mediaOptionId,
|
||
nextMaxAutoOptionId: NoMediaOption.mediaOptionId,
|
||
highBWTrigger: getLowestSuperiorBW(variantKey.mediaOptionId, updatedMediaOptionListInfo.mediaOptions),
|
||
};
|
||
return Object.assign(Object.assign({}, rootPlaylistEntity), { enabledMediaOptionKeys: [variantKey, altAudioKey, subtitleKey], mediaOptionListTuple,
|
||
audioMediaSelectionGroup,
|
||
abrStatus,
|
||
highestVideoCodec });
|
||
}
|
||
/**
|
||
* @brief Load the root playlist
|
||
*
|
||
*/
|
||
const retrieveRootMediaOptions = (loadPolicy, rootPlaylistService, config, platformQuery, existingSelection, statsService, playerEvents) => (source) => {
|
||
return source.pipe(tag('retrieveRootMediaOptions.input'), switchMap((item) => {
|
||
var _a, _b;
|
||
if (!item)
|
||
return EMPTY;
|
||
const { itemId, platformInfo } = item;
|
||
const rootPlaylistQuery = createRootPlaylistQuery(itemId);
|
||
const { logger } = rootPlaylistService;
|
||
if (rootPlaylistQuery.hasEntity(itemId)) {
|
||
// these needs to handle failures and stuff
|
||
return of(rootPlaylistQuery);
|
||
}
|
||
rootPlaylistStore.setLoading(true);
|
||
const tManifestLoadStart = performance.now();
|
||
return loadRootMediaOptions(item, loadPolicy, logger, config, statsService, (_b = (_a = globalHlsService()) === null || _a === void 0 ? void 0 : _a.getQuery()) === null || _b === void 0 ? void 0 : _b.extendMaxTTFB).pipe(tap((loadRootMediaOptionsResult) => playerEvents.triggerManifestLoaded(loadRootMediaOptionsResult)), tap(({ initialDetails, sessionData, stats }) => {
|
||
if (initialDetails) {
|
||
archiveMediaOptionDetails(initialDetails, stats, true);
|
||
}
|
||
}), withLatestFrom(platformQuery.displaySupportsHdr$), switchMap(([loadRootMediaOptionsResult, displaySupportsHdr]) => updateBasedOnPlatformCapabilities(loadRootMediaOptionsResult, platformInfo, config, displaySupportsHdr, logger)), map((rootPlaylistEntity) => {
|
||
rootPlaylistService.rootPlaylistEntity = initializeRootEntity(item, rootPlaylistEntity, existingSelection, tManifestLoadStart, config, logger);
|
||
return rootPlaylistQuery;
|
||
}), addLoadErrorHandlingPolicy(itemId, null, getLoadConfig(item, loadPolicy), 0, false, rootPlaylistQuery, rootPlaylistService, statsService), finalize$1(() => {
|
||
rootPlaylistStore.setLoading(false);
|
||
}));
|
||
}), tag('retrieveRootMediaOptions.emit'));
|
||
};
|
||
/**
|
||
* @brief Choose first media options and update initial seek position
|
||
* @returns updated RootPlaylistEntity with selected media options and seek time
|
||
*/
|
||
function initializeRootEntity(item, rootPlaylistEntity, existingSelection, tManifestLoadStart, config, logger) {
|
||
const { itemId, initialSeekTime, itemStartOffset } = item;
|
||
const statsQuery = createStatsQuery(itemId);
|
||
const bandwidthEstimate = config.enableAdaptiveStartup ? statsQuery.getBandwidthEstimate(config, item.serviceName) : undefined;
|
||
const playlistEstimate = config.enableAdaptiveStartup ? statsQuery.getPlaylistEstimate(config, item.serviceName) : undefined;
|
||
const fragEstimate = config.enableAdaptiveStartup ? statsQuery.getFragEstimate(config, item.serviceName) : undefined;
|
||
const bufferEstimate = config.enableAdaptiveStartup ? statsQuery.getBufferEstimate(config, item.serviceName) : undefined;
|
||
const timeElapsed = performance.now() - tManifestLoadStart;
|
||
let targetStartupMs;
|
||
if (config.targetStartupMs > timeElapsed) {
|
||
targetStartupMs = config.targetStartupMs - timeElapsed;
|
||
}
|
||
else {
|
||
/* In some edge cases, such as network is too slow/lossy, it is seen that the manifestLoad takes longer resulting in timeElapsed go above config.targetStartupMs.
|
||
To avoid targetStartupMs being negative, fall back to config.targetStartupMs */
|
||
targetStartupMs = config.targetStartupMs;
|
||
logger.warn(`Manifest load took ${timeElapsed}ms and exceeds targetStartupMs: ${config.targetStartupMs}; resetting targetStartupMs to ${config.targetStartupMs}`);
|
||
}
|
||
const adaptiveStartupConfig = config.enableAdaptiveStartup
|
||
? {
|
||
targetDuration: fragEstimate.maxDurationSec || config.defaultTargetDuration,
|
||
targetStartupMs: targetStartupMs,
|
||
}
|
||
: undefined;
|
||
const updatedRootPlaylistEntity = updateRootPlaylistEntityWithEnabledMediaOptionKeys(rootPlaylistEntity, existingSelection, logger, bandwidthEstimate, adaptiveStartupConfig, playlistEstimate, fragEstimate, bufferEstimate);
|
||
// Initialize pendingSeek if this is not a preloading item. don't update anchorTime yet.
|
||
updatedRootPlaylistEntity.pendingSeek = initialSeekTime;
|
||
return updatedRootPlaylistEntity;
|
||
}
|
||
|
||
/**
|
||
* @brief Create all adapters for hls. they will live while media is attached
|
||
*/
|
||
function makeAdapters(itemRemove$, ksService, config, platformQuery, eventEmitter, rtcService, logger) {
|
||
return (source$) => {
|
||
const keySystemAdapter = makeKeySystemService(ksService, source$, itemRemove$, config, platformQuery, eventEmitter, rtcService, logger);
|
||
const legibleSystemAdapter = makeLegibleService(source$, config, eventEmitter, logger);
|
||
return combineLatest([keySystemAdapter, legibleSystemAdapter, source$]).pipe(map(([keySystemAdapter, legibleSystemAdapter, mediaSink]) => {
|
||
return { keySystemAdapter, legibleSystemAdapter, mediaSink };
|
||
}));
|
||
};
|
||
}
|
||
function makeActiveItemAdapters(config, logger, rootPlaylistService, platformQuery, statsService, playerEvents, rpcClients) {
|
||
return (activeItem$) => activeItem$.pipe(filterNullOrUndefined(), switchMap((activeItem) => {
|
||
logger.info(`active item changed ${activeItem === null || activeItem === void 0 ? void 0 : activeItem.itemId}`);
|
||
if (!activeItem) {
|
||
return of(null);
|
||
}
|
||
const rootPlaylistQuery$ = of(activeItem).pipe(retrieveRootMediaOptions(config.manifestLoadPolicy, rootPlaylistService, config, platformQuery, null, statsService, playerEvents));
|
||
return combineLatest([rootPlaylistQuery$, createMediaParser(config, logger, rpcClients.mux), createIframeMachine(config.trickPlaybackConfig, logger)]).pipe(map(([rootPlaylistQuery, mediaParser, iframeMachine]) => ({ rootPlaylistQuery, mediaParser, iframeMachine })));
|
||
}));
|
||
}
|
||
/**
|
||
* @brief Observable for updating platform information
|
||
*/
|
||
function platformUpdater(platformService) {
|
||
return listenForHdrUpdates(platformService).pipe(switchMapTo(EMPTY));
|
||
}
|
||
/**
|
||
* @brief Top level class that handles setup of playback for a queue of items
|
||
*/
|
||
class Hls$1 extends HlsEventEmitter {
|
||
constructor(userConfig = {}, logger) {
|
||
var _a;
|
||
super();
|
||
this.destroy$ = new Subject();
|
||
this.mediaElement$ = new BehaviorSubject(null);
|
||
this.publicQueriesInternal$ = new BehaviorSubject(null);
|
||
this.mediaElementAdapter = null;
|
||
this.rpcService = null;
|
||
this.rpcClients = null;
|
||
this.platformService = platformService();
|
||
this.keySystemAdapter = null;
|
||
this.legibleSystemAdapter = null;
|
||
this.sessionID = guid();
|
||
this.statsService = statsServiceSingleton();
|
||
this.gaplessCapable = true;
|
||
this.teardownWG$ = new WaitGroup();
|
||
this.itemQueue = new ItemQueue();
|
||
// Sanitize userConfig
|
||
if ((userConfig.liveSyncDurationCount || userConfig.liveMaxLatencyDurationCount) && (userConfig.liveSyncDuration || userConfig.liveMaxLatencyDuration)) {
|
||
throw new Error('Illegal hls.js config: don\'t mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration');
|
||
}
|
||
const config = Object.assign(Object.assign({}, hlsDefaultConfig), userConfig);
|
||
if (config.maxRequiredStartDuration < config.minRequiredStartDuration || config.minRequiredStartDuration < 0) {
|
||
throw new Error('Illegal config: bad maxRequiredStartDuration or minRequiredStartDuration');
|
||
}
|
||
this.hlsConfig = config;
|
||
// setup the redact url function
|
||
setupRedactUrl(config.buildType);
|
||
// set up logger
|
||
const { sessionID: sessionId } = this;
|
||
let logLevel = 'silent';
|
||
if (userConfig.debug) {
|
||
logLevel = config.debugLevel;
|
||
}
|
||
// rdar://88106960 (HLS with buildType "production" much slower than with buildType "development")
|
||
// setting autoFreeze to true follows default behavior with immer 8.0+
|
||
setAutoFreeze_1(true);
|
||
{
|
||
// for unit tests only
|
||
this.logger = logger;
|
||
}
|
||
this.logger =
|
||
(_a = this.logger) !== null && _a !== void 0 ? _a : setupLoggerSingleton(sessionId, 'hls', getLoggerConfig({
|
||
sendLogs: config.sendLogs || (config.log ? LoggerExternals$1().logStore : null),
|
||
level: logLevel === 'log' ? 'debug' : logLevel,
|
||
consoleOverride: typeof userConfig.debug !== 'boolean' ? userConfig.debug : undefined,
|
||
buildType: config.buildType,
|
||
}));
|
||
this.logger.qe({ critical: true, name: 'playerVersion', data: { version: Hls$1.version } });
|
||
{
|
||
this.logger.qe({ critical: true, name: 'playerCommit', data: { hash: '0c65bb95' } });
|
||
this.logger.qe({ critical: true, name: 'playerBranch', data: { hash: 'release/2.162' } });
|
||
initialize(userConfig.socketurl, userConfig.socketid);
|
||
setHls(this);
|
||
setLogger(this.logger);
|
||
}
|
||
this.hlsConfig.audioPrimingDelay = 0;
|
||
this.logger.info(`force audioPrimingDelay to ${this.hlsConfig.audioPrimingDelay}`);
|
||
this.rootPlaylistService = rootPlaylistService(this.logger);
|
||
this.customUrlLoader = getCustomUrlLoader();
|
||
this.sessionDataLoader = new SessionDataLoader(config, fromXMLHttpRequest, this.customUrlLoader.load, this.logger);
|
||
const liveMaxLatencyDurationCount = config.liveMaxLatencyDurationCount;
|
||
const liveSyncDurationCount = config.liveSyncDurationCount;
|
||
if (isFiniteNumber(liveMaxLatencyDurationCount) && isFiniteNumber(liveSyncDurationCount) && liveMaxLatencyDurationCount <= liveSyncDurationCount) {
|
||
throw new Error('Illegal hls.js config: "liveMaxLatencyDurationCount" must be gt "liveSyncDurationCount"');
|
||
}
|
||
if (isFiniteNumber(config.liveMaxLatencyDuration) && (config.liveMaxLatencyDuration <= config.liveSyncDuration || !isFiniteNumber(config.liveSyncDuration))) {
|
||
throw new Error('Illegal hls.js config: "liveMaxLatencyDuration" must be gt "liveSyncDuration"');
|
||
}
|
||
const hlsService = globalHlsService();
|
||
this.logger.info(`start Hls sessionId:${sessionId}`);
|
||
hlsService.setHlsEntity({ id: sessionId, config });
|
||
const mediaLibService = mediaLibraryService();
|
||
const platformQuery = createPlatformQuery();
|
||
const ksService = keySystemService();
|
||
this.accessLogInstance = new AccessLog(this, sessionId);
|
||
this.rtcService = new RTCService(this, config, this.accessLogInstance, this.logger);
|
||
this.playerEvents = new HlsPlayerEvents(this, this.logger, this.rtcService);
|
||
// Update platform information (display, etc)
|
||
const updatePlatformInfo$ = platformUpdater(this.platformService);
|
||
const mediaSink$ = this.mediaElement$.pipe(mediaElementServiceEpic(() => {
|
||
return new MediaSource();
|
||
}, config, this, this.logger, this.teardownWG$, this.rtcService), share() // Prevent us from making a new MediaSink for each thing subscribing to this observable
|
||
);
|
||
// Things that depend on active item.
|
||
const handleActiveItemChange$ = this.itemQueue.activeItemById$.pipe(switchMap((entity) => {
|
||
if (!entity) {
|
||
return EMPTY;
|
||
}
|
||
return statsProcessor(config, this.statsService, entity, this.logger);
|
||
}));
|
||
this.rpcService = (() => {
|
||
let create = null;
|
||
if (config.createRPCService != null) {
|
||
create = createRPCServiceWithFallbacks(config.createRPCService, createRPCInlineService);
|
||
}
|
||
if (hasUMDWorker() && config.enableWorker) {
|
||
if (create == null) {
|
||
create = createRPCService;
|
||
}
|
||
}
|
||
if (create == null) {
|
||
create = createRPCInlineService;
|
||
}
|
||
return create(this.logger);
|
||
})();
|
||
this.rpcClients = createRPCClients(this.rpcService);
|
||
// Emits when we have all context available for pipeline.
|
||
// CAUTION: every time this observable emits it will cause pipelineCtxHandlers$ to
|
||
// teardown and re-subscribe (if applicable). Only emit on /changes/ to pipeline context!
|
||
const pipelineContext$ = combineLatest([
|
||
// Emit when root playlist loaded
|
||
this.itemQueue.activeItemById$.pipe(makeActiveItemAdapters(config, this.logger, this.rootPlaylistService, platformQuery, this.statsService, this.playerEvents, this.rpcClients), tap((rootAdapters) => {
|
||
const rootPlaylistQuery = rootAdapters === null || rootAdapters === void 0 ? void 0 : rootAdapters.rootPlaylistQuery;
|
||
// prep publicQueriesInternal to serve api calls, that
|
||
// depend on rootPlaylistQuery exclusively.
|
||
this.publicQueriesInternal$.next([rootPlaylistQuery, null]);
|
||
this.iframeMachine = rootAdapters === null || rootAdapters === void 0 ? void 0 : rootAdapters.iframeMachine;
|
||
if (rootPlaylistQuery) {
|
||
// this is delayed trigger for manifest parsed, but doing it before
|
||
// setting in publicQueriesInternal will make some api such as
|
||
// sessionData non serviceable.
|
||
this.playerEvents.triggerManifestParsed(rootPlaylistQuery);
|
||
}
|
||
})),
|
||
// Emit when all adapters created / destroyed
|
||
mediaSink$.pipe(makeAdapters(this.itemQueue.removedItems$, ksService, config, platformQuery, this, this.rtcService, this.logger), tap(({ keySystemAdapter, legibleSystemAdapter, mediaSink }) => {
|
||
this.keySystemAdapter = keySystemAdapter;
|
||
this.legibleSystemAdapter = legibleSystemAdapter;
|
||
this.mediaElementAdapter = mediaSink;
|
||
})),
|
||
]).pipe(map(([activeItemAdapters, mediaAdapters]) => {
|
||
const { keySystemAdapter, legibleSystemAdapter, mediaSink } = mediaAdapters;
|
||
if (!activeItemAdapters || !keySystemAdapter || !legibleSystemAdapter || !mediaSink) {
|
||
return null; // Stop everything depending on this
|
||
}
|
||
const { rootPlaylistQuery, iframeMachine, mediaParser } = activeItemAdapters;
|
||
return {
|
||
logger: this.logger,
|
||
config,
|
||
platformService: this.platformService,
|
||
statsService: this.statsService,
|
||
rtcService: this.rtcService,
|
||
rpcClients: this.rpcClients,
|
||
rootPlaylistService: this.rootPlaylistService,
|
||
rootPlaylistQuery,
|
||
mediaLibraryService: mediaLibService,
|
||
keySystemAdapter,
|
||
legibleSystemAdapter,
|
||
mediaSink,
|
||
mediaParser,
|
||
iframeMachine,
|
||
customUrlLoader: this.customUrlLoader,
|
||
gaplessInstance: this,
|
||
};
|
||
}), share());
|
||
// Things that depend on PipelineContext. This means rootPlaylist loaded && mediaSink != null
|
||
const pipelineCtxHandlers$ = pipelineContext$.pipe(switchMap((context) => {
|
||
if (!context) {
|
||
return EMPTY;
|
||
}
|
||
const { rootPlaylistQuery, mediaSink, mediaLibraryService } = context;
|
||
this.publicQueriesInternal$.next([rootPlaylistQuery, mediaSink.mediaQuery]);
|
||
const mediaQuery = mediaSink.mediaQuery;
|
||
const mediaFragmentPipeline$ = of(context).pipe(mediaFragmentPipelineEpic());
|
||
// TODO: Fix rdar://81171922 and re-enable content steering
|
||
// const loadSteeringManifest$ = of(rootPlaylistQuery).pipe(contentSteeringEpic(context.rootPlaylistService, config, config.steeringManifestLoadPolicy, this.logger));
|
||
// <rdar://81918939> AirPlay may pick the alternates before hls.rootQuery and rootQuery.rootPlaylistEntity are ready.
|
||
// Delay these early selections when hls is instantiating.
|
||
// We should not need these early alternate selections for gapless items since
|
||
// (i) follow-up items should inherit the existing language selections.
|
||
// (ii) hls.rootQuery has been setup and rootQuery.rootPlaylistEntity lifecycle (for the next itemId) is controlled by hls.js
|
||
const earlySelection$ = rootPlaylistQuery.rootPlaylistEntity$.pipe(filterNullOrUndefined(), take(1), tap(() => {
|
||
this.commitEarlySelection(context.logger); // earliest point where this._activeRootQuery is valid
|
||
}));
|
||
const loadSessionData$ = waitFor(combineLatest([mediaQuery.haveEnough$, rootPlaylistQuery.sessionData$]), ([haveEnough]) => haveEnough === true, 1).pipe(switchMap(([, sessionData]) => {
|
||
return this.sessionDataLoader.loadSessionData(sessionData);
|
||
}), tap((sessionData) => {
|
||
this.rootPlaylistService.setSessionData(rootPlaylistQuery.itemId, sessionData);
|
||
}), catchError((err) => {
|
||
this.logger.error(err.message);
|
||
return EMPTY;
|
||
}));
|
||
// Is this a good place to put this....
|
||
const statsQuery = createStatsQuery(rootPlaylistQuery.itemId);
|
||
const haveEnough$ = checkForHaveEnough(config, this.logger, rootPlaylistQuery, mediaSink, mediaLibraryService, statsQuery);
|
||
const ended$ = checkForEndOfStream(context);
|
||
const iframeAutoPause$ = checkForIframeAutoPause(context);
|
||
const iframePrefetch$ = checkForIframePrefetch(context);
|
||
const playbackInfo$ = waitFor(combineQueries([mediaQuery.gotPlaying$, mediaQuery.gotLoadStart$, mediaQuery.readyState$]), ([playing, loadStart, readyState]) => playing === true || loadStart === true || readyState >= 1).pipe(switchMap(() => mediaQuery.ended$),
|
||
// on ended, fire once and stop, else every 1s
|
||
switchMap((ended) => timer(0, ended ? undefined : 1000)), tap(() => {
|
||
this.playbackInfo(config, mediaQuery);
|
||
}));
|
||
const updatePlayingId$ = mediaQuery.timeupdate$.pipe(map((pos) => {
|
||
if (this.inGaplessMode && this.isPreloading) {
|
||
if (isFiniteNumber(this.loadingItem.itemStartOffset) && pos >= this.loadingItem.itemStartOffset) {
|
||
const prevItemId = this.itemQueue.playingItem.itemId;
|
||
const nextItemId = this.itemQueue.loadingItem.itemId;
|
||
const nextStartTime = this.loadingItem.itemStartOffset;
|
||
const nextDuration = mediaQuery.msDuration - this.loadingItem.itemStartOffset;
|
||
const data = { prevItemId, nextItemId, nextStartTime, nextDuration };
|
||
this.logger.info(`[gapless] Item transitioned prevItem: ${prevItemId}, nextItem: ${nextItemId}, nextStartTime: ${nextStartTime}, nextDuration: ${nextDuration}`);
|
||
this.itemQueue.updatePlayingItemId();
|
||
this.trigger(HlsEvent.ITEM_TRANSITIONED, data);
|
||
this.rtcService.itemTransitioned(prevItemId, nextItemId);
|
||
this.logger.qe({ critical: true, name: 'gapless', data: { transitionFrom: prevItemId, transitionTo: nextItemId } });
|
||
}
|
||
}
|
||
}));
|
||
const liveSeekableRange$ = this.updateLiveSeekableRange(rootPlaylistQuery, mediaSink);
|
||
const observableList = [
|
||
earlySelection$,
|
||
mediaFragmentPipeline$,
|
||
// TODO: Fix rdar://81171922 and re-enable content steering
|
||
// loadSteeringManifest$, // Load steering manifest
|
||
loadSessionData$,
|
||
haveEnough$,
|
||
playbackInfo$,
|
||
iframeAutoPause$,
|
||
iframePrefetch$,
|
||
updatePlayingId$,
|
||
liveSeekableRange$,
|
||
ended$, // Check for end of stream
|
||
];
|
||
if (config.enablePerformanceLogging) {
|
||
const tlog = this.logger.child({ name: 'timing' });
|
||
// Log currently loading fragments
|
||
const inFlightFrags$ = zip(...AVMediaOptionTypes.map((type) => {
|
||
return rootPlaylistQuery.getInFlightFragByType$(type).pipe(distinctUntilChanged((a, b) => (a === null || a === void 0 ? void 0 : a.state) === (b === null || b === void 0 ? void 0 : b.state)), filterNullOrUndefined(), withLatestFrom(mediaQuery.bufferedRangeTuple$), tap(([inFlightInfo, bufferedRanges]) => {
|
||
const payload = Object.assign(Object.assign({}, inFlightInfo), { event: 'fragment', name: MediaOptionNames[type], buffered: undefined });
|
||
if (inFlightInfo.state === 'appended') {
|
||
payload.buffered = bufferedRanges;
|
||
}
|
||
tlog.info(JSON.stringify(payload));
|
||
}), catchError(() => EMPTY));
|
||
}));
|
||
// Log whenever detailsLoading changes
|
||
const loadingPlaylist$ = zip(...MediaOptionTypes.map((type) => {
|
||
return rootPlaylistQuery.enabledMediaOptionByType$(type).pipe(switchMap((option) => {
|
||
if ((option === null || option === void 0 ? void 0 : option.url) == null || !isEnabledMediaOption(option)) {
|
||
return EMPTY;
|
||
}
|
||
const query = createMediaLibraryQuery(option);
|
||
return query.mediaOptionDetailsEntity$.pipe(filterNullOrUndefined(), map((entity) => ({ entity, option })), distinctUntilChanged((a, b) => (a === null || a === void 0 ? void 0 : a.entity.detailsLoading) === (b === null || b === void 0 ? void 0 : b.entity.detailsLoading)), tap(({ entity, option }) => {
|
||
if (!entity || !option) {
|
||
return;
|
||
}
|
||
const payload = {
|
||
event: 'playlist',
|
||
name: MediaOptionNames[option.mediaOptionType],
|
||
mediaOptionId: option.mediaOptionId,
|
||
state: entity.detailsLoading ? 'loading' : 'loaded',
|
||
};
|
||
tlog.info(JSON.stringify(payload));
|
||
}));
|
||
}), catchError(() => EMPTY));
|
||
}));
|
||
observableList.push(inFlightFrags$, loadingPlaylist$);
|
||
}
|
||
return merge(...observableList);
|
||
}));
|
||
const handleQueueItemRemove$ = this.itemQueue.removedItems$.pipe(withTransaction((ids) => {
|
||
// Cleanup everything with lifecycle tied to QueueItem
|
||
mediaLibraryRemove(ids);
|
||
this.rootPlaylistService.removeItems(ids);
|
||
}));
|
||
const userSeek$ = hlsService.getQuery().userSeek$.pipe(userSeekEpic(this.itemQueue, this.rootPlaylistService));
|
||
const gotNewHls$ = hlsService
|
||
.getQuery()
|
||
.selectEntityAction(EntityActions.Add)
|
||
.pipe(tap(() => {
|
||
this.logger.warn(`new Hls instance added while old one still active sessionId:${sessionId}`);
|
||
}));
|
||
merge(
|
||
// Keep mediaSink alive even if other stuff errors out just in case we need play to end of buffer handling
|
||
// PipelineContext is subscribed to by pipelineCtxHandler$ so errors should be caught in other observable.
|
||
mediaSink$.pipe(catchError(() => EMPTY)), merge(updatePlatformInfo$, userSeek$, handleActiveItemChange$, handleQueueItemRemove$, pipelineCtxHandlers$, this.teardownWG$).pipe(catchError((err) => this._handleError(err))))
|
||
.pipe(finalize$1(() => {
|
||
var _a, _b;
|
||
try {
|
||
this.logger.info(`finalize Hls sessionId:${sessionId}`);
|
||
// Clean up everything with lifecycle tied to HLS instance:
|
||
this.detachMedia();
|
||
this.trigger(HlsEvent.DESTROYING);
|
||
this.playerEvents.destroy();
|
||
(_a = this.accessLogInstance) === null || _a === void 0 ? void 0 : _a.destroy();
|
||
(_b = this.rtcService) === null || _b === void 0 ? void 0 : _b.destroy();
|
||
mediaLibraryClear();
|
||
this.rootPlaylistService.removeAll();
|
||
this.itemQueue.clearQueue();
|
||
hlsService.removeEntity(this.sessionID);
|
||
}
|
||
catch (err) {
|
||
this.logger.error(`Got error in finalize ${err.message}`);
|
||
}
|
||
}), takeUntil(race(this.destroy$, gotNewHls$)))
|
||
.subscribe();
|
||
}
|
||
get publicQueries$() {
|
||
return this.publicQueriesInternal$.pipe(filter((queries) => Boolean(queries) && Boolean(queries[0]) && Boolean(queries[1])));
|
||
}
|
||
get _activeRootQuery() {
|
||
var _a;
|
||
const publicQueries = this.publicQueriesInternal$.value;
|
||
return (_a = publicQueries === null || publicQueries === void 0 ? void 0 : publicQueries[0]) !== null && _a !== void 0 ? _a : null;
|
||
}
|
||
get _mediaElementQuery() {
|
||
var _a;
|
||
const publicQueries = this.publicQueriesInternal$.value;
|
||
return (_a = publicQueries === null || publicQueries === void 0 ? void 0 : publicQueries[1]) !== null && _a !== void 0 ? _a : null;
|
||
}
|
||
static get version() {
|
||
return '2.162.2';
|
||
}
|
||
/**
|
||
* @type {HlsEvent}
|
||
*/
|
||
static get Events() {
|
||
return HlsEvent;
|
||
}
|
||
get Events() {
|
||
return Hls$1.Events;
|
||
}
|
||
/**
|
||
* @type {HlsConfig}
|
||
*/
|
||
static get DefaultConfig() {
|
||
return deepCpy(hlsDefaultConfig);
|
||
}
|
||
get DefaultConfig() {
|
||
return Hls$1.DefaultConfig;
|
||
}
|
||
/**
|
||
* @type {boolean}
|
||
*/
|
||
static isSupported() {
|
||
return isSupported();
|
||
}
|
||
commitEarlySelection(logger) {
|
||
const audioPersistentId = this.itemQueue.earlyAudioSelection;
|
||
if (isFiniteNumber(audioPersistentId)) {
|
||
logger.info(`use early audio selection ${audioPersistentId}`);
|
||
this.audioSelectedPersistentID = audioPersistentId;
|
||
this.itemQueue.earlyAudioSelection = null;
|
||
}
|
||
const subtitlePersistentId = this.itemQueue.earlySubtitleSelection;
|
||
if (isFiniteNumber(subtitlePersistentId)) {
|
||
logger.info(`use early subtitle selection ${subtitlePersistentId}`);
|
||
this.subtitleSelectedPersistentID = subtitlePersistentId;
|
||
this.itemQueue.earlySubtitleSelection = null;
|
||
}
|
||
}
|
||
/**
|
||
* Global error handler. If we've reached here, we've exhausted all other options
|
||
* and it's likely fatal. All errors should bubble up from the observables in the pipe
|
||
*/
|
||
_handleError(err) {
|
||
var _a;
|
||
try {
|
||
let errMessage = err.message;
|
||
{
|
||
errMessage += `\n${err.stack}`;
|
||
}
|
||
this.logger.error(`Got unhandled or fatal error ${errMessage}`, err);
|
||
(_a = this.rtcService) === null || _a === void 0 ? void 0 : _a.handleError(err);
|
||
let newError;
|
||
if (err instanceof HlsError) {
|
||
newError = err;
|
||
}
|
||
else {
|
||
newError = new ExceptionError(true, err.message, ErrorResponses.InternalError);
|
||
}
|
||
if (newError.fatal && this.isPreloading) {
|
||
this.logger.warn('Fatal error seen while preloading, calling dequeueSource');
|
||
this.dequeueSource('FatalErrorWhileLoading');
|
||
}
|
||
const shouldEscalateError = newError.fatal;
|
||
if (shouldEscalateError) {
|
||
this.logger.qe({ critical: true, name: 'fatalError', data: { msg: newError.message, stack: newError.stack } });
|
||
let triggerError$ = VOID;
|
||
if (this.mediaElementAdapter) {
|
||
const mediaQuery = this.mediaElementAdapter.mediaQuery;
|
||
const combinedBuffer = mediaQuery.getCombinedBufferInfo(mediaQuery.currentTime, 0);
|
||
if ((combinedBuffer === null || combinedBuffer === void 0 ? void 0 : combinedBuffer.len) > 0) {
|
||
this.logger.info(`playing to buffer end @${combinedBuffer.end}, pos=${mediaQuery.currentTime}`);
|
||
triggerError$ = waitFor(this.mediaElementAdapter.mediaQuery.stallInfo$, (stallInfo) => stallInfo != null).pipe(map(() => { }));
|
||
}
|
||
}
|
||
return triggerError$.pipe(switchMap(() => {
|
||
this.trigger(HlsEvent.ERROR, newError);
|
||
return EMPTY;
|
||
}));
|
||
}
|
||
this.trigger(HlsEvent.ERROR, newError);
|
||
}
|
||
catch (thrownError) {
|
||
this.logger.error(`Error thrown inside _handleError ${thrownError.message}`, thrownError);
|
||
throw thrownError;
|
||
}
|
||
return EMPTY;
|
||
}
|
||
updateLiveSeekableRange(rootPlaylistQuery, mediaSink) {
|
||
return rootPlaylistQuery.enabledMediaOptionByType$(MediaOptionType.Variant).pipe(switchMap((mediaOption) => {
|
||
const query = createMediaLibraryQuery(mediaOption);
|
||
let lastUpdate = 0;
|
||
// filter on entities that have loading set to false and have stats filled and have been updated
|
||
return query.mediaOptionDetailsEntity$.pipe(filterNullOrUndefined(), filter((entity) => {
|
||
var _a;
|
||
const retValue = entity.stats !== null && entity.detailsLoading === false && entity.lastUpdateMillis > lastUpdate;
|
||
lastUpdate = (_a = entity.lastUpdateMillis) !== null && _a !== void 0 ? _a : 0;
|
||
return retValue;
|
||
}));
|
||
}), switchMap((mediaDetailsEntity) => {
|
||
if (mediaDetailsEntity.unchangedCount === 0) {
|
||
if (mediaDetailsEntity.mediaOptionDetails.liveOrEvent) {
|
||
mediaSink.updateLiveSeekableRange(mediaDetailsEntity.mediaOptionDetails);
|
||
}
|
||
else {
|
||
mediaSink.clearLiveSeekableRange();
|
||
}
|
||
}
|
||
return EMPTY;
|
||
}));
|
||
}
|
||
playbackInfo(config, mediaQuery) {
|
||
var _a;
|
||
const video = this.mediaElement$.getValue();
|
||
if (!video) {
|
||
return;
|
||
}
|
||
const readyToPlay = video.readyState >= video.HAVE_FUTURE_DATA;
|
||
const playbackLikelyToKeepUp = mediaQuery.haveEnough && readyToPlay;
|
||
const playbackInfo = {
|
||
readyToPlay: readyToPlay,
|
||
playbackLikelyToKeepUp: playbackLikelyToKeepUp,
|
||
rate: video.playbackRate,
|
||
paused: video.paused,
|
||
position: video.currentTime,
|
||
duration: video.duration,
|
||
seekableTimeRanges: MediaElementHelper.timeRangeToArray(video.seekable),
|
||
loadedTimeRanges: MediaElementHelper.timeRangeToArray(video.buffered),
|
||
};
|
||
let droppedVideoFrames = 0, decodedFrameCount = 0;
|
||
if (MediaElementHelper.isHtmlVideoElement(video)) {
|
||
const videoPlaybackQuality = video.getVideoPlaybackQuality;
|
||
if (videoPlaybackQuality && typeof videoPlaybackQuality === typeof Function) {
|
||
const videoQuality = video.getVideoPlaybackQuality();
|
||
droppedVideoFrames = playbackInfo.droppedVideoFrames = videoQuality.droppedVideoFrames;
|
||
playbackInfo.corruptedVideoFrames = videoQuality.corruptedVideoFrames;
|
||
playbackInfo.totalVideoFrames = videoQuality.totalVideoFrames;
|
||
decodedFrameCount = playbackInfo.totalVideoFrames - droppedVideoFrames;
|
||
}
|
||
}
|
||
else if (MediaElementHelper.isWebkitMediaElement(video)) {
|
||
droppedVideoFrames = playbackInfo.droppedVideoFrames = video.webkitDroppedFrameCount;
|
||
decodedFrameCount = playbackInfo.decodedFrameCount = video.webkitDecodedFrameCount;
|
||
}
|
||
if (config.enablePerformanceLogging) {
|
||
this.logger.qe({ critical: true, name: 'playbackInfo', data: playbackInfo });
|
||
const [variantBufferInfo, altAudioBufferInfo] = mediaQuery.getCombinedMediaSourceBufferInfo(config.maxBufferHole);
|
||
this.logger.qe({ critical: true, name: 'bufferInfo', data: { variant: variantBufferInfo, altAudio: altAudioBufferInfo } });
|
||
}
|
||
(_a = this.rtcService) === null || _a === void 0 ? void 0 : _a.handlePlaybackInfo(droppedVideoFrames, decodedFrameCount);
|
||
}
|
||
get currentItem() {
|
||
if (this.isPreloading) {
|
||
this.logger.info('Currently preloading, returning playing item');
|
||
return this.playingItem;
|
||
}
|
||
return this.itemQueue.activeItem;
|
||
}
|
||
get realCurrentTime() {
|
||
var _a, _b;
|
||
const mediaQuery = this._mediaElementQuery;
|
||
if (!mediaQuery) {
|
||
return NaN;
|
||
}
|
||
// during trick-playback
|
||
if ((_a = this.iframeMachine) === null || _a === void 0 ? void 0 : _a.isStarted) {
|
||
const duration = mediaQuery.mediaElementDuration;
|
||
const ifct = this.iframeMachine.iframeClockTimeSeconds;
|
||
return ifct > duration ? duration : ifct;
|
||
}
|
||
// After trick-play is stopped, before seek to postFlushSeek, return postFlushSeek time until postFlushSeek is set to undefined when seek complete
|
||
let currentTime = isFiniteNumber(mediaQuery.postFlushSeek) ? mediaQuery.postFlushSeek : mediaQuery.currentTime;
|
||
// if the playing item is a finite number, remove it from real current time.
|
||
// No need to check inGaplessMode mode here because there situations where inGaplessMode is false
|
||
// but itemStartOffset is finite e.g. when playing the last song in an album
|
||
if (isFiniteNumber(currentTime) && isFiniteNumber((_b = this.playingItem) === null || _b === void 0 ? void 0 : _b.itemStartOffset)) {
|
||
currentTime -= this.playingItem.itemStartOffset;
|
||
}
|
||
return currentTime;
|
||
}
|
||
set realCurrentTime(value) {
|
||
var _a;
|
||
this.logger.info(`[seek] realCurrentTime ${value}`);
|
||
// if the playing item is a finite number, add it to the value.
|
||
// No need to check inGaplessMode mode here because there situations where inGaplessMode is false
|
||
// but itemStartOffset is finite e.g. when playing the last song in an album
|
||
if (isFiniteNumber((_a = this.playingItem) === null || _a === void 0 ? void 0 : _a.itemStartOffset)) {
|
||
value += this.playingItem.itemStartOffset;
|
||
}
|
||
this.seekTo = value;
|
||
}
|
||
get bufferedDuration() {
|
||
var _a;
|
||
const mediaQuery = this._mediaElementQuery;
|
||
return (_a = mediaQuery === null || mediaQuery === void 0 ? void 0 : mediaQuery.getBufferedDuration()) !== null && _a !== void 0 ? _a : 0;
|
||
}
|
||
get sessionData() {
|
||
const rootPlaylistQuery = this._activeRootQuery;
|
||
return rootPlaylistQuery === null || rootPlaylistQuery === void 0 ? void 0 : rootPlaylistQuery.sessionData;
|
||
}
|
||
get supportedFrameRates() {
|
||
const { enabled } = this.hlsConfig.trickPlaybackConfig;
|
||
const supported = [0, 1];
|
||
const rootQuery = this._activeRootQuery;
|
||
const libQuery = mediaLibraryService().getQuery();
|
||
if (enabled && rootQuery && libQuery.getEntity(rootQuery.itemId)) {
|
||
const isLive = libQuery.getEntity(rootQuery.itemId).liveOrEvent;
|
||
if (isLive === false) {
|
||
supported.push(8, 24, 48, 96);
|
||
}
|
||
}
|
||
return supported;
|
||
}
|
||
loadSource(url, itemOptions, initialSeekTime) {
|
||
var _a, _b, _c, _d;
|
||
// If Playready key system is being requested and if it's not enabled for the browser, reject loading the source
|
||
if (this.config.keySystemPreference === 'playready' && !this.config.enablePlayReadyKeySystem) {
|
||
throw new ExceptionError(true, 'Playready key system is not supported now', ErrorResponses.UnsupportedKeySystemError);
|
||
}
|
||
if (!url || !url.trim().length) {
|
||
throw new ExceptionError(true, 'Empty loadSource url', ErrorResponses.EmptyLoadSourceError);
|
||
}
|
||
// inherit protocol from href
|
||
url = URLToolkit$1.buildAbsoluteURL(window.location.href, url, { alwaysNormalize: true });
|
||
const logItemOptions = itemOptions &&
|
||
Object.keys(itemOptions)
|
||
.filter((key) => ['itemId', 'streamID'].indexOf(key) >= 0)
|
||
.reduce((newObj, key) => (key in itemOptions ? Object.assign(newObj, { [key]: itemOptions[key] }) : newObj), {});
|
||
this.logger.qe({ critical: true, name: 'loadSource', data: { url: redactUrl(url), itemOptions: logItemOptions, initialSeekTime } });
|
||
if ((_a = itemOptions === null || itemOptions === void 0 ? void 0 : itemOptions.appData) === null || _a === void 0 ? void 0 : _a.reportingAgent) {
|
||
this.reportingAgent = itemOptions.appData.reportingAgent;
|
||
}
|
||
if (itemOptions === null || itemOptions === void 0 ? void 0 : itemOptions.userInfo) {
|
||
this.userInfo = itemOptions.userInfo;
|
||
}
|
||
(_b = this.accessLogInstance) === null || _b === void 0 ? void 0 : _b.setupReporter(itemOptions.appData);
|
||
if (itemOptions === null || itemOptions === void 0 ? void 0 : itemOptions.platformInfo) {
|
||
this.platformService.updatePlatformInfo(itemOptions.platformInfo);
|
||
}
|
||
// Append queries specific to certain hosts
|
||
if (urlNeedsUpdate(url, this.config.enableQueryParamsForITunes)) {
|
||
const queryParameters = {
|
||
language: itemOptions.language,
|
||
dsid: itemOptions.dsid,
|
||
subs: itemOptions.subs,
|
||
};
|
||
url = updateUrlWithQueryStrings(url, itemOptions === null || itemOptions === void 0 ? void 0 : itemOptions.platformInfo, queryParameters);
|
||
itemOptions.inheritQuery = false; // don't inherit queries to subsequent requests
|
||
}
|
||
this.itemQueue.setQueueItem(`item:${(_c = itemOptions === null || itemOptions === void 0 ? void 0 : itemOptions.itemId) !== null && _c !== void 0 ? _c : guid()}`, url, initialSeekTime, itemOptions === null || itemOptions === void 0 ? void 0 : itemOptions.platformInfo, (_d = itemOptions === null || itemOptions === void 0 ? void 0 : itemOptions.appData) === null || _d === void 0 ? void 0 : _d.serviceName);
|
||
// clear this out. only makes sense for first asset
|
||
globalHlsService().setStartTime(undefined);
|
||
}
|
||
queueSource(url, itemOptions, initialSeekTime) {
|
||
var _a, _b, _c, _d;
|
||
this.logger.qe({ critical: true, name: 'queueSource', data: { url: redactUrl(url), itemOptions, initialSeekTime } });
|
||
if (itemOptions === null || itemOptions === void 0 ? void 0 : itemOptions.userInfo) {
|
||
this.userInfo = itemOptions.userInfo;
|
||
}
|
||
const combinedBuffer = (_a = this._mediaElementQuery) === null || _a === void 0 ? void 0 : _a.getCombinedBufferInfo((_b = this._mediaElementQuery) === null || _b === void 0 ? void 0 : _b.currentTime, 0);
|
||
let duration = 0;
|
||
if (combinedBuffer) {
|
||
duration = combinedBuffer.end;
|
||
}
|
||
this.logger.info(`queueSource ${redactUrl(url)} initialSeekTime:${initialSeekTime === null || initialSeekTime === void 0 ? void 0 : initialSeekTime.toFixed(3)}`);
|
||
this.logger.qe({ critical: true, name: 'gapless', data: { itemLoadingAt: duration } });
|
||
this.itemQueue.addQueueItem(`item:${(_c = itemOptions === null || itemOptions === void 0 ? void 0 : itemOptions.itemId) !== null && _c !== void 0 ? _c : guid()}`, url, initialSeekTime, itemOptions === null || itemOptions === void 0 ? void 0 : itemOptions.platformInfo, duration, (_d = itemOptions === null || itemOptions === void 0 ? void 0 : itemOptions.appData) === null || _d === void 0 ? void 0 : _d.serviceName);
|
||
}
|
||
dequeueSource(reason = 'ApplicationInitiated') {
|
||
// if we are not preloading and this is the first item and it's invalid format, disallow gapless
|
||
// but do not evict the item. It's probably a video
|
||
if (!this.isPreloading && reason === 'InvalidFormat' && this.isFirstItem) {
|
||
this.logger.error('First item has invalid format for gapless. Probably video. Disabling gapless.');
|
||
this.gaplessCapable = false;
|
||
return;
|
||
}
|
||
if (!this.isPreloading) {
|
||
this.logger.warn(`Nothing to dequeue, no item is preloading dequeue reason: ${reason}`);
|
||
return;
|
||
}
|
||
this.logger.qe({ critical: true, name: 'gapless', data: { event: 'clean up current source' } });
|
||
// Store loading item information
|
||
const itemToEvictUrl = this.loadingItem.url;
|
||
const itemToEvictId = this.loadingItem.itemId;
|
||
// flush all data from the loading item and set media sink duration
|
||
this.mediaElementAdapter.flushData(SourceBufferType.Variant, this.loadingItem.itemStartOffset, Infinity);
|
||
this.mediaElementAdapter.msDuration = this.loadingItem.itemStartOffset;
|
||
// clear loading item in queue and make playing item active.
|
||
this.itemQueue.resetLoadingItem();
|
||
// Disallow gapless on invalidFormat and FatalErrorWhileLoading
|
||
if (reason === 'InvalidFormat' || reason === 'FatalErrorWhileLoading') {
|
||
// Disallow gapless
|
||
this.gaplessCapable = false;
|
||
}
|
||
// Send out item evicted event
|
||
this.triggerItemEvicted({ url: itemToEvictUrl, itemId: itemToEvictId }, reason);
|
||
}
|
||
triggerItemEvicted(itemToEvict, reason) {
|
||
if (itemToEvict === null) {
|
||
this.logger.error('dequeueSource called with no playing or loading item');
|
||
return;
|
||
}
|
||
const data = { url: itemToEvict.url, evictedItemId: itemToEvict.itemId, reason };
|
||
const loggableData = Object.assign(Object.assign({}, data), { url: redactUrl(itemToEvict.url) });
|
||
this.logger.info('Item evicted evictedData: %o', loggableData);
|
||
this.trigger(HlsEvent.ITEM_EVICTED, data);
|
||
}
|
||
endSource() {
|
||
// When endSource is called, this hls instance is no longer gapless capable.
|
||
this.gaplessCapable = false;
|
||
this.logger.info('[gapless] end source');
|
||
// if preloading clear buffers and remove loading item
|
||
if (this.isPreloading) {
|
||
this.logger.warn('EndSource called during preloading. Loading item will be removed');
|
||
// flush all data from the loading item and set media sink duration
|
||
this.mediaElementAdapter.flushData(SourceBufferType.Variant, this.loadingItem.itemStartOffset, Infinity);
|
||
this.mediaElementAdapter.msDuration = this.loadingItem.itemStartOffset;
|
||
// clear loading item in queue and make playing item active.
|
||
this.itemQueue.resetLoadingItem();
|
||
}
|
||
}
|
||
get inGaplessMode() {
|
||
//return this.config.gapless; //&& figure out who should know if gapless is allowed... item OR rootPlaylistService
|
||
// Don't call this.config because it does deepCpy and is deprecated
|
||
return getCurrentConfig().gapless && this.gaplessCapable;
|
||
}
|
||
get isPreloading() {
|
||
return this.itemQueue.isPreloading();
|
||
}
|
||
get isFirstItem() {
|
||
return this.itemQueue.isFirstItem;
|
||
}
|
||
get loadingItem() {
|
||
return this.itemQueue.loadingItem;
|
||
}
|
||
get playingItem() {
|
||
return this.itemQueue.playingItem;
|
||
}
|
||
get url() {
|
||
if (this.playingItem) {
|
||
return this.playingItem.url;
|
||
}
|
||
if (this.loadingItem) {
|
||
return this.loadingItem.url;
|
||
}
|
||
return undefined;
|
||
}
|
||
destroy() {
|
||
const { logger } = this;
|
||
logger.info('destroy');
|
||
// See finalize in Hls constructor for what happens on destroy$
|
||
this.destroy$.next();
|
||
if (this.rpcService != null) {
|
||
this.teardownWG$.add();
|
||
this.rpcService.teardown((err) => {
|
||
if (err) {
|
||
logger.error('RPCService teardown error:', err);
|
||
}
|
||
this.teardownWG$.done();
|
||
});
|
||
this.rpcService = null;
|
||
}
|
||
if (this.iframeMachine != null) {
|
||
this.iframeMachine.destroy();
|
||
this.iframeMachine = null;
|
||
}
|
||
return this.teardownWG$.toPromise();
|
||
}
|
||
attachMedia(mediaElement) {
|
||
this.logger.info('attachMedia');
|
||
this.trigger(HlsEvent.MEDIA_ATTACHING, { media: mediaElement });
|
||
this.mediaElement$.next(mediaElement);
|
||
this.trigger(HlsEvent.MEDIA_ATTACHED, { media: mediaElement });
|
||
}
|
||
detachMedia() {
|
||
var _a;
|
||
if (!this.mediaElement$.getValue()) {
|
||
this.logger.info('detachMedia called with no media element to detach');
|
||
return;
|
||
}
|
||
this.logger.info('detachMedia');
|
||
this.trigger(HlsEvent.MEDIA_DETACHING);
|
||
(_a = this.rtcService) === null || _a === void 0 ? void 0 : _a.detachMedia();
|
||
if (this.iframeMachine != null) {
|
||
this.iframeMachine.stop();
|
||
}
|
||
this.mediaElement$.next(null);
|
||
this.trigger(HlsEvent.MEDIA_DETACHED);
|
||
}
|
||
handleResolvedUri(originalURI, response) {
|
||
this.customUrlLoader.setCustomUrlResponse(originalURI, { uri: response.uri, response: response });
|
||
}
|
||
get variantOptions$() {
|
||
return this.publicQueries$.pipe(switchMap((queries) => {
|
||
const [rootQuery, meQuery] = queries;
|
||
return combineLatest([rootQuery.preferredMediaOptions$, meQuery.desiredRate$]).pipe(map(([rootTuple]) => {
|
||
const iframeMode = meQuery.isIframeRate;
|
||
return rootTuple[MediaOptionType.Variant].filter((option) => { var _a; return ((_a = option.iframes) !== null && _a !== void 0 ? _a : false) === iframeMode; }).map((option) => option.mediaOptionId);
|
||
}));
|
||
}));
|
||
}
|
||
get altAudioOptions$() {
|
||
return this.publicQueries$.pipe(switchMap((queries) => {
|
||
const [rootQuery] = queries;
|
||
return of(rootQuery.audioMediaSelectionOptions);
|
||
}));
|
||
}
|
||
get subtitleOptions$() {
|
||
return this.publicQueries$.pipe(switchMap((queries) => {
|
||
const [rootQuery] = queries;
|
||
const disableOption = [
|
||
{
|
||
MediaSelectionOptionsName: 'Disable subtitle',
|
||
MediaSelectionOptionsPersistentID: -1,
|
||
},
|
||
];
|
||
return of(disableOption.concat(rootQuery.subtitleMediaSelectionOptions));
|
||
}));
|
||
}
|
||
// Old public API used by webapp
|
||
get levels() {
|
||
var _a, _b;
|
||
return (_b = (_a = this._activeRootQuery) === null || _a === void 0 ? void 0 : _a.preferredMediaOptions[MediaOptionType.Variant]) !== null && _b !== void 0 ? _b : [];
|
||
}
|
||
// Old public API used by webapp
|
||
get audioTracks() {
|
||
var _a, _b;
|
||
return (_b = (_a = this._activeRootQuery) === null || _a === void 0 ? void 0 : _a.preferredMediaOptions[MediaOptionType.AltAudio]) !== null && _b !== void 0 ? _b : [];
|
||
}
|
||
get audioMediaOptions() {
|
||
var _a, _b;
|
||
return (_b = (_a = this._activeRootQuery) === null || _a === void 0 ? void 0 : _a.audioMediaSelectionOptions) !== null && _b !== void 0 ? _b : [];
|
||
}
|
||
get subtitleMediaOptions() {
|
||
var _a, _b;
|
||
return (_b = (_a = this._activeRootQuery) === null || _a === void 0 ? void 0 : _a.subtitleMediaSelectionOptions) !== null && _b !== void 0 ? _b : [];
|
||
}
|
||
/**
|
||
* @returns whether we are likely to keep up based on network and buffer
|
||
*/
|
||
get playbackLikelyToKeepUp() {
|
||
var _a, _b;
|
||
return (_b = (_a = this._mediaElementQuery) === null || _a === void 0 ? void 0 : _a.playbackLikelyToKeepUp) !== null && _b !== void 0 ? _b : false;
|
||
}
|
||
get duration$() {
|
||
return this.publicQueries$.pipe(switchMap((queries) => {
|
||
const [, mediaQuery] = queries;
|
||
return mediaQuery.mediaElementDuration$;
|
||
}));
|
||
}
|
||
get timeupdate$() {
|
||
return this.publicQueries$.pipe(switchMap((queries) => {
|
||
const [, mediaQuery] = queries;
|
||
return mediaQuery.timeupdate$;
|
||
}));
|
||
}
|
||
get playing$() {
|
||
return this.publicQueries$.pipe(switchMap((queries) => {
|
||
const [, mediaQuery] = queries;
|
||
return mediaQuery.gotPlaying$;
|
||
}));
|
||
}
|
||
get desiredRate$() {
|
||
return this.publicQueries$.pipe(switchMap((queries) => {
|
||
const [, mediaQuery] = queries;
|
||
return mediaQuery.desiredRate$;
|
||
}));
|
||
}
|
||
set desiredRate(desiredRate) {
|
||
if (desiredRate == null) {
|
||
return;
|
||
}
|
||
this.setRate(desiredRate);
|
||
}
|
||
get desiredRate() {
|
||
var _a, _b;
|
||
return (_b = (_a = this._mediaElementQuery) === null || _a === void 0 ? void 0 : _a.desiredRate) !== null && _b !== void 0 ? _b : 0;
|
||
}
|
||
get effectiveRate() {
|
||
var _a, _b;
|
||
return (_b = (_a = this._mediaElementQuery) === null || _a === void 0 ? void 0 : _a.effectiveRate) !== null && _b !== void 0 ? _b : 0;
|
||
}
|
||
get iframeMode() {
|
||
var _a, _b;
|
||
return (_b = (_a = this._mediaElementQuery) === null || _a === void 0 ? void 0 : _a.isIframeRate) !== null && _b !== void 0 ? _b : false;
|
||
}
|
||
get accessLog() {
|
||
return this.accessLogInstance && this._activeRootQuery ? this.accessLogInstance.getAccessLog(this._activeRootQuery.itemId) : [];
|
||
}
|
||
get errorLog() {
|
||
return this.accessLogInstance ? this.accessLogInstance.errorLog : [];
|
||
}
|
||
setRate(newRate) {
|
||
var _a;
|
||
const Errors = { UNABLE_TO_SWITCH: -1, ALREADY_IN_RATE: -2, UNSUPPORTED_RATE: -3 };
|
||
const logger = this.logger.child({ name: 'iframes' });
|
||
const oldRate = this.desiredRate;
|
||
if (newRate === oldRate) {
|
||
return Errors.ALREADY_IN_RATE;
|
||
}
|
||
logger.info(`setRate ${oldRate} -> ${newRate}`);
|
||
const mediaSink = this.mediaElementAdapter;
|
||
if (!mediaSink || isNaN(newRate)) {
|
||
logger.warn('unable to switch to rate, missing adapter or newRate isNaN');
|
||
return Errors.UNABLE_TO_SWITCH;
|
||
}
|
||
newRate = Number(newRate);
|
||
const absRate = Math.abs(newRate);
|
||
const isSupportedRate = this.supportedFrameRates.some((rate) => rate === absRate);
|
||
if (!isSupportedRate) {
|
||
logger.warn(`unsupported rate(${newRate})`);
|
||
return Errors.UNSUPPORTED_RATE;
|
||
}
|
||
const iframeRate = isIframeRate(newRate);
|
||
const iframeMachine = this.iframeMachine;
|
||
if (iframeRate) {
|
||
const rootQuery = this._activeRootQuery;
|
||
if (!(rootQuery === null || rootQuery === void 0 ? void 0 : rootQuery.mediaOptionListQueries[MediaOptionType.Variant].hasIframes)) {
|
||
logger.warn('no iframe variants available');
|
||
return Errors.UNABLE_TO_SWITCH;
|
||
}
|
||
mediaSink.postFlushSeek = null;
|
||
}
|
||
else if ((iframeMachine === null || iframeMachine === void 0 ? void 0 : iframeMachine.isStarted) && !isFiniteNumber((_a = mediaSink.mediaQuery) === null || _a === void 0 ? void 0 : _a.postFlushSeek)) {
|
||
iframeMachine.pause();
|
||
logger.info(`resuming from trick-play postFlushSeek=${iframeMachine.iframeClockTimeSeconds}`);
|
||
mediaSink.postFlushSeek = iframeMachine.iframeClockTimeSeconds;
|
||
}
|
||
mediaSink.desiredRate = newRate;
|
||
return 0;
|
||
}
|
||
get sessionData$() {
|
||
return this.publicQueries$.pipe(switchMap(([rootPlaylistQuery]) => {
|
||
return rootPlaylistQuery.sessionData$;
|
||
}));
|
||
}
|
||
set skip(skip) {
|
||
this.logger.info(`skip=${skip}`);
|
||
if (this._mediaElementQuery) {
|
||
this.logger.info(`[seek] skip ${skip}`);
|
||
this.realCurrentTime = Math.max(0, this.realCurrentTime + skip);
|
||
}
|
||
}
|
||
// Helper function for seeking when in gapless mode
|
||
gaplessSeekTo(seekTo) {
|
||
// Adjust seekTo Value if needed
|
||
this.logger.debug(`seekTo before gapless adjust: ${seekTo}, startOffset: ${this.playingItem.itemStartOffset}`);
|
||
if (seekTo < this.playingItem.itemStartOffset) {
|
||
this.logger.warn(`[Gapless] Seeking past track boundary oldSeek=${seekTo}, adjustedSeek=${this.playingItem.itemStartOffset}`);
|
||
seekTo = this.playingItem.itemStartOffset;
|
||
}
|
||
if (this.isPreloading) {
|
||
if (seekTo > this.loadingItem.itemStartOffset) {
|
||
this.logger.warn(`[Gapless] Seeking past track boundary oldSeek=${seekTo}, adjustedSeek=${this.loadingItem.itemStartOffset}`);
|
||
seekTo = this.loadingItem.itemStartOffset;
|
||
}
|
||
const buf = this._mediaElementQuery.getBufferInfo(this._mediaElementQuery.currentTime, this.config.maxBufferHole);
|
||
this.logger.info('Hls seekTo during preloading currentTime:%d seekTo:%d Buf:%o', this.realCurrentTime, seekTo, buf);
|
||
if (seekTo < buf[0].buffered.start) {
|
||
this.dequeueSource('SeekToUnbufferedTimeRanges');
|
||
}
|
||
}
|
||
globalHlsService().setUserSeek(seekTo);
|
||
}
|
||
isIframeInternalSeek(seekTo) {
|
||
var _a;
|
||
return seekTo === ((_a = this.iframeMachine) === null || _a === void 0 ? void 0 : _a.mediaRootTime);
|
||
}
|
||
set seekTo(seekValue) {
|
||
var _a;
|
||
const seekTo = Number(seekValue);
|
||
if (!isFiniteNumber(seekTo)) {
|
||
this.logger.error(`[seek] got invalid seek value ${seekValue}`);
|
||
return;
|
||
}
|
||
this.logger.info(`[seek] seekTo=${seekTo}`);
|
||
if (this.inGaplessMode) {
|
||
this.gaplessSeekTo(seekTo);
|
||
return;
|
||
}
|
||
// After trick-play is stopped, before seek to postFlushSeek, update the post flush resume time
|
||
const mediaSink = this.mediaElementAdapter;
|
||
if (mediaSink && isFiniteNumber((_a = mediaSink.mediaQuery) === null || _a === void 0 ? void 0 : _a.postFlushSeek) && (!mediaSink.mediaQuery.seekTo || this.isIframeInternalSeek(mediaSink.mediaQuery.seekTo.pos))) {
|
||
this.logger.info(`[seek] clearing seekTo ${JSON.stringify(mediaSink.mediaQuery.seekTo)} and resuming from trick-play postFlushSeek=${seekTo}`);
|
||
mediaSink.schedulePostFlushSeek(seekTo);
|
||
return;
|
||
}
|
||
// Store seek in userSeek to be processed by userSeekEpic
|
||
globalHlsService().setUserSeek(seekTo);
|
||
}
|
||
/**
|
||
* Seek to a date. Only valid for playlists with PROGRAM-DATE-TIME tags
|
||
* @param searchDate The date to seek to
|
||
*/
|
||
seekToDate(searchDate) {
|
||
// Note: Airplay allows seekToDate() to occur before loadSource() so we must store it at Hls level
|
||
this.logger.info(`[seek] seekToDate=${searchDate.toISOString()}`);
|
||
globalHlsService().setUserSeek(searchDate);
|
||
}
|
||
/**
|
||
* @returns a map of Date (ms) to time in media element (s)
|
||
*/
|
||
get availableProgramDateTime() {
|
||
return new Map(this._currentDateToMediaTimeTuple);
|
||
}
|
||
get _currentDateToMediaTimeTuple() {
|
||
var _a, _b;
|
||
if (!this._activeRootQuery) {
|
||
return [];
|
||
}
|
||
const curVariant = this._activeRootQuery.enabledMediaOptionKeys[MediaOptionType.Variant];
|
||
if (!isEnabledMediaOption(curVariant)) {
|
||
return [];
|
||
}
|
||
const libQuery = mediaLibraryService().getQueryForOption(curVariant);
|
||
return (_b = (_a = libQuery.mediaOptionDetails) === null || _a === void 0 ? void 0 : _a.dateMediaTimePairs) !== null && _b !== void 0 ? _b : [];
|
||
}
|
||
/**
|
||
* Convert the playing time into equivalent DateTime
|
||
*/
|
||
get playingDate() {
|
||
return resolvePTSToDate(this._currentDateToMediaTimeTuple, this.realCurrentTime);
|
||
}
|
||
/**
|
||
* Manually set variantID. Only in development mode
|
||
*/
|
||
set variantId(variantId) {
|
||
{
|
||
const rootQuery = this._activeRootQuery;
|
||
if (!rootQuery) {
|
||
return;
|
||
}
|
||
this.logger.info(`variantId=${variantId}`);
|
||
const itemId = rootQuery.itemId;
|
||
const mediaOption = rootQuery.variantMediaOptionById(variantId);
|
||
this.rootPlaylistService.setManualMode(itemId, true);
|
||
this.rootPlaylistService.setEnabledMediaOptionByType(itemId, MediaOptionType.Variant, mediaOption);
|
||
}
|
||
}
|
||
set audioSelectedPersistentID(persistentID) {
|
||
const rootQuery = this._activeRootQuery;
|
||
const audioMediaOptions = rootQuery === null || rootQuery === void 0 ? void 0 : rootQuery.preferredMediaOptions[MediaOptionType.AltAudio];
|
||
if (audioMediaOptions) {
|
||
const itemId = rootQuery.itemId;
|
||
if (persistentID === this.audioSelectedPersistentID) {
|
||
this.logger.info('skipping set audioSelectedPersistentID, id is same. PersistentID = %d', persistentID);
|
||
return;
|
||
}
|
||
this.rootPlaylistService.setEnabledMediaOptionTupleWithMatchedGroups(itemId, MediaOptionType.AltAudio, persistentID, { userInitiated: true });
|
||
}
|
||
else {
|
||
// MatchPoint spams audioSelectedPersistentID(-1) during startup (when hls is not ready)
|
||
// hls 1.0 - 2.0 ignores them explicitly.
|
||
// Otherwise, they may override a valid selection.
|
||
if (!isFiniteNumber(persistentID) || persistentID < 0) {
|
||
this.logger.info(`ignore early invalid audio selection: ${persistentID}`);
|
||
return;
|
||
}
|
||
this.logger.warn(`[audio] no active item, defer audio track selection: persistentId ${persistentID}`);
|
||
this.itemQueue.earlyAudioSelection = persistentID;
|
||
}
|
||
}
|
||
get audioSelectedPersistentID() {
|
||
var _a;
|
||
if (this._activeRootQuery) {
|
||
return (_a = this._activeRootQuery.enabledAlternateMediaOptionByType(MediaOptionType.AltAudio)) === null || _a === void 0 ? void 0 : _a.persistentID;
|
||
}
|
||
else {
|
||
return this.itemQueue.earlyAudioSelection;
|
||
}
|
||
}
|
||
/**
|
||
* @param persistentID = the persistentId
|
||
*/
|
||
set subtitleSelectedPersistentID(persistentID) {
|
||
const rootQuery = this._activeRootQuery;
|
||
const subtitleMediaOptions = rootQuery === null || rootQuery === void 0 ? void 0 : rootQuery.preferredMediaOptions[MediaOptionType.Subtitle];
|
||
if (subtitleMediaOptions) {
|
||
if (persistentID === this.subtitleSelectedPersistentID) {
|
||
this.logger.info('skipping set subtitleSelectedPersistentID, id is same. PersistentID = %d', persistentID);
|
||
return;
|
||
}
|
||
const itemId = rootQuery.itemId;
|
||
if (subtitleMediaOptions.length === 0 && (!isFiniteNumber(persistentID) || persistentID < 0)) {
|
||
this.logger.info(`ignore early invalid subtitle selection ${persistentID}; no subtitle media options yet`);
|
||
return;
|
||
}
|
||
this.logger.info(`subtitleSelectedPersistentID ${persistentID}`);
|
||
if (!isFiniteNumber(persistentID) || persistentID === -1) {
|
||
// Disable subtitles
|
||
this.rootPlaylistService.setEnabledMediaOptionByType(itemId, MediaOptionType.Subtitle, NoMediaOption);
|
||
}
|
||
else {
|
||
this.rootPlaylistService.setEnabledMediaOptionTupleWithMatchedGroups(itemId, MediaOptionType.Subtitle, persistentID);
|
||
}
|
||
}
|
||
else {
|
||
// MatchPoint spams subtitleSelectedPersistentID(-1) during startup (when hls is not ready)
|
||
// hls 1.0 - 2.0 ignores them explicitly.
|
||
// Otherwise, they may override a valid selection.
|
||
if (!isFiniteNumber(persistentID) || persistentID < 0) {
|
||
this.logger.info(`ignore early invalid subtitle selection ${persistentID}`);
|
||
return;
|
||
}
|
||
// cache valid selection
|
||
this.logger.warn(`[subtitle] no active item, defer subtitle track selection: persistentId ${persistentID}`);
|
||
this.itemQueue.earlySubtitleSelection = persistentID;
|
||
}
|
||
}
|
||
/**
|
||
* @returns the persistentID of the enabled subtitle
|
||
*/
|
||
get subtitleSelectedPersistentID() {
|
||
var _a;
|
||
if (this._activeRootQuery) {
|
||
return (_a = this._activeRootQuery.enabledAlternateMediaOptionByType(MediaOptionType.Subtitle)) === null || _a === void 0 ? void 0 : _a.persistentID;
|
||
}
|
||
else {
|
||
return this.itemQueue.earlySubtitleSelection;
|
||
}
|
||
}
|
||
/**
|
||
* Get array of selected tracks
|
||
*/
|
||
get selectedMediaArray() {
|
||
const rootQuery = this._activeRootQuery;
|
||
if (!rootQuery) {
|
||
return [];
|
||
}
|
||
const selectedMediaArray = [];
|
||
const audioMediaOption = rootQuery.enabledAlternateMediaOptionByType(MediaOptionType.AltAudio);
|
||
const subtitleMediaOption = rootQuery.enabledAlternateMediaOptionByType(MediaOptionType.Subtitle);
|
||
const audioSelectedMediaOption = audioMediaOption ? rootQuery.audioMediaSelectionOptions.find((option) => option.MediaSelectionOptionsPersistentID === audioMediaOption.persistentID) : undefined;
|
||
const subtitleSelectedMediaOption = subtitleMediaOption
|
||
? rootQuery.subtitleMediaSelectionOptions.find((option) => option.MediaSelectionOptionsPersistentID === subtitleMediaOption.persistentID)
|
||
: undefined;
|
||
if (audioSelectedMediaOption) {
|
||
const selectedItem = {
|
||
MediaSelectionGroupMediaType: MediaTypeFourCC.AUDIO,
|
||
MediaSelectionOptionsPersistentID: audioSelectedMediaOption.MediaSelectionOptionsPersistentID,
|
||
};
|
||
selectedMediaArray.push(selectedItem);
|
||
}
|
||
if (subtitleSelectedMediaOption) {
|
||
let showNonForcedSubtitles = Allowed.NO;
|
||
if (subtitleSelectedMediaOption.MediaSelectionOptionsDisplaysNonForcedSubtitles) {
|
||
showNonForcedSubtitles = subtitleSelectedMediaOption.MediaSelectionOptionsDisplaysNonForcedSubtitles;
|
||
}
|
||
const selectedItem = {
|
||
MediaSelectionGroupMediaType: MediaTypeFourCC.SUBTITLE,
|
||
MediaSelectionOptionsDisplaysNonForcedSubtitles: showNonForcedSubtitles,
|
||
MediaSelectionOptionsPersistentID: subtitleSelectedMediaOption.MediaSelectionOptionsPersistentID,
|
||
};
|
||
selectedMediaArray.push(selectedItem);
|
||
}
|
||
this.logger.debug(`get selectedMediaArray: ${JSON.stringify(selectedMediaArray)}`);
|
||
return selectedMediaArray;
|
||
}
|
||
set selectedMediaArray(mediaArray) {
|
||
if (this._activeRootQuery) {
|
||
this.logger.debug(`selectedMediaArray=${JSON.stringify(mediaArray)}`);
|
||
mediaArray.forEach((selectedItem) => {
|
||
if (selectedItem.MediaSelectionGroupMediaType === MediaTypeFourCC.AUDIO || selectedItem.MediaSelectionOptionsMediaType === MediaTypeFourCC.AUDIO) {
|
||
this.audioSelectedPersistentID = selectedItem.MediaSelectionOptionsPersistentID;
|
||
}
|
||
else if (selectedItem.MediaSelectionGroupMediaType === MediaTypeFourCC.SUBTITLE ||
|
||
selectedItem.MediaSelectionOptionsMediaType === MediaTypeFourCC.SUBTITLE ||
|
||
selectedItem.MediaSelectionOptionsMediaType === MediaTypeFourCC.CLOSEDCAPTION) {
|
||
this.subtitleSelectedPersistentID = selectedItem.MediaSelectionOptionsPersistentID;
|
||
}
|
||
});
|
||
}
|
||
else {
|
||
this.logger.warn('selectedMediaArray: no active item');
|
||
}
|
||
}
|
||
getHTMLTextTrack(subtitleTrackId) {
|
||
return this.legibleSystemAdapter.getExistingHTMLTextTrackWithSubtitleTrackId(subtitleTrackId);
|
||
}
|
||
get keysystems() {
|
||
return this.keySystemAdapter.availableKeySystems;
|
||
}
|
||
setProtectionData(data) {
|
||
this.keySystemAdapter.initialize(data);
|
||
}
|
||
generateKeyRequest(keyuri, requestInfo) {
|
||
this.keySystemAdapter.generateRequest(keyuri, requestInfo);
|
||
this.rtcService.licenseChallengeReceived({ keyuri: keyuri });
|
||
}
|
||
setLicenseResponse(keyuri, response) {
|
||
this.keySystemAdapter.setLicenseResponse(keyuri, response);
|
||
}
|
||
get bufferInfo$() {
|
||
return this.publicQueries$.pipe(switchMap((queries) => {
|
||
const [, mediaQuery] = queries;
|
||
const config = createHlsQuery().currentConfig;
|
||
return merge(mediaQuery.timeupdate$, mediaQuery.bufferedRangeTuple$).pipe(throttleTime(1000), map(() => {
|
||
const pos = mediaQuery.currentTime;
|
||
return {
|
||
combined: mediaQuery.getCombinedBufferInfo(pos, config.maxBufferHole),
|
||
sbTuple: mediaQuery.getBufferInfo(pos, config.maxBufferHole),
|
||
};
|
||
}));
|
||
}));
|
||
}
|
||
bufferInfoByType$(type) {
|
||
return this.bufferInfo$.pipe(map((bufInfoTuple) => {
|
||
var _a;
|
||
return (_a = bufInfoTuple === null || bufInfoTuple === void 0 ? void 0 : bufInfoTuple.sbTuple) === null || _a === void 0 ? void 0 : _a[type];
|
||
}));
|
||
}
|
||
/**
|
||
* DEPRECATED
|
||
*/
|
||
levelWithPersistentId(levelId) {
|
||
this.logger.warn('levelWithPersistentId is deprecated');
|
||
}
|
||
/**
|
||
* DEPRECATED
|
||
*/
|
||
startLoad(startTimeSec) {
|
||
this.logger.warn('startLoad is deprecated');
|
||
// Hack for Vuze. remove when they adopt initialSeekTime API.
|
||
if (isFiniteNumber(startTimeSec)) {
|
||
this.logger.warn(`[seek] Seeking to ${startTimeSec === null || startTimeSec === void 0 ? void 0 : startTimeSec.toFixed(3)} via deprecated "startLoad" method. Use loadSource(url, options, startTime) instead.`);
|
||
this.seekTo = startTimeSec;
|
||
}
|
||
}
|
||
/**
|
||
* DEPRECATED
|
||
*/
|
||
stopLoad() { }
|
||
/**
|
||
* DEPRECATED
|
||
* @returns current config
|
||
*/
|
||
get config() {
|
||
return Object.assign(Object.assign({}, deepCpy(getCurrentConfig())), { set startPosition(startTimeSec) {
|
||
// Hack for airplay. remove when they adopt initialSeekTime API
|
||
getLogger().warn(`Setting start position ${startTimeSec === null || startTimeSec === void 0 ? void 0 : startTimeSec.toFixed(3)} using deprecated method`);
|
||
globalHlsService().setStartTime(startTimeSec);
|
||
} });
|
||
}
|
||
/**
|
||
* DEPRECATED
|
||
* @returns if media is attached
|
||
*/
|
||
get media() {
|
||
return this.mediaElement$.value != null;
|
||
}
|
||
/**
|
||
* DEPRECATED
|
||
*/
|
||
set subtitleDisplay(subtitleDisplay) {
|
||
this.logger.warn(`set subtitleDisplay ${subtitleDisplay} is deprecated`);
|
||
}
|
||
}
|
||
|
||
var Hls = Hls$1;
|
||
|
||
return Hls;
|
||
|
||
}));
|
||
})(false); |