/*! 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: 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: \0 */ 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 * 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 ? '' : 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 || ''; 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 { * 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 { } * * @StoreConfig({ name: 'widgets' }) * export class WidgetsStore extends EntityStore { * 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 {} * * export class ProductsStore EntityStore { * ui: EntityUIStore; * * 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 { * 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 { * ui: EntityUIQuery; * * 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 * * 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;s0;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=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;n0);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;c1?e-1:0),r=1;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);n1?e-1:0),r=1;r1?n-1:0),o=1;o1?n-1:0),o=1;o1&&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;e1&&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;t0&&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>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>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';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;n0&&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;ne;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;ie;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 is non-empty, we skip to // Step 7. Otherwise, the embedded URL inherits the // (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 is non-empty, we skip to // step 7; otherwise, it inherits the of the base // URL (if any) and if (!relativeParts.params) { builtParts.params = baseParts.params; // 5b) if the embedded URL's is non-empty, we skip to // step 7; otherwise, it inherits the 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 "/../", where 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 "/..", where is a // complete path segment not equal to "..", that // "/.." 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, // deprecated in favor of rxjs fetch // loader: FetchLoader, // deprecated in favor of rxjs fetch fLoader: void 0, pLoader: void 0, xhrSetup: void 0, // fetchSetup: undefined, // abrController: AbrController, // deprecated in favor or redux/rxjs // bufferController: BufferController, // deprecated in favor or redux/rxjs // #if altaudio // audioStreamController: AudioStreamController, // deprecated in favor or redux/rxjs // audioTrackController: AudioTrackController, // deprecated in favor or redux/rxjs iframeMaxExitSeekDuration: 2000, iframeStallMaxRetry: 5, audioPrimingDelay: 0, // #endif // #if subtitle // subtitleStreamController: SubtitleStreamController, // deprecated in favor or redux/rxjs // subtitleTrackController: SubtitleTrackController, // deprecated in favor or redux/rxjs // timelineController: TimelineController, // 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 // 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:[ 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} 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} 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} 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 // // // base64Challenge // // // 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} 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 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() 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(//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 += ''; 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 += ''; context.styleStack.splice(totalStyleCount - 1 - childrenStyleCount); break; } else { if (styleTag[0] === 'c') { context.background = ''; context.foreground = ''; context.flash = false; closingChildrenStyles += ''; } else if (styleTag[0] === 'u') { context.underline = false; closingChildrenStyles += ''; } else if (styleTag[0] === 'i') { context.italics = false; closingChildrenStyles += ''; } 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]* ${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: '); 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(); * public destroyWG = new WaitGroup(); * constructor() { * observableWithAsyncTeardown(this).pipe( * takeUntil(this.destroy$) // chain its lifecycle to Hls * ).subscribe(); * } * public destroy(): Promise { * 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((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: 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) { // 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 : 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; } // 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} 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} 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)); // 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} 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} 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, }; // Do not use max values for this estimate const fragEstimate = statsQuery.getFragEstimate(); const bufferEstimate = { avgBufferCreateMs: 0, avgInitFragAppendMs: 0, avgDataFragAppendMs: 0, }; const fragDuration = fragEstimate.maxDurationSec; // 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 // 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: 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: 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; // 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))) { // 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(); // (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 }); } // 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 <- 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)); // 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);