1"use strict"; 2(self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([[6306],{ 3 4/***/ 28276: 5/***/ (function(__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) { 6 7/* harmony export */ __webpack_require__.d(__webpack_exports__, { 8/* harmony export */ ZP: function() { return /* binding */ Hls; } 9/* harmony export */ }); 10/* unused harmony exports AbrController, AttrList, AudioStreamController, AudioTrackController, BasePlaylistController, BaseSegment, BaseStreamController, BufferController, CMCDController, CapLevelController, ChunkMetadata, ContentSteeringController, Cues, DateRange, EMEController, ErrorActionFlags, ErrorController, ErrorDetails, ErrorTypes, Events, FPSController, FetchLoader, Fragment, Hls, HlsSkip, HlsUrlParameters, KeySystemFormats, KeySystems, Level, LevelDetails, LevelKey, LoadStats, M3U8Parser, MetadataSchema, NetworkErrorAction, Part, PlaylistLevelType, SubtitleStreamController, SubtitleTrackController, TimelineController, XhrLoader, fetchSupported, getMediaSource, isMSESupported, isSupported, requestMediaKeySystemAccess */
vendor: 15,348 bytes, lines 10-207
10 11// https://caniuse.com/mdn-javascript_builtins_number_isfinite 12const isFiniteNumber = Number.isFinite || function (value) { 13 return typeof value === 'number' && isFinite(value); 14}; 15 16// https://caniuse.com/mdn-javascript_builtins_number_issafeinteger 17const isSafeInteger = Number.isSafeInteger || function (value) { 18 return typeof value === 'number' && Math.abs(value) <= MAX_SAFE_INTEGER; 19}; 20const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; 21 22let ErrorTypes = /*#__PURE__*/function (ErrorTypes) { 23 // Identifier for a network error (loading error / timeout ...) 24 ErrorTypes["NETWORK_ERROR"] = "networkError"; 25 // Identifier for a media Error (video/parsing/mediasource error) 26 ErrorTypes["MEDIA_ERROR"] = "mediaError"; 27 // EME (encrypted media extensions) errors 28 ErrorTypes["KEY_SYSTEM_ERROR"] = "keySystemError"; 29 // Identifier for a mux Error (demuxing/remuxing) 30 ErrorTypes["MUX_ERROR"] = "muxError"; 31 // Identifier for all other errors 32 ErrorTypes["OTHER_ERROR"] = "otherError"; 33 return ErrorTypes; 34}({}); 35let ErrorDetails = /*#__PURE__*/function (ErrorDetails) { 36 ErrorDetails["KEY_SYSTEM_NO_KEYS"] = "keySystemNoKeys"; 37 ErrorDetails["KEY_SYSTEM_NO_ACCESS"] = "keySystemNoAccess"; 38 ErrorDetails["KEY_SYSTEM_NO_SESSION"] = "keySystemNoSession"; 39 ErrorDetails["KEY_SYSTEM_NO_CONFIGURED_LICENSE"] = "keySystemNoConfiguredLicense"; 40 ErrorDetails["KEY_SYSTEM_LICENSE_REQUEST_FAILED"] = "keySystemLicenseRequestFailed"; 41 ErrorDetails["KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED"] = "keySystemServerCertificateRequestFailed"; 42 ErrorDetails["KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED"] = "keySystemServerCertificateUpdateFailed"; 43 ErrorDetails["KEY_SYSTEM_SESSION_UPDATE_FAILED"] = "keySystemSessionUpdateFailed"; 44 ErrorDetails["KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED"] = "keySystemStatusOutputRestricted"; 45 ErrorDetails["KEY_SYSTEM_STATUS_INTERNAL_ERROR"] = "keySystemStatusInternalError"; 46 ErrorDetails["KEY_SYSTEM_DESTROY_MEDIA_KEYS_ERROR"] = "keySystemDestroyMediaKeysError"; 47 ErrorDetails["KEY_SYSTEM_DESTROY_CLOSE_SESSION_ERROR"] = "keySystemDestroyCloseSessionError"; 48 ErrorDetails["KEY_SYSTEM_DESTROY_REMOVE_SESSION_ERROR"] = "keySystemDestroyRemoveSessionError"; 49 // Identifier for a manifest load error - data: { url : faulty URL, response : { code: error code, text: error text }} 50 ErrorDetails["MANIFEST_LOAD_ERROR"] = "manifestLoadError"; 51 // Identifier for a manifest load timeout - data: { url : faulty URL, response : { code: error code, text: error text }} 52 ErrorDetails["MANIFEST_LOAD_TIMEOUT"] = "manifestLoadTimeOut"; 53 // Identifier for a manifest parsing error - data: { url : faulty URL, reason : error reason} 54 ErrorDetails["MANIFEST_PARSING_ERROR"] = "manifestParsingError"; 55 // Identifier for a manifest with only incompatible codecs error - data: { url : faulty URL, reason : error reason} 56 ErrorDetails["MANIFEST_INCOMPATIBLE_CODECS_ERROR"] = "manifestIncompatibleCodecsError"; 57 // Identifier for a level which contains no fragments - data: { url: faulty URL, reason: "no fragments found in level", level: index of the bad level } 58 ErrorDetails["LEVEL_EMPTY_ERROR"] = "levelEmptyError"; 59 // Identifier for a level load error - data: { url : faulty URL, response : { code: error code, text: error text }} 60 ErrorDetails["LEVEL_LOAD_ERROR"] = "levelLoadError"; 61 // Identifier for a level load timeout - data: { url : faulty URL, response : { code: error code, text: error text }} 62 ErrorDetails["LEVEL_LOAD_TIMEOUT"] = "levelLoadTimeOut"; 63 // Identifier for a level parse error - data: { url : faulty URL, error: Error, reason: error message } 64 ErrorDetails["LEVEL_PARSING_ERROR"] = "levelParsingError"; 65 // Identifier for a level switch error - data: { level : faulty level Id, event : error description} 66 ErrorDetails["LEVEL_SWITCH_ERROR"] = "levelSwitchError"; 67 // Identifier for an audio track load error - data: { url : faulty URL, response : { code: error code, text: error text }} 68 ErrorDetails["AUDIO_TRACK_LOAD_ERROR"] = "audioTrackLoadError"; 69 // Identifier for an audio track load timeout - data: { url : faulty URL, response : { code: error code, text: error text }} 70 ErrorDetails["AUDIO_TRACK_LOAD_TIMEOUT"] = "audioTrackLoadTimeOut"; 71 // Identifier for a subtitle track load error - data: { url : faulty URL, response : { code: error code, text: error text }} 72 ErrorDetails["SUBTITLE_LOAD_ERROR"] = "subtitleTrackLoadError"; 73 // Identifier for a subtitle track load timeout - data: { url : faulty URL, response : { code: error code, text: error text }} 74 ErrorDetails["SUBTITLE_TRACK_LOAD_TIMEOUT"] = "subtitleTrackLoadTimeOut"; 75 // Identifier for fragment load error - data: { frag : fragment object, response : { code: error code, text: error text }} 76 ErrorDetails["FRAG_LOAD_ERROR"] = "fragLoadError"; 77 // Identifier for fragment load timeout error - data: { frag : fragment object} 78 ErrorDetails["FRAG_LOAD_TIMEOUT"] = "fragLoadTimeOut"; 79 // Identifier for a fragment decryption error event - data: {id : demuxer Id,frag: fragment object, reason : parsing error description } 80 ErrorDetails["FRAG_DECRYPT_ERROR"] = "fragDecryptError"; 81 // Identifier for a fragment parsing error event - data: { id : demuxer Id, reason : parsing error description } 82 // will be renamed DEMUX_PARSING_ERROR and switched to MUX_ERROR in the next major release 83 ErrorDetails["FRAG_PARSING_ERROR"] = "fragParsingError"; 84 // Identifier for a fragment or part load skipped because of a GAP tag or attribute 85 ErrorDetails["FRAG_GAP"] = "fragGap"; 86 // 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 } 87 ErrorDetails["REMUX_ALLOC_ERROR"] = "remuxAllocError"; 88 // Identifier for decrypt key load error - data: { frag : fragment object, response : { code: error code, text: error text }} 89 ErrorDetails["KEY_LOAD_ERROR"] = "keyLoadError"; 90 // Identifier for decrypt key load timeout error - data: { frag : fragment object} 91 ErrorDetails["KEY_LOAD_TIMEOUT"] = "keyLoadTimeOut"; 92 // Triggered when an exception occurs while adding a sourceBuffer to MediaSource - data : { error : exception , mimeType : mimeType } 93 ErrorDetails["BUFFER_ADD_CODEC_ERROR"] = "bufferAddCodecError"; 94 // Triggered when source buffer(s) could not be created using level (manifest CODECS attribute), parsed media, or best guess codec(s) - data: { reason : error reason } 95 ErrorDetails["BUFFER_INCOMPATIBLE_CODECS_ERROR"] = "bufferIncompatibleCodecsError"; 96 // Identifier for a buffer append error - data: append error description 97 ErrorDetails["BUFFER_APPEND_ERROR"] = "bufferAppendError"; 98 // Identifier for a buffer appending error event - data: appending error description 99 ErrorDetails["BUFFER_APPENDING_ERROR"] = "bufferAppendingError"; 100 // Identifier for a buffer stalled error event 101 ErrorDetails["BUFFER_STALLED_ERROR"] = "bufferStalledError"; 102 // Identifier for a buffer full event 103 ErrorDetails["BUFFER_FULL_ERROR"] = "bufferFullError"; 104 // Identifier for a buffer seek over hole event 105 ErrorDetails["BUFFER_SEEK_OVER_HOLE"] = "bufferSeekOverHole"; 106 // Identifier for a buffer nudge on stall (playback is stuck although currentTime is in a buffered area) 107 ErrorDetails["BUFFER_NUDGE_ON_STALL"] = "bufferNudgeOnStall"; 108 // Identifier for a Interstitial Asset List load error - data: { url: faulty URL, response: { code: error code, text: error text } } 109 ErrorDetails["ASSET_LIST_LOAD_ERROR"] = "assetListLoadError"; 110 // Identifier for a Interstitial Asset List load timeout - data: { url: faulty URL, response: { code: error code, text: error text } } 111 ErrorDetails["ASSET_LIST_LOAD_TIMEOUT"] = "assetListLoadTimeout"; 112 // Identifier for a Interstitial Asset List parsing error - data: { url : faulty URL, reason : error reason, response : { code: error code, text: error text }} 113 ErrorDetails["ASSET_LIST_PARSING_ERROR"] = "assetListParsingError"; 114 // Identifier for a Interstitial Asset List parsing error - data: { url : faulty URL, reason : error reason, response : { code: error code, text: error text }} 115 ErrorDetails["INTERSTITIAL_ASSET_ITEM_ERROR"] = "interstitialAssetItemError"; 116 // Identifier for an internal exception happening inside hls.js while handling an event 117 ErrorDetails["INTERNAL_EXCEPTION"] = "internalException"; 118 // Identifier for an internal call to abort a loader 119 ErrorDetails["INTERNAL_ABORTED"] = "aborted"; 120 // Triggered when attachMedia fails 121 ErrorDetails["ATTACH_MEDIA_ERROR"] = "attachMediaError"; 122 // Uncategorized error 123 ErrorDetails["UNKNOWN"] = "unknown"; 124 return ErrorDetails; 125}({}); 126 127let Events = /*#__PURE__*/function (Events) { 128 // Fired before MediaSource is attaching to media element 129 Events["MEDIA_ATTACHING"] = "hlsMediaAttaching"; 130 // Fired when MediaSource has been successfully attached to media element 131 Events["MEDIA_ATTACHED"] = "hlsMediaAttached"; 132 // Fired before detaching MediaSource from media element 133 Events["MEDIA_DETACHING"] = "hlsMediaDetaching"; 134 // Fired when MediaSource has been detached from media element 135 Events["MEDIA_DETACHED"] = "hlsMediaDetached"; 136 // Fired when HTMLMediaElement dispatches "ended" event, or stalls at end of VOD program 137 Events["MEDIA_ENDED"] = "hlsMediaEnded"; 138 // Fired after playback stall is resolved with playing, seeked, or ended event following BUFFER_STALLED_ERROR 139 Events["STALL_RESOLVED"] = "hlsStallResolved"; 140 // Fired when the buffer is going to be reset 141 Events["BUFFER_RESET"] = "hlsBufferReset"; 142 // Fired when we know about the codecs that we need buffers for to push into - data: {tracks : { container, codec, levelCodec, initSegment, metadata }} 143 Events["BUFFER_CODECS"] = "hlsBufferCodecs"; 144 // fired when sourcebuffers have been created - data: { tracks : tracks } 145 Events["BUFFER_CREATED"] = "hlsBufferCreated"; 146 // fired when we append a segment to the buffer - data: { segment: segment object } 147 Events["BUFFER_APPENDING"] = "hlsBufferAppending"; 148 // 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 parent} 149 Events["BUFFER_APPENDED"] = "hlsBufferAppended"; 150 // fired when the stream is finished and we want to notify the media buffer that there will be no more data - data: { } 151 Events["BUFFER_EOS"] = "hlsBufferEos"; 152 // fired when all buffers are full to the end of the program, after calling MediaSource.endOfStream() (unless restricted) 153 Events["BUFFERED_TO_END"] = "hlsBufferedToEnd"; 154 // fired when the media buffer should be flushed - data { startOffset, endOffset } 155 Events["BUFFER_FLUSHING"] = "hlsBufferFlushing"; 156 // fired when the media buffer has been flushed - data: { } 157 Events["BUFFER_FLUSHED"] = "hlsBufferFlushed"; 158 // fired to signal that a manifest loading starts - data: { url : manifestURL} 159 Events["MANIFEST_LOADING"] = "hlsManifestLoading"; 160 // fired after manifest has been loaded - data: { levels : [available quality levels], audioTracks : [ available audio tracks ], url : manifestURL, stats : LoaderStats } 161 Events["MANIFEST_LOADED"] = "hlsManifestLoaded"; 162 // fired after manifest has been parsed - data: { levels : [available quality levels], firstLevel : index of first quality level appearing in Manifest} 163 Events["MANIFEST_PARSED"] = "hlsManifestParsed"; 164 // fired when a level switch is requested - data: { level : id of new level } 165 Events["LEVEL_SWITCHING"] = "hlsLevelSwitching"; 166 // fired when a level switch is effective - data: { level : id of new level } 167 Events["LEVEL_SWITCHED"] = "hlsLevelSwitched"; 168 // fired when a level playlist loading starts - data: { url : level URL, level : id of level being loaded} 169 Events["LEVEL_LOADING"] = "hlsLevelLoading"; 170 // fired when a level playlist loading finishes - data: { details : levelDetails object, level : id of loaded level, stats : LoaderStats } 171 Events["LEVEL_LOADED"] = "hlsLevelLoaded"; 172 // 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 } 173 Events["LEVEL_UPDATED"] = "hlsLevelUpdated"; 174 // fired when a level's PTS information has been updated after parsing a fragment - data: { details : levelDetails object, level : id of updated level, drift: PTS drift observed when parsing last fragment } 175 Events["LEVEL_PTS_UPDATED"] = "hlsLevelPtsUpdated"; 176 // fired to notify that levels have changed after removing a level - data: { levels : [available quality levels] } 177 Events["LEVELS_UPDATED"] = "hlsLevelsUpdated"; 178 // fired to notify that audio track lists has been updated - data: { audioTracks : audioTracks } 179 Events["AUDIO_TRACKS_UPDATED"] = "hlsAudioTracksUpdated"; 180 // fired when an audio track switching is requested - data: { id : audio track id } 181 Events["AUDIO_TRACK_SWITCHING"] = "hlsAudioTrackSwitching"; 182 // fired when an audio track switch actually occurs - data: { id : audio track id } 183 Events["AUDIO_TRACK_SWITCHED"] = "hlsAudioTrackSwitched"; 184 // fired when an audio track loading starts - data: { url : audio track URL, id : audio track id } 185 Events["AUDIO_TRACK_LOADING"] = "hlsAudioTrackLoading"; 186 // fired when an audio track loading finishes - data: { details : levelDetails object, id : audio track id, stats : LoaderStats } 187 Events["AUDIO_TRACK_LOADED"] = "hlsAudioTrackLoaded"; 188 // fired when an audio tracks's details have been updated based on previous details, after it has been loaded - data: { details : levelDetails object, id : track id } 189 Events["AUDIO_TRACK_UPDATED"] = "hlsAudioTrackUpdated"; 190 // fired to notify that subtitle track lists has been updated - data: { subtitleTracks : subtitleTracks } 191 Events["SUBTITLE_TRACKS_UPDATED"] = "hlsSubtitleTracksUpdated"; 192 // fired to notify that subtitle tracks were cleared as a result of stopping the media 193 Events["SUBTITLE_TRACKS_CLEARED"] = "hlsSubtitleTracksCleared"; 194 // fired when an subtitle track switch occurs - data: { id : subtitle track id } 195 Events["SUBTITLE_TRACK_SWITCH"] = "hlsSubtitleTrackSwitch"; 196 // fired when a subtitle track loading starts - data: { url : subtitle track URL, id : subtitle track id } 197 Events["SUBTITLE_TRACK_LOADING"] = "hlsSubtitleTrackLoading"; 198 // fired when a subtitle track loading finishes - data: { details : levelDetails object, id : subtitle track id, stats : LoaderStats } 199 Events["SUBTITLE_TRACK_LOADED"] = "hlsSubtitleTrackLoaded"; 200 // fired when a subtitle racks's details have been updated based on previous details, after it has been loaded - data: { details : levelDetails object, id : track id } 201 Events["SUBTITLE_TRACK_UPDATED"] = "hlsSubtitleTrackUpdated"; 202 // fired when a subtitle fragment has been processed - data: { success : boolean, frag : the processed frag } 203 Events["SUBTITLE_FRAG_PROCESSED"] = "hlsSubtitleFragProcessed"; 204 // fired when a set of VTTCues to be managed externally has been parsed - data: { type: string, track: string, cues: [ VTTCue ] } 205 Events["CUES_PARSED"] = "hlsCuesParsed"; 206 // fired when a text track to be managed externally is found - data: { tracks: [ { label: string, kind: string, default: boolean } ] } 207 Events["NON_NATIVE_TEXT_TRACKS_FOUND"] = "hlsNonNativeTextTracksFound";
vendor: 12,581 bytes, lines 208-508
208 // fired when the first timestamp is found - data: { id : demuxer id, initPTS: initPTS, timescale: timescale, frag : fragment object } 209 Events["INIT_PTS_FOUND"] = "hlsInitPtsFound"; 210 // fired when a fragment loading starts - data: { frag : fragment object } 211 Events["FRAG_LOADING"] = "hlsFragLoading"; 212 // fired when a fragment loading is progressing - data: { frag : fragment object, { trequest, tfirst, loaded } } 213 // FRAG_LOAD_PROGRESS = 'hlsFragLoadProgress', 214 // Identifier for fragment load aborting for emergency switch down - data: { frag : fragment object } 215 Events["FRAG_LOAD_EMERGENCY_ABORTED"] = "hlsFragLoadEmergencyAborted"; 216 // fired when a fragment loading is completed - data: { frag : fragment object, payload : fragment payload, stats : LoaderStats } 217 Events["FRAG_LOADED"] = "hlsFragLoaded"; 218 // fired when a fragment has finished decrypting - data: { id : demuxer id, frag: fragment object, payload : fragment payload, stats : { tstart, tdecrypt } } 219 Events["FRAG_DECRYPTED"] = "hlsFragDecrypted"; 220 // fired when Init Segment has been extracted from fragment - data: { id : demuxer id, frag: fragment object, moov : moov MP4 box, codecs : codecs found while parsing fragment } 221 Events["FRAG_PARSING_INIT_SEGMENT"] = "hlsFragParsingInitSegment"; 222 // fired when parsing sei text is completed - data: { id : demuxer id, frag: fragment object, samples : [ sei samples pes ] } 223 Events["FRAG_PARSING_USERDATA"] = "hlsFragParsingUserdata"; 224 // fired when parsing id3 is completed - data: { id : demuxer id, frag: fragment object, samples : [ id3 samples pes ] } 225 Events["FRAG_PARSING_METADATA"] = "hlsFragParsingMetadata"; 226 // fired when data have been extracted from fragment - data: { id : demuxer id, frag: fragment object, data1 : moof MP4 box or TS fragments, data2 : mdat MP4 box or null} 227 // FRAG_PARSING_DATA = 'hlsFragParsingData', 228 // fired when fragment parsing is completed - data: { id : demuxer id, frag: fragment object } 229 Events["FRAG_PARSED"] = "hlsFragParsed"; 230 // fired when fragment remuxed MP4 boxes have all been appended into SourceBuffer - data: { id : demuxer id, frag : fragment object, stats : LoaderStats } 231 Events["FRAG_BUFFERED"] = "hlsFragBuffered"; 232 // fired when fragment matching with current media position is changing - data : { id : demuxer id, frag : fragment object } 233 Events["FRAG_CHANGED"] = "hlsFragChanged"; 234 // Identifier for a FPS drop event - data: { currentDropped, currentDecoded, totalDroppedFrames } 235 Events["FPS_DROP"] = "hlsFpsDrop"; 236 // triggered when FPS drop triggers auto level capping - data: { level, droppedLevel } 237 Events["FPS_DROP_LEVEL_CAPPING"] = "hlsFpsDropLevelCapping"; 238 // triggered when maxAutoLevel changes - data { autoLevelCapping, levels, maxAutoLevel, minAutoLevel, maxHdcpLevel } 239 Events["MAX_AUTO_LEVEL_UPDATED"] = "hlsMaxAutoLevelUpdated"; 240 // Identifier for an 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 } 241 Events["ERROR"] = "hlsError"; 242 // 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: { } 243 Events["DESTROYING"] = "hlsDestroying"; 244 // fired when a decrypt key loading starts - data: { frag : fragment object } 245 Events["KEY_LOADING"] = "hlsKeyLoading"; 246 // fired when a decrypt key loading is completed - data: { frag : fragment object, keyInfo : KeyLoaderInfo } 247 Events["KEY_LOADED"] = "hlsKeyLoaded"; 248 // deprecated; please use BACK_BUFFER_REACHED - data : { bufferEnd: number } 249 Events["LIVE_BACK_BUFFER_REACHED"] = "hlsLiveBackBufferReached"; 250 // fired when the back buffer is reached as defined by the backBufferLength config option - data : { bufferEnd: number } 251 Events["BACK_BUFFER_REACHED"] = "hlsBackBufferReached"; 252 // fired after steering manifest has been loaded - data: { steeringManifest: SteeringManifest object, url: steering manifest URL } 253 Events["STEERING_MANIFEST_LOADED"] = "hlsSteeringManifestLoaded"; 254 // fired when asset list has begun loading 255 Events["ASSET_LIST_LOADING"] = "hlsAssetListLoading"; 256 // fired when a valid asset list is loaded 257 Events["ASSET_LIST_LOADED"] = "hlsAssetListLoaded"; 258 // fired when the list of Interstitial Events and Interstitial Schedule is updated 259 Events["INTERSTITIALS_UPDATED"] = "hlsInterstitialsUpdated"; 260 // fired when the buffer reaches an Interstitial Schedule boundary (both Primary segments and Interstitial Assets) 261 Events["INTERSTITIALS_BUFFERED_TO_BOUNDARY"] = "hlsInterstitialsBufferedToBoundary"; 262 // fired when a player instance for an Interstitial Asset has been created 263 Events["INTERSTITIAL_ASSET_PLAYER_CREATED"] = "hlsInterstitialAssetPlayerCreated"; 264 // Interstitial playback started 265 Events["INTERSTITIAL_STARTED"] = "hlsInterstitialStarted"; 266 // InterstitialAsset playback started 267 Events["INTERSTITIAL_ASSET_STARTED"] = "hlsInterstitialAssetStarted"; 268 // InterstitialAsset playback ended 269 Events["INTERSTITIAL_ASSET_ENDED"] = "hlsInterstitialAssetEnded"; 270 // InterstitialAsset playback errored 271 Events["INTERSTITIAL_ASSET_ERROR"] = "hlsInterstitialAssetError"; 272 // Interstitial playback ended 273 Events["INTERSTITIAL_ENDED"] = "hlsInterstitialEnded"; 274 // Interstitial schedule resumed primary playback 275 Events["INTERSTITIALS_PRIMARY_RESUMED"] = "hlsInterstitialsPrimaryResumed"; 276 // Interstitial players dispatch this event when playout limit is reached 277 Events["PLAYOUT_LIMIT_REACHED"] = "hlsPlayoutLimitReached"; 278 // Event DateRange cue "enter" event dispatched 279 Events["EVENT_CUE_ENTER"] = "hlsEventCueEnter"; 280 return Events; 281}({}); 282 283/** 284 * Defines each Event type and payload by Event name. Used in {@link hls.js#HlsEventEmitter} to strongly type the event listener API. 285 */ 286 287var PlaylistContextType = { 288 MANIFEST: "manifest", 289 LEVEL: "level", 290 AUDIO_TRACK: "audioTrack", 291 SUBTITLE_TRACK: "subtitleTrack" 292}; 293var PlaylistLevelType = { 294 MAIN: "main", 295 AUDIO: "audio", 296 SUBTITLE: "subtitle" 297}; 298 299/* 300 * compute an Exponential Weighted moving average 301 * - https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average 302 * - heavily inspired from shaka-player 303 */ 304 305class EWMA { 306 // About half of the estimated value will be from the last |halfLife| samples by weight. 307 constructor(halfLife, estimate = 0, weight = 0) { 308 this.halfLife = void 0; 309 this.alpha_ = void 0; 310 this.estimate_ = void 0; 311 this.totalWeight_ = void 0; 312 this.halfLife = halfLife; 313 // Larger values of alpha expire historical data more slowly. 314 this.alpha_ = halfLife ? Math.exp(Math.log(0.5) / halfLife) : 0; 315 this.estimate_ = estimate; 316 this.totalWeight_ = weight; 317 } 318 sample(weight, value) { 319 const adjAlpha = Math.pow(this.alpha_, weight); 320 this.estimate_ = value * (1 - adjAlpha) + adjAlpha * this.estimate_; 321 this.totalWeight_ += weight; 322 } 323 getTotalWeight() { 324 return this.totalWeight_; 325 } 326 getEstimate() { 327 if (this.alpha_) { 328 const zeroFactor = 1 - Math.pow(this.alpha_, this.totalWeight_); 329 if (zeroFactor) { 330 return this.estimate_ / zeroFactor; 331 } 332 } 333 return this.estimate_; 334 } 335} 336 337/* 338 * EWMA Bandwidth Estimator 339 * - heavily inspired from shaka-player 340 * Tracks bandwidth samples and estimates available bandwidth. 341 * Based on the minimum of two exponentially-weighted moving averages with 342 * different half-lives. 343 */ 344 345class EwmaBandWidthEstimator { 346 constructor(slow, fast, defaultEstimate, defaultTTFB = 100) { 347 this.defaultEstimate_ = void 0; 348 this.minWeight_ = void 0; 349 this.minDelayMs_ = void 0; 350 this.slow_ = void 0; 351 this.fast_ = void 0; 352 this.defaultTTFB_ = void 0; 353 this.ttfb_ = void 0; 354 this.defaultEstimate_ = defaultEstimate; 355 this.minWeight_ = 0.001; 356 this.minDelayMs_ = 50; 357 this.slow_ = new EWMA(slow); 358 this.fast_ = new EWMA(fast); 359 this.defaultTTFB_ = defaultTTFB; 360 this.ttfb_ = new EWMA(slow); 361 } 362 update(slow, fast) { 363 const { 364 slow_, 365 fast_, 366 ttfb_ 367 } = this; 368 if (slow_.halfLife !== slow) { 369 this.slow_ = new EWMA(slow, slow_.getEstimate(), slow_.getTotalWeight()); 370 } 371 if (fast_.halfLife !== fast) { 372 this.fast_ = new EWMA(fast, fast_.getEstimate(), fast_.getTotalWeight()); 373 } 374 if (ttfb_.halfLife !== slow) { 375 this.ttfb_ = new EWMA(slow, ttfb_.getEstimate(), ttfb_.getTotalWeight()); 376 } 377 } 378 sample(durationMs, numBytes) { 379 durationMs = Math.max(durationMs, this.minDelayMs_); 380 const numBits = 8 * numBytes; 381 // weight is duration in seconds 382 const durationS = durationMs / 1000; 383 // value is bandwidth in bits/s 384 const bandwidthInBps = numBits / durationS; 385 this.fast_.sample(durationS, bandwidthInBps); 386 this.slow_.sample(durationS, bandwidthInBps); 387 } 388 sampleTTFB(ttfb) { 389 // weight is frequency curve applied to TTFB in seconds 390 // (longer times have less weight with expected input under 1 second) 391 const seconds = ttfb / 1000; 392 const weight = Math.sqrt(2) * Math.exp(-Math.pow(seconds, 2) / 2); 393 this.ttfb_.sample(weight, Math.max(ttfb, 5)); 394 } 395 canEstimate() { 396 return this.fast_.getTotalWeight() >= this.minWeight_; 397 } 398 getEstimate() { 399 if (this.canEstimate()) { 400 // console.log('slow estimate:'+ Math.round(this.slow_.getEstimate())); 401 // console.log('fast estimate:'+ Math.round(this.fast_.getEstimate())); 402 // Take the minimum of these two estimates. This should have the effect of 403 // adapting down quickly, but up more slowly. 404 return Math.min(this.fast_.getEstimate(), this.slow_.getEstimate()); 405 } else { 406 return this.defaultEstimate_; 407 } 408 } 409 getEstimateTTFB() { 410 if (this.ttfb_.getTotalWeight() >= this.minWeight_) { 411 return this.ttfb_.getEstimate(); 412 } else { 413 return this.defaultTTFB_; 414 } 415 } 416 get defaultEstimate() { 417 return this.defaultEstimate_; 418 } 419 destroy() {} 420} 421 422function _defineProperty(e, r, t) { 423 return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { 424 value: t, 425 enumerable: true, 426 configurable: true, 427 writable: true 428 }) : e[r] = t, e; 429} 430function _extends() { 431 return _extends = Object.assign ? Object.assign.bind() : function (n) { 432 for (var e = 1; e < arguments.length; e++) { 433 var t = arguments[e]; 434 for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); 435 } 436 return n; 437 }, _extends.apply(null, arguments); 438} 439function ownKeys(e, r) { 440 var t = Object.keys(e); 441 if (Object.getOwnPropertySymbols) { 442 var o = Object.getOwnPropertySymbols(e); 443 r && (o = o.filter(function (r) { 444 return Object.getOwnPropertyDescriptor(e, r).enumerable; 445 })), t.push.apply(t, o); 446 } 447 return t; 448} 449function _objectSpread2(e) { 450 for (var r = 1; r < arguments.length; r++) { 451 var t = null != arguments[r] ? arguments[r] : {}; 452 r % 2 ? ownKeys(Object(t), true).forEach(function (r) { 453 _defineProperty(e, r, t[r]); 454 }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { 455 Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); 456 }); 457 } 458 return e; 459} 460function _toPrimitive(t, r) { 461 if ("object" != typeof t || !t) return t; 462 var e = t[Symbol.toPrimitive]; 463 if (void 0 !== e) { 464 var i = e.call(t, r); 465 if ("object" != typeof i) return i; 466 throw new TypeError("@@toPrimitive must return a primitive value."); 467 } 468 return ("string" === r ? String : Number)(t); 469} 470function _toPropertyKey(t) { 471 var i = _toPrimitive(t, "string"); 472 return "symbol" == typeof i ? i : i + ""; 473} 474 475class Logger { 476 constructor(label, logger) { 477 this.trace = void 0; 478 this.debug = void 0; 479 this.log = void 0; 480 this.warn = void 0; 481 this.info = void 0; 482 this.error = void 0; 483 const lb = `[${label}]:`; 484 this.trace = noop; 485 this.debug = logger.debug.bind(null, lb); 486 this.log = logger.log.bind(null, lb); 487 this.warn = logger.warn.bind(null, lb); 488 this.info = logger.info.bind(null, lb); 489 this.error = logger.error.bind(null, lb); 490 } 491} 492const noop = function noop() {}; 493const fakeLogger = { 494 trace: noop, 495 debug: noop, 496 log: noop, 497 warn: noop, 498 info: noop, 499 error: noop 500}; 501function createLogger() { 502 return _extends({}, fakeLogger); 503} 504 505// let lastCallTime; 506// function formatMsgWithTimeInfo(type, msg) { 507// const now = Date.now(); 508// const diff = lastCallTime ? '+' + (now - lastCallTime) : '0';
vendor: 5,023 bytes, lines 509-673
509// lastCallTime = now; 510// msg = (new Date(now)).toISOString() + ' | [' + type + '] > ' + msg + ' ( ' + diff + ' ms )'; 511// return msg; 512// } 513 514function consolePrintFn(type, id) { 515 const func = self.console[type]; 516 return func ? func.bind(self.console, `${id ? '[' + id + '] ' : ''}[${type}] >`) : noop; 517} 518function getLoggerFn(key, debugConfig, id) { 519 return debugConfig[key] ? debugConfig[key].bind(debugConfig) : consolePrintFn(key, id); 520} 521const exportedLogger = createLogger(); 522function enableLogs(debugConfig, context, id) { 523 // check that console is available 524 const newLogger = createLogger(); 525 if (typeof console === 'object' && debugConfig === true || typeof debugConfig === 'object') { 526 const keys = [ 527 // Remove out from list here to hard-disable a log-level 528 // 'trace', 529 'debug', 'log', 'info', 'warn', 'error']; 530 keys.forEach(key => { 531 newLogger[key] = getLoggerFn(key, debugConfig, id); 532 }); 533 // Some browsers don't allow to use bind on console object anyway 534 // fallback to default if needed 535 try { 536 newLogger.log(`Debug logs enabled for "${context}" in hls.js version ${"1.6.16"}`); 537 } catch (e) { 538 /* log fn threw an exception. All logger methods are no-ops. */ 539 return createLogger(); 540 } 541 // global exported logger uses the same functions as new logger without `id` 542 keys.forEach(key => { 543 exportedLogger[key] = getLoggerFn(key, debugConfig); 544 }); 545 } else { 546 // Reset global exported logger 547 _extends(exportedLogger, newLogger); 548 } 549 return newLogger; 550} 551const logger = exportedLogger; 552 553function getMediaSource(preferManagedMediaSource = true) { 554 if (typeof self === 'undefined') return undefined; 555 const mms = (preferManagedMediaSource || !self.MediaSource) && self.ManagedMediaSource; 556 return mms || self.MediaSource || self.WebKitMediaSource; 557} 558function isManagedMediaSource(source) { 559 return typeof self !== 'undefined' && source === self.ManagedMediaSource; 560} 561function isCompatibleTrackChange(currentTracks, requiredTracks) { 562 const trackNames = Object.keys(currentTracks); 563 const requiredTrackNames = Object.keys(requiredTracks); 564 const trackCount = trackNames.length; 565 const requiredTrackCount = requiredTrackNames.length; 566 return !trackCount || !requiredTrackCount || trackCount === requiredTrackCount && !trackNames.some(name => requiredTrackNames.indexOf(name) === -1); 567} 568 569// http://stackoverflow.com/questions/8936984/uint8array-to-string-in-javascript/22373197 570// http://www.onicos.com/staff/iz/amuse/javascript/expert/utf.txt 571/* utf.js - UTF-8 <=> UTF-16 convertion 572 * 573 * Copyright (C) 1999 Masanao Izumo <[email protected]> 574 * Version: 1.0 575 * LastModified: Dec 25 1999 576 * This library is free. You can redistribute it and/or modify it. 577 */ 578/** 579 * Converts a UTF-8 array to a string. 580 * 581 * @param array - The UTF-8 array to convert 582 * 583 * @returns The string 584 * 585 * @group Utils 586 * 587 * @beta 588 */ 589function utf8ArrayToStr(array, exitOnNull = false) { 590 if (typeof TextDecoder !== 'undefined') { 591 const decoder = new TextDecoder('utf-8'); 592 const decoded = decoder.decode(array); 593 if (exitOnNull) { 594 // grab up to the first null 595 const idx = decoded.indexOf('\0'); 596 return idx !== -1 ? decoded.substring(0, idx) : decoded; 597 } 598 // remove any null characters 599 return decoded.replace(/\0/g, ''); 600 } 601 const len = array.length; 602 let c; 603 let char2; 604 let char3; 605 let out = ''; 606 let i = 0; 607 while (i < len) { 608 c = array[i++]; 609 if (c === 0x00 && exitOnNull) { 610 return out; 611 } else if (c === 0x00 || c === 0x03) { 612 // If the character is 3 (END_OF_TEXT) or 0 (NULL) then skip it 613 continue; 614 } 615 switch (c >> 4) { 616 case 0: 617 case 1: 618 case 2: 619 case 3: 620 case 4: 621 case 5: 622 case 6: 623 case 7: 624 // 0xxxxxxx 625 out += String.fromCharCode(c); 626 break; 627 case 12: 628 case 13: 629 // 110x xxxx 10xx xxxx 630 char2 = array[i++]; 631 out += String.fromCharCode((c & 0x1f) << 6 | char2 & 0x3f); 632 break; 633 case 14: 634 // 1110 xxxx 10xx xxxx 10xx xxxx 635 char2 = array[i++]; 636 char3 = array[i++]; 637 out += String.fromCharCode((c & 0x0f) << 12 | (char2 & 0x3f) << 6 | (char3 & 0x3f) << 0); 638 break; 639 } 640 } 641 return out; 642} 643 644/** 645 * hex dump helper class 646 */ 647 648function arrayToHex(array) { 649 let str = ''; 650 for (let i = 0; i < array.length; i++) { 651 let h = array[i].toString(16); 652 if (h.length < 2) { 653 h = '0' + h; 654 } 655 str += h; 656 } 657 return str; 658} 659function hexToArrayBuffer(str) { 660 return Uint8Array.from(str.replace(/^0x/, '').replace(/([\da-fA-F]{2}) ?/g, '0x$1 ').replace(/ +$/, '').split(' ')).buffer; 661} 662 663function getDefaultExportFromCjs (x) { 664 return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; 665} 666 667var urlToolkit = {exports: {}}; 668 669var hasRequiredUrlToolkit; 670 671function requireUrlToolkit () { 672 if (hasRequiredUrlToolkit) return urlToolkit.exports; 673 hasRequiredUrlToolkit = 1;
vendor: 38,922 bytes, lines 674-1813
674 (function (module, exports) { 675 // see https://tools.ietf.org/html/rfc1808 676 677 (function (root) { 678 var URL_REGEX = 679 /^(?=((?:[a-zA-Z0-9+\-.]+:)?))\1(?=((?:\/\/[^\/?#]*)?))\2(?=((?:(?:[^?#\/]*\/)*[^;?#\/]*)?))\3((?:;[^?#]*)?)(\?[^#]*)?(#[^]*)?$/; 680 var FIRST_SEGMENT_REGEX = /^(?=([^\/?#]*))\1([^]*)$/; 681 var SLASH_DOT_REGEX = /(?:\/|^)\.(?=\/)/g; 682 var SLASH_DOT_DOT_REGEX = /(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g; 683 684 var URLToolkit = { 685 // If opts.alwaysNormalize is true then the path will always be normalized even when it starts with / or // 686 // E.g 687 // With opts.alwaysNormalize = false (default, spec compliant) 688 // http://a.com/b/cd + /e/f/../g => http://a.com/e/f/../g 689 // With opts.alwaysNormalize = true (not spec compliant) 690 // http://a.com/b/cd + /e/f/../g => http://a.com/e/g 691 buildAbsoluteURL: function (baseURL, relativeURL, opts) { 692 opts = opts || {}; 693 // remove any remaining space and CRLF 694 baseURL = baseURL.trim(); 695 relativeURL = relativeURL.trim(); 696 if (!relativeURL) { 697 // 2a) If the embedded URL is entirely empty, it inherits the 698 // entire base URL (i.e., is set equal to the base URL) 699 // and we are done. 700 if (!opts.alwaysNormalize) { 701 return baseURL; 702 } 703 var basePartsForNormalise = URLToolkit.parseURL(baseURL); 704 if (!basePartsForNormalise) { 705 throw new Error('Error trying to parse base URL.'); 706 } 707 basePartsForNormalise.path = URLToolkit.normalizePath( 708 basePartsForNormalise.path 709 ); 710 return URLToolkit.buildURLFromParts(basePartsForNormalise); 711 } 712 var relativeParts = URLToolkit.parseURL(relativeURL); 713 if (!relativeParts) { 714 throw new Error('Error trying to parse relative URL.'); 715 } 716 if (relativeParts.scheme) { 717 // 2b) If the embedded URL starts with a scheme name, it is 718 // interpreted as an absolute URL and we are done. 719 if (!opts.alwaysNormalize) { 720 return relativeURL; 721 } 722 relativeParts.path = URLToolkit.normalizePath(relativeParts.path); 723 return URLToolkit.buildURLFromParts(relativeParts); 724 } 725 var baseParts = URLToolkit.parseURL(baseURL); 726 if (!baseParts) { 727 throw new Error('Error trying to parse base URL.'); 728 } 729 if (!baseParts.netLoc && baseParts.path && baseParts.path[0] !== '/') { 730 // If netLoc missing and path doesn't start with '/', assume everthing before the first '/' is the netLoc 731 // This causes 'example.com/a' to be handled as '//example.com/a' instead of '/example.com/a' 732 var pathParts = FIRST_SEGMENT_REGEX.exec(baseParts.path); 733 baseParts.netLoc = pathParts[1]; 734 baseParts.path = pathParts[2]; 735 } 736 if (baseParts.netLoc && !baseParts.path) { 737 baseParts.path = '/'; 738 } 739 var builtParts = { 740 // 2c) Otherwise, the embedded URL inherits the scheme of 741 // the base URL. 742 scheme: baseParts.scheme, 743 netLoc: relativeParts.netLoc, 744 path: null, 745 params: relativeParts.params, 746 query: relativeParts.query, 747 fragment: relativeParts.fragment, 748 }; 749 if (!relativeParts.netLoc) { 750 // 3) If the embedded URL's <net_loc> is non-empty, we skip to 751 // Step 7. Otherwise, the embedded URL inherits the <net_loc> 752 // (if any) of the base URL. 753 builtParts.netLoc = baseParts.netLoc; 754 // 4) If the embedded URL path is preceded by a slash "/", the 755 // path is not relative and we skip to Step 7. 756 if (relativeParts.path[0] !== '/') { 757 if (!relativeParts.path) { 758 // 5) If the embedded URL path is empty (and not preceded by a 759 // slash), then the embedded URL inherits the base URL path 760 builtParts.path = baseParts.path; 761 // 5a) if the embedded URL's <params> is non-empty, we skip to 762 // step 7; otherwise, it inherits the <params> of the base 763 // URL (if any) and 764 if (!relativeParts.params) { 765 builtParts.params = baseParts.params; 766 // 5b) if the embedded URL's <query> is non-empty, we skip to 767 // step 7; otherwise, it inherits the <query> of the base 768 // URL (if any) and we skip to step 7. 769 if (!relativeParts.query) { 770 builtParts.query = baseParts.query; 771 } 772 } 773 } else { 774 // 6) The last segment of the base URL's path (anything 775 // following the rightmost slash "/", or the entire path if no 776 // slash is present) is removed and the embedded URL's path is 777 // appended in its place. 778 var baseURLPath = baseParts.path; 779 var newPath = 780 baseURLPath.substring(0, baseURLPath.lastIndexOf('/') + 1) + 781 relativeParts.path; 782 builtParts.path = URLToolkit.normalizePath(newPath); 783 } 784 } 785 } 786 if (builtParts.path === null) { 787 builtParts.path = opts.alwaysNormalize 788 ? URLToolkit.normalizePath(relativeParts.path) 789 : relativeParts.path; 790 } 791 return URLToolkit.buildURLFromParts(builtParts); 792 }, 793 parseURL: function (url) { 794 var parts = URL_REGEX.exec(url); 795 if (!parts) { 796 return null; 797 } 798 return { 799 scheme: parts[1] || '', 800 netLoc: parts[2] || '', 801 path: parts[3] || '', 802 params: parts[4] || '', 803 query: parts[5] || '', 804 fragment: parts[6] || '', 805 }; 806 }, 807 normalizePath: function (path) { 808 // The following operations are 809 // then applied, in order, to the new path: 810 // 6a) All occurrences of "./", where "." is a complete path 811 // segment, are removed. 812 // 6b) If the path ends with "." as a complete path segment, 813 // that "." is removed. 814 path = path.split('').reverse().join('').replace(SLASH_DOT_REGEX, ''); 815 // 6c) All occurrences of "<segment>/../", where <segment> is a 816 // complete path segment not equal to "..", are removed. 817 // Removal of these path segments is performed iteratively, 818 // removing the leftmost matching pattern on each iteration, 819 // until no matching pattern remains. 820 // 6d) If the path ends with "<segment>/..", where <segment> is a 821 // complete path segment not equal to "..", that 822 // "<segment>/.." is removed. 823 while ( 824 path.length !== (path = path.replace(SLASH_DOT_DOT_REGEX, '')).length 825 ) {} 826 return path.split('').reverse().join(''); 827 }, 828 buildURLFromParts: function (parts) { 829 return ( 830 parts.scheme + 831 parts.netLoc + 832 parts.path + 833 parts.params + 834 parts.query + 835 parts.fragment 836 ); 837 }, 838 }; 839 840 module.exports = URLToolkit; 841 })(); 842 } (urlToolkit)); 843 return urlToolkit.exports; 844} 845 846var urlToolkitExports = requireUrlToolkit(); 847 848class LoadStats { 849 constructor() { 850 this.aborted = false; 851 this.loaded = 0; 852 this.retry = 0; 853 this.total = 0; 854 this.chunkCount = 0; 855 this.bwEstimate = 0; 856 this.loading = { 857 start: 0, 858 first: 0, 859 end: 0 860 }; 861 this.parsing = { 862 start: 0, 863 end: 0 864 }; 865 this.buffering = { 866 start: 0, 867 first: 0, 868 end: 0 869 }; 870 } 871} 872 873var ElementaryStreamTypes = { 874 AUDIO: "audio", 875 VIDEO: "video", 876 AUDIOVIDEO: "audiovideo" 877}; 878class BaseSegment { 879 constructor(base) { 880 this._byteRange = null; 881 this._url = null; 882 this._stats = null; 883 this._streams = null; 884 // baseurl is the URL to the playlist 885 this.base = void 0; 886 // relurl is the portion of the URL that comes from inside the playlist. 887 this.relurl = void 0; 888 if (typeof base === 'string') { 889 base = { 890 url: base 891 }; 892 } 893 this.base = base; 894 makeEnumerable(this, 'stats'); 895 } 896 897 // setByteRange converts a EXT-X-BYTERANGE attribute into a two element array 898 setByteRange(value, previous) { 899 const params = value.split('@', 2); 900 let start; 901 if (params.length === 1) { 902 start = (previous == null ? void 0 : previous.byteRangeEndOffset) || 0; 903 } else { 904 start = parseInt(params[1]); 905 } 906 this._byteRange = [start, parseInt(params[0]) + start]; 907 } 908 get baseurl() { 909 return this.base.url; 910 } 911 get byteRange() { 912 if (this._byteRange === null) { 913 return []; 914 } 915 return this._byteRange; 916 } 917 get byteRangeStartOffset() { 918 return this.byteRange[0]; 919 } 920 get byteRangeEndOffset() { 921 return this.byteRange[1]; 922 } 923 get elementaryStreams() { 924 if (this._streams === null) { 925 this._streams = { 926 [ElementaryStreamTypes.AUDIO]: null, 927 [ElementaryStreamTypes.VIDEO]: null, 928 [ElementaryStreamTypes.AUDIOVIDEO]: null 929 }; 930 } 931 return this._streams; 932 } 933 set elementaryStreams(value) { 934 this._streams = value; 935 } 936 get hasStats() { 937 return this._stats !== null; 938 } 939 get hasStreams() { 940 return this._streams !== null; 941 } 942 get stats() { 943 if (this._stats === null) { 944 this._stats = new LoadStats(); 945 } 946 return this._stats; 947 } 948 set stats(value) { 949 this._stats = value; 950 } 951 get url() { 952 if (!this._url && this.baseurl && this.relurl) { 953 this._url = urlToolkitExports.buildAbsoluteURL(this.baseurl, this.relurl, { 954 alwaysNormalize: true 955 }); 956 } 957 return this._url || ''; 958 } 959 set url(value) { 960 this._url = value; 961 } 962 clearElementaryStreamInfo() { 963 const { 964 elementaryStreams 965 } = this; 966 elementaryStreams[ElementaryStreamTypes.AUDIO] = null; 967 elementaryStreams[ElementaryStreamTypes.VIDEO] = null; 968 elementaryStreams[ElementaryStreamTypes.AUDIOVIDEO] = null; 969 } 970} 971function isMediaFragment(frag) { 972 return frag.sn !== 'initSegment'; 973} 974 975/** 976 * Object representing parsed data from an HLS Segment. Found in {@link hls.js#LevelDetails.fragments}. 977 */ 978class Fragment extends BaseSegment { 979 constructor(type, base) { 980 super(base); 981 this._decryptdata = null; 982 this._programDateTime = null; 983 this._ref = null; 984 // Approximate bit rate of the fragment expressed in bits per second (bps) as indicated by the last EXT-X-BITRATE (kbps) tag 985 this._bitrate = void 0; 986 this.rawProgramDateTime = null; 987 this.tagList = []; 988 // EXTINF has to be present for a m3u8 to be considered valid 989 this.duration = 0; 990 // sn notates the sequence number for a segment, and if set to a string can be 'initSegment' 991 this.sn = 0; 992 // levelkeys are the EXT-X-KEY tags that apply to this segment for decryption 993 // core difference from the private field _decryptdata is the lack of the initialized IV 994 // _decryptdata will set the IV for this segment based on the segment number in the fragment 995 this.levelkeys = void 0; 996 // A string representing the fragment type 997 this.type = void 0; 998 // A reference to the loader. Set while the fragment is loading, and removed afterwards. Used to abort fragment loading 999 this.loader = null; 1000 // A reference to the key loader. Set while the key is loading, and removed afterwards. Used to abort key loading 1001 this.keyLoader = null; 1002 // The level/track index to which the fragment belongs 1003 this.level = -1; 1004 // The continuity counter of the fragment 1005 this.cc = 0; 1006 // The starting Presentation Time Stamp (PTS) of the fragment. Set after transmux complete. 1007 this.startPTS = void 0; 1008 // The ending Presentation Time Stamp (PTS) of the fragment. Set after transmux complete. 1009 this.endPTS = void 0; 1010 // The starting Decode Time Stamp (DTS) of the fragment. Set after transmux complete. 1011 this.startDTS = void 0; 1012 // The ending Decode Time Stamp (DTS) of the fragment. Set after transmux complete. 1013 this.endDTS = void 0; 1014 // The start time of the fragment, as listed in the manifest. Updated after transmux complete. 1015 this.start = 0; 1016 // The offset time (seconds) of the fragment from the start of the Playlist 1017 this.playlistOffset = 0; 1018 // Set by `updateFragPTSDTS` in level-helper 1019 this.deltaPTS = void 0; 1020 // The maximum starting Presentation Time Stamp (audio/video PTS) of the fragment. Set after transmux complete. 1021 this.maxStartPTS = void 0; 1022 // The minimum ending Presentation Time Stamp (audio/video PTS) of the fragment. Set after transmux complete. 1023 this.minEndPTS = void 0; 1024 // Init Segment bytes (unset for media segments) 1025 this.data = void 0; 1026 // A flag indicating whether the segment was downloaded in order to test bitrate, and was not buffered 1027 this.bitrateTest = false; 1028 // #EXTINF segment title 1029 this.title = null; 1030 // The Media Initialization Section for this segment 1031 this.initSegment = null; 1032 // Fragment is the last fragment in the media playlist 1033 this.endList = void 0; 1034 // Fragment is marked by an EXT-X-GAP tag indicating that it does not contain media data and should not be loaded 1035 this.gap = void 0; 1036 // Deprecated 1037 this.urlId = 0; 1038 this.type = type; 1039 } 1040 get byteLength() { 1041 if (this.hasStats) { 1042 const total = this.stats.total; 1043 if (total) { 1044 return total; 1045 } 1046 } 1047 if (this.byteRange.length) { 1048 const start = this.byteRange[0]; 1049 const end = this.byteRange[1]; 1050 if (isFiniteNumber(start) && isFiniteNumber(end)) { 1051 return end - start; 1052 } 1053 } 1054 return null; 1055 } 1056 get bitrate() { 1057 if (this.byteLength) { 1058 return this.byteLength * 8 / this.duration; 1059 } 1060 if (this._bitrate) { 1061 return this._bitrate; 1062 } 1063 return null; 1064 } 1065 set bitrate(value) { 1066 this._bitrate = value; 1067 } 1068 get decryptdata() { 1069 var _this$_decryptdata; 1070 const { 1071 levelkeys 1072 } = this; 1073 if (!levelkeys || levelkeys.NONE) { 1074 return null; 1075 } 1076 if (levelkeys.identity) { 1077 if (!this._decryptdata) { 1078 this._decryptdata = levelkeys.identity.getDecryptData(this.sn); 1079 } 1080 } else if (!((_this$_decryptdata = this._decryptdata) != null && _this$_decryptdata.keyId)) { 1081 const keyFormats = Object.keys(levelkeys); 1082 if (keyFormats.length === 1) { 1083 const levelKey = this._decryptdata = levelkeys[keyFormats[0]] || null; 1084 if (levelKey) { 1085 this._decryptdata = levelKey.getDecryptData(this.sn, levelkeys); 1086 } 1087 } 1088 } 1089 return this._decryptdata; 1090 } 1091 get end() { 1092 return this.start + this.duration; 1093 } 1094 get endProgramDateTime() { 1095 if (this.programDateTime === null) { 1096 return null; 1097 } 1098 const duration = !isFiniteNumber(this.duration) ? 0 : this.duration; 1099 return this.programDateTime + duration * 1000; 1100 } 1101 get encrypted() { 1102 var _this$_decryptdata2; 1103 // At the m3u8-parser level we need to add support for manifest signalled keyformats 1104 // when we want the fragment to start reporting that it is encrypted. 1105 // Currently, keyFormat will only be set for identity keys 1106 if ((_this$_decryptdata2 = this._decryptdata) != null && _this$_decryptdata2.encrypted) { 1107 return true; 1108 } else if (this.levelkeys) { 1109 var _this$levelkeys$keyFo; 1110 const keyFormats = Object.keys(this.levelkeys); 1111 const len = keyFormats.length; 1112 if (len > 1 || len === 1 && (_this$levelkeys$keyFo = this.levelkeys[keyFormats[0]]) != null && _this$levelkeys$keyFo.encrypted) { 1113 return true; 1114 } 1115 } 1116 return false; 1117 } 1118 get programDateTime() { 1119 if (this._programDateTime === null && this.rawProgramDateTime) { 1120 this.programDateTime = Date.parse(this.rawProgramDateTime); 1121 } 1122 return this._programDateTime; 1123 } 1124 set programDateTime(value) { 1125 if (!isFiniteNumber(value)) { 1126 this._programDateTime = this.rawProgramDateTime = null; 1127 return; 1128 } 1129 this._programDateTime = value; 1130 } 1131 get ref() { 1132 if (!isMediaFragment(this)) { 1133 return null; 1134 } 1135 if (!this._ref) { 1136 this._ref = { 1137 base: this.base, 1138 start: this.start, 1139 duration: this.duration, 1140 sn: this.sn, 1141 programDateTime: this.programDateTime 1142 }; 1143 } 1144 return this._ref; 1145 } 1146 addStart(value) { 1147 this.setStart(this.start + value); 1148 } 1149 setStart(value) { 1150 this.start = value; 1151 if (this._ref) { 1152 this._ref.start = value; 1153 } 1154 } 1155 setDuration(value) { 1156 this.duration = value; 1157 if (this._ref) { 1158 this._ref.duration = value; 1159 } 1160 } 1161 setKeyFormat(keyFormat) { 1162 const levelkeys = this.levelkeys; 1163 if (levelkeys) { 1164 var _this$_decryptdata3; 1165 const key = levelkeys[keyFormat]; 1166 if (key && !((_this$_decryptdata3 = this._decryptdata) != null && _this$_decryptdata3.keyId)) { 1167 this._decryptdata = key.getDecryptData(this.sn, levelkeys); 1168 } 1169 } 1170 } 1171 abortRequests() { 1172 var _this$loader, _this$keyLoader; 1173 (_this$loader = this.loader) == null || _this$loader.abort(); 1174 (_this$keyLoader = this.keyLoader) == null || _this$keyLoader.abort(); 1175 } 1176 setElementaryStreamInfo(type, startPTS, endPTS, startDTS, endDTS, partial = false) { 1177 const { 1178 elementaryStreams 1179 } = this; 1180 const info = elementaryStreams[type]; 1181 if (!info) { 1182 elementaryStreams[type] = { 1183 startPTS, 1184 endPTS, 1185 startDTS, 1186 endDTS, 1187 partial 1188 }; 1189 return; 1190 } 1191 info.startPTS = Math.min(info.startPTS, startPTS); 1192 info.endPTS = Math.max(info.endPTS, endPTS); 1193 info.startDTS = Math.min(info.startDTS, startDTS); 1194 info.endDTS = Math.max(info.endDTS, endDTS); 1195 } 1196} 1197 1198/** 1199 * Object representing parsed data from an HLS Partial Segment. Found in {@link hls.js#LevelDetails.partList}. 1200 */ 1201class Part extends BaseSegment { 1202 constructor(partAttrs, frag, base, index, previous) { 1203 super(base); 1204 this.fragOffset = 0; 1205 this.duration = 0; 1206 this.gap = false; 1207 this.independent = false; 1208 this.relurl = void 0; 1209 this.fragment = void 0; 1210 this.index = void 0; 1211 this.duration = partAttrs.decimalFloatingPoint('DURATION'); 1212 this.gap = partAttrs.bool('GAP'); 1213 this.independent = partAttrs.bool('INDEPENDENT'); 1214 this.relurl = partAttrs.enumeratedString('URI'); 1215 this.fragment = frag; 1216 this.index = index; 1217 const byteRange = partAttrs.enumeratedString('BYTERANGE'); 1218 if (byteRange) { 1219 this.setByteRange(byteRange, previous); 1220 } 1221 if (previous) { 1222 this.fragOffset = previous.fragOffset + previous.duration; 1223 } 1224 } 1225 get start() { 1226 return this.fragment.start + this.fragOffset; 1227 } 1228 get end() { 1229 return this.start + this.duration; 1230 } 1231 get loaded() { 1232 const { 1233 elementaryStreams 1234 } = this; 1235 return !!(elementaryStreams.audio || elementaryStreams.video || elementaryStreams.audiovideo); 1236 } 1237} 1238function getOwnPropertyDescriptorFromPrototypeChain(object, property) { 1239 const prototype = Object.getPrototypeOf(object); 1240 if (prototype) { 1241 const propertyDescriptor = Object.getOwnPropertyDescriptor(prototype, property); 1242 if (propertyDescriptor) { 1243 return propertyDescriptor; 1244 } 1245 return getOwnPropertyDescriptorFromPrototypeChain(prototype, property); 1246 } 1247} 1248function makeEnumerable(object, property) { 1249 const d = getOwnPropertyDescriptorFromPrototypeChain(object, property); 1250 if (d) { 1251 d.enumerable = true; 1252 Object.defineProperty(object, property, d); 1253 } 1254} 1255 1256const UINT32_MAX$1 = Math.pow(2, 32) - 1; 1257const push = [].push; 1258 1259// We are using fixed track IDs for driving the MP4 remuxer 1260// instead of following the TS PIDs. 1261// There is no reason not to do this and some browsers/SourceBuffer-demuxers 1262// may not like if there are TrackID "switches" 1263// See https://github.com/video-dev/hls.js/issues/1331 1264// Here we are mapping our internal track types to constant MP4 track IDs 1265// With MSE currently one can only have one track of each, and we are muxing 1266// whatever video/audio rendition in them. 1267const RemuxerTrackIdConfig = { 1268 video: 1, 1269 audio: 2, 1270 id3: 3, 1271 text: 4 1272}; 1273function bin2str(data) { 1274 return String.fromCharCode.apply(null, data); 1275} 1276function readUint16(buffer, offset) { 1277 const val = buffer[offset] << 8 | buffer[offset + 1]; 1278 return val < 0 ? 65536 + val : val; 1279} 1280function readUint32(buffer, offset) { 1281 const val = readSint32(buffer, offset); 1282 return val < 0 ? 4294967296 + val : val; 1283} 1284function readUint64(buffer, offset) { 1285 let result = readUint32(buffer, offset); 1286 result *= Math.pow(2, 32); 1287 result += readUint32(buffer, offset + 4); 1288 return result; 1289} 1290function readSint32(buffer, offset) { 1291 return buffer[offset] << 24 | buffer[offset + 1] << 16 | buffer[offset + 2] << 8 | buffer[offset + 3]; 1292} 1293 1294// Find "moof" box 1295function hasMoofData(data) { 1296 const end = data.byteLength; 1297 for (let i = 0; i < end;) { 1298 const size = readUint32(data, i); 1299 if (size > 8 && data[i + 4] === 0x6d && data[i + 5] === 0x6f && data[i + 6] === 0x6f && data[i + 7] === 0x66) { 1300 return true; 1301 } 1302 i = size > 1 ? i + size : end; 1303 } 1304 return false; 1305} 1306 1307// Find the data for a box specified by its path 1308function findBox(data, path) { 1309 const results = []; 1310 if (!path.length) { 1311 // short-circuit the search for empty paths 1312 return results; 1313 } 1314 const end = data.byteLength; 1315 for (let i = 0; i < end;) { 1316 const size = readUint32(data, i); 1317 const type = bin2str(data.subarray(i + 4, i + 8)); 1318 const endbox = size > 1 ? i + size : end; 1319 if (type === path[0]) { 1320 if (path.length === 1) { 1321 // this is the end of the path and we've found the box we were 1322 // looking for 1323 results.push(data.subarray(i + 8, endbox)); 1324 } else { 1325 // recursively search for the next box along the path 1326 const subresults = findBox(data.subarray(i + 8, endbox), path.slice(1)); 1327 if (subresults.length) { 1328 push.apply(results, subresults); 1329 } 1330 } 1331 } 1332 i = endbox; 1333 } 1334 1335 // we've finished searching all of data 1336 return results; 1337} 1338function parseSegmentIndex(sidx) { 1339 const references = []; 1340 const version = sidx[0]; 1341 1342 // set initial offset, we skip the reference ID (not needed) 1343 let index = 8; 1344 const timescale = readUint32(sidx, index); 1345 index += 4; 1346 let earliestPresentationTime = 0; 1347 let firstOffset = 0; 1348 if (version === 0) { 1349 earliestPresentationTime = readUint32(sidx, index); 1350 firstOffset = readUint32(sidx, index + 4); 1351 index += 8; 1352 } else { 1353 earliestPresentationTime = readUint64(sidx, index); 1354 firstOffset = readUint64(sidx, index + 8); 1355 index += 16; 1356 } 1357 1358 // skip reserved 1359 index += 2; 1360 let startByte = sidx.length + firstOffset; 1361 const referencesCount = readUint16(sidx, index); 1362 index += 2; 1363 for (let i = 0; i < referencesCount; i++) { 1364 let referenceIndex = index; 1365 const referenceInfo = readUint32(sidx, referenceIndex); 1366 referenceIndex += 4; 1367 const referenceSize = referenceInfo & 0x7fffffff; 1368 const referenceType = (referenceInfo & 0x80000000) >>> 31; 1369 if (referenceType === 1) { 1370 logger.warn('SIDX has hierarchical references (not supported)'); 1371 return null; 1372 } 1373 const subsegmentDuration = readUint32(sidx, referenceIndex); 1374 referenceIndex += 4; 1375 references.push({ 1376 referenceSize, 1377 subsegmentDuration, 1378 // unscaled 1379 info: { 1380 duration: subsegmentDuration / timescale, 1381 start: startByte, 1382 end: startByte + referenceSize - 1 1383 } 1384 }); 1385 startByte += referenceSize; 1386 1387 // Skipping 1 bit for |startsWithSap|, 3 bits for |sapType|, and 28 bits 1388 // for |sapDelta|. 1389 referenceIndex += 4; 1390 1391 // skip to next ref 1392 index = referenceIndex; 1393 } 1394 return { 1395 earliestPresentationTime, 1396 timescale, 1397 version, 1398 referencesCount, 1399 references 1400 }; 1401} 1402 1403/** 1404 * Parses an MP4 initialization segment and extracts stream type and 1405 * timescale values for any declared tracks. Timescale values indicate the 1406 * number of clock ticks per second to assume for time-based values 1407 * elsewhere in the MP4. 1408 * 1409 * To determine the start time of an MP4, you need two pieces of 1410 * information: the timescale unit and the earliest base media decode 1411 * time. Multiple timescales can be specified within an MP4 but the 1412 * base media decode time is always expressed in the timescale from 1413 * the media header box for the track: 1414 * ``` 1415 * moov > trak > mdia > mdhd.timescale 1416 * moov > trak > mdia > hdlr 1417 * ``` 1418 * @param initSegment the bytes of the init segment 1419 * @returns a hash of track type to timescale values or null if 1420 * the init segment is malformed. 1421 */ 1422 1423function parseInitSegment(initSegment) { 1424 const result = []; 1425 const traks = findBox(initSegment, ['moov', 'trak']); 1426 for (let i = 0; i < traks.length; i++) { 1427 const trak = traks[i]; 1428 const tkhd = findBox(trak, ['tkhd'])[0]; 1429 if (tkhd) { 1430 let version = tkhd[0]; 1431 const trackId = readUint32(tkhd, version === 0 ? 12 : 20); 1432 const mdhd = findBox(trak, ['mdia', 'mdhd'])[0]; 1433 if (mdhd) { 1434 version = mdhd[0]; 1435 const timescale = readUint32(mdhd, version === 0 ? 12 : 20); 1436 const hdlr = findBox(trak, ['mdia', 'hdlr'])[0]; 1437 if (hdlr) { 1438 const hdlrType = bin2str(hdlr.subarray(8, 12)); 1439 const type = { 1440 soun: ElementaryStreamTypes.AUDIO, 1441 vide: ElementaryStreamTypes.VIDEO 1442 }[hdlrType]; 1443 // Parse codec details 1444 const stsdBox = findBox(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0]; 1445 const stsd = parseStsd(stsdBox); 1446 if (type) { 1447 // Add 'audio', 'video', and 'audiovideo' track records that will map to SourceBuffers 1448 result[trackId] = { 1449 timescale, 1450 type, 1451 stsd 1452 }; 1453 result[type] = _objectSpread2({ 1454 timescale, 1455 id: trackId 1456 }, stsd); 1457 } else { 1458 // Add 'meta' and other track records 1459 result[trackId] = { 1460 timescale, 1461 type: hdlrType, 1462 stsd 1463 }; 1464 } 1465 } 1466 } 1467 } 1468 } 1469 const trex = findBox(initSegment, ['moov', 'mvex', 'trex']); 1470 trex.forEach(trex => { 1471 const trackId = readUint32(trex, 4); 1472 const track = result[trackId]; 1473 if (track) { 1474 track.default = { 1475 duration: readUint32(trex, 12), 1476 flags: readUint32(trex, 20) 1477 }; 1478 } 1479 }); 1480 return result; 1481} 1482function parseStsd(stsd) { 1483 const sampleEntries = stsd.subarray(8); 1484 const sampleEntriesEnd = sampleEntries.subarray(8 + 78); 1485 const fourCC = bin2str(sampleEntries.subarray(4, 8)); 1486 let codec = fourCC; 1487 let supplemental; 1488 const encrypted = fourCC === 'enca' || fourCC === 'encv'; 1489 if (encrypted) { 1490 const encBox = findBox(sampleEntries, [fourCC])[0]; 1491 const encBoxChildren = encBox.subarray(fourCC === 'enca' ? 28 : 78); 1492 const sinfs = findBox(encBoxChildren, ['sinf']); 1493 sinfs.forEach(sinf => { 1494 const schm = findBox(sinf, ['schm'])[0]; 1495 if (schm) { 1496 const scheme = bin2str(schm.subarray(4, 8)); 1497 if (scheme === 'cbcs' || scheme === 'cenc') { 1498 const frma = findBox(sinf, ['frma'])[0]; 1499 if (frma) { 1500 // for encrypted content codec fourCC will be in frma 1501 codec = bin2str(frma); 1502 } 1503 } 1504 } 1505 }); 1506 } 1507 const codecFourCC = codec; 1508 switch (codec) { 1509 case 'avc1': 1510 case 'avc2': 1511 case 'avc3': 1512 case 'avc4': 1513 { 1514 // extract profile + compatibility + level out of avcC box 1515 const avcCBox = findBox(sampleEntriesEnd, ['avcC'])[0]; 1516 if (avcCBox && avcCBox.length > 3) { 1517 codec += '.' + toHex(avcCBox[1]) + toHex(avcCBox[2]) + toHex(avcCBox[3]); 1518 supplemental = parseSupplementalDoViCodec(codecFourCC === 'avc1' ? 'dva1' : 'dvav', sampleEntriesEnd); 1519 } 1520 break; 1521 } 1522 case 'mp4a': 1523 { 1524 const codecBox = findBox(sampleEntries, [fourCC])[0]; 1525 const esdsBox = findBox(codecBox.subarray(28), ['esds'])[0]; 1526 if (esdsBox && esdsBox.length > 7) { 1527 let i = 4; 1528 // ES Descriptor tag 1529 if (esdsBox[i++] !== 0x03) { 1530 break; 1531 } 1532 i = skipBERInteger(esdsBox, i); 1533 i += 2; // skip es_id; 1534 const flags = esdsBox[i++]; 1535 if (flags & 0x80) { 1536 i += 2; // skip dependency es_id 1537 } 1538 if (flags & 0x40) { 1539 i += esdsBox[i++]; // skip URL 1540 } 1541 // Decoder config descriptor 1542 if (esdsBox[i++] !== 0x04) { 1543 break; 1544 } 1545 i = skipBERInteger(esdsBox, i); 1546 const objectType = esdsBox[i++]; 1547 if (objectType === 0x40) { 1548 codec += '.' + toHex(objectType); 1549 } else { 1550 break; 1551 } 1552 i += 12; 1553 // Decoder specific info 1554 if (esdsBox[i++] !== 0x05) { 1555 break; 1556 } 1557 i = skipBERInteger(esdsBox, i); 1558 const firstByte = esdsBox[i++]; 1559 let audioObjectType = (firstByte & 0xf8) >> 3; 1560 if (audioObjectType === 31) { 1561 audioObjectType += 1 + ((firstByte & 0x7) << 3) + ((esdsBox[i] & 0xe0) >> 5); 1562 } 1563 codec += '.' + audioObjectType; 1564 } 1565 break; 1566 } 1567 case 'hvc1': 1568 case 'hev1': 1569 { 1570 const hvcCBox = findBox(sampleEntriesEnd, ['hvcC'])[0]; 1571 if (hvcCBox && hvcCBox.length > 12) { 1572 const profileByte = hvcCBox[1]; 1573 const profileSpace = ['', 'A', 'B', 'C'][profileByte >> 6]; 1574 const generalProfileIdc = profileByte & 0x1f; 1575 const profileCompat = readUint32(hvcCBox, 2); 1576 const tierFlag = (profileByte & 0x20) >> 5 ? 'H' : 'L'; 1577 const levelIDC = hvcCBox[12]; 1578 const constraintIndicator = hvcCBox.subarray(6, 12); 1579 codec += '.' + profileSpace + generalProfileIdc; 1580 codec += '.' + reverse32BitInt(profileCompat).toString(16).toUpperCase(); 1581 codec += '.' + tierFlag + levelIDC; 1582 let constraintString = ''; 1583 for (let i = constraintIndicator.length; i--;) { 1584 const byte = constraintIndicator[i]; 1585 if (byte || constraintString) { 1586 const encodedByte = byte.toString(16).toUpperCase(); 1587 constraintString = '.' + encodedByte + constraintString; 1588 } 1589 } 1590 codec += constraintString; 1591 } 1592 supplemental = parseSupplementalDoViCodec(codecFourCC == 'hev1' ? 'dvhe' : 'dvh1', sampleEntriesEnd); 1593 break; 1594 } 1595 case 'dvh1': 1596 case 'dvhe': 1597 case 'dvav': 1598 case 'dva1': 1599 case 'dav1': 1600 { 1601 codec = parseSupplementalDoViCodec(codec, sampleEntriesEnd) || codec; 1602 break; 1603 } 1604 case 'vp09': 1605 { 1606 const vpcCBox = findBox(sampleEntriesEnd, ['vpcC'])[0]; 1607 if (vpcCBox && vpcCBox.length > 6) { 1608 const profile = vpcCBox[4]; 1609 const level = vpcCBox[5]; 1610 const bitDepth = vpcCBox[6] >> 4 & 0x0f; 1611 codec += '.' + addLeadingZero(profile) + '.' + addLeadingZero(level) + '.' + addLeadingZero(bitDepth); 1612 } 1613 break; 1614 } 1615 case 'av01': 1616 { 1617 const av1CBox = findBox(sampleEntriesEnd, ['av1C'])[0]; 1618 if (av1CBox && av1CBox.length > 2) { 1619 const profile = av1CBox[1] >>> 5; 1620 const level = av1CBox[1] & 0x1f; 1621 const tierFlag = av1CBox[2] >>> 7 ? 'H' : 'M'; 1622 const highBitDepth = (av1CBox[2] & 0x40) >> 6; 1623 const twelveBit = (av1CBox[2] & 0x20) >> 5; 1624 const bitDepth = profile === 2 && highBitDepth ? twelveBit ? 12 : 10 : highBitDepth ? 10 : 8; 1625 const monochrome = (av1CBox[2] & 0x10) >> 4; 1626 const chromaSubsamplingX = (av1CBox[2] & 0x08) >> 3; 1627 const chromaSubsamplingY = (av1CBox[2] & 0x04) >> 2; 1628 const chromaSamplePosition = av1CBox[2] & 0x03; 1629 // TODO: parse color_description_present_flag 1630 // default it to BT.709/limited range for now 1631 // more info https://aomediacodec.github.io/av1-isobmff/#av1codecconfigurationbox-syntax 1632 const colorPrimaries = 1; 1633 const transferCharacteristics = 1; 1634 const matrixCoefficients = 1; 1635 const videoFullRangeFlag = 0; 1636 codec += '.' + profile + '.' + addLeadingZero(level) + tierFlag + '.' + addLeadingZero(bitDepth) + '.' + monochrome + '.' + chromaSubsamplingX + chromaSubsamplingY + chromaSamplePosition + '.' + addLeadingZero(colorPrimaries) + '.' + addLeadingZero(transferCharacteristics) + '.' + addLeadingZero(matrixCoefficients) + '.' + videoFullRangeFlag; 1637 supplemental = parseSupplementalDoViCodec('dav1', sampleEntriesEnd); 1638 } 1639 break; 1640 } 1641 } 1642 return { 1643 codec, 1644 encrypted, 1645 supplemental 1646 }; 1647} 1648function parseSupplementalDoViCodec(fourCC, sampleEntriesEnd) { 1649 const dvvCResult = findBox(sampleEntriesEnd, ['dvvC']); // used by DoVi Profile 8 to 10 1650 const dvXCBox = dvvCResult.length ? dvvCResult[0] : findBox(sampleEntriesEnd, ['dvcC'])[0]; // used by DoVi Profiles up to 7 and 20 1651 if (dvXCBox) { 1652 const doViProfile = dvXCBox[2] >> 1 & 0x7f; 1653 const doViLevel = dvXCBox[2] << 5 & 0x20 | dvXCBox[3] >> 3 & 0x1f; 1654 return fourCC + '.' + addLeadingZero(doViProfile) + '.' + addLeadingZero(doViLevel); 1655 } 1656} 1657function reverse32BitInt(val) { 1658 let result = 0; 1659 for (let i = 0; i < 32; i++) { 1660 result |= (val >> i & 1) << 32 - 1 - i; 1661 } 1662 return result >>> 0; 1663} 1664function skipBERInteger(bytes, i) { 1665 const limit = i + 5; 1666 while (bytes[i++] & 0x80 && i < limit) { 1667 /* do nothing */ 1668 } 1669 return i; 1670} 1671function toHex(x) { 1672 return ('0' + x.toString(16).toUpperCase()).slice(-2); 1673} 1674function addLeadingZero(num) { 1675 return (num < 10 ? '0' : '') + num; 1676} 1677function patchEncyptionData(initSegment, decryptdata) { 1678 if (!initSegment || !decryptdata) { 1679 return; 1680 } 1681 const keyId = decryptdata.keyId; 1682 if (keyId && decryptdata.isCommonEncryption) { 1683 applyToTencBoxes(initSegment, (tenc, isAudio) => { 1684 // Look for default key id (keyID offset is always 8 within the tenc box): 1685 const tencKeyId = tenc.subarray(8, 24); 1686 if (!tencKeyId.some(b => b !== 0)) { 1687 logger.log(`[eme] Patching keyId in 'enc${isAudio ? 'a' : 'v'}>sinf>>tenc' box: ${arrayToHex(tencKeyId)} -> ${arrayToHex(keyId)}`); 1688 tenc.set(keyId, 8); 1689 } 1690 }); 1691 } 1692} 1693function parseKeyIdsFromTenc(initSegment) { 1694 const keyIds = []; 1695 applyToTencBoxes(initSegment, tenc => keyIds.push(tenc.subarray(8, 24))); 1696 return keyIds; 1697} 1698function applyToTencBoxes(initSegment, predicate) { 1699 const traks = findBox(initSegment, ['moov', 'trak']); 1700 traks.forEach(trak => { 1701 const stsd = findBox(trak, ['mdia', 'minf', 'stbl', 'stsd'])[0]; 1702 if (!stsd) return; 1703 const sampleEntries = stsd.subarray(8); 1704 let encBoxes = findBox(sampleEntries, ['enca']); 1705 const isAudio = encBoxes.length > 0; 1706 if (!isAudio) { 1707 encBoxes = findBox(sampleEntries, ['encv']); 1708 } 1709 encBoxes.forEach(enc => { 1710 const encBoxChildren = isAudio ? enc.subarray(28) : enc.subarray(78); 1711 const sinfBoxes = findBox(encBoxChildren, ['sinf']); 1712 sinfBoxes.forEach(sinf => { 1713 const tenc = parseSinf(sinf); 1714 if (tenc) { 1715 predicate(tenc, isAudio); 1716 } 1717 }); 1718 }); 1719 }); 1720} 1721function parseSinf(sinf) { 1722 const schm = findBox(sinf, ['schm'])[0]; 1723 if (schm) { 1724 const scheme = bin2str(schm.subarray(4, 8)); 1725 if (scheme === 'cbcs' || scheme === 'cenc') { 1726 const tenc = findBox(sinf, ['schi', 'tenc'])[0]; 1727 if (tenc) { 1728 return tenc; 1729 } 1730 } 1731 } 1732} 1733 1734/* 1735 For Reference: 1736 aligned(8) class TrackFragmentHeaderBox 1737 extends FullBox(‘tfhd’, 0, tf_flags){ 1738 unsigned int(32) track_ID; 1739 // all the following are optional fields 1740 unsigned int(64) base_data_offset; 1741 unsigned int(32) sample_description_index; 1742 unsigned int(32) default_sample_duration; 1743 unsigned int(32) default_sample_size; 1744 unsigned int(32) default_sample_flags 1745 } 1746 */ 1747 1748function getSampleData(data, initData, logger) { 1749 const tracks = {}; 1750 const trafs = findBox(data, ['moof', 'traf']); 1751 for (let i = 0; i < trafs.length; i++) { 1752 const traf = trafs[i]; 1753 // There is only one tfhd & trun per traf 1754 // This is true for CMAF style content, and we should perhaps check the ftyp 1755 // and only look for a single trun then, but for ISOBMFF we should check 1756 // for multiple track runs. 1757 const tfhd = findBox(traf, ['tfhd'])[0]; 1758 // get the track id from the tfhd 1759 const id = readUint32(tfhd, 4); 1760 const track = initData[id]; 1761 if (!track) { 1762 continue; 1763 } 1764 tracks[id] || (tracks[id] = { 1765 start: NaN, 1766 duration: 0, 1767 sampleCount: 0, 1768 timescale: track.timescale, 1769 type: track.type 1770 }); 1771 const trackTimes = tracks[id]; 1772 // get start DTS 1773 const tfdt = findBox(traf, ['tfdt'])[0]; 1774 if (tfdt) { 1775 const version = tfdt[0]; 1776 let baseTime = readUint32(tfdt, 4); 1777 if (version === 1) { 1778 // If value is too large, assume signed 64-bit. Negative track fragment decode times are invalid, but they exist in the wild. 1779 // This prevents large values from being used for initPTS, which can cause playlist sync issues. 1780 // https://github.com/video-dev/hls.js/issues/5303 1781 if (baseTime === UINT32_MAX$1) { 1782 logger.warn(`[mp4-demuxer]: Ignoring assumed invalid signed 64-bit track fragment decode time`); 1783 } else { 1784 baseTime *= UINT32_MAX$1 + 1; 1785 baseTime += readUint32(tfdt, 8); 1786 } 1787 } 1788 if (isFiniteNumber(baseTime) && (!isFiniteNumber(trackTimes.start) || baseTime < trackTimes.start)) { 1789 trackTimes.start = baseTime; 1790 } 1791 } 1792 const trackDefault = track.default; 1793 const tfhdFlags = readUint32(tfhd, 0) | (trackDefault == null ? void 0 : trackDefault.flags); 1794 let defaultSampleDuration = (trackDefault == null ? void 0 : trackDefault.duration) || 0; 1795 if (tfhdFlags & 0x000008) { 1796 // 0x000008 indicates the presence of the default_sample_duration field 1797 if (tfhdFlags & 0x000002) { 1798 // 0x000002 indicates the presence of the sample_description_index field, which precedes default_sample_duration 1799 // If present, the default_sample_duration exists at byte offset 12 1800 defaultSampleDuration = readUint32(tfhd, 12); 1801 } else { 1802 // Otherwise, the duration is at byte offset 8 1803 defaultSampleDuration = readUint32(tfhd, 8); 1804 } 1805 } 1806 const truns = findBox(traf, ['trun']); 1807 let sampleDTS = trackTimes.start || 0; 1808 let rawDuration = 0; 1809 let sampleDuration = defaultSampleDuration; 1810 for (let j = 0; j < truns.length; j++) { 1811 const trun = truns[j]; 1812 const sampleCount = readUint32(trun, 4); 1813 const sampleIndex = trackTimes.sampleCount;
vendor: 2,995 bytes, lines 1814-1894
1814 trackTimes.sampleCount += sampleCount; 1815 // Get duration from samples 1816 const dataOffsetPresent = trun[3] & 0x01; 1817 const firstSampleFlagsPresent = trun[3] & 0x04; 1818 const sampleDurationPresent = trun[2] & 0x01; 1819 const sampleSizePresent = trun[2] & 0x02; 1820 const sampleFlagsPresent = trun[2] & 0x04; 1821 const sampleCompositionTimeOffsetPresent = trun[2] & 0x08; 1822 let offset = 8; 1823 let remaining = sampleCount; 1824 if (dataOffsetPresent) { 1825 offset += 4; 1826 } 1827 if (firstSampleFlagsPresent && sampleCount) { 1828 const isNonSyncSample = trun[offset + 1] & 0x01; 1829 if (!isNonSyncSample && trackTimes.keyFrameIndex === undefined) { 1830 trackTimes.keyFrameIndex = sampleIndex; 1831 } 1832 offset += 4; 1833 if (sampleDurationPresent) { 1834 sampleDuration = readUint32(trun, offset); 1835 offset += 4; 1836 } else { 1837 sampleDuration = defaultSampleDuration; 1838 } 1839 if (sampleSizePresent) { 1840 offset += 4; 1841 } 1842 if (sampleCompositionTimeOffsetPresent) { 1843 offset += 4; 1844 } 1845 sampleDTS += sampleDuration; 1846 rawDuration += sampleDuration; 1847 remaining--; 1848 } 1849 while (remaining--) { 1850 if (sampleDurationPresent) { 1851 sampleDuration = readUint32(trun, offset); 1852 offset += 4; 1853 } else { 1854 sampleDuration = defaultSampleDuration; 1855 } 1856 if (sampleSizePresent) { 1857 offset += 4; 1858 } 1859 if (sampleFlagsPresent) { 1860 const isNonSyncSample = trun[offset + 1] & 0x01; 1861 if (!isNonSyncSample) { 1862 if (trackTimes.keyFrameIndex === undefined) { 1863 trackTimes.keyFrameIndex = trackTimes.sampleCount - (remaining + 1); 1864 trackTimes.keyFrameStart = sampleDTS; 1865 } 1866 } 1867 offset += 4; 1868 } 1869 if (sampleCompositionTimeOffsetPresent) { 1870 offset += 4; 1871 } 1872 sampleDTS += sampleDuration; 1873 rawDuration += sampleDuration; 1874 } 1875 if (!rawDuration && defaultSampleDuration) { 1876 rawDuration += defaultSampleDuration * sampleCount; 1877 } 1878 } 1879 trackTimes.duration += rawDuration; 1880 } 1881 if (!Object.keys(tracks).some(trackId => tracks[trackId].duration)) { 1882 // If duration samples are not available in the traf use sidx subsegment_duration 1883 let sidxMinStart = Infinity; 1884 let sidxMaxEnd = 0; 1885 const sidxs = findBox(data, ['sidx']); 1886 for (let i = 0; i < sidxs.length; i++) { 1887 const sidx = parseSegmentIndex(sidxs[i]); 1888 if (sidx != null && sidx.references) { 1889 sidxMinStart = Math.min(sidxMinStart, sidx.earliestPresentationTime / sidx.timescale); 1890 const subSegmentDuration = sidx.references.reduce((dur, ref) => dur + ref.info.duration || 0, 0); 1891 sidxMaxEnd = Math.max(sidxMaxEnd, subSegmentDuration + sidx.earliestPresentationTime / sidx.timescale); 1892 } 1893 } 1894 if (sidxMaxEnd && isFiniteNumber(sidxMaxEnd)) {
vendor: 92,033 bytes, lines 1895-4324
1895 Object.keys(tracks).forEach(trackId => { 1896 if (!tracks[trackId].duration) { 1897 tracks[trackId].duration = sidxMaxEnd * tracks[trackId].timescale - tracks[trackId].start; 1898 } 1899 }); 1900 } 1901 } 1902 return tracks; 1903} 1904 1905// TODO: Check if the last moof+mdat pair is part of the valid range 1906function segmentValidRange(data) { 1907 const segmentedRange = { 1908 valid: null, 1909 remainder: null 1910 }; 1911 const moofs = findBox(data, ['moof']); 1912 if (moofs.length < 2) { 1913 segmentedRange.remainder = data; 1914 return segmentedRange; 1915 } 1916 const last = moofs[moofs.length - 1]; 1917 // Offset by 8 bytes; findBox offsets the start by as much 1918 segmentedRange.valid = data.slice(0, last.byteOffset - 8); 1919 segmentedRange.remainder = data.slice(last.byteOffset - 8); 1920 return segmentedRange; 1921} 1922function appendUint8Array(data1, data2) { 1923 const temp = new Uint8Array(data1.length + data2.length); 1924 temp.set(data1); 1925 temp.set(data2, data1.length); 1926 return temp; 1927} 1928function parseSamples(timeOffset, track) { 1929 const seiSamples = []; 1930 const videoData = track.samples; 1931 const timescale = track.timescale; 1932 const trackId = track.id; 1933 let isHEVCFlavor = false; 1934 const moofs = findBox(videoData, ['moof']); 1935 moofs.map(moof => { 1936 const moofOffset = moof.byteOffset - 8; 1937 const trafs = findBox(moof, ['traf']); 1938 trafs.map(traf => { 1939 // get the base media decode time from the tfdt 1940 const baseTime = findBox(traf, ['tfdt']).map(tfdt => { 1941 const version = tfdt[0]; 1942 let result = readUint32(tfdt, 4); 1943 if (version === 1) { 1944 result *= Math.pow(2, 32); 1945 result += readUint32(tfdt, 8); 1946 } 1947 return result / timescale; 1948 })[0]; 1949 if (baseTime !== undefined) { 1950 timeOffset = baseTime; 1951 } 1952 return findBox(traf, ['tfhd']).map(tfhd => { 1953 const id = readUint32(tfhd, 4); 1954 const tfhdFlags = readUint32(tfhd, 0) & 0xffffff; 1955 const baseDataOffsetPresent = (tfhdFlags & 0x000001) !== 0; 1956 const sampleDescriptionIndexPresent = (tfhdFlags & 0x000002) !== 0; 1957 const defaultSampleDurationPresent = (tfhdFlags & 0x000008) !== 0; 1958 let defaultSampleDuration = 0; 1959 const defaultSampleSizePresent = (tfhdFlags & 0x000010) !== 0; 1960 let defaultSampleSize = 0; 1961 const defaultSampleFlagsPresent = (tfhdFlags & 0x000020) !== 0; 1962 let tfhdOffset = 8; 1963 if (id === trackId) { 1964 if (baseDataOffsetPresent) { 1965 tfhdOffset += 8; 1966 } 1967 if (sampleDescriptionIndexPresent) { 1968 tfhdOffset += 4; 1969 } 1970 if (defaultSampleDurationPresent) { 1971 defaultSampleDuration = readUint32(tfhd, tfhdOffset); 1972 tfhdOffset += 4; 1973 } 1974 if (defaultSampleSizePresent) { 1975 defaultSampleSize = readUint32(tfhd, tfhdOffset); 1976 tfhdOffset += 4; 1977 } 1978 if (defaultSampleFlagsPresent) { 1979 tfhdOffset += 4; 1980 } 1981 if (track.type === 'video') { 1982 isHEVCFlavor = isHEVC(track.codec); 1983 } 1984 findBox(traf, ['trun']).map(trun => { 1985 const version = trun[0]; 1986 const flags = readUint32(trun, 0) & 0xffffff; 1987 const dataOffsetPresent = (flags & 0x000001) !== 0; 1988 let dataOffset = 0; 1989 const firstSampleFlagsPresent = (flags & 0x000004) !== 0; 1990 const sampleDurationPresent = (flags & 0x000100) !== 0; 1991 let sampleDuration = 0; 1992 const sampleSizePresent = (flags & 0x000200) !== 0; 1993 let sampleSize = 0; 1994 const sampleFlagsPresent = (flags & 0x000400) !== 0; 1995 const sampleCompositionOffsetsPresent = (flags & 0x000800) !== 0; 1996 let compositionOffset = 0; 1997 const sampleCount = readUint32(trun, 4); 1998 let trunOffset = 8; // past version, flags, and sample count 1999 2000 if (dataOffsetPresent) { 2001 dataOffset = readUint32(trun, trunOffset); 2002 trunOffset += 4; 2003 } 2004 if (firstSampleFlagsPresent) { 2005 trunOffset += 4; 2006 } 2007 let sampleOffset = dataOffset + moofOffset; 2008 for (let ix = 0; ix < sampleCount; ix++) { 2009 if (sampleDurationPresent) { 2010 sampleDuration = readUint32(trun, trunOffset); 2011 trunOffset += 4; 2012 } else { 2013 sampleDuration = defaultSampleDuration; 2014 } 2015 if (sampleSizePresent) { 2016 sampleSize = readUint32(trun, trunOffset); 2017 trunOffset += 4; 2018 } else { 2019 sampleSize = defaultSampleSize; 2020 } 2021 if (sampleFlagsPresent) { 2022 trunOffset += 4; 2023 } 2024 if (sampleCompositionOffsetsPresent) { 2025 if (version === 0) { 2026 compositionOffset = readUint32(trun, trunOffset); 2027 } else { 2028 compositionOffset = readSint32(trun, trunOffset); 2029 } 2030 trunOffset += 4; 2031 } 2032 if (track.type === ElementaryStreamTypes.VIDEO) { 2033 let naluTotalSize = 0; 2034 while (naluTotalSize < sampleSize) { 2035 const naluSize = readUint32(videoData, sampleOffset); 2036 sampleOffset += 4; 2037 if (isSEIMessage(isHEVCFlavor, videoData[sampleOffset])) { 2038 const data = videoData.subarray(sampleOffset, sampleOffset + naluSize); 2039 parseSEIMessageFromNALu(data, isHEVCFlavor ? 2 : 1, timeOffset + compositionOffset / timescale, seiSamples); 2040 } 2041 sampleOffset += naluSize; 2042 naluTotalSize += naluSize + 4; 2043 } 2044 } 2045 timeOffset += sampleDuration / timescale; 2046 } 2047 }); 2048 } 2049 }); 2050 }); 2051 }); 2052 return seiSamples; 2053} 2054function isHEVC(codec) { 2055 if (!codec) { 2056 return false; 2057 } 2058 const baseCodec = codec.substring(0, 4); 2059 return baseCodec === 'hvc1' || baseCodec === 'hev1' || 2060 // Dolby Vision 2061 baseCodec === 'dvh1' || baseCodec === 'dvhe'; 2062} 2063function isSEIMessage(isHEVCFlavor, naluHeader) { 2064 if (isHEVCFlavor) { 2065 const naluType = naluHeader >> 1 & 0x3f; 2066 return naluType === 39 || naluType === 40; 2067 } else { 2068 const naluType = naluHeader & 0x1f; 2069 return naluType === 6; 2070 } 2071} 2072function parseSEIMessageFromNALu(unescapedData, headerSize, pts, samples) { 2073 const data = discardEPB(unescapedData); 2074 let seiPtr = 0; 2075 // skip nal header 2076 seiPtr += headerSize; 2077 let payloadType = 0; 2078 let payloadSize = 0; 2079 let b = 0; 2080 while (seiPtr < data.length) { 2081 payloadType = 0; 2082 do { 2083 if (seiPtr >= data.length) { 2084 break; 2085 } 2086 b = data[seiPtr++]; 2087 payloadType += b; 2088 } while (b === 0xff); 2089 2090 // Parse payload size. 2091 payloadSize = 0; 2092 do { 2093 if (seiPtr >= data.length) { 2094 break; 2095 } 2096 b = data[seiPtr++]; 2097 payloadSize += b; 2098 } while (b === 0xff); 2099 const leftOver = data.length - seiPtr; 2100 // Create a variable to process the payload 2101 let payPtr = seiPtr; 2102 2103 // Increment the seiPtr to the end of the payload 2104 if (payloadSize < leftOver) { 2105 seiPtr += payloadSize; 2106 } else if (payloadSize > leftOver) { 2107 // Some type of corruption has happened? 2108 logger.error(`Malformed SEI payload. ${payloadSize} is too small, only ${leftOver} bytes left to parse.`); 2109 // We might be able to parse some data, but let's be safe and ignore it. 2110 break; 2111 } 2112 if (payloadType === 4) { 2113 const countryCode = data[payPtr++]; 2114 if (countryCode === 181) { 2115 const providerCode = readUint16(data, payPtr); 2116 payPtr += 2; 2117 if (providerCode === 49) { 2118 const userStructure = readUint32(data, payPtr); 2119 payPtr += 4; 2120 if (userStructure === 0x47413934) { 2121 const userDataType = data[payPtr++]; 2122 2123 // Raw CEA-608 bytes wrapped in CEA-708 packet 2124 if (userDataType === 3) { 2125 const firstByte = data[payPtr++]; 2126 const totalCCs = 0x1f & firstByte; 2127 const enabled = 0x40 & firstByte; 2128 const totalBytes = enabled ? 2 + totalCCs * 3 : 0; 2129 const byteArray = new Uint8Array(totalBytes); 2130 if (enabled) { 2131 byteArray[0] = firstByte; 2132 for (let i = 1; i < totalBytes; i++) { 2133 byteArray[i] = data[payPtr++]; 2134 } 2135 } 2136 samples.push({ 2137 type: userDataType, 2138 payloadType, 2139 pts, 2140 bytes: byteArray 2141 }); 2142 } 2143 } 2144 } 2145 } 2146 } else if (payloadType === 5) { 2147 if (payloadSize > 16) { 2148 const uuidStrArray = []; 2149 for (let i = 0; i < 16; i++) { 2150 const _b = data[payPtr++].toString(16); 2151 uuidStrArray.push(_b.length == 1 ? '0' + _b : _b); 2152 if (i === 3 || i === 5 || i === 7 || i === 9) { 2153 uuidStrArray.push('-'); 2154 } 2155 } 2156 const length = payloadSize - 16; 2157 const userDataBytes = new Uint8Array(length); 2158 for (let i = 0; i < length; i++) { 2159 userDataBytes[i] = data[payPtr++]; 2160 } 2161 samples.push({ 2162 payloadType, 2163 pts, 2164 uuid: uuidStrArray.join(''), 2165 userData: utf8ArrayToStr(userDataBytes), 2166 userDataBytes 2167 }); 2168 } 2169 } 2170 } 2171} 2172 2173/** 2174 * remove Emulation Prevention bytes from a RBSP 2175 */ 2176function discardEPB(data) { 2177 const length = data.byteLength; 2178 const EPBPositions = []; 2179 let i = 1; 2180 2181 // Find all `Emulation Prevention Bytes` 2182 while (i < length - 2) { 2183 if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) { 2184 EPBPositions.push(i + 2); 2185 i += 2; 2186 } else { 2187 i++; 2188 } 2189 } 2190 2191 // If no Emulation Prevention Bytes were found just return the original 2192 // array 2193 if (EPBPositions.length === 0) { 2194 return data; 2195 } 2196 2197 // Create a new array to hold the NAL unit data 2198 const newLength = length - EPBPositions.length; 2199 const newData = new Uint8Array(newLength); 2200 let sourceIndex = 0; 2201 for (i = 0; i < newLength; sourceIndex++, i++) { 2202 if (sourceIndex === EPBPositions[0]) { 2203 // Skip this byte 2204 sourceIndex++; 2205 // Remove this position index 2206 EPBPositions.shift(); 2207 } 2208 newData[i] = data[sourceIndex]; 2209 } 2210 return newData; 2211} 2212function parseEmsg(data) { 2213 const version = data[0]; 2214 let schemeIdUri = ''; 2215 let value = ''; 2216 let timeScale = 0; 2217 let presentationTimeDelta = 0; 2218 let presentationTime = 0; 2219 let eventDuration = 0; 2220 let id = 0; 2221 let offset = 0; 2222 if (version === 0) { 2223 while (bin2str(data.subarray(offset, offset + 1)) !== '\0') { 2224 schemeIdUri += bin2str(data.subarray(offset, offset + 1)); 2225 offset += 1; 2226 } 2227 schemeIdUri += bin2str(data.subarray(offset, offset + 1)); 2228 offset += 1; 2229 while (bin2str(data.subarray(offset, offset + 1)) !== '\0') { 2230 value += bin2str(data.subarray(offset, offset + 1)); 2231 offset += 1; 2232 } 2233 value += bin2str(data.subarray(offset, offset + 1)); 2234 offset += 1; 2235 timeScale = readUint32(data, 12); 2236 presentationTimeDelta = readUint32(data, 16); 2237 eventDuration = readUint32(data, 20); 2238 id = readUint32(data, 24); 2239 offset = 28; 2240 } else if (version === 1) { 2241 offset += 4; 2242 timeScale = readUint32(data, offset); 2243 offset += 4; 2244 const leftPresentationTime = readUint32(data, offset); 2245 offset += 4; 2246 const rightPresentationTime = readUint32(data, offset); 2247 offset += 4; 2248 presentationTime = 2 ** 32 * leftPresentationTime + rightPresentationTime; 2249 if (!isSafeInteger(presentationTime)) { 2250 presentationTime = Number.MAX_SAFE_INTEGER; 2251 logger.warn('Presentation time exceeds safe integer limit and wrapped to max safe integer in parsing emsg box'); 2252 } 2253 eventDuration = readUint32(data, offset); 2254 offset += 4; 2255 id = readUint32(data, offset); 2256 offset += 4; 2257 while (bin2str(data.subarray(offset, offset + 1)) !== '\0') { 2258 schemeIdUri += bin2str(data.subarray(offset, offset + 1)); 2259 offset += 1; 2260 } 2261 schemeIdUri += bin2str(data.subarray(offset, offset + 1)); 2262 offset += 1; 2263 while (bin2str(data.subarray(offset, offset + 1)) !== '\0') { 2264 value += bin2str(data.subarray(offset, offset + 1)); 2265 offset += 1; 2266 } 2267 value += bin2str(data.subarray(offset, offset + 1)); 2268 offset += 1; 2269 } 2270 const payload = data.subarray(offset, data.byteLength); 2271 return { 2272 schemeIdUri, 2273 value, 2274 timeScale, 2275 presentationTime, 2276 presentationTimeDelta, 2277 eventDuration, 2278 id, 2279 payload 2280 }; 2281} 2282function mp4Box(type, ...payload) { 2283 const len = payload.length; 2284 let size = 8; 2285 let i = len; 2286 while (i--) { 2287 size += payload[i].byteLength; 2288 } 2289 const result = new Uint8Array(size); 2290 result[0] = size >> 24 & 0xff; 2291 result[1] = size >> 16 & 0xff; 2292 result[2] = size >> 8 & 0xff; 2293 result[3] = size & 0xff; 2294 result.set(type, 4); 2295 for (i = 0, size = 8; i < len; i++) { 2296 result.set(payload[i], size); 2297 size += payload[i].byteLength; 2298 } 2299 return result; 2300} 2301function mp4pssh(systemId, keyids, data) { 2302 if (systemId.byteLength !== 16) { 2303 throw new RangeError('Invalid system id'); 2304 } 2305 let version; 2306 let kids; 2307 { 2308 version = 0; 2309 kids = new Uint8Array(); 2310 } 2311 let kidCount; 2312 if (version > 0) { 2313 kidCount = new Uint8Array(4); 2314 if (keyids.length > 0) { 2315 new DataView(kidCount.buffer).setUint32(0, keyids.length, false); 2316 } 2317 } else { 2318 kidCount = new Uint8Array(); 2319 } 2320 const dataSize = new Uint8Array(4); 2321 if (data.byteLength > 0) { 2322 new DataView(dataSize.buffer).setUint32(0, data.byteLength, false); 2323 } 2324 return mp4Box([112, 115, 115, 104], new Uint8Array([version, 0x00, 0x00, 0x00 // Flags 2325 ]), systemId, 2326 // 16 bytes 2327 kidCount, kids, dataSize, data); 2328} 2329function parseMultiPssh(initData) { 2330 const results = []; 2331 if (initData instanceof ArrayBuffer) { 2332 const length = initData.byteLength; 2333 let offset = 0; 2334 while (offset + 32 < length) { 2335 const view = new DataView(initData, offset); 2336 const pssh = parsePssh(view); 2337 results.push(pssh); 2338 offset += pssh.size; 2339 } 2340 } 2341 return results; 2342} 2343function parsePssh(view) { 2344 const size = view.getUint32(0); 2345 const offset = view.byteOffset; 2346 const length = view.byteLength; 2347 if (length < size) { 2348 return { 2349 offset, 2350 size: length 2351 }; 2352 } 2353 const type = view.getUint32(4); 2354 if (type !== 0x70737368) { 2355 return { 2356 offset, 2357 size 2358 }; 2359 } 2360 const version = view.getUint32(8) >>> 24; 2361 if (version !== 0 && version !== 1) { 2362 return { 2363 offset, 2364 size 2365 }; 2366 } 2367 const buffer = view.buffer; 2368 const systemId = arrayToHex(new Uint8Array(buffer, offset + 12, 16)); 2369 let kids = null; 2370 let data = null; 2371 let dataSizeOffset = 0; 2372 if (version === 0) { 2373 dataSizeOffset = 28; 2374 } else { 2375 const kidCounts = view.getUint32(28); 2376 if (!kidCounts || length < 32 + kidCounts * 16) { 2377 return { 2378 offset, 2379 size 2380 }; 2381 } 2382 kids = []; 2383 for (let i = 0; i < kidCounts; i++) { 2384 kids.push(new Uint8Array(buffer, offset + 32 + i * 16, 16)); 2385 } 2386 dataSizeOffset = 32 + kidCounts * 16; 2387 } 2388 if (!dataSizeOffset) { 2389 return { 2390 offset, 2391 size 2392 }; 2393 } 2394 const dataSizeOrKidCount = view.getUint32(dataSizeOffset); 2395 if (size - 32 < dataSizeOrKidCount) { 2396 return { 2397 offset, 2398 size 2399 }; 2400 } 2401 data = new Uint8Array(buffer, offset + dataSizeOffset + 4, dataSizeOrKidCount); 2402 return { 2403 version, 2404 systemId, 2405 kids, 2406 data, 2407 offset, 2408 size 2409 }; 2410} 2411 2412const userAgentHevcSupportIsInaccurate = () => { 2413 return /\(Windows.+Firefox\//i.test(navigator.userAgent); 2414}; 2415 2416// from http://mp4ra.org/codecs.html 2417// values indicate codec selection preference (lower is higher priority) 2418const sampleEntryCodesISO = { 2419 audio: { 2420 a3ds: 1, 2421 'ac-3': 0.95, 2422 'ac-4': 1, 2423 alac: 0.9, 2424 alaw: 1, 2425 dra1: 1, 2426 'dts+': 1, 2427 'dts-': 1, 2428 dtsc: 1, 2429 dtse: 1, 2430 dtsh: 1, 2431 'ec-3': 0.9, 2432 enca: 1, 2433 fLaC: 0.9, 2434 // MP4-RA listed codec entry for FLAC 2435 flac: 0.9, 2436 // legacy browser codec name for FLAC 2437 FLAC: 0.9, 2438 // some manifests may list "FLAC" with Apple's tools 2439 g719: 1, 2440 g726: 1, 2441 m4ae: 1, 2442 mha1: 1, 2443 mha2: 1, 2444 mhm1: 1, 2445 mhm2: 1, 2446 mlpa: 1, 2447 mp4a: 1, 2448 'raw ': 1, 2449 Opus: 1, 2450 opus: 1, 2451 // browsers expect this to be lowercase despite MP4RA says 'Opus' 2452 samr: 1, 2453 sawb: 1, 2454 sawp: 1, 2455 sevc: 1, 2456 sqcp: 1, 2457 ssmv: 1, 2458 twos: 1, 2459 ulaw: 1 2460 }, 2461 video: { 2462 avc1: 1, 2463 avc2: 1, 2464 avc3: 1, 2465 avc4: 1, 2466 avcp: 1, 2467 av01: 0.8, 2468 dav1: 0.8, 2469 drac: 1, 2470 dva1: 1, 2471 dvav: 1, 2472 dvh1: 0.7, 2473 dvhe: 0.7, 2474 encv: 1, 2475 hev1: 0.75, 2476 hvc1: 0.75, 2477 mjp2: 1, 2478 mp4v: 1, 2479 mvc1: 1, 2480 mvc2: 1, 2481 mvc3: 1, 2482 mvc4: 1, 2483 resv: 1, 2484 rv60: 1, 2485 s263: 1, 2486 svc1: 1, 2487 svc2: 1, 2488 'vc-1': 1, 2489 vp08: 1, 2490 vp09: 0.9 2491 }, 2492 text: { 2493 stpp: 1, 2494 wvtt: 1 2495 } 2496}; 2497function isCodecType(codec, type) { 2498 const typeCodes = sampleEntryCodesISO[type]; 2499 return !!typeCodes && !!typeCodes[codec.slice(0, 4)]; 2500} 2501function areCodecsMediaSourceSupported(codecs, type, preferManagedMediaSource = true) { 2502 return !codecs.split(',').some(codec => !isCodecMediaSourceSupported(codec, type, preferManagedMediaSource)); 2503} 2504function isCodecMediaSourceSupported(codec, type, preferManagedMediaSource = true) { 2505 var _MediaSource$isTypeSu; 2506 const MediaSource = getMediaSource(preferManagedMediaSource); 2507 return (_MediaSource$isTypeSu = MediaSource == null ? void 0 : MediaSource.isTypeSupported(mimeTypeForCodec(codec, type))) != null ? _MediaSource$isTypeSu : false; 2508} 2509function mimeTypeForCodec(codec, type) { 2510 return `${type}/mp4;codecs=${codec}`; 2511} 2512function videoCodecPreferenceValue(videoCodec) { 2513 if (videoCodec) { 2514 const fourCC = videoCodec.substring(0, 4); 2515 return sampleEntryCodesISO.video[fourCC]; 2516 } 2517 return 2; 2518} 2519function codecsSetSelectionPreferenceValue(codecSet) { 2520 const limitedHevcSupport = userAgentHevcSupportIsInaccurate(); 2521 return codecSet.split(',').reduce((num, fourCC) => { 2522 const lowerPriority = limitedHevcSupport && isHEVC(fourCC); 2523 const preferenceValue = lowerPriority ? 9 : sampleEntryCodesISO.video[fourCC]; 2524 if (preferenceValue) { 2525 return (preferenceValue * 2 + num) / (num ? 3 : 2); 2526 } 2527 return (sampleEntryCodesISO.audio[fourCC] + num) / (num ? 2 : 1); 2528 }, 0); 2529} 2530const CODEC_COMPATIBLE_NAMES = {}; 2531function getCodecCompatibleNameLower(lowerCaseCodec, preferManagedMediaSource = true) { 2532 if (CODEC_COMPATIBLE_NAMES[lowerCaseCodec]) { 2533 return CODEC_COMPATIBLE_NAMES[lowerCaseCodec]; 2534 } 2535 const codecsToCheck = { 2536 // Idealy fLaC and Opus would be first (spec-compliant) but 2537 // some browsers will report that fLaC is supported then fail. 2538 // see: https://bugs.chromium.org/p/chromium/issues/detail?id=1422728 2539 flac: ['flac', 'fLaC', 'FLAC'], 2540 opus: ['opus', 'Opus'], 2541 // Replace audio codec info if browser does not support mp4a.40.34, 2542 // and demuxer can fallback to 'audio/mpeg' or 'audio/mp4;codecs="mp3"' 2543 'mp4a.40.34': ['mp3'] 2544 }[lowerCaseCodec]; 2545 for (let i = 0; i < codecsToCheck.length; i++) { 2546 var _getMediaSource; 2547 if (isCodecMediaSourceSupported(codecsToCheck[i], 'audio', preferManagedMediaSource)) { 2548 CODEC_COMPATIBLE_NAMES[lowerCaseCodec] = codecsToCheck[i]; 2549 return codecsToCheck[i]; 2550 } else if (codecsToCheck[i] === 'mp3' && (_getMediaSource = getMediaSource(preferManagedMediaSource)) != null && _getMediaSource.isTypeSupported('audio/mpeg')) { 2551 return ''; 2552 } 2553 } 2554 return lowerCaseCodec; 2555} 2556const AUDIO_CODEC_REGEXP = /flac|opus|mp4a\.40\.34/i; 2557function getCodecCompatibleName(codec, preferManagedMediaSource = true) { 2558 return codec.replace(AUDIO_CODEC_REGEXP, m => getCodecCompatibleNameLower(m.toLowerCase(), preferManagedMediaSource)); 2559} 2560function replaceVideoCodec(originalCodecs, newVideoCodec) { 2561 const codecs = []; 2562 if (originalCodecs) { 2563 const allCodecs = originalCodecs.split(','); 2564 for (let i = 0; i < allCodecs.length; i++) { 2565 if (!isCodecType(allCodecs[i], 'video')) { 2566 codecs.push(allCodecs[i]); 2567 } 2568 } 2569 } 2570 if (newVideoCodec) { 2571 codecs.push(newVideoCodec); 2572 } 2573 return codecs.join(','); 2574} 2575function pickMostCompleteCodecName(parsedCodec, levelCodec) { 2576 // Parsing of mp4a codecs strings in mp4-tools from media is incomplete as of d8c6c7a 2577 // so use level codec is parsed codec is unavailable or incomplete 2578 if (parsedCodec && (parsedCodec.length > 4 || ['ac-3', 'ec-3', 'alac', 'fLaC', 'Opus'].indexOf(parsedCodec) !== -1)) { 2579 if (isCodecSupportedAsType(parsedCodec, 'audio') || isCodecSupportedAsType(parsedCodec, 'video')) { 2580 return parsedCodec; 2581 } 2582 } 2583 if (levelCodec) { 2584 const levelCodecs = levelCodec.split(','); 2585 if (levelCodecs.length > 1) { 2586 if (parsedCodec) { 2587 for (let i = levelCodecs.length; i--;) { 2588 if (levelCodecs[i].substring(0, 4) === parsedCodec.substring(0, 4)) { 2589 return levelCodecs[i]; 2590 } 2591 } 2592 } 2593 return levelCodecs[0]; 2594 } 2595 } 2596 return levelCodec || parsedCodec; 2597} 2598function isCodecSupportedAsType(codec, type) { 2599 return isCodecType(codec, type) && isCodecMediaSourceSupported(codec, type); 2600} 2601function convertAVC1ToAVCOTI(videoCodecs) { 2602 // Convert avc1 codec string from RFC-4281 to RFC-6381 for MediaSource.isTypeSupported 2603 // Examples: avc1.66.30 to avc1.42001e and avc1.77.30,avc1.66.30 to avc1.4d001e,avc1.42001e. 2604 const codecs = videoCodecs.split(','); 2605 for (let i = 0; i < codecs.length; i++) { 2606 const avcdata = codecs[i].split('.'); 2607 // only convert codec strings starting with avc1 (Examples: avc1.64001f,dvh1.05.07) 2608 if (avcdata.length > 2 && avcdata[0] === 'avc1') { 2609 codecs[i] = `avc1.${parseInt(avcdata[1]).toString(16)}${('000' + parseInt(avcdata[2]).toString(16)).slice(-4)}`; 2610 } 2611 } 2612 return codecs.join(','); 2613} 2614function fillInMissingAV01Params(videoCodec) { 2615 // Used to fill in incomplete AV1 playlist CODECS strings for mediaCapabilities.decodingInfo queries 2616 if (videoCodec.startsWith('av01.')) { 2617 const av1params = videoCodec.split('.'); 2618 const placeholders = ['0', '111', '01', '01', '01', '0']; 2619 for (let i = av1params.length; i > 4 && i < 10; i++) { 2620 av1params[i] = placeholders[i - 4]; 2621 } 2622 return av1params.join('.'); 2623 } 2624 return videoCodec; 2625} 2626function getM2TSSupportedAudioTypes(preferManagedMediaSource) { 2627 const MediaSource = getMediaSource(preferManagedMediaSource) || { 2628 isTypeSupported: () => false 2629 }; 2630 return { 2631 mpeg: MediaSource.isTypeSupported('audio/mpeg'), 2632 mp3: MediaSource.isTypeSupported('audio/mp4; codecs="mp3"'), 2633 ac3: MediaSource.isTypeSupported('audio/mp4; codecs="ac-3"') 2634 }; 2635} 2636function getCodecsForMimeType(mimeType) { 2637 return mimeType.replace(/^.+codecs=["']?([^"']+).*$/, '$1'); 2638} 2639 2640// @ts-ignore 2641const supportedResult = { 2642 supported: true, 2643 powerEfficient: true, 2644 smooth: true 2645 // keySystemAccess: null, 2646}; 2647 2648// @ts-ignore 2649const unsupportedResult = { 2650 supported: false, 2651 smooth: false, 2652 powerEfficient: false 2653 // keySystemAccess: null, 2654}; 2655const SUPPORTED_INFO_DEFAULT = { 2656 supported: true, 2657 configurations: [], 2658 decodingInfoResults: [supportedResult] 2659}; 2660function getUnsupportedResult(error, configurations) { 2661 return { 2662 supported: false, 2663 configurations, 2664 decodingInfoResults: [unsupportedResult], 2665 error 2666 }; 2667} 2668function requiresMediaCapabilitiesDecodingInfo(level, audioTracksByGroup, currentVideoRange, currentFrameRate, currentBw, audioPreference) { 2669 // Only test support when configuration is exceeds minimum options 2670 const videoCodecs = level.videoCodec; 2671 const audioGroups = level.audioCodec ? level.audioGroups : null; 2672 const audioCodecPreference = audioPreference == null ? void 0 : audioPreference.audioCodec; 2673 const channelsPreference = audioPreference == null ? void 0 : audioPreference.channels; 2674 const maxChannels = channelsPreference ? parseInt(channelsPreference) : audioCodecPreference ? Infinity : 2; 2675 let audioChannels = null; 2676 if (audioGroups != null && audioGroups.length) { 2677 try { 2678 if (audioGroups.length === 1 && audioGroups[0]) { 2679 audioChannels = audioTracksByGroup.groups[audioGroups[0]].channels; 2680 } else { 2681 audioChannels = audioGroups.reduce((acc, groupId) => { 2682 if (groupId) { 2683 const audioTrackGroup = audioTracksByGroup.groups[groupId]; 2684 if (!audioTrackGroup) { 2685 throw new Error(`Audio track group ${groupId} not found`); 2686 } 2687 // Sum all channel key values 2688 Object.keys(audioTrackGroup.channels).forEach(key => { 2689 acc[key] = (acc[key] || 0) + audioTrackGroup.channels[key]; 2690 }); 2691 } 2692 return acc; 2693 }, { 2694 2: 0 2695 }); 2696 } 2697 } catch (error) { 2698 return true; 2699 } 2700 } 2701 return videoCodecs !== undefined && ( 2702 // Force media capabilities check for HEVC to avoid failure on Windows 2703 videoCodecs.split(',').some(videoCodec => isHEVC(videoCodec)) || level.width > 1920 && level.height > 1088 || level.height > 1920 && level.width > 1088 || level.frameRate > Math.max(currentFrameRate, 30) || level.videoRange !== 'SDR' && level.videoRange !== currentVideoRange || level.bitrate > Math.max(currentBw, 8e6)) || !!audioChannels && isFiniteNumber(maxChannels) && Object.keys(audioChannels).some(channels => parseInt(channels) > maxChannels); 2704} 2705function getMediaDecodingInfoPromise(level, audioTracksByGroup, mediaCapabilities, cache = {}) { 2706 const videoCodecs = level.videoCodec; 2707 if (!videoCodecs && !level.audioCodec || !mediaCapabilities) { 2708 return Promise.resolve(SUPPORTED_INFO_DEFAULT); 2709 } 2710 const configurations = []; 2711 const videoDecodeList = makeVideoConfigurations(level); 2712 const videoCount = videoDecodeList.length; 2713 const audioDecodeList = makeAudioConfigurations(level, audioTracksByGroup, videoCount > 0); 2714 const audioCount = audioDecodeList.length; 2715 for (let i = videoCount || 1 * audioCount || 1; i--;) { 2716 const configuration = { 2717 type: 'media-source' 2718 }; 2719 if (videoCount) { 2720 configuration.video = videoDecodeList[i % videoCount]; 2721 } 2722 if (audioCount) { 2723 configuration.audio = audioDecodeList[i % audioCount]; 2724 const audioBitrate = configuration.audio.bitrate; 2725 if (configuration.video && audioBitrate) { 2726 configuration.video.bitrate -= audioBitrate; 2727 } 2728 } 2729 configurations.push(configuration); 2730 } 2731 if (videoCodecs) { 2732 // Override Windows Firefox HEVC MediaCapabilities result (https://github.com/video-dev/hls.js/issues/7046) 2733 const ua = navigator.userAgent; 2734 if (videoCodecs.split(',').some(videoCodec => isHEVC(videoCodec)) && userAgentHevcSupportIsInaccurate()) { 2735 return Promise.resolve(getUnsupportedResult(new Error(`Overriding Windows Firefox HEVC MediaCapabilities result based on user-agent string: (${ua})`), configurations)); 2736 } 2737 } 2738 return Promise.all(configurations.map(configuration => { 2739 // Cache MediaCapabilities promises 2740 const decodingInfoKey = getMediaDecodingInfoKey(configuration); 2741 return cache[decodingInfoKey] || (cache[decodingInfoKey] = mediaCapabilities.decodingInfo(configuration)); 2742 })).then(decodingInfoResults => ({ 2743 supported: !decodingInfoResults.some(info => !info.supported), 2744 configurations, 2745 decodingInfoResults 2746 })).catch(error => ({ 2747 supported: false, 2748 configurations, 2749 decodingInfoResults: [], 2750 error 2751 })); 2752} 2753function makeVideoConfigurations(level) { 2754 var _level$videoCodec; 2755 const videoCodecs = (_level$videoCodec = level.videoCodec) == null ? void 0 : _level$videoCodec.split(','); 2756 const bitrate = getVariantDecodingBitrate(level); 2757 const width = level.width || 640; 2758 const height = level.height || 480; 2759 // Assume a framerate of 30fps since MediaCapabilities will not accept Level default of 0. 2760 const framerate = level.frameRate || 30; 2761 const videoRange = level.videoRange.toLowerCase(); 2762 return videoCodecs ? videoCodecs.map(videoCodec => { 2763 const videoConfiguration = { 2764 contentType: mimeTypeForCodec(fillInMissingAV01Params(videoCodec), 'video'), 2765 width, 2766 height, 2767 bitrate, 2768 framerate 2769 }; 2770 if (videoRange !== 'sdr') { 2771 videoConfiguration.transferFunction = videoRange; 2772 } 2773 return videoConfiguration; 2774 }) : []; 2775} 2776function makeAudioConfigurations(level, audioTracksByGroup, hasVideo) { 2777 var _level$audioCodec; 2778 const audioCodecs = (_level$audioCodec = level.audioCodec) == null ? void 0 : _level$audioCodec.split(','); 2779 const combinedBitrate = getVariantDecodingBitrate(level); 2780 if (audioCodecs && level.audioGroups) { 2781 return level.audioGroups.reduce((configurations, audioGroupId) => { 2782 var _audioTracksByGroup$g; 2783 const tracks = audioGroupId ? (_audioTracksByGroup$g = audioTracksByGroup.groups[audioGroupId]) == null ? void 0 : _audioTracksByGroup$g.tracks : null; 2784 if (tracks) { 2785 return tracks.reduce((configs, audioTrack) => { 2786 if (audioTrack.groupId === audioGroupId) { 2787 const channelsNumber = parseFloat(audioTrack.channels || ''); 2788 audioCodecs.forEach(audioCodec => { 2789 const audioConfiguration = { 2790 contentType: mimeTypeForCodec(audioCodec, 'audio'), 2791 bitrate: hasVideo ? estimatedAudioBitrate(audioCodec, combinedBitrate) : combinedBitrate 2792 }; 2793 if (channelsNumber) { 2794 audioConfiguration.channels = '' + channelsNumber; 2795 } 2796 configs.push(audioConfiguration); 2797 }); 2798 } 2799 return configs; 2800 }, configurations); 2801 } 2802 return configurations; 2803 }, []); 2804 } 2805 return []; 2806} 2807function estimatedAudioBitrate(audioCodec, levelBitrate) { 2808 if (levelBitrate <= 1) { 2809 return 1; 2810 } 2811 let audioBitrate = 128000; 2812 if (audioCodec === 'ec-3') { 2813 audioBitrate = 768000; 2814 } else if (audioCodec === 'ac-3') { 2815 audioBitrate = 640000; 2816 } 2817 return Math.min(levelBitrate / 2, audioBitrate); // Don't exceed some % of level bitrate 2818} 2819function getVariantDecodingBitrate(level) { 2820 return Math.ceil(Math.max(level.bitrate * 0.9, level.averageBitrate) / 1000) * 1000 || 1; 2821} 2822function getMediaDecodingInfoKey(config) { 2823 let key = ''; 2824 const { 2825 audio, 2826 video 2827 } = config; 2828 if (video) { 2829 const codec = getCodecsForMimeType(video.contentType); 2830 key += `${codec}_r${video.height}x${video.width}f${Math.ceil(video.framerate)}${video.transferFunction || 'sd'}_${Math.ceil(video.bitrate / 1e5)}`; 2831 } 2832 if (audio) { 2833 const codec = getCodecsForMimeType(audio.contentType); 2834 key += `${video ? '_' : ''}${codec}_c${audio.channels}`; 2835 } 2836 return key; 2837} 2838 2839const HdcpLevels = ['NONE', 'TYPE-0', 'TYPE-1', null]; 2840function isHdcpLevel(value) { 2841 return HdcpLevels.indexOf(value) > -1; 2842} 2843const VideoRangeValues = ['SDR', 'PQ', 'HLG']; 2844function isVideoRange(value) { 2845 return !!value && VideoRangeValues.indexOf(value) > -1; 2846} 2847var HlsSkip = { 2848 No: "", 2849 Yes: "YES", 2850 v2: "v2" 2851}; 2852function getSkipValue(details) { 2853 const { 2854 canSkipUntil, 2855 canSkipDateRanges, 2856 age 2857 } = details; 2858 // A Client SHOULD NOT request a Playlist Delta Update unless it already 2859 // has a version of the Playlist that is no older than one-half of the Skip Boundary. 2860 // @see: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-6.3.7 2861 const playlistRecentEnough = age < canSkipUntil / 2; 2862 if (canSkipUntil && playlistRecentEnough) { 2863 if (canSkipDateRanges) { 2864 return HlsSkip.v2; 2865 } 2866 return HlsSkip.Yes; 2867 } 2868 return HlsSkip.No; 2869} 2870class HlsUrlParameters { 2871 constructor(msn, part, skip) { 2872 this.msn = void 0; 2873 this.part = void 0; 2874 this.skip = void 0; 2875 this.msn = msn; 2876 this.part = part; 2877 this.skip = skip; 2878 } 2879 addDirectives(uri) { 2880 const url = new self.URL(uri); 2881 if (this.msn !== undefined) { 2882 url.searchParams.set('_HLS_msn', this.msn.toString()); 2883 } 2884 if (this.part !== undefined) { 2885 url.searchParams.set('_HLS_part', this.part.toString()); 2886 } 2887 if (this.skip) { 2888 url.searchParams.set('_HLS_skip', this.skip); 2889 } 2890 return url.href; 2891 } 2892} 2893class Level { 2894 constructor(data) { 2895 this._attrs = void 0; 2896 this.audioCodec = void 0; 2897 this.bitrate = void 0; 2898 this.codecSet = void 0; 2899 this.url = void 0; 2900 this.frameRate = void 0; 2901 this.height = void 0; 2902 this.id = void 0; 2903 this.name = void 0; 2904 this.supplemental = void 0; 2905 this.videoCodec = void 0; 2906 this.width = void 0; 2907 this.details = void 0; 2908 this.fragmentError = 0; 2909 this.loadError = 0; 2910 this.loaded = void 0; 2911 this.realBitrate = 0; 2912 this.supportedPromise = void 0; 2913 this.supportedResult = void 0; 2914 this._avgBitrate = 0; 2915 this._audioGroups = void 0; 2916 this._subtitleGroups = void 0; 2917 // Deprecated (retained for backwards compatibility) 2918 this._urlId = 0; 2919 this.url = [data.url]; 2920 this._attrs = [data.attrs]; 2921 this.bitrate = data.bitrate; 2922 if (data.details) { 2923 this.details = data.details; 2924 } 2925 this.id = data.id || 0; 2926 this.name = data.name; 2927 this.width = data.width || 0; 2928 this.height = data.height || 0; 2929 this.frameRate = data.attrs.optionalFloat('FRAME-RATE', 0); 2930 this._avgBitrate = data.attrs.decimalInteger('AVERAGE-BANDWIDTH'); 2931 this.audioCodec = data.audioCodec; 2932 this.videoCodec = data.videoCodec; 2933 this.codecSet = [data.videoCodec, data.audioCodec].filter(c => !!c).map(s => s.substring(0, 4)).join(','); 2934 if ('supplemental' in data) { 2935 var _data$supplemental; 2936 this.supplemental = data.supplemental; 2937 const supplementalVideo = (_data$supplemental = data.supplemental) == null ? void 0 : _data$supplemental.videoCodec; 2938 if (supplementalVideo && supplementalVideo !== data.videoCodec) { 2939 this.codecSet += `,${supplementalVideo.substring(0, 4)}`; 2940 } 2941 } 2942 this.addGroupId('audio', data.attrs.AUDIO); 2943 this.addGroupId('text', data.attrs.SUBTITLES); 2944 } 2945 get maxBitrate() { 2946 return Math.max(this.realBitrate, this.bitrate); 2947 } 2948 get averageBitrate() { 2949 return this._avgBitrate || this.realBitrate || this.bitrate; 2950 } 2951 get attrs() { 2952 return this._attrs[0]; 2953 } 2954 get codecs() { 2955 return this.attrs.CODECS || ''; 2956 } 2957 get pathwayId() { 2958 return this.attrs['PATHWAY-ID'] || '.'; 2959 } 2960 get videoRange() { 2961 return this.attrs['VIDEO-RANGE'] || 'SDR'; 2962 } 2963 get score() { 2964 return this.attrs.optionalFloat('SCORE', 0); 2965 } 2966 get uri() { 2967 return this.url[0] || ''; 2968 } 2969 hasAudioGroup(groupId) { 2970 return hasGroup(this._audioGroups, groupId); 2971 } 2972 hasSubtitleGroup(groupId) { 2973 return hasGroup(this._subtitleGroups, groupId); 2974 } 2975 get audioGroups() { 2976 return this._audioGroups; 2977 } 2978 get subtitleGroups() { 2979 return this._subtitleGroups; 2980 } 2981 addGroupId(type, groupId) { 2982 if (!groupId) { 2983 return; 2984 } 2985 if (type === 'audio') { 2986 let audioGroups = this._audioGroups; 2987 if (!audioGroups) { 2988 audioGroups = this._audioGroups = []; 2989 } 2990 if (audioGroups.indexOf(groupId) === -1) { 2991 audioGroups.push(groupId); 2992 } 2993 } else if (type === 'text') { 2994 let subtitleGroups = this._subtitleGroups; 2995 if (!subtitleGroups) { 2996 subtitleGroups = this._subtitleGroups = []; 2997 } 2998 if (subtitleGroups.indexOf(groupId) === -1) { 2999 subtitleGroups.push(groupId); 3000 } 3001 } 3002 } 3003 3004 // Deprecated methods (retained for backwards compatibility) 3005 get urlId() { 3006 return 0; 3007 } 3008 set urlId(value) {} 3009 get audioGroupIds() { 3010 return this.audioGroups ? [this.audioGroupId] : undefined; 3011 } 3012 get textGroupIds() { 3013 return this.subtitleGroups ? [this.textGroupId] : undefined; 3014 } 3015 get audioGroupId() { 3016 var _this$audioGroups; 3017 return (_this$audioGroups = this.audioGroups) == null ? void 0 : _this$audioGroups[0]; 3018 } 3019 get textGroupId() { 3020 var _this$subtitleGroups; 3021 return (_this$subtitleGroups = this.subtitleGroups) == null ? void 0 : _this$subtitleGroups[0]; 3022 } 3023 addFallback() {} 3024} 3025function hasGroup(groups, groupId) { 3026 if (!groupId || !groups) { 3027 return false; 3028 } 3029 return groups.indexOf(groupId) !== -1; 3030} 3031 3032/** 3033 * @returns Whether we can detect and validate HDR capability within the window context 3034 */ 3035function isHdrSupported() { 3036 if (typeof matchMedia === 'function') { 3037 const mediaQueryList = matchMedia('(dynamic-range: high)'); 3038 const badQuery = matchMedia('bad query'); 3039 if (mediaQueryList.media !== badQuery.media) { 3040 return mediaQueryList.matches === true; 3041 } 3042 } 3043 return false; 3044} 3045 3046/** 3047 * Sanitizes inputs to return the active video selection options for HDR/SDR. 3048 * When both inputs are null: 3049 * 3050 * `{ preferHDR: false, allowedVideoRanges: [] }` 3051 * 3052 * When `currentVideoRange` non-null, maintain the active range: 3053 * 3054 * `{ preferHDR: currentVideoRange !== 'SDR', allowedVideoRanges: [currentVideoRange] }` 3055 * 3056 * When VideoSelectionOption non-null: 3057 * 3058 * - Allow all video ranges if `allowedVideoRanges` unspecified. 3059 * - If `preferHDR` is non-null use the value to filter `allowedVideoRanges`. 3060 * - Else check window for HDR support and set `preferHDR` to the result. 3061 * 3062 * @param currentVideoRange 3063 * @param videoPreference 3064 */ 3065function getVideoSelectionOptions(currentVideoRange, videoPreference) { 3066 let preferHDR = false; 3067 let allowedVideoRanges = []; 3068 if (currentVideoRange) { 3069 preferHDR = currentVideoRange !== 'SDR'; 3070 allowedVideoRanges = [currentVideoRange]; 3071 } 3072 if (videoPreference) { 3073 allowedVideoRanges = videoPreference.allowedVideoRanges || VideoRangeValues.slice(0); 3074 const allowAutoPreferHDR = allowedVideoRanges.join('') !== 'SDR' && !videoPreference.videoCodec; 3075 preferHDR = videoPreference.preferHDR !== undefined ? videoPreference.preferHDR : allowAutoPreferHDR && isHdrSupported(); 3076 if (!preferHDR) { 3077 allowedVideoRanges = ['SDR']; 3078 } 3079 } 3080 return { 3081 preferHDR, 3082 allowedVideoRanges 3083 }; 3084} 3085 3086const omitCircularRefsReplacer = replacer => { 3087 const known = new WeakSet(); 3088 return (_, value) => { 3089 if (replacer) { 3090 value = replacer(_, value); 3091 } 3092 if (typeof value === 'object' && value !== null) { 3093 if (known.has(value)) { 3094 return; 3095 } 3096 known.add(value); 3097 } 3098 return value; 3099 }; 3100}; 3101const stringify = (object, replacer) => JSON.stringify(object, omitCircularRefsReplacer(replacer)); 3102 3103function getStartCodecTier(codecTiers, currentVideoRange, currentBw, audioPreference, videoPreference) { 3104 const codecSets = Object.keys(codecTiers); 3105 const channelsPreference = audioPreference == null ? void 0 : audioPreference.channels; 3106 const audioCodecPreference = audioPreference == null ? void 0 : audioPreference.audioCodec; 3107 const videoCodecPreference = videoPreference == null ? void 0 : videoPreference.videoCodec; 3108 const preferStereo = channelsPreference && parseInt(channelsPreference) === 2; 3109 // Use first level set to determine stereo, and minimum resolution and framerate 3110 let hasStereo = false; 3111 let hasCurrentVideoRange = false; 3112 let minHeight = Infinity; 3113 let minFramerate = Infinity; 3114 let minBitrate = Infinity; 3115 let minIndex = Infinity; 3116 let selectedScore = 0; 3117 let videoRanges = []; 3118 const { 3119 preferHDR, 3120 allowedVideoRanges 3121 } = getVideoSelectionOptions(currentVideoRange, videoPreference); 3122 for (let i = codecSets.length; i--;) { 3123 const tier = codecTiers[codecSets[i]]; 3124 hasStereo || (hasStereo = tier.channels[2] > 0); 3125 minHeight = Math.min(minHeight, tier.minHeight); 3126 minFramerate = Math.min(minFramerate, tier.minFramerate); 3127 minBitrate = Math.min(minBitrate, tier.minBitrate); 3128 const matchingVideoRanges = allowedVideoRanges.filter(range => tier.videoRanges[range] > 0); 3129 if (matchingVideoRanges.length > 0) { 3130 hasCurrentVideoRange = true; 3131 } 3132 } 3133 minHeight = isFiniteNumber(minHeight) ? minHeight : 0; 3134 minFramerate = isFiniteNumber(minFramerate) ? minFramerate : 0; 3135 const maxHeight = Math.max(1080, minHeight); 3136 const maxFramerate = Math.max(30, minFramerate); 3137 minBitrate = isFiniteNumber(minBitrate) ? minBitrate : currentBw; 3138 currentBw = Math.max(minBitrate, currentBw); 3139 // If there are no variants with matching preference, set currentVideoRange to undefined 3140 if (!hasCurrentVideoRange) { 3141 currentVideoRange = undefined; 3142 } 3143 const hasMultipleSets = codecSets.length > 1; 3144 const codecSet = codecSets.reduce((selected, candidate) => { 3145 // Remove candiates which do not meet bitrate, default audio, stereo or channels preference, 1080p or lower, 30fps or lower, or SDR/HDR selection if present 3146 const candidateTier = codecTiers[candidate]; 3147 if (candidate === selected) { 3148 return selected; 3149 } 3150 videoRanges = hasCurrentVideoRange ? allowedVideoRanges.filter(range => candidateTier.videoRanges[range] > 0) : []; 3151 if (hasMultipleSets) { 3152 if (candidateTier.minBitrate > currentBw) { 3153 logStartCodecCandidateIgnored(candidate, `min bitrate of ${candidateTier.minBitrate} > current estimate of ${currentBw}`); 3154 return selected; 3155 } 3156 if (!candidateTier.hasDefaultAudio) { 3157 logStartCodecCandidateIgnored(candidate, `no renditions with default or auto-select sound found`); 3158 return selected; 3159 } 3160 if (audioCodecPreference && candidate.indexOf(audioCodecPreference.substring(0, 4)) % 5 !== 0) { 3161 logStartCodecCandidateIgnored(candidate, `audio codec preference "${audioCodecPreference}" not found`); 3162 return selected; 3163 } 3164 if (channelsPreference && !preferStereo) { 3165 if (!candidateTier.channels[channelsPreference]) { 3166 logStartCodecCandidateIgnored(candidate, `no renditions with ${channelsPreference} channel sound found (channels options: ${Object.keys(candidateTier.channels)})`); 3167 return selected; 3168 } 3169 } else if ((!audioCodecPreference || preferStereo) && hasStereo && candidateTier.channels['2'] === 0) { 3170 logStartCodecCandidateIgnored(candidate, `no renditions with stereo sound found`); 3171 return selected; 3172 } 3173 if (candidateTier.minHeight > maxHeight) { 3174 logStartCodecCandidateIgnored(candidate, `min resolution of ${candidateTier.minHeight} > maximum of ${maxHeight}`); 3175 return selected; 3176 } 3177 if (candidateTier.minFramerate > maxFramerate) { 3178 logStartCodecCandidateIgnored(candidate, `min framerate of ${candidateTier.minFramerate} > maximum of ${maxFramerate}`); 3179 return selected; 3180 } 3181 if (!videoRanges.some(range => candidateTier.videoRanges[range] > 0)) { 3182 logStartCodecCandidateIgnored(candidate, `no variants with VIDEO-RANGE of ${stringify(videoRanges)} found`); 3183 return selected; 3184 } 3185 if (videoCodecPreference && candidate.indexOf(videoCodecPreference.substring(0, 4)) % 5 !== 0) { 3186 logStartCodecCandidateIgnored(candidate, `video codec preference "${videoCodecPreference}" not found`); 3187 return selected; 3188 } 3189 if (candidateTier.maxScore < selectedScore) { 3190 logStartCodecCandidateIgnored(candidate, `max score of ${candidateTier.maxScore} < selected max of ${selectedScore}`); 3191 return selected; 3192 } 3193 } 3194 // Remove candiates with less preferred codecs or more errors 3195 if (selected && (codecsSetSelectionPreferenceValue(candidate) >= codecsSetSelectionPreferenceValue(selected) || candidateTier.fragmentError > codecTiers[selected].fragmentError)) { 3196 return selected; 3197 } 3198 minIndex = candidateTier.minIndex; 3199 selectedScore = candidateTier.maxScore; 3200 return candidate; 3201 }, undefined); 3202 return { 3203 codecSet, 3204 videoRanges, 3205 preferHDR, 3206 minFramerate, 3207 minBitrate, 3208 minIndex 3209 }; 3210} 3211function logStartCodecCandidateIgnored(codeSet, reason) { 3212 logger.log(`[abr] start candidates with "${codeSet}" ignored because ${reason}`); 3213} 3214function getAudioTracksByGroup(allAudioTracks) { 3215 return allAudioTracks.reduce((audioTracksByGroup, track) => { 3216 let trackGroup = audioTracksByGroup.groups[track.groupId]; 3217 if (!trackGroup) { 3218 trackGroup = audioTracksByGroup.groups[track.groupId] = { 3219 tracks: [], 3220 channels: { 3221 2: 0 3222 }, 3223 hasDefault: false, 3224 hasAutoSelect: false 3225 }; 3226 } 3227 trackGroup.tracks.push(track); 3228 const channelsKey = track.channels || '2'; 3229 trackGroup.channels[channelsKey] = (trackGroup.channels[channelsKey] || 0) + 1; 3230 trackGroup.hasDefault = trackGroup.hasDefault || track.default; 3231 trackGroup.hasAutoSelect = trackGroup.hasAutoSelect || track.autoselect; 3232 if (trackGroup.hasDefault) { 3233 audioTracksByGroup.hasDefaultAudio = true; 3234 } 3235 if (trackGroup.hasAutoSelect) { 3236 audioTracksByGroup.hasAutoSelectAudio = true; 3237 } 3238 return audioTracksByGroup; 3239 }, { 3240 hasDefaultAudio: false, 3241 hasAutoSelectAudio: false, 3242 groups: {} 3243 }); 3244} 3245function getCodecTiers(levels, audioTracksByGroup, minAutoLevel, maxAutoLevel) { 3246 return levels.slice(minAutoLevel, maxAutoLevel + 1).reduce((tiers, level, index) => { 3247 if (!level.codecSet) { 3248 return tiers; 3249 } 3250 const audioGroups = level.audioGroups; 3251 let tier = tiers[level.codecSet]; 3252 if (!tier) { 3253 tiers[level.codecSet] = tier = { 3254 minBitrate: Infinity, 3255 minHeight: Infinity, 3256 minFramerate: Infinity, 3257 minIndex: index, 3258 maxScore: 0, 3259 videoRanges: { 3260 SDR: 0 3261 }, 3262 channels: { 3263 '2': 0 3264 }, 3265 hasDefaultAudio: !audioGroups, 3266 fragmentError: 0 3267 }; 3268 } 3269 tier.minBitrate = Math.min(tier.minBitrate, level.bitrate); 3270 const lesserWidthOrHeight = Math.min(level.height, level.width); 3271 tier.minHeight = Math.min(tier.minHeight, lesserWidthOrHeight); 3272 tier.minFramerate = Math.min(tier.minFramerate, level.frameRate); 3273 tier.minIndex = Math.min(tier.minIndex, index); 3274 tier.maxScore = Math.max(tier.maxScore, level.score); 3275 tier.fragmentError += level.fragmentError; 3276 tier.videoRanges[level.videoRange] = (tier.videoRanges[level.videoRange] || 0) + 1; 3277 if (audioGroups) { 3278 audioGroups.forEach(audioGroupId => { 3279 if (!audioGroupId) { 3280 return; 3281 } 3282 const audioGroup = audioTracksByGroup.groups[audioGroupId]; 3283 if (!audioGroup) { 3284 return; 3285 } 3286 // Default audio is any group with DEFAULT=YES, or if missing then any group with AUTOSELECT=YES, or all variants 3287 tier.hasDefaultAudio = tier.hasDefaultAudio || audioTracksByGroup.hasDefaultAudio ? audioGroup.hasDefault : audioGroup.hasAutoSelect || !audioTracksByGroup.hasDefaultAudio && !audioTracksByGroup.hasAutoSelectAudio; 3288 Object.keys(audioGroup.channels).forEach(channels => { 3289 tier.channels[channels] = (tier.channels[channels] || 0) + audioGroup.channels[channels]; 3290 }); 3291 }); 3292 } 3293 return tiers; 3294 }, {}); 3295} 3296function getBasicSelectionOption(option) { 3297 if (!option) { 3298 return option; 3299 } 3300 const { 3301 lang, 3302 assocLang, 3303 characteristics, 3304 channels, 3305 audioCodec 3306 } = option; 3307 return { 3308 lang, 3309 assocLang, 3310 characteristics, 3311 channels, 3312 audioCodec 3313 }; 3314} 3315function findMatchingOption(option, tracks, matchPredicate) { 3316 if ('attrs' in option) { 3317 const index = tracks.indexOf(option); 3318 if (index !== -1) { 3319 return index; 3320 } 3321 } 3322 for (let i = 0; i < tracks.length; i++) { 3323 const track = tracks[i]; 3324 if (matchesOption(option, track, matchPredicate)) { 3325 return i; 3326 } 3327 } 3328 return -1; 3329} 3330function matchesOption(option, track, matchPredicate) { 3331 const { 3332 groupId, 3333 name, 3334 lang, 3335 assocLang, 3336 default: isDefault 3337 } = option; 3338 const forced = option.forced; 3339 return (groupId === undefined || track.groupId === groupId) && (name === undefined || track.name === name) && (lang === undefined || languagesMatch(lang, track.lang)) && (lang === undefined || track.assocLang === assocLang) && (isDefault === undefined || track.default === isDefault) && (forced === undefined || track.forced === forced) && (!('characteristics' in option) || characteristicsMatch(option.characteristics || '', track.characteristics)) && (matchPredicate === undefined || matchPredicate(option, track)); 3340} 3341function languagesMatch(languageA, languageB = '--') { 3342 if (languageA.length === languageB.length) { 3343 return languageA === languageB; 3344 } 3345 return languageA.startsWith(languageB) || languageB.startsWith(languageA); 3346} 3347function characteristicsMatch(characteristicsA, characteristicsB = '') { 3348 const arrA = characteristicsA.split(','); 3349 const arrB = characteristicsB.split(','); 3350 // Expects each item to be unique: 3351 return arrA.length === arrB.length && !arrA.some(el => arrB.indexOf(el) === -1); 3352} 3353function audioMatchPredicate(option, track) { 3354 const { 3355 audioCodec, 3356 channels 3357 } = option; 3358 return (audioCodec === undefined || (track.audioCodec || '').substring(0, 4) === audioCodec.substring(0, 4)) && (channels === undefined || channels === (track.channels || '2')); 3359} 3360function findClosestLevelWithAudioGroup(option, levels, allAudioTracks, searchIndex, matchPredicate) { 3361 const currentLevel = levels[searchIndex]; 3362 // Are there variants with same URI as current level? 3363 // If so, find a match that does not require any level URI change 3364 const variants = levels.reduce((variantMap, level, index) => { 3365 const uri = level.uri; 3366 const renditions = variantMap[uri] || (variantMap[uri] = []); 3367 renditions.push(index); 3368 return variantMap; 3369 }, {}); 3370 const renditions = variants[currentLevel.uri]; 3371 if (renditions.length > 1) { 3372 searchIndex = Math.max.apply(Math, renditions); 3373 } 3374 // Find best match 3375 const currentVideoRange = currentLevel.videoRange; 3376 const currentFrameRate = currentLevel.frameRate; 3377 const currentVideoCodec = currentLevel.codecSet.substring(0, 4); 3378 const matchingVideo = searchDownAndUpList(levels, searchIndex, level => { 3379 if (level.videoRange !== currentVideoRange || level.frameRate !== currentFrameRate || level.codecSet.substring(0, 4) !== currentVideoCodec) { 3380 return false; 3381 } 3382 const audioGroups = level.audioGroups; 3383 const tracks = allAudioTracks.filter(track => !audioGroups || audioGroups.indexOf(track.groupId) !== -1); 3384 return findMatchingOption(option, tracks, matchPredicate) > -1; 3385 }); 3386 if (matchingVideo > -1) { 3387 return matchingVideo; 3388 } 3389 return searchDownAndUpList(levels, searchIndex, level => { 3390 const audioGroups = level.audioGroups; 3391 const tracks = allAudioTracks.filter(track => !audioGroups || audioGroups.indexOf(track.groupId) !== -1); 3392 return findMatchingOption(option, tracks, matchPredicate) > -1; 3393 }); 3394} 3395function searchDownAndUpList(arr, searchIndex, predicate) { 3396 for (let i = searchIndex; i > -1; i--) { 3397 if (predicate(arr[i])) { 3398 return i; 3399 } 3400 } 3401 for (let i = searchIndex + 1; i < arr.length; i++) { 3402 if (predicate(arr[i])) { 3403 return i; 3404 } 3405 } 3406 return -1; 3407} 3408function useAlternateAudio(audioTrackUrl, hls) { 3409 var _hls$loadLevelObj; 3410 return !!audioTrackUrl && audioTrackUrl !== ((_hls$loadLevelObj = hls.loadLevelObj) == null ? void 0 : _hls$loadLevelObj.uri); 3411} 3412 3413class AbrController extends Logger { 3414 constructor(_hls) { 3415 super('abr', _hls.logger); 3416 this.hls = void 0; 3417 this.lastLevelLoadSec = 0; 3418 this.lastLoadedFragLevel = -1; 3419 this.firstSelection = -1; 3420 this._nextAutoLevel = -1; 3421 this.nextAutoLevelKey = ''; 3422 this.audioTracksByGroup = null; 3423 this.codecTiers = null; 3424 this.timer = -1; 3425 this.fragCurrent = null; 3426 this.partCurrent = null; 3427 this.bitrateTestDelay = 0; 3428 this.rebufferNotice = -1; 3429 this.supportedCache = {}; 3430 this.bwEstimator = void 0; 3431 /* 3432 This method monitors the download rate of the current fragment, and will downswitch if that fragment will not load 3433 quickly enough to prevent underbuffering 3434 */ 3435 this._abandonRulesCheck = levelLoaded => { 3436 var _ref; 3437 const { 3438 fragCurrent: frag, 3439 partCurrent: part, 3440 hls 3441 } = this; 3442 const { 3443 autoLevelEnabled, 3444 media 3445 } = hls; 3446 if (!frag || !media) { 3447 return; 3448 } 3449 const now = performance.now(); 3450 const stats = part ? part.stats : frag.stats; 3451 const duration = part ? part.duration : frag.duration; 3452 const timeLoading = now - stats.loading.start; 3453 const minAutoLevel = hls.minAutoLevel; 3454 const loadingFragForLevel = frag.level; 3455 const currentAutoLevel = this._nextAutoLevel; 3456 // If frag loading is aborted, complete, or from lowest level, stop timer and return 3457 if (stats.aborted || stats.loaded && stats.loaded === stats.total || loadingFragForLevel <= minAutoLevel) { 3458 this.clearTimer(); 3459 // reset forced auto level value so that next level will be selected 3460 this._nextAutoLevel = -1; 3461 return; 3462 } 3463 3464 // This check only runs if we're in ABR mode 3465 if (!autoLevelEnabled) { 3466 return; 3467 } 3468 3469 // Must be loading/loaded a new level or be in a playing state 3470 const fragBlockingSwitch = currentAutoLevel > -1 && currentAutoLevel !== loadingFragForLevel; 3471 const levelChange = !!levelLoaded || fragBlockingSwitch; 3472 if (!levelChange && (media.paused || !media.playbackRate || !media.readyState)) { 3473 return; 3474 } 3475 const bufferInfo = hls.mainForwardBufferInfo; 3476 if (!levelChange && bufferInfo === null) { 3477 return; 3478 } 3479 const ttfbEstimate = this.bwEstimator.getEstimateTTFB(); 3480 const playbackRate = Math.abs(media.playbackRate); 3481 // To maintain stable adaptive playback, only begin monitoring frag loading after half or more of its playback duration has passed 3482 if (timeLoading <= Math.max(ttfbEstimate, 1000 * (duration / (playbackRate * 2)))) { 3483 return; 3484 } 3485 3486 // bufferStarvationDelay is an estimate of the amount time (in seconds) it will take to exhaust the buffer 3487 const bufferStarvationDelay = bufferInfo ? bufferInfo.len / playbackRate : 0; 3488 const ttfb = stats.loading.first ? stats.loading.first - stats.loading.start : -1; 3489 const loadedFirstByte = stats.loaded && ttfb > -1; 3490 const bwEstimate = this.getBwEstimate(); 3491 const levels = hls.levels; 3492 const level = levels[loadingFragForLevel]; 3493 const expectedLen = Math.max(stats.loaded, Math.round(duration * (frag.bitrate || level.averageBitrate) / 8)); 3494 let timeStreaming = loadedFirstByte ? timeLoading - ttfb : timeLoading; 3495 if (timeStreaming < 1 && loadedFirstByte) { 3496 timeStreaming = Math.min(timeLoading, stats.loaded * 8 / bwEstimate); 3497 } 3498 const loadRate = loadedFirstByte ? stats.loaded * 1000 / timeStreaming : 0; 3499 // fragLoadDelay is an estimate of the time (in seconds) it will take to buffer the remainder of the fragment 3500 const ttfbSeconds = ttfbEstimate / 1000; 3501 const fragLoadedDelay = loadRate ? (expectedLen - stats.loaded) / loadRate : expectedLen * 8 / bwEstimate + ttfbSeconds; 3502 // Only downswitch if the time to finish loading the current fragment is greater than the amount of buffer left 3503 if (fragLoadedDelay <= bufferStarvationDelay) { 3504 return; 3505 } 3506 const bwe = loadRate ? loadRate * 8 : bwEstimate; 3507 const live = ((_ref = (levelLoaded == null ? void 0 : levelLoaded.details) || this.hls.latestLevelDetails) == null ? void 0 : _ref.live) === true; 3508 const abrBandWidthUpFactor = this.hls.config.abrBandWidthUpFactor; 3509 let fragLevelNextLoadedDelay = Number.POSITIVE_INFINITY; 3510 let nextLoadLevel; 3511 // Iterate through lower level and try to find the largest one that avoids rebuffering 3512 for (nextLoadLevel = loadingFragForLevel - 1; nextLoadLevel > minAutoLevel; nextLoadLevel--) { 3513 // compute time to load next fragment at lower level 3514 // 8 = bits per byte (bps/Bps) 3515 const levelNextBitrate = levels[nextLoadLevel].maxBitrate; 3516 const requiresLevelLoad = !levels[nextLoadLevel].details || live; 3517 fragLevelNextLoadedDelay = this.getTimeToLoadFrag(ttfbSeconds, bwe, duration * levelNextBitrate, requiresLevelLoad); 3518 if (fragLevelNextLoadedDelay < Math.min(bufferStarvationDelay, duration + ttfbSeconds)) { 3519 break; 3520 } 3521 } 3522 // Only emergency switch down if it takes less time to load a new fragment at lowest level instead of continuing 3523 // to load the current one 3524 if (fragLevelNextLoadedDelay >= fragLoadedDelay) { 3525 return; 3526 } 3527 3528 // if estimated load time of new segment is completely unreasonable, ignore and do not emergency switch down 3529 if (fragLevelNextLoadedDelay > duration * 10) { 3530 return; 3531 } 3532 if (loadedFirstByte) { 3533 // If there has been loading progress, sample bandwidth using loading time offset by minimum TTFB time 3534 this.bwEstimator.sample(timeLoading - Math.min(ttfbEstimate, ttfb), stats.loaded); 3535 } else { 3536 // If there has been no loading progress, sample TTFB 3537 this.bwEstimator.sampleTTFB(timeLoading); 3538 } 3539 const nextLoadLevelBitrate = levels[nextLoadLevel].maxBitrate; 3540 if (this.getBwEstimate() * abrBandWidthUpFactor > nextLoadLevelBitrate) { 3541 this.resetEstimator(nextLoadLevelBitrate); 3542 } 3543 const bestSwitchLevel = this.findBestLevel(nextLoadLevelBitrate, minAutoLevel, nextLoadLevel, 0, bufferStarvationDelay, 1, 1); 3544 if (bestSwitchLevel > -1) { 3545 nextLoadLevel = bestSwitchLevel; 3546 } 3547 this.warn(`Fragment ${frag.sn}${part ? ' part ' + part.index : ''} of level ${loadingFragForLevel} is loading too slowly; 3548 Fragment duration: ${frag.duration.toFixed(3)} 3549 Time to underbuffer: ${bufferStarvationDelay.toFixed(3)} s 3550 Estimated load time for current fragment: ${fragLoadedDelay.toFixed(3)} s 3551 Estimated load time for down switch fragment: ${fragLevelNextLoadedDelay.toFixed(3)} s 3552 TTFB estimate: ${ttfb | 0} ms 3553 Current BW estimate: ${isFiniteNumber(bwEstimate) ? bwEstimate | 0 : 'Unknown'} bps 3554 New BW estimate: ${this.getBwEstimate() | 0} bps 3555 Switching to level ${nextLoadLevel} @ ${nextLoadLevelBitrate | 0} bps`); 3556 hls.nextLoadLevel = hls.nextAutoLevel = nextLoadLevel; 3557 this.clearTimer(); 3558 const abortAndSwitch = () => { 3559 // Are nextLoadLevel details available or is stream-controller still in "WAITING_LEVEL" state? 3560 this.clearTimer(); 3561 if (this.fragCurrent === frag && this.hls.loadLevel === nextLoadLevel && nextLoadLevel > 0) { 3562 const bufferStarvationDelay = this.getStarvationDelay(); 3563 this.warn(`Aborting inflight request ${nextLoadLevel > 0 ? 'and switching down' : ''} 3564 Fragment duration: ${frag.duration.toFixed(3)} s 3565 Time to underbuffer: ${bufferStarvationDelay.toFixed(3)} s`); 3566 frag.abortRequests(); 3567 this.fragCurrent = this.partCurrent = null; 3568 if (nextLoadLevel > minAutoLevel) { 3569 let lowestSwitchLevel = this.findBestLevel(this.hls.levels[minAutoLevel].bitrate, minAutoLevel, nextLoadLevel, 0, bufferStarvationDelay, 1, 1); 3570 if (lowestSwitchLevel === -1) { 3571 lowestSwitchLevel = minAutoLevel; 3572 } 3573 this.hls.nextLoadLevel = this.hls.nextAutoLevel = lowestSwitchLevel; 3574 this.resetEstimator(this.hls.levels[lowestSwitchLevel].bitrate); 3575 } 3576 } 3577 }; 3578 if (fragBlockingSwitch || fragLoadedDelay > fragLevelNextLoadedDelay * 2) { 3579 abortAndSwitch(); 3580 } else { 3581 this.timer = self.setInterval(abortAndSwitch, fragLevelNextLoadedDelay * 1000); 3582 } 3583 hls.trigger(Events.FRAG_LOAD_EMERGENCY_ABORTED, { 3584 frag, 3585 part, 3586 stats 3587 }); 3588 }; 3589 this.hls = _hls; 3590 this.bwEstimator = this.initEstimator(); 3591 this.registerListeners(); 3592 } 3593 resetEstimator(abrEwmaDefaultEstimate) { 3594 if (abrEwmaDefaultEstimate) { 3595 this.log(`setting initial bwe to ${abrEwmaDefaultEstimate}`); 3596 this.hls.config.abrEwmaDefaultEstimate = abrEwmaDefaultEstimate; 3597 } 3598 this.firstSelection = -1; 3599 this.bwEstimator = this.initEstimator(); 3600 } 3601 initEstimator() { 3602 const config = this.hls.config; 3603 return new EwmaBandWidthEstimator(config.abrEwmaSlowVoD, config.abrEwmaFastVoD, config.abrEwmaDefaultEstimate); 3604 } 3605 registerListeners() { 3606 const { 3607 hls 3608 } = this; 3609 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 3610 hls.on(Events.FRAG_LOADING, this.onFragLoading, this); 3611 hls.on(Events.FRAG_LOADED, this.onFragLoaded, this); 3612 hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this); 3613 hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); 3614 hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this); 3615 hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); 3616 hls.on(Events.MAX_AUTO_LEVEL_UPDATED, this.onMaxAutoLevelUpdated, this); 3617 hls.on(Events.ERROR, this.onError, this); 3618 } 3619 unregisterListeners() { 3620 const { 3621 hls 3622 } = this; 3623 if (!hls) { 3624 return; 3625 } 3626 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 3627 hls.off(Events.FRAG_LOADING, this.onFragLoading, this); 3628 hls.off(Events.FRAG_LOADED, this.onFragLoaded, this); 3629 hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this); 3630 hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); 3631 hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this); 3632 hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); 3633 hls.off(Events.MAX_AUTO_LEVEL_UPDATED, this.onMaxAutoLevelUpdated, this); 3634 hls.off(Events.ERROR, this.onError, this); 3635 } 3636 destroy() { 3637 this.unregisterListeners(); 3638 this.clearTimer(); 3639 // @ts-ignore 3640 this.hls = this._abandonRulesCheck = this.supportedCache = null; 3641 this.fragCurrent = this.partCurrent = null; 3642 } 3643 onManifestLoading(event, data) { 3644 this.lastLoadedFragLevel = -1; 3645 this.firstSelection = -1; 3646 this.lastLevelLoadSec = 0; 3647 this.supportedCache = {}; 3648 this.fragCurrent = this.partCurrent = null; 3649 this.onLevelsUpdated(); 3650 this.clearTimer(); 3651 } 3652 onLevelsUpdated() { 3653 if (this.lastLoadedFragLevel > -1 && this.fragCurrent) { 3654 this.lastLoadedFragLevel = this.fragCurrent.level; 3655 } 3656 this._nextAutoLevel = -1; 3657 this.onMaxAutoLevelUpdated(); 3658 this.codecTiers = null; 3659 this.audioTracksByGroup = null; 3660 } 3661 onMaxAutoLevelUpdated() { 3662 this.firstSelection = -1; 3663 this.nextAutoLevelKey = ''; 3664 } 3665 onFragLoading(event, data) { 3666 const frag = data.frag; 3667 if (this.ignoreFragment(frag)) { 3668 return; 3669 } 3670 if (!frag.bitrateTest) { 3671 var _data$part; 3672 this.fragCurrent = frag; 3673 this.partCurrent = (_data$part = data.part) != null ? _data$part : null; 3674 } 3675 this.clearTimer(); 3676 this.timer = self.setInterval(this._abandonRulesCheck, 100); 3677 } 3678 onLevelSwitching(event, data) { 3679 this.clearTimer(); 3680 } 3681 onError(event, data) { 3682 if (data.fatal) { 3683 return; 3684 } 3685 switch (data.details) { 3686 case ErrorDetails.BUFFER_ADD_CODEC_ERROR: 3687 case ErrorDetails.BUFFER_APPEND_ERROR: 3688 // Reset last loaded level so that a new selection can be made after calling recoverMediaError 3689 this.lastLoadedFragLevel = -1; 3690 this.firstSelection = -1; 3691 break; 3692 case ErrorDetails.FRAG_LOAD_TIMEOUT: 3693 { 3694 const frag = data.frag; 3695 const { 3696 fragCurrent, 3697 partCurrent: part 3698 } = this; 3699 if (frag && fragCurrent && frag.sn === fragCurrent.sn && frag.level === fragCurrent.level) { 3700 const now = performance.now(); 3701 const stats = part ? part.stats : frag.stats; 3702 const timeLoading = now - stats.loading.start; 3703 const ttfb = stats.loading.first ? stats.loading.first - stats.loading.start : -1; 3704 const loadedFirstByte = stats.loaded && ttfb > -1; 3705 if (loadedFirstByte) { 3706 const ttfbEstimate = this.bwEstimator.getEstimateTTFB(); 3707 this.bwEstimator.sample(timeLoading - Math.min(ttfbEstimate, ttfb), stats.loaded); 3708 } else { 3709 this.bwEstimator.sampleTTFB(timeLoading); 3710 } 3711 } 3712 break; 3713 } 3714 } 3715 } 3716 getTimeToLoadFrag(timeToFirstByteSec, bandwidth, fragSizeBits, isSwitch) { 3717 const fragLoadSec = timeToFirstByteSec + fragSizeBits / bandwidth; 3718 const playlistLoadSec = isSwitch ? timeToFirstByteSec + this.lastLevelLoadSec : 0; 3719 return fragLoadSec + playlistLoadSec; 3720 } 3721 onLevelLoaded(event, data) { 3722 const config = this.hls.config; 3723 const { 3724 loading 3725 } = data.stats; 3726 const timeLoadingMs = loading.end - loading.first; 3727 if (isFiniteNumber(timeLoadingMs)) { 3728 this.lastLevelLoadSec = timeLoadingMs / 1000; 3729 } 3730 if (data.details.live) { 3731 this.bwEstimator.update(config.abrEwmaSlowLive, config.abrEwmaFastLive); 3732 } else { 3733 this.bwEstimator.update(config.abrEwmaSlowVoD, config.abrEwmaFastVoD); 3734 } 3735 if (this.timer > -1) { 3736 this._abandonRulesCheck(data.levelInfo); 3737 } 3738 } 3739 onFragLoaded(event, { 3740 frag, 3741 part 3742 }) { 3743 const stats = part ? part.stats : frag.stats; 3744 if (frag.type === PlaylistLevelType.MAIN) { 3745 this.bwEstimator.sampleTTFB(stats.loading.first - stats.loading.start); 3746 } 3747 if (this.ignoreFragment(frag)) { 3748 return; 3749 } 3750 // stop monitoring bw once frag loaded 3751 this.clearTimer(); 3752 // reset forced auto level value so that next level will be selected 3753 if (frag.level === this._nextAutoLevel) { 3754 this._nextAutoLevel = -1; 3755 } 3756 this.firstSelection = -1; 3757 3758 // compute level average bitrate 3759 if (this.hls.config.abrMaxWithRealBitrate) { 3760 const duration = part ? part.duration : frag.duration; 3761 const level = this.hls.levels[frag.level]; 3762 const loadedBytes = (level.loaded ? level.loaded.bytes : 0) + stats.loaded; 3763 const loadedDuration = (level.loaded ? level.loaded.duration : 0) + duration; 3764 level.loaded = { 3765 bytes: loadedBytes, 3766 duration: loadedDuration 3767 }; 3768 level.realBitrate = Math.round(8 * loadedBytes / loadedDuration); 3769 } 3770 if (frag.bitrateTest) { 3771 const fragBufferedData = { 3772 stats, 3773 frag, 3774 part, 3775 id: frag.type 3776 }; 3777 this.onFragBuffered(Events.FRAG_BUFFERED, fragBufferedData); 3778 frag.bitrateTest = false; 3779 } else { 3780 // store level id after successful fragment load for playback 3781 this.lastLoadedFragLevel = frag.level; 3782 } 3783 } 3784 onFragBuffered(event, data) { 3785 const { 3786 frag, 3787 part 3788 } = data; 3789 const stats = part != null && part.stats.loaded ? part.stats : frag.stats; 3790 if (stats.aborted) { 3791 return; 3792 } 3793 if (this.ignoreFragment(frag)) { 3794 return; 3795 } 3796 // Use the difference between parsing and request instead of buffering and request to compute fragLoadingProcessing; 3797 // rationale is that buffer appending only happens once media is attached. This can happen when config.startFragPrefetch 3798 // is used. If we used buffering in that case, our BW estimate sample will be very large. 3799 const processingMs = stats.parsing.end - stats.loading.start - Math.min(stats.loading.first - stats.loading.start, this.bwEstimator.getEstimateTTFB()); 3800 this.bwEstimator.sample(processingMs, stats.loaded); 3801 stats.bwEstimate = this.getBwEstimate(); 3802 if (frag.bitrateTest) { 3803 this.bitrateTestDelay = processingMs / 1000; 3804 } else { 3805 this.bitrateTestDelay = 0; 3806 } 3807 } 3808 ignoreFragment(frag) { 3809 // Only count non-alt-audio frags which were actually buffered in our BW calculations 3810 return frag.type !== PlaylistLevelType.MAIN || frag.sn === 'initSegment'; 3811 } 3812 clearTimer() { 3813 if (this.timer > -1) { 3814 self.clearInterval(this.timer); 3815 this.timer = -1; 3816 } 3817 } 3818 get firstAutoLevel() { 3819 const { 3820 maxAutoLevel, 3821 minAutoLevel 3822 } = this.hls; 3823 const bwEstimate = this.getBwEstimate(); 3824 const maxStartDelay = this.hls.config.maxStarvationDelay; 3825 const abrAutoLevel = this.findBestLevel(bwEstimate, minAutoLevel, maxAutoLevel, 0, maxStartDelay, 1, 1); 3826 if (abrAutoLevel > -1) { 3827 return abrAutoLevel; 3828 } 3829 const firstLevel = this.hls.firstLevel; 3830 const clamped = Math.min(Math.max(firstLevel, minAutoLevel), maxAutoLevel); 3831 this.warn(`Could not find best starting auto level. Defaulting to first in playlist ${firstLevel} clamped to ${clamped}`); 3832 return clamped; 3833 } 3834 get forcedAutoLevel() { 3835 if (this.nextAutoLevelKey) { 3836 return -1; 3837 } 3838 return this._nextAutoLevel; 3839 } 3840 3841 // return next auto level 3842 get nextAutoLevel() { 3843 const forcedAutoLevel = this.forcedAutoLevel; 3844 const bwEstimator = this.bwEstimator; 3845 const useEstimate = bwEstimator.canEstimate(); 3846 const loadedFirstFrag = this.lastLoadedFragLevel > -1; 3847 // in case next auto level has been forced, and bw not available or not reliable, return forced value 3848 if (forcedAutoLevel !== -1 && (!useEstimate || !loadedFirstFrag || this.nextAutoLevelKey === this.getAutoLevelKey())) { 3849 return forcedAutoLevel; 3850 } 3851 3852 // compute next level using ABR logic 3853 const nextABRAutoLevel = useEstimate && loadedFirstFrag ? this.getNextABRAutoLevel() : this.firstAutoLevel; 3854 3855 // use forced auto level while it hasn't errored more than ABR selection 3856 if (forcedAutoLevel !== -1) { 3857 const levels = this.hls.levels; 3858 if (levels.length > Math.max(forcedAutoLevel, nextABRAutoLevel) && levels[forcedAutoLevel].loadError <= levels[nextABRAutoLevel].loadError) { 3859 return forcedAutoLevel; 3860 } 3861 } 3862 3863 // save result until state has changed 3864 this._nextAutoLevel = nextABRAutoLevel; 3865 this.nextAutoLevelKey = this.getAutoLevelKey(); 3866 return nextABRAutoLevel; 3867 } 3868 getAutoLevelKey() { 3869 return `${this.getBwEstimate()}_${this.getStarvationDelay().toFixed(2)}`; 3870 } 3871 getNextABRAutoLevel() { 3872 const { 3873 fragCurrent, 3874 partCurrent, 3875 hls 3876 } = this; 3877 if (hls.levels.length <= 1) { 3878 return hls.loadLevel; 3879 } 3880 const { 3881 maxAutoLevel, 3882 config, 3883 minAutoLevel 3884 } = hls; 3885 const currentFragDuration = partCurrent ? partCurrent.duration : fragCurrent ? fragCurrent.duration : 0; 3886 const avgbw = this.getBwEstimate(); 3887 // bufferStarvationDelay is the wall-clock time left until the playback buffer is exhausted. 3888 const bufferStarvationDelay = this.getStarvationDelay(); 3889 let bwFactor = config.abrBandWidthFactor; 3890 let bwUpFactor = config.abrBandWidthUpFactor; 3891 3892 // First, look to see if we can find a level matching with our avg bandwidth AND that could also guarantee no rebuffering at all 3893 if (bufferStarvationDelay) { 3894 const _bestLevel = this.findBestLevel(avgbw, minAutoLevel, maxAutoLevel, bufferStarvationDelay, 0, bwFactor, bwUpFactor); 3895 if (_bestLevel >= 0) { 3896 this.rebufferNotice = -1; 3897 return _bestLevel; 3898 } 3899 } 3900 // not possible to get rid of rebuffering... try to find level that will guarantee less than maxStarvationDelay of rebuffering 3901 let maxStarvationDelay = currentFragDuration ? Math.min(currentFragDuration, config.maxStarvationDelay) : config.maxStarvationDelay; 3902 if (!bufferStarvationDelay) { 3903 // in case buffer is empty, let's check if previous fragment was loaded to perform a bitrate test 3904 const bitrateTestDelay = this.bitrateTestDelay; 3905 if (bitrateTestDelay) { 3906 // if it is the case, then we need to adjust our max starvation delay using maxLoadingDelay config value 3907 // max video loading delay used in automatic start level selection : 3908 // in that mode ABR controller will ensure that video loading time (ie the time to fetch the first fragment at lowest quality level + 3909 // the time to fetch the fragment at the appropriate quality level is less than ```maxLoadingDelay``` ) 3910 // cap maxLoadingDelay and ensure it is not bigger 'than bitrate test' frag duration 3911 const maxLoadingDelay = currentFragDuration ? Math.min(currentFragDuration, config.maxLoadingDelay) : config.maxLoadingDelay; 3912 maxStarvationDelay = maxLoadingDelay - bitrateTestDelay; 3913 this.info(`bitrate test took ${Math.round(1000 * bitrateTestDelay)}ms, set first fragment max fetchDuration to ${Math.round(1000 * maxStarvationDelay)} ms`); 3914 // don't use conservative factor on bitrate test 3915 bwFactor = bwUpFactor = 1; 3916 } 3917 } 3918 const bestLevel = this.findBestLevel(avgbw, minAutoLevel, maxAutoLevel, bufferStarvationDelay, maxStarvationDelay, bwFactor, bwUpFactor); 3919 if (this.rebufferNotice !== bestLevel) { 3920 this.rebufferNotice = bestLevel; 3921 this.info(`${bufferStarvationDelay ? 'rebuffering expected' : 'buffer is empty'}, optimal quality level ${bestLevel}`); 3922 } 3923 if (bestLevel > -1) { 3924 return bestLevel; 3925 } 3926 // If no matching level found, see if min auto level would be a better option 3927 const minLevel = hls.levels[minAutoLevel]; 3928 const autoLevel = hls.loadLevelObj; 3929 if (autoLevel && (minLevel == null ? void 0 : minLevel.bitrate) < autoLevel.bitrate) { 3930 return minAutoLevel; 3931 } 3932 // or if bitrate is not lower, continue to use loadLevel 3933 return hls.loadLevel; 3934 } 3935 getStarvationDelay() { 3936 const hls = this.hls; 3937 const media = hls.media; 3938 if (!media) { 3939 return Infinity; 3940 } 3941 // playbackRate is the absolute value of the playback rate; if media.playbackRate is 0, we use 1 to load as 3942 // if we're playing back at the normal rate. 3943 const playbackRate = media && media.playbackRate !== 0 ? Math.abs(media.playbackRate) : 1.0; 3944 const bufferInfo = hls.mainForwardBufferInfo; 3945 return (bufferInfo ? bufferInfo.len : 0) / playbackRate; 3946 } 3947 getBwEstimate() { 3948 return this.bwEstimator.canEstimate() ? this.bwEstimator.getEstimate() : this.hls.config.abrEwmaDefaultEstimate; 3949 } 3950 findBestLevel(currentBw, minAutoLevel, maxAutoLevel, bufferStarvationDelay, maxStarvationDelay, bwFactor, bwUpFactor) { 3951 var _this$hls$latestLevel; 3952 const maxFetchDuration = bufferStarvationDelay + maxStarvationDelay; 3953 const lastLoadedFragLevel = this.lastLoadedFragLevel; 3954 const selectionBaseLevel = lastLoadedFragLevel === -1 ? this.hls.firstLevel : lastLoadedFragLevel; 3955 const { 3956 fragCurrent, 3957 partCurrent 3958 } = this; 3959 const { 3960 levels, 3961 allAudioTracks, 3962 loadLevel, 3963 config 3964 } = this.hls; 3965 if (levels.length === 1) { 3966 return 0; 3967 } 3968 const level = levels[selectionBaseLevel]; 3969 const live = !!((_this$hls$latestLevel = this.hls.latestLevelDetails) != null && _this$hls$latestLevel.live); 3970 const firstSelection = loadLevel === -1 || lastLoadedFragLevel === -1; 3971 let currentCodecSet; 3972 let currentVideoRange = 'SDR'; 3973 let currentFrameRate = (level == null ? void 0 : level.frameRate) || 0; 3974 const { 3975 audioPreference, 3976 videoPreference 3977 } = config; 3978 const audioTracksByGroup = this.audioTracksByGroup || (this.audioTracksByGroup = getAudioTracksByGroup(allAudioTracks)); 3979 let minStartIndex = -1; 3980 if (firstSelection) { 3981 if (this.firstSelection !== -1) { 3982 return this.firstSelection; 3983 } 3984 const codecTiers = this.codecTiers || (this.codecTiers = getCodecTiers(levels, audioTracksByGroup, minAutoLevel, maxAutoLevel)); 3985 const startTier = getStartCodecTier(codecTiers, currentVideoRange, currentBw, audioPreference, videoPreference); 3986 const { 3987 codecSet, 3988 videoRanges, 3989 minFramerate, 3990 minBitrate, 3991 minIndex, 3992 preferHDR 3993 } = startTier; 3994 minStartIndex = minIndex; 3995 currentCodecSet = codecSet; 3996 currentVideoRange = preferHDR ? videoRanges[videoRanges.length - 1] : videoRanges[0]; 3997 currentFrameRate = minFramerate; 3998 currentBw = Math.max(currentBw, minBitrate); 3999 this.log(`picked start tier ${stringify(startTier)}`); 4000 } else { 4001 currentCodecSet = level == null ? void 0 : level.codecSet; 4002 currentVideoRange = level == null ? void 0 : level.videoRange; 4003 } 4004 const currentFragDuration = partCurrent ? partCurrent.duration : fragCurrent ? fragCurrent.duration : 0; 4005 const ttfbEstimateSec = this.bwEstimator.getEstimateTTFB() / 1000; 4006 const levelsSkipped = []; 4007 for (let i = maxAutoLevel; i >= minAutoLevel; i--) { 4008 var _levelInfo$supportedR; 4009 const levelInfo = levels[i]; 4010 const upSwitch = i > selectionBaseLevel; 4011 if (!levelInfo) { 4012 continue; 4013 } 4014 if (config.useMediaCapabilities && !levelInfo.supportedResult && !levelInfo.supportedPromise) { 4015 const mediaCapabilities = navigator.mediaCapabilities; 4016 if (typeof (mediaCapabilities == null ? void 0 : mediaCapabilities.decodingInfo) === 'function' && requiresMediaCapabilitiesDecodingInfo(levelInfo, audioTracksByGroup, currentVideoRange, currentFrameRate, currentBw, audioPreference)) { 4017 levelInfo.supportedPromise = getMediaDecodingInfoPromise(levelInfo, audioTracksByGroup, mediaCapabilities, this.supportedCache); 4018 levelInfo.supportedPromise.then(decodingInfo => { 4019 if (!this.hls) { 4020 return; 4021 } 4022 levelInfo.supportedResult = decodingInfo; 4023 const levels = this.hls.levels; 4024 const index = levels.indexOf(levelInfo); 4025 if (decodingInfo.error) { 4026 this.warn(`MediaCapabilities decodingInfo error: "${decodingInfo.error}" for level ${index} ${stringify(decodingInfo)}`); 4027 } else if (!decodingInfo.supported) { 4028 this.warn(`Unsupported MediaCapabilities decodingInfo result for level ${index} ${stringify(decodingInfo)}`); 4029 if (index > -1 && levels.length > 1) { 4030 this.log(`Removing unsupported level ${index}`); 4031 this.hls.removeLevel(index); 4032 if (this.hls.loadLevel === -1) { 4033 this.hls.nextLoadLevel = 0; 4034 } 4035 } 4036 } else if (decodingInfo.decodingInfoResults.some(info => info.smooth === false || info.powerEfficient === false)) { 4037 this.log(`MediaCapabilities decodingInfo for level ${index} not smooth or powerEfficient: ${stringify(decodingInfo)}`); 4038 } 4039 }).catch(error => { 4040 this.warn(`Error handling MediaCapabilities decodingInfo: ${error}`); 4041 }); 4042 } else { 4043 levelInfo.supportedResult = SUPPORTED_INFO_DEFAULT; 4044 } 4045 } 4046 4047 // skip candidates which change codec-family or video-range, 4048 // and which decrease or increase frame-rate for up and down-switch respectfully 4049 if (currentCodecSet && levelInfo.codecSet !== currentCodecSet || currentVideoRange && levelInfo.videoRange !== currentVideoRange || upSwitch && currentFrameRate > levelInfo.frameRate || !upSwitch && currentFrameRate > 0 && currentFrameRate < levelInfo.frameRate || (_levelInfo$supportedR = levelInfo.supportedResult) != null && (_levelInfo$supportedR = _levelInfo$supportedR.decodingInfoResults) != null && _levelInfo$supportedR.some(info => info.smooth === false)) { 4050 if (!firstSelection || i !== minStartIndex) { 4051 levelsSkipped.push(i); 4052 continue; 4053 } 4054 } 4055 const levelDetails = levelInfo.details; 4056 const avgDuration = (partCurrent ? levelDetails == null ? void 0 : levelDetails.partTarget : levelDetails == null ? void 0 : levelDetails.averagetargetduration) || currentFragDuration; 4057 let adjustedbw; 4058 // follow algorithm captured from stagefright : 4059 // https://android.googlesource.com/platform/frameworks/av/+/master/media/libstagefright/httplive/LiveSession.cpp 4060 // Pick the highest bandwidth stream below or equal to estimated bandwidth. 4061 // consider only 80% of the available bandwidth, but if we are switching up, 4062 // be even more conservative (70%) to avoid overestimating and immediately 4063 // switching back. 4064 if (!upSwitch) { 4065 adjustedbw = bwFactor * currentBw; 4066 } else { 4067 adjustedbw = bwUpFactor * currentBw; 4068 } 4069 4070 // Use average bitrate when starvation delay (buffer length) is gt or eq two segment durations and rebuffering is not expected (maxStarvationDelay > 0) 4071 const bitrate = currentFragDuration && bufferStarvationDelay >= currentFragDuration * 2 && maxStarvationDelay === 0 ? levelInfo.averageBitrate : levelInfo.maxBitrate; 4072 const fetchDuration = this.getTimeToLoadFrag(ttfbEstimateSec, adjustedbw, bitrate * avgDuration, levelDetails === undefined); 4073 const canSwitchWithinTolerance = 4074 // if adjusted bw is greater than level bitrate AND 4075 adjustedbw >= bitrate && ( 4076 // no level change, or new level has no error history 4077 i === lastLoadedFragLevel || levelInfo.loadError === 0 && levelInfo.fragmentError === 0) && ( 4078 // fragment fetchDuration unknown OR live stream OR fragment fetchDuration less than max allowed fetch duration, then this level matches 4079 // we don't account for max Fetch Duration for live streams, this is to avoid switching down when near the edge of live sliding window ... 4080 // special case to support startLevel = -1 (bitrateTest) on live streams : in that case we should not exit loop so that findBestLevel will return -1 4081 fetchDuration <= ttfbEstimateSec || !isFiniteNumber(fetchDuration) || live && !this.bitrateTestDelay || fetchDuration < maxFetchDuration); 4082 if (canSwitchWithinTolerance) { 4083 const forcedAutoLevel = this.forcedAutoLevel; 4084 if (i !== loadLevel && (forcedAutoLevel === -1 || forcedAutoLevel !== loadLevel)) { 4085 if (levelsSkipped.length) { 4086 this.trace(`Skipped level(s) ${levelsSkipped.join(',')} of ${maxAutoLevel} max with CODECS and VIDEO-RANGE:"${levels[levelsSkipped[0]].codecs}" ${levels[levelsSkipped[0]].videoRange}; not compatible with "${currentCodecSet}" ${currentVideoRange}`); 4087 } 4088 this.info(`switch candidate:${selectionBaseLevel}->${i} adjustedbw(${Math.round(adjustedbw)})-bitrate=${Math.round(adjustedbw - bitrate)} ttfb:${ttfbEstimateSec.toFixed(1)} avgDuration:${avgDuration.toFixed(1)} maxFetchDuration:${maxFetchDuration.toFixed(1)} fetchDuration:${fetchDuration.toFixed(1)} firstSelection:${firstSelection} codecSet:${levelInfo.codecSet} videoRange:${levelInfo.videoRange} hls.loadLevel:${loadLevel}`); 4089 } 4090 if (firstSelection) { 4091 this.firstSelection = i; 4092 } 4093 // as we are looping from highest to lowest, this will return the best achievable quality level 4094 return i; 4095 } 4096 } 4097 // not enough time budget even with quality level 0 ... rebuffering might happen 4098 return -1; 4099 } 4100 set nextAutoLevel(nextLevel) { 4101 const value = this.deriveNextAutoLevel(nextLevel); 4102 if (this._nextAutoLevel !== value) { 4103 this.nextAutoLevelKey = ''; 4104 this._nextAutoLevel = value; 4105 } 4106 } 4107 deriveNextAutoLevel(nextLevel) { 4108 const { 4109 maxAutoLevel, 4110 minAutoLevel 4111 } = this.hls; 4112 return Math.min(Math.max(nextLevel, minAutoLevel), maxAutoLevel); 4113 } 4114} 4115 4116const BinarySearch = { 4117 /** 4118 * Searches for an item in an array which matches a certain condition. 4119 * This requires the condition to only match one item in the array, 4120 * and for the array to be ordered. 4121 * 4122 * @param list The array to search. 4123 * @param comparisonFn 4124 * Called and provided a candidate item as the first argument. 4125 * Should return: 4126 * > -1 if the item should be located at a lower index than the provided item. 4127 * > 1 if the item should be located at a higher index than the provided item. 4128 * > 0 if the item is the item you're looking for. 4129 * 4130 * @returns the object if found, otherwise returns null 4131 */ 4132 search: function (list, comparisonFn) { 4133 let minIndex = 0; 4134 let maxIndex = list.length - 1; 4135 let currentIndex = null; 4136 let currentElement = null; 4137 while (minIndex <= maxIndex) { 4138 currentIndex = (minIndex + maxIndex) / 2 | 0; 4139 currentElement = list[currentIndex]; 4140 const comparisonResult = comparisonFn(currentElement); 4141 if (comparisonResult > 0) { 4142 minIndex = currentIndex + 1; 4143 } else if (comparisonResult < 0) { 4144 maxIndex = currentIndex - 1; 4145 } else { 4146 return currentElement; 4147 } 4148 } 4149 return null; 4150 } 4151}; 4152 4153/** 4154 * Returns first fragment whose endPdt value exceeds the given PDT, or null. 4155 * @param fragments - The array of candidate fragments 4156 * @param PDTValue - The PDT value which must be exceeded 4157 * @param maxFragLookUpTolerance - The amount of time that a fragment's start/end can be within in order to be considered contiguous 4158 */ 4159function findFragmentByPDT(fragments, PDTValue, maxFragLookUpTolerance) { 4160 if (PDTValue === null || !Array.isArray(fragments) || !fragments.length || !isFiniteNumber(PDTValue)) { 4161 return null; 4162 } 4163 4164 // if less than start 4165 const startPDT = fragments[0].programDateTime; 4166 if (PDTValue < (startPDT || 0)) { 4167 return null; 4168 } 4169 const endPDT = fragments[fragments.length - 1].endProgramDateTime; 4170 if (PDTValue >= (endPDT || 0)) { 4171 return null; 4172 } 4173 for (let seg = 0; seg < fragments.length; ++seg) { 4174 const frag = fragments[seg]; 4175 if (pdtWithinToleranceTest(PDTValue, maxFragLookUpTolerance, frag)) { 4176 return frag; 4177 } 4178 } 4179 return null; 4180} 4181 4182/** 4183 * Finds a fragment based on the SN of the previous fragment; or based on the needs of the current buffer. 4184 * This method compensates for small buffer gaps by applying a tolerance to the start of any candidate fragment, thus 4185 * breaking any traps which would cause the same fragment to be continuously selected within a small range. 4186 * @param fragPrevious - The last frag successfully appended 4187 * @param fragments - The array of candidate fragments 4188 * @param bufferEnd - The end of the contiguous buffered range the playhead is currently within 4189 * @param maxFragLookUpTolerance - The amount of time that a fragment's start/end can be within in order to be considered contiguous 4190 * @returns a matching fragment or null 4191 */ 4192function findFragmentByPTS(fragPrevious, fragments, bufferEnd = 0, maxFragLookUpTolerance = 0, nextFragLookupTolerance = 0.005) { 4193 let fragNext = null; 4194 if (fragPrevious) { 4195 fragNext = fragments[1 + fragPrevious.sn - fragments[0].sn] || null; 4196 // check for buffer-end rounding error 4197 const bufferEdgeError = fragPrevious.endDTS - bufferEnd; 4198 if (bufferEdgeError > 0 && bufferEdgeError < 0.0000015) { 4199 bufferEnd += 0.0000015; 4200 } 4201 if (fragNext && fragPrevious.level !== fragNext.level && fragNext.end <= fragPrevious.end) { 4202 fragNext = fragments[2 + fragPrevious.sn - fragments[0].sn] || null; 4203 } 4204 } else if (bufferEnd === 0 && fragments[0].start === 0) { 4205 fragNext = fragments[0]; 4206 } 4207 // Prefer the next fragment if it's within tolerance 4208 if (fragNext && ((!fragPrevious || fragPrevious.level === fragNext.level) && fragmentWithinToleranceTest(bufferEnd, maxFragLookUpTolerance, fragNext) === 0 || fragmentWithinFastStartSwitch(fragNext, fragPrevious, Math.min(nextFragLookupTolerance, maxFragLookUpTolerance)))) { 4209 return fragNext; 4210 } 4211 // We might be seeking past the tolerance so find the best match 4212 const foundFragment = BinarySearch.search(fragments, fragmentWithinToleranceTest.bind(null, bufferEnd, maxFragLookUpTolerance)); 4213 if (foundFragment && (foundFragment !== fragPrevious || !fragNext)) { 4214 return foundFragment; 4215 } 4216 // If no match was found return the next fragment after fragPrevious, or null 4217 return fragNext; 4218} 4219function fragmentWithinFastStartSwitch(fragNext, fragPrevious, nextFragLookupTolerance) { 4220 if (fragPrevious && fragPrevious.start === 0 && fragPrevious.level < fragNext.level && (fragPrevious.endPTS || 0) > 0) { 4221 const firstDuration = fragPrevious.tagList.reduce((duration, tag) => { 4222 if (tag[0] === 'INF') { 4223 duration += parseFloat(tag[1]); 4224 } 4225 return duration; 4226 }, nextFragLookupTolerance); 4227 return fragNext.start <= firstDuration; 4228 } 4229 return false; 4230} 4231 4232/** 4233 * The test function used by the findFragmentBySn's BinarySearch to look for the best match to the current buffer conditions. 4234 * @param candidate - The fragment to test 4235 * @param bufferEnd - The end of the current buffered range the playhead is currently within 4236 * @param maxFragLookUpTolerance - The amount of time that a fragment's start can be within in order to be considered contiguous 4237 * @returns 0 if it matches, 1 if too low, -1 if too high 4238 */ 4239function fragmentWithinToleranceTest(bufferEnd = 0, maxFragLookUpTolerance = 0, candidate) { 4240 // eagerly accept an accurate match (no tolerance) 4241 if (candidate.start <= bufferEnd && candidate.start + candidate.duration > bufferEnd) { 4242 return 0; 4243 } 4244 // offset should be within fragment boundary - config.maxFragLookUpTolerance 4245 // this is to cope with situations like 4246 // bufferEnd = 9.991 4247 // frag[Ø] : [0,10] 4248 // frag[1] : [10,20] 4249 // bufferEnd is within frag[0] range ... although what we are expecting is to return frag[1] here 4250 // frag start frag start+duration 4251 // |-----------------------------| 4252 // <---> <---> 4253 // ...--------><-----------------------------><---------.... 4254 // previous frag matching fragment next frag 4255 // return -1 return 0 return 1 4256 // logger.log(`level/sn/start/end/bufEnd:${level}/${candidate.sn}/${candidate.start}/${(candidate.start+candidate.duration)}/${bufferEnd}`); 4257 // Set the lookup tolerance to be small enough to detect the current segment - ensures we don't skip over very small segments 4258 const candidateLookupTolerance = Math.min(maxFragLookUpTolerance, candidate.duration + (candidate.deltaPTS ? candidate.deltaPTS : 0)); 4259 if (candidate.start + candidate.duration - candidateLookupTolerance <= bufferEnd) { 4260 return 1; 4261 } else if (candidate.start - candidateLookupTolerance > bufferEnd && candidate.start) { 4262 // if maxFragLookUpTolerance will have negative value then don't return -1 for first element 4263 return -1; 4264 } 4265 return 0; 4266} 4267 4268/** 4269 * The test function used by the findFragmentByPdt's BinarySearch to look for the best match to the current buffer conditions. 4270 * This function tests the candidate's program date time values, as represented in Unix time 4271 * @param candidate - The fragment to test 4272 * @param pdtBufferEnd - The Unix time representing the end of the current buffered range 4273 * @param maxFragLookUpTolerance - The amount of time that a fragment's start can be within in order to be considered contiguous 4274 * @returns true if contiguous, false otherwise 4275 */ 4276function pdtWithinToleranceTest(pdtBufferEnd, maxFragLookUpTolerance, candidate) { 4277 const candidateLookupTolerance = Math.min(maxFragLookUpTolerance, candidate.duration + (candidate.deltaPTS ? candidate.deltaPTS : 0)) * 1000; 4278 4279 // endProgramDateTime can be null, default to zero 4280 const endProgramDateTime = candidate.endProgramDateTime || 0; 4281 return endProgramDateTime - candidateLookupTolerance > pdtBufferEnd; 4282} 4283function findNearestWithCC(details, cc, pos) { 4284 if (details) { 4285 if (details.startCC <= cc && details.endCC >= cc) { 4286 let fragments = details.fragments; 4287 const { 4288 fragmentHint 4289 } = details; 4290 if (fragmentHint) { 4291 fragments = fragments.concat(fragmentHint); 4292 } 4293 let closest; 4294 BinarySearch.search(fragments, candidate => { 4295 if (candidate.cc < cc) { 4296 return 1; 4297 } 4298 if (candidate.cc > cc) { 4299 return -1; 4300 } 4301 closest = candidate; 4302 if (candidate.end <= pos) { 4303 return 1; 4304 } 4305 if (candidate.start > pos) { 4306 return -1; 4307 } 4308 return 0; 4309 }); 4310 return closest || null; 4311 } 4312 } 4313 return null; 4314} 4315 4316function isTimeoutError(error) { 4317 switch (error.details) { 4318 case ErrorDetails.FRAG_LOAD_TIMEOUT: 4319 case ErrorDetails.KEY_LOAD_TIMEOUT: 4320 case ErrorDetails.LEVEL_LOAD_TIMEOUT: 4321 case ErrorDetails.MANIFEST_LOAD_TIMEOUT: 4322 return true; 4323 } 4324 return false;
vendor: 5,728 bytes, lines 4325-4488
4325} 4326function isKeyError(error) { 4327 return error.details.startsWith('key'); 4328} 4329function isUnusableKeyError(error) { 4330 return isKeyError(error) && !!error.frag && !error.frag.decryptdata; 4331} 4332function getRetryConfig(loadPolicy, error) { 4333 const isTimeout = isTimeoutError(error); 4334 return loadPolicy.default[`${isTimeout ? 'timeout' : 'error'}Retry`]; 4335} 4336function getRetryDelay(retryConfig, retryCount) { 4337 // exponential backoff capped to max retry delay 4338 const backoffFactor = retryConfig.backoff === 'linear' ? 1 : Math.pow(2, retryCount); 4339 return Math.min(backoffFactor * retryConfig.retryDelayMs, retryConfig.maxRetryDelayMs); 4340} 4341function getLoaderConfigWithoutReties(loderConfig) { 4342 return _objectSpread2(_objectSpread2({}, loderConfig), { 4343 errorRetry: null, 4344 timeoutRetry: null 4345 }); 4346} 4347function shouldRetry(retryConfig, retryCount, isTimeout, loaderResponse) { 4348 if (!retryConfig) { 4349 return false; 4350 } 4351 const httpStatus = loaderResponse == null ? void 0 : loaderResponse.code; 4352 const retry = retryCount < retryConfig.maxNumRetry && (retryForHttpStatus(httpStatus) || !!isTimeout); 4353 return retryConfig.shouldRetry ? retryConfig.shouldRetry(retryConfig, retryCount, isTimeout, loaderResponse, retry) : retry; 4354} 4355function retryForHttpStatus(httpStatus) { 4356 // Do not retry on status 4xx, status 0 (CORS error), or undefined (decrypt/gap/parse error) 4357 return offlineHttpStatus(httpStatus) || !!httpStatus && (httpStatus < 400 || httpStatus > 499); 4358} 4359function offlineHttpStatus(httpStatus) { 4360 return httpStatus === 0 && navigator.onLine === false; 4361} 4362 4363var NetworkErrorAction = { 4364 DoNothing: 0, 4365 SendEndCallback: 1, 4366 SendAlternateToPenaltyBox: 2, 4367 RemoveAlternatePermanently: 3, 4368 InsertDiscontinuity: 4, 4369 RetryRequest: 5 4370}; 4371var ErrorActionFlags = { 4372 None: 0, 4373 MoveAllAlternatesMatchingHost: 1, 4374 MoveAllAlternatesMatchingHDCP: 2, 4375 MoveAllAlternatesMatchingKey: 4, 4376 SwitchToSDR: 8 4377}; 4378class ErrorController extends Logger { 4379 constructor(hls) { 4380 super('error-controller', hls.logger); 4381 this.hls = void 0; 4382 this.playlistError = 0; 4383 this.hls = hls; 4384 this.registerListeners(); 4385 } 4386 registerListeners() { 4387 const hls = this.hls; 4388 hls.on(Events.ERROR, this.onError, this); 4389 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 4390 hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this); 4391 } 4392 unregisterListeners() { 4393 const hls = this.hls; 4394 if (!hls) { 4395 return; 4396 } 4397 hls.off(Events.ERROR, this.onError, this); 4398 hls.off(Events.ERROR, this.onErrorOut, this); 4399 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 4400 hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this); 4401 } 4402 destroy() { 4403 this.unregisterListeners(); 4404 // @ts-ignore 4405 this.hls = null; 4406 } 4407 startLoad(startPosition) {} 4408 stopLoad() { 4409 this.playlistError = 0; 4410 } 4411 getVariantLevelIndex(frag) { 4412 if ((frag == null ? void 0 : frag.type) === PlaylistLevelType.MAIN) { 4413 return frag.level; 4414 } 4415 return this.getVariantIndex(); 4416 } 4417 getVariantIndex() { 4418 var _hls$loadLevelObj; 4419 const hls = this.hls; 4420 const currentLevel = hls.currentLevel; 4421 if ((_hls$loadLevelObj = hls.loadLevelObj) != null && _hls$loadLevelObj.details || currentLevel === -1) { 4422 return hls.loadLevel; 4423 } 4424 return currentLevel; 4425 } 4426 variantHasKey(level, keyInError) { 4427 if (level) { 4428 var _level$details; 4429 if ((_level$details = level.details) != null && _level$details.hasKey(keyInError)) { 4430 return true; 4431 } 4432 const audioGroupsIds = level.audioGroups; 4433 if (audioGroupsIds) { 4434 const audioTracks = this.hls.allAudioTracks.filter(track => audioGroupsIds.indexOf(track.groupId) >= 0); 4435 return audioTracks.some(track => { 4436 var _track$details; 4437 return (_track$details = track.details) == null ? void 0 : _track$details.hasKey(keyInError); 4438 }); 4439 } 4440 } 4441 return false; 4442 } 4443 onManifestLoading() { 4444 this.playlistError = 0; 4445 } 4446 onLevelUpdated() { 4447 this.playlistError = 0; 4448 } 4449 onError(event, data) { 4450 var _data$frag; 4451 if (data.fatal) { 4452 return; 4453 } 4454 const hls = this.hls; 4455 const context = data.context; 4456 switch (data.details) { 4457 case ErrorDetails.FRAG_LOAD_ERROR: 4458 case ErrorDetails.FRAG_LOAD_TIMEOUT: 4459 case ErrorDetails.KEY_LOAD_ERROR: 4460 case ErrorDetails.KEY_LOAD_TIMEOUT: 4461 data.errorAction = this.getFragRetryOrSwitchAction(data); 4462 return; 4463 case ErrorDetails.FRAG_PARSING_ERROR: 4464 // ignore empty segment errors marked as gap 4465 if ((_data$frag = data.frag) != null && _data$frag.gap) { 4466 data.errorAction = createDoNothingErrorAction(); 4467 return; 4468 } 4469 // falls through 4470 case ErrorDetails.FRAG_GAP: 4471 case ErrorDetails.FRAG_DECRYPT_ERROR: 4472 { 4473 // Switch level if possible, otherwise allow retry count to reach max error retries 4474 data.errorAction = this.getFragRetryOrSwitchAction(data); 4475 data.errorAction.action = NetworkErrorAction.SendAlternateToPenaltyBox; 4476 return; 4477 } 4478 case ErrorDetails.LEVEL_EMPTY_ERROR: 4479 case ErrorDetails.LEVEL_PARSING_ERROR: 4480 { 4481 var _data$context; 4482 // Only retry when empty and live 4483 const levelIndex = data.parent === PlaylistLevelType.MAIN ? data.level : hls.loadLevel; 4484 if (data.details === ErrorDetails.LEVEL_EMPTY_ERROR && !!((_data$context = data.context) != null && (_data$context = _data$context.levelDetails) != null && _data$context.live)) { 4485 data.errorAction = this.getPlaylistRetryOrSwitchAction(data, levelIndex); 4486 } else { 4487 // Escalate to fatal if not retrying or switching 4488 data.levelRetry = false;
vendor: 7,005 bytes, lines 4489-4652
4489 data.errorAction = this.getLevelSwitchAction(data, levelIndex); 4490 } 4491 } 4492 return; 4493 case ErrorDetails.LEVEL_LOAD_ERROR: 4494 case ErrorDetails.LEVEL_LOAD_TIMEOUT: 4495 if (typeof (context == null ? void 0 : context.level) === 'number') { 4496 data.errorAction = this.getPlaylistRetryOrSwitchAction(data, context.level); 4497 } 4498 return; 4499 case ErrorDetails.AUDIO_TRACK_LOAD_ERROR: 4500 case ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT: 4501 case ErrorDetails.SUBTITLE_LOAD_ERROR: 4502 case ErrorDetails.SUBTITLE_TRACK_LOAD_TIMEOUT: 4503 if (context) { 4504 const level = hls.loadLevelObj; 4505 if (level && (context.type === PlaylistContextType.AUDIO_TRACK && level.hasAudioGroup(context.groupId) || context.type === PlaylistContextType.SUBTITLE_TRACK && level.hasSubtitleGroup(context.groupId))) { 4506 // Perform Pathway switch or Redundant failover if possible for fastest recovery 4507 // otherwise allow playlist retry count to reach max error retries 4508 data.errorAction = this.getPlaylistRetryOrSwitchAction(data, hls.loadLevel); 4509 data.errorAction.action = NetworkErrorAction.SendAlternateToPenaltyBox; 4510 data.errorAction.flags = ErrorActionFlags.MoveAllAlternatesMatchingHost; 4511 return; 4512 } 4513 } 4514 return; 4515 case ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED: 4516 { 4517 data.errorAction = { 4518 action: NetworkErrorAction.SendAlternateToPenaltyBox, 4519 flags: ErrorActionFlags.MoveAllAlternatesMatchingHDCP 4520 }; 4521 } 4522 return; 4523 case ErrorDetails.KEY_SYSTEM_SESSION_UPDATE_FAILED: 4524 case ErrorDetails.KEY_SYSTEM_STATUS_INTERNAL_ERROR: 4525 case ErrorDetails.KEY_SYSTEM_NO_SESSION: 4526 { 4527 data.errorAction = { 4528 action: NetworkErrorAction.SendAlternateToPenaltyBox, 4529 flags: ErrorActionFlags.MoveAllAlternatesMatchingKey 4530 }; 4531 } 4532 return; 4533 case ErrorDetails.BUFFER_ADD_CODEC_ERROR: 4534 case ErrorDetails.REMUX_ALLOC_ERROR: 4535 case ErrorDetails.BUFFER_APPEND_ERROR: 4536 // Buffer-controller can set errorAction when append errors can be ignored or resolved locally 4537 if (!data.errorAction) { 4538 var _data$level; 4539 data.errorAction = this.getLevelSwitchAction(data, (_data$level = data.level) != null ? _data$level : hls.loadLevel); 4540 } 4541 return; 4542 case ErrorDetails.INTERNAL_EXCEPTION: 4543 case ErrorDetails.BUFFER_APPENDING_ERROR: 4544 case ErrorDetails.BUFFER_FULL_ERROR: 4545 case ErrorDetails.LEVEL_SWITCH_ERROR: 4546 case ErrorDetails.BUFFER_STALLED_ERROR: 4547 case ErrorDetails.BUFFER_SEEK_OVER_HOLE: 4548 case ErrorDetails.BUFFER_NUDGE_ON_STALL: 4549 data.errorAction = createDoNothingErrorAction(); 4550 return; 4551 } 4552 if (data.type === ErrorTypes.KEY_SYSTEM_ERROR) { 4553 // Do not retry level. Should be fatal if ErrorDetails.KEY_SYSTEM_<ERROR> not handled with early return above. 4554 data.levelRetry = false; 4555 data.errorAction = createDoNothingErrorAction(); 4556 } 4557 } 4558 getPlaylistRetryOrSwitchAction(data, levelIndex) { 4559 const hls = this.hls; 4560 const retryConfig = getRetryConfig(hls.config.playlistLoadPolicy, data); 4561 const retryCount = this.playlistError++; 4562 const retry = shouldRetry(retryConfig, retryCount, isTimeoutError(data), data.response); 4563 if (retry) { 4564 return { 4565 action: NetworkErrorAction.RetryRequest, 4566 flags: ErrorActionFlags.None, 4567 retryConfig, 4568 retryCount 4569 }; 4570 } 4571 const errorAction = this.getLevelSwitchAction(data, levelIndex); 4572 if (retryConfig) { 4573 errorAction.retryConfig = retryConfig; 4574 errorAction.retryCount = retryCount; 4575 } 4576 return errorAction; 4577 } 4578 getFragRetryOrSwitchAction(data) { 4579 const hls = this.hls; 4580 // Share fragment error count accross media options (main, audio, subs) 4581 // This allows for level based rendition switching when media option assets fail 4582 const variantLevelIndex = this.getVariantLevelIndex(data.frag); 4583 const level = hls.levels[variantLevelIndex]; 4584 const { 4585 fragLoadPolicy, 4586 keyLoadPolicy 4587 } = hls.config; 4588 const retryConfig = getRetryConfig(isKeyError(data) ? keyLoadPolicy : fragLoadPolicy, data); 4589 const fragmentErrors = hls.levels.reduce((acc, level) => acc + level.fragmentError, 0); 4590 // Switch levels when out of retried or level index out of bounds 4591 if (level) { 4592 if (data.details !== ErrorDetails.FRAG_GAP) { 4593 level.fragmentError++; 4594 } 4595 if (!isUnusableKeyError(data)) { 4596 const retry = shouldRetry(retryConfig, fragmentErrors, isTimeoutError(data), data.response); 4597 if (retry) { 4598 return { 4599 action: NetworkErrorAction.RetryRequest, 4600 flags: ErrorActionFlags.None, 4601 retryConfig, 4602 retryCount: fragmentErrors 4603 }; 4604 } 4605 } 4606 } 4607 // Reach max retry count, or Missing level reference 4608 // Switch to valid index 4609 const errorAction = this.getLevelSwitchAction(data, variantLevelIndex); 4610 // Add retry details to allow skipping of FRAG_PARSING_ERROR 4611 if (retryConfig) { 4612 errorAction.retryConfig = retryConfig; 4613 errorAction.retryCount = fragmentErrors; 4614 } 4615 return errorAction; 4616 } 4617 getLevelSwitchAction(data, levelIndex) { 4618 const hls = this.hls; 4619 if (levelIndex === null || levelIndex === undefined) { 4620 levelIndex = hls.loadLevel; 4621 } 4622 const level = this.hls.levels[levelIndex]; 4623 if (level) { 4624 var _data$frag2, _data$context2; 4625 const errorDetails = data.details; 4626 level.loadError++; 4627 if (errorDetails === ErrorDetails.BUFFER_APPEND_ERROR) { 4628 level.fragmentError++; 4629 } 4630 // Search for next level to retry 4631 let nextLevel = -1; 4632 const { 4633 levels, 4634 loadLevel, 4635 minAutoLevel, 4636 maxAutoLevel 4637 } = hls; 4638 if (!hls.autoLevelEnabled && !hls.config.preserveManualLevelOnError) { 4639 hls.loadLevel = -1; 4640 } 4641 const fragErrorType = (_data$frag2 = data.frag) == null ? void 0 : _data$frag2.type; 4642 // Find alternate audio codec if available on audio codec error 4643 const isAudioCodecError = fragErrorType === PlaylistLevelType.AUDIO && errorDetails === ErrorDetails.FRAG_PARSING_ERROR || data.sourceBufferName === 'audio' && (errorDetails === ErrorDetails.BUFFER_ADD_CODEC_ERROR || errorDetails === ErrorDetails.BUFFER_APPEND_ERROR); 4644 const findAudioCodecAlternate = isAudioCodecError && levels.some(({ 4645 audioCodec 4646 }) => level.audioCodec !== audioCodec); 4647 // Find alternate video codec if available on video codec error 4648 const isVideoCodecError = data.sourceBufferName === 'video' && (errorDetails === ErrorDetails.BUFFER_ADD_CODEC_ERROR || errorDetails === ErrorDetails.BUFFER_APPEND_ERROR); 4649 const findVideoCodecAlternate = isVideoCodecError && levels.some(({ 4650 codecSet, 4651 audioCodec 4652 }) =>
vendor: 6,571 bytes, lines 4652-4783
4652 level.codecSet !== codecSet && level.audioCodec === audioCodec); 4653 const { 4654 type: playlistErrorType, 4655 groupId: playlistErrorGroupId 4656 } = (_data$context2 = data.context) != null ? _data$context2 : {}; 4657 for (let i = levels.length; i--;) { 4658 const candidate = (i + loadLevel) % levels.length; 4659 if (candidate !== loadLevel && candidate >= minAutoLevel && candidate <= maxAutoLevel && levels[candidate].loadError === 0) { 4660 var _level$audioGroups, _level$subtitleGroups; 4661 const levelCandidate = levels[candidate]; 4662 // Skip level switch if GAP tag is found in next level at same position 4663 if (errorDetails === ErrorDetails.FRAG_GAP && fragErrorType === PlaylistLevelType.MAIN && data.frag) { 4664 const levelDetails = levels[candidate].details; 4665 if (levelDetails) { 4666 const fragCandidate = findFragmentByPTS(data.frag, levelDetails.fragments, data.frag.start); 4667 if (fragCandidate != null && fragCandidate.gap) { 4668 continue; 4669 } 4670 } 4671 } else if (playlistErrorType === PlaylistContextType.AUDIO_TRACK && levelCandidate.hasAudioGroup(playlistErrorGroupId) || playlistErrorType === PlaylistContextType.SUBTITLE_TRACK && levelCandidate.hasSubtitleGroup(playlistErrorGroupId)) { 4672 // For audio/subs playlist errors find another group ID or fallthrough to redundant fail-over 4673 continue; 4674 } else if (fragErrorType === PlaylistLevelType.AUDIO && (_level$audioGroups = level.audioGroups) != null && _level$audioGroups.some(groupId => levelCandidate.hasAudioGroup(groupId)) || fragErrorType === PlaylistLevelType.SUBTITLE && (_level$subtitleGroups = level.subtitleGroups) != null && _level$subtitleGroups.some(groupId => levelCandidate.hasSubtitleGroup(groupId)) || findAudioCodecAlternate && level.audioCodec === levelCandidate.audioCodec || findVideoCodecAlternate && level.codecSet === levelCandidate.codecSet || !findAudioCodecAlternate && level.codecSet !== levelCandidate.codecSet) { 4675 // For video/audio/subs frag errors find another group ID or fallthrough to redundant fail-over 4676 continue; 4677 } 4678 nextLevel = candidate; 4679 break; 4680 } 4681 } 4682 if (nextLevel > -1 && hls.loadLevel !== nextLevel) { 4683 data.levelRetry = true; 4684 this.playlistError = 0; 4685 return { 4686 action: NetworkErrorAction.SendAlternateToPenaltyBox, 4687 flags: ErrorActionFlags.None, 4688 nextAutoLevel: nextLevel 4689 }; 4690 } 4691 } 4692 // No levels to switch / Manual level selection / Level not found 4693 // Resolve with Pathway switch, Redundant fail-over, or stay on lowest Level 4694 return { 4695 action: NetworkErrorAction.SendAlternateToPenaltyBox, 4696 flags: ErrorActionFlags.MoveAllAlternatesMatchingHost 4697 }; 4698 } 4699 onErrorOut(event, data) { 4700 var _data$errorAction; 4701 switch ((_data$errorAction = data.errorAction) == null ? void 0 : _data$errorAction.action) { 4702 case NetworkErrorAction.DoNothing: 4703 break; 4704 case NetworkErrorAction.SendAlternateToPenaltyBox: 4705 this.sendAlternateToPenaltyBox(data); 4706 if (!data.errorAction.resolved && data.details !== ErrorDetails.FRAG_GAP) { 4707 data.fatal = true; 4708 } else if (/MediaSource readyState: ended/.test(data.error.message)) { 4709 this.warn(`MediaSource ended after "${data.sourceBufferName}" sourceBuffer append error. Attempting to recover from media error.`); 4710 this.hls.recoverMediaError(); 4711 } 4712 break; 4713 case NetworkErrorAction.RetryRequest: 4714 // handled by stream and playlist/level controllers 4715 break; 4716 } 4717 if (data.fatal) { 4718 this.hls.stopLoad(); 4719 return; 4720 } 4721 } 4722 sendAlternateToPenaltyBox(data) { 4723 const hls = this.hls; 4724 const errorAction = data.errorAction; 4725 if (!errorAction) { 4726 return; 4727 } 4728 const { 4729 flags 4730 } = errorAction; 4731 const nextAutoLevel = errorAction.nextAutoLevel; 4732 switch (flags) { 4733 case ErrorActionFlags.None: 4734 this.switchLevel(data, nextAutoLevel); 4735 break; 4736 case ErrorActionFlags.MoveAllAlternatesMatchingHDCP: 4737 { 4738 const levelIndex = this.getVariantLevelIndex(data.frag); 4739 const level = hls.levels[levelIndex]; 4740 const restrictedHdcpLevel = level == null ? void 0 : level.attrs['HDCP-LEVEL']; 4741 errorAction.hdcpLevel = restrictedHdcpLevel; 4742 if (restrictedHdcpLevel === 'NONE') { 4743 this.warn(`HDCP policy resticted output with HDCP-LEVEL=NONE`); 4744 } else if (restrictedHdcpLevel) { 4745 hls.maxHdcpLevel = HdcpLevels[HdcpLevels.indexOf(restrictedHdcpLevel) - 1]; 4746 errorAction.resolved = true; 4747 this.warn(`Restricting playback to HDCP-LEVEL of "${hls.maxHdcpLevel}" or lower`); 4748 break; 4749 } 4750 // Fallthrough when no HDCP-LEVEL attribute is found 4751 } 4752 // eslint-disable-next-line no-fallthrough 4753 case ErrorActionFlags.MoveAllAlternatesMatchingKey: 4754 { 4755 const levelKey = data.decryptdata; 4756 if (levelKey) { 4757 // Penalize all levels with key 4758 const levels = this.hls.levels; 4759 const levelCountWithError = levels.length; 4760 for (let i = levelCountWithError; i--;) { 4761 if (this.variantHasKey(levels[i], levelKey)) { 4762 var _levels$i$audioGroups, _data$frag3; 4763 this.log(`Banned key found in level ${i} (${levels[i].bitrate}bps) or audio group "${(_levels$i$audioGroups = levels[i].audioGroups) == null ? void 0 : _levels$i$audioGroups.join(',')}" (${(_data$frag3 = data.frag) == null ? void 0 : _data$frag3.type} fragment) ${arrayToHex(levelKey.keyId || [])}`); 4764 levels[i].fragmentError++; 4765 levels[i].loadError++; 4766 this.log(`Removing level ${i} with key error (${data.error})`); 4767 this.hls.removeLevel(i); 4768 } 4769 } 4770 const frag = data.frag; 4771 if (this.hls.levels.length < levelCountWithError) { 4772 errorAction.resolved = true; 4773 } else if (frag && frag.type !== PlaylistLevelType.MAIN) { 4774 // Ignore key error for audio track with unmatched key (main session error) 4775 const fragLevelKey = frag.decryptdata; 4776 if (fragLevelKey && !levelKey.matches(fragLevelKey)) { 4777 errorAction.resolved = true; 4778 } 4779 } 4780 } 4781 break; 4782 } 4783 }
vendor: 169,885 bytes, lines 4784-9498
4784 // If not resolved by previous actions try to switch to next level 4785 if (!errorAction.resolved) { 4786 this.switchLevel(data, nextAutoLevel); 4787 } 4788 } 4789 switchLevel(data, levelIndex) { 4790 if (levelIndex !== undefined && data.errorAction) { 4791 this.warn(`switching to level ${levelIndex} after ${data.details}`); 4792 this.hls.nextAutoLevel = levelIndex; 4793 data.errorAction.resolved = true; 4794 // Stream controller is responsible for this but won't switch on false start 4795 this.hls.nextLoadLevel = this.hls.nextAutoLevel; 4796 if (data.details === ErrorDetails.BUFFER_ADD_CODEC_ERROR && data.mimeType && data.sourceBufferName !== 'audiovideo') { 4797 const codec = getCodecsForMimeType(data.mimeType); 4798 const levels = this.hls.levels; 4799 for (let i = levels.length; i--;) { 4800 if (levels[i][`${data.sourceBufferName}Codec`] === codec) { 4801 this.log(`Removing level ${i} for ${data.details} ("${codec}" not supported)`); 4802 this.hls.removeLevel(i); 4803 } 4804 } 4805 } 4806 } 4807 } 4808} 4809function createDoNothingErrorAction(resolved) { 4810 const errorAction = { 4811 action: NetworkErrorAction.DoNothing, 4812 flags: ErrorActionFlags.None 4813 }; 4814 if (resolved) { 4815 errorAction.resolved = true; 4816 } 4817 return errorAction; 4818} 4819 4820var FragmentState = { 4821 NOT_LOADED: "NOT_LOADED", 4822 APPENDING: "APPENDING", 4823 PARTIAL: "PARTIAL", 4824 OK: "OK" 4825}; 4826class FragmentTracker { 4827 constructor(hls) { 4828 this.activePartLists = Object.create(null); 4829 this.endListFragments = Object.create(null); 4830 this.fragments = Object.create(null); 4831 this.timeRanges = Object.create(null); 4832 this.bufferPadding = 0.2; 4833 this.hls = void 0; 4834 this.hasGaps = false; 4835 this.hls = hls; 4836 this._registerListeners(); 4837 } 4838 _registerListeners() { 4839 const { 4840 hls 4841 } = this; 4842 if (hls) { 4843 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 4844 hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this); 4845 hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this); 4846 hls.on(Events.FRAG_LOADED, this.onFragLoaded, this); 4847 } 4848 } 4849 _unregisterListeners() { 4850 const { 4851 hls 4852 } = this; 4853 if (hls) { 4854 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 4855 hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this); 4856 hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this); 4857 hls.off(Events.FRAG_LOADED, this.onFragLoaded, this); 4858 } 4859 } 4860 destroy() { 4861 this._unregisterListeners(); 4862 // @ts-ignore 4863 this.hls = 4864 // @ts-ignore 4865 this.fragments = 4866 // @ts-ignore 4867 this.activePartLists = 4868 // @ts-ignore 4869 this.endListFragments = this.timeRanges = null; 4870 } 4871 4872 /** 4873 * Return a Fragment or Part with an appended range that matches the position and levelType 4874 * Otherwise, return null 4875 */ 4876 getAppendedFrag(position, levelType) { 4877 const activeParts = this.activePartLists[levelType]; 4878 if (activeParts) { 4879 for (let i = activeParts.length; i--;) { 4880 const activePart = activeParts[i]; 4881 if (!activePart) { 4882 break; 4883 } 4884 if (activePart.start <= position && position <= activePart.end && activePart.loaded) { 4885 return activePart; 4886 } 4887 } 4888 } 4889 return this.getBufferedFrag(position, levelType); 4890 } 4891 4892 /** 4893 * Return a buffered Fragment that matches the position and levelType. 4894 * A buffered Fragment is one whose loading, parsing and appending is done (completed or "partial" meaning aborted). 4895 * If not found any Fragment, return null 4896 */ 4897 getBufferedFrag(position, levelType) { 4898 return this.getFragAtPos(position, levelType, true); 4899 } 4900 getFragAtPos(position, levelType, buffered) { 4901 const { 4902 fragments 4903 } = this; 4904 const keys = Object.keys(fragments); 4905 for (let i = keys.length; i--;) { 4906 const fragmentEntity = fragments[keys[i]]; 4907 if ((fragmentEntity == null ? void 0 : fragmentEntity.body.type) === levelType && (!buffered || fragmentEntity.buffered)) { 4908 const frag = fragmentEntity.body; 4909 if (frag.start <= position && position <= frag.end) { 4910 return frag; 4911 } 4912 } 4913 } 4914 return null; 4915 } 4916 4917 /** 4918 * Partial fragments effected by coded frame eviction will be removed 4919 * The browser will unload parts of the buffer to free up memory for new buffer data 4920 * Fragments will need to be reloaded when the buffer is freed up, removing partial fragments will allow them to reload(since there might be parts that are still playable) 4921 */ 4922 detectEvictedFragments(elementaryStream, timeRange, playlistType, appendedPart, removeAppending) { 4923 if (this.timeRanges) { 4924 this.timeRanges[elementaryStream] = timeRange; 4925 } 4926 // Check if any flagged fragments have been unloaded 4927 // excluding anything newer than appendedPartSn 4928 const appendedPartSn = (appendedPart == null ? void 0 : appendedPart.fragment.sn) || -1; 4929 Object.keys(this.fragments).forEach(key => { 4930 const fragmentEntity = this.fragments[key]; 4931 if (!fragmentEntity) { 4932 return; 4933 } 4934 if (appendedPartSn >= fragmentEntity.body.sn) { 4935 return; 4936 } 4937 if (!fragmentEntity.buffered && (!fragmentEntity.loaded || removeAppending)) { 4938 if (fragmentEntity.body.type === playlistType) { 4939 this.removeFragment(fragmentEntity.body); 4940 } 4941 return; 4942 } 4943 const esData = fragmentEntity.range[elementaryStream]; 4944 if (!esData) { 4945 return; 4946 } 4947 if (esData.time.length === 0) { 4948 this.removeFragment(fragmentEntity.body); 4949 return; 4950 } 4951 esData.time.some(time => { 4952 const isNotBuffered = !this.isTimeBuffered(time.startPTS, time.endPTS, timeRange); 4953 if (isNotBuffered) { 4954 // Unregister partial fragment as it needs to load again to be reused 4955 this.removeFragment(fragmentEntity.body); 4956 } 4957 return isNotBuffered; 4958 }); 4959 }); 4960 } 4961 4962 /** 4963 * Checks if the fragment passed in is loaded in the buffer properly 4964 * Partially loaded fragments will be registered as a partial fragment 4965 */ 4966 detectPartialFragments(data) { 4967 const timeRanges = this.timeRanges; 4968 if (!timeRanges || data.frag.sn === 'initSegment') { 4969 return; 4970 } 4971 const frag = data.frag; 4972 const fragKey = getFragmentKey(frag); 4973 const fragmentEntity = this.fragments[fragKey]; 4974 if (!fragmentEntity || fragmentEntity.buffered && frag.gap) { 4975 return; 4976 } 4977 const isFragHint = !frag.relurl; 4978 Object.keys(timeRanges).forEach(elementaryStream => { 4979 const streamInfo = frag.elementaryStreams[elementaryStream]; 4980 if (!streamInfo) { 4981 return; 4982 } 4983 const timeRange = timeRanges[elementaryStream]; 4984 const partial = isFragHint || streamInfo.partial === true; 4985 fragmentEntity.range[elementaryStream] = this.getBufferedTimes(frag, data.part, partial, timeRange); 4986 }); 4987 fragmentEntity.loaded = null; 4988 if (Object.keys(fragmentEntity.range).length) { 4989 this.bufferedEnd(fragmentEntity, frag); 4990 if (!isPartial(fragmentEntity)) { 4991 // Remove older fragment parts from lookup after frag is tracked as buffered 4992 this.removeParts(frag.sn - 1, frag.type); 4993 } 4994 } else { 4995 // remove fragment if nothing was appended 4996 this.removeFragment(fragmentEntity.body); 4997 } 4998 } 4999 bufferedEnd(fragmentEntity, frag) { 5000 fragmentEntity.buffered = true; 5001 const endList = fragmentEntity.body.endList = frag.endList || fragmentEntity.body.endList; 5002 if (endList) { 5003 this.endListFragments[fragmentEntity.body.type] = fragmentEntity; 5004 } 5005 } 5006 removeParts(snToKeep, levelType) { 5007 const activeParts = this.activePartLists[levelType]; 5008 if (!activeParts) { 5009 return; 5010 } 5011 this.activePartLists[levelType] = filterParts(activeParts, part => part.fragment.sn >= snToKeep); 5012 } 5013 fragBuffered(frag, force) { 5014 const fragKey = getFragmentKey(frag); 5015 let fragmentEntity = this.fragments[fragKey]; 5016 if (!fragmentEntity && force) { 5017 fragmentEntity = this.fragments[fragKey] = { 5018 body: frag, 5019 appendedPTS: null, 5020 loaded: null, 5021 buffered: false, 5022 range: Object.create(null) 5023 }; 5024 if (frag.gap) { 5025 this.hasGaps = true; 5026 } 5027 } 5028 if (fragmentEntity) { 5029 fragmentEntity.loaded = null; 5030 this.bufferedEnd(fragmentEntity, frag); 5031 } 5032 } 5033 getBufferedTimes(fragment, part, partial, timeRange) { 5034 const buffered = { 5035 time: [], 5036 partial 5037 }; 5038 const startPTS = fragment.start; 5039 const endPTS = fragment.end; 5040 const minEndPTS = fragment.minEndPTS || endPTS; 5041 const maxStartPTS = fragment.maxStartPTS || startPTS; 5042 for (let i = 0; i < timeRange.length; i++) { 5043 const startTime = timeRange.start(i) - this.bufferPadding; 5044 const endTime = timeRange.end(i) + this.bufferPadding; 5045 if (maxStartPTS >= startTime && minEndPTS <= endTime) { 5046 // Fragment is entirely contained in buffer 5047 // No need to check the other timeRange times since it's completely playable 5048 buffered.time.push({ 5049 startPTS: Math.max(startPTS, timeRange.start(i)), 5050 endPTS: Math.min(endPTS, timeRange.end(i)) 5051 }); 5052 break; 5053 } else if (startPTS < endTime && endPTS > startTime) { 5054 const start = Math.max(startPTS, timeRange.start(i)); 5055 const end = Math.min(endPTS, timeRange.end(i)); 5056 if (end > start) { 5057 buffered.partial = true; 5058 // Check for intersection with buffer 5059 // Get playable sections of the fragment 5060 buffered.time.push({ 5061 startPTS: start, 5062 endPTS: end 5063 }); 5064 } 5065 } else if (endPTS <= startTime) { 5066 // No need to check the rest of the timeRange as it is in order 5067 break; 5068 } 5069 } 5070 return buffered; 5071 } 5072 5073 /** 5074 * Gets the partial fragment for a certain time 5075 */ 5076 getPartialFragment(time) { 5077 let bestFragment = null; 5078 let timePadding; 5079 let startTime; 5080 let endTime; 5081 let bestOverlap = 0; 5082 const { 5083 bufferPadding, 5084 fragments 5085 } = this; 5086 Object.keys(fragments).forEach(key => { 5087 const fragmentEntity = fragments[key]; 5088 if (!fragmentEntity) { 5089 return; 5090 } 5091 if (isPartial(fragmentEntity)) { 5092 startTime = fragmentEntity.body.start - bufferPadding; 5093 endTime = fragmentEntity.body.end + bufferPadding; 5094 if (time >= startTime && time <= endTime) { 5095 // Use the fragment that has the most padding from start and end time 5096 timePadding = Math.min(time - startTime, endTime - time); 5097 if (bestOverlap <= timePadding) { 5098 bestFragment = fragmentEntity.body; 5099 bestOverlap = timePadding; 5100 } 5101 } 5102 } 5103 }); 5104 return bestFragment; 5105 } 5106 isEndListAppended(type) { 5107 const lastFragmentEntity = this.endListFragments[type]; 5108 return lastFragmentEntity !== undefined && (lastFragmentEntity.buffered || isPartial(lastFragmentEntity)); 5109 } 5110 getState(fragment) { 5111 const fragKey = getFragmentKey(fragment); 5112 const fragmentEntity = this.fragments[fragKey]; 5113 if (fragmentEntity) { 5114 if (!fragmentEntity.buffered) { 5115 return FragmentState.APPENDING; 5116 } else if (isPartial(fragmentEntity)) { 5117 return FragmentState.PARTIAL; 5118 } else { 5119 return FragmentState.OK; 5120 } 5121 } 5122 return FragmentState.NOT_LOADED; 5123 } 5124 isTimeBuffered(startPTS, endPTS, timeRange) { 5125 let startTime; 5126 let endTime; 5127 for (let i = 0; i < timeRange.length; i++) { 5128 startTime = timeRange.start(i) - this.bufferPadding; 5129 endTime = timeRange.end(i) + this.bufferPadding; 5130 if (startPTS >= startTime && endPTS <= endTime) { 5131 return true; 5132 } 5133 if (endPTS <= startTime) { 5134 // No need to check the rest of the timeRange as it is in order 5135 return false; 5136 } 5137 } 5138 return false; 5139 } 5140 onManifestLoading() { 5141 this.removeAllFragments(); 5142 } 5143 onFragLoaded(event, data) { 5144 // don't track initsegment (for which sn is not a number) 5145 // don't track frags used for bitrateTest, they're irrelevant. 5146 if (data.frag.sn === 'initSegment' || data.frag.bitrateTest) { 5147 return; 5148 } 5149 const frag = data.frag; 5150 // Fragment entity `loaded` FragLoadedData is null when loading parts 5151 const loaded = data.part ? null : data; 5152 const fragKey = getFragmentKey(frag); 5153 this.fragments[fragKey] = { 5154 body: frag, 5155 appendedPTS: null, 5156 loaded, 5157 buffered: false, 5158 range: Object.create(null) 5159 }; 5160 } 5161 onBufferAppended(event, data) { 5162 const { 5163 frag, 5164 part, 5165 timeRanges, 5166 type 5167 } = data; 5168 if (frag.sn === 'initSegment') { 5169 return; 5170 } 5171 const playlistType = frag.type; 5172 if (part) { 5173 let activeParts = this.activePartLists[playlistType]; 5174 if (!activeParts) { 5175 this.activePartLists[playlistType] = activeParts = []; 5176 } 5177 activeParts.push(part); 5178 } 5179 // Store the latest timeRanges loaded in the buffer 5180 this.timeRanges = timeRanges; 5181 const timeRange = timeRanges[type]; 5182 this.detectEvictedFragments(type, timeRange, playlistType, part); 5183 } 5184 onFragBuffered(event, data) { 5185 this.detectPartialFragments(data); 5186 } 5187 hasFragment(fragment) { 5188 const fragKey = getFragmentKey(fragment); 5189 return !!this.fragments[fragKey]; 5190 } 5191 hasFragments(type) { 5192 const { 5193 fragments 5194 } = this; 5195 const keys = Object.keys(fragments); 5196 if (!type) { 5197 return keys.length > 0; 5198 } 5199 for (let i = keys.length; i--;) { 5200 const fragmentEntity = fragments[keys[i]]; 5201 if ((fragmentEntity == null ? void 0 : fragmentEntity.body.type) === type) { 5202 return true; 5203 } 5204 } 5205 return false; 5206 } 5207 hasParts(type) { 5208 var _this$activePartLists; 5209 return !!((_this$activePartLists = this.activePartLists[type]) != null && _this$activePartLists.length); 5210 } 5211 removeFragmentsInRange(start, end, playlistType, withGapOnly, unbufferedOnly) { 5212 if (withGapOnly && !this.hasGaps) { 5213 return; 5214 } 5215 Object.keys(this.fragments).forEach(key => { 5216 const fragmentEntity = this.fragments[key]; 5217 if (!fragmentEntity) { 5218 return; 5219 } 5220 const frag = fragmentEntity.body; 5221 if (frag.type !== playlistType || withGapOnly && !frag.gap) { 5222 return; 5223 } 5224 if (frag.start < end && frag.end > start && (fragmentEntity.buffered || unbufferedOnly)) { 5225 this.removeFragment(frag); 5226 } 5227 }); 5228 } 5229 removeFragment(fragment) { 5230 const fragKey = getFragmentKey(fragment); 5231 fragment.clearElementaryStreamInfo(); 5232 const activeParts = this.activePartLists[fragment.type]; 5233 if (activeParts) { 5234 const snToRemove = fragment.sn; 5235 this.activePartLists[fragment.type] = filterParts(activeParts, part => part.fragment.sn !== snToRemove); 5236 } 5237 delete this.fragments[fragKey]; 5238 if (fragment.endList) { 5239 delete this.endListFragments[fragment.type]; 5240 } 5241 } 5242 removeAllFragments() { 5243 var _this$hls; 5244 this.fragments = Object.create(null); 5245 this.endListFragments = Object.create(null); 5246 this.activePartLists = Object.create(null); 5247 this.hasGaps = false; 5248 const partlist = (_this$hls = this.hls) == null || (_this$hls = _this$hls.latestLevelDetails) == null ? void 0 : _this$hls.partList; 5249 if (partlist) { 5250 partlist.forEach(part => part.clearElementaryStreamInfo()); 5251 } 5252 } 5253} 5254function isPartial(fragmentEntity) { 5255 var _fragmentEntity$range, _fragmentEntity$range2, _fragmentEntity$range3; 5256 return fragmentEntity.buffered && !!(fragmentEntity.body.gap || (_fragmentEntity$range = fragmentEntity.range.video) != null && _fragmentEntity$range.partial || (_fragmentEntity$range2 = fragmentEntity.range.audio) != null && _fragmentEntity$range2.partial || (_fragmentEntity$range3 = fragmentEntity.range.audiovideo) != null && _fragmentEntity$range3.partial); 5257} 5258function getFragmentKey(fragment) { 5259 return `${fragment.type}_${fragment.level}_${fragment.sn}`; 5260} 5261function filterParts(partList, predicate) { 5262 return partList.filter(part => { 5263 const keep = predicate(part); 5264 if (!keep) { 5265 part.clearElementaryStreamInfo(); 5266 } 5267 return keep; 5268 }); 5269} 5270 5271var DecrypterAesMode = { 5272 cbc: 0, 5273 ctr: 1 5274}; 5275 5276class AESCrypto { 5277 constructor(subtle, iv, aesMode) { 5278 this.subtle = void 0; 5279 this.aesIV = void 0; 5280 this.aesMode = void 0; 5281 this.subtle = subtle; 5282 this.aesIV = iv; 5283 this.aesMode = aesMode; 5284 } 5285 decrypt(data, key) { 5286 switch (this.aesMode) { 5287 case DecrypterAesMode.cbc: 5288 return this.subtle.decrypt({ 5289 name: 'AES-CBC', 5290 iv: this.aesIV 5291 }, key, data); 5292 case DecrypterAesMode.ctr: 5293 return this.subtle.decrypt({ 5294 name: 'AES-CTR', 5295 counter: this.aesIV, 5296 length: 64 5297 }, 5298 //64 : NIST SP800-38A standard suggests that the counter should occupy half of the counter block 5299 key, data); 5300 default: 5301 throw new Error(`[AESCrypto] invalid aes mode ${this.aesMode}`); 5302 } 5303 } 5304} 5305 5306// PKCS7 5307function removePadding(array) { 5308 const outputBytes = array.byteLength; 5309 const paddingBytes = outputBytes && new DataView(array.buffer).getUint8(outputBytes - 1); 5310 if (paddingBytes) { 5311 return array.slice(0, outputBytes - paddingBytes); 5312 } 5313 return array; 5314} 5315class AESDecryptor { 5316 constructor() { 5317 this.rcon = [0x0, 0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36]; 5318 this.subMix = [new Uint32Array(256), new Uint32Array(256), new Uint32Array(256), new Uint32Array(256)]; 5319 this.invSubMix = [new Uint32Array(256), new Uint32Array(256), new Uint32Array(256), new Uint32Array(256)]; 5320 this.sBox = new Uint32Array(256); 5321 this.invSBox = new Uint32Array(256); 5322 this.key = new Uint32Array(0); 5323 this.ksRows = 0; 5324 this.keySize = 0; 5325 this.keySchedule = void 0; 5326 this.invKeySchedule = void 0; 5327 this.initTable(); 5328 } 5329 5330 // Using view.getUint32() also swaps the byte order. 5331 uint8ArrayToUint32Array_(arrayBuffer) { 5332 const view = new DataView(arrayBuffer); 5333 const newArray = new Uint32Array(4); 5334 for (let i = 0; i < 4; i++) { 5335 newArray[i] = view.getUint32(i * 4); 5336 } 5337 return newArray; 5338 } 5339 initTable() { 5340 const sBox = this.sBox; 5341 const invSBox = this.invSBox; 5342 const subMix = this.subMix; 5343 const subMix0 = subMix[0]; 5344 const subMix1 = subMix[1]; 5345 const subMix2 = subMix[2]; 5346 const subMix3 = subMix[3]; 5347 const invSubMix = this.invSubMix; 5348 const invSubMix0 = invSubMix[0]; 5349 const invSubMix1 = invSubMix[1]; 5350 const invSubMix2 = invSubMix[2]; 5351 const invSubMix3 = invSubMix[3]; 5352 const d = new Uint32Array(256); 5353 let x = 0; 5354 let xi = 0; 5355 let i = 0; 5356 for (i = 0; i < 256; i++) { 5357 if (i < 128) { 5358 d[i] = i << 1; 5359 } else { 5360 d[i] = i << 1 ^ 0x11b; 5361 } 5362 } 5363 for (i = 0; i < 256; i++) { 5364 let sx = xi ^ xi << 1 ^ xi << 2 ^ xi << 3 ^ xi << 4; 5365 sx = sx >>> 8 ^ sx & 0xff ^ 0x63; 5366 sBox[x] = sx; 5367 invSBox[sx] = x; 5368 5369 // Compute multiplication 5370 const x2 = d[x]; 5371 const x4 = d[x2]; 5372 const x8 = d[x4]; 5373 5374 // Compute sub/invSub bytes, mix columns tables 5375 let t = d[sx] * 0x101 ^ sx * 0x1010100; 5376 subMix0[x] = t << 24 | t >>> 8; 5377 subMix1[x] = t << 16 | t >>> 16; 5378 subMix2[x] = t << 8 | t >>> 24; 5379 subMix3[x] = t; 5380 5381 // Compute inv sub bytes, inv mix columns tables 5382 t = x8 * 0x1010101 ^ x4 * 0x10001 ^ x2 * 0x101 ^ x * 0x1010100; 5383 invSubMix0[sx] = t << 24 | t >>> 8; 5384 invSubMix1[sx] = t << 16 | t >>> 16; 5385 invSubMix2[sx] = t << 8 | t >>> 24; 5386 invSubMix3[sx] = t; 5387 5388 // Compute next counter 5389 if (!x) { 5390 x = xi = 1; 5391 } else { 5392 x = x2 ^ d[d[d[x8 ^ x2]]]; 5393 xi ^= d[d[xi]]; 5394 } 5395 } 5396 } 5397 expandKey(keyBuffer) { 5398 // convert keyBuffer to Uint32Array 5399 const key = this.uint8ArrayToUint32Array_(keyBuffer); 5400 let sameKey = true; 5401 let offset = 0; 5402 while (offset < key.length && sameKey) { 5403 sameKey = key[offset] === this.key[offset]; 5404 offset++; 5405 } 5406 if (sameKey) { 5407 return; 5408 } 5409 this.key = key; 5410 const keySize = this.keySize = key.length; 5411 if (keySize !== 4 && keySize !== 6 && keySize !== 8) { 5412 throw new Error('Invalid aes key size=' + keySize); 5413 } 5414 const ksRows = this.ksRows = (keySize + 6 + 1) * 4; 5415 let ksRow; 5416 let invKsRow; 5417 const keySchedule = this.keySchedule = new Uint32Array(ksRows); 5418 const invKeySchedule = this.invKeySchedule = new Uint32Array(ksRows); 5419 const sbox = this.sBox; 5420 const rcon = this.rcon; 5421 const invSubMix = this.invSubMix; 5422 const invSubMix0 = invSubMix[0]; 5423 const invSubMix1 = invSubMix[1]; 5424 const invSubMix2 = invSubMix[2]; 5425 const invSubMix3 = invSubMix[3]; 5426 let prev; 5427 let t; 5428 for (ksRow = 0; ksRow < ksRows; ksRow++) { 5429 if (ksRow < keySize) { 5430 prev = keySchedule[ksRow] = key[ksRow]; 5431 continue; 5432 } 5433 t = prev; 5434 if (ksRow % keySize === 0) { 5435 // Rot word 5436 t = t << 8 | t >>> 24; 5437 5438 // Sub word 5439 t = sbox[t >>> 24] << 24 | sbox[t >>> 16 & 0xff] << 16 | sbox[t >>> 8 & 0xff] << 8 | sbox[t & 0xff]; 5440 5441 // Mix Rcon 5442 t ^= rcon[ksRow / keySize | 0] << 24; 5443 } else if (keySize > 6 && ksRow % keySize === 4) { 5444 // Sub word 5445 t = sbox[t >>> 24] << 24 | sbox[t >>> 16 & 0xff] << 16 | sbox[t >>> 8 & 0xff] << 8 | sbox[t & 0xff]; 5446 } 5447 keySchedule[ksRow] = prev = (keySchedule[ksRow - keySize] ^ t) >>> 0; 5448 } 5449 for (invKsRow = 0; invKsRow < ksRows; invKsRow++) { 5450 ksRow = ksRows - invKsRow; 5451 if (invKsRow & 3) { 5452 t = keySchedule[ksRow]; 5453 } else { 5454 t = keySchedule[ksRow - 4]; 5455 } 5456 if (invKsRow < 4 || ksRow <= 4) { 5457 invKeySchedule[invKsRow] = t; 5458 } else { 5459 invKeySchedule[invKsRow] = invSubMix0[sbox[t >>> 24]] ^ invSubMix1[sbox[t >>> 16 & 0xff]] ^ invSubMix2[sbox[t >>> 8 & 0xff]] ^ invSubMix3[sbox[t & 0xff]]; 5460 } 5461 invKeySchedule[invKsRow] = invKeySchedule[invKsRow] >>> 0; 5462 } 5463 } 5464 5465 // Adding this as a method greatly improves performance. 5466 networkToHostOrderSwap(word) { 5467 return word << 24 | (word & 0xff00) << 8 | (word & 0xff0000) >> 8 | word >>> 24; 5468 } 5469 decrypt(inputArrayBuffer, offset, aesIV) { 5470 const nRounds = this.keySize + 6; 5471 const invKeySchedule = this.invKeySchedule; 5472 const invSBOX = this.invSBox; 5473 const invSubMix = this.invSubMix; 5474 const invSubMix0 = invSubMix[0]; 5475 const invSubMix1 = invSubMix[1]; 5476 const invSubMix2 = invSubMix[2]; 5477 const invSubMix3 = invSubMix[3]; 5478 const initVector = this.uint8ArrayToUint32Array_(aesIV); 5479 let initVector0 = initVector[0]; 5480 let initVector1 = initVector[1]; 5481 let initVector2 = initVector[2]; 5482 let initVector3 = initVector[3]; 5483 const inputInt32 = new Int32Array(inputArrayBuffer); 5484 const outputInt32 = new Int32Array(inputInt32.length); 5485 let t0, t1, t2, t3; 5486 let s0, s1, s2, s3; 5487 let inputWords0, inputWords1, inputWords2, inputWords3; 5488 let ksRow, i; 5489 const swapWord = this.networkToHostOrderSwap; 5490 while (offset < inputInt32.length) { 5491 inputWords0 = swapWord(inputInt32[offset]); 5492 inputWords1 = swapWord(inputInt32[offset + 1]); 5493 inputWords2 = swapWord(inputInt32[offset + 2]); 5494 inputWords3 = swapWord(inputInt32[offset + 3]); 5495 s0 = inputWords0 ^ invKeySchedule[0]; 5496 s1 = inputWords3 ^ invKeySchedule[1]; 5497 s2 = inputWords2 ^ invKeySchedule[2]; 5498 s3 = inputWords1 ^ invKeySchedule[3]; 5499 ksRow = 4; 5500 5501 // Iterate through the rounds of decryption 5502 for (i = 1; i < nRounds; i++) { 5503 t0 = invSubMix0[s0 >>> 24] ^ invSubMix1[s1 >> 16 & 0xff] ^ invSubMix2[s2 >> 8 & 0xff] ^ invSubMix3[s3 & 0xff] ^ invKeySchedule[ksRow]; 5504 t1 = invSubMix0[s1 >>> 24] ^ invSubMix1[s2 >> 16 & 0xff] ^ invSubMix2[s3 >> 8 & 0xff] ^ invSubMix3[s0 & 0xff] ^ invKeySchedule[ksRow + 1]; 5505 t2 = invSubMix0[s2 >>> 24] ^ invSubMix1[s3 >> 16 & 0xff] ^ invSubMix2[s0 >> 8 & 0xff] ^ invSubMix3[s1 & 0xff] ^ invKeySchedule[ksRow + 2]; 5506 t3 = invSubMix0[s3 >>> 24] ^ invSubMix1[s0 >> 16 & 0xff] ^ invSubMix2[s1 >> 8 & 0xff] ^ invSubMix3[s2 & 0xff] ^ invKeySchedule[ksRow + 3]; 5507 // Update state 5508 s0 = t0; 5509 s1 = t1; 5510 s2 = t2; 5511 s3 = t3; 5512 ksRow = ksRow + 4; 5513 } 5514 5515 // Shift rows, sub bytes, add round key 5516 t0 = invSBOX[s0 >>> 24] << 24 ^ invSBOX[s1 >> 16 & 0xff] << 16 ^ invSBOX[s2 >> 8 & 0xff] << 8 ^ invSBOX[s3 & 0xff] ^ invKeySchedule[ksRow]; 5517 t1 = invSBOX[s1 >>> 24] << 24 ^ invSBOX[s2 >> 16 & 0xff] << 16 ^ invSBOX[s3 >> 8 & 0xff] << 8 ^ invSBOX[s0 & 0xff] ^ invKeySchedule[ksRow + 1]; 5518 t2 = invSBOX[s2 >>> 24] << 24 ^ invSBOX[s3 >> 16 & 0xff] << 16 ^ invSBOX[s0 >> 8 & 0xff] << 8 ^ invSBOX[s1 & 0xff] ^ invKeySchedule[ksRow + 2]; 5519 t3 = invSBOX[s3 >>> 24] << 24 ^ invSBOX[s0 >> 16 & 0xff] << 16 ^ invSBOX[s1 >> 8 & 0xff] << 8 ^ invSBOX[s2 & 0xff] ^ invKeySchedule[ksRow + 3]; 5520 5521 // Write 5522 outputInt32[offset] = swapWord(t0 ^ initVector0); 5523 outputInt32[offset + 1] = swapWord(t3 ^ initVector1); 5524 outputInt32[offset + 2] = swapWord(t2 ^ initVector2); 5525 outputInt32[offset + 3] = swapWord(t1 ^ initVector3); 5526 5527 // reset initVector to last 4 unsigned int 5528 initVector0 = inputWords0; 5529 initVector1 = inputWords1; 5530 initVector2 = inputWords2; 5531 initVector3 = inputWords3; 5532 offset = offset + 4; 5533 } 5534 return outputInt32.buffer; 5535 } 5536} 5537 5538class FastAESKey { 5539 constructor(subtle, key, aesMode) { 5540 this.subtle = void 0; 5541 this.key = void 0; 5542 this.aesMode = void 0; 5543 this.subtle = subtle; 5544 this.key = key; 5545 this.aesMode = aesMode; 5546 } 5547 expandKey() { 5548 const subtleAlgoName = getSubtleAlgoName(this.aesMode); 5549 return this.subtle.importKey('raw', this.key, { 5550 name: subtleAlgoName 5551 }, false, ['encrypt', 'decrypt']); 5552 } 5553} 5554function getSubtleAlgoName(aesMode) { 5555 switch (aesMode) { 5556 case DecrypterAesMode.cbc: 5557 return 'AES-CBC'; 5558 case DecrypterAesMode.ctr: 5559 return 'AES-CTR'; 5560 default: 5561 throw new Error(`[FastAESKey] invalid aes mode ${aesMode}`); 5562 } 5563} 5564 5565const CHUNK_SIZE = 16; // 16 bytes, 128 bits 5566 5567class Decrypter { 5568 constructor(config, { 5569 removePKCS7Padding = true 5570 } = {}) { 5571 this.logEnabled = true; 5572 this.removePKCS7Padding = void 0; 5573 this.subtle = null; 5574 this.softwareDecrypter = null; 5575 this.key = null; 5576 this.fastAesKey = null; 5577 this.remainderData = null; 5578 this.currentIV = null; 5579 this.currentResult = null; 5580 this.useSoftware = void 0; 5581 this.enableSoftwareAES = void 0; 5582 this.enableSoftwareAES = config.enableSoftwareAES; 5583 this.removePKCS7Padding = removePKCS7Padding; 5584 // built in decryptor expects PKCS7 padding 5585 if (removePKCS7Padding) { 5586 try { 5587 const browserCrypto = self.crypto; 5588 if (browserCrypto) { 5589 this.subtle = browserCrypto.subtle || browserCrypto.webkitSubtle; 5590 } 5591 } catch (e) { 5592 /* no-op */ 5593 } 5594 } 5595 this.useSoftware = !this.subtle; 5596 } 5597 destroy() { 5598 this.subtle = null; 5599 this.softwareDecrypter = null; 5600 this.key = null; 5601 this.fastAesKey = null; 5602 this.remainderData = null; 5603 this.currentIV = null; 5604 this.currentResult = null; 5605 } 5606 isSync() { 5607 return this.useSoftware; 5608 } 5609 flush() { 5610 const { 5611 currentResult, 5612 remainderData 5613 } = this; 5614 if (!currentResult || remainderData) { 5615 this.reset(); 5616 return null; 5617 } 5618 const data = new Uint8Array(currentResult); 5619 this.reset(); 5620 if (this.removePKCS7Padding) { 5621 return removePadding(data); 5622 } 5623 return data; 5624 } 5625 reset() { 5626 this.currentResult = null; 5627 this.currentIV = null; 5628 this.remainderData = null; 5629 if (this.softwareDecrypter) { 5630 this.softwareDecrypter = null; 5631 } 5632 } 5633 decrypt(data, key, iv, aesMode) { 5634 if (this.useSoftware) { 5635 return new Promise((resolve, reject) => { 5636 const dataView = ArrayBuffer.isView(data) ? data : new Uint8Array(data); 5637 this.softwareDecrypt(dataView, key, iv, aesMode); 5638 const decryptResult = this.flush(); 5639 if (decryptResult) { 5640 resolve(decryptResult.buffer); 5641 } else { 5642 reject(new Error('[softwareDecrypt] Failed to decrypt data')); 5643 } 5644 }); 5645 } 5646 return this.webCryptoDecrypt(new Uint8Array(data), key, iv, aesMode); 5647 } 5648 5649 // Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached 5650 // data is handled in the flush() call 5651 softwareDecrypt(data, key, iv, aesMode) { 5652 const { 5653 currentIV, 5654 currentResult, 5655 remainderData 5656 } = this; 5657 if (aesMode !== DecrypterAesMode.cbc || key.byteLength !== 16) { 5658 logger.warn('SoftwareDecrypt: can only handle AES-128-CBC'); 5659 return null; 5660 } 5661 this.logOnce('JS AES decrypt'); 5662 // The output is staggered during progressive parsing - the current result is cached, and emitted on the next call 5663 // This is done in order to strip PKCS7 padding, which is found at the end of each segment. We only know we've reached 5664 // the end on flush(), but by that time we have already received all bytes for the segment. 5665 // Progressive decryption does not work with WebCrypto 5666 5667 if (remainderData) { 5668 data = appendUint8Array(remainderData, data); 5669 this.remainderData = null; 5670 } 5671 5672 // Byte length must be a multiple of 16 (AES-128 = 128 bit blocks = 16 bytes) 5673 const currentChunk = this.getValidChunk(data); 5674 if (!currentChunk.length) { 5675 return null; 5676 } 5677 if (currentIV) { 5678 iv = currentIV; 5679 } 5680 let softwareDecrypter = this.softwareDecrypter; 5681 if (!softwareDecrypter) { 5682 softwareDecrypter = this.softwareDecrypter = new AESDecryptor(); 5683 } 5684 softwareDecrypter.expandKey(key); 5685 const result = currentResult; 5686 this.currentResult = softwareDecrypter.decrypt(currentChunk.buffer, 0, iv); 5687 this.currentIV = currentChunk.slice(-16).buffer; 5688 if (!result) { 5689 return null; 5690 } 5691 return result; 5692 } 5693 webCryptoDecrypt(data, key, iv, aesMode) { 5694 if (this.key !== key || !this.fastAesKey) { 5695 if (!this.subtle) { 5696 return Promise.resolve(this.onWebCryptoError(data, key, iv, aesMode)); 5697 } 5698 this.key = key; 5699 this.fastAesKey = new FastAESKey(this.subtle, key, aesMode); 5700 } 5701 return this.fastAesKey.expandKey().then(aesKey => { 5702 // decrypt using web crypto 5703 if (!this.subtle) { 5704 return Promise.reject(new Error('web crypto not initialized')); 5705 } 5706 this.logOnce('WebCrypto AES decrypt'); 5707 const crypto = new AESCrypto(this.subtle, new Uint8Array(iv), aesMode); 5708 return crypto.decrypt(data.buffer, aesKey); 5709 }).catch(err => { 5710 logger.warn(`[decrypter]: WebCrypto Error, disable WebCrypto API, ${err.name}: ${err.message}`); 5711 return this.onWebCryptoError(data, key, iv, aesMode); 5712 }); 5713 } 5714 onWebCryptoError(data, key, iv, aesMode) { 5715 const enableSoftwareAES = this.enableSoftwareAES; 5716 if (enableSoftwareAES) { 5717 this.useSoftware = true; 5718 this.logEnabled = true; 5719 this.softwareDecrypt(data, key, iv, aesMode); 5720 const decryptResult = this.flush(); 5721 if (decryptResult) { 5722 return decryptResult.buffer; 5723 } 5724 } 5725 throw new Error('WebCrypto' + (enableSoftwareAES ? ' and softwareDecrypt' : '') + ': failed to decrypt data'); 5726 } 5727 getValidChunk(data) { 5728 let currentChunk = data; 5729 const splitPoint = data.length - data.length % CHUNK_SIZE; 5730 if (splitPoint !== data.length) { 5731 currentChunk = data.slice(0, splitPoint); 5732 this.remainderData = data.slice(splitPoint); 5733 } 5734 return currentChunk; 5735 } 5736 logOnce(msg) { 5737 if (!this.logEnabled) { 5738 return; 5739 } 5740 logger.log(`[decrypter]: ${msg}`); 5741 this.logEnabled = false; 5742 } 5743} 5744 5745const MIN_CHUNK_SIZE = Math.pow(2, 17); // 128kb 5746 5747class FragmentLoader { 5748 constructor(config) { 5749 this.config = void 0; 5750 this.loader = null; 5751 this.partLoadTimeout = -1; 5752 this.config = config; 5753 } 5754 destroy() { 5755 if (this.loader) { 5756 this.loader.destroy(); 5757 this.loader = null; 5758 } 5759 } 5760 abort() { 5761 if (this.loader) { 5762 // Abort the loader for current fragment. Only one may load at any given time 5763 this.loader.abort(); 5764 } 5765 } 5766 load(frag, onProgress) { 5767 const url = frag.url; 5768 if (!url) { 5769 return Promise.reject(new LoadError({ 5770 type: ErrorTypes.NETWORK_ERROR, 5771 details: ErrorDetails.FRAG_LOAD_ERROR, 5772 fatal: false, 5773 frag, 5774 error: new Error(`Fragment does not have a ${url ? 'part list' : 'url'}`), 5775 networkDetails: null 5776 })); 5777 } 5778 this.abort(); 5779 const config = this.config; 5780 const FragmentILoader = config.fLoader; 5781 const DefaultILoader = config.loader; 5782 return new Promise((resolve, reject) => { 5783 if (this.loader) { 5784 this.loader.destroy(); 5785 } 5786 if (frag.gap) { 5787 if (frag.tagList.some(tags => tags[0] === 'GAP')) { 5788 reject(createGapLoadError(frag)); 5789 return; 5790 } else { 5791 // Reset temporary treatment as GAP tag 5792 frag.gap = false; 5793 } 5794 } 5795 const loader = this.loader = FragmentILoader ? new FragmentILoader(config) : new DefaultILoader(config); 5796 const loaderContext = createLoaderContext(frag); 5797 frag.loader = loader; 5798 const loadPolicy = getLoaderConfigWithoutReties(config.fragLoadPolicy.default); 5799 const loaderConfig = { 5800 loadPolicy, 5801 timeout: loadPolicy.maxLoadTimeMs, 5802 maxRetry: 0, 5803 retryDelay: 0, 5804 maxRetryDelay: 0, 5805 highWaterMark: frag.sn === 'initSegment' ? Infinity : MIN_CHUNK_SIZE 5806 }; 5807 // Assign frag stats to the loader's stats reference 5808 frag.stats = loader.stats; 5809 const callbacks = { 5810 onSuccess: (response, stats, context, networkDetails) => { 5811 this.resetLoader(frag, loader); 5812 let payload = response.data; 5813 if (context.resetIV && frag.decryptdata) { 5814 frag.decryptdata.iv = new Uint8Array(payload.slice(0, 16)); 5815 payload = payload.slice(16); 5816 } 5817 resolve({ 5818 frag, 5819 part: null, 5820 payload, 5821 networkDetails 5822 }); 5823 }, 5824 onError: (response, context, networkDetails, stats) => { 5825 this.resetLoader(frag, loader); 5826 reject(new LoadError({ 5827 type: ErrorTypes.NETWORK_ERROR, 5828 details: ErrorDetails.FRAG_LOAD_ERROR, 5829 fatal: false, 5830 frag, 5831 response: _objectSpread2({ 5832 url, 5833 data: undefined 5834 }, response), 5835 error: new Error(`HTTP Error ${response.code} ${response.text}`), 5836 networkDetails, 5837 stats 5838 })); 5839 }, 5840 onAbort: (stats, context, networkDetails) => { 5841 this.resetLoader(frag, loader); 5842 reject(new LoadError({ 5843 type: ErrorTypes.NETWORK_ERROR, 5844 details: ErrorDetails.INTERNAL_ABORTED, 5845 fatal: false, 5846 frag, 5847 error: new Error('Aborted'), 5848 networkDetails, 5849 stats 5850 })); 5851 }, 5852 onTimeout: (stats, context, networkDetails) => { 5853 this.resetLoader(frag, loader); 5854 reject(new LoadError({ 5855 type: ErrorTypes.NETWORK_ERROR, 5856 details: ErrorDetails.FRAG_LOAD_TIMEOUT, 5857 fatal: false, 5858 frag, 5859 error: new Error(`Timeout after ${loaderConfig.timeout}ms`), 5860 networkDetails, 5861 stats 5862 })); 5863 } 5864 }; 5865 if (onProgress) { 5866 callbacks.onProgress = (stats, context, data, networkDetails) => onProgress({ 5867 frag, 5868 part: null, 5869 payload: data, 5870 networkDetails 5871 }); 5872 } 5873 loader.load(loaderContext, loaderConfig, callbacks); 5874 }); 5875 } 5876 loadPart(frag, part, onProgress) { 5877 this.abort(); 5878 const config = this.config; 5879 const FragmentILoader = config.fLoader; 5880 const DefaultILoader = config.loader; 5881 return new Promise((resolve, reject) => { 5882 if (this.loader) { 5883 this.loader.destroy(); 5884 } 5885 if (frag.gap || part.gap) { 5886 reject(createGapLoadError(frag, part)); 5887 return; 5888 } 5889 const loader = this.loader = FragmentILoader ? new FragmentILoader(config) : new DefaultILoader(config); 5890 const loaderContext = createLoaderContext(frag, part); 5891 frag.loader = loader; 5892 // Should we define another load policy for parts? 5893 const loadPolicy = getLoaderConfigWithoutReties(config.fragLoadPolicy.default); 5894 const loaderConfig = { 5895 loadPolicy, 5896 timeout: loadPolicy.maxLoadTimeMs, 5897 maxRetry: 0, 5898 retryDelay: 0, 5899 maxRetryDelay: 0, 5900 highWaterMark: MIN_CHUNK_SIZE 5901 }; 5902 // Assign part stats to the loader's stats reference 5903 part.stats = loader.stats; 5904 loader.load(loaderContext, loaderConfig, { 5905 onSuccess: (response, stats, context, networkDetails) => { 5906 this.resetLoader(frag, loader); 5907 this.updateStatsFromPart(frag, part); 5908 const partLoadedData = { 5909 frag, 5910 part, 5911 payload: response.data, 5912 networkDetails 5913 }; 5914 onProgress(partLoadedData); 5915 resolve(partLoadedData); 5916 }, 5917 onError: (response, context, networkDetails, stats) => { 5918 this.resetLoader(frag, loader); 5919 reject(new LoadError({ 5920 type: ErrorTypes.NETWORK_ERROR, 5921 details: ErrorDetails.FRAG_LOAD_ERROR, 5922 fatal: false, 5923 frag, 5924 part, 5925 response: _objectSpread2({ 5926 url: loaderContext.url, 5927 data: undefined 5928 }, response), 5929 error: new Error(`HTTP Error ${response.code} ${response.text}`), 5930 networkDetails, 5931 stats 5932 })); 5933 }, 5934 onAbort: (stats, context, networkDetails) => { 5935 frag.stats.aborted = part.stats.aborted; 5936 this.resetLoader(frag, loader); 5937 reject(new LoadError({ 5938 type: ErrorTypes.NETWORK_ERROR, 5939 details: ErrorDetails.INTERNAL_ABORTED, 5940 fatal: false, 5941 frag, 5942 part, 5943 error: new Error('Aborted'), 5944 networkDetails, 5945 stats 5946 })); 5947 }, 5948 onTimeout: (stats, context, networkDetails) => { 5949 this.resetLoader(frag, loader); 5950 reject(new LoadError({ 5951 type: ErrorTypes.NETWORK_ERROR, 5952 details: ErrorDetails.FRAG_LOAD_TIMEOUT, 5953 fatal: false, 5954 frag, 5955 part, 5956 error: new Error(`Timeout after ${loaderConfig.timeout}ms`), 5957 networkDetails, 5958 stats 5959 })); 5960 } 5961 }); 5962 }); 5963 } 5964 updateStatsFromPart(frag, part) { 5965 const fragStats = frag.stats; 5966 const partStats = part.stats; 5967 const partTotal = partStats.total; 5968 fragStats.loaded += partStats.loaded; 5969 if (partTotal) { 5970 const estTotalParts = Math.round(frag.duration / part.duration); 5971 const estLoadedParts = Math.min(Math.round(fragStats.loaded / partTotal), estTotalParts); 5972 const estRemainingParts = estTotalParts - estLoadedParts; 5973 const estRemainingBytes = estRemainingParts * Math.round(fragStats.loaded / estLoadedParts); 5974 fragStats.total = fragStats.loaded + estRemainingBytes; 5975 } else { 5976 fragStats.total = Math.max(fragStats.loaded, fragStats.total); 5977 } 5978 const fragLoading = fragStats.loading; 5979 const partLoading = partStats.loading; 5980 if (fragLoading.start) { 5981 // add to fragment loader latency 5982 fragLoading.first += partLoading.first - partLoading.start; 5983 } else { 5984 fragLoading.start = partLoading.start; 5985 fragLoading.first = partLoading.first; 5986 } 5987 fragLoading.end = partLoading.end; 5988 } 5989 resetLoader(frag, loader) { 5990 frag.loader = null; 5991 if (this.loader === loader) { 5992 self.clearTimeout(this.partLoadTimeout); 5993 this.loader = null; 5994 } 5995 loader.destroy(); 5996 } 5997} 5998function createLoaderContext(frag, part = null) { 5999 const segment = part || frag; 6000 const loaderContext = { 6001 frag, 6002 part, 6003 responseType: 'arraybuffer', 6004 url: segment.url, 6005 headers: {}, 6006 rangeStart: 0, 6007 rangeEnd: 0 6008 }; 6009 const start = segment.byteRangeStartOffset; 6010 const end = segment.byteRangeEndOffset; 6011 if (isFiniteNumber(start) && isFiniteNumber(end)) { 6012 var _frag$decryptdata; 6013 let byteRangeStart = start; 6014 let byteRangeEnd = end; 6015 if (frag.sn === 'initSegment' && isMethodFullSegmentAesCbc((_frag$decryptdata = frag.decryptdata) == null ? void 0 : _frag$decryptdata.method)) { 6016 // MAP segment encrypted with method 'AES-128' or 'AES-256' (cbc), when served with HTTP Range, 6017 // has the unencrypted size specified in the range. 6018 // Ref: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-08#section-6.3.6 6019 const fragmentLen = end - start; 6020 if (fragmentLen % 16) { 6021 byteRangeEnd = end + (16 - fragmentLen % 16); 6022 } 6023 if (start !== 0) { 6024 loaderContext.resetIV = true; 6025 byteRangeStart = start - 16; 6026 } 6027 } 6028 loaderContext.rangeStart = byteRangeStart; 6029 loaderContext.rangeEnd = byteRangeEnd; 6030 } 6031 return loaderContext; 6032} 6033function createGapLoadError(frag, part) { 6034 const error = new Error(`GAP ${frag.gap ? 'tag' : 'attribute'} found`); 6035 const errorData = { 6036 type: ErrorTypes.MEDIA_ERROR, 6037 details: ErrorDetails.FRAG_GAP, 6038 fatal: false, 6039 frag, 6040 error, 6041 networkDetails: null 6042 }; 6043 if (part) { 6044 errorData.part = part; 6045 } 6046 (part ? part : frag).stats.aborted = true; 6047 return new LoadError(errorData); 6048} 6049function isMethodFullSegmentAesCbc(method) { 6050 return method === 'AES-128' || method === 'AES-256'; 6051} 6052class LoadError extends Error { 6053 constructor(data) { 6054 super(data.error.message); 6055 this.data = void 0; 6056 this.data = data; 6057 } 6058} 6059 6060/** 6061 * @ignore 6062 * Sub-class specialization of EventHandler base class. 6063 * 6064 * TaskLoop allows to schedule a task function being called (optionnaly repeatedly) on the main loop, 6065 * scheduled asynchroneously, avoiding recursive calls in the same tick. 6066 * 6067 * The task itself is implemented in `doTick`. It can be requested and called for single execution 6068 * using the `tick` method. 6069 * 6070 * It will be assured that the task execution method (`tick`) only gets called once per main loop "tick", 6071 * no matter how often it gets requested for execution. Execution in further ticks will be scheduled accordingly. 6072 * 6073 * If further execution requests have already been scheduled on the next tick, it can be checked with `hasNextTick`, 6074 * and cancelled with `clearNextTick`. 6075 * 6076 * The task can be scheduled as an interval repeatedly with a period as parameter (see `setInterval`, `clearInterval`). 6077 * 6078 * Sub-classes need to implement the `doTick` method which will effectively have the task execution routine. 6079 * 6080 * Further explanations: 6081 * 6082 * The baseclass has a `tick` method that will schedule the doTick call. It may be called synchroneously 6083 * only for a stack-depth of one. On re-entrant calls, sub-sequent calls are scheduled for next main loop ticks. 6084 * 6085 * When the task execution (`tick` method) is called in re-entrant way this is detected and 6086 * we are limiting the task execution per call stack to exactly one, but scheduling/post-poning further 6087 * task processing on the next main loop iteration (also known as "next tick" in the Node/JS runtime lingo). 6088 */ 6089class TaskLoop extends Logger { 6090 constructor(label, logger) { 6091 super(label, logger); 6092 this._boundTick = void 0; 6093 this._tickTimer = null; 6094 this._tickInterval = null; 6095 this._tickCallCount = 0; 6096 this._boundTick = this.tick.bind(this); 6097 } 6098 destroy() { 6099 this.onHandlerDestroying(); 6100 this.onHandlerDestroyed(); 6101 } 6102 onHandlerDestroying() { 6103 // clear all timers before unregistering from event bus 6104 this.clearNextTick(); 6105 this.clearInterval(); 6106 } 6107 onHandlerDestroyed() {} 6108 hasInterval() { 6109 return !!this._tickInterval; 6110 } 6111 hasNextTick() { 6112 return !!this._tickTimer; 6113 } 6114 6115 /** 6116 * @param millis - Interval time (ms) 6117 * @eturns True when interval has been scheduled, false when already scheduled (no effect) 6118 */ 6119 setInterval(millis) { 6120 if (!this._tickInterval) { 6121 this._tickCallCount = 0; 6122 this._tickInterval = self.setInterval(this._boundTick, millis); 6123 return true; 6124 } 6125 return false; 6126 } 6127 6128 /** 6129 * @returns True when interval was cleared, false when none was set (no effect) 6130 */ 6131 clearInterval() { 6132 if (this._tickInterval) { 6133 self.clearInterval(this._tickInterval); 6134 this._tickInterval = null; 6135 return true; 6136 } 6137 return false; 6138 } 6139 6140 /** 6141 * @returns True when timeout was cleared, false when none was set (no effect) 6142 */ 6143 clearNextTick() { 6144 if (this._tickTimer) { 6145 self.clearTimeout(this._tickTimer); 6146 this._tickTimer = null; 6147 return true; 6148 } 6149 return false; 6150 } 6151 6152 /** 6153 * Will call the subclass doTick implementation in this main loop tick 6154 * or in the next one (via setTimeout(,0)) in case it has already been called 6155 * in this tick (in case this is a re-entrant call). 6156 */ 6157 tick() { 6158 this._tickCallCount++; 6159 if (this._tickCallCount === 1) { 6160 this.doTick(); 6161 // re-entrant call to tick from previous doTick call stack 6162 // -> schedule a call on the next main loop iteration to process this task processing request 6163 if (this._tickCallCount > 1) { 6164 // make sure only one timer exists at any time at max 6165 this.tickImmediate(); 6166 } 6167 this._tickCallCount = 0; 6168 } 6169 } 6170 tickImmediate() { 6171 this.clearNextTick(); 6172 this._tickTimer = self.setTimeout(this._boundTick, 0); 6173 } 6174 6175 /** 6176 * For subclass to implement task logic 6177 * @abstract 6178 */ 6179 doTick() {} 6180} 6181 6182class ChunkMetadata { 6183 constructor(level, sn, id, size = 0, part = -1, partial = false) { 6184 this.level = void 0; 6185 this.sn = void 0; 6186 this.part = void 0; 6187 this.id = void 0; 6188 this.size = void 0; 6189 this.partial = void 0; 6190 this.transmuxing = getNewPerformanceTiming(); 6191 this.buffering = { 6192 audio: getNewPerformanceTiming(), 6193 video: getNewPerformanceTiming(), 6194 audiovideo: getNewPerformanceTiming() 6195 }; 6196 this.level = level; 6197 this.sn = sn; 6198 this.id = id; 6199 this.size = size; 6200 this.part = part; 6201 this.partial = partial; 6202 } 6203} 6204function getNewPerformanceTiming() { 6205 return { 6206 start: 0, 6207 executeStart: 0, 6208 executeEnd: 0, 6209 end: 0 6210 }; 6211} 6212 6213/** 6214 * Provides methods dealing with buffer length retrieval for example. 6215 * 6216 * In general, a helper around HTML5 MediaElement TimeRanges gathered from `buffered` property. 6217 * 6218 * Also @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/buffered 6219 */ 6220 6221const noopBuffered = { 6222 length: 0, 6223 start: () => 0, 6224 end: () => 0 6225}; 6226class BufferHelper { 6227 /** 6228 * Return true if `media`'s buffered include `position` 6229 */ 6230 static isBuffered(media, position) { 6231 if (media) { 6232 const buffered = BufferHelper.getBuffered(media); 6233 for (let i = buffered.length; i--;) { 6234 if (position >= buffered.start(i) && position <= buffered.end(i)) { 6235 return true; 6236 } 6237 } 6238 } 6239 return false; 6240 } 6241 static bufferedRanges(media) { 6242 if (media) { 6243 const timeRanges = BufferHelper.getBuffered(media); 6244 return BufferHelper.timeRangesToArray(timeRanges); 6245 } 6246 return []; 6247 } 6248 static timeRangesToArray(timeRanges) { 6249 const buffered = []; 6250 for (let i = 0; i < timeRanges.length; i++) { 6251 buffered.push({ 6252 start: timeRanges.start(i), 6253 end: timeRanges.end(i) 6254 }); 6255 } 6256 return buffered; 6257 } 6258 static bufferInfo(media, pos, maxHoleDuration) { 6259 if (media) { 6260 const buffered = BufferHelper.bufferedRanges(media); 6261 if (buffered.length) { 6262 return BufferHelper.bufferedInfo(buffered, pos, maxHoleDuration); 6263 } 6264 } 6265 return { 6266 len: 0, 6267 start: pos, 6268 end: pos, 6269 bufferedIndex: -1 6270 }; 6271 } 6272 static bufferedInfo(buffered, pos, maxHoleDuration) { 6273 pos = Math.max(0, pos); 6274 // sort on buffer.start/smaller end (IE does not always return sorted buffered range) 6275 if (buffered.length > 1) { 6276 buffered.sort((a, b) => a.start - b.start || b.end - a.end); 6277 } 6278 let bufferedIndex = -1; 6279 let buffered2 = []; 6280 if (maxHoleDuration) { 6281 // there might be some small holes between buffer time range 6282 // consider that holes smaller than maxHoleDuration are irrelevant and build another 6283 // buffer time range representations that discards those holes 6284 for (let i = 0; i < buffered.length; i++) { 6285 if (pos >= buffered[i].start && pos <= buffered[i].end) { 6286 bufferedIndex = i; 6287 } 6288 const buf2len = buffered2.length; 6289 if (buf2len) { 6290 const buf2end = buffered2[buf2len - 1].end; 6291 // if small hole (value between 0 or maxHoleDuration ) or overlapping (negative) 6292 if (buffered[i].start - buf2end < maxHoleDuration) { 6293 // merge overlapping time ranges 6294 // update lastRange.end only if smaller than item.end 6295 // e.g. [ 1, 15] with [ 2,8] => [ 1,15] (no need to modify lastRange.end) 6296 // whereas [ 1, 8] with [ 2,15] => [ 1,15] ( lastRange should switch from [1,8] to [1,15]) 6297 if (buffered[i].end > buf2end) { 6298 buffered2[buf2len - 1].end = buffered[i].end; 6299 } 6300 } else { 6301 // big hole 6302 buffered2.push(buffered[i]); 6303 } 6304 } else { 6305 // first value 6306 buffered2.push(buffered[i]); 6307 } 6308 } 6309 } else { 6310 buffered2 = buffered; 6311 } 6312 let bufferLen = 0; 6313 let nextStart; 6314 6315 // bufferStart and bufferEnd are buffer boundaries around current playback position (pos) 6316 let bufferStart = pos; 6317 let bufferEnd = pos; 6318 for (let i = 0; i < buffered2.length; i++) { 6319 const start = buffered2[i].start; 6320 const end = buffered2[i].end; 6321 // logger.log('buf start/end:' + buffered.start(i) + '/' + buffered.end(i)); 6322 if (bufferedIndex === -1 && pos >= start && pos <= end) { 6323 bufferedIndex = i; 6324 } 6325 if (pos + maxHoleDuration >= start && pos < end) { 6326 // play position is inside this buffer TimeRange, retrieve end of buffer position and buffer length 6327 bufferStart = start; 6328 bufferEnd = end; 6329 bufferLen = bufferEnd - pos; 6330 } else if (pos + maxHoleDuration < start) { 6331 nextStart = start; 6332 break; 6333 } 6334 } 6335 return { 6336 len: bufferLen, 6337 start: bufferStart || 0, 6338 end: bufferEnd || 0, 6339 nextStart, 6340 buffered, 6341 bufferedIndex 6342 }; 6343 } 6344 6345 /** 6346 * Safe method to get buffered property. 6347 * SourceBuffer.buffered may throw if SourceBuffer is removed from it's MediaSource 6348 */ 6349 static getBuffered(media) { 6350 try { 6351 return media.buffered || noopBuffered; 6352 } catch (e) { 6353 logger.log('failed to get media.buffered', e); 6354 return noopBuffered; 6355 } 6356 } 6357} 6358 6359const VARIABLE_REPLACEMENT_REGEX = /\{\$([a-zA-Z0-9-_]+)\}/g; 6360function hasVariableReferences(str) { 6361 return VARIABLE_REPLACEMENT_REGEX.test(str); 6362} 6363function substituteVariables(parsed, value) { 6364 if (parsed.variableList !== null || parsed.hasVariableRefs) { 6365 const variableList = parsed.variableList; 6366 return value.replace(VARIABLE_REPLACEMENT_REGEX, variableReference => { 6367 const variableName = variableReference.substring(2, variableReference.length - 1); 6368 const variableValue = variableList == null ? void 0 : variableList[variableName]; 6369 if (variableValue === undefined) { 6370 parsed.playlistParsingError || (parsed.playlistParsingError = new Error(`Missing preceding EXT-X-DEFINE tag for Variable Reference: "${variableName}"`)); 6371 return variableReference; 6372 } 6373 return variableValue; 6374 }); 6375 } 6376 return value; 6377} 6378function addVariableDefinition(parsed, attr, parentUrl) { 6379 let variableList = parsed.variableList; 6380 if (!variableList) { 6381 parsed.variableList = variableList = {}; 6382 } 6383 let NAME; 6384 let VALUE; 6385 if ('QUERYPARAM' in attr) { 6386 NAME = attr.QUERYPARAM; 6387 try { 6388 const searchParams = new self.URL(parentUrl).searchParams; 6389 if (searchParams.has(NAME)) { 6390 VALUE = searchParams.get(NAME); 6391 } else { 6392 throw new Error(`"${NAME}" does not match any query parameter in URI: "${parentUrl}"`); 6393 } 6394 } catch (error) { 6395 parsed.playlistParsingError || (parsed.playlistParsingError = new Error(`EXT-X-DEFINE QUERYPARAM: ${error.message}`)); 6396 } 6397 } else { 6398 NAME = attr.NAME; 6399 VALUE = attr.VALUE; 6400 } 6401 if (NAME in variableList) { 6402 parsed.playlistParsingError || (parsed.playlistParsingError = new Error(`EXT-X-DEFINE duplicate Variable Name declarations: "${NAME}"`)); 6403 } else { 6404 variableList[NAME] = VALUE || ''; 6405 } 6406} 6407function importVariableDefinition(parsed, attr, sourceVariableList) { 6408 const IMPORT = attr.IMPORT; 6409 if (sourceVariableList && IMPORT in sourceVariableList) { 6410 let variableList = parsed.variableList; 6411 if (!variableList) { 6412 parsed.variableList = variableList = {}; 6413 } 6414 variableList[IMPORT] = sourceVariableList[IMPORT]; 6415 } else { 6416 parsed.playlistParsingError || (parsed.playlistParsingError = new Error(`EXT-X-DEFINE IMPORT attribute not found in Multivariant Playlist: "${IMPORT}"`)); 6417 } 6418} 6419 6420const DECIMAL_RESOLUTION_REGEX = /^(\d+)x(\d+)$/; 6421const ATTR_LIST_REGEX = /(.+?)=(".*?"|.*?)(?:,|$)/g; 6422 6423// adapted from https://github.com/kanongil/node-m3u8parse/blob/master/attrlist.js 6424class AttrList { 6425 constructor(attrs, parsed) { 6426 if (typeof attrs === 'string') { 6427 attrs = AttrList.parseAttrList(attrs, parsed); 6428 } 6429 _extends(this, attrs); 6430 } 6431 get clientAttrs() { 6432 return Object.keys(this).filter(attr => attr.substring(0, 2) === 'X-'); 6433 } 6434 decimalInteger(attrName) { 6435 const intValue = parseInt(this[attrName], 10); 6436 if (intValue > Number.MAX_SAFE_INTEGER) { 6437 return Infinity; 6438 } 6439 return intValue; 6440 } 6441 hexadecimalInteger(attrName) { 6442 if (this[attrName]) { 6443 let stringValue = (this[attrName] || '0x').slice(2); 6444 stringValue = (stringValue.length & 1 ? '0' : '') + stringValue; 6445 const value = new Uint8Array(stringValue.length / 2); 6446 for (let i = 0; i < stringValue.length / 2; i++) { 6447 value[i] = parseInt(stringValue.slice(i * 2, i * 2 + 2), 16); 6448 } 6449 return value; 6450 } 6451 return null; 6452 } 6453 hexadecimalIntegerAsNumber(attrName) { 6454 const intValue = parseInt(this[attrName], 16); 6455 if (intValue > Number.MAX_SAFE_INTEGER) { 6456 return Infinity; 6457 } 6458 return intValue; 6459 } 6460 decimalFloatingPoint(attrName) { 6461 return parseFloat(this[attrName]); 6462 } 6463 optionalFloat(attrName, defaultValue) { 6464 const value = this[attrName]; 6465 return value ? parseFloat(value) : defaultValue; 6466 } 6467 enumeratedString(attrName) { 6468 return this[attrName]; 6469 } 6470 enumeratedStringList(attrName, dict) { 6471 const attrValue = this[attrName]; 6472 return (attrValue ? attrValue.split(/[ ,]+/) : []).reduce((result, identifier) => { 6473 result[identifier.toLowerCase()] = true; 6474 return result; 6475 }, dict); 6476 } 6477 bool(attrName) { 6478 return this[attrName] === 'YES'; 6479 } 6480 decimalResolution(attrName) { 6481 const res = DECIMAL_RESOLUTION_REGEX.exec(this[attrName]); 6482 if (res === null) { 6483 return undefined; 6484 } 6485 return { 6486 width: parseInt(res[1], 10), 6487 height: parseInt(res[2], 10) 6488 }; 6489 } 6490 static parseAttrList(input, parsed) { 6491 let match; 6492 const attrs = {}; 6493 const quote = '"'; 6494 ATTR_LIST_REGEX.lastIndex = 0; 6495 while ((match = ATTR_LIST_REGEX.exec(input)) !== null) { 6496 const name = match[1].trim(); 6497 let value = match[2]; 6498 const quotedString = value.indexOf(quote) === 0 && value.lastIndexOf(quote) === value.length - 1; 6499 let hexadecimalSequence = false; 6500 if (quotedString) { 6501 value = value.slice(1, -1); 6502 } else { 6503 switch (name) { 6504 case 'IV': 6505 case 'SCTE35-CMD': 6506 case 'SCTE35-IN': 6507 case 'SCTE35-OUT': 6508 hexadecimalSequence = true; 6509 } 6510 } 6511 if (parsed && (quotedString || hexadecimalSequence)) { 6512 { 6513 value = substituteVariables(parsed, value); 6514 } 6515 } else if (!hexadecimalSequence && !quotedString) { 6516 switch (name) { 6517 case 'CLOSED-CAPTIONS': 6518 if (value === 'NONE') { 6519 break; 6520 } 6521 // falls through 6522 case 'ALLOWED-CPC': 6523 case 'CLASS': 6524 case 'ASSOC-LANGUAGE': 6525 case 'AUDIO': 6526 case 'BYTERANGE': 6527 case 'CHANNELS': 6528 case 'CHARACTERISTICS': 6529 case 'CODECS': 6530 case 'DATA-ID': 6531 case 'END-DATE': 6532 case 'GROUP-ID': 6533 case 'ID': 6534 case 'IMPORT': 6535 case 'INSTREAM-ID': 6536 case 'KEYFORMAT': 6537 case 'KEYFORMATVERSIONS': 6538 case 'LANGUAGE': 6539 case 'NAME': 6540 case 'PATHWAY-ID': 6541 case 'QUERYPARAM': 6542 case 'RECENTLY-REMOVED-DATERANGES': 6543 case 'SERVER-URI': 6544 case 'STABLE-RENDITION-ID': 6545 case 'STABLE-VARIANT-ID': 6546 case 'START-DATE': 6547 case 'SUBTITLES': 6548 case 'SUPPLEMENTAL-CODECS': 6549 case 'URI': 6550 case 'VALUE': 6551 case 'VIDEO': 6552 case 'X-ASSET-LIST': 6553 case 'X-ASSET-URI': 6554 // Since we are not checking tag:attribute combination, just warn rather than ignoring attribute 6555 logger.warn(`${input}: attribute ${name} is missing quotes`); 6556 // continue; 6557 } 6558 } 6559 attrs[name] = value; 6560 } 6561 return attrs; 6562 } 6563} 6564 6565// Avoid exporting const enum so that these values can be inlined 6566 6567const CLASS_INTERSTITIAL = 'com.apple.hls.interstitial'; 6568function isDateRangeCueAttribute(attrName) { 6569 return attrName !== "ID" && attrName !== "CLASS" && attrName !== "CUE" && attrName !== "START-DATE" && attrName !== "DURATION" && attrName !== "END-DATE" && attrName !== "END-ON-NEXT"; 6570} 6571function isSCTE35Attribute(attrName) { 6572 return attrName === "SCTE35-OUT" || attrName === "SCTE35-IN" || attrName === "SCTE35-CMD"; 6573} 6574class DateRange { 6575 constructor(dateRangeAttr, dateRangeWithSameId, tagCount = 0) { 6576 var _dateRangeWithSameId$; 6577 this.attr = void 0; 6578 this.tagAnchor = void 0; 6579 this.tagOrder = void 0; 6580 this._startDate = void 0; 6581 this._endDate = void 0; 6582 this._dateAtEnd = void 0; 6583 this._cue = void 0; 6584 this._badValueForSameId = void 0; 6585 this.tagAnchor = (dateRangeWithSameId == null ? void 0 : dateRangeWithSameId.tagAnchor) || null; 6586 this.tagOrder = (_dateRangeWithSameId$ = dateRangeWithSameId == null ? void 0 : dateRangeWithSameId.tagOrder) != null ? _dateRangeWithSameId$ : tagCount; 6587 if (dateRangeWithSameId) { 6588 const previousAttr = dateRangeWithSameId.attr; 6589 for (const key in previousAttr) { 6590 if (Object.prototype.hasOwnProperty.call(dateRangeAttr, key) && dateRangeAttr[key] !== previousAttr[key]) { 6591 logger.warn(`DATERANGE tag attribute: "${key}" does not match for tags with ID: "${dateRangeAttr.ID}"`); 6592 this._badValueForSameId = key; 6593 break; 6594 } 6595 } 6596 // Merge DateRange tags with the same ID 6597 dateRangeAttr = _extends(new AttrList({}), previousAttr, dateRangeAttr); 6598 } 6599 this.attr = dateRangeAttr; 6600 if (dateRangeWithSameId) { 6601 this._startDate = dateRangeWithSameId._startDate; 6602 this._cue = dateRangeWithSameId._cue; 6603 this._endDate = dateRangeWithSameId._endDate; 6604 this._dateAtEnd = dateRangeWithSameId._dateAtEnd; 6605 } else { 6606 this._startDate = new Date(dateRangeAttr["START-DATE"]); 6607 } 6608 if ("END-DATE" in this.attr) { 6609 const endDate = (dateRangeWithSameId == null ? void 0 : dateRangeWithSameId.endDate) || new Date(this.attr["END-DATE"]); 6610 if (isFiniteNumber(endDate.getTime())) { 6611 this._endDate = endDate; 6612 } 6613 } 6614 } 6615 get id() { 6616 return this.attr.ID; 6617 } 6618 get class() { 6619 return this.attr.CLASS; 6620 } 6621 get cue() { 6622 const _cue = this._cue; 6623 if (_cue === undefined) { 6624 return this._cue = this.attr.enumeratedStringList(this.attr.CUE ? 'CUE' : 'X-CUE', { 6625 pre: false, 6626 post: false, 6627 once: false 6628 }); 6629 } 6630 return _cue; 6631 } 6632 get startTime() { 6633 const { 6634 tagAnchor 6635 } = this; 6636 // eslint-disable-next-line @typescript-eslint/prefer-optional-chain 6637 if (tagAnchor === null || tagAnchor.programDateTime === null) { 6638 logger.warn(`Expected tagAnchor Fragment with PDT set for DateRange "${this.id}": ${tagAnchor}`); 6639 return NaN; 6640 } 6641 return tagAnchor.start + (this.startDate.getTime() - tagAnchor.programDateTime) / 1000; 6642 } 6643 get startDate() { 6644 return this._startDate; 6645 } 6646 get endDate() { 6647 const dateAtEnd = this._endDate || this._dateAtEnd; 6648 if (dateAtEnd) { 6649 return dateAtEnd; 6650 } 6651 const duration = this.duration; 6652 if (duration !== null) { 6653 return this._dateAtEnd = new Date(this._startDate.getTime() + duration * 1000); 6654 } 6655 return null; 6656 } 6657 get duration() { 6658 if ("DURATION" in this.attr) { 6659 const duration = this.attr.decimalFloatingPoint("DURATION"); 6660 if (isFiniteNumber(duration)) { 6661 return duration; 6662 } 6663 } else if (this._endDate) { 6664 return (this._endDate.getTime() - this._startDate.getTime()) / 1000; 6665 } 6666 return null; 6667 } 6668 get plannedDuration() { 6669 if ("PLANNED-DURATION" in this.attr) { 6670 return this.attr.decimalFloatingPoint("PLANNED-DURATION"); 6671 } 6672 return null; 6673 } 6674 get endOnNext() { 6675 return this.attr.bool("END-ON-NEXT"); 6676 } 6677 get isInterstitial() { 6678 return this.class === CLASS_INTERSTITIAL; 6679 } 6680 get isValid() { 6681 return !!this.id && !this._badValueForSameId && isFiniteNumber(this.startDate.getTime()) && (this.duration === null || this.duration >= 0) && (!this.endOnNext || !!this.class) && (!this.attr.CUE || !this.cue.pre && !this.cue.post || this.cue.pre !== this.cue.post) && (!this.isInterstitial || 'X-ASSET-URI' in this.attr || 'X-ASSET-LIST' in this.attr); 6682 } 6683} 6684 6685const DEFAULT_TARGET_DURATION = 10; 6686 6687/** 6688 * Object representing parsed data from an HLS Media Playlist. Found in {@link hls.js#Level.details}. 6689 */ 6690class LevelDetails { 6691 constructor(baseUrl) { 6692 this.PTSKnown = false; 6693 this.alignedSliding = false; 6694 this.averagetargetduration = void 0; 6695 this.endCC = 0; 6696 this.endSN = 0; 6697 this.fragments = void 0; 6698 this.fragmentHint = void 0; 6699 this.partList = null; 6700 this.dateRanges = void 0; 6701 this.dateRangeTagCount = 0; 6702 this.live = true; 6703 this.requestScheduled = -1; 6704 this.ageHeader = 0; 6705 this.advancedDateTime = void 0; 6706 this.updated = true; 6707 this.advanced = true; 6708 this.misses = 0; 6709 this.startCC = 0; 6710 this.startSN = 0; 6711 this.startTimeOffset = null; 6712 this.targetduration = 0; 6713 this.totalduration = 0; 6714 this.type = null; 6715 this.url = void 0; 6716 this.m3u8 = ''; 6717 this.version = null; 6718 this.canBlockReload = false; 6719 this.canSkipUntil = 0; 6720 this.canSkipDateRanges = false; 6721 this.skippedSegments = 0; 6722 this.recentlyRemovedDateranges = void 0; 6723 this.partHoldBack = 0; 6724 this.holdBack = 0; 6725 this.partTarget = 0; 6726 this.preloadHint = void 0; 6727 this.renditionReports = void 0; 6728 this.tuneInGoal = 0; 6729 this.deltaUpdateFailed = void 0; 6730 this.driftStartTime = 0; 6731 this.driftEndTime = 0; 6732 this.driftStart = 0; 6733 this.driftEnd = 0; 6734 this.encryptedFragments = void 0; 6735 this.playlistParsingError = null; 6736 this.variableList = null; 6737 this.hasVariableRefs = false; 6738 this.appliedTimelineOffset = void 0; 6739 this.fragments = []; 6740 this.encryptedFragments = []; 6741 this.dateRanges = {}; 6742 this.url = baseUrl; 6743 } 6744 reloaded(previous) { 6745 if (!previous) { 6746 this.advanced = true; 6747 this.updated = true; 6748 return; 6749 } 6750 const partSnDiff = this.lastPartSn - previous.lastPartSn; 6751 const partIndexDiff = this.lastPartIndex - previous.lastPartIndex; 6752 this.updated = this.endSN !== previous.endSN || !!partIndexDiff || !!partSnDiff || !this.live; 6753 this.advanced = this.endSN > previous.endSN || partSnDiff > 0 || partSnDiff === 0 && partIndexDiff > 0; 6754 if (this.updated || this.advanced) { 6755 this.misses = Math.floor(previous.misses * 0.6); 6756 } else { 6757 this.misses = previous.misses + 1; 6758 } 6759 } 6760 hasKey(levelKey) { 6761 return this.encryptedFragments.some(frag => { 6762 let decryptdata = frag.decryptdata; 6763 if (!decryptdata) { 6764 frag.setKeyFormat(levelKey.keyFormat); 6765 decryptdata = frag.decryptdata; 6766 } 6767 return !!decryptdata && levelKey.matches(decryptdata); 6768 }); 6769 } 6770 get hasProgramDateTime() { 6771 if (this.fragments.length) { 6772 return isFiniteNumber(this.fragments[this.fragments.length - 1].programDateTime); 6773 } 6774 return false; 6775 } 6776 get levelTargetDuration() { 6777 return this.averagetargetduration || this.targetduration || DEFAULT_TARGET_DURATION; 6778 } 6779 get drift() { 6780 const runTime = this.driftEndTime - this.driftStartTime; 6781 if (runTime > 0) { 6782 const runDuration = this.driftEnd - this.driftStart; 6783 return runDuration * 1000 / runTime; 6784 } 6785 return 1; 6786 } 6787 get edge() { 6788 return this.partEnd || this.fragmentEnd; 6789 } 6790 get partEnd() { 6791 var _this$partList; 6792 if ((_this$partList = this.partList) != null && _this$partList.length) { 6793 return this.partList[this.partList.length - 1].end; 6794 } 6795 return this.fragmentEnd; 6796 } 6797 get fragmentEnd() { 6798 if (this.fragments.length) { 6799 return this.fragments[this.fragments.length - 1].end; 6800 } 6801 return 0; 6802 } 6803 get fragmentStart() { 6804 if (this.fragments.length) { 6805 return this.fragments[0].start; 6806 } 6807 return 0; 6808 } 6809 get age() { 6810 if (this.advancedDateTime) { 6811 return Math.max(Date.now() - this.advancedDateTime, 0) / 1000; 6812 } 6813 return 0; 6814 } 6815 get lastPartIndex() { 6816 var _this$partList2; 6817 if ((_this$partList2 = this.partList) != null && _this$partList2.length) { 6818 return this.partList[this.partList.length - 1].index; 6819 } 6820 return -1; 6821 } 6822 get maxPartIndex() { 6823 const partList = this.partList; 6824 if (partList) { 6825 const lastIndex = this.lastPartIndex; 6826 if (lastIndex !== -1) { 6827 for (let i = partList.length; i--;) { 6828 if (partList[i].index > lastIndex) { 6829 return partList[i].index; 6830 } 6831 } 6832 return lastIndex; 6833 } 6834 } 6835 return 0; 6836 } 6837 get lastPartSn() { 6838 var _this$partList3; 6839 if ((_this$partList3 = this.partList) != null && _this$partList3.length) { 6840 return this.partList[this.partList.length - 1].fragment.sn; 6841 } 6842 return this.endSN; 6843 } 6844 get expired() { 6845 if (this.live && this.age && this.misses < 3) { 6846 const playlistWindowDuration = this.partEnd - this.fragmentStart; 6847 return this.age > Math.max(playlistWindowDuration, this.totalduration) + this.levelTargetDuration; 6848 } 6849 return false; 6850 } 6851} 6852 6853function arrayValuesMatch(a, b) { 6854 if (a.length === b.length) { 6855 return !a.some((value, i) => value !== b[i]); 6856 } 6857 return false; 6858} 6859function optionalArrayValuesMatch(a, b) { 6860 if (!a && !b) { 6861 return true; 6862 } 6863 if (!a || !b) { 6864 return false; 6865 } 6866 return arrayValuesMatch(a, b); 6867} 6868 6869function isFullSegmentEncryption(method) { 6870 return method === 'AES-128' || method === 'AES-256' || method === 'AES-256-CTR'; 6871} 6872function getAesModeFromFullSegmentMethod(method) { 6873 switch (method) { 6874 case 'AES-128': 6875 case 'AES-256': 6876 return DecrypterAesMode.cbc; 6877 case 'AES-256-CTR': 6878 return DecrypterAesMode.ctr; 6879 default: 6880 throw new Error(`invalid full segment method ${method}`); 6881 } 6882} 6883 6884function base64Decode(base64encodedStr) { 6885 return Uint8Array.from(atob(base64encodedStr), c => c.charCodeAt(0)); 6886} 6887 6888// breaking up those two types in order to clarify what is happening in the decoding path. 6889 6890// http://stackoverflow.com/questions/8936984/uint8array-to-string-in-javascript/22373197 6891// http://www.onicos.com/staff/iz/amuse/javascript/expert/utf.txt 6892/* utf.js - UTF-8 <=> UTF-16 convertion 6893 * 6894 * Copyright (C) 1999 Masanao Izumo <[email protected]> 6895 * Version: 1.0 6896 * LastModified: Dec 25 1999 6897 * This library is free. You can redistribute it and/or modify it. 6898 */ 6899 6900function strToUtf8array(str) { 6901 return Uint8Array.from(unescape(encodeURIComponent(str)), c => c.charCodeAt(0)); 6902} 6903 6904function getKeyIdBytes(str) { 6905 const keyIdbytes = strToUtf8array(str).subarray(0, 16); 6906 const paddedkeyIdbytes = new Uint8Array(16); 6907 paddedkeyIdbytes.set(keyIdbytes, 16 - keyIdbytes.length); 6908 return paddedkeyIdbytes; 6909} 6910function changeEndianness(keyId) { 6911 const swap = function swap(array, from, to) { 6912 const cur = array[from]; 6913 array[from] = array[to]; 6914 array[to] = cur; 6915 }; 6916 swap(keyId, 0, 3); 6917 swap(keyId, 1, 2); 6918 swap(keyId, 4, 5); 6919 swap(keyId, 6, 7); 6920} 6921function convertDataUriToArrayBytes(uri) { 6922 // data:[<media type][;attribute=value][;base64],<data> 6923 const colonsplit = uri.split(':'); 6924 let keydata = null; 6925 if (colonsplit[0] === 'data' && colonsplit.length === 2) { 6926 const semicolonsplit = colonsplit[1].split(';'); 6927 const commasplit = semicolonsplit[semicolonsplit.length - 1].split(','); 6928 if (commasplit.length === 2) { 6929 const isbase64 = commasplit[0] === 'base64'; 6930 const data = commasplit[1]; 6931 if (isbase64) { 6932 semicolonsplit.splice(-1, 1); // remove from processing 6933 keydata = base64Decode(data); 6934 } else { 6935 keydata = getKeyIdBytes(data); 6936 } 6937 } 6938 } 6939 return keydata; 6940} 6941 6942/** returns `undefined` is `self` is missing, e.g. in node */ 6943const optionalSelf = typeof self !== 'undefined' ? self : undefined; 6944 6945/** 6946 * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/requestMediaKeySystemAccess 6947 */ 6948var KeySystems = { 6949 CLEARKEY: "org.w3.clearkey", 6950 FAIRPLAY: "com.apple.fps", 6951 PLAYREADY: "com.microsoft.playready", 6952 WIDEVINE: "com.widevine.alpha" 6953}; 6954 6955// Playlist #EXT-X-KEY KEYFORMAT values 6956var KeySystemFormats = { 6957 CLEARKEY: "org.w3.clearkey", 6958 FAIRPLAY: "com.apple.streamingkeydelivery", 6959 PLAYREADY: "com.microsoft.playready", 6960 WIDEVINE: "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed" 6961}; 6962function keySystemFormatToKeySystemDomain(format) { 6963 switch (format) { 6964 case KeySystemFormats.FAIRPLAY: 6965 return KeySystems.FAIRPLAY; 6966 case KeySystemFormats.PLAYREADY: 6967 return KeySystems.PLAYREADY; 6968 case KeySystemFormats.WIDEVINE: 6969 return KeySystems.WIDEVINE; 6970 case KeySystemFormats.CLEARKEY: 6971 return KeySystems.CLEARKEY; 6972 } 6973} 6974function keySystemDomainToKeySystemFormat(keySystem) { 6975 switch (keySystem) { 6976 case KeySystems.FAIRPLAY: 6977 return KeySystemFormats.FAIRPLAY; 6978 case KeySystems.PLAYREADY: 6979 return KeySystemFormats.PLAYREADY; 6980 case KeySystems.WIDEVINE: 6981 return KeySystemFormats.WIDEVINE; 6982 case KeySystems.CLEARKEY: 6983 return KeySystemFormats.CLEARKEY; 6984 } 6985} 6986function getKeySystemsForConfig(config) { 6987 const { 6988 drmSystems, 6989 widevineLicenseUrl 6990 } = config; 6991 const keySystemsToAttempt = drmSystems ? [KeySystems.FAIRPLAY, KeySystems.WIDEVINE, KeySystems.PLAYREADY, KeySystems.CLEARKEY].filter(keySystem => !!drmSystems[keySystem]) : []; 6992 if (!keySystemsToAttempt[KeySystems.WIDEVINE] && widevineLicenseUrl) { 6993 keySystemsToAttempt.push(KeySystems.WIDEVINE); 6994 } 6995 return keySystemsToAttempt; 6996} 6997const requestMediaKeySystemAccess = function (_optionalSelf$navigat) { 6998 if (optionalSelf != null && (_optionalSelf$navigat = optionalSelf.navigator) != null && _optionalSelf$navigat.requestMediaKeySystemAccess) { 6999 return self.navigator.requestMediaKeySystemAccess.bind(self.navigator); 7000 } else { 7001 return null; 7002 } 7003}(); 7004 7005/** 7006 * @see https://developer.mozilla.org/en-US/docs/Web/API/MediaKeySystemConfiguration 7007 */ 7008function getSupportedMediaKeySystemConfigurations(keySystem, audioCodecs, videoCodecs, drmSystemOptions) { 7009 let initDataTypes; 7010 switch (keySystem) { 7011 case KeySystems.FAIRPLAY: 7012 initDataTypes = ['cenc', 'sinf']; 7013 break; 7014 case KeySystems.WIDEVINE: 7015 case KeySystems.PLAYREADY: 7016 initDataTypes = ['cenc']; 7017 break; 7018 case KeySystems.CLEARKEY: 7019 initDataTypes = ['cenc', 'keyids']; 7020 break; 7021 default: 7022 throw new Error(`Unknown key-system: ${keySystem}`); 7023 } 7024 return createMediaKeySystemConfigurations(initDataTypes, audioCodecs, videoCodecs, drmSystemOptions); 7025} 7026function createMediaKeySystemConfigurations(initDataTypes, audioCodecs, videoCodecs, drmSystemOptions) { 7027 const baseConfig = { 7028 initDataTypes: initDataTypes, 7029 persistentState: drmSystemOptions.persistentState || 'optional', 7030 distinctiveIdentifier: drmSystemOptions.distinctiveIdentifier || 'optional', 7031 sessionTypes: drmSystemOptions.sessionTypes || [drmSystemOptions.sessionType || 'temporary'], 7032 audioCapabilities: audioCodecs.map(codec => ({ 7033 contentType: `audio/mp4; codecs=${codec}`, 7034 robustness: drmSystemOptions.audioRobustness || '', 7035 encryptionScheme: drmSystemOptions.audioEncryptionScheme || null 7036 })), 7037 videoCapabilities: videoCodecs.map(codec => ({ 7038 contentType: `video/mp4; codecs=${codec}`, 7039 robustness: drmSystemOptions.videoRobustness || '', 7040 encryptionScheme: drmSystemOptions.videoEncryptionScheme || null 7041 })) 7042 }; 7043 return [baseConfig]; 7044} 7045function isPersistentSessionType(drmSystemOptions) { 7046 var _drmSystemOptions$ses; 7047 return !!drmSystemOptions && (drmSystemOptions.sessionType === 'persistent-license' || !!((_drmSystemOptions$ses = drmSystemOptions.sessionTypes) != null && _drmSystemOptions$ses.some(type => type === 'persistent-license'))); 7048} 7049function parsePlayReadyWRM(keyBytes) { 7050 const keyBytesUtf16 = new Uint16Array(keyBytes.buffer, keyBytes.byteOffset, keyBytes.byteLength / 2); 7051 const keyByteStr = String.fromCharCode.apply(null, Array.from(keyBytesUtf16)); 7052 7053 // Parse Playready WRMHeader XML 7054 const xmlKeyBytes = keyByteStr.substring(keyByteStr.indexOf('<'), keyByteStr.length); 7055 const parser = new DOMParser(); 7056 const xmlDoc = parser.parseFromString(xmlKeyBytes, 'text/xml'); 7057 const keyData = xmlDoc.getElementsByTagName('KID')[0]; 7058 if (keyData) { 7059 const keyId = keyData.childNodes[0] ? keyData.childNodes[0].nodeValue : keyData.getAttribute('VALUE'); 7060 if (keyId) { 7061 const keyIdArray = base64Decode(keyId).subarray(0, 16); 7062 // KID value in PRO is a base64-encoded little endian GUID interpretation of UUID 7063 // KID value in ‘tenc’ is a big endian UUID GUID interpretation of UUID 7064 changeEndianness(keyIdArray); 7065 return keyIdArray; 7066 } 7067 } 7068 return null; 7069} 7070 7071let keyUriToKeyIdMap = {}; 7072class LevelKey { 7073 static clearKeyUriToKeyIdMap() { 7074 keyUriToKeyIdMap = {}; 7075 } 7076 static setKeyIdForUri(uri, keyId) { 7077 keyUriToKeyIdMap[uri] = keyId; 7078 } 7079 static addKeyIdForUri(uri) { 7080 const val = Object.keys(keyUriToKeyIdMap).length % Number.MAX_SAFE_INTEGER; 7081 const keyId = new Uint8Array(16); 7082 const dv = new DataView(keyId.buffer, 12, 4); // Just set the last 4 bytes 7083 dv.setUint32(0, val); 7084 keyUriToKeyIdMap[uri] = keyId; 7085 return keyId; 7086 } 7087 constructor(method, uri, format, formatversions = [1], iv = null, keyId) { 7088 this.uri = void 0; 7089 this.method = void 0; 7090 this.keyFormat = void 0; 7091 this.keyFormatVersions = void 0; 7092 this.encrypted = void 0; 7093 this.isCommonEncryption = void 0; 7094 this.iv = null; 7095 this.key = null; 7096 this.keyId = null; 7097 this.pssh = null; 7098 this.method = method; 7099 this.uri = uri; 7100 this.keyFormat = format; 7101 this.keyFormatVersions = formatversions; 7102 this.iv = iv; 7103 this.encrypted = method ? method !== 'NONE' : false; 7104 this.isCommonEncryption = this.encrypted && !isFullSegmentEncryption(method); 7105 if (keyId != null && keyId.startsWith('0x')) { 7106 this.keyId = new Uint8Array(hexToArrayBuffer(keyId)); 7107 } 7108 } 7109 matches(key) { 7110 return key.uri === this.uri && key.method === this.method && key.encrypted === this.encrypted && key.keyFormat === this.keyFormat && arrayValuesMatch(key.keyFormatVersions, this.keyFormatVersions) && optionalArrayValuesMatch(key.iv, this.iv) && optionalArrayValuesMatch(key.keyId, this.keyId); 7111 } 7112 isSupported() { 7113 // If it's Segment encryption or No encryption, just select that key system 7114 if (this.method) { 7115 if (isFullSegmentEncryption(this.method) || this.method === 'NONE') { 7116 return true; 7117 } 7118 if (this.keyFormat === 'identity') { 7119 // Maintain support for clear SAMPLE-AES with MPEG-3 TS 7120 return this.method === 'SAMPLE-AES'; 7121 } else { 7122 switch (this.keyFormat) { 7123 case KeySystemFormats.FAIRPLAY: 7124 case KeySystemFormats.WIDEVINE: 7125 case KeySystemFormats.PLAYREADY: 7126 case KeySystemFormats.CLEARKEY: 7127 return ['SAMPLE-AES', 'SAMPLE-AES-CENC', 'SAMPLE-AES-CTR'].indexOf(this.method) !== -1; 7128 } 7129 } 7130 } 7131 return false; 7132 } 7133 getDecryptData(sn, levelKeys) { 7134 if (!this.encrypted || !this.uri) { 7135 return null; 7136 } 7137 if (isFullSegmentEncryption(this.method)) { 7138 let iv = this.iv; 7139 if (!iv) { 7140 if (typeof sn !== 'number') { 7141 // We are fetching decryption data for a initialization segment 7142 // If the segment was encrypted with AES-128/256 7143 // It must have an IV defined. We cannot substitute the Segment Number in. 7144 logger.warn(`missing IV for initialization segment with method="${this.method}" - compliance issue`); 7145 7146 // Explicitly set sn to resulting value from implicit conversions 'initSegment' values for IV generation. 7147 sn = 0; 7148 } 7149 iv = createInitializationVector(sn); 7150 } 7151 const decryptdata = new LevelKey(this.method, this.uri, 'identity', this.keyFormatVersions, iv); 7152 return decryptdata; 7153 } 7154 if (this.keyId) { 7155 // Handle case where key id is changed in KEY_LOADING event handler #7542#issuecomment-3305203929 7156 const assignedKeyId = keyUriToKeyIdMap[this.uri]; 7157 if (assignedKeyId && !arrayValuesMatch(this.keyId, assignedKeyId)) { 7158 LevelKey.setKeyIdForUri(this.uri, this.keyId); 7159 } 7160 if (this.pssh) { 7161 return this; 7162 } 7163 } 7164 7165 // Key bytes are signalled the KEYID attribute, typically only found on WideVine KEY tags 7166 // Initialize keyId if possible 7167 const keyBytes = convertDataUriToArrayBytes(this.uri); 7168 if (keyBytes) { 7169 switch (this.keyFormat) { 7170 case KeySystemFormats.WIDEVINE: 7171 // Setting `pssh` on this LevelKey/DecryptData allows HLS.js to generate a session using 7172 // the playlist-key before the "encrypted" event. (Comment out to only use "encrypted" path.) 7173 this.pssh = keyBytes; 7174 // In case of Widevine, if KEYID is not in the playlist, assume only two fields in the pssh KEY tag URI. 7175 if (!this.keyId) { 7176 const results = parseMultiPssh(keyBytes.buffer); 7177 if (results.length) { 7178 var _psshData$kids; 7179 const psshData = results[0]; 7180 this.keyId = (_psshData$kids = psshData.kids) != null && _psshData$kids.length ? psshData.kids[0] : null; 7181 } 7182 } 7183 if (!this.keyId) { 7184 this.keyId = getKeyIdFromPlayReadyKey(levelKeys); 7185 } 7186 break; 7187 case KeySystemFormats.PLAYREADY: 7188 { 7189 const PlayReadyKeySystemUUID = new Uint8Array([0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95]); 7190 7191 // Setting `pssh` on this LevelKey/DecryptData allows HLS.js to generate a session using 7192 // the playlist-key before the "encrypted" event. (Comment out to only use "encrypted" path.) 7193 this.pssh = mp4pssh(PlayReadyKeySystemUUID, null, keyBytes); 7194 this.keyId = parsePlayReadyWRM(keyBytes); 7195 break; 7196 } 7197 default: 7198 { 7199 let keydata = keyBytes.subarray(0, 16); 7200 if (keydata.length !== 16) { 7201 const padded = new Uint8Array(16); 7202 padded.set(keydata, 16 - keydata.length); 7203 keydata = padded; 7204 } 7205 this.keyId = keydata; 7206 break; 7207 } 7208 } 7209 } 7210 7211 // Default behavior: get keyId from other KEY tag or URI lookup 7212 if (!this.keyId || this.keyId.byteLength !== 16) { 7213 let keyId; 7214 keyId = getKeyIdFromWidevineKey(levelKeys); 7215 if (!keyId) { 7216 keyId = getKeyIdFromPlayReadyKey(levelKeys); 7217 if (!keyId) { 7218 keyId = keyUriToKeyIdMap[this.uri]; 7219 } 7220 } 7221 if (keyId) { 7222 this.keyId = keyId; 7223 LevelKey.setKeyIdForUri(this.uri, keyId); 7224 } 7225 } 7226 return this; 7227 } 7228} 7229function getKeyIdFromWidevineKey(levelKeys) { 7230 const widevineKey = levelKeys == null ? void 0 : levelKeys[KeySystemFormats.WIDEVINE]; 7231 if (widevineKey) { 7232 return widevineKey.keyId; 7233 } 7234 return null; 7235} 7236function getKeyIdFromPlayReadyKey(levelKeys) { 7237 const playReadyKey = levelKeys == null ? void 0 : levelKeys[KeySystemFormats.PLAYREADY]; 7238 if (playReadyKey) { 7239 const playReadyKeyBytes = convertDataUriToArrayBytes(playReadyKey.uri); 7240 if (playReadyKeyBytes) { 7241 return parsePlayReadyWRM(playReadyKeyBytes); 7242 } 7243 } 7244 return null; 7245} 7246function createInitializationVector(segmentNumber) { 7247 const uint8View = new Uint8Array(16); 7248 for (let i = 12; i < 16; i++) { 7249 uint8View[i] = segmentNumber >> 8 * (15 - i) & 0xff; 7250 } 7251 return uint8View; 7252} 7253 7254const MASTER_PLAYLIST_REGEX = /#EXT-X-STREAM-INF:([^\r\n]*)(?:[\r\n](?:#[^\r\n]*)?)*([^\r\n]+)|#EXT-X-(SESSION-DATA|SESSION-KEY|DEFINE|CONTENT-STEERING|START):([^\r\n]*)[\r\n]+/g; 7255const MASTER_PLAYLIST_MEDIA_REGEX = /#EXT-X-MEDIA:(.*)/g; 7256const IS_MEDIA_PLAYLIST = /^#EXT(?:INF|-X-TARGETDURATION):/m; // Handle empty Media Playlist (first EXTINF not signaled, but TARGETDURATION present) 7257 7258const LEVEL_PLAYLIST_REGEX_FAST = new RegExp([/#EXTINF:\s*(\d*(?:\.\d+)?)(?:,(.*)\s+)?/.source, 7259// duration (#EXTINF:<duration>,<title>), group 1 => duration, group 2 => title 7260/(?!#) *(\S[^\r\n]*)/.source, 7261// segment URI, group 3 => the URI (note newline is not eaten) 7262/#.*/.source // All other non-segment oriented tags will match with all groups empty 7263].join('|'), 'g'); 7264const LEVEL_PLAYLIST_REGEX_SLOW = new RegExp([/#EXT-X-(PROGRAM-DATE-TIME|BYTERANGE|DATERANGE|DEFINE|KEY|MAP|PART|PART-INF|PLAYLIST-TYPE|PRELOAD-HINT|RENDITION-REPORT|SERVER-CONTROL|SKIP|START):(.+)/.source, /#EXT-X-(BITRATE|DISCONTINUITY-SEQUENCE|MEDIA-SEQUENCE|TARGETDURATION|VERSION): *(\d+)/.source, /#EXT-X-(DISCONTINUITY|ENDLIST|GAP|INDEPENDENT-SEGMENTS)/.source, /(#)([^:]*):(.*)/.source, /(#)(.*)(?:.*)\r?\n?/.source].join('|')); 7265class M3U8Parser { 7266 static findGroup(groups, mediaGroupId) { 7267 for (let i = 0; i < groups.length; i++) { 7268 const group = groups[i]; 7269 if (group.id === mediaGroupId) { 7270 return group; 7271 } 7272 } 7273 } 7274 static resolve(url, baseUrl) { 7275 return urlToolkitExports.buildAbsoluteURL(baseUrl, url, { 7276 alwaysNormalize: true 7277 }); 7278 } 7279 static isMediaPlaylist(str) { 7280 return IS_MEDIA_PLAYLIST.test(str); 7281 } 7282 static parseMasterPlaylist(string, baseurl) { 7283 const hasVariableRefs = hasVariableReferences(string) ; 7284 const parsed = { 7285 contentSteering: null, 7286 levels: [], 7287 playlistParsingError: null, 7288 sessionData: null, 7289 sessionKeys: null, 7290 startTimeOffset: null, 7291 variableList: null, 7292 hasVariableRefs 7293 }; 7294 const levelsWithKnownCodecs = []; 7295 MASTER_PLAYLIST_REGEX.lastIndex = 0; 7296 if (!string.startsWith('#EXTM3U')) { 7297 parsed.playlistParsingError = new Error('no EXTM3U delimiter'); 7298 return parsed; 7299 } 7300 let result; 7301 while ((result = MASTER_PLAYLIST_REGEX.exec(string)) != null) { 7302 if (result[1]) { 7303 var _level$unknownCodecs; 7304 // '#EXT-X-STREAM-INF' is found, parse level tag in group 1 7305 const attrs = new AttrList(result[1], parsed); 7306 const uri = substituteVariables(parsed, result[2]) ; 7307 const level = { 7308 attrs, 7309 bitrate: attrs.decimalInteger('BANDWIDTH') || attrs.decimalInteger('AVERAGE-BANDWIDTH'), 7310 name: attrs.NAME, 7311 url: M3U8Parser.resolve(uri, baseurl) 7312 }; 7313 const resolution = attrs.decimalResolution('RESOLUTION'); 7314 if (resolution) { 7315 level.width = resolution.width; 7316 level.height = resolution.height; 7317 } 7318 setCodecs(attrs.CODECS, level); 7319 const supplementalCodecs = attrs['SUPPLEMENTAL-CODECS']; 7320 if (supplementalCodecs) { 7321 level.supplemental = {}; 7322 setCodecs(supplementalCodecs, level.supplemental); 7323 } 7324 if (!((_level$unknownCodecs = level.unknownCodecs) != null && _level$unknownCodecs.length)) { 7325 levelsWithKnownCodecs.push(level); 7326 } 7327 parsed.levels.push(level); 7328 } else if (result[3]) { 7329 const tag = result[3]; 7330 const attributes = result[4]; 7331 switch (tag) { 7332 case 'SESSION-DATA': 7333 { 7334 // #EXT-X-SESSION-DATA 7335 const sessionAttrs = new AttrList(attributes, parsed); 7336 const dataId = sessionAttrs['DATA-ID']; 7337 if (dataId) { 7338 if (parsed.sessionData === null) { 7339 parsed.sessionData = {}; 7340 } 7341 parsed.sessionData[dataId] = sessionAttrs; 7342 } 7343 break; 7344 } 7345 case 'SESSION-KEY': 7346 { 7347 // #EXT-X-SESSION-KEY 7348 const sessionKey = parseKey(attributes, baseurl, parsed); 7349 if (sessionKey.encrypted && sessionKey.isSupported()) { 7350 if (parsed.sessionKeys === null) { 7351 parsed.sessionKeys = []; 7352 } 7353 parsed.sessionKeys.push(sessionKey); 7354 } else { 7355 logger.warn(`[Keys] Ignoring invalid EXT-X-SESSION-KEY tag: "${attributes}"`); 7356 } 7357 break; 7358 } 7359 case 'DEFINE': 7360 { 7361 // #EXT-X-DEFINE 7362 { 7363 const variableAttributes = new AttrList(attributes, parsed); 7364 addVariableDefinition(parsed, variableAttributes, baseurl); 7365 } 7366 break; 7367 } 7368 case 'CONTENT-STEERING': 7369 { 7370 // #EXT-X-CONTENT-STEERING 7371 const contentSteeringAttributes = new AttrList(attributes, parsed); 7372 parsed.contentSteering = { 7373 uri: M3U8Parser.resolve(contentSteeringAttributes['SERVER-URI'], baseurl), 7374 pathwayId: contentSteeringAttributes['PATHWAY-ID'] || '.' 7375 }; 7376 break; 7377 } 7378 case 'START': 7379 { 7380 // #EXT-X-START 7381 parsed.startTimeOffset = parseStartTimeOffset(attributes); 7382 break; 7383 } 7384 } 7385 } 7386 } 7387 // Filter out levels with unknown codecs if it does not remove all levels 7388 const stripUnknownCodecLevels = levelsWithKnownCodecs.length > 0 && levelsWithKnownCodecs.length < parsed.levels.length; 7389 parsed.levels = stripUnknownCodecLevels ? levelsWithKnownCodecs : parsed.levels; 7390 if (parsed.levels.length === 0) { 7391 parsed.playlistParsingError = new Error('no levels found in manifest'); 7392 } 7393 return parsed; 7394 } 7395 static parseMasterPlaylistMedia(string, baseurl, parsed) { 7396 let result; 7397 const results = {}; 7398 const levels = parsed.levels; 7399 const groupsByType = { 7400 AUDIO: levels.map(level => ({ 7401 id: level.attrs.AUDIO, 7402 audioCodec: level.audioCodec 7403 })), 7404 SUBTITLES: levels.map(level => ({ 7405 id: level.attrs.SUBTITLES, 7406 textCodec: level.textCodec 7407 })), 7408 'CLOSED-CAPTIONS': [] 7409 }; 7410 let id = 0; 7411 MASTER_PLAYLIST_MEDIA_REGEX.lastIndex = 0; 7412 while ((result = MASTER_PLAYLIST_MEDIA_REGEX.exec(string)) !== null) { 7413 const attrs = new AttrList(result[1], parsed); 7414 const type = attrs.TYPE; 7415 if (type) { 7416 const groups = groupsByType[type]; 7417 const medias = results[type] || []; 7418 results[type] = medias; 7419 const lang = attrs.LANGUAGE; 7420 const assocLang = attrs['ASSOC-LANGUAGE']; 7421 const channels = attrs.CHANNELS; 7422 const characteristics = attrs.CHARACTERISTICS; 7423 const instreamId = attrs['INSTREAM-ID']; 7424 const media = { 7425 attrs, 7426 bitrate: 0, 7427 id: id++, 7428 groupId: attrs['GROUP-ID'] || '', 7429 name: attrs.NAME || lang || '', 7430 type, 7431 default: attrs.bool('DEFAULT'), 7432 autoselect: attrs.bool('AUTOSELECT'), 7433 forced: attrs.bool('FORCED'), 7434 lang, 7435 url: attrs.URI ? M3U8Parser.resolve(attrs.URI, baseurl) : '' 7436 }; 7437 if (assocLang) { 7438 media.assocLang = assocLang; 7439 } 7440 if (channels) { 7441 media.channels = channels; 7442 } 7443 if (characteristics) { 7444 media.characteristics = characteristics; 7445 } 7446 if (instreamId) { 7447 media.instreamId = instreamId; 7448 } 7449 if (groups != null && groups.length) { 7450 // If there are audio or text groups signalled in the manifest, let's look for a matching codec string for this track 7451 // If we don't find the track signalled, lets use the first audio groups codec we have 7452 // Acting as a best guess 7453 const groupCodec = M3U8Parser.findGroup(groups, media.groupId) || groups[0]; 7454 assignCodec(media, groupCodec, 'audioCodec'); 7455 assignCodec(media, groupCodec, 'textCodec'); 7456 } 7457 medias.push(media); 7458 } 7459 } 7460 return results; 7461 } 7462 static parseLevelPlaylist(string, baseurl, id, type, levelUrlId, multivariantVariableList) { 7463 var _LEVEL_PLAYLIST_REGEX; 7464 const base = { 7465 url: baseurl 7466 }; 7467 const level = new LevelDetails(baseurl); 7468 const fragments = level.fragments; 7469 const programDateTimes = []; 7470 // The most recent init segment seen (applies to all subsequent segments) 7471 let currentInitSegment = null; 7472 let currentSN = 0; 7473 let currentPart = 0; 7474 let totalduration = 0; 7475 let discontinuityCounter = 0; 7476 let currentBitrate = 0; 7477 let prevFrag = null; 7478 let frag = new Fragment(type, base); 7479 let result; 7480 let i; 7481 let levelkeys; 7482 let firstPdtIndex = -1; 7483 let createNextFrag = false; 7484 let nextByteRange = null; 7485 let serverControlAttrs; 7486 LEVEL_PLAYLIST_REGEX_FAST.lastIndex = 0; 7487 level.m3u8 = string; 7488 level.hasVariableRefs = hasVariableReferences(string) ; 7489 if (((_LEVEL_PLAYLIST_REGEX = LEVEL_PLAYLIST_REGEX_FAST.exec(string)) == null ? void 0 : _LEVEL_PLAYLIST_REGEX[0]) !== '#EXTM3U') { 7490 level.playlistParsingError = new Error('Missing format identifier #EXTM3U'); 7491 return level; 7492 } 7493 while ((result = LEVEL_PLAYLIST_REGEX_FAST.exec(string)) !== null) { 7494 if (createNextFrag) { 7495 createNextFrag = false; 7496 frag = new Fragment(type, base); 7497 // setup the next fragment for part loading 7498 frag.playlistOffset = totalduration; 7499 frag.setStart(totalduration); 7500 frag.sn = currentSN; 7501 frag.cc = discontinuityCounter; 7502 if (currentBitrate) { 7503 frag.bitrate = currentBitrate; 7504 } 7505 frag.level = id; 7506 if (currentInitSegment) { 7507 frag.initSegment = currentInitSegment; 7508 if (currentInitSegment.rawProgramDateTime) { 7509 frag.rawProgramDateTime = currentInitSegment.rawProgramDateTime; 7510 currentInitSegment.rawProgramDateTime = null; 7511 } 7512 if (nextByteRange) { 7513 frag.setByteRange(nextByteRange); 7514 nextByteRange = null; 7515 } 7516 } 7517 } 7518 const duration = result[1]; 7519 if (duration) { 7520 // INF 7521 frag.duration = parseFloat(duration); 7522 // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 7523 const title = (' ' + result[2]).slice(1); 7524 frag.title = title || null; 7525 frag.tagList.push(title ? ['INF', duration, title] : ['INF', duration]); 7526 } else if (result[3]) { 7527 // url 7528 if (isFiniteNumber(frag.duration)) { 7529 frag.playlistOffset = totalduration; 7530 frag.setStart(totalduration); 7531 if (levelkeys) { 7532 setFragLevelKeys(frag, levelkeys, level); 7533 } 7534 frag.sn = currentSN; 7535 frag.level = id; 7536 frag.cc = discontinuityCounter; 7537 fragments.push(frag); 7538 // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 7539 const uri = (' ' + result[3]).slice(1); 7540 frag.relurl = substituteVariables(level, uri) ; 7541 assignProgramDateTime(frag, prevFrag, programDateTimes); 7542 prevFrag = frag; 7543 totalduration += frag.duration; 7544 currentSN++; 7545 currentPart = 0; 7546 createNextFrag = true; 7547 } 7548 } else { 7549 result = result[0].match(LEVEL_PLAYLIST_REGEX_SLOW); 7550 if (!result) { 7551 logger.warn('No matches on slow regex match for level playlist!'); 7552 continue; 7553 } 7554 for (i = 1; i < result.length; i++) { 7555 if (result[i] !== undefined) { 7556 break; 7557 } 7558 } 7559 7560 // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 7561 const tag = (' ' + result[i]).slice(1); 7562 const value1 = (' ' + result[i + 1]).slice(1); 7563 const value2 = result[i + 2] ? (' ' + result[i + 2]).slice(1) : null; 7564 switch (tag) { 7565 case 'BYTERANGE': 7566 if (prevFrag) { 7567 frag.setByteRange(value1, prevFrag); 7568 } else { 7569 frag.setByteRange(value1); 7570 } 7571 break; 7572 case 'PROGRAM-DATE-TIME': 7573 // avoid sliced strings https://github.com/video-dev/hls.js/issues/939 7574 frag.rawProgramDateTime = value1; 7575 frag.tagList.push(['PROGRAM-DATE-TIME', value1]); 7576 if (firstPdtIndex === -1) { 7577 firstPdtIndex = fragments.length; 7578 } 7579 break; 7580 case 'PLAYLIST-TYPE': 7581 if (level.type) { 7582 assignMultipleMediaPlaylistTagOccuranceError(level, tag, result); 7583 } 7584 level.type = value1.toUpperCase(); 7585 break; 7586 case 'MEDIA-SEQUENCE': 7587 if (level.startSN !== 0) { 7588 assignMultipleMediaPlaylistTagOccuranceError(level, tag, result); 7589 } else if (fragments.length > 0) { 7590 assignMustAppearBeforeSegmentsError(level, tag, result); 7591 } 7592 currentSN = level.startSN = parseInt(value1); 7593 break; 7594 case 'SKIP': 7595 { 7596 if (level.skippedSegments) { 7597 assignMultipleMediaPlaylistTagOccuranceError(level, tag, result); 7598 } 7599 const skipAttrs = new AttrList(value1, level); 7600 const skippedSegments = skipAttrs.decimalInteger('SKIPPED-SEGMENTS'); 7601 if (isFiniteNumber(skippedSegments)) { 7602 level.skippedSegments += skippedSegments; 7603 // This will result in fragments[] containing undefined values, which we will fill in with `mergeDetails` 7604 for (let _i = skippedSegments; _i--;) { 7605 fragments.push(null); 7606 } 7607 currentSN += skippedSegments; 7608 } 7609 const recentlyRemovedDateranges = skipAttrs.enumeratedString('RECENTLY-REMOVED-DATERANGES'); 7610 if (recentlyRemovedDateranges) { 7611 level.recentlyRemovedDateranges = (level.recentlyRemovedDateranges || []).concat(recentlyRemovedDateranges.split('\t')); 7612 } 7613 break; 7614 } 7615 case 'TARGETDURATION': 7616 if (level.targetduration !== 0) { 7617 assignMultipleMediaPlaylistTagOccuranceError(level, tag, result); 7618 } 7619 level.targetduration = Math.max(parseInt(value1), 1); 7620 break; 7621 case 'VERSION': 7622 if (level.version !== null) { 7623 assignMultipleMediaPlaylistTagOccuranceError(level, tag, result); 7624 } 7625 level.version = parseInt(value1); 7626 break; 7627 case 'INDEPENDENT-SEGMENTS': 7628 break; 7629 case 'ENDLIST': 7630 if (!level.live) { 7631 assignMultipleMediaPlaylistTagOccuranceError(level, tag, result); 7632 } 7633 level.live = false; 7634 break; 7635 case '#': 7636 if (value1 || value2) { 7637 frag.tagList.push(value2 ? [value1, value2] : [value1]); 7638 } 7639 break; 7640 case 'DISCONTINUITY': 7641 discontinuityCounter++; 7642 frag.tagList.push(['DIS']); 7643 break; 7644 case 'GAP': 7645 frag.gap = true; 7646 frag.tagList.push([tag]); 7647 break; 7648 case 'BITRATE': 7649 frag.tagList.push([tag, value1]); 7650 currentBitrate = parseInt(value1) * 1000; 7651 if (isFiniteNumber(currentBitrate)) { 7652 frag.bitrate = currentBitrate; 7653 } else { 7654 currentBitrate = 0; 7655 } 7656 break; 7657 case 'DATERANGE': 7658 { 7659 const dateRangeAttr = new AttrList(value1, level); 7660 const dateRange = new DateRange(dateRangeAttr, level.dateRanges[dateRangeAttr.ID], level.dateRangeTagCount); 7661 level.dateRangeTagCount++; 7662 if (dateRange.isValid || level.skippedSegments) { 7663 level.dateRanges[dateRange.id] = dateRange; 7664 } else { 7665 logger.warn(`Ignoring invalid DATERANGE tag: "${value1}"`); 7666 } 7667 // Add to fragment tag list for backwards compatibility (< v1.2.0) 7668 frag.tagList.push(['EXT-X-DATERANGE', value1]); 7669 break; 7670 } 7671 case 'DEFINE': 7672 { 7673 { 7674 const variableAttributes = new AttrList(value1, level); 7675 if ('IMPORT' in variableAttributes) { 7676 importVariableDefinition(level, variableAttributes, multivariantVariableList); 7677 } else { 7678 addVariableDefinition(level, variableAttributes, baseurl); 7679 } 7680 } 7681 break; 7682 } 7683 case 'DISCONTINUITY-SEQUENCE': 7684 if (level.startCC !== 0) { 7685 assignMultipleMediaPlaylistTagOccuranceError(level, tag, result); 7686 } else if (fragments.length > 0) { 7687 assignMustAppearBeforeSegmentsError(level, tag, result); 7688 } 7689 level.startCC = discontinuityCounter = parseInt(value1); 7690 break; 7691 case 'KEY': 7692 { 7693 const levelKey = parseKey(value1, baseurl, level); 7694 if (levelKey.isSupported()) { 7695 if (levelKey.method === 'NONE') { 7696 levelkeys = undefined; 7697 break; 7698 } 7699 if (!levelkeys) { 7700 levelkeys = {}; 7701 } 7702 const currentKey = levelkeys[levelKey.keyFormat]; 7703 // Ignore duplicate playlist KEY tags 7704 if (!(currentKey != null && currentKey.matches(levelKey))) { 7705 if (currentKey) { 7706 levelkeys = _extends({}, levelkeys); 7707 } 7708 levelkeys[levelKey.keyFormat] = levelKey; 7709 } 7710 } else { 7711 logger.warn(`[Keys] Ignoring unsupported EXT-X-KEY tag: "${value1}"${'' }`); 7712 } 7713 break; 7714 } 7715 case 'START': 7716 level.startTimeOffset = parseStartTimeOffset(value1); 7717 break; 7718 case 'MAP': 7719 { 7720 const mapAttrs = new AttrList(value1, level); 7721 if (frag.duration) { 7722 // Initial segment tag is after segment duration tag. 7723 // #EXTINF: 6.0 7724 // #EXT-X-MAP:URI="init.mp4 7725 const init = new Fragment(type, base); 7726 setInitSegment(init, mapAttrs, id, levelkeys); 7727 currentInitSegment = init; 7728 frag.initSegment = currentInitSegment; 7729 if (currentInitSegment.rawProgramDateTime && !frag.rawProgramDateTime) { 7730 frag.rawProgramDateTime = currentInitSegment.rawProgramDateTime; 7731 } 7732 } else { 7733 // Initial segment tag is before segment duration tag 7734 // Handle case where EXT-X-MAP is declared after EXT-X-BYTERANGE 7735 const end = frag.byteRangeEndOffset; 7736 if (end) { 7737 const start = frag.byteRangeStartOffset; 7738 nextByteRange = `${end - start}@${start}`; 7739 } else { 7740 nextByteRange = null; 7741 } 7742 setInitSegment(frag, mapAttrs, id, levelkeys); 7743 currentInitSegment = frag; 7744 createNextFrag = true; 7745 } 7746 currentInitSegment.cc = discontinuityCounter; 7747 break; 7748 } 7749 case 'SERVER-CONTROL': 7750 { 7751 if (serverControlAttrs) { 7752 assignMultipleMediaPlaylistTagOccuranceError(level, tag, result); 7753 } 7754 serverControlAttrs = new AttrList(value1); 7755 level.canBlockReload = serverControlAttrs.bool('CAN-BLOCK-RELOAD'); 7756 level.canSkipUntil = serverControlAttrs.optionalFloat('CAN-SKIP-UNTIL', 0); 7757 level.canSkipDateRanges = level.canSkipUntil > 0 && serverControlAttrs.bool('CAN-SKIP-DATERANGES'); 7758 level.partHoldBack = serverControlAttrs.optionalFloat('PART-HOLD-BACK', 0); 7759 level.holdBack = serverControlAttrs.optionalFloat('HOLD-BACK', 0); 7760 break; 7761 } 7762 case 'PART-INF': 7763 { 7764 if (level.partTarget) { 7765 assignMultipleMediaPlaylistTagOccuranceError(level, tag, result); 7766 } 7767 const partInfAttrs = new AttrList(value1); 7768 level.partTarget = partInfAttrs.decimalFloatingPoint('PART-TARGET'); 7769 break; 7770 } 7771 case 'PART': 7772 { 7773 let partList = level.partList; 7774 if (!partList) { 7775 partList = level.partList = []; 7776 } 7777 const previousFragmentPart = currentPart > 0 ? partList[partList.length - 1] : undefined; 7778 const index = currentPart++; 7779 const partAttrs = new AttrList(value1, level); 7780 const part = new Part(partAttrs, frag, base, index, previousFragmentPart); 7781 partList.push(part); 7782 frag.duration += part.duration; 7783 break; 7784 } 7785 case 'PRELOAD-HINT': 7786 { 7787 const preloadHintAttrs = new AttrList(value1, level); 7788 level.preloadHint = preloadHintAttrs; 7789 break; 7790 } 7791 case 'RENDITION-REPORT': 7792 { 7793 const renditionReportAttrs = new AttrList(value1, level); 7794 level.renditionReports = level.renditionReports || []; 7795 level.renditionReports.push(renditionReportAttrs); 7796 break; 7797 } 7798 default: 7799 logger.warn(`line parsed but not handled: ${result}`); 7800 break; 7801 } 7802 } 7803 } 7804 if (prevFrag && !prevFrag.relurl) { 7805 fragments.pop(); 7806 totalduration -= prevFrag.duration; 7807 if (level.partList) { 7808 level.fragmentHint = prevFrag; 7809 } 7810 } else if (level.partList) { 7811 assignProgramDateTime(frag, prevFrag, programDateTimes); 7812 frag.cc = discontinuityCounter; 7813 level.fragmentHint = frag; 7814 if (levelkeys) { 7815 setFragLevelKeys(frag, levelkeys, level); 7816 } 7817 } 7818 if (!level.targetduration) { 7819 level.playlistParsingError = new Error(`Missing Target Duration`); 7820 } 7821 const fragmentLength = fragments.length; 7822 const firstFragment = fragments[0]; 7823 const lastFragment = fragments[fragmentLength - 1]; 7824 totalduration += level.skippedSegments * level.targetduration; 7825 if (totalduration > 0 && fragmentLength && lastFragment) { 7826 level.averagetargetduration = totalduration / fragmentLength; 7827 const lastSn = lastFragment.sn; 7828 level.endSN = lastSn !== 'initSegment' ? lastSn : 0; 7829 if (!level.live) { 7830 lastFragment.endList = true; 7831 } 7832 /** 7833 * Backfill any missing PDT values 7834 * "If the first EXT-X-PROGRAM-DATE-TIME tag in a Playlist appears after 7835 * one or more Media Segment URIs, the client SHOULD extrapolate 7836 * backward from that tag (using EXTINF durations and/or media 7837 * timestamps) to associate dates with those segments." 7838 * We have already extrapolated forward, but all fragments up to the first instance of PDT do not have their PDTs 7839 * computed. 7840 */ 7841 if (firstPdtIndex > 0) { 7842 backfillProgramDateTimes(fragments, firstPdtIndex); 7843 if (firstFragment) { 7844 programDateTimes.unshift(firstFragment); 7845 } 7846 } 7847 } 7848 if (level.fragmentHint) { 7849 totalduration += level.fragmentHint.duration; 7850 } 7851 level.totalduration = totalduration; 7852 if (programDateTimes.length && level.dateRangeTagCount && firstFragment) { 7853 mapDateRanges(programDateTimes, level); 7854 } 7855 level.endCC = discontinuityCounter; 7856 return level; 7857 } 7858} 7859function mapDateRanges(programDateTimes, details) { 7860 // Make sure DateRanges are mapped to a ProgramDateTime tag that applies a date to a segment that overlaps with its start date 7861 let programDateTimeCount = programDateTimes.length; 7862 if (!programDateTimeCount) { 7863 if (details.hasProgramDateTime) { 7864 const lastFragment = details.fragments[details.fragments.length - 1]; 7865 programDateTimes.push(lastFragment); 7866 programDateTimeCount++; 7867 } else { 7868 // no segments with EXT-X-PROGRAM-DATE-TIME references in playlist history 7869 return; 7870 } 7871 } 7872 const lastProgramDateTime = programDateTimes[programDateTimeCount - 1]; 7873 const playlistEnd = details.live ? Infinity : details.totalduration; 7874 const dateRangeIds = Object.keys(details.dateRanges); 7875 for (let i = dateRangeIds.length; i--;) { 7876 const dateRange = details.dateRanges[dateRangeIds[i]]; 7877 const startDateTime = dateRange.startDate.getTime(); 7878 dateRange.tagAnchor = lastProgramDateTime.ref; 7879 for (let j = programDateTimeCount; j--;) { 7880 var _programDateTimes$j; 7881 if (((_programDateTimes$j = programDateTimes[j]) == null ? void 0 : _programDateTimes$j.sn) < details.startSN) { 7882 break; 7883 } 7884 const fragIndex = findFragmentWithStartDate(details, startDateTime, programDateTimes, j, playlistEnd); 7885 if (fragIndex !== -1) { 7886 dateRange.tagAnchor = details.fragments[fragIndex].ref; 7887 break; 7888 } 7889 } 7890 } 7891} 7892function findFragmentWithStartDate(details, startDateTime, programDateTimes, index, endTime) { 7893 const pdtFragment = programDateTimes[index]; 7894 if (pdtFragment) { 7895 // find matching range between PDT tags 7896 const pdtStart = pdtFragment.programDateTime; 7897 if (startDateTime >= pdtStart || index === 0) { 7898 var _programDateTimes; 7899 const durationBetweenPdt = (((_programDateTimes = programDateTimes[index + 1]) == null ? void 0 : _programDateTimes.start) || endTime) - pdtFragment.start; 7900 if (startDateTime <= pdtStart + durationBetweenPdt * 1000) { 7901 // map to fragment with date-time range 7902 const startIndex = programDateTimes[index].sn - details.startSN; 7903 if (startIndex < 0) { 7904 return -1; 7905 } 7906 const fragments = details.fragments; 7907 if (fragments.length > programDateTimes.length) { 7908 const endSegment = programDateTimes[index + 1] || fragments[fragments.length - 1]; 7909 const endIndex = endSegment.sn - details.startSN; 7910 for (let i = endIndex; i > startIndex; i--) { 7911 const fragStartDateTime = fragments[i].programDateTime; 7912 if (startDateTime >= fragStartDateTime && startDateTime < fragStartDateTime + fragments[i].duration * 1000) { 7913 return i; 7914 } 7915 } 7916 } 7917 return startIndex; 7918 } 7919 } 7920 } 7921 return -1; 7922} 7923function parseKey(keyTagAttributes, baseurl, parsed) { 7924 var _keyAttrs$METHOD, _keyAttrs$KEYFORMAT; 7925 // https://tools.ietf.org/html/rfc8216#section-4.3.2.4 7926 const keyAttrs = new AttrList(keyTagAttributes, parsed); 7927 const decryptmethod = (_keyAttrs$METHOD = keyAttrs.METHOD) != null ? _keyAttrs$METHOD : ''; 7928 const decrypturi = keyAttrs.URI; 7929 const decryptiv = keyAttrs.hexadecimalInteger('IV'); 7930 const decryptkeyformatversions = keyAttrs.KEYFORMATVERSIONS; 7931 // From RFC: This attribute is OPTIONAL; its absence indicates an implicit value of "identity". 7932 const decryptkeyformat = (_keyAttrs$KEYFORMAT = keyAttrs.KEYFORMAT) != null ? _keyAttrs$KEYFORMAT : 'identity'; 7933 if (decrypturi && keyAttrs.IV && !decryptiv) { 7934 logger.error(`Invalid IV: ${keyAttrs.IV}`); 7935 } 7936 // If decrypturi is a URI with a scheme, then baseurl will be ignored 7937 // No uri is allowed when METHOD is NONE 7938 const resolvedUri = decrypturi ? M3U8Parser.resolve(decrypturi, baseurl) : ''; 7939 const keyFormatVersions = (decryptkeyformatversions ? decryptkeyformatversions : '1').split('/').map(Number).filter(Number.isFinite); 7940 return new LevelKey(decryptmethod, resolvedUri, decryptkeyformat, keyFormatVersions, decryptiv, keyAttrs.KEYID); 7941} 7942function parseStartTimeOffset(startAttributes) { 7943 const startAttrs = new AttrList(startAttributes); 7944 const startTimeOffset = startAttrs.decimalFloatingPoint('TIME-OFFSET'); 7945 if (isFiniteNumber(startTimeOffset)) { 7946 return startTimeOffset; 7947 } 7948 return null; 7949} 7950function setCodecs(codecsAttributeValue, level) { 7951 let codecs = (codecsAttributeValue || '').split(/[ ,]+/).filter(c => c); 7952 ['video', 'audio', 'text'].forEach(type => { 7953 const filtered = codecs.filter(codec => isCodecType(codec, type)); 7954 if (filtered.length) { 7955 // Comma separated list of all codecs for type 7956 level[`${type}Codec`] = filtered.map(c => c.split('/')[0]).join(','); 7957 // Remove known codecs so that only unknownCodecs are left after iterating through each type 7958 codecs = codecs.filter(codec => filtered.indexOf(codec) === -1); 7959 } 7960 }); 7961 level.unknownCodecs = codecs; 7962} 7963function assignCodec(media, groupItem, codecProperty) { 7964 const codecValue = groupItem[codecProperty]; 7965 if (codecValue) { 7966 media[codecProperty] = codecValue; 7967 } 7968} 7969function backfillProgramDateTimes(fragments, firstPdtIndex) { 7970 let fragPrev = fragments[firstPdtIndex]; 7971 for (let i = firstPdtIndex; i--;) { 7972 const frag = fragments[i]; 7973 // Exit on delta-playlist skipped segments 7974 if (!frag) { 7975 return; 7976 } 7977 frag.programDateTime = fragPrev.programDateTime - frag.duration * 1000; 7978 fragPrev = frag; 7979 } 7980} 7981function assignProgramDateTime(frag, prevFrag, programDateTimes) { 7982 if (frag.rawProgramDateTime) { 7983 programDateTimes.push(frag); 7984 } else if (prevFrag != null && prevFrag.programDateTime) { 7985 frag.programDateTime = prevFrag.endProgramDateTime; 7986 } 7987} 7988function setInitSegment(frag, mapAttrs, id, levelkeys) { 7989 frag.relurl = mapAttrs.URI; 7990 if (mapAttrs.BYTERANGE) { 7991 frag.setByteRange(mapAttrs.BYTERANGE); 7992 } 7993 frag.level = id; 7994 frag.sn = 'initSegment'; 7995 if (levelkeys) { 7996 frag.levelkeys = levelkeys; 7997 } 7998 frag.initSegment = null; 7999} 8000function setFragLevelKeys(frag, levelkeys, level) { 8001 frag.levelkeys = levelkeys; 8002 const { 8003 encryptedFragments 8004 } = level; 8005 if ((!encryptedFragments.length || encryptedFragments[encryptedFragments.length - 1].levelkeys !== levelkeys) && Object.keys(levelkeys).some(format => levelkeys[format].isCommonEncryption)) { 8006 encryptedFragments.push(frag); 8007 } 8008} 8009function assignMultipleMediaPlaylistTagOccuranceError(level, tag, result) { 8010 level.playlistParsingError = new Error(`#EXT-X-${tag} must not appear more than once (${result[0]})`); 8011} 8012function assignMustAppearBeforeSegmentsError(level, tag, result) { 8013 level.playlistParsingError = new Error(`#EXT-X-${tag} must appear before the first Media Segment (${result[0]})`); 8014} 8015 8016function updateFromToPTS(fragFrom, fragTo) { 8017 const fragToPTS = fragTo.startPTS; 8018 // if we know startPTS[toIdx] 8019 if (isFiniteNumber(fragToPTS)) { 8020 // update fragment duration. 8021 // it helps to fix drifts between playlist reported duration and fragment real duration 8022 let duration = 0; 8023 let frag; 8024 if (fragTo.sn > fragFrom.sn) { 8025 duration = fragToPTS - fragFrom.start; 8026 frag = fragFrom; 8027 } else { 8028 duration = fragFrom.start - fragToPTS; 8029 frag = fragTo; 8030 } 8031 if (frag.duration !== duration) { 8032 frag.setDuration(duration); 8033 } 8034 // we dont know startPTS[toIdx] 8035 } else if (fragTo.sn > fragFrom.sn) { 8036 const contiguous = fragFrom.cc === fragTo.cc; 8037 // TODO: With part-loading end/durations we need to confirm the whole fragment is loaded before using (or setting) minEndPTS 8038 if (contiguous && fragFrom.minEndPTS) { 8039 fragTo.setStart(fragFrom.start + (fragFrom.minEndPTS - fragFrom.start)); 8040 } else { 8041 fragTo.setStart(fragFrom.start + fragFrom.duration); 8042 } 8043 } else { 8044 fragTo.setStart(Math.max(fragFrom.start - fragTo.duration, 0)); 8045 } 8046} 8047function updateFragPTSDTS(details, frag, startPTS, endPTS, startDTS, endDTS, logger) { 8048 const parsedMediaDuration = endPTS - startPTS; 8049 if (parsedMediaDuration <= 0) { 8050 logger.warn('Fragment should have a positive duration', frag); 8051 endPTS = startPTS + frag.duration; 8052 endDTS = startDTS + frag.duration; 8053 } 8054 let maxStartPTS = startPTS; 8055 let minEndPTS = endPTS; 8056 const fragStartPts = frag.startPTS; 8057 const fragEndPts = frag.endPTS; 8058 if (isFiniteNumber(fragStartPts)) { 8059 // delta PTS between audio and video 8060 const deltaPTS = Math.abs(fragStartPts - startPTS); 8061 if (details && deltaPTS > details.totalduration) { 8062 logger.warn(`media timestamps and playlist times differ by ${deltaPTS}s for level ${frag.level} ${details.url}`); 8063 } else if (!isFiniteNumber(frag.deltaPTS)) { 8064 frag.deltaPTS = deltaPTS; 8065 } else { 8066 frag.deltaPTS = Math.max(deltaPTS, frag.deltaPTS); 8067 } 8068 maxStartPTS = Math.max(startPTS, fragStartPts); 8069 startPTS = Math.min(startPTS, fragStartPts); 8070 startDTS = frag.startDTS !== undefined ? Math.min(startDTS, frag.startDTS) : startDTS; 8071 minEndPTS = Math.min(endPTS, fragEndPts); 8072 endPTS = Math.max(endPTS, fragEndPts); 8073 endDTS = frag.endDTS !== undefined ? Math.max(endDTS, frag.endDTS) : endDTS; 8074 } 8075 const drift = startPTS - frag.start; 8076 if (frag.start !== 0) { 8077 frag.setStart(startPTS); 8078 } 8079 frag.setDuration(endPTS - frag.start); 8080 frag.startPTS = startPTS; 8081 frag.maxStartPTS = maxStartPTS; 8082 frag.startDTS = startDTS; 8083 frag.endPTS = endPTS; 8084 frag.minEndPTS = minEndPTS; 8085 frag.endDTS = endDTS; 8086 const sn = frag.sn; 8087 // exit if sn out of range 8088 if (!details || sn < details.startSN || sn > details.endSN) { 8089 return 0; 8090 } 8091 let i; 8092 const fragIdx = sn - details.startSN; 8093 const fragments = details.fragments; 8094 // update frag reference in fragments array 8095 // rationale is that fragments array might not contain this frag object. 8096 // this will happen if playlist has been refreshed between frag loading and call to updateFragPTSDTS() 8097 // if we don't update frag, we won't be able to propagate PTS info on the playlist 8098 // resulting in invalid sliding computation 8099 fragments[fragIdx] = frag; 8100 // adjust fragment PTS/duration from seqnum-1 to frag 0 8101 for (i = fragIdx; i > 0; i--) { 8102 updateFromToPTS(fragments[i], fragments[i - 1]); 8103 } 8104 8105 // adjust fragment PTS/duration from seqnum to last frag 8106 for (i = fragIdx; i < fragments.length - 1; i++) { 8107 updateFromToPTS(fragments[i], fragments[i + 1]); 8108 } 8109 if (details.fragmentHint) { 8110 updateFromToPTS(fragments[fragments.length - 1], details.fragmentHint); 8111 } 8112 details.PTSKnown = details.alignedSliding = true; 8113 return drift; 8114} 8115function mergeDetails(oldDetails, newDetails, logger) { 8116 if (oldDetails === newDetails) { 8117 return; 8118 } 8119 // Track the last initSegment processed. Initialize it to the last one on the timeline. 8120 let currentInitSegment = null; 8121 const oldFragments = oldDetails.fragments; 8122 for (let i = oldFragments.length - 1; i >= 0; i--) { 8123 const oldInit = oldFragments[i].initSegment; 8124 if (oldInit) { 8125 currentInitSegment = oldInit; 8126 break; 8127 } 8128 } 8129 if (oldDetails.fragmentHint) { 8130 // prevent PTS and duration from being adjusted on the next hint 8131 delete oldDetails.fragmentHint.endPTS; 8132 } 8133 // check if old/new playlists have fragments in common 8134 // loop through overlapping SN and update startPTS, cc, and duration if any found 8135 let PTSFrag; 8136 mapFragmentIntersection(oldDetails, newDetails, (oldFrag, newFrag, newFragIndex, newFragments) => { 8137 if ((!newDetails.startCC || newDetails.skippedSegments) && newFrag.cc !== oldFrag.cc) { 8138 const ccOffset = oldFrag.cc - newFrag.cc; 8139 for (let i = newFragIndex; i < newFragments.length; i++) { 8140 newFragments[i].cc += ccOffset; 8141 } 8142 newDetails.endCC = newFragments[newFragments.length - 1].cc; 8143 } 8144 if (isFiniteNumber(oldFrag.startPTS) && isFiniteNumber(oldFrag.endPTS)) { 8145 newFrag.setStart(newFrag.startPTS = oldFrag.startPTS); 8146 newFrag.startDTS = oldFrag.startDTS; 8147 newFrag.maxStartPTS = oldFrag.maxStartPTS; 8148 newFrag.endPTS = oldFrag.endPTS; 8149 newFrag.endDTS = oldFrag.endDTS; 8150 newFrag.minEndPTS = oldFrag.minEndPTS; 8151 newFrag.setDuration(oldFrag.endPTS - oldFrag.startPTS); 8152 if (newFrag.duration) { 8153 PTSFrag = newFrag; 8154 } 8155 8156 // PTS is known when any segment has startPTS and endPTS 8157 newDetails.PTSKnown = newDetails.alignedSliding = true; 8158 } 8159 if (oldFrag.hasStreams) { 8160 newFrag.elementaryStreams = oldFrag.elementaryStreams; 8161 } 8162 newFrag.loader = oldFrag.loader; 8163 if (oldFrag.hasStats) { 8164 newFrag.stats = oldFrag.stats; 8165 } 8166 if (oldFrag.initSegment) { 8167 newFrag.initSegment = oldFrag.initSegment; 8168 currentInitSegment = oldFrag.initSegment; 8169 } 8170 }); 8171 const newFragments = newDetails.fragments; 8172 const fragmentsToCheck = newDetails.fragmentHint ? newFragments.concat(newDetails.fragmentHint) : newFragments; 8173 if (currentInitSegment) { 8174 fragmentsToCheck.forEach(frag => { 8175 var _currentInitSegment; 8176 if (frag && (!frag.initSegment || frag.initSegment.relurl === ((_currentInitSegment = currentInitSegment) == null ? void 0 : _currentInitSegment.relurl))) { 8177 frag.initSegment = currentInitSegment; 8178 } 8179 }); 8180 } 8181 if (newDetails.skippedSegments) { 8182 newDetails.deltaUpdateFailed = newFragments.some(frag => !frag); 8183 if (newDetails.deltaUpdateFailed) { 8184 logger.warn('[level-helper] Previous playlist missing segments skipped in delta playlist'); 8185 for (let i = newDetails.skippedSegments; i--;) { 8186 newFragments.shift(); 8187 } 8188 newDetails.startSN = newFragments[0].sn; 8189 } else { 8190 if (newDetails.canSkipDateRanges) { 8191 newDetails.dateRanges = mergeDateRanges(oldDetails.dateRanges, newDetails, logger); 8192 } 8193 const programDateTimes = oldDetails.fragments.filter(frag => frag.rawProgramDateTime); 8194 if (oldDetails.hasProgramDateTime && !newDetails.hasProgramDateTime) { 8195 for (let i = 1; i < fragmentsToCheck.length; i++) { 8196 if (fragmentsToCheck[i].programDateTime === null) { 8197 assignProgramDateTime(fragmentsToCheck[i], fragmentsToCheck[i - 1], programDateTimes); 8198 } 8199 } 8200 } 8201 mapDateRanges(programDateTimes, newDetails); 8202 } 8203 newDetails.endCC = newFragments[newFragments.length - 1].cc; 8204 } 8205 if (!newDetails.startCC) { 8206 var _fragPriorToNewStart$; 8207 const fragPriorToNewStart = getFragmentWithSN(oldDetails, newDetails.startSN - 1); 8208 newDetails.startCC = (_fragPriorToNewStart$ = fragPriorToNewStart == null ? void 0 : fragPriorToNewStart.cc) != null ? _fragPriorToNewStart$ : newFragments[0].cc; 8209 } 8210 8211 // Merge parts 8212 mapPartIntersection(oldDetails.partList, newDetails.partList, (oldPart, newPart) => { 8213 newPart.elementaryStreams = oldPart.elementaryStreams; 8214 newPart.stats = oldPart.stats; 8215 }); 8216 8217 // if at least one fragment contains PTS info, recompute PTS information for all fragments 8218 if (PTSFrag) { 8219 updateFragPTSDTS(newDetails, PTSFrag, PTSFrag.startPTS, PTSFrag.endPTS, PTSFrag.startDTS, PTSFrag.endDTS, logger); 8220 } else { 8221 // ensure that delta is within oldFragments range 8222 // also adjust sliding in case delta is 0 (we could have old=[50-60] and new=old=[50-61]) 8223 // in that case we also need to adjust start offset of all fragments 8224 adjustSliding(oldDetails, newDetails); 8225 } 8226 if (newFragments.length) { 8227 newDetails.totalduration = newDetails.edge - newFragments[0].start; 8228 } 8229 newDetails.driftStartTime = oldDetails.driftStartTime; 8230 newDetails.driftStart = oldDetails.driftStart; 8231 const advancedDateTime = newDetails.advancedDateTime; 8232 if (newDetails.advanced && advancedDateTime) { 8233 const edge = newDetails.edge; 8234 if (!newDetails.driftStart) { 8235 newDetails.driftStartTime = advancedDateTime; 8236 newDetails.driftStart = edge; 8237 } 8238 newDetails.driftEndTime = advancedDateTime; 8239 newDetails.driftEnd = edge; 8240 } else { 8241 newDetails.driftEndTime = oldDetails.driftEndTime; 8242 newDetails.driftEnd = oldDetails.driftEnd; 8243 newDetails.advancedDateTime = oldDetails.advancedDateTime; 8244 } 8245 if (newDetails.requestScheduled === -1) { 8246 newDetails.requestScheduled = oldDetails.requestScheduled; 8247 } 8248} 8249function mergeDateRanges(oldDateRanges, newDetails, logger) { 8250 const { 8251 dateRanges: deltaDateRanges, 8252 recentlyRemovedDateranges 8253 } = newDetails; 8254 const dateRanges = _extends({}, oldDateRanges); 8255 if (recentlyRemovedDateranges) { 8256 recentlyRemovedDateranges.forEach(id => { 8257 delete dateRanges[id]; 8258 }); 8259 } 8260 const mergeIds = Object.keys(dateRanges); 8261 const mergeCount = mergeIds.length; 8262 if (!mergeCount) { 8263 return deltaDateRanges; 8264 } 8265 Object.keys(deltaDateRanges).forEach(id => { 8266 const mergedDateRange = dateRanges[id]; 8267 const dateRange = new DateRange(deltaDateRanges[id].attr, mergedDateRange); 8268 if (dateRange.isValid) { 8269 dateRanges[id] = dateRange; 8270 if (!mergedDateRange) { 8271 dateRange.tagOrder += mergeCount; 8272 } 8273 } else { 8274 logger.warn(`Ignoring invalid Playlist Delta Update DATERANGE tag: "${stringify(deltaDateRanges[id].attr)}"`); 8275 } 8276 }); 8277 return dateRanges; 8278} 8279function mapPartIntersection(oldParts, newParts, intersectionFn) { 8280 if (oldParts && newParts) { 8281 let delta = 0; 8282 for (let i = 0, len = oldParts.length; i <= len; i++) { 8283 const oldPart = oldParts[i]; 8284 const newPart = newParts[i + delta]; 8285 if (oldPart && newPart && oldPart.index === newPart.index && oldPart.fragment.sn === newPart.fragment.sn) { 8286 intersectionFn(oldPart, newPart); 8287 } else { 8288 delta--; 8289 } 8290 } 8291 } 8292} 8293function mapFragmentIntersection(oldDetails, newDetails, intersectionFn) { 8294 const skippedSegments = newDetails.skippedSegments; 8295 const start = Math.max(oldDetails.startSN, newDetails.startSN) - newDetails.startSN; 8296 const end = (oldDetails.fragmentHint ? 1 : 0) + (skippedSegments ? newDetails.endSN : Math.min(oldDetails.endSN, newDetails.endSN)) - newDetails.startSN; 8297 const delta = newDetails.startSN - oldDetails.startSN; 8298 const newFrags = newDetails.fragmentHint ? newDetails.fragments.concat(newDetails.fragmentHint) : newDetails.fragments; 8299 const oldFrags = oldDetails.fragmentHint ? oldDetails.fragments.concat(oldDetails.fragmentHint) : oldDetails.fragments; 8300 for (let i = start; i <= end; i++) { 8301 const oldFrag = oldFrags[delta + i]; 8302 let newFrag = newFrags[i]; 8303 if (skippedSegments && !newFrag && oldFrag) { 8304 // Fill in skipped segments in delta playlist 8305 newFrag = newDetails.fragments[i] = oldFrag; 8306 } 8307 if (oldFrag && newFrag) { 8308 intersectionFn(oldFrag, newFrag, i, newFrags); 8309 const uriBefore = oldFrag.relurl; 8310 const uriAfter = newFrag.relurl; 8311 if (uriBefore && notEqualAfterStrippingQueries(uriBefore, uriAfter)) { 8312 newDetails.playlistParsingError = getSequenceError(`media sequence mismatch ${newFrag.sn}:`, oldDetails, newDetails, oldFrag, newFrag); 8313 return; 8314 } else if (oldFrag.cc !== newFrag.cc) { 8315 newDetails.playlistParsingError = getSequenceError(`discontinuity sequence mismatch (${oldFrag.cc}!=${newFrag.cc})`, oldDetails, newDetails, oldFrag, newFrag); 8316 return; 8317 } 8318 } 8319 } 8320} 8321function getSequenceError(message, oldDetails, newDetails, oldFrag, newFrag) { 8322 return new Error(`${message} ${newFrag.url} 8323Playlist starting @${oldDetails.startSN} 8324${oldDetails.m3u8} 8325 8326Playlist starting @${newDetails.startSN} 8327${newDetails.m3u8}`); 8328} 8329function adjustSliding(oldDetails, newDetails, matchingStableVariantOrRendition = true) { 8330 const delta = newDetails.startSN + newDetails.skippedSegments - oldDetails.startSN; 8331 const oldFragments = oldDetails.fragments; 8332 const advancedOrStable = delta >= 0; 8333 let sliding = 0; 8334 if (advancedOrStable && delta < oldFragments.length) { 8335 sliding = oldFragments[delta].start; 8336 } else if (advancedOrStable && newDetails.startSN === oldDetails.endSN + 1) { 8337 sliding = oldDetails.fragmentEnd; 8338 } else if (advancedOrStable && matchingStableVariantOrRendition) { 8339 // align with expected position (updated playlist start sequence is past end sequence of last update) 8340 sliding = oldDetails.fragmentStart + delta * newDetails.levelTargetDuration; 8341 } else if (!newDetails.skippedSegments && newDetails.fragmentStart === 0) { 8342 // align new start with old (playlist switch has a sequence with no overlap and should not be used for alignment) 8343 sliding = oldDetails.fragmentStart; 8344 } else { 8345 // new details already has a sliding offset or has skipped segments 8346 return; 8347 } 8348 addSliding(newDetails, sliding); 8349} 8350function addSliding(details, sliding) { 8351 if (sliding) { 8352 const fragments = details.fragments; 8353 for (let i = details.skippedSegments; i < fragments.length; i++) { 8354 fragments[i].addStart(sliding); 8355 } 8356 if (details.fragmentHint) { 8357 details.fragmentHint.addStart(sliding); 8358 } 8359 } 8360} 8361function computeReloadInterval(newDetails, distanceToLiveEdgeMs = Infinity) { 8362 let reloadInterval = 1000 * newDetails.targetduration; 8363 if (newDetails.updated) { 8364 // Use last segment duration when shorter than target duration and near live edge 8365 const fragments = newDetails.fragments; 8366 const liveEdgeMaxTargetDurations = 4; 8367 if (fragments.length && reloadInterval * liveEdgeMaxTargetDurations > distanceToLiveEdgeMs) { 8368 const lastSegmentDuration = fragments[fragments.length - 1].duration * 1000; 8369 if (lastSegmentDuration < reloadInterval) { 8370 reloadInterval = lastSegmentDuration; 8371 } 8372 } 8373 } else { 8374 // estimate = 'miss half average'; 8375 // follow HLS Spec, If the client reloads a Playlist file and finds that it has not 8376 // changed then it MUST wait for a period of one-half the target 8377 // duration before retrying. 8378 reloadInterval /= 2; 8379 } 8380 return Math.round(reloadInterval); 8381} 8382function getFragmentWithSN(details, sn, fragCurrent) { 8383 if (!details) { 8384 return null; 8385 } 8386 let fragment = details.fragments[sn - details.startSN]; 8387 if (fragment) { 8388 return fragment; 8389 } 8390 fragment = details.fragmentHint; 8391 if (fragment && fragment.sn === sn) { 8392 return fragment; 8393 } 8394 if (sn < details.startSN && fragCurrent && fragCurrent.sn === sn) { 8395 return fragCurrent; 8396 } 8397 return null; 8398} 8399function getPartWith(details, sn, partIndex) { 8400 if (!details) { 8401 return null; 8402 } 8403 return findPart(details.partList, sn, partIndex); 8404} 8405function findPart(partList, sn, partIndex) { 8406 if (partList) { 8407 for (let i = partList.length; i--;) { 8408 const part = partList[i]; 8409 if (part.index === partIndex && part.fragment.sn === sn) { 8410 return part; 8411 } 8412 } 8413 } 8414 return null; 8415} 8416function reassignFragmentLevelIndexes(levels) { 8417 levels.forEach((level, index) => { 8418 var _level$details; 8419 (_level$details = level.details) == null || _level$details.fragments.forEach(fragment => { 8420 fragment.level = index; 8421 if (fragment.initSegment) { 8422 fragment.initSegment.level = index; 8423 } 8424 }); 8425 }); 8426} 8427function notEqualAfterStrippingQueries(uriBefore, uriAfter) { 8428 if (uriBefore !== uriAfter && uriAfter) { 8429 return stripQuery(uriBefore) !== stripQuery(uriAfter); 8430 } 8431 return false; 8432} 8433function stripQuery(uri) { 8434 return uri.replace(/\?[^?]*$/, ''); 8435} 8436 8437function findFirstFragWithCC(fragments, cc) { 8438 for (let i = 0, len = fragments.length; i < len; i++) { 8439 var _fragments$i; 8440 if (((_fragments$i = fragments[i]) == null ? void 0 : _fragments$i.cc) === cc) { 8441 return fragments[i]; 8442 } 8443 } 8444 return null; 8445} 8446function shouldAlignOnDiscontinuities(refDetails, details) { 8447 if (refDetails) { 8448 if (details.startCC < refDetails.endCC && details.endCC > refDetails.startCC) { 8449 return true; 8450 } 8451 } 8452 return false; 8453} 8454function adjustFragmentStart(frag, sliding) { 8455 const start = frag.start + sliding; 8456 frag.startPTS = start; 8457 frag.setStart(start); 8458 frag.endPTS = start + frag.duration; 8459} 8460function adjustSlidingStart(sliding, details) { 8461 // Update segments 8462 const fragments = details.fragments; 8463 for (let i = 0, len = fragments.length; i < len; i++) { 8464 adjustFragmentStart(fragments[i], sliding); 8465 } 8466 // Update LL-HLS parts at the end of the playlist 8467 if (details.fragmentHint) { 8468 adjustFragmentStart(details.fragmentHint, sliding); 8469 } 8470 details.alignedSliding = true; 8471} 8472 8473/** 8474 * Using the parameters of the last level, this function computes PTS' of the new fragments so that they form a 8475 * contiguous stream with the last fragments. 8476 * The PTS of a fragment lets Hls.js know where it fits into a stream - by knowing every PTS, we know which fragment to 8477 * download at any given time. PTS is normally computed when the fragment is demuxed, so taking this step saves us time 8478 * and an extra download. 8479 * @param lastLevel 8480 * @param details 8481 */ 8482function alignStream(switchDetails, details) { 8483 if (!switchDetails) { 8484 return; 8485 } 8486 alignDiscontinuities(details, switchDetails); 8487 if (!details.alignedSliding) { 8488 // If the PTS wasn't figured out via discontinuity sequence that means there was no CC increase within the level. 8489 // Aligning via Program Date Time should therefore be reliable, since PDT should be the same within the same 8490 // discontinuity sequence. 8491 alignMediaPlaylistByPDT(details, switchDetails); 8492 } 8493 if (!details.alignedSliding && !details.skippedSegments) { 8494 // Try to align on sn so that we pick a better start fragment. 8495 // Do not perform this on playlists with delta updates as this is only to align levels on switch 8496 // and adjustSliding only adjusts fragments after skippedSegments. 8497 adjustSliding(switchDetails, details, false); 8498 } 8499} 8500 8501/** 8502 * Ajust the start of fragments in `details` by the difference in time between fragments of the latest 8503 * shared discontinuity sequence change. 8504 * @param lastLevel - The details of the last loaded level 8505 * @param details - The details of the new level 8506 */ 8507function alignDiscontinuities(details, refDetails) { 8508 if (!shouldAlignOnDiscontinuities(refDetails, details)) { 8509 return; 8510 } 8511 const targetCC = Math.min(refDetails.endCC, details.endCC); 8512 const refFrag = findFirstFragWithCC(refDetails.fragments, targetCC); 8513 const frag = findFirstFragWithCC(details.fragments, targetCC); 8514 if (!refFrag || !frag) { 8515 return; 8516 } 8517 logger.log(`Aligning playlist at start of dicontinuity sequence ${targetCC}`); 8518 const delta = refFrag.start - frag.start; 8519 adjustSlidingStart(delta, details); 8520} 8521 8522/** 8523 * Ensures appropriate time-alignment between renditions based on PDT. 8524 * This function assumes the timelines represented in `refDetails` are accurate, including the PDTs 8525 * for the last discontinuity sequence number shared by both playlists when present, 8526 * and uses the "wallclock"/PDT timeline as a cross-reference to `details`, adjusting the presentation 8527 * times/timelines of `details` accordingly. 8528 * Given the asynchronous nature of fetches and initial loads of live `main` and audio/subtitle tracks, 8529 * the primary purpose of this function is to ensure the "local timelines" of audio/subtitle tracks 8530 * are aligned to the main/video timeline, using PDT as the cross-reference/"anchor" that should 8531 * be consistent across playlists, per the HLS spec. 8532 * @param details - The details of the rendition you'd like to time-align (e.g. an audio rendition). 8533 * @param refDetails - The details of the reference rendition with start and PDT times for alignment. 8534 */ 8535function alignMediaPlaylistByPDT(details, refDetails) { 8536 if (!details.hasProgramDateTime || !refDetails.hasProgramDateTime) { 8537 return; 8538 } 8539 const fragments = details.fragments; 8540 const refFragments = refDetails.fragments; 8541 if (!fragments.length || !refFragments.length) { 8542 return; 8543 } 8544 8545 // Calculate a delta to apply to all fragments according to the delta in PDT times and start times 8546 // of a fragment in the reference details, and a fragment in the target details of the same discontinuity. 8547 // If a fragment of the same discontinuity was not found use the middle fragment of both. 8548 let refFrag; 8549 let frag; 8550 const targetCC = Math.min(refDetails.endCC, details.endCC); 8551 if (refDetails.startCC < targetCC && details.startCC < targetCC) { 8552 refFrag = findFirstFragWithCC(refFragments, targetCC); 8553 frag = findFirstFragWithCC(fragments, targetCC); 8554 } 8555 if (!refFrag || !frag) { 8556 refFrag = refFragments[Math.floor(refFragments.length / 2)]; 8557 frag = findFirstFragWithCC(fragments, refFrag.cc) || fragments[Math.floor(fragments.length / 2)]; 8558 } 8559 const refPDT = refFrag.programDateTime; 8560 const targetPDT = frag.programDateTime; 8561 if (!refPDT || !targetPDT) { 8562 return; 8563 } 8564 const delta = (targetPDT - refPDT) / 1000 - (frag.start - refFrag.start); 8565 adjustSlidingStart(delta, details); 8566} 8567 8568function addEventListener(el, type, listener) { 8569 removeEventListener(el, type, listener); 8570 el.addEventListener(type, listener); 8571} 8572function removeEventListener(el, type, listener) { 8573 el.removeEventListener(type, listener); 8574} 8575 8576/** 8577 * TimeRanges to string helper 8578 */ 8579 8580const TimeRanges = { 8581 toString: function (r) { 8582 let log = ''; 8583 const len = r.length; 8584 for (let i = 0; i < len; i++) { 8585 log += `[${r.start(i).toFixed(3)}-${r.end(i).toFixed(3)}]`; 8586 } 8587 return log; 8588 } 8589}; 8590 8591const State = { 8592 STOPPED: 'STOPPED', 8593 IDLE: 'IDLE', 8594 KEY_LOADING: 'KEY_LOADING', 8595 FRAG_LOADING: 'FRAG_LOADING', 8596 FRAG_LOADING_WAITING_RETRY: 'FRAG_LOADING_WAITING_RETRY', 8597 WAITING_TRACK: 'WAITING_TRACK', 8598 PARSING: 'PARSING', 8599 PARSED: 'PARSED', 8600 ENDED: 'ENDED', 8601 ERROR: 'ERROR', 8602 WAITING_INIT_PTS: 'WAITING_INIT_PTS', 8603 WAITING_LEVEL: 'WAITING_LEVEL' 8604}; 8605class BaseStreamController extends TaskLoop { 8606 constructor(hls, fragmentTracker, keyLoader, logPrefix, playlistType) { 8607 super(logPrefix, hls.logger); 8608 this.hls = void 0; 8609 this.fragPrevious = null; 8610 this.fragCurrent = null; 8611 this.fragmentTracker = void 0; 8612 this.transmuxer = null; 8613 this._state = State.STOPPED; 8614 this.playlistType = void 0; 8615 this.media = null; 8616 this.mediaBuffer = null; 8617 this.config = void 0; 8618 this.bitrateTest = false; 8619 this.lastCurrentTime = 0; 8620 this.nextLoadPosition = 0; 8621 this.startPosition = 0; 8622 this.startTimeOffset = null; 8623 this.retryDate = 0; 8624 this.levels = null; 8625 this.fragmentLoader = void 0; 8626 this.keyLoader = void 0; 8627 this.levelLastLoaded = null; 8628 this.startFragRequested = false; 8629 this.decrypter = void 0; 8630 this.initPTS = []; 8631 this.buffering = true; 8632 this.loadingParts = false; 8633 this.loopSn = void 0; 8634 this.onMediaSeeking = () => { 8635 const { 8636 config, 8637 fragCurrent, 8638 media, 8639 mediaBuffer, 8640 state 8641 } = this; 8642 const currentTime = media ? media.currentTime : 0; 8643 const bufferInfo = BufferHelper.bufferInfo(mediaBuffer ? mediaBuffer : media, currentTime, config.maxBufferHole); 8644 const noFowardBuffer = !bufferInfo.len; 8645 this.log(`Media seeking to ${isFiniteNumber(currentTime) ? currentTime.toFixed(3) : currentTime}, state: ${state}, ${noFowardBuffer ? 'out of' : 'in'} buffer`); 8646 if (this.state === State.ENDED) { 8647 this.resetLoadingState(); 8648 } else if (fragCurrent) { 8649 // Seeking while frag load is in progress 8650 const tolerance = config.maxFragLookUpTolerance; 8651 const fragStartOffset = fragCurrent.start - tolerance; 8652 const fragEndOffset = fragCurrent.start + fragCurrent.duration + tolerance; 8653 // if seeking out of buffered range or into new one 8654 if (noFowardBuffer || fragEndOffset < bufferInfo.start || fragStartOffset > bufferInfo.end) { 8655 const pastFragment = currentTime > fragEndOffset; 8656 // if the seek position is outside the current fragment range 8657 if (currentTime < fragStartOffset || pastFragment) { 8658 if (pastFragment && fragCurrent.loader) { 8659 this.log(`Cancelling fragment load for seek (sn: ${fragCurrent.sn})`); 8660 fragCurrent.abortRequests(); 8661 this.resetLoadingState(); 8662 } 8663 this.fragPrevious = null; 8664 } 8665 } 8666 } 8667 if (media) { 8668 // Remove gap fragments 8669 this.fragmentTracker.removeFragmentsInRange(currentTime, Infinity, this.playlistType, true); 8670 8671 // Don't set lastCurrentTime with backward seeks (allows for frag selection with strict tolerances) 8672 const lastCurrentTime = this.lastCurrentTime; 8673 if (currentTime > lastCurrentTime) { 8674 this.lastCurrentTime = currentTime; 8675 } 8676 if (!this.loadingParts) { 8677 const bufferEnd = Math.max(bufferInfo.end, currentTime); 8678 const shouldLoadParts = this.shouldLoadParts(this.getLevelDetails(), bufferEnd); 8679 if (shouldLoadParts) { 8680 this.log(`LL-Part loading ON after seeking to ${currentTime.toFixed(2)} with buffer @${bufferEnd.toFixed(2)}`); 8681 this.loadingParts = shouldLoadParts; 8682 } 8683 } 8684 } 8685 8686 // in case seeking occurs although no media buffered, adjust startPosition and nextLoadPosition to seek target 8687 if (!this.hls.hasEnoughToStart) { 8688 this.log(`Setting ${noFowardBuffer ? 'startPosition' : 'nextLoadPosition'} to ${currentTime} for seek without enough to start`); 8689 this.nextLoadPosition = currentTime; 8690 if (noFowardBuffer) { 8691 this.startPosition = currentTime; 8692 } 8693 } 8694 if (noFowardBuffer && this.state === State.IDLE) { 8695 // Async tick to speed up processing 8696 this.tickImmediate(); 8697 } 8698 }; 8699 this.onMediaEnded = () => { 8700 // reset startPosition and lastCurrentTime to restart playback @ stream beginning 8701 this.log(`setting startPosition to 0 because media ended`); 8702 this.startPosition = this.lastCurrentTime = 0; 8703 }; 8704 this.playlistType = playlistType; 8705 this.hls = hls; 8706 this.fragmentLoader = new FragmentLoader(hls.config); 8707 this.keyLoader = keyLoader; 8708 this.fragmentTracker = fragmentTracker; 8709 this.config = hls.config; 8710 this.decrypter = new Decrypter(hls.config); 8711 } 8712 registerListeners() { 8713 const { 8714 hls 8715 } = this; 8716 hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 8717 hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 8718 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 8719 hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 8720 hls.on(Events.ERROR, this.onError, this); 8721 } 8722 unregisterListeners() { 8723 const { 8724 hls 8725 } = this; 8726 hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 8727 hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 8728 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 8729 hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 8730 hls.off(Events.ERROR, this.onError, this); 8731 } 8732 doTick() { 8733 this.onTickEnd(); 8734 } 8735 onTickEnd() {} 8736 startLoad(startPosition) {} 8737 stopLoad() { 8738 if (this.state === State.STOPPED) { 8739 return; 8740 } 8741 this.fragmentLoader.abort(); 8742 this.keyLoader.abort(this.playlistType); 8743 const frag = this.fragCurrent; 8744 if (frag != null && frag.loader) { 8745 frag.abortRequests(); 8746 this.fragmentTracker.removeFragment(frag); 8747 } 8748 this.resetTransmuxer(); 8749 this.fragCurrent = null; 8750 this.fragPrevious = null; 8751 this.clearInterval(); 8752 this.clearNextTick(); 8753 this.state = State.STOPPED; 8754 } 8755 get startPositionValue() { 8756 const { 8757 nextLoadPosition, 8758 startPosition 8759 } = this; 8760 if (startPosition === -1 && nextLoadPosition) { 8761 return nextLoadPosition; 8762 } 8763 return startPosition; 8764 } 8765 get bufferingEnabled() { 8766 return this.buffering; 8767 } 8768 pauseBuffering() { 8769 this.buffering = false; 8770 } 8771 resumeBuffering() { 8772 this.buffering = true; 8773 } 8774 get inFlightFrag() { 8775 return { 8776 frag: this.fragCurrent, 8777 state: this.state 8778 }; 8779 } 8780 _streamEnded(bufferInfo, levelDetails) { 8781 // Stream is never "ended" when playlist is live or media is detached 8782 if (levelDetails.live || !this.media) { 8783 return false; 8784 } 8785 // Stream is not "ended" when nothing is buffered past the start 8786 const bufferEnd = bufferInfo.end || 0; 8787 const timelineStart = this.config.timelineOffset || 0; 8788 if (bufferEnd <= timelineStart) { 8789 return false; 8790 } 8791 // Stream is not "ended" when there is a second buffered range starting before the end of the playlist 8792 const bufferedRanges = bufferInfo.buffered; 8793 if (this.config.maxBufferHole && bufferedRanges && bufferedRanges.length > 1) { 8794 // make sure bufferInfo accounts for any gaps 8795 bufferInfo = BufferHelper.bufferedInfo(bufferedRanges, bufferInfo.start, 0); 8796 } 8797 const nextStart = bufferInfo.nextStart; 8798 const hasSecondBufferedRange = nextStart && nextStart > timelineStart && nextStart < levelDetails.edge; 8799 if (hasSecondBufferedRange) { 8800 return false; 8801 } 8802 // Playhead is in unbuffered region. Marking EoS now could result in Safari failing to dispatch "ended" event following seek on start. 8803 if (this.media.currentTime < bufferInfo.start) { 8804 return false; 8805 } 8806 const partList = levelDetails.partList; 8807 // Since the last part isn't guaranteed to correspond to the last playlist segment for Low-Latency HLS, 8808 // check instead if the last part is buffered. 8809 if (partList != null && partList.length) { 8810 const lastPart = partList[partList.length - 1]; 8811 8812 // Checking the midpoint of the part for potential margin of error and related issues. 8813 // NOTE: Technically I believe parts could yield content that is < the computed duration (including potential a duration of 0) 8814 // and still be spec-compliant, so there may still be edge cases here. Likewise, there could be issues in end of stream 8815 // part mismatches for independent audio and video playlists/segments. 8816 const lastPartBuffered = BufferHelper.isBuffered(this.media, lastPart.start + lastPart.duration / 2); 8817 return lastPartBuffered; 8818 } 8819 const playlistType = levelDetails.fragments[levelDetails.fragments.length - 1].type; 8820 return this.fragmentTracker.isEndListAppended(playlistType); 8821 } 8822 getLevelDetails() { 8823 if (this.levels && this.levelLastLoaded !== null) { 8824 return this.levelLastLoaded.details; 8825 } 8826 } 8827 get timelineOffset() { 8828 const configuredTimelineOffset = this.config.timelineOffset; 8829 if (configuredTimelineOffset) { 8830 var _this$getLevelDetails; 8831 return ((_this$getLevelDetails = this.getLevelDetails()) == null ? void 0 : _this$getLevelDetails.appliedTimelineOffset) || configuredTimelineOffset; 8832 } 8833 return 0; 8834 } 8835 onMediaAttached(event, data) { 8836 const media = this.media = this.mediaBuffer = data.media; 8837 addEventListener(media, 'seeking', this.onMediaSeeking); 8838 addEventListener(media, 'ended', this.onMediaEnded); 8839 const config = this.config; 8840 if (this.levels && config.autoStartLoad && this.state === State.STOPPED) { 8841 this.startLoad(config.startPosition); 8842 } 8843 } 8844 onMediaDetaching(event, data) { 8845 const transferringMedia = !!data.transferMedia; 8846 const media = this.media; 8847 if (media === null) { 8848 return; 8849 } 8850 if (media.ended) { 8851 this.log('MSE detaching and video ended, reset startPosition'); 8852 this.startPosition = this.lastCurrentTime = 0; 8853 } 8854 8855 // remove video listeners 8856 removeEventListener(media, 'seeking', this.onMediaSeeking); 8857 removeEventListener(media, 'ended', this.onMediaEnded); 8858 if (this.keyLoader && !transferringMedia) { 8859 this.keyLoader.detach(); 8860 } 8861 this.media = this.mediaBuffer = null; 8862 this.loopSn = undefined; 8863 if (transferringMedia) { 8864 this.resetLoadingState(); 8865 this.resetTransmuxer(); 8866 return; 8867 } 8868 this.loadingParts = false; 8869 this.fragmentTracker.removeAllFragments(); 8870 this.stopLoad(); 8871 } 8872 onManifestLoading() { 8873 this.initPTS = []; 8874 this.levels = this.levelLastLoaded = this.fragCurrent = null; 8875 this.lastCurrentTime = this.startPosition = 0; 8876 this.startFragRequested = false; 8877 } 8878 onError(event, data) {} 8879 onManifestLoaded(event, data) { 8880 this.startTimeOffset = data.startTimeOffset; 8881 } 8882 onHandlerDestroying() { 8883 this.stopLoad(); 8884 if (this.transmuxer) { 8885 this.transmuxer.destroy(); 8886 this.transmuxer = null; 8887 } 8888 super.onHandlerDestroying(); 8889 // @ts-ignore 8890 this.hls = this.onMediaSeeking = this.onMediaEnded = null; 8891 } 8892 onHandlerDestroyed() { 8893 this.state = State.STOPPED; 8894 if (this.fragmentLoader) { 8895 this.fragmentLoader.destroy(); 8896 } 8897 if (this.keyLoader) { 8898 this.keyLoader.destroy(); 8899 } 8900 if (this.decrypter) { 8901 this.decrypter.destroy(); 8902 } 8903 this.hls = this.log = this.warn = this.decrypter = this.keyLoader = this.fragmentLoader = this.fragmentTracker = null; 8904 super.onHandlerDestroyed(); 8905 } 8906 loadFragment(frag, level, targetBufferTime) { 8907 this.startFragRequested = true; 8908 this._loadFragForPlayback(frag, level, targetBufferTime); 8909 } 8910 _loadFragForPlayback(fragment, level, targetBufferTime) { 8911 const progressCallback = data => { 8912 const frag = data.frag; 8913 if (this.fragContextChanged(frag)) { 8914 this.warn(`${frag.type} sn: ${frag.sn}${data.part ? ' part: ' + data.part.index : ''} of ${this.fragInfo(frag, false, data.part)}) was dropped during download.`); 8915 this.fragmentTracker.removeFragment(frag); 8916 return; 8917 } 8918 frag.stats.chunkCount++; 8919 this._handleFragmentLoadProgress(data); 8920 }; 8921 this._doFragLoad(fragment, level, targetBufferTime, progressCallback).then(data => { 8922 if (!data) { 8923 // if we're here we probably needed to backtrack or are waiting for more parts 8924 return; 8925 } 8926 const state = this.state; 8927 const frag = data.frag; 8928 if (this.fragContextChanged(frag)) { 8929 if (state === State.FRAG_LOADING || !this.fragCurrent && state === State.PARSING) { 8930 this.fragmentTracker.removeFragment(frag); 8931 this.state = State.IDLE; 8932 } 8933 return; 8934 } 8935 if ('payload' in data) { 8936 this.log(`Loaded ${frag.type} sn: ${frag.sn} of ${this.playlistLabel()} ${frag.level}`); 8937 this.hls.trigger(Events.FRAG_LOADED, data); 8938 } 8939 8940 // Pass through the whole payload; controllers not implementing progressive loading receive data from this callback 8941 this._handleFragmentLoadComplete(data); 8942 }).catch(reason => { 8943 if (this.state === State.STOPPED || this.state === State.ERROR) { 8944 return; 8945 } 8946 this.warn(`Frag error: ${(reason == null ? void 0 : reason.message) || reason}`); 8947 this.resetFragmentLoading(fragment); 8948 }); 8949 } 8950 clearTrackerIfNeeded(frag) { 8951 var _this$mediaBuffer; 8952 const { 8953 fragmentTracker 8954 } = this; 8955 const fragState = fragmentTracker.getState(frag); 8956 if (fragState === FragmentState.APPENDING) { 8957 // Lower the max buffer length and try again 8958 const playlistType = frag.type; 8959 const bufferedInfo = this.getFwdBufferInfo(this.mediaBuffer, playlistType); 8960 const minForwardBufferLength = Math.max(frag.duration, bufferedInfo ? bufferedInfo.len : this.config.maxBufferLength); 8961 // If backtracking, always remove from the tracker without reducing max buffer length 8962 const backtrackFragment = this.backtrackFragment; 8963 const backtracked = backtrackFragment ? frag.sn - backtrackFragment.sn : 0; 8964 if (backtracked === 1 || this.reduceMaxBufferLength(minForwardBufferLength, frag.duration)) { 8965 fragmentTracker.removeFragment(frag); 8966 } 8967 } else if (((_this$mediaBuffer = this.mediaBuffer) == null ? void 0 : _this$mediaBuffer.buffered.length) === 0) { 8968 // Stop gap for bad tracker / buffer flush behavior 8969 fragmentTracker.removeAllFragments(); 8970 } else if (fragmentTracker.hasParts(frag.type)) { 8971 // In low latency mode, remove fragments for which only some parts were buffered 8972 fragmentTracker.detectPartialFragments({ 8973 frag, 8974 part: null, 8975 stats: frag.stats, 8976 id: frag.type 8977 }); 8978 if (fragmentTracker.getState(frag) === FragmentState.PARTIAL) { 8979 fragmentTracker.removeFragment(frag); 8980 } 8981 } 8982 } 8983 checkLiveUpdate(details) { 8984 if (details.updated && !details.live) { 8985 // Live stream ended, update fragment tracker 8986 const lastFragment = details.fragments[details.fragments.length - 1]; 8987 this.fragmentTracker.detectPartialFragments({ 8988 frag: lastFragment, 8989 part: null, 8990 stats: lastFragment.stats, 8991 id: lastFragment.type 8992 }); 8993 } 8994 if (!details.fragments[0]) { 8995 details.deltaUpdateFailed = true; 8996 } 8997 } 8998 waitForLive(levelInfo) { 8999 const details = levelInfo.details; 9000 return (details == null ? void 0 : details.live) && details.type !== 'EVENT' && (this.levelLastLoaded !== levelInfo || details.expired); 9001 } 9002 flushMainBuffer(startOffset, endOffset, type = null) { 9003 if (!(startOffset - endOffset)) { 9004 return; 9005 } 9006 // When alternate audio is playing, the audio-stream-controller is responsible for the audio buffer. Otherwise, 9007 // passing a null type flushes both buffers 9008 const flushScope = { 9009 startOffset, 9010 endOffset, 9011 type 9012 }; 9013 this.hls.trigger(Events.BUFFER_FLUSHING, flushScope); 9014 } 9015 _loadInitSegment(fragment, level) { 9016 this._doFragLoad(fragment, level).then(data => { 9017 const frag = data == null ? void 0 : data.frag; 9018 if (!frag || this.fragContextChanged(frag) || !this.levels) { 9019 throw new Error('init load aborted'); 9020 } 9021 return data; 9022 }).then(data => { 9023 const { 9024 hls 9025 } = this; 9026 const { 9027 frag, 9028 payload 9029 } = data; 9030 const decryptData = frag.decryptdata; 9031 9032 // check to see if the payload needs to be decrypted 9033 if (payload && payload.byteLength > 0 && decryptData != null && decryptData.key && decryptData.iv && isFullSegmentEncryption(decryptData.method)) { 9034 const startTime = self.performance.now(); 9035 // decrypt init segment data 9036 return this.decrypter.decrypt(new Uint8Array(payload), decryptData.key.buffer, decryptData.iv.buffer, getAesModeFromFullSegmentMethod(decryptData.method)).catch(err => { 9037 hls.trigger(Events.ERROR, { 9038 type: ErrorTypes.MEDIA_ERROR, 9039 details: ErrorDetails.FRAG_DECRYPT_ERROR, 9040 fatal: false, 9041 error: err, 9042 reason: err.message, 9043 frag 9044 }); 9045 throw err; 9046 }).then(decryptedData => { 9047 const endTime = self.performance.now(); 9048 hls.trigger(Events.FRAG_DECRYPTED, { 9049 frag, 9050 payload: decryptedData, 9051 stats: { 9052 tstart: startTime, 9053 tdecrypt: endTime 9054 } 9055 }); 9056 data.payload = decryptedData; 9057 return this.completeInitSegmentLoad(data); 9058 }); 9059 } 9060 return this.completeInitSegmentLoad(data); 9061 }).catch(reason => { 9062 if (this.state === State.STOPPED || this.state === State.ERROR) { 9063 return; 9064 } 9065 this.warn(reason); 9066 this.resetFragmentLoading(fragment); 9067 }); 9068 } 9069 completeInitSegmentLoad(data) { 9070 const { 9071 levels 9072 } = this; 9073 if (!levels) { 9074 throw new Error('init load aborted, missing levels'); 9075 } 9076 const stats = data.frag.stats; 9077 if (this.state !== State.STOPPED) { 9078 this.state = State.IDLE; 9079 } 9080 data.frag.data = new Uint8Array(data.payload); 9081 stats.parsing.start = stats.buffering.start = self.performance.now(); 9082 stats.parsing.end = stats.buffering.end = self.performance.now(); 9083 this.tick(); 9084 } 9085 unhandledEncryptionError(initSegment, frag) { 9086 var _tracks$audio, _tracks$video; 9087 const tracks = initSegment.tracks; 9088 if (tracks && !frag.encrypted && ((_tracks$audio = tracks.audio) != null && _tracks$audio.encrypted || (_tracks$video = tracks.video) != null && _tracks$video.encrypted) && (!this.config.emeEnabled || !this.keyLoader.emeController)) { 9089 const media = this.media; 9090 const error = new Error(`Encrypted track with no key in ${this.fragInfo(frag)} (media ${media ? 'attached mediaKeys: ' + media.mediaKeys : 'detached'})` ); 9091 this.warn(error.message); 9092 // Ignore if media is detached or mediaKeys are set 9093 if (!media || media.mediaKeys) { 9094 return false; 9095 } 9096 this.hls.trigger(Events.ERROR, { 9097 type: ErrorTypes.KEY_SYSTEM_ERROR, 9098 details: ErrorDetails.KEY_SYSTEM_NO_KEYS, 9099 fatal: false, 9100 error, 9101 frag 9102 }); 9103 this.resetTransmuxer(); 9104 return true; 9105 } 9106 return false; 9107 } 9108 fragContextChanged(frag) { 9109 const { 9110 fragCurrent 9111 } = this; 9112 return !frag || !fragCurrent || frag.sn !== fragCurrent.sn || frag.level !== fragCurrent.level; 9113 } 9114 fragBufferedComplete(frag, part) { 9115 const media = this.mediaBuffer ? this.mediaBuffer : this.media; 9116 this.log(`Buffered ${frag.type} sn: ${frag.sn}${part ? ' part: ' + part.index : ''} of ${this.fragInfo(frag, false, part)} > buffer:${media ? TimeRanges.toString(BufferHelper.getBuffered(media)) : '(detached)'})`); 9117 if (isMediaFragment(frag)) { 9118 var _this$levels; 9119 if (frag.type !== PlaylistLevelType.SUBTITLE) { 9120 const el = frag.elementaryStreams; 9121 if (!Object.keys(el).some(type => !!el[type])) { 9122 // empty segment 9123 this.state = State.IDLE; 9124 return; 9125 } 9126 } 9127 const level = (_this$levels = this.levels) == null ? void 0 : _this$levels[frag.level]; 9128 if (level != null && level.fragmentError) { 9129 this.log(`Resetting level fragment error count of ${level.fragmentError} on frag buffered`); 9130 level.fragmentError = 0; 9131 } 9132 } 9133 this.state = State.IDLE; 9134 } 9135 _handleFragmentLoadComplete(fragLoadedEndData) { 9136 const { 9137 transmuxer 9138 } = this; 9139 if (!transmuxer) { 9140 return; 9141 } 9142 const { 9143 frag, 9144 part, 9145 partsLoaded 9146 } = fragLoadedEndData; 9147 // If we did not load parts, or loaded all parts, we have complete (not partial) fragment data 9148 const complete = !partsLoaded || partsLoaded.length === 0 || partsLoaded.some(fragLoaded => !fragLoaded); 9149 const chunkMeta = new ChunkMetadata(frag.level, frag.sn, frag.stats.chunkCount + 1, 0, part ? part.index : -1, !complete); 9150 transmuxer.flush(chunkMeta); 9151 } 9152 _handleFragmentLoadProgress(frag) {} 9153 _doFragLoad(frag, level, targetBufferTime = null, progressCallback) { 9154 var _frag$decryptdata; 9155 this.fragCurrent = frag; 9156 const details = level.details; 9157 if (!this.levels || !details) { 9158 throw new Error(`frag load aborted, missing level${details ? '' : ' detail'}s`); 9159 } 9160 let keyLoadingPromise = null; 9161 if (frag.encrypted && !((_frag$decryptdata = frag.decryptdata) != null && _frag$decryptdata.key)) { 9162 this.log(`Loading key for ${frag.sn} of [${details.startSN}-${details.endSN}], ${this.playlistLabel()} ${frag.level}`); 9163 this.state = State.KEY_LOADING; 9164 this.fragCurrent = frag; 9165 keyLoadingPromise = this.keyLoader.load(frag).then(keyLoadedData => { 9166 if (!this.fragContextChanged(keyLoadedData.frag)) { 9167 this.hls.trigger(Events.KEY_LOADED, keyLoadedData); 9168 if (this.state === State.KEY_LOADING) { 9169 this.state = State.IDLE; 9170 } 9171 return keyLoadedData; 9172 } 9173 }); 9174 this.hls.trigger(Events.KEY_LOADING, { 9175 frag 9176 }); 9177 if (this.fragCurrent === null) { 9178 this.log(`context changed in KEY_LOADING`); 9179 return Promise.resolve(null); 9180 } 9181 } else if (!frag.encrypted) { 9182 keyLoadingPromise = this.keyLoader.loadClear(frag, details.encryptedFragments, this.startFragRequested); 9183 if (keyLoadingPromise) { 9184 this.log(`[eme] blocking frag load until media-keys acquired`); 9185 } 9186 } 9187 const fragPrevious = this.fragPrevious; 9188 if (isMediaFragment(frag) && (!fragPrevious || frag.sn !== fragPrevious.sn)) { 9189 const shouldLoadParts = this.shouldLoadParts(level.details, frag.end); 9190 if (shouldLoadParts !== this.loadingParts) { 9191 this.log(`LL-Part loading ${shouldLoadParts ? 'ON' : 'OFF'} loading sn ${fragPrevious == null ? void 0 : fragPrevious.sn}->${frag.sn}`); 9192 this.loadingParts = shouldLoadParts; 9193 } 9194 } 9195 targetBufferTime = Math.max(frag.start, targetBufferTime || 0); 9196 if (this.loadingParts && isMediaFragment(frag)) { 9197 const partList = details.partList; 9198 if (partList && progressCallback) { 9199 if (targetBufferTime > details.fragmentEnd && details.fragmentHint) { 9200 frag = details.fragmentHint; 9201 } 9202 const partIndex = this.getNextPart(partList, frag, targetBufferTime); 9203 if (partIndex > -1) { 9204 const part = partList[partIndex]; 9205 frag = this.fragCurrent = part.fragment; 9206 this.log(`Loading ${frag.type} sn: ${frag.sn} part: ${part.index} (${partIndex}/${partList.length - 1}) of ${this.fragInfo(frag, false, part)}) cc: ${frag.cc} [${details.startSN}-${details.endSN}], target: ${parseFloat(targetBufferTime.toFixed(3))}`); 9207 this.nextLoadPosition = part.start + part.duration; 9208 this.state = State.FRAG_LOADING; 9209 let _result; 9210 if (keyLoadingPromise) { 9211 _result = keyLoadingPromise.then(keyLoadedData => { 9212 if (!keyLoadedData || this.fragContextChanged(keyLoadedData.frag)) { 9213 return null; 9214 } 9215 return this.doFragPartsLoad(frag, part, level, progressCallback); 9216 }).catch(error => this.handleFragLoadError(error)); 9217 } else { 9218 _result = this.doFragPartsLoad(frag, part, level, progressCallback).catch(error => this.handleFragLoadError(error)); 9219 } 9220 this.hls.trigger(Events.FRAG_LOADING, { 9221 frag, 9222 part, 9223 targetBufferTime 9224 }); 9225 if (this.fragCurrent === null) { 9226 return Promise.reject(new Error(`frag load aborted, context changed in FRAG_LOADING parts`)); 9227 } 9228 return _result; 9229 } else if (!frag.url || this.loadedEndOfParts(partList, targetBufferTime)) { 9230 // Fragment hint has no parts 9231 return Promise.resolve(null); 9232 } 9233 } 9234 } 9235 if (isMediaFragment(frag) && this.loadingParts) { 9236 var _details$partList; 9237 this.log(`LL-Part loading OFF after next part miss @${targetBufferTime.toFixed(2)} Check buffer at sn: ${frag.sn} loaded parts: ${(_details$partList = details.partList) == null ? void 0 : _details$partList.filter(p => p.loaded).map(p => `[${p.start}-${p.end}]`)}`); 9238 this.loadingParts = false; 9239 } else if (!frag.url) { 9240 // Selected fragment hint for part but not loading parts 9241 return Promise.resolve(null); 9242 } 9243 this.log(`Loading ${frag.type} sn: ${frag.sn} of ${this.fragInfo(frag, false)}) cc: ${frag.cc} ${'[' + details.startSN + '-' + details.endSN + ']'}, target: ${parseFloat(targetBufferTime.toFixed(3))}`); 9244 // Don't update nextLoadPosition for fragments which are not buffered 9245 if (isFiniteNumber(frag.sn) && !this.bitrateTest) { 9246 this.nextLoadPosition = frag.start + frag.duration; 9247 } 9248 this.state = State.FRAG_LOADING; 9249 9250 // Load key before streaming fragment data 9251 const dataOnProgress = this.config.progressive && frag.type !== PlaylistLevelType.SUBTITLE; 9252 let result; 9253 if (dataOnProgress && keyLoadingPromise) { 9254 result = keyLoadingPromise.then(keyLoadedData => { 9255 if (!keyLoadedData || this.fragContextChanged(keyLoadedData.frag)) { 9256 return null; 9257 } 9258 return this.fragmentLoader.load(frag, progressCallback); 9259 }).catch(error => this.handleFragLoadError(error)); 9260 } else { 9261 // load unencrypted fragment data with progress event, 9262 // or handle fragment result after key and fragment are finished loading 9263 result = Promise.all([this.fragmentLoader.load(frag, dataOnProgress ? progressCallback : undefined), keyLoadingPromise]).then(([fragLoadedData]) => { 9264 if (!dataOnProgress && progressCallback) { 9265 progressCallback(fragLoadedData); 9266 } 9267 return fragLoadedData; 9268 }).catch(error => this.handleFragLoadError(error)); 9269 } 9270 this.hls.trigger(Events.FRAG_LOADING, { 9271 frag, 9272 targetBufferTime 9273 }); 9274 if (this.fragCurrent === null) { 9275 return Promise.reject(new Error(`frag load aborted, context changed in FRAG_LOADING`)); 9276 } 9277 return result; 9278 } 9279 doFragPartsLoad(frag, fromPart, level, progressCallback) { 9280 return new Promise((resolve, reject) => { 9281 var _level$details; 9282 const partsLoaded = []; 9283 const initialPartList = (_level$details = level.details) == null ? void 0 : _level$details.partList; 9284 const loadPart = part => { 9285 this.fragmentLoader.loadPart(frag, part, progressCallback).then(partLoadedData => { 9286 partsLoaded[part.index] = partLoadedData; 9287 const loadedPart = partLoadedData.part; 9288 this.hls.trigger(Events.FRAG_LOADED, partLoadedData); 9289 const nextPart = getPartWith(level.details, frag.sn, part.index + 1) || findPart(initialPartList, frag.sn, part.index + 1); 9290 if (nextPart) { 9291 loadPart(nextPart); 9292 } else { 9293 return resolve({ 9294 frag, 9295 part: loadedPart, 9296 partsLoaded 9297 }); 9298 } 9299 }).catch(reject); 9300 }; 9301 loadPart(fromPart); 9302 }); 9303 } 9304 handleFragLoadError(error) { 9305 if ('data' in error) { 9306 const data = error.data; 9307 if (data.frag && data.details === ErrorDetails.INTERNAL_ABORTED) { 9308 this.handleFragLoadAborted(data.frag, data.part); 9309 } else if (data.frag && data.type === ErrorTypes.KEY_SYSTEM_ERROR) { 9310 data.frag.abortRequests(); 9311 this.resetStartWhenNotLoaded(); 9312 this.resetFragmentLoading(data.frag); 9313 } else { 9314 this.hls.trigger(Events.ERROR, data); 9315 } 9316 } else { 9317 this.hls.trigger(Events.ERROR, { 9318 type: ErrorTypes.OTHER_ERROR, 9319 details: ErrorDetails.INTERNAL_EXCEPTION, 9320 err: error, 9321 error, 9322 fatal: true 9323 }); 9324 } 9325 return null; 9326 } 9327 _handleTransmuxerFlush(chunkMeta) { 9328 const context = this.getCurrentContext(chunkMeta); 9329 if (!context || this.state !== State.PARSING) { 9330 if (!this.fragCurrent && this.state !== State.STOPPED && this.state !== State.ERROR) { 9331 this.state = State.IDLE; 9332 } 9333 return; 9334 } 9335 const { 9336 frag, 9337 part, 9338 level 9339 } = context; 9340 const now = self.performance.now(); 9341 frag.stats.parsing.end = now; 9342 if (part) { 9343 part.stats.parsing.end = now; 9344 } 9345 // See if part loading should be disabled/enabled based on buffer and playback position. 9346 const levelDetails = this.getLevelDetails(); 9347 const loadingPartsAtEdge = levelDetails && frag.sn > levelDetails.endSN; 9348 const shouldLoadParts = loadingPartsAtEdge || this.shouldLoadParts(levelDetails, frag.end); 9349 if (shouldLoadParts !== this.loadingParts) { 9350 this.log(`LL-Part loading ${shouldLoadParts ? 'ON' : 'OFF'} after parsing segment ending @${frag.end.toFixed(2)}`); 9351 this.loadingParts = shouldLoadParts; 9352 } 9353 this.updateLevelTiming(frag, part, level, chunkMeta.partial); 9354 } 9355 shouldLoadParts(details, bufferEnd) { 9356 if (this.config.lowLatencyMode) { 9357 if (!details) { 9358 return this.loadingParts; 9359 } 9360 if (details.partList) { 9361 var _details$fragmentHint; 9362 // Buffer must be ahead of first part + duration of parts after last segment 9363 // and playback must be at or past segment adjacent to part list 9364 const firstPart = details.partList[0]; 9365 // Loading of VTT subtitle parts is not implemented in subtitle-stream-controller (#7460) 9366 if (firstPart.fragment.type === PlaylistLevelType.SUBTITLE) { 9367 return false; 9368 } 9369 const safePartStart = firstPart.end + (((_details$fragmentHint = details.fragmentHint) == null ? void 0 : _details$fragmentHint.duration) || 0); 9370 if (bufferEnd >= safePartStart) { 9371 var _this$media; 9372 const playhead = this.hls.hasEnoughToStart ? ((_this$media = this.media) == null ? void 0 : _this$media.currentTime) || this.lastCurrentTime : this.getLoadPosition(); 9373 if (playhead > firstPart.start - firstPart.fragment.duration) { 9374 return true; 9375 } 9376 } 9377 } 9378 } 9379 return false; 9380 } 9381 getCurrentContext(chunkMeta) { 9382 const { 9383 levels, 9384 fragCurrent 9385 } = this; 9386 const { 9387 level: levelIndex, 9388 sn, 9389 part: partIndex 9390 } = chunkMeta; 9391 if (!(levels != null && levels[levelIndex])) { 9392 this.warn(`Levels object was unset while buffering fragment ${sn} of ${this.playlistLabel()} ${levelIndex}. The current chunk will not be buffered.`); 9393 return null; 9394 } 9395 const level = levels[levelIndex]; 9396 const levelDetails = level.details; 9397 const part = partIndex > -1 ? getPartWith(levelDetails, sn, partIndex) : null; 9398 const frag = part ? part.fragment : getFragmentWithSN(levelDetails, sn, fragCurrent); 9399 if (!frag) { 9400 return null; 9401 } 9402 if (fragCurrent && fragCurrent !== frag) { 9403 frag.stats = fragCurrent.stats; 9404 } 9405 return { 9406 frag, 9407 part, 9408 level 9409 }; 9410 } 9411 bufferFragmentData(data, frag, part, chunkMeta, noBacktracking) { 9412 if (this.state !== State.PARSING) { 9413 return; 9414 } 9415 const { 9416 data1, 9417 data2 9418 } = data; 9419 let buffer = data1; 9420 if (data2) { 9421 // Combine the moof + mdat so that we buffer with a single append 9422 buffer = appendUint8Array(data1, data2); 9423 } 9424 if (!buffer.length) { 9425 return; 9426 } 9427 const offsetTimestamp = this.initPTS[frag.cc]; 9428 const offset = offsetTimestamp ? -offsetTimestamp.baseTime / offsetTimestamp.timescale : undefined; 9429 const segment = { 9430 type: data.type, 9431 frag, 9432 part, 9433 chunkMeta, 9434 offset, 9435 parent: frag.type, 9436 data: buffer 9437 }; 9438 this.hls.trigger(Events.BUFFER_APPENDING, segment); 9439 if (data.dropped && data.independent && !part) { 9440 if (noBacktracking) { 9441 return; 9442 } 9443 // Clear buffer so that we reload previous segments sequentially if required 9444 this.flushBufferGap(frag); 9445 } 9446 } 9447 flushBufferGap(frag) { 9448 const media = this.media; 9449 if (!media) { 9450 return; 9451 } 9452 // If currentTime is not buffered, clear the back buffer so that we can backtrack as much as needed 9453 if (!BufferHelper.isBuffered(media, media.currentTime)) { 9454 this.flushMainBuffer(0, frag.start); 9455 return; 9456 } 9457 // Remove back-buffer without interrupting playback to allow back tracking 9458 const currentTime = media.currentTime; 9459 const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0); 9460 const fragDuration = frag.duration; 9461 const segmentFraction = Math.min(this.config.maxFragLookUpTolerance * 2, fragDuration * 0.25); 9462 const start = Math.max(Math.min(frag.start - segmentFraction, bufferInfo.end - segmentFraction), currentTime + segmentFraction); 9463 if (frag.start - start > segmentFraction) { 9464 this.flushMainBuffer(start, frag.start); 9465 } 9466 } 9467 getFwdBufferInfo(bufferable, type) { 9468 var _this$media2; 9469 const pos = this.getLoadPosition(); 9470 if (!isFiniteNumber(pos)) { 9471 return null; 9472 } 9473 const backwardSeek = this.lastCurrentTime > pos; 9474 const maxBufferHole = backwardSeek || (_this$media2 = this.media) != null && _this$media2.paused ? 0 : this.config.maxBufferHole; 9475 return this.getFwdBufferInfoAtPos(bufferable, pos, type, maxBufferHole); 9476 } 9477 getFwdBufferInfoAtPos(bufferable, pos, type, maxBufferHole) { 9478 const bufferInfo = BufferHelper.bufferInfo(bufferable, pos, maxBufferHole); 9479 // Workaround flaw in getting forward buffer when maxBufferHole is smaller than gap at current pos 9480 if (bufferInfo.len === 0 && bufferInfo.nextStart !== undefined) { 9481 const bufferedFragAtPos = this.fragmentTracker.getBufferedFrag(pos, type); 9482 if (bufferedFragAtPos && (bufferInfo.nextStart <= bufferedFragAtPos.end || bufferedFragAtPos.gap)) { 9483 const gapDuration = Math.max(Math.min(bufferInfo.nextStart, bufferedFragAtPos.end) - pos, maxBufferHole); 9484 return BufferHelper.bufferInfo(bufferable, pos, gapDuration); 9485 } 9486 } 9487 return bufferInfo; 9488 } 9489 getMaxBufferLength(levelBitrate) { 9490 const { 9491 config 9492 } = this; 9493 let maxBufLen; 9494 if (levelBitrate) { 9495 maxBufLen = Math.max(8 * config.maxBufferSize / levelBitrate, config.maxBufferLength); 9496 } else { 9497 maxBufLen = config.maxBufferLength; 9498 }
vendor: 21,189 bytes, lines 9499-9977
9499 return Math.min(maxBufLen, config.maxMaxBufferLength); 9500 } 9501 exceedsMaxBuffer(bufferInfo, maxBufLen, selected) { 9502 const nextStart = bufferInfo.nextStart; 9503 if (nextStart && selected.start > nextStart) { 9504 const bufferedRanges = bufferInfo.buffered; 9505 if (bufferedRanges) { 9506 let fullBufferLength = bufferInfo.len; 9507 const bufferedIndex = bufferInfo.bufferedIndex; 9508 for (let i = bufferedRanges.length - 1; i > bufferedIndex; i--) { 9509 if (bufferedRanges[i].start < selected.start) { 9510 fullBufferLength += bufferedRanges[i].end - bufferedRanges[i].start; 9511 } 9512 } 9513 return fullBufferLength >= maxBufLen; 9514 } 9515 } 9516 return false; 9517 } 9518 reduceMaxBufferLength(threshold, fragDuration) { 9519 const config = this.config; 9520 const minLength = Math.max(Math.min(threshold - fragDuration, config.maxBufferLength), fragDuration); 9521 const reducedLength = Math.max(threshold - fragDuration * 3, config.maxMaxBufferLength / 2, minLength); 9522 if (reducedLength >= minLength) { 9523 // reduce max buffer length as it might be too high. we do this to avoid loop flushing ... 9524 config.maxMaxBufferLength = reducedLength; 9525 this.warn(`Reduce max buffer length to ${reducedLength}s`); 9526 return true; 9527 } 9528 return false; 9529 } 9530 getAppendedFrag(position, playlistType = PlaylistLevelType.MAIN) { 9531 const fragOrPart = this.fragmentTracker ? this.fragmentTracker.getAppendedFrag(position, playlistType) : null; 9532 if (fragOrPart && 'fragment' in fragOrPart) { 9533 return fragOrPart.fragment; 9534 } 9535 return fragOrPart; 9536 } 9537 getNextFragment(pos, levelDetails) { 9538 const fragments = levelDetails.fragments; 9539 const fragLen = fragments.length; 9540 if (!fragLen) { 9541 return null; 9542 } 9543 9544 // find fragment index, contiguous with end of buffer position 9545 const { 9546 config 9547 } = this; 9548 const playlistStart = levelDetails.fragmentStart; 9549 const canLoadParts = config.lowLatencyMode && !!levelDetails.partList; 9550 let frag = null; 9551 if (levelDetails.live) { 9552 const initialLiveManifestSize = config.initialLiveManifestSize; 9553 if (fragLen < initialLiveManifestSize) { 9554 this.warn(`Not enough fragments to start playback (have: ${fragLen}, need: ${initialLiveManifestSize})`); 9555 return null; 9556 } 9557 // The real fragment start times for a live stream are only known after the PTS range for that level is known. 9558 // In order to discover the range, we load the best matching fragment for that level and demux it. 9559 // Do not load using live logic if the starting frag is requested - we want to use getFragmentAtPosition() so that 9560 // we get the fragment matching that start time 9561 if (!levelDetails.PTSKnown && !this.startFragRequested && this.startPosition === -1 || pos < playlistStart) { 9562 var _frag; 9563 if (canLoadParts && !this.loadingParts) { 9564 this.log(`LL-Part loading ON for initial live fragment`); 9565 this.loadingParts = true; 9566 } 9567 frag = this.getInitialLiveFragment(levelDetails); 9568 const configValue = this.config.startPosition; 9569 const mainStart = this.hls.startPosition; 9570 const liveSyncPosition = this.hls.liveSyncPosition; 9571 const fragStart = ((_frag = frag) == null ? void 0 : _frag.start) || 0; 9572 let startPosition; 9573 let reason; 9574 if (mainStart !== -1 && mainStart >= playlistStart) { 9575 startPosition = mainStart; 9576 reason = mainStart === configValue ? 'config' : 'next load start'; 9577 } else if (liveSyncPosition !== null) { 9578 startPosition = liveSyncPosition; 9579 reason = 'live edge'; 9580 } else { 9581 startPosition = pos; 9582 reason = 'buffer pos'; 9583 } 9584 if (startPosition < fragStart) { 9585 startPosition = fragStart; 9586 reason = 'live frag start'; 9587 } 9588 if (startPosition < playlistStart) { 9589 startPosition = playlistStart; 9590 reason = 'playlist start'; 9591 } 9592 if (this.startPosition != startPosition || this.nextLoadPosition != startPosition) { 9593 this.log(`Setting startPosition to ${startPosition.toFixed(3)} ${mainStart === -1 ? '' : `(from ${configValue}) `}based on ${reason}. live edge: ${liveSyncPosition} live frag start: ${fragStart.toFixed(3)} playlist start: ${playlistStart.toFixed(3)} buffer pos: ${pos}`); 9594 this.startPosition = this.nextLoadPosition = startPosition; 9595 } 9596 } 9597 } else if (pos <= playlistStart) { 9598 // VoD playlist: if loadPosition before start of playlist, load first fragment 9599 frag = fragments[0]; 9600 } 9601 9602 // If we haven't run into any special cases already, just load the fragment most closely matching the requested position 9603 if (!frag) { 9604 const end = this.loadingParts ? levelDetails.partEnd : levelDetails.fragmentEnd; 9605 frag = this.getFragmentAtPosition(pos, end, levelDetails); 9606 } 9607 let programFrag = this.filterReplacedPrimary(frag, levelDetails); 9608 if (!programFrag && frag) { 9609 const curSNIdx = frag.sn - levelDetails.startSN; 9610 programFrag = this.filterReplacedPrimary(fragments[curSNIdx + 1] || null, levelDetails); 9611 } 9612 return this.mapToInitFragWhenRequired(programFrag); 9613 } 9614 isLoopLoading(frag, targetBufferTime) { 9615 if (this.nextLoadPosition <= targetBufferTime) { 9616 return false; 9617 } 9618 const trackerState = this.fragmentTracker.getState(frag); 9619 return trackerState === FragmentState.OK || trackerState === FragmentState.PARTIAL && !!frag.gap; 9620 } 9621 getNextFragmentLoopLoading(frag, levelDetails, bufferInfo, playlistType, maxBufLen) { 9622 let nextFragment = null; 9623 if (frag.gap) { 9624 nextFragment = this.getNextFragment(this.nextLoadPosition, levelDetails); 9625 if (nextFragment && !nextFragment.gap && bufferInfo.nextStart) { 9626 // Media buffered after GAP tags should not make the next buffer timerange exceed forward buffer length 9627 const nextbufferInfo = this.getFwdBufferInfoAtPos(this.mediaBuffer ? this.mediaBuffer : this.media, bufferInfo.nextStart, playlistType, 0); 9628 if (nextbufferInfo !== null && bufferInfo.len + nextbufferInfo.len >= maxBufLen) { 9629 // Returning here might result in not finding an audio and video candiate to skip to 9630 const sn = nextFragment.sn; 9631 if (this.loopSn !== sn) { 9632 this.log(`buffer full after gaps in "${playlistType}" playlist starting at sn: ${sn}`); 9633 this.loopSn = sn; 9634 } 9635 return null; 9636 } 9637 } 9638 } 9639 this.loopSn = undefined; 9640 return nextFragment; 9641 } 9642 get primaryPrefetch() { 9643 if (interstitialsEnabled(this.config)) { 9644 var _this$hls$interstitia; 9645 const playingInterstitial = (_this$hls$interstitia = this.hls.interstitialsManager) == null || (_this$hls$interstitia = _this$hls$interstitia.playingItem) == null ? void 0 : _this$hls$interstitia.event; 9646 if (playingInterstitial) { 9647 return true; 9648 } 9649 } 9650 return false; 9651 } 9652 filterReplacedPrimary(frag, details) { 9653 if (!frag) { 9654 return frag; 9655 } 9656 if (interstitialsEnabled(this.config) && frag.type !== PlaylistLevelType.SUBTITLE) { 9657 // Do not load fragments outside the buffering schedule segment 9658 const interstitials = this.hls.interstitialsManager; 9659 const bufferingItem = interstitials == null ? void 0 : interstitials.bufferingItem; 9660 if (bufferingItem) { 9661 const bufferingInterstitial = bufferingItem.event; 9662 if (bufferingInterstitial) { 9663 // Do not stream fragments while buffering Interstitial Events (except for overlap at the start) 9664 if (bufferingInterstitial.appendInPlace || Math.abs(frag.start - bufferingItem.start) > 1 || bufferingItem.start === 0) { 9665 return null; 9666 } 9667 } else { 9668 // Limit fragment loading to media in schedule item 9669 if (frag.end <= bufferingItem.start && (details == null ? void 0 : details.live) === false) { 9670 // fragment ends by schedule item start 9671 // this.fragmentTracker.fragBuffered(frag, true); 9672 return null; 9673 } 9674 if (frag.start > bufferingItem.end && bufferingItem.nextEvent) { 9675 // fragment is past schedule item end 9676 // allow some overflow when not appending in place to prevent stalls 9677 if (bufferingItem.nextEvent.appendInPlace || frag.start - bufferingItem.end > 1) { 9678 return null; 9679 } 9680 } 9681 } 9682 } 9683 // Skip loading of fragments that overlap completely with appendInPlace interstitials 9684 const playerQueue = interstitials == null ? void 0 : interstitials.playerQueue; 9685 if (playerQueue) { 9686 for (let i = playerQueue.length; i--;) { 9687 const interstitial = playerQueue[i].interstitial; 9688 if (interstitial.appendInPlace && frag.start >= interstitial.startTime && frag.end <= interstitial.resumeTime) { 9689 return null; 9690 } 9691 } 9692 } 9693 } 9694 return frag; 9695 } 9696 mapToInitFragWhenRequired(frag) { 9697 // If an initSegment is present, it must be buffered first 9698 if (frag != null && frag.initSegment && !frag.initSegment.data && !this.bitrateTest) { 9699 return frag.initSegment; 9700 } 9701 return frag; 9702 } 9703 getNextPart(partList, frag, targetBufferTime) { 9704 let nextPart = -1; 9705 let contiguous = false; 9706 let independentAttrOmitted = true; 9707 for (let i = 0, len = partList.length; i < len; i++) { 9708 const part = partList[i]; 9709 independentAttrOmitted = independentAttrOmitted && !part.independent; 9710 if (nextPart > -1 && targetBufferTime < part.start) { 9711 break; 9712 } 9713 const loaded = part.loaded; 9714 if (loaded) { 9715 nextPart = -1; 9716 } else if (contiguous || (part.independent || independentAttrOmitted) && part.fragment === frag) { 9717 if (part.fragment !== frag) { 9718 this.warn(`Need buffer at ${targetBufferTime} but next unloaded part starts at ${part.start}`); 9719 } 9720 nextPart = i; 9721 } 9722 contiguous = loaded; 9723 } 9724 return nextPart; 9725 } 9726 loadedEndOfParts(partList, targetBufferTime) { 9727 let part; 9728 for (let i = partList.length; i--;) { 9729 part = partList[i]; 9730 if (!part.loaded) { 9731 return false; 9732 } 9733 if (targetBufferTime > part.start) { 9734 return true; 9735 } 9736 } 9737 return false; 9738 } 9739 9740 /* 9741 This method is used find the best matching first fragment for a live playlist. This fragment is used to calculate the 9742 "sliding" of the playlist, which is its offset from the start of playback. After sliding we can compute the real 9743 start and end times for each fragment in the playlist (after which this method will not need to be called). 9744 */ 9745 getInitialLiveFragment(levelDetails) { 9746 const fragments = levelDetails.fragments; 9747 const fragPrevious = this.fragPrevious; 9748 let frag = null; 9749 if (fragPrevious) { 9750 if (levelDetails.hasProgramDateTime) { 9751 // Prefer using PDT, because it can be accurate enough to choose the correct fragment without knowing the level sliding 9752 frag = findFragmentByPDT(fragments, fragPrevious.endProgramDateTime, this.config.maxFragLookUpTolerance); 9753 if (frag) { 9754 this.log(`Live playlist, switching playlist, load frag with same PDT: ${fragPrevious.programDateTime}`); 9755 } 9756 } 9757 if (!frag) { 9758 // SN does not need to be accurate between renditions, but depending on the packaging it may be so. 9759 const targetSN = fragPrevious.sn + 1; 9760 if (targetSN >= levelDetails.startSN && targetSN <= levelDetails.endSN) { 9761 const fragNext = fragments[targetSN - levelDetails.startSN]; 9762 // Ensure that we're staying within the continuity range, since PTS resets upon a new range 9763 if (fragPrevious.cc === fragNext.cc) { 9764 frag = fragNext; 9765 this.log(`Live playlist, switching playlist, load frag with next SN: ${frag.sn}`); 9766 } 9767 } 9768 // It's important to stay within the continuity range if available; otherwise the fragments in the playlist 9769 // will have the wrong start times 9770 if (!frag) { 9771 frag = findNearestWithCC(levelDetails, fragPrevious.cc, fragPrevious.end); 9772 if (frag) { 9773 this.log(`Live playlist, switching playlist, load frag with same CC: ${frag.sn}`); 9774 } 9775 } 9776 } 9777 } else { 9778 // Find a new start fragment when fragPrevious is null 9779 const liveStart = this.hls.liveSyncPosition; 9780 if (liveStart !== null) { 9781 frag = this.getFragmentAtPosition(liveStart, this.bitrateTest ? levelDetails.fragmentEnd : levelDetails.edge, levelDetails); 9782 } 9783 } 9784 return frag; 9785 } 9786 9787 /* 9788 This method finds the best matching fragment given the provided position. 9789 */ 9790 getFragmentAtPosition(bufferEnd, end, levelDetails) { 9791 const { 9792 config 9793 } = this; 9794 let { 9795 fragPrevious 9796 } = this; 9797 let { 9798 fragments, 9799 endSN 9800 } = levelDetails; 9801 const { 9802 fragmentHint 9803 } = levelDetails; 9804 const { 9805 maxFragLookUpTolerance 9806 } = config; 9807 const partList = levelDetails.partList; 9808 const loadingParts = !!(this.loadingParts && partList != null && partList.length && fragmentHint); 9809 if (loadingParts && !this.bitrateTest && partList[partList.length - 1].fragment.sn === fragmentHint.sn) { 9810 // Include incomplete fragment with parts at end 9811 fragments = fragments.concat(fragmentHint); 9812 endSN = fragmentHint.sn; 9813 } 9814 let frag; 9815 if (bufferEnd < end) { 9816 var _this$media3; 9817 const backwardSeek = bufferEnd < this.lastCurrentTime; 9818 const lookupTolerance = backwardSeek || bufferEnd > end - maxFragLookUpTolerance || (_this$media3 = this.media) != null && _this$media3.paused || !this.startFragRequested ? 0 : maxFragLookUpTolerance; 9819 // Remove the tolerance if it would put the bufferEnd past the actual end of stream 9820 // Uses buffer and sequence number to calculate switch segment (required if using EXT-X-DISCONTINUITY-SEQUENCE) 9821 frag = findFragmentByPTS(fragPrevious, fragments, bufferEnd, lookupTolerance); 9822 } else { 9823 // reach end of playlist 9824 frag = fragments[fragments.length - 1]; 9825 } 9826 if (frag) { 9827 const curSNIdx = frag.sn - levelDetails.startSN; 9828 // Move fragPrevious forward to support forcing the next fragment to load 9829 // when the buffer catches up to a previously buffered range. 9830 const fragState = this.fragmentTracker.getState(frag); 9831 if (fragState === FragmentState.OK || fragState === FragmentState.PARTIAL && frag.gap) { 9832 fragPrevious = frag; 9833 } 9834 if (fragPrevious && frag.sn === fragPrevious.sn && (!loadingParts || partList[0].fragment.sn > frag.sn || !levelDetails.live)) { 9835 // Force the next fragment to load if the previous one was already selected. This can occasionally happen with 9836 // non-uniform fragment durations 9837 const sameLevel = frag.level === fragPrevious.level; 9838 if (sameLevel) { 9839 const nextFrag = fragments[curSNIdx + 1]; 9840 if (frag.sn < endSN && this.fragmentTracker.getState(nextFrag) !== FragmentState.OK) { 9841 frag = nextFrag; 9842 } else { 9843 frag = null; 9844 } 9845 } 9846 } 9847 } 9848 return frag; 9849 } 9850 alignPlaylists(details, previousDetails, switchDetails) { 9851 // TODO: If not for `shouldAlignOnDiscontinuities` requiring fragPrevious.cc, 9852 // this could all go in level-helper mergeDetails() 9853 const length = details.fragments.length; 9854 if (!length) { 9855 this.warn(`No fragments in live playlist`); 9856 return 0; 9857 } 9858 const slidingStart = details.fragmentStart; 9859 const firstLevelLoad = !previousDetails; 9860 const aligned = details.alignedSliding && isFiniteNumber(slidingStart); 9861 if (firstLevelLoad || !aligned && !slidingStart) { 9862 alignStream(switchDetails, details); 9863 const alignedSlidingStart = details.fragmentStart; 9864 this.log(`Live playlist sliding: ${alignedSlidingStart.toFixed(2)} start-sn: ${previousDetails ? previousDetails.startSN : 'na'}->${details.startSN} fragments: ${length}`); 9865 return alignedSlidingStart; 9866 } 9867 return slidingStart; 9868 } 9869 waitForCdnTuneIn(details) { 9870 // Wait for Low-Latency CDN Tune-in to get an updated playlist 9871 const advancePartLimit = 3; 9872 return details.live && details.canBlockReload && details.partTarget && details.tuneInGoal > Math.max(details.partHoldBack, details.partTarget * advancePartLimit); 9873 } 9874 setStartPosition(details, sliding) { 9875 // compute start position if set to -1. use it straight away if value is defined 9876 let startPosition = this.startPosition; 9877 if (startPosition < sliding) { 9878 startPosition = -1; 9879 } 9880 const timelineOffset = this.timelineOffset; 9881 if (startPosition === -1) { 9882 // Use Playlist EXT-X-START:TIME-OFFSET when set 9883 // Prioritize Multivariant Playlist offset so that main, audio, and subtitle stream-controller start times match 9884 const offsetInMultivariantPlaylist = this.startTimeOffset !== null; 9885 const startTimeOffset = offsetInMultivariantPlaylist ? this.startTimeOffset : details.startTimeOffset; 9886 if (startTimeOffset !== null && isFiniteNumber(startTimeOffset)) { 9887 startPosition = sliding + startTimeOffset; 9888 if (startTimeOffset < 0) { 9889 startPosition += details.edge; 9890 } 9891 startPosition = Math.min(Math.max(sliding, startPosition), sliding + details.totalduration); 9892 this.log(`Setting startPosition to ${startPosition} for start time offset ${startTimeOffset} found in ${offsetInMultivariantPlaylist ? 'multivariant' : 'media'} playlist`); 9893 this.startPosition = startPosition; 9894 } else if (details.live) { 9895 // Leave this.startPosition at -1, so that we can use `getInitialLiveFragment` logic when startPosition has 9896 // not been specified via the config or an as an argument to startLoad (#3736). 9897 startPosition = this.hls.liveSyncPosition || sliding; 9898 this.log(`Setting startPosition to -1 to start at live edge ${startPosition}`); 9899 this.startPosition = -1; 9900 } else { 9901 this.log(`setting startPosition to 0 by default`); 9902 this.startPosition = startPosition = 0; 9903 } 9904 this.lastCurrentTime = startPosition + timelineOffset; 9905 } 9906 this.nextLoadPosition = startPosition + timelineOffset; 9907 } 9908 getLoadPosition() { 9909 var _this$hls; 9910 const { 9911 media 9912 } = this; 9913 // if we have not yet loaded any fragment, start loading from start position 9914 let pos = 0; 9915 if ((_this$hls = this.hls) != null && _this$hls.hasEnoughToStart && media) { 9916 pos = media.currentTime; 9917 } else if (this.nextLoadPosition >= 0) { 9918 pos = this.nextLoadPosition; 9919 } 9920 return pos; 9921 } 9922 handleFragLoadAborted(frag, part) { 9923 if (this.transmuxer && frag.type === this.playlistType && isMediaFragment(frag) && frag.stats.aborted) { 9924 this.log(`Fragment ${frag.sn}${part ? ' part ' + part.index : ''} of ${this.playlistLabel()} ${frag.level} was aborted`); 9925 this.resetFragmentLoading(frag); 9926 } 9927 } 9928 resetFragmentLoading(frag) { 9929 if (!this.fragCurrent || !this.fragContextChanged(frag) && this.state !== State.FRAG_LOADING_WAITING_RETRY) { 9930 this.state = State.IDLE; 9931 } 9932 } 9933 onFragmentOrKeyLoadError(filterType, data) { 9934 var _this$hls$latestLevel; 9935 if (data.chunkMeta && !data.frag) { 9936 const context = this.getCurrentContext(data.chunkMeta); 9937 if (context) { 9938 data.frag = context.frag; 9939 } 9940 } 9941 const frag = data.frag; 9942 // Handle frag error related to caller's filterType 9943 if (!frag || frag.type !== filterType || !this.levels) { 9944 return; 9945 } 9946 if (this.fragContextChanged(frag)) { 9947 var _this$fragCurrent; 9948 this.warn(`Frag load error must match current frag to retry ${frag.url} > ${(_this$fragCurrent = this.fragCurrent) == null ? void 0 : _this$fragCurrent.url}`); 9949 return; 9950 } 9951 const gapTagEncountered = data.details === ErrorDetails.FRAG_GAP; 9952 if (gapTagEncountered) { 9953 this.fragmentTracker.fragBuffered(frag, true); 9954 } 9955 // keep retrying until the limit will be reached 9956 const errorAction = data.errorAction; 9957 if (!errorAction) { 9958 this.state = State.ERROR; 9959 return; 9960 } 9961 const { 9962 action, 9963 flags, 9964 retryCount = 0, 9965 retryConfig 9966 } = errorAction; 9967 const couldRetry = !!retryConfig; 9968 const retry = couldRetry && action === NetworkErrorAction.RetryRequest; 9969 const noAlternate = couldRetry && !errorAction.resolved && flags === ErrorActionFlags.MoveAllAlternatesMatchingHost; 9970 const live = (_this$hls$latestLevel = this.hls.latestLevelDetails) == null ? void 0 : _this$hls$latestLevel.live; 9971 if (!retry && noAlternate && isMediaFragment(frag) && !frag.endList && live && !isUnusableKeyError(data)) { 9972 this.resetFragmentErrors(filterType); 9973 this.treatAsGap(frag); 9974 errorAction.resolved = true; 9975 } else if ((retry || noAlternate) && retryCount < retryConfig.maxNumRetry) { 9976 var _data$response; 9977 const offlineStatus = offlineHttpStatus((_data$response = data.response) == null ? void 0 : _data$response.code);
vendor: 11,663 bytes, lines 9978-10293
9978 const delay = getRetryDelay(retryConfig, retryCount); 9979 this.resetStartWhenNotLoaded(); 9980 this.retryDate = self.performance.now() + delay; 9981 this.state = State.FRAG_LOADING_WAITING_RETRY; 9982 errorAction.resolved = true; 9983 if (offlineStatus) { 9984 this.log(`Waiting for connection (offline)`); 9985 this.retryDate = Infinity; 9986 data.reason = 'offline'; 9987 return; 9988 } 9989 this.warn(`Fragment ${frag.sn} of ${filterType} ${frag.level} errored with ${data.details}, retrying loading ${retryCount + 1}/${retryConfig.maxNumRetry} in ${delay}ms`); 9990 } else if (retryConfig) { 9991 this.resetFragmentErrors(filterType); 9992 if (retryCount < retryConfig.maxNumRetry) { 9993 // Network retry is skipped when level switch is preferred 9994 if (!gapTagEncountered && action !== NetworkErrorAction.RemoveAlternatePermanently) { 9995 errorAction.resolved = true; 9996 } 9997 } else { 9998 this.warn(`${data.details} reached or exceeded max retry (${retryCount})`); 9999 return; 10000 } 10001 } else if (action === NetworkErrorAction.SendAlternateToPenaltyBox) { 10002 this.state = State.WAITING_LEVEL; 10003 } else { 10004 this.state = State.ERROR; 10005 } 10006 // Perform next async tick sooner to speed up error action resolution 10007 this.tickImmediate(); 10008 } 10009 checkRetryDate() { 10010 const now = self.performance.now(); 10011 const retryDate = this.retryDate; 10012 // if current time is gt than retryDate, or if media seeking let's switch to IDLE state to retry loading 10013 const waitingForConnection = retryDate === Infinity; 10014 if (!retryDate || now >= retryDate || waitingForConnection && !offlineHttpStatus(0)) { 10015 if (waitingForConnection) { 10016 this.log(`Connection restored (online)`); 10017 } 10018 this.resetStartWhenNotLoaded(); 10019 this.state = State.IDLE; 10020 } 10021 } 10022 reduceLengthAndFlushBuffer(data) { 10023 // if in appending state 10024 if (this.state === State.PARSING || this.state === State.PARSED) { 10025 const frag = data.frag; 10026 const playlistType = data.parent; 10027 const bufferedInfo = this.getFwdBufferInfo(this.mediaBuffer, playlistType); 10028 // 0.5 : tolerance needed as some browsers stalls playback before reaching buffered end 10029 // reduce max buf len if current position is buffered 10030 const buffered = bufferedInfo && bufferedInfo.len > 0.5; 10031 if (buffered) { 10032 this.reduceMaxBufferLength(bufferedInfo.len, (frag == null ? void 0 : frag.duration) || 10); 10033 } 10034 const flushBuffer = !buffered; 10035 if (flushBuffer) { 10036 // current position is not buffered, but browser is still complaining about buffer full error 10037 // this happens on IE/Edge, refer to https://github.com/video-dev/hls.js/pull/708 10038 // in that case flush the whole audio buffer to recover 10039 this.warn(`Buffer full error while media.currentTime (${this.getLoadPosition()}) is not buffered, flush ${playlistType} buffer`); 10040 } 10041 if (frag) { 10042 this.fragmentTracker.removeFragment(frag); 10043 this.nextLoadPosition = frag.start; 10044 } 10045 this.resetLoadingState(); 10046 return flushBuffer; 10047 } 10048 return false; 10049 } 10050 resetFragmentErrors(filterType) { 10051 if (filterType === PlaylistLevelType.AUDIO) { 10052 // Reset current fragment since audio track audio is essential and may not have a fail-over track 10053 this.fragCurrent = null; 10054 } 10055 // Fragment errors that result in a level switch or redundant fail-over 10056 // should reset the stream controller state to idle 10057 if (!this.hls.hasEnoughToStart) { 10058 this.startFragRequested = false; 10059 } 10060 if (this.state !== State.STOPPED) { 10061 this.state = State.IDLE; 10062 } 10063 } 10064 afterBufferFlushed(media, bufferType, playlistType) { 10065 if (!media) { 10066 return; 10067 } 10068 // After successful buffer flushing, filter flushed fragments from bufferedFrags use mediaBuffered instead of media 10069 // (so that we will check against video.buffered ranges in case of alt audio track) 10070 const bufferedTimeRanges = BufferHelper.getBuffered(media); 10071 this.fragmentTracker.detectEvictedFragments(bufferType, bufferedTimeRanges, playlistType); 10072 if (this.state === State.ENDED) { 10073 this.resetLoadingState(); 10074 } 10075 } 10076 resetLoadingState() { 10077 this.log('Reset loading state'); 10078 this.fragCurrent = null; 10079 this.fragPrevious = null; 10080 if (this.state !== State.STOPPED) { 10081 this.state = State.IDLE; 10082 } 10083 } 10084 resetStartWhenNotLoaded() { 10085 // if loadedmetadata is not set, it means that first frag request failed 10086 // in that case, reset startFragRequested flag 10087 if (!this.hls.hasEnoughToStart) { 10088 this.startFragRequested = false; 10089 const level = this.levelLastLoaded; 10090 const details = level ? level.details : null; 10091 if (details != null && details.live) { 10092 // Update the start position and return to IDLE to recover live start 10093 this.log(`resetting startPosition for live start`); 10094 this.startPosition = -1; 10095 this.setStartPosition(details, details.fragmentStart); 10096 this.resetLoadingState(); 10097 } else { 10098 this.nextLoadPosition = this.startPosition; 10099 } 10100 } 10101 } 10102 resetWhenMissingContext(chunkMeta) { 10103 this.log(`Loading context changed while buffering sn ${chunkMeta.sn} of ${this.playlistLabel()} ${chunkMeta.level === -1 ? '<removed>' : chunkMeta.level}. This chunk will not be buffered.`); 10104 this.removeUnbufferedFrags(); 10105 this.resetStartWhenNotLoaded(); 10106 this.resetLoadingState(); 10107 } 10108 removeUnbufferedFrags(start = 0) { 10109 this.fragmentTracker.removeFragmentsInRange(start, Infinity, this.playlistType, false, true); 10110 } 10111 updateLevelTiming(frag, part, level, partial) { 10112 const details = level.details; 10113 if (!details) { 10114 this.warn('level.details undefined'); 10115 return; 10116 } 10117 const parsed = Object.keys(frag.elementaryStreams).reduce((result, type) => { 10118 const info = frag.elementaryStreams[type]; 10119 if (info) { 10120 const parsedDuration = info.endPTS - info.startPTS; 10121 if (parsedDuration <= 0) { 10122 // Destroy the transmuxer after it's next time offset failed to advance because duration was <= 0. 10123 // The new transmuxer will be configured with a time offset matching the next fragment start, 10124 // preventing the timeline from shifting. 10125 this.warn(`Could not parse fragment ${frag.sn} ${type} duration reliably (${parsedDuration})`); 10126 return result || false; 10127 } 10128 const drift = partial ? 0 : updateFragPTSDTS(details, frag, info.startPTS, info.endPTS, info.startDTS, info.endDTS, this); 10129 this.hls.trigger(Events.LEVEL_PTS_UPDATED, { 10130 details, 10131 level, 10132 drift, 10133 type, 10134 frag, 10135 start: info.startPTS, 10136 end: info.endPTS 10137 }); 10138 return true; 10139 } 10140 return result; 10141 }, false); 10142 if (!parsed) { 10143 var _this$transmuxer; 10144 const mediaNotFound = ((_this$transmuxer = this.transmuxer) == null ? void 0 : _this$transmuxer.error) === null; 10145 if (level.fragmentError === 0 || mediaNotFound && (level.fragmentError < 2 || frag.endList)) { 10146 // Mark and track the odd (or last) empty segment as a gap to avoid reloading 10147 this.treatAsGap(frag, level); 10148 } 10149 if (mediaNotFound) { 10150 const error = new Error(`Found no media in fragment ${frag.sn} of ${this.playlistLabel()} ${frag.level} resetting transmuxer to fallback to playlist timing`); 10151 this.warn(error.message); 10152 this.hls.trigger(Events.ERROR, { 10153 type: ErrorTypes.MEDIA_ERROR, 10154 details: ErrorDetails.FRAG_PARSING_ERROR, 10155 fatal: false, 10156 error, 10157 frag, 10158 reason: `Found no media in msn ${frag.sn} of ${this.playlistLabel()} "${level.url}"` 10159 }); 10160 if (!this.hls) { 10161 return; 10162 } 10163 this.resetTransmuxer(); 10164 } 10165 // For this error fallthrough. Marking parsed will allow advancing to next fragment. 10166 } 10167 this.state = State.PARSED; 10168 this.log(`Parsed ${frag.type} sn: ${frag.sn}${part ? ' part: ' + part.index : ''} of ${this.fragInfo(frag, false, part)})`); 10169 this.hls.trigger(Events.FRAG_PARSED, { 10170 frag, 10171 part 10172 }); 10173 } 10174 playlistLabel() { 10175 return this.playlistType === PlaylistLevelType.MAIN ? 'level' : 'track'; 10176 } 10177 fragInfo(frag, pts = true, part) { 10178 var _ref, _ref2; 10179 return `${this.playlistLabel()} ${frag.level} (${part ? 'part' : 'frag'}:[${((_ref = pts && !part ? frag.startPTS : (part || frag).start) != null ? _ref : NaN).toFixed(3)}-${((_ref2 = pts && !part ? frag.endPTS : (part || frag).end) != null ? _ref2 : NaN).toFixed(3)}]${part && frag.type === 'main' ? 'INDEPENDENT=' + (part.independent ? 'YES' : 'NO') : ''}`; 10180 } 10181 treatAsGap(frag, level) { 10182 if (level) { 10183 level.fragmentError++; 10184 } 10185 frag.gap = true; 10186 this.fragmentTracker.removeFragment(frag); 10187 this.fragmentTracker.fragBuffered(frag, true); 10188 } 10189 resetTransmuxer() { 10190 var _this$transmuxer2; 10191 (_this$transmuxer2 = this.transmuxer) == null || _this$transmuxer2.reset(); 10192 } 10193 recoverWorkerError(data) { 10194 if (data.event === 'demuxerWorker') { 10195 this.fragmentTracker.removeAllFragments(); 10196 if (this.transmuxer) { 10197 this.transmuxer.destroy(); 10198 this.transmuxer = null; 10199 } 10200 this.resetStartWhenNotLoaded(); 10201 this.resetLoadingState(); 10202 } 10203 } 10204 set state(nextState) { 10205 const previousState = this._state; 10206 if (previousState !== nextState) { 10207 this._state = nextState; 10208 this.log(`${previousState}->${nextState}`); 10209 } 10210 } 10211 get state() { 10212 return this._state; 10213 } 10214} 10215function interstitialsEnabled(config) { 10216 return !!config.interstitialsController && config.enableInterstitialPlayback !== false; 10217} 10218 10219class ChunkCache { 10220 constructor() { 10221 this.chunks = []; 10222 this.dataLength = 0; 10223 } 10224 push(chunk) { 10225 this.chunks.push(chunk); 10226 this.dataLength += chunk.length; 10227 } 10228 flush() { 10229 const { 10230 chunks, 10231 dataLength 10232 } = this; 10233 let result; 10234 if (!chunks.length) { 10235 return new Uint8Array(0); 10236 } else if (chunks.length === 1) { 10237 result = chunks[0]; 10238 } else { 10239 result = concatUint8Arrays(chunks, dataLength); 10240 } 10241 this.reset(); 10242 return result; 10243 } 10244 reset() { 10245 this.chunks.length = 0; 10246 this.dataLength = 0; 10247 } 10248} 10249function concatUint8Arrays(chunks, dataLength) { 10250 const result = new Uint8Array(dataLength); 10251 let offset = 0; 10252 for (let i = 0; i < chunks.length; i++) { 10253 const chunk = chunks[i]; 10254 result.set(chunk, offset); 10255 offset += chunk.length; 10256 } 10257 return result; 10258} 10259 10260var eventemitter3 = {exports: {}}; 10261 10262var hasRequiredEventemitter3; 10263 10264function requireEventemitter3 () { 10265 if (hasRequiredEventemitter3) return eventemitter3.exports; 10266 hasRequiredEventemitter3 = 1; 10267 (function (module) { 10268 10269 var has = Object.prototype.hasOwnProperty 10270 , prefix = '~'; 10271 10272 /** 10273 * Constructor to create a storage for our `EE` objects. 10274 * An `Events` instance is a plain object whose properties are event names. 10275 * 10276 * @constructor 10277 * @private 10278 */ 10279 function Events() {} 10280 10281 // 10282 // We try to not inherit from `Object.prototype`. In some engines creating an 10283 // instance in this way is faster than calling `Object.create(null)` directly. 10284 // If `Object.create(null)` is not supported we prefix the event names with a 10285 // character to make sure that the built-in object properties are not 10286 // overridden or used as an attack vector. 10287 // 10288 if (Object.create) { 10289 Events.prototype = Object.create(null); 10290 10291 // 10292 // This hack is needed because the `__proto__` property is still inherited in 10293 // some old browsers like Android 4, iPhone 5.1, Opera 11 and
vendor: 4,749 bytes, lines 10293-10457
10293Safari 5. 10294 // 10295 if (!new Events().__proto__) prefix = false; 10296 } 10297 10298 /** 10299 * Representation of a single event listener. 10300 * 10301 * @param {Function} fn The listener function. 10302 * @param {*} context The context to invoke the listener with. 10303 * @param {Boolean} [once=false] Specify if the listener is a one-time listener. 10304 * @constructor 10305 * @private 10306 */ 10307 function EE(fn, context, once) { 10308 this.fn = fn; 10309 this.context = context; 10310 this.once = once || false; 10311 } 10312 10313 /** 10314 * Add a listener for a given event. 10315 * 10316 * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. 10317 * @param {(String|Symbol)} event The event name. 10318 * @param {Function} fn The listener function. 10319 * @param {*} context The context to invoke the listener with. 10320 * @param {Boolean} once Specify if the listener is a one-time listener. 10321 * @returns {EventEmitter} 10322 * @private 10323 */ 10324 function addListener(emitter, event, fn, context, once) { 10325 if (typeof fn !== 'function') { 10326 throw new TypeError('The listener must be a function'); 10327 } 10328 10329 var listener = new EE(fn, context || emitter, once) 10330 , evt = prefix ? prefix + event : event; 10331 10332 if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++; 10333 else if (!emitter._events[evt].fn) emitter._events[evt].push(listener); 10334 else emitter._events[evt] = [emitter._events[evt], listener]; 10335 10336 return emitter; 10337 } 10338 10339 /** 10340 * Clear event by name. 10341 * 10342 * @param {EventEmitter} emitter Reference to the `EventEmitter` instance. 10343 * @param {(String|Symbol)} evt The Event name. 10344 * @private 10345 */ 10346 function clearEvent(emitter, evt) { 10347 if (--emitter._eventsCount === 0) emitter._events = new Events(); 10348 else delete emitter._events[evt]; 10349 } 10350 10351 /** 10352 * Minimal `EventEmitter` interface that is molded against the Node.js 10353 * `EventEmitter` interface. 10354 * 10355 * @constructor 10356 * @public 10357 */ 10358 function EventEmitter() { 10359 this._events = new Events(); 10360 this._eventsCount = 0; 10361 } 10362 10363 /** 10364 * Return an array listing the events for which the emitter has registered 10365 * listeners. 10366 * 10367 * @returns {Array} 10368 * @public 10369 */ 10370 EventEmitter.prototype.eventNames = function eventNames() { 10371 var names = [] 10372 , events 10373 , name; 10374 10375 if (this._eventsCount === 0) return names; 10376 10377 for (name in (events = this._events)) { 10378 if (has.call(events, name)) names.push(prefix ? name.slice(1) : name); 10379 } 10380 10381 if (Object.getOwnPropertySymbols) { 10382 return names.concat(Object.getOwnPropertySymbols(events)); 10383 } 10384 10385 return names; 10386 }; 10387 10388 /** 10389 * Return the listeners registered for a given event. 10390 * 10391 * @param {(String|Symbol)} event The event name. 10392 * @returns {Array} The registered listeners. 10393 * @public 10394 */ 10395 EventEmitter.prototype.listeners = function listeners(event) { 10396 var evt = prefix ? prefix + event : event 10397 , handlers = this._events[evt]; 10398 10399 if (!handlers) return []; 10400 if (handlers.fn) return [handlers.fn]; 10401 10402 for (var i = 0, l = handlers.length, ee = new Array(l); i < l; i++) { 10403 ee[i] = handlers[i].fn; 10404 } 10405 10406 return ee; 10407 }; 10408 10409 /** 10410 * Return the number of listeners listening to a given event. 10411 * 10412 * @param {(String|Symbol)} event The event name. 10413 * @returns {Number} The number of listeners. 10414 * @public 10415 */ 10416 EventEmitter.prototype.listenerCount = function listenerCount(event) { 10417 var evt = prefix ? prefix + event : event 10418 , listeners = this._events[evt]; 10419 10420 if (!listeners) return 0; 10421 if (listeners.fn) return 1; 10422 return listeners.length; 10423 }; 10424 10425 /** 10426 * Calls each of the listeners registered for a given event. 10427 * 10428 * @param {(String|Symbol)} event The event name. 10429 * @returns {Boolean} `true` if the event had listeners, else `false`. 10430 * @public 10431 */ 10432 EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { 10433 var evt = prefix ? prefix + event : event; 10434 10435 if (!this._events[evt]) return false; 10436 10437 var listeners = this._events[evt] 10438 , len = arguments.length 10439 , args 10440 , i; 10441 10442 if (listeners.fn) { 10443 if (listeners.once) this.removeListener(event, listeners.fn, undefined, true); 10444 10445 switch (len) { 10446 case 1: return listeners.fn.call(listeners.context), true; 10447 case 2: return listeners.fn.call(listeners.context, a1), true; 10448 case 3: return listeners.fn.call(listeners.context, a1, a2), true; 10449 case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true; 10450 case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true; 10451 case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true; 10452 } 10453 10454 for (i = 1, args = new Array(len -1); i < len; i++) { 10455 args[i - 1] = arguments[i]; 10456 } 10457
vendor: 4,806 bytes, lines 10458-10624
10458 listeners.fn.apply(listeners.context, args); 10459 } else { 10460 var length = listeners.length 10461 , j; 10462 10463 for (i = 0; i < length; i++) { 10464 if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true); 10465 10466 switch (len) { 10467 case 1: listeners[i].fn.call(listeners[i].context); break; 10468 case 2: listeners[i].fn.call(listeners[i].context, a1); break; 10469 case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break; 10470 case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break; 10471 default: 10472 if (!args) for (j = 1, args = new Array(len -1); j < len; j++) { 10473 args[j - 1] = arguments[j]; 10474 } 10475 10476 listeners[i].fn.apply(listeners[i].context, args); 10477 } 10478 } 10479 } 10480 10481 return true; 10482 }; 10483 10484 /** 10485 * Add a listener for a given event. 10486 * 10487 * @param {(String|Symbol)} event The event name. 10488 * @param {Function} fn The listener function. 10489 * @param {*} [context=this] The context to invoke the listener with. 10490 * @returns {EventEmitter} `this`. 10491 * @public 10492 */ 10493 EventEmitter.prototype.on = function on(event, fn, context) { 10494 return addListener(this, event, fn, context, false); 10495 }; 10496 10497 /** 10498 * Add a one-time listener for a given event. 10499 * 10500 * @param {(String|Symbol)} event The event name. 10501 * @param {Function} fn The listener function. 10502 * @param {*} [context=this] The context to invoke the listener with. 10503 * @returns {EventEmitter} `this`. 10504 * @public 10505 */ 10506 EventEmitter.prototype.once = function once(event, fn, context) { 10507 return addListener(this, event, fn, context, true); 10508 }; 10509 10510 /** 10511 * Remove the listeners of a given event. 10512 * 10513 * @param {(String|Symbol)} event The event name. 10514 * @param {Function} fn Only remove the listeners that match this function. 10515 * @param {*} context Only remove the listeners that have this context. 10516 * @param {Boolean} once Only remove one-time listeners. 10517 * @returns {EventEmitter} `this`. 10518 * @public 10519 */ 10520 EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) { 10521 var evt = prefix ? prefix + event : event; 10522 10523 if (!this._events[evt]) return this; 10524 if (!fn) { 10525 clearEvent(this, evt); 10526 return this; 10527 } 10528 10529 var listeners = this._events[evt]; 10530 10531 if (listeners.fn) { 10532 if ( 10533 listeners.fn === fn && 10534 (!once || listeners.once) && 10535 (!context || listeners.context === context) 10536 ) { 10537 clearEvent(this, evt); 10538 } 10539 } else { 10540 for (var i = 0, events = [], length = listeners.length; i < length; i++) { 10541 if ( 10542 listeners[i].fn !== fn || 10543 (once && !listeners[i].once) || 10544 (context && listeners[i].context !== context) 10545 ) { 10546 events.push(listeners[i]); 10547 } 10548 } 10549 10550 // 10551 // Reset the array, or remove it completely if we have no more listeners. 10552 // 10553 if (events.length) this._events[evt] = events.length === 1 ? events[0] : events; 10554 else clearEvent(this, evt); 10555 } 10556 10557 return this; 10558 }; 10559 10560 /** 10561 * Remove all listeners, or those of the specified event. 10562 * 10563 * @param {(String|Symbol)} [event] The event name. 10564 * @returns {EventEmitter} `this`. 10565 * @public 10566 */ 10567 EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) { 10568 var evt; 10569 10570 if (event) { 10571 evt = prefix ? prefix + event : event; 10572 if (this._events[evt]) clearEvent(this, evt); 10573 } else { 10574 this._events = new Events(); 10575 this._eventsCount = 0; 10576 } 10577 10578 return this; 10579 }; 10580 10581 // 10582 // Alias methods names because people roll like that. 10583 // 10584 EventEmitter.prototype.off = EventEmitter.prototype.removeListener; 10585 EventEmitter.prototype.addListener = EventEmitter.prototype.on; 10586 10587 // 10588 // Expose the prefix. 10589 // 10590 EventEmitter.prefixed = prefix; 10591 10592 // 10593 // Allow `EventEmitter` to be imported as module namespace. 10594 // 10595 EventEmitter.EventEmitter = EventEmitter; 10596 10597 // 10598 // Expose the module. 10599 // 10600 { 10601 module.exports = EventEmitter; 10602 } 10603 } (eventemitter3)); 10604 return eventemitter3.exports; 10605} 10606 10607var eventemitter3Exports = requireEventemitter3(); 10608var EventEmitter = /*@__PURE__*/getDefaultExportFromCjs(eventemitter3Exports); 10609 10610const version = "1.6.16"; 10611 10612// ensure the worker ends up in the bundle 10613// If the worker should not be included this gets aliased to empty.js 10614const workerStore = {}; 10615function hasUMDWorker() { 10616 return typeof __HLS_WORKER_BUNDLE__ === 'function'; 10617} 10618function injectWorker() { 10619 const workerContext = workerStore[version]; 10620 if (workerContext) { 10621 workerContext.clientCount++; 10622 return workerContext; 10623 } 10624 const blob = new self.Blob([`var exports={};var module={exports:exports};function define(f){f()};define.amd=true;(${__HLS_WORKER_BUNDLE__.toString()})(true);`], {
vendor: 225,853 bytes, lines 10625-17112
10625 type: 'text/javascript' 10626 }); 10627 const objectURL = self.URL.createObjectURL(blob); 10628 const worker = new self.Worker(objectURL); 10629 const result = { 10630 worker, 10631 objectURL, 10632 clientCount: 1 10633 }; 10634 workerStore[version] = result; 10635 return result; 10636} 10637function loadWorker(path) { 10638 const workerContext = workerStore[path]; 10639 if (workerContext) { 10640 workerContext.clientCount++; 10641 return workerContext; 10642 } 10643 const scriptURL = new self.URL(path, self.location.href).href; 10644 const worker = new self.Worker(scriptURL); 10645 const result = { 10646 worker, 10647 scriptURL, 10648 clientCount: 1 10649 }; 10650 workerStore[path] = result; 10651 return result; 10652} 10653function removeWorkerFromStore(path) { 10654 const workerContext = workerStore[path || version]; 10655 if (workerContext) { 10656 const clientCount = workerContext.clientCount--; 10657 if (clientCount === 1) { 10658 const { 10659 worker, 10660 objectURL 10661 } = workerContext; 10662 delete workerStore[path || version]; 10663 if (objectURL) { 10664 // revoke the Object URL that was used to create transmuxer worker, so as not to leak it 10665 self.URL.revokeObjectURL(objectURL); 10666 } 10667 worker.terminate(); 10668 } 10669 } 10670} 10671 10672/** 10673 * Returns true if an ID3 footer can be found at offset in data 10674 * 10675 * @param data - The data to search in 10676 * @param offset - The offset at which to start searching 10677 * 10678 * @returns `true` if an ID3 footer is found 10679 * 10680 * @internal 10681 * 10682 * @group ID3 10683 */ 10684function isId3Footer(data, offset) { 10685 /* 10686 * The footer is a copy of the header, but with a different identifier 10687 */ 10688 if (offset + 10 <= data.length) { 10689 // look for '3DI' identifier 10690 if (data[offset] === 0x33 && data[offset + 1] === 0x44 && data[offset + 2] === 0x49) { 10691 // check version is within range 10692 if (data[offset + 3] < 0xff && data[offset + 4] < 0xff) { 10693 // check size is within range 10694 if (data[offset + 6] < 0x80 && data[offset + 7] < 0x80 && data[offset + 8] < 0x80 && data[offset + 9] < 0x80) { 10695 return true; 10696 } 10697 } 10698 } 10699 } 10700 return false; 10701} 10702 10703/** 10704 * Returns true if an ID3 header can be found at offset in data 10705 * 10706 * @param data - The data to search in 10707 * @param offset - The offset at which to start searching 10708 * 10709 * @returns `true` if an ID3 header is found 10710 * 10711 * @internal 10712 * 10713 * @group ID3 10714 */ 10715function isId3Header(data, offset) { 10716 /* 10717 * http://id3.org/id3v2.3.0 10718 * [0] = 'I' 10719 * [1] = 'D' 10720 * [2] = '3' 10721 * [3,4] = {Version} 10722 * [5] = {Flags} 10723 * [6-9] = {ID3 Size} 10724 * 10725 * An ID3v2 tag can be detected with the following pattern: 10726 * $49 44 33 yy yy xx zz zz zz zz 10727 * Where yy is less than $FF, xx is the 'flags' byte and zz is less than $80 10728 */ 10729 if (offset + 10 <= data.length) { 10730 // look for 'ID3' identifier 10731 if (data[offset] === 0x49 && data[offset + 1] === 0x44 && data[offset + 2] === 0x33) { 10732 // check version is within range 10733 if (data[offset + 3] < 0xff && data[offset + 4] < 0xff) { 10734 // check size is within range 10735 if (data[offset + 6] < 0x80 && data[offset + 7] < 0x80 && data[offset + 8] < 0x80 && data[offset + 9] < 0x80) { 10736 return true; 10737 } 10738 } 10739 } 10740 } 10741 return false; 10742} 10743 10744/** 10745 * Read ID3 size 10746 * 10747 * @param data - The data to read from 10748 * @param offset - The offset at which to start reading 10749 * 10750 * @returns The size 10751 * 10752 * @internal 10753 * 10754 * @group ID3 10755 */ 10756function readId3Size(data, offset) { 10757 let size = 0; 10758 size = (data[offset] & 0x7f) << 21; 10759 size |= (data[offset + 1] & 0x7f) << 14; 10760 size |= (data[offset + 2] & 0x7f) << 7; 10761 size |= data[offset + 3] & 0x7f; 10762 return size; 10763} 10764 10765/** 10766 * Returns any adjacent ID3 tags found in data starting at offset, as one block of data 10767 * 10768 * @param data - The data to search in 10769 * @param offset - The offset at which to start searching 10770 * 10771 * @returns The block of data containing any ID3 tags found 10772 * or `undefined` if no header is found at the starting offset 10773 * 10774 * @internal 10775 * 10776 * @group ID3 10777 */ 10778function getId3Data(data, offset) { 10779 const front = offset; 10780 let length = 0; 10781 while (isId3Header(data, offset)) { 10782 // ID3 header is 10 bytes 10783 length += 10; 10784 const size = readId3Size(data, offset + 6); 10785 length += size; 10786 if (isId3Footer(data, offset + 10)) { 10787 // ID3 footer is 10 bytes 10788 length += 10; 10789 } 10790 offset += length; 10791 } 10792 if (length > 0) { 10793 return data.subarray(front, front + length); 10794 } 10795 return undefined; 10796} 10797 10798function getAudioConfig(observer, data, offset, manifestCodec) { 10799 const adtsSamplingRates = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350]; 10800 const byte2 = data[offset + 2]; 10801 const adtsSamplingIndex = byte2 >> 2 & 0xf; 10802 if (adtsSamplingIndex > 12) { 10803 const error = new Error(`invalid ADTS sampling index:${adtsSamplingIndex}`); 10804 observer.emit(Events.ERROR, Events.ERROR, { 10805 type: ErrorTypes.MEDIA_ERROR, 10806 details: ErrorDetails.FRAG_PARSING_ERROR, 10807 fatal: true, 10808 error, 10809 reason: error.message 10810 }); 10811 return; 10812 } 10813 // MPEG-4 Audio Object Type (profile_ObjectType+1) 10814 const adtsObjectType = (byte2 >> 6 & 0x3) + 1; 10815 const channelCount = data[offset + 3] >> 6 & 0x3 | (byte2 & 1) << 2; 10816 const codec = 'mp4a.40.' + adtsObjectType; 10817 /* refer to http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Audio_Specific_Config 10818 ISO/IEC 14496-3 - Table 1.13 — Syntax of AudioSpecificConfig() 10819 Audio Profile / Audio Object Type 10820 0: Null 10821 1: AAC Main 10822 2: AAC LC (Low Complexity) 10823 3: AAC SSR (Scalable Sample Rate) 10824 4: AAC LTP (Long Term Prediction) 10825 5: SBR (Spectral Band Replication) 10826 6: AAC Scalable 10827 sampling freq 10828 0: 96000 Hz 10829 1: 88200 Hz 10830 2: 64000 Hz 10831 3: 48000 Hz 10832 4: 44100 Hz 10833 5: 32000 Hz 10834 6: 24000 Hz 10835 7: 22050 Hz 10836 8: 16000 Hz 10837 9: 12000 Hz 10838 10: 11025 Hz 10839 11: 8000 Hz 10840 12: 7350 Hz 10841 13: Reserved 10842 14: Reserved 10843 15: frequency is written explictly 10844 Channel Configurations 10845 These are the channel configurations: 10846 0: Defined in AOT Specifc Config 10847 1: 1 channel: front-center 10848 2: 2 channels: front-left, front-right 10849 */ 10850 // audioObjectType = profile => profile, the MPEG-4 Audio Object Type minus 1 10851 const samplerate = adtsSamplingRates[adtsSamplingIndex]; 10852 let aacSampleIndex = adtsSamplingIndex; 10853 if (adtsObjectType === 5 || adtsObjectType === 29) { 10854 // HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies 10855 // there is a factor 2 between frame sample rate and output sample rate 10856 // multiply frequency by 2 (see table above, equivalent to substract 3) 10857 aacSampleIndex -= 3; 10858 } 10859 const config = [adtsObjectType << 3 | (aacSampleIndex & 0x0e) >> 1, (aacSampleIndex & 0x01) << 7 | channelCount << 3]; 10860 logger.log(`manifest codec:${manifestCodec}, parsed codec:${codec}, channels:${channelCount}, rate:${samplerate} (ADTS object type:${adtsObjectType} sampling index:${adtsSamplingIndex})`); 10861 return { 10862 config, 10863 samplerate, 10864 channelCount, 10865 codec, 10866 parsedCodec: codec, 10867 manifestCodec 10868 }; 10869} 10870function isHeaderPattern$1(data, offset) { 10871 return data[offset] === 0xff && (data[offset + 1] & 0xf6) === 0xf0; 10872} 10873function getHeaderLength(data, offset) { 10874 return data[offset + 1] & 0x01 ? 7 : 9; 10875} 10876function getFullFrameLength(data, offset) { 10877 return (data[offset + 3] & 0x03) << 11 | data[offset + 4] << 3 | (data[offset + 5] & 0xe0) >>> 5; 10878} 10879function canGetFrameLength(data, offset) { 10880 return offset + 5 < data.length; 10881} 10882function isHeader$1(data, offset) { 10883 // Look for ADTS header | 1111 1111 | 1111 X00X | where X can be either 0 or 1 10884 // Layer bits (position 14 and 15) in header should be always 0 for ADTS 10885 // More info https://wiki.multimedia.cx/index.php?title=ADTS 10886 return offset + 1 < data.length && isHeaderPattern$1(data, offset); 10887} 10888function canParse$1(data, offset) { 10889 return canGetFrameLength(data, offset) && isHeaderPattern$1(data, offset) && getFullFrameLength(data, offset) <= data.length - offset; 10890} 10891function probe$1(data, offset) { 10892 // same as isHeader but we also check that ADTS frame follows last ADTS frame 10893 // or end of data is reached 10894 if (isHeader$1(data, offset)) { 10895 // ADTS header Length 10896 const headerLength = getHeaderLength(data, offset); 10897 if (offset + headerLength >= data.length) { 10898 return false; 10899 } 10900 // ADTS frame Length 10901 const frameLength = getFullFrameLength(data, offset); 10902 if (frameLength <= headerLength) { 10903 return false; 10904 } 10905 const newOffset = offset + frameLength; 10906 return newOffset === data.length || isHeader$1(data, newOffset); 10907 } 10908 return false; 10909} 10910function initTrackConfig(track, observer, data, offset, audioCodec) { 10911 if (!track.samplerate) { 10912 const config = getAudioConfig(observer, data, offset, audioCodec); 10913 if (!config) { 10914 return; 10915 } 10916 _extends(track, config); 10917 } 10918} 10919function getFrameDuration(samplerate) { 10920 return 1024 * 90000 / samplerate; 10921} 10922function parseFrameHeader(data, offset) { 10923 // The protection skip bit tells us if we have 2 bytes of CRC data at the end of the ADTS header 10924 const headerLength = getHeaderLength(data, offset); 10925 if (offset + headerLength <= data.length) { 10926 // retrieve frame size 10927 const frameLength = getFullFrameLength(data, offset) - headerLength; 10928 if (frameLength > 0) { 10929 // logger.log(`AAC frame, offset/length/total/pts:${offset+headerLength}/${frameLength}/${data.byteLength}`); 10930 return { 10931 headerLength, 10932 frameLength 10933 }; 10934 } 10935 } 10936} 10937function appendFrame$2(track, data, offset, pts, frameIndex) { 10938 const frameDuration = getFrameDuration(track.samplerate); 10939 const stamp = pts + frameIndex * frameDuration; 10940 const header = parseFrameHeader(data, offset); 10941 let unit; 10942 if (header) { 10943 const { 10944 frameLength, 10945 headerLength 10946 } = header; 10947 const _length = headerLength + frameLength; 10948 const missing = Math.max(0, offset + _length - data.length); 10949 // logger.log(`AAC frame ${frameIndex}, pts:${stamp} length@offset/total: ${frameLength}@${offset+headerLength}/${data.byteLength} missing: ${missing}`); 10950 if (missing) { 10951 unit = new Uint8Array(_length - headerLength); 10952 unit.set(data.subarray(offset + headerLength, data.length), 0); 10953 } else { 10954 unit = data.subarray(offset + headerLength, offset + _length); 10955 } 10956 const _sample = { 10957 unit, 10958 pts: stamp 10959 }; 10960 if (!missing) { 10961 track.samples.push(_sample); 10962 } 10963 return { 10964 sample: _sample, 10965 length: _length, 10966 missing 10967 }; 10968 } 10969 // overflow incomplete header 10970 const length = data.length - offset; 10971 unit = new Uint8Array(length); 10972 unit.set(data.subarray(offset, data.length), 0); 10973 const sample = { 10974 unit, 10975 pts: stamp 10976 }; 10977 return { 10978 sample, 10979 length, 10980 missing: -1 10981 }; 10982} 10983 10984/** 10985 * Checks if the given data contains an ID3 tag. 10986 * 10987 * @param data - The data to check 10988 * @param offset - The offset at which to start checking 10989 * 10990 * @returns `true` if an ID3 tag is found 10991 * 10992 * @group ID3 10993 * 10994 * @beta 10995 */ 10996function canParseId3(data, offset) { 10997 return isId3Header(data, offset) && readId3Size(data, offset + 6) + 10 <= data.length - offset; 10998} 10999 11000function toArrayBuffer(view) { 11001 if (view instanceof ArrayBuffer) { 11002 return view; 11003 } else { 11004 if (view.byteOffset == 0 && view.byteLength == view.buffer.byteLength) { 11005 // This is a TypedArray over the whole buffer. 11006 return view.buffer; 11007 } 11008 // This is a 'view' on the buffer. Create a new buffer that only contains 11009 // the data. Note that since this isn't an ArrayBuffer, the 'new' call 11010 // will allocate a new buffer to hold the copy. 11011 return new Uint8Array(view).buffer; 11012 } 11013} 11014 11015function toUint8(data, offset = 0, length = Infinity) { 11016 return view(data, offset, length, Uint8Array); 11017} 11018function view(data, offset, length, Type) { 11019 const buffer = unsafeGetArrayBuffer(data); 11020 let bytesPerElement = 1; 11021 if ('BYTES_PER_ELEMENT' in Type) { 11022 bytesPerElement = Type.BYTES_PER_ELEMENT; 11023 } 11024 // Absolute end of the |data| view within |buffer|. 11025 const dataOffset = isArrayBufferView(data) ? data.byteOffset : 0; 11026 const dataEnd = (dataOffset + data.byteLength) / bytesPerElement; 11027 // Absolute start of the result within |buffer|. 11028 const rawStart = (dataOffset + offset) / bytesPerElement; 11029 const start = Math.floor(Math.max(0, Math.min(rawStart, dataEnd))); 11030 // Absolute end of the result within |buffer|. 11031 const end = Math.floor(Math.min(start + Math.max(length, 0), dataEnd)); 11032 return new Type(buffer, start, end - start); 11033} 11034function unsafeGetArrayBuffer(view) { 11035 if (view instanceof ArrayBuffer) { 11036 return view; 11037 } else { 11038 return view.buffer; 11039 } 11040} 11041function isArrayBufferView(obj) { 11042 return obj && obj.buffer instanceof ArrayBuffer && obj.byteLength !== undefined && obj.byteOffset !== undefined; 11043} 11044 11045function decodeId3ImageFrame(frame) { 11046 const metadataFrame = { 11047 key: frame.type, 11048 description: '', 11049 data: '', 11050 mimeType: null, 11051 pictureType: null 11052 }; 11053 const utf8Encoding = 0x03; 11054 if (frame.size < 2) { 11055 return undefined; 11056 } 11057 if (frame.data[0] !== utf8Encoding) { 11058 console.log('Ignore frame with unrecognized character ' + 'encoding'); 11059 return undefined; 11060 } 11061 const mimeTypeEndIndex = frame.data.subarray(1).indexOf(0); 11062 if (mimeTypeEndIndex === -1) { 11063 return undefined; 11064 } 11065 const mimeType = utf8ArrayToStr(toUint8(frame.data, 1, mimeTypeEndIndex)); 11066 const pictureType = frame.data[2 + mimeTypeEndIndex]; 11067 const descriptionEndIndex = frame.data.subarray(3 + mimeTypeEndIndex).indexOf(0); 11068 if (descriptionEndIndex === -1) { 11069 return undefined; 11070 } 11071 const description = utf8ArrayToStr(toUint8(frame.data, 3 + mimeTypeEndIndex, descriptionEndIndex)); 11072 let data; 11073 if (mimeType === '-->') { 11074 data = utf8ArrayToStr(toUint8(frame.data, 4 + mimeTypeEndIndex + descriptionEndIndex)); 11075 } else { 11076 data = toArrayBuffer(frame.data.subarray(4 + mimeTypeEndIndex + descriptionEndIndex)); 11077 } 11078 metadataFrame.mimeType = mimeType; 11079 metadataFrame.pictureType = pictureType; 11080 metadataFrame.description = description; 11081 metadataFrame.data = data; 11082 return metadataFrame; 11083} 11084 11085/** 11086 * Decode an ID3 PRIV frame. 11087 * 11088 * @param frame - the ID3 PRIV frame 11089 * 11090 * @returns The decoded ID3 PRIV frame 11091 * 11092 * @internal 11093 * 11094 * @group ID3 11095 */ 11096function decodeId3PrivFrame(frame) { 11097 /* 11098 Format: <text string>\0<binary data> 11099 */ 11100 if (frame.size < 2) { 11101 return undefined; 11102 } 11103 const owner = utf8ArrayToStr(frame.data, true); 11104 const privateData = new Uint8Array(frame.data.subarray(owner.length + 1)); 11105 return { 11106 key: frame.type, 11107 info: owner, 11108 data: privateData.buffer 11109 }; 11110} 11111 11112/** 11113 * Decodes an ID3 text frame 11114 * 11115 * @param frame - the ID3 text frame 11116 * 11117 * @returns The decoded ID3 text frame 11118 * 11119 * @internal 11120 * 11121 * @group ID3 11122 */ 11123function decodeId3TextFrame(frame) { 11124 if (frame.size < 2) { 11125 return undefined; 11126 } 11127 if (frame.type === 'TXXX') { 11128 /* 11129 Format: 11130 [0] = {Text Encoding} 11131 [1-?] = {Description}\0{Value} 11132 */ 11133 let index = 1; 11134 const description = utf8ArrayToStr(frame.data.subarray(index), true); 11135 index += description.length + 1; 11136 const value = utf8ArrayToStr(frame.data.subarray(index)); 11137 return { 11138 key: frame.type, 11139 info: description, 11140 data: value 11141 }; 11142 } 11143 /* 11144 Format: 11145 [0] = {Text Encoding} 11146 [1-?] = {Value} 11147 */ 11148 const text = utf8ArrayToStr(frame.data.subarray(1)); 11149 return { 11150 key: frame.type, 11151 info: '', 11152 data: text 11153 }; 11154} 11155 11156/** 11157 * Decode a URL frame 11158 * 11159 * @param frame - the ID3 URL frame 11160 * 11161 * @returns The decoded ID3 URL frame 11162 * 11163 * @internal 11164 * 11165 * @group ID3 11166 */ 11167function decodeId3UrlFrame(frame) { 11168 if (frame.type === 'WXXX') { 11169 /* 11170 Format: 11171 [0] = {Text Encoding} 11172 [1-?] = {Description}\0{URL} 11173 */ 11174 if (frame.size < 2) { 11175 return undefined; 11176 } 11177 let index = 1; 11178 const description = utf8ArrayToStr(frame.data.subarray(index), true); 11179 index += description.length + 1; 11180 const value = utf8ArrayToStr(frame.data.subarray(index)); 11181 return { 11182 key: frame.type, 11183 info: description, 11184 data: value 11185 }; 11186 } 11187 /* 11188 Format: 11189 [0-?] = {URL} 11190 */ 11191 const url = utf8ArrayToStr(frame.data); 11192 return { 11193 key: frame.type, 11194 info: '', 11195 data: url 11196 }; 11197} 11198 11199/** 11200 * Decode an ID3 frame. 11201 * 11202 * @param frame - the ID3 frame 11203 * 11204 * @returns The decoded ID3 frame 11205 * 11206 * @internal 11207 * 11208 * @group ID3 11209 */ 11210function decodeId3Frame(frame) { 11211 if (frame.type === 'PRIV') { 11212 return decodeId3PrivFrame(frame); 11213 } else if (frame.type[0] === 'W') { 11214 return decodeId3UrlFrame(frame); 11215 } else if (frame.type === 'APIC') { 11216 return decodeId3ImageFrame(frame); 11217 } 11218 return decodeId3TextFrame(frame); 11219} 11220 11221/** 11222 * Returns the data of an ID3 frame. 11223 * 11224 * @param data - The data to read from 11225 * 11226 * @returns The data of the ID3 frame 11227 * 11228 * @internal 11229 * 11230 * @group ID3 11231 */ 11232function getId3FrameData(data) { 11233 /* 11234 Frame ID $xx xx xx xx (four characters) 11235 Size $xx xx xx xx 11236 Flags $xx xx 11237 */ 11238 const type = String.fromCharCode(data[0], data[1], data[2], data[3]); 11239 const size = readId3Size(data, 4); 11240 // skip frame id, size, and flags 11241 const offset = 10; 11242 return { 11243 type, 11244 size, 11245 data: data.subarray(offset, offset + size) 11246 }; 11247} 11248 11249const HEADER_FOOTER_SIZE = 10; 11250const FRAME_SIZE = 10; 11251/** 11252 * Returns an array of ID3 frames found in all the ID3 tags in the id3Data 11253 * 11254 * @param id3Data - The ID3 data containing one or more ID3 tags 11255 * 11256 * @returns Array of ID3 frame objects 11257 * 11258 * @group ID3 11259 * 11260 * @beta 11261 */ 11262function getId3Frames(id3Data) { 11263 let offset = 0; 11264 const frames = []; 11265 while (isId3Header(id3Data, offset)) { 11266 const size = readId3Size(id3Data, offset + 6); 11267 if (id3Data[offset + 5] >> 6 & 1) { 11268 // skip extended header 11269 offset += HEADER_FOOTER_SIZE; 11270 } 11271 // skip past ID3 header 11272 offset += HEADER_FOOTER_SIZE; 11273 const end = offset + size; 11274 // loop through frames in the ID3 tag 11275 while (offset + FRAME_SIZE < end) { 11276 const frameData = getId3FrameData(id3Data.subarray(offset)); 11277 const frame = decodeId3Frame(frameData); 11278 if (frame) { 11279 frames.push(frame); 11280 } 11281 // skip frame header and frame data 11282 offset += frameData.size + HEADER_FOOTER_SIZE; 11283 } 11284 if (isId3Footer(id3Data, offset)) { 11285 offset += HEADER_FOOTER_SIZE; 11286 } 11287 } 11288 return frames; 11289} 11290 11291/** 11292 * Returns true if the ID3 frame is an Elementary Stream timestamp frame 11293 * 11294 * @param frame - the ID3 frame 11295 * 11296 * @returns `true` if the ID3 frame is an Elementary Stream timestamp frame 11297 * 11298 * @internal 11299 * 11300 * @group ID3 11301 */ 11302function isId3TimestampFrame(frame) { 11303 return frame && frame.key === 'PRIV' && frame.info === 'com.apple.streaming.transportStreamTimestamp'; 11304} 11305 11306/** 11307 * Read a 33 bit timestamp from an ID3 frame. 11308 * 11309 * @param timeStampFrame - the ID3 frame 11310 * 11311 * @returns The timestamp 11312 * 11313 * @internal 11314 * 11315 * @group ID3 11316 */ 11317function readId3Timestamp(timeStampFrame) { 11318 if (timeStampFrame.data.byteLength === 8) { 11319 const data = new Uint8Array(timeStampFrame.data); 11320 // timestamp is 33 bit expressed as a big-endian eight-octet number, 11321 // with the upper 31 bits set to zero. 11322 const pts33Bit = data[3] & 0x1; 11323 let timestamp = (data[4] << 23) + (data[5] << 15) + (data[6] << 7) + data[7]; 11324 timestamp /= 45; 11325 if (pts33Bit) { 11326 timestamp += 47721858.84; 11327 } // 2^32 / 90 11328 return Math.round(timestamp); 11329 } 11330 return undefined; 11331} 11332 11333/** 11334 * Searches for the Elementary Stream timestamp found in the ID3 data chunk 11335 * 11336 * @param data - Block of data containing one or more ID3 tags 11337 * 11338 * @returns The timestamp 11339 * 11340 * @group ID3 11341 * 11342 * @beta 11343 */ 11344function getId3Timestamp(data) { 11345 const frames = getId3Frames(data); 11346 for (let i = 0; i < frames.length; i++) { 11347 const frame = frames[i]; 11348 if (isId3TimestampFrame(frame)) { 11349 return readId3Timestamp(frame); 11350 } 11351 } 11352 return undefined; 11353} 11354 11355let MetadataSchema = /*#__PURE__*/function (MetadataSchema) { 11356 MetadataSchema["audioId3"] = "org.id3"; 11357 MetadataSchema["dateRange"] = "com.apple.quicktime.HLS"; 11358 MetadataSchema["emsg"] = "https://aomedia.org/emsg/ID3"; 11359 MetadataSchema["misbklv"] = "urn:misb:KLV:bin:1910.1"; 11360 return MetadataSchema; 11361}({}); 11362 11363function dummyTrack(type = '', inputTimeScale = 90000) { 11364 return { 11365 type, 11366 id: -1, 11367 pid: -1, 11368 inputTimeScale, 11369 sequenceNumber: -1, 11370 samples: [], 11371 dropped: 0 11372 }; 11373} 11374 11375class BaseAudioDemuxer { 11376 constructor() { 11377 this._audioTrack = void 0; 11378 this._id3Track = void 0; 11379 this.frameIndex = 0; 11380 this.cachedData = null; 11381 this.basePTS = null; 11382 this.initPTS = null; 11383 this.lastPTS = null; 11384 } 11385 resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration) { 11386 this._id3Track = { 11387 type: 'id3', 11388 id: 3, 11389 pid: -1, 11390 inputTimeScale: 90000, 11391 sequenceNumber: 0, 11392 samples: [], 11393 dropped: 0 11394 }; 11395 } 11396 resetTimeStamp(deaultTimestamp) { 11397 this.initPTS = deaultTimestamp; 11398 this.resetContiguity(); 11399 } 11400 resetContiguity() { 11401 this.basePTS = null; 11402 this.lastPTS = null; 11403 this.frameIndex = 0; 11404 } 11405 canParse(data, offset) { 11406 return false; 11407 } 11408 appendFrame(track, data, offset) {} 11409 11410 // feed incoming data to the front of the parsing pipeline 11411 demux(data, timeOffset) { 11412 if (this.cachedData) { 11413 data = appendUint8Array(this.cachedData, data); 11414 this.cachedData = null; 11415 } 11416 let id3Data = getId3Data(data, 0); 11417 let offset = id3Data ? id3Data.length : 0; 11418 let lastDataIndex; 11419 const track = this._audioTrack; 11420 const id3Track = this._id3Track; 11421 const timestamp = id3Data ? getId3Timestamp(id3Data) : undefined; 11422 const length = data.length; 11423 if (this.basePTS === null || this.frameIndex === 0 && isFiniteNumber(timestamp)) { 11424 this.basePTS = initPTSFn(timestamp, timeOffset, this.initPTS); 11425 this.lastPTS = this.basePTS; 11426 } 11427 if (this.lastPTS === null) { 11428 this.lastPTS = this.basePTS; 11429 } 11430 11431 // more expressive than alternative: id3Data?.length 11432 if (id3Data && id3Data.length > 0) { 11433 id3Track.samples.push({ 11434 pts: this.lastPTS, 11435 dts: this.lastPTS, 11436 data: id3Data, 11437 type: MetadataSchema.audioId3, 11438 duration: Number.POSITIVE_INFINITY 11439 }); 11440 } 11441 while (offset < length) { 11442 if (this.canParse(data, offset)) { 11443 const frame = this.appendFrame(track, data, offset); 11444 if (frame) { 11445 this.frameIndex++; 11446 this.lastPTS = frame.sample.pts; 11447 offset += frame.length; 11448 lastDataIndex = offset; 11449 } else { 11450 offset = length; 11451 } 11452 } else if (canParseId3(data, offset)) { 11453 // after a canParse, a call to getId3Data *should* always returns some data 11454 id3Data = getId3Data(data, offset); 11455 id3Track.samples.push({ 11456 pts: this.lastPTS, 11457 dts: this.lastPTS, 11458 data: id3Data, 11459 type: MetadataSchema.audioId3, 11460 duration: Number.POSITIVE_INFINITY 11461 }); 11462 offset += id3Data.length; 11463 lastDataIndex = offset; 11464 } else { 11465 offset++; 11466 } 11467 if (offset === length && lastDataIndex !== length) { 11468 const partialData = data.slice(lastDataIndex); 11469 if (this.cachedData) { 11470 this.cachedData = appendUint8Array(this.cachedData, partialData); 11471 } else { 11472 this.cachedData = partialData; 11473 } 11474 } 11475 } 11476 return { 11477 audioTrack: track, 11478 videoTrack: dummyTrack(), 11479 id3Track, 11480 textTrack: dummyTrack() 11481 }; 11482 } 11483 demuxSampleAes(data, keyData, timeOffset) { 11484 return Promise.reject(new Error(`[${this}] This demuxer does not support Sample-AES decryption`)); 11485 } 11486 flush(timeOffset) { 11487 // Parse cache in case of remaining frames. 11488 const cachedData = this.cachedData; 11489 if (cachedData) { 11490 this.cachedData = null; 11491 this.demux(cachedData, 0); 11492 } 11493 return { 11494 audioTrack: this._audioTrack, 11495 videoTrack: dummyTrack(), 11496 id3Track: this._id3Track, 11497 textTrack: dummyTrack() 11498 }; 11499 } 11500 destroy() { 11501 this.cachedData = null; 11502 // @ts-ignore 11503 this._audioTrack = this._id3Track = undefined; 11504 } 11505} 11506 11507/** 11508 * Initialize PTS 11509 * <p> 11510 * use timestamp unless it is undefined, NaN or Infinity 11511 * </p> 11512 */ 11513const initPTSFn = (timestamp, timeOffset, initPTS) => { 11514 if (isFiniteNumber(timestamp)) { 11515 return timestamp * 90; 11516 } 11517 const init90kHz = initPTS ? initPTS.baseTime * 90000 / initPTS.timescale : 0; 11518 return timeOffset * 90000 + init90kHz; 11519}; 11520 11521/** 11522 * MPEG parser helper 11523 */ 11524 11525let chromeVersion$1 = null; 11526const 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]; 11527const SamplingRateMap = [44100, 48000, 32000, 22050, 24000, 16000, 11025, 12000, 8000]; 11528const SamplesCoefficients = [ 11529// MPEG 2.5 11530[0, 11531// Reserved 1153272, 11533// Layer3 11534144, 11535// Layer2 1153612 // Layer1 11537], 11538// Reserved 11539[0, 11540// Reserved 115410, 11542// Layer3 115430, 11544// Layer2 115450 // Layer1 11546], 11547// MPEG 2 11548[0, 11549// Reserved 1155072, 11551// Layer3 11552144, 11553// Layer2 1155412 // Layer1 11555], 11556// MPEG 1 11557[0, 11558// Reserved 11559144, 11560// Layer3 11561144, 11562// Layer2 1156312 // Layer1 11564]]; 11565const BytesInSlot = [0, 11566// Reserved 115671, 11568// Layer3 115691, 11570// Layer2 115714 // Layer1 11572]; 11573function appendFrame$1(track, data, offset, pts, frameIndex) { 11574 // Using http://www.datavoyage.com/mpgscript/mpeghdr.htm as a reference 11575 if (offset + 24 > data.length) { 11576 return; 11577 } 11578 const header = parseHeader(data, offset); 11579 if (header && offset + header.frameLength <= data.length) { 11580 const frameDuration = header.samplesPerFrame * 90000 / header.sampleRate; 11581 const stamp = pts + frameIndex * frameDuration; 11582 const sample = { 11583 unit: data.subarray(offset, offset + header.frameLength), 11584 pts: stamp, 11585 dts: stamp 11586 }; 11587 track.config = []; 11588 track.channelCount = header.channelCount; 11589 track.samplerate = header.sampleRate; 11590 track.samples.push(sample); 11591 return { 11592 sample, 11593 length: header.frameLength, 11594 missing: 0 11595 }; 11596 } 11597} 11598function parseHeader(data, offset) { 11599 const mpegVersion = data[offset + 1] >> 3 & 3; 11600 const mpegLayer = data[offset + 1] >> 1 & 3; 11601 const bitRateIndex = data[offset + 2] >> 4 & 15; 11602 const sampleRateIndex = data[offset + 2] >> 2 & 3; 11603 if (mpegVersion !== 1 && bitRateIndex !== 0 && bitRateIndex !== 15 && sampleRateIndex !== 3) { 11604 const paddingBit = data[offset + 2] >> 1 & 1; 11605 const channelMode = data[offset + 3] >> 6; 11606 const columnInBitrates = mpegVersion === 3 ? 3 - mpegLayer : mpegLayer === 3 ? 3 : 4; 11607 const bitRate = BitratesMap[columnInBitrates * 14 + bitRateIndex - 1] * 1000; 11608 const columnInSampleRates = mpegVersion === 3 ? 0 : mpegVersion === 2 ? 1 : 2; 11609 const sampleRate = SamplingRateMap[columnInSampleRates * 3 + sampleRateIndex]; 11610 const channelCount = channelMode === 3 ? 1 : 2; // If bits of channel mode are `11` then it is a single channel (Mono) 11611 const sampleCoefficient = SamplesCoefficients[mpegVersion][mpegLayer]; 11612 const bytesInSlot = BytesInSlot[mpegLayer]; 11613 const samplesPerFrame = sampleCoefficient * 8 * bytesInSlot; 11614 const frameLength = Math.floor(sampleCoefficient * bitRate / sampleRate + paddingBit) * bytesInSlot; 11615 if (chromeVersion$1 === null) { 11616 const userAgent = navigator.userAgent || ''; 11617 const result = userAgent.match(/Chrome\/(\d+)/i); 11618 chromeVersion$1 = result ? parseInt(result[1]) : 0; 11619 } 11620 const needChromeFix = !!chromeVersion$1 && chromeVersion$1 <= 87; 11621 if (needChromeFix && mpegLayer === 2 && bitRate >= 224000 && channelMode === 0) { 11622 // Work around bug in Chromium by setting channelMode to dual-channel (01) instead of stereo (00) 11623 data[offset + 3] = data[offset + 3] | 0x80; 11624 } 11625 return { 11626 sampleRate, 11627 channelCount, 11628 frameLength, 11629 samplesPerFrame 11630 }; 11631 } 11632} 11633function isHeaderPattern(data, offset) { 11634 return data[offset] === 0xff && (data[offset + 1] & 0xe0) === 0xe0 && (data[offset + 1] & 0x06) !== 0x00; 11635} 11636function isHeader(data, offset) { 11637 // Look for MPEG header | 1111 1111 | 111X XYZX | where X can be either 0 or 1 and Y or Z should be 1 11638 // Layer bits (position 14 and 15) in header should be always different from 0 (Layer I or Layer II or Layer III) 11639 // More info http://www.mp3-tech.org/programmer/frame_header.html 11640 return offset + 1 < data.length && isHeaderPattern(data, offset); 11641} 11642function canParse(data, offset) { 11643 const headerSize = 4; 11644 return isHeaderPattern(data, offset) && headerSize <= data.length - offset; 11645} 11646function probe(data, offset) { 11647 // same as isHeader but we also check that MPEG frame follows last MPEG frame 11648 // or end of data is reached 11649 if (offset + 1 < data.length && isHeaderPattern(data, offset)) { 11650 // MPEG header Length 11651 const headerLength = 4; 11652 // MPEG frame Length 11653 const header = parseHeader(data, offset); 11654 let frameLength = headerLength; 11655 if (header != null && header.frameLength) { 11656 frameLength = header.frameLength; 11657 } 11658 const newOffset = offset + frameLength; 11659 return newOffset === data.length || isHeader(data, newOffset); 11660 } 11661 return false; 11662} 11663 11664/** 11665 * AAC demuxer 11666 */ 11667class AACDemuxer extends BaseAudioDemuxer { 11668 constructor(observer, config) { 11669 super(); 11670 this.observer = void 0; 11671 this.config = void 0; 11672 this.observer = observer; 11673 this.config = config; 11674 } 11675 resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration) { 11676 super.resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration); 11677 this._audioTrack = { 11678 container: 'audio/adts', 11679 type: 'audio', 11680 id: 2, 11681 pid: -1, 11682 sequenceNumber: 0, 11683 segmentCodec: 'aac', 11684 samples: [], 11685 manifestCodec: audioCodec, 11686 duration: trackDuration, 11687 inputTimeScale: 90000, 11688 dropped: 0 11689 }; 11690 } 11691 11692 // Source for probe info - https://wiki.multimedia.cx/index.php?title=ADTS 11693 static probe(data, logger) { 11694 if (!data) { 11695 return false; 11696 } 11697 11698 // Check for the ADTS sync word 11699 // Look for ADTS header | 1111 1111 | 1111 X00X | where X can be either 0 or 1 11700 // Layer bits (position 14 and 15) in header should be always 0 for ADTS 11701 // More info https://wiki.multimedia.cx/index.php?title=ADTS 11702 const id3Data = getId3Data(data, 0); 11703 let offset = (id3Data == null ? void 0 : id3Data.length) || 0; 11704 if (probe(data, offset)) { 11705 return false; 11706 } 11707 for (let length = data.length; offset < length; offset++) { 11708 if (probe$1(data, offset)) { 11709 logger.log('ADTS sync word found !'); 11710 return true; 11711 } 11712 } 11713 return false; 11714 } 11715 canParse(data, offset) { 11716 return canParse$1(data, offset); 11717 } 11718 appendFrame(track, data, offset) { 11719 initTrackConfig(track, this.observer, data, offset, track.manifestCodec); 11720 const frame = appendFrame$2(track, data, offset, this.basePTS, this.frameIndex); 11721 if (frame && frame.missing === 0) { 11722 return frame; 11723 } 11724 } 11725} 11726 11727const getAudioBSID = (data, offset) => { 11728 // check the bsid to confirm ac-3 | ec-3 11729 let bsid = 0; 11730 let numBits = 5; 11731 offset += numBits; 11732 const temp = new Uint32Array(1); // unsigned 32 bit for temporary storage 11733 const mask = new Uint32Array(1); // unsigned 32 bit mask value 11734 const byte = new Uint8Array(1); // unsigned 8 bit for temporary storage 11735 while (numBits > 0) { 11736 byte[0] = data[offset]; 11737 // read remaining bits, upto 8 bits at a time 11738 const bits = Math.min(numBits, 8); 11739 const shift = 8 - bits; 11740 mask[0] = 0xff000000 >>> 24 + shift << shift; 11741 temp[0] = (byte[0] & mask[0]) >> shift; 11742 bsid = !bsid ? temp[0] : bsid << bits | temp[0]; 11743 offset += 1; 11744 numBits -= bits; 11745 } 11746 return bsid; 11747}; 11748 11749class AC3Demuxer extends BaseAudioDemuxer { 11750 constructor(observer) { 11751 super(); 11752 this.observer = void 0; 11753 this.observer = observer; 11754 } 11755 resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration) { 11756 super.resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration); 11757 this._audioTrack = { 11758 container: 'audio/ac-3', 11759 type: 'audio', 11760 id: 2, 11761 pid: -1, 11762 sequenceNumber: 0, 11763 segmentCodec: 'ac3', 11764 samples: [], 11765 manifestCodec: audioCodec, 11766 duration: trackDuration, 11767 inputTimeScale: 90000, 11768 dropped: 0 11769 }; 11770 } 11771 canParse(data, offset) { 11772 return offset + 64 < data.length; 11773 } 11774 appendFrame(track, data, offset) { 11775 const frameLength = appendFrame(track, data, offset, this.basePTS, this.frameIndex); 11776 if (frameLength !== -1) { 11777 const sample = track.samples[track.samples.length - 1]; 11778 return { 11779 sample, 11780 length: frameLength, 11781 missing: 0 11782 }; 11783 } 11784 } 11785 static probe(data) { 11786 if (!data) { 11787 return false; 11788 } 11789 const id3Data = getId3Data(data, 0); 11790 if (!id3Data) { 11791 return false; 11792 } 11793 11794 // look for the ac-3 sync bytes 11795 const offset = id3Data.length; 11796 if (data[offset] === 0x0b && data[offset + 1] === 0x77 && getId3Timestamp(id3Data) !== undefined && 11797 // check the bsid to confirm ac-3 11798 getAudioBSID(data, offset) < 16) { 11799 return true; 11800 } 11801 return false; 11802 } 11803} 11804function appendFrame(track, data, start, pts, frameIndex) { 11805 if (start + 8 > data.length) { 11806 return -1; // not enough bytes left 11807 } 11808 if (data[start] !== 0x0b || data[start + 1] !== 0x77) { 11809 return -1; // invalid magic 11810 } 11811 11812 // get sample rate 11813 const samplingRateCode = data[start + 4] >> 6; 11814 if (samplingRateCode >= 3) { 11815 return -1; // invalid sampling rate 11816 } 11817 const samplingRateMap = [48000, 44100, 32000]; 11818 const sampleRate = samplingRateMap[samplingRateCode]; 11819 11820 // get frame size 11821 const frameSizeCode = data[start + 4] & 0x3f; 11822 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]; 11823 const frameLength = frameSizeMap[frameSizeCode * 3 + samplingRateCode] * 2; 11824 if (start + frameLength > data.length) { 11825 return -1; 11826 } 11827 11828 // get channel count 11829 const channelMode = data[start + 6] >> 5; 11830 let skipCount = 0; 11831 if (channelMode === 2) { 11832 skipCount += 2; 11833 } else { 11834 if (channelMode & 1 && channelMode !== 1) { 11835 skipCount += 2; 11836 } 11837 if (channelMode & 4) { 11838 skipCount += 2; 11839 } 11840 } 11841 const lfeon = (data[start + 6] << 8 | data[start + 7]) >> 12 - skipCount & 1; 11842 const channelsMap = [2, 1, 2, 3, 3, 4, 4, 5]; 11843 const channelCount = channelsMap[channelMode] + lfeon; 11844 11845 // build dac3 box 11846 const bsid = data[start + 5] >> 3; 11847 const bsmod = data[start + 5] & 7; 11848 const config = new Uint8Array([samplingRateCode << 6 | bsid << 1 | bsmod >> 2, (bsmod & 3) << 6 | channelMode << 3 | lfeon << 2 | frameSizeCode >> 4, frameSizeCode << 4 & 0xe0]); 11849 const frameDuration = 1536 / sampleRate * 90000; 11850 const stamp = pts + frameIndex * frameDuration; 11851 const unit = data.subarray(start, start + frameLength); 11852 track.config = config; 11853 track.channelCount = channelCount; 11854 track.samplerate = sampleRate; 11855 track.samples.push({ 11856 unit, 11857 pts: stamp 11858 }); 11859 return frameLength; 11860} 11861 11862/** 11863 * MP3 demuxer 11864 */ 11865class MP3Demuxer extends BaseAudioDemuxer { 11866 resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration) { 11867 super.resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration); 11868 this._audioTrack = { 11869 container: 'audio/mpeg', 11870 type: 'audio', 11871 id: 2, 11872 pid: -1, 11873 sequenceNumber: 0, 11874 segmentCodec: 'mp3', 11875 samples: [], 11876 manifestCodec: audioCodec, 11877 duration: trackDuration, 11878 inputTimeScale: 90000, 11879 dropped: 0 11880 }; 11881 } 11882 static probe(data) { 11883 if (!data) { 11884 return false; 11885 } 11886 11887 // check if data contains ID3 timestamp and MPEG sync word 11888 // Look for MPEG header | 1111 1111 | 111X XYZX | where X can be either 0 or 1 and Y or Z should be 1 11889 // Layer bits (position 14 and 15) in header should be always different from 0 (Layer I or Layer II or Layer III) 11890 // More info http://www.mp3-tech.org/programmer/frame_header.html 11891 const id3Data = getId3Data(data, 0); 11892 let offset = (id3Data == null ? void 0 : id3Data.length) || 0; 11893 11894 // Check for ac-3|ec-3 sync bytes and return false if present 11895 if (id3Data && data[offset] === 0x0b && data[offset + 1] === 0x77 && getId3Timestamp(id3Data) !== undefined && 11896 // check the bsid to confirm ac-3 or ec-3 (not mp3) 11897 getAudioBSID(data, offset) <= 16) { 11898 return false; 11899 } 11900 for (let length = data.length; offset < length; offset++) { 11901 if (probe(data, offset)) { 11902 logger.log('MPEG Audio sync word found !'); 11903 return true; 11904 } 11905 } 11906 return false; 11907 } 11908 canParse(data, offset) { 11909 return canParse(data, offset); 11910 } 11911 appendFrame(track, data, offset) { 11912 if (this.basePTS === null) { 11913 return; 11914 } 11915 return appendFrame$1(track, data, offset, this.basePTS, this.frameIndex); 11916 } 11917} 11918 11919const emsgSchemePattern = /\/emsg[-/]ID3/i; 11920class MP4Demuxer { 11921 constructor(observer, config) { 11922 this.remainderData = null; 11923 this.timeOffset = 0; 11924 this.config = void 0; 11925 this.videoTrack = void 0; 11926 this.audioTrack = void 0; 11927 this.id3Track = void 0; 11928 this.txtTrack = void 0; 11929 this.config = config; 11930 } 11931 resetTimeStamp() {} 11932 resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration) { 11933 const videoTrack = this.videoTrack = dummyTrack('video', 1); 11934 const audioTrack = this.audioTrack = dummyTrack('audio', 1); 11935 const captionTrack = this.txtTrack = dummyTrack('text', 1); 11936 this.id3Track = dummyTrack('id3', 1); 11937 this.timeOffset = 0; 11938 if (!(initSegment != null && initSegment.byteLength)) { 11939 return; 11940 } 11941 const initData = parseInitSegment(initSegment); 11942 if (initData.video) { 11943 const { 11944 id, 11945 timescale, 11946 codec, 11947 supplemental 11948 } = initData.video; 11949 videoTrack.id = id; 11950 videoTrack.timescale = captionTrack.timescale = timescale; 11951 videoTrack.codec = codec; 11952 videoTrack.supplemental = supplemental; 11953 } 11954 if (initData.audio) { 11955 const { 11956 id, 11957 timescale, 11958 codec 11959 } = initData.audio; 11960 audioTrack.id = id; 11961 audioTrack.timescale = timescale; 11962 audioTrack.codec = codec; 11963 } 11964 captionTrack.id = RemuxerTrackIdConfig.text; 11965 videoTrack.sampleDuration = 0; 11966 videoTrack.duration = audioTrack.duration = trackDuration; 11967 } 11968 resetContiguity() { 11969 this.remainderData = null; 11970 } 11971 static probe(data) { 11972 return hasMoofData(data); 11973 } 11974 demux(data, timeOffset) { 11975 this.timeOffset = timeOffset; 11976 // Load all data into the avc track. The CMAF remuxer will look for the data in the samples object; the rest of the fields do not matter 11977 let videoSamples = data; 11978 const videoTrack = this.videoTrack; 11979 const textTrack = this.txtTrack; 11980 if (this.config.progressive) { 11981 // Split the bytestream into two ranges: one encompassing all data up until the start of the last moof, and everything else. 11982 // This is done to guarantee that we're sending valid data to MSE - when demuxing progressively, we have no guarantee 11983 // that the fetch loader gives us flush moof+mdat pairs. If we push jagged data to MSE, it will throw an exception. 11984 if (this.remainderData) { 11985 videoSamples = appendUint8Array(this.remainderData, data); 11986 } 11987 const segmentedData = segmentValidRange(videoSamples); 11988 this.remainderData = segmentedData.remainder; 11989 videoTrack.samples = segmentedData.valid || new Uint8Array(); 11990 } else { 11991 videoTrack.samples = videoSamples; 11992 } 11993 const id3Track = this.extractID3Track(videoTrack, timeOffset); 11994 textTrack.samples = parseSamples(timeOffset, videoTrack); 11995 return { 11996 videoTrack, 11997 audioTrack: this.audioTrack, 11998 id3Track, 11999 textTrack: this.txtTrack 12000 }; 12001 } 12002 flush() { 12003 const timeOffset = this.timeOffset; 12004 const videoTrack = this.videoTrack; 12005 const textTrack = this.txtTrack; 12006 videoTrack.samples = this.remainderData || new Uint8Array(); 12007 this.remainderData = null; 12008 const id3Track = this.extractID3Track(videoTrack, this.timeOffset); 12009 textTrack.samples = parseSamples(timeOffset, videoTrack); 12010 return { 12011 videoTrack, 12012 audioTrack: dummyTrack(), 12013 id3Track, 12014 textTrack: dummyTrack() 12015 }; 12016 } 12017 extractID3Track(videoTrack, timeOffset) { 12018 const id3Track = this.id3Track; 12019 if (videoTrack.samples.length) { 12020 const emsgs = findBox(videoTrack.samples, ['emsg']); 12021 if (emsgs) { 12022 emsgs.forEach(data => { 12023 const emsgInfo = parseEmsg(data); 12024 if (emsgSchemePattern.test(emsgInfo.schemeIdUri)) { 12025 const pts = getEmsgStartTime(emsgInfo, timeOffset); 12026 let duration = emsgInfo.eventDuration === 0xffffffff ? Number.POSITIVE_INFINITY : emsgInfo.eventDuration / emsgInfo.timeScale; 12027 // Safari takes anything <= 0.001 seconds and maps it to Infinity 12028 if (duration <= 0.001) { 12029 duration = Number.POSITIVE_INFINITY; 12030 } 12031 const payload = emsgInfo.payload; 12032 id3Track.samples.push({ 12033 data: payload, 12034 len: payload.byteLength, 12035 dts: pts, 12036 pts: pts, 12037 type: MetadataSchema.emsg, 12038 duration: duration 12039 }); 12040 } else if (this.config.enableEmsgKLVMetadata && emsgInfo.schemeIdUri.startsWith('urn:misb:KLV:bin:1910.1')) { 12041 const pts = getEmsgStartTime(emsgInfo, timeOffset); 12042 id3Track.samples.push({ 12043 data: emsgInfo.payload, 12044 len: emsgInfo.payload.byteLength, 12045 dts: pts, 12046 pts: pts, 12047 type: MetadataSchema.misbklv, 12048 duration: Number.POSITIVE_INFINITY 12049 }); 12050 } 12051 }); 12052 } 12053 } 12054 return id3Track; 12055 } 12056 demuxSampleAes(data, keyData, timeOffset) { 12057 return Promise.reject(new Error('The MP4 demuxer does not support SAMPLE-AES decryption')); 12058 } 12059 destroy() { 12060 // @ts-ignore 12061 this.config = null; 12062 this.remainderData = null; 12063 this.videoTrack = this.audioTrack = this.id3Track = this.txtTrack = undefined; 12064 } 12065} 12066function getEmsgStartTime(emsgInfo, timeOffset) { 12067 return isFiniteNumber(emsgInfo.presentationTime) ? emsgInfo.presentationTime / emsgInfo.timeScale : timeOffset + emsgInfo.presentationTimeDelta / emsgInfo.timeScale; 12068} 12069 12070/** 12071 * SAMPLE-AES decrypter 12072 */ 12073 12074class SampleAesDecrypter { 12075 constructor(observer, config, keyData) { 12076 this.keyData = void 0; 12077 this.decrypter = void 0; 12078 this.keyData = keyData; 12079 this.decrypter = new Decrypter(config, { 12080 removePKCS7Padding: false 12081 }); 12082 } 12083 decryptBuffer(encryptedData) { 12084 return this.decrypter.decrypt(encryptedData, this.keyData.key.buffer, this.keyData.iv.buffer, DecrypterAesMode.cbc); 12085 } 12086 12087 // AAC - encrypt all full 16 bytes blocks starting from offset 16 12088 decryptAacSample(samples, sampleIndex, callback) { 12089 const curUnit = samples[sampleIndex].unit; 12090 if (curUnit.length <= 16) { 12091 // No encrypted portion in this sample (first 16 bytes is not 12092 // encrypted, see https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/HLS_Sample_Encryption/Encryption/Encryption.html), 12093 return; 12094 } 12095 const encryptedData = curUnit.subarray(16, curUnit.length - curUnit.length % 16); 12096 const encryptedBuffer = encryptedData.buffer.slice(encryptedData.byteOffset, encryptedData.byteOffset + encryptedData.length); 12097 this.decryptBuffer(encryptedBuffer).then(decryptedBuffer => { 12098 const decryptedData = new Uint8Array(decryptedBuffer); 12099 curUnit.set(decryptedData, 16); 12100 if (!this.decrypter.isSync()) { 12101 this.decryptAacSamples(samples, sampleIndex + 1, callback); 12102 } 12103 }).catch(callback); 12104 } 12105 decryptAacSamples(samples, sampleIndex, callback) { 12106 for (;; sampleIndex++) { 12107 if (sampleIndex >= samples.length) { 12108 callback(); 12109 return; 12110 } 12111 if (samples[sampleIndex].unit.length < 32) { 12112 continue; 12113 } 12114 this.decryptAacSample(samples, sampleIndex, callback); 12115 if (!this.decrypter.isSync()) { 12116 return; 12117 } 12118 } 12119 } 12120 12121 // AVC - encrypt one 16 bytes block out of ten, starting from offset 32 12122 getAvcEncryptedData(decodedData) { 12123 const encryptedDataLen = Math.floor((decodedData.length - 48) / 160) * 16 + 16; 12124 const encryptedData = new Int8Array(encryptedDataLen); 12125 let outputPos = 0; 12126 for (let inputPos = 32; inputPos < decodedData.length - 16; inputPos += 160, outputPos += 16) { 12127 encryptedData.set(decodedData.subarray(inputPos, inputPos + 16), outputPos); 12128 } 12129 return encryptedData; 12130 } 12131 getAvcDecryptedUnit(decodedData, decryptedData) { 12132 const uint8DecryptedData = new Uint8Array(decryptedData); 12133 let inputPos = 0; 12134 for (let outputPos = 32; outputPos < decodedData.length - 16; outputPos += 160, inputPos += 16) { 12135 decodedData.set(uint8DecryptedData.subarray(inputPos, inputPos + 16), outputPos); 12136 } 12137 return decodedData; 12138 } 12139 decryptAvcSample(samples, sampleIndex, unitIndex, callback, curUnit) { 12140 const decodedData = discardEPB(curUnit.data); 12141 const encryptedData = this.getAvcEncryptedData(decodedData); 12142 this.decryptBuffer(encryptedData.buffer).then(decryptedBuffer => { 12143 curUnit.data = this.getAvcDecryptedUnit(decodedData, decryptedBuffer); 12144 if (!this.decrypter.isSync()) { 12145 this.decryptAvcSamples(samples, sampleIndex, unitIndex + 1, callback); 12146 } 12147 }).catch(callback); 12148 } 12149 decryptAvcSamples(samples, sampleIndex, unitIndex, callback) { 12150 if (samples instanceof Uint8Array) { 12151 throw new Error('Cannot decrypt samples of type Uint8Array'); 12152 } 12153 for (;; sampleIndex++, unitIndex = 0) { 12154 if (sampleIndex >= samples.length) { 12155 callback(); 12156 return; 12157 } 12158 const curUnits = samples[sampleIndex].units; 12159 for (;; unitIndex++) { 12160 if (unitIndex >= curUnits.length) { 12161 break; 12162 } 12163 const curUnit = curUnits[unitIndex]; 12164 if (curUnit.data.length <= 48 || curUnit.type !== 1 && curUnit.type !== 5) { 12165 continue; 12166 } 12167 this.decryptAvcSample(samples, sampleIndex, unitIndex, callback, curUnit); 12168 if (!this.decrypter.isSync()) { 12169 return; 12170 } 12171 } 12172 } 12173 } 12174} 12175 12176class BaseVideoParser { 12177 constructor() { 12178 this.VideoSample = null; 12179 } 12180 createVideoSample(key, pts, dts) { 12181 return { 12182 key, 12183 frame: false, 12184 pts, 12185 dts, 12186 units: [], 12187 length: 0 12188 }; 12189 } 12190 getLastNalUnit(samples) { 12191 var _VideoSample; 12192 let VideoSample = this.VideoSample; 12193 let lastUnit; 12194 // try to fallback to previous sample if current one is empty 12195 if (!VideoSample || VideoSample.units.length === 0) { 12196 VideoSample = samples[samples.length - 1]; 12197 } 12198 if ((_VideoSample = VideoSample) != null && _VideoSample.units) { 12199 const units = VideoSample.units; 12200 lastUnit = units[units.length - 1]; 12201 } 12202 return lastUnit; 12203 } 12204 pushAccessUnit(VideoSample, videoTrack) { 12205 if (VideoSample.units.length && VideoSample.frame) { 12206 // if sample does not have PTS/DTS, patch with last sample PTS/DTS 12207 if (VideoSample.pts === undefined) { 12208 const samples = videoTrack.samples; 12209 const nbSamples = samples.length; 12210 if (nbSamples) { 12211 const lastSample = samples[nbSamples - 1]; 12212 VideoSample.pts = lastSample.pts; 12213 VideoSample.dts = lastSample.dts; 12214 } else { 12215 // dropping samples, no timestamp found 12216 videoTrack.dropped++; 12217 return; 12218 } 12219 } 12220 videoTrack.samples.push(VideoSample); 12221 } 12222 } 12223 parseNALu(track, array, endOfSegment) { 12224 const len = array.byteLength; 12225 let state = track.naluState || 0; 12226 const lastState = state; 12227 const units = []; 12228 let i = 0; 12229 let value; 12230 let overflow; 12231 let unitType; 12232 let lastUnitStart = -1; 12233 let lastUnitType = 0; 12234 // logger.log('PES:' + Hex.hexDump(array)); 12235 12236 if (state === -1) { 12237 // special use case where we found 3 or 4-byte start codes exactly at the end of previous PES packet 12238 lastUnitStart = 0; 12239 // NALu type is value read from offset 0 12240 lastUnitType = this.getNALuType(array, 0); 12241 state = 0; 12242 i = 1; 12243 } 12244 while (i < len) { 12245 value = array[i++]; 12246 // optimization. state 0 and 1 are the predominant case. let's handle them outside of the switch/case 12247 if (!state) { 12248 state = value ? 0 : 1; 12249 continue; 12250 } 12251 if (state === 1) { 12252 state = value ? 0 : 2; 12253 continue; 12254 } 12255 // here we have state either equal to 2 or 3 12256 if (!value) { 12257 state = 3; 12258 } else if (value === 1) { 12259 overflow = i - state - 1; 12260 if (lastUnitStart >= 0) { 12261 const unit = { 12262 data: array.subarray(lastUnitStart, overflow), 12263 type: lastUnitType 12264 }; 12265 // logger.log('pushing NALU, type/size:' + unit.type + '/' + unit.data.byteLength); 12266 units.push(unit); 12267 } else { 12268 // lastUnitStart is undefined => this is the first start code found in this PES packet 12269 // first check if start code delimiter is overlapping between 2 PES packets, 12270 // ie it started in last packet (lastState not zero) 12271 // and ended at the beginning of this PES packet (i <= 4 - lastState) 12272 const lastUnit = this.getLastNalUnit(track.samples); 12273 if (lastUnit) { 12274 if (lastState && i <= 4 - lastState) { 12275 // start delimiter overlapping between PES packets 12276 // strip start delimiter bytes from the end of last NAL unit 12277 // check if lastUnit had a state different from zero 12278 if (lastUnit.state) { 12279 // strip last bytes 12280 lastUnit.data = lastUnit.data.subarray(0, lastUnit.data.byteLength - lastState); 12281 } 12282 } 12283 // If NAL units are not starting right at the beginning of the PES packet, push preceding data into previous NAL unit. 12284 12285 if (overflow > 0) { 12286 // logger.log('first NALU found with overflow:' + overflow); 12287 lastUnit.data = appendUint8Array(lastUnit.data, array.subarray(0, overflow)); 12288 lastUnit.state = 0; 12289 } 12290 } 12291 } 12292 // check if we can read unit type 12293 if (i < len) { 12294 unitType = this.getNALuType(array, i); 12295 // logger.log('find NALU @ offset:' + i + ',type:' + unitType); 12296 lastUnitStart = i; 12297 lastUnitType = unitType; 12298 state = 0; 12299 } else { 12300 // not enough byte to read unit type. let's read it on next PES parsing 12301 state = -1; 12302 } 12303 } else { 12304 state = 0; 12305 } 12306 } 12307 if (lastUnitStart >= 0 && state >= 0) { 12308 const unit = { 12309 data: array.subarray(lastUnitStart, len), 12310 type: lastUnitType, 12311 state: state 12312 }; 12313 units.push(unit); 12314 // logger.log('pushing NALU, type/size/state:' + unit.type + '/' + unit.data.byteLength + '/' + state); 12315 } 12316 // no NALu found 12317 if (units.length === 0) { 12318 // append pes.data to previous NAL unit 12319 const lastUnit = this.getLastNalUnit(track.samples); 12320 if (lastUnit) { 12321 lastUnit.data = appendUint8Array(lastUnit.data, array); 12322 } 12323 } 12324 track.naluState = state; 12325 return units; 12326 } 12327} 12328 12329/** 12330 * Parser for exponential Golomb codes, a variable-bitwidth number encoding scheme used by h264. 12331 */ 12332 12333class ExpGolomb { 12334 constructor(data) { 12335 this.data = void 0; 12336 this.bytesAvailable = void 0; 12337 this.word = void 0; 12338 this.bitsAvailable = void 0; 12339 this.data = data; 12340 // the number of bytes left to examine in this.data 12341 this.bytesAvailable = data.byteLength; 12342 // the current word being examined 12343 this.word = 0; // :uint 12344 // the number of bits left to examine in the current word 12345 this.bitsAvailable = 0; // :uint 12346 } 12347 12348 // ():void 12349 loadWord() { 12350 const data = this.data; 12351 const bytesAvailable = this.bytesAvailable; 12352 const position = data.byteLength - bytesAvailable; 12353 const workingBytes = new Uint8Array(4); 12354 const availableBytes = Math.min(4, bytesAvailable); 12355 if (availableBytes === 0) { 12356 throw new Error('no bytes available'); 12357 } 12358 workingBytes.set(data.subarray(position, position + availableBytes)); 12359 this.word = new DataView(workingBytes.buffer).getUint32(0); 12360 // track the amount of this.data that has been processed 12361 this.bitsAvailable = availableBytes * 8; 12362 this.bytesAvailable -= availableBytes; 12363 } 12364 12365 // (count:int):void 12366 skipBits(count) { 12367 let skipBytes; // :int 12368 count = Math.min(count, this.bytesAvailable * 8 + this.bitsAvailable); 12369 if (this.bitsAvailable > count) { 12370 this.word <<= count; 12371 this.bitsAvailable -= count; 12372 } else { 12373 count -= this.bitsAvailable; 12374 skipBytes = count >> 3; 12375 count -= skipBytes << 3; 12376 this.bytesAvailable -= skipBytes; 12377 this.loadWord(); 12378 this.word <<= count; 12379 this.bitsAvailable -= count; 12380 } 12381 } 12382 12383 // (size:int):uint 12384 readBits(size) { 12385 let bits = Math.min(this.bitsAvailable, size); // :uint 12386 const valu = this.word >>> 32 - bits; // :uint 12387 if (size > 32) { 12388 logger.error('Cannot read more than 32 bits at a time'); 12389 } 12390 this.bitsAvailable -= bits; 12391 if (this.bitsAvailable > 0) { 12392 this.word <<= bits; 12393 } else if (this.bytesAvailable > 0) { 12394 this.loadWord(); 12395 } else { 12396 throw new Error('no bits available'); 12397 } 12398 bits = size - bits; 12399 if (bits > 0 && this.bitsAvailable) { 12400 return valu << bits | this.readBits(bits); 12401 } else { 12402 return valu; 12403 } 12404 } 12405 12406 // ():uint 12407 skipLZ() { 12408 let leadingZeroCount; // :uint 12409 for (leadingZeroCount = 0; leadingZeroCount < this.bitsAvailable; ++leadingZeroCount) { 12410 if ((this.word & 0x80000000 >>> leadingZeroCount) !== 0) { 12411 // the first bit of working word is 1 12412 this.word <<= leadingZeroCount; 12413 this.bitsAvailable -= leadingZeroCount; 12414 return leadingZeroCount; 12415 } 12416 } 12417 // we exhausted word and still have not found a 1 12418 this.loadWord(); 12419 return leadingZeroCount + this.skipLZ(); 12420 } 12421 12422 // ():void 12423 skipUEG() { 12424 this.skipBits(1 + this.skipLZ()); 12425 } 12426 12427 // ():void 12428 skipEG() { 12429 this.skipBits(1 + this.skipLZ()); 12430 } 12431 12432 // ():uint 12433 readUEG() { 12434 const clz = this.skipLZ(); // :uint 12435 return this.readBits(clz + 1) - 1; 12436 } 12437 12438 // ():int 12439 readEG() { 12440 const valu = this.readUEG(); // :int 12441 if (0x01 & valu) { 12442 // the number is odd if the low order bit is set 12443 return 1 + valu >>> 1; // add 1 to make it even, and divide by 2 12444 } else { 12445 return -1 * (valu >>> 1); // divide by two then make it negative 12446 } 12447 } 12448 12449 // Some convenience functions 12450 // :Boolean 12451 readBoolean() { 12452 return this.readBits(1) === 1; 12453 } 12454 12455 // ():int 12456 readUByte() { 12457 return this.readBits(8); 12458 } 12459 12460 // ():int 12461 readUShort() { 12462 return this.readBits(16); 12463 } 12464 12465 // ():int 12466 readUInt() { 12467 return this.readBits(32); 12468 } 12469} 12470 12471class AvcVideoParser extends BaseVideoParser { 12472 parsePES(track, textTrack, pes, endOfSegment) { 12473 const units = this.parseNALu(track, pes.data, endOfSegment); 12474 let VideoSample = this.VideoSample; 12475 let push; 12476 let spsfound = false; 12477 // free pes.data to save up some memory 12478 pes.data = null; 12479 12480 // if new NAL units found and last sample still there, let's push ... 12481 // this helps parsing streams with missing AUD (only do this if AUD never found) 12482 if (VideoSample && units.length && !track.audFound) { 12483 this.pushAccessUnit(VideoSample, track); 12484 VideoSample = this.VideoSample = this.createVideoSample(false, pes.pts, pes.dts); 12485 } 12486 units.forEach(unit => { 12487 var _VideoSample2, _VideoSample3; 12488 switch (unit.type) { 12489 // NDR 12490 case 1: 12491 { 12492 let iskey = false; 12493 push = true; 12494 const data = unit.data; 12495 // only check slice type to detect KF in case SPS found in same packet (any keyframe is preceded by SPS ...) 12496 if (spsfound && data.length > 4) { 12497 // retrieve slice type by parsing beginning of NAL unit (follow H264 spec, slice_header definition) to detect keyframe embedded in NDR 12498 const sliceType = this.readSliceType(data); 12499 // 2 : I slice, 4 : SI slice, 7 : I slice, 9: SI slice 12500 // SI slice : A slice that is coded using intra prediction only and using quantisation of the prediction samples. 12501 // An SI slice can be coded such that its decoded samples can be constructed identically to an SP slice. 12502 // I slice: A slice that is not an SI slice that is decoded using intra prediction only. 12503 // if (sliceType === 2 || sliceType === 7) { 12504 if (sliceType === 2 || sliceType === 4 || sliceType === 7 || sliceType === 9) { 12505 iskey = true; 12506 } 12507 } 12508 if (iskey) { 12509 var _VideoSample; 12510 // if we have non-keyframe data already, that cannot belong to the same frame as a keyframe, so force a push 12511 if ((_VideoSample = VideoSample) != null && _VideoSample.frame && !VideoSample.key) { 12512 this.pushAccessUnit(VideoSample, track); 12513 VideoSample = this.VideoSample = null; 12514 } 12515 } 12516 if (!VideoSample) { 12517 VideoSample = this.VideoSample = this.createVideoSample(true, pes.pts, pes.dts); 12518 } 12519 VideoSample.frame = true; 12520 VideoSample.key = iskey; 12521 break; 12522 // IDR 12523 } 12524 case 5: 12525 push = true; 12526 // handle PES not starting with AUD 12527 // if we have frame data already, that cannot belong to the same frame, so force a push 12528 if ((_VideoSample2 = VideoSample) != null && _VideoSample2.frame && !VideoSample.key) { 12529 this.pushAccessUnit(VideoSample, track); 12530 VideoSample = this.VideoSample = null; 12531 } 12532 if (!VideoSample) { 12533 VideoSample = this.VideoSample = this.createVideoSample(true, pes.pts, pes.dts); 12534 } 12535 VideoSample.key = true; 12536 VideoSample.frame = true; 12537 break; 12538 // SEI 12539 case 6: 12540 { 12541 push = true; 12542 parseSEIMessageFromNALu(unit.data, 1, pes.pts, textTrack.samples); 12543 break; 12544 // SPS 12545 } 12546 case 7: 12547 { 12548 var _track$pixelRatio, _track$pixelRatio2; 12549 push = true; 12550 spsfound = true; 12551 const sps = unit.data; 12552 const config = this.readSPS(sps); 12553 if (!track.sps || track.width !== config.width || track.height !== config.height || ((_track$pixelRatio = track.pixelRatio) == null ? void 0 : _track$pixelRatio[0]) !== config.pixelRatio[0] || ((_track$pixelRatio2 = track.pixelRatio) == null ? void 0 : _track$pixelRatio2[1]) !== config.pixelRatio[1]) { 12554 track.width = config.width; 12555 track.height = config.height; 12556 track.pixelRatio = config.pixelRatio; 12557 track.sps = [sps]; 12558 const codecarray = sps.subarray(1, 4); 12559 let codecstring = 'avc1.'; 12560 for (let i = 0; i < 3; i++) { 12561 let h = codecarray[i].toString(16); 12562 if (h.length < 2) { 12563 h = '0' + h; 12564 } 12565 codecstring += h; 12566 } 12567 track.codec = codecstring; 12568 } 12569 break; 12570 } 12571 // PPS 12572 case 8: 12573 push = true; 12574 track.pps = [unit.data]; 12575 break; 12576 // AUD 12577 case 9: 12578 push = true; 12579 track.audFound = true; 12580 if ((_VideoSample3 = VideoSample) != null && _VideoSample3.frame) { 12581 this.pushAccessUnit(VideoSample, track); 12582 VideoSample = null; 12583 } 12584 if (!VideoSample) { 12585 VideoSample = this.VideoSample = this.createVideoSample(false, pes.pts, pes.dts); 12586 } 12587 break; 12588 // Filler Data 12589 case 12: 12590 push = true; 12591 break; 12592 default: 12593 push = false; 12594 break; 12595 } 12596 if (VideoSample && push) { 12597 const units = VideoSample.units; 12598 units.push(unit); 12599 } 12600 }); 12601 // if last PES packet, push samples 12602 if (endOfSegment && VideoSample) { 12603 this.pushAccessUnit(VideoSample, track); 12604 this.VideoSample = null; 12605 } 12606 } 12607 getNALuType(data, offset) { 12608 return data[offset] & 0x1f; 12609 } 12610 readSliceType(data) { 12611 const eg = new ExpGolomb(data); 12612 // skip NALu type 12613 eg.readUByte(); 12614 // discard first_mb_in_slice 12615 eg.readUEG(); 12616 // return slice_type 12617 return eg.readUEG(); 12618 } 12619 12620 /** 12621 * The scaling list is optionally transmitted as part of a sequence parameter 12622 * set and is not relevant to transmuxing. 12623 * @param count the number of entries in this scaling list 12624 * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1 12625 */ 12626 skipScalingList(count, reader) { 12627 let lastScale = 8; 12628 let nextScale = 8; 12629 let deltaScale; 12630 for (let j = 0; j < count; j++) { 12631 if (nextScale !== 0) { 12632 deltaScale = reader.readEG(); 12633 nextScale = (lastScale + deltaScale + 256) % 256; 12634 } 12635 lastScale = nextScale === 0 ? lastScale : nextScale; 12636 } 12637 } 12638 12639 /** 12640 * Read a sequence parameter set and return some interesting video 12641 * properties. A sequence parameter set is the H264 metadata that 12642 * describes the properties of upcoming video frames. 12643 * @returns an object with configuration parsed from the 12644 * sequence parameter set, including the dimensions of the 12645 * associated video frames. 12646 */ 12647 readSPS(sps) { 12648 const eg = new ExpGolomb(sps); 12649 let frameCropLeftOffset = 0; 12650 let frameCropRightOffset = 0; 12651 let frameCropTopOffset = 0; 12652 let frameCropBottomOffset = 0; 12653 let numRefFramesInPicOrderCntCycle; 12654 let scalingListCount; 12655 let i; 12656 const readUByte = eg.readUByte.bind(eg); 12657 const readBits = eg.readBits.bind(eg); 12658 const readUEG = eg.readUEG.bind(eg); 12659 const readBoolean = eg.readBoolean.bind(eg); 12660 const skipBits = eg.skipBits.bind(eg); 12661 const skipEG = eg.skipEG.bind(eg); 12662 const skipUEG = eg.skipUEG.bind(eg); 12663 const skipScalingList = this.skipScalingList.bind(this); 12664 readUByte(); 12665 const profileIdc = readUByte(); // profile_idc 12666 readBits(5); // profileCompat constraint_set[0-4]_flag, u(5) 12667 skipBits(3); // reserved_zero_3bits u(3), 12668 readUByte(); // level_idc u(8) 12669 skipUEG(); // seq_parameter_set_id 12670 // some profiles have more optional data we don't need 12671 if (profileIdc === 100 || profileIdc === 110 || profileIdc === 122 || profileIdc === 244 || profileIdc === 44 || profileIdc === 83 || profileIdc === 86 || profileIdc === 118 || profileIdc === 128) { 12672 const chromaFormatIdc = readUEG(); 12673 if (chromaFormatIdc === 3) { 12674 skipBits(1); 12675 } // separate_colour_plane_flag 12676 12677 skipUEG(); // bit_depth_luma_minus8 12678 skipUEG(); // bit_depth_chroma_minus8 12679 skipBits(1); // qpprime_y_zero_transform_bypass_flag 12680 if (readBoolean()) { 12681 // seq_scaling_matrix_present_flag 12682 scalingListCount = chromaFormatIdc !== 3 ? 8 : 12; 12683 for (i = 0; i < scalingListCount; i++) { 12684 if (readBoolean()) { 12685 // seq_scaling_list_present_flag[ i ] 12686 if (i < 6) { 12687 skipScalingList(16, eg); 12688 } else { 12689 skipScalingList(64, eg); 12690 } 12691 } 12692 } 12693 } 12694 } 12695 skipUEG(); // log2_max_frame_num_minus4 12696 const picOrderCntType = readUEG(); 12697 if (picOrderCntType === 0) { 12698 readUEG(); // log2_max_pic_order_cnt_lsb_minus4 12699 } else if (picOrderCntType === 1) { 12700 skipBits(1); // delta_pic_order_always_zero_flag 12701 skipEG(); // offset_for_non_ref_pic 12702 skipEG(); // offset_for_top_to_bottom_field 12703 numRefFramesInPicOrderCntCycle = readUEG(); 12704 for (i = 0; i < numRefFramesInPicOrderCntCycle; i++) { 12705 skipEG(); 12706 } // offset_for_ref_frame[ i ] 12707 } 12708 skipUEG(); // max_num_ref_frames 12709 skipBits(1); // gaps_in_frame_num_value_allowed_flag 12710 const picWidthInMbsMinus1 = readUEG(); 12711 const picHeightInMapUnitsMinus1 = readUEG(); 12712 const frameMbsOnlyFlag = readBits(1); 12713 if (frameMbsOnlyFlag === 0) { 12714 skipBits(1); 12715 } // mb_adaptive_frame_field_flag 12716 12717 skipBits(1); // direct_8x8_inference_flag 12718 if (readBoolean()) { 12719 // frame_cropping_flag 12720 frameCropLeftOffset = readUEG(); 12721 frameCropRightOffset = readUEG(); 12722 frameCropTopOffset = readUEG(); 12723 frameCropBottomOffset = readUEG(); 12724 } 12725 let pixelRatio = [1, 1]; 12726 if (readBoolean()) { 12727 // vui_parameters_present_flag 12728 if (readBoolean()) { 12729 // aspect_ratio_info_present_flag 12730 const aspectRatioIdc = readUByte(); 12731 switch (aspectRatioIdc) { 12732 case 1: 12733 pixelRatio = [1, 1]; 12734 break; 12735 case 2: 12736 pixelRatio = [12, 11]; 12737 break; 12738 case 3: 12739 pixelRatio = [10, 11]; 12740 break; 12741 case 4: 12742 pixelRatio = [16, 11]; 12743 break; 12744 case 5: 12745 pixelRatio = [40, 33]; 12746 break; 12747 case 6: 12748 pixelRatio = [24, 11]; 12749 break; 12750 case 7: 12751 pixelRatio = [20, 11]; 12752 break; 12753 case 8: 12754 pixelRatio = [32, 11]; 12755 break; 12756 case 9: 12757 pixelRatio = [80, 33]; 12758 break; 12759 case 10: 12760 pixelRatio = [18, 11]; 12761 break; 12762 case 11: 12763 pixelRatio = [15, 11]; 12764 break; 12765 case 12: 12766 pixelRatio = [64, 33]; 12767 break; 12768 case 13: 12769 pixelRatio = [160, 99]; 12770 break; 12771 case 14: 12772 pixelRatio = [4, 3]; 12773 break; 12774 case 15: 12775 pixelRatio = [3, 2]; 12776 break; 12777 case 16: 12778 pixelRatio = [2, 1]; 12779 break; 12780 case 255: 12781 { 12782 pixelRatio = [readUByte() << 8 | readUByte(), readUByte() << 8 | readUByte()]; 12783 break; 12784 } 12785 } 12786 } 12787 } 12788 return { 12789 width: Math.ceil((picWidthInMbsMinus1 + 1) * 16 - frameCropLeftOffset * 2 - frameCropRightOffset * 2), 12790 height: (2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16 - (frameMbsOnlyFlag ? 2 : 4) * (frameCropTopOffset + frameCropBottomOffset), 12791 pixelRatio: pixelRatio 12792 }; 12793 } 12794} 12795 12796class HevcVideoParser extends BaseVideoParser { 12797 constructor(...args) { 12798 super(...args); 12799 this.initVPS = null; 12800 } 12801 parsePES(track, textTrack, pes, endOfSegment) { 12802 const units = this.parseNALu(track, pes.data, endOfSegment); 12803 let VideoSample = this.VideoSample; 12804 let push; 12805 let spsfound = false; 12806 // free pes.data to save up some memory 12807 pes.data = null; 12808 12809 // if new NAL units found and last sample still there, let's push ... 12810 // this helps parsing streams with missing AUD (only do this if AUD never found) 12811 if (VideoSample && units.length && !track.audFound) { 12812 this.pushAccessUnit(VideoSample, track); 12813 VideoSample = this.VideoSample = this.createVideoSample(false, pes.pts, pes.dts); 12814 } 12815 units.forEach(unit => { 12816 var _VideoSample2, _VideoSample3; 12817 switch (unit.type) { 12818 // NON-IDR, NON RANDOM ACCESS SLICE 12819 case 0: 12820 case 1: 12821 case 2: 12822 case 3: 12823 case 4: 12824 case 5: 12825 case 6: 12826 case 7: 12827 case 8: 12828 case 9: 12829 if (!VideoSample) { 12830 VideoSample = this.VideoSample = this.createVideoSample(false, pes.pts, pes.dts); 12831 } 12832 VideoSample.frame = true; 12833 push = true; 12834 break; 12835 12836 // CRA, BLA (random access picture) 12837 case 16: 12838 case 17: 12839 case 18: 12840 case 21: 12841 push = true; 12842 if (spsfound) { 12843 var _VideoSample; 12844 // handle PES not starting with AUD 12845 // if we have frame data already, that cannot belong to the same frame, so force a push 12846 if ((_VideoSample = VideoSample) != null && _VideoSample.frame && !VideoSample.key) { 12847 this.pushAccessUnit(VideoSample, track); 12848 VideoSample = this.VideoSample = null; 12849 } 12850 } 12851 if (!VideoSample) { 12852 VideoSample = this.VideoSample = this.createVideoSample(true, pes.pts, pes.dts); 12853 } 12854 VideoSample.key = true; 12855 VideoSample.frame = true; 12856 break; 12857 12858 // IDR 12859 case 19: 12860 case 20: 12861 push = true; 12862 // handle PES not starting with AUD 12863 // if we have frame data already, that cannot belong to the same frame, so force a push 12864 if ((_VideoSample2 = VideoSample) != null && _VideoSample2.frame && !VideoSample.key) { 12865 this.pushAccessUnit(VideoSample, track); 12866 VideoSample = this.VideoSample = null; 12867 } 12868 if (!VideoSample) { 12869 VideoSample = this.VideoSample = this.createVideoSample(true, pes.pts, pes.dts); 12870 } 12871 VideoSample.key = true; 12872 VideoSample.frame = true; 12873 break; 12874 12875 // SEI 12876 case 39: 12877 push = true; 12878 parseSEIMessageFromNALu(unit.data, 2, 12879 // NALu header size 12880 pes.pts, textTrack.samples); 12881 break; 12882 12883 // VPS 12884 case 32: 12885 push = true; 12886 if (!track.vps) { 12887 if (typeof track.params !== 'object') { 12888 track.params = {}; 12889 } 12890 track.params = _extends(track.params, this.readVPS(unit.data)); 12891 this.initVPS = unit.data; 12892 } 12893 track.vps = [unit.data]; 12894 break; 12895 12896 // SPS 12897 case 33: 12898 push = true; 12899 spsfound = true; 12900 if (track.vps !== undefined && track.vps[0] !== this.initVPS && track.sps !== undefined && !this.matchSPS(track.sps[0], unit.data)) { 12901 this.initVPS = track.vps[0]; 12902 track.sps = track.pps = undefined; 12903 } 12904 if (!track.sps) { 12905 const config = this.readSPS(unit.data); 12906 track.width = config.width; 12907 track.height = config.height; 12908 track.pixelRatio = config.pixelRatio; 12909 track.codec = config.codecString; 12910 track.sps = []; 12911 if (typeof track.params !== 'object') { 12912 track.params = {}; 12913 } 12914 for (const prop in config.params) { 12915 track.params[prop] = config.params[prop]; 12916 } 12917 } 12918 this.pushParameterSet(track.sps, unit.data, track.vps); 12919 if (!VideoSample) { 12920 VideoSample = this.VideoSample = this.createVideoSample(true, pes.pts, pes.dts); 12921 } 12922 VideoSample.key = true; 12923 break; 12924 12925 // PPS 12926 case 34: 12927 push = true; 12928 if (typeof track.params === 'object') { 12929 if (!track.pps) { 12930 track.pps = []; 12931 const config = this.readPPS(unit.data); 12932 for (const prop in config) { 12933 track.params[prop] = config[prop]; 12934 } 12935 } 12936 this.pushParameterSet(track.pps, unit.data, track.vps); 12937 } 12938 break; 12939 12940 // ACCESS UNIT DELIMITER 12941 case 35: 12942 push = true; 12943 track.audFound = true; 12944 if ((_VideoSample3 = VideoSample) != null && _VideoSample3.frame) { 12945 this.pushAccessUnit(VideoSample, track); 12946 VideoSample = null; 12947 } 12948 if (!VideoSample) { 12949 VideoSample = this.VideoSample = this.createVideoSample(false, pes.pts, pes.dts); 12950 } 12951 break; 12952 default: 12953 push = false; 12954 break; 12955 } 12956 if (VideoSample && push) { 12957 const units = VideoSample.units; 12958 units.push(unit); 12959 } 12960 }); 12961 // if last PES packet, push samples 12962 if (endOfSegment && VideoSample) { 12963 this.pushAccessUnit(VideoSample, track); 12964 this.VideoSample = null; 12965 } 12966 } 12967 pushParameterSet(parameterSets, data, vps) { 12968 if (vps && vps[0] === this.initVPS || !vps && !parameterSets.length) { 12969 parameterSets.push(data); 12970 } 12971 } 12972 getNALuType(data, offset) { 12973 return (data[offset] & 0x7e) >>> 1; 12974 } 12975 ebsp2rbsp(arr) { 12976 const dst = new Uint8Array(arr.byteLength); 12977 let dstIdx = 0; 12978 for (let i = 0; i < arr.byteLength; i++) { 12979 if (i >= 2) { 12980 // Unescape: Skip 0x03 after 00 00 12981 if (arr[i] === 0x03 && arr[i - 1] === 0x00 && arr[i - 2] === 0x00) { 12982 continue; 12983 } 12984 } 12985 dst[dstIdx] = arr[i]; 12986 dstIdx++; 12987 } 12988 return new Uint8Array(dst.buffer, 0, dstIdx); 12989 } 12990 pushAccessUnit(VideoSample, videoTrack) { 12991 super.pushAccessUnit(VideoSample, videoTrack); 12992 if (this.initVPS) { 12993 this.initVPS = null; // null initVPS to prevent possible track's sps/pps growth until next VPS 12994 } 12995 } 12996 readVPS(vps) { 12997 const eg = new ExpGolomb(vps); 12998 // remove header 12999 eg.readUByte(); 13000 eg.readUByte(); 13001 eg.readBits(4); // video_parameter_set_id 13002 eg.skipBits(2); 13003 eg.readBits(6); // max_layers_minus1 13004 const max_sub_layers_minus1 = eg.readBits(3); 13005 const temporal_id_nesting_flag = eg.readBoolean(); 13006 // ...vui fps can be here, but empty fps value is not critical for metadata 13007 13008 return { 13009 numTemporalLayers: max_sub_layers_minus1 + 1, 13010 temporalIdNested: temporal_id_nesting_flag 13011 }; 13012 } 13013 readSPS(sps) { 13014 const eg = new ExpGolomb(this.ebsp2rbsp(sps)); 13015 eg.readUByte(); 13016 eg.readUByte(); 13017 eg.readBits(4); //video_parameter_set_id 13018 const max_sub_layers_minus1 = eg.readBits(3); 13019 eg.readBoolean(); // temporal_id_nesting_flag 13020 13021 // profile_tier_level 13022 const general_profile_space = eg.readBits(2); 13023 const general_tier_flag = eg.readBoolean(); 13024 const general_profile_idc = eg.readBits(5); 13025 const general_profile_compatibility_flags_1 = eg.readUByte(); 13026 const general_profile_compatibility_flags_2 = eg.readUByte(); 13027 const general_profile_compatibility_flags_3 = eg.readUByte(); 13028 const general_profile_compatibility_flags_4 = eg.readUByte(); 13029 const general_constraint_indicator_flags_1 = eg.readUByte(); 13030 const general_constraint_indicator_flags_2 = eg.readUByte(); 13031 const general_constraint_indicator_flags_3 = eg.readUByte(); 13032 const general_constraint_indicator_flags_4 = eg.readUByte(); 13033 const general_constraint_indicator_flags_5 = eg.readUByte(); 13034 const general_constraint_indicator_flags_6 = eg.readUByte(); 13035 const general_level_idc = eg.readUByte(); 13036 const sub_layer_profile_present_flags = []; 13037 const sub_layer_level_present_flags = []; 13038 for (let i = 0; i < max_sub_layers_minus1; i++) { 13039 sub_layer_profile_present_flags.push(eg.readBoolean()); 13040 sub_layer_level_present_flags.push(eg.readBoolean()); 13041 } 13042 if (max_sub_layers_minus1 > 0) { 13043 for (let i = max_sub_layers_minus1; i < 8; i++) { 13044 eg.readBits(2); 13045 } 13046 } 13047 for (let i = 0; i < max_sub_layers_minus1; i++) { 13048 if (sub_layer_profile_present_flags[i]) { 13049 eg.readUByte(); // sub_layer_profile_space, sub_layer_tier_flag, sub_layer_profile_idc 13050 eg.readUByte(); 13051 eg.readUByte(); 13052 eg.readUByte(); 13053 eg.readUByte(); // sub_layer_profile_compatibility_flag 13054 eg.readUByte(); 13055 eg.readUByte(); 13056 eg.readUByte(); 13057 eg.readUByte(); 13058 eg.readUByte(); 13059 eg.readUByte(); 13060 } 13061 if (sub_layer_level_present_flags[i]) { 13062 eg.readUByte(); 13063 } 13064 } 13065 eg.readUEG(); // seq_parameter_set_id 13066 const chroma_format_idc = eg.readUEG(); 13067 if (chroma_format_idc == 3) { 13068 eg.skipBits(1); //separate_colour_plane_flag 13069 } 13070 const pic_width_in_luma_samples = eg.readUEG(); 13071 const pic_height_in_luma_samples = eg.readUEG(); 13072 const conformance_window_flag = eg.readBoolean(); 13073 let pic_left_offset = 0, 13074 pic_right_offset = 0, 13075 pic_top_offset = 0, 13076 pic_bottom_offset = 0; 13077 if (conformance_window_flag) { 13078 pic_left_offset += eg.readUEG(); 13079 pic_right_offset += eg.readUEG(); 13080 pic_top_offset += eg.readUEG(); 13081 pic_bottom_offset += eg.readUEG(); 13082 } 13083 const bit_depth_luma_minus8 = eg.readUEG(); 13084 const bit_depth_chroma_minus8 = eg.readUEG(); 13085 const log2_max_pic_order_cnt_lsb_minus4 = eg.readUEG(); 13086 const sub_layer_ordering_info_present_flag = eg.readBoolean(); 13087 for (let i = sub_layer_ordering_info_present_flag ? 0 : max_sub_layers_minus1; i <= max_sub_layers_minus1; i++) { 13088 eg.skipUEG(); // max_dec_pic_buffering_minus1[i] 13089 eg.skipUEG(); // max_num_reorder_pics[i] 13090 eg.skipUEG(); // max_latency_increase_plus1[i] 13091 } 13092 eg.skipUEG(); // log2_min_luma_coding_block_size_minus3 13093 eg.skipUEG(); // log2_diff_max_min_luma_coding_block_size 13094 eg.skipUEG(); // log2_min_transform_block_size_minus2 13095 eg.skipUEG(); // log2_diff_max_min_transform_block_size 13096 eg.skipUEG(); // max_transform_hierarchy_depth_inter 13097 eg.skipUEG(); // max_transform_hierarchy_depth_intra 13098 const scaling_list_enabled_flag = eg.readBoolean(); 13099 if (scaling_list_enabled_flag) { 13100 const sps_scaling_list_data_present_flag = eg.readBoolean(); 13101 if (sps_scaling_list_data_present_flag) { 13102 for (let sizeId = 0; sizeId < 4; sizeId++) { 13103 for (let matrixId = 0; matrixId < (sizeId === 3 ? 2 : 6); matrixId++) { 13104 const scaling_list_pred_mode_flag = eg.readBoolean(); 13105 if (!scaling_list_pred_mode_flag) { 13106 eg.readUEG(); // scaling_list_pred_matrix_id_delta 13107 } else { 13108 const coefNum = Math.min(64, 1 << 4 + (sizeId << 1)); 13109 if (sizeId > 1) { 13110 eg.readEG(); 13111 } 13112 for (let i = 0; i < coefNum; i++) { 13113 eg.readEG(); 13114 } 13115 } 13116 } 13117 } 13118 } 13119 } 13120 eg.readBoolean(); // amp_enabled_flag 13121 eg.readBoolean(); // sample_adaptive_offset_enabled_flag 13122 const pcm_enabled_flag = eg.readBoolean(); 13123 if (pcm_enabled_flag) { 13124 eg.readUByte(); 13125 eg.skipUEG(); 13126 eg.skipUEG(); 13127 eg.readBoolean(); 13128 } 13129 const num_short_term_ref_pic_sets = eg.readUEG(); 13130 let num_delta_pocs = 0; 13131 for (let i = 0; i < num_short_term_ref_pic_sets; i++) { 13132 let inter_ref_pic_set_prediction_flag = false; 13133 if (i !== 0) { 13134 inter_ref_pic_set_prediction_flag = eg.readBoolean(); 13135 } 13136 if (inter_ref_pic_set_prediction_flag) { 13137 if (i === num_short_term_ref_pic_sets) { 13138 eg.readUEG(); 13139 } 13140 eg.readBoolean(); 13141 eg.readUEG(); 13142 let next_num_delta_pocs = 0; 13143 for (let j = 0; j <= num_delta_pocs; j++) { 13144 const used_by_curr_pic_flag = eg.readBoolean(); 13145 let use_delta_flag = false; 13146 if (!used_by_curr_pic_flag) { 13147 use_delta_flag = eg.readBoolean(); 13148 } 13149 if (used_by_curr_pic_flag || use_delta_flag) { 13150 next_num_delta_pocs++; 13151 } 13152 } 13153 num_delta_pocs = next_num_delta_pocs; 13154 } else { 13155 const num_negative_pics = eg.readUEG(); 13156 const num_positive_pics = eg.readUEG(); 13157 num_delta_pocs = num_negative_pics + num_positive_pics; 13158 for (let j = 0; j < num_negative_pics; j++) { 13159 eg.readUEG(); 13160 eg.readBoolean(); 13161 } 13162 for (let j = 0; j < num_positive_pics; j++) { 13163 eg.readUEG(); 13164 eg.readBoolean(); 13165 } 13166 } 13167 } 13168 const long_term_ref_pics_present_flag = eg.readBoolean(); 13169 if (long_term_ref_pics_present_flag) { 13170 const num_long_term_ref_pics_sps = eg.readUEG(); 13171 for (let i = 0; i < num_long_term_ref_pics_sps; i++) { 13172 for (let j = 0; j < log2_max_pic_order_cnt_lsb_minus4 + 4; j++) { 13173 eg.readBits(1); 13174 } 13175 eg.readBits(1); 13176 } 13177 } 13178 let min_spatial_segmentation_idc = 0; 13179 let sar_width = 1, 13180 sar_height = 1; 13181 let fps_fixed = true, 13182 fps_den = 1, 13183 fps_num = 0; 13184 eg.readBoolean(); // sps_temporal_mvp_enabled_flag 13185 eg.readBoolean(); // strong_intra_smoothing_enabled_flag 13186 let default_display_window_flag = false; 13187 const vui_parameters_present_flag = eg.readBoolean(); 13188 if (vui_parameters_present_flag) { 13189 const aspect_ratio_info_present_flag = eg.readBoolean(); 13190 if (aspect_ratio_info_present_flag) { 13191 const aspect_ratio_idc = eg.readUByte(); 13192 const sar_width_table = [1, 12, 10, 16, 40, 24, 20, 32, 80, 18, 15, 64, 160, 4, 3, 2]; 13193 const sar_height_table = [1, 11, 11, 11, 33, 11, 11, 11, 33, 11, 11, 33, 99, 3, 2, 1]; 13194 if (aspect_ratio_idc > 0 && aspect_ratio_idc < 16) { 13195 sar_width = sar_width_table[aspect_ratio_idc - 1]; 13196 sar_height = sar_height_table[aspect_ratio_idc - 1]; 13197 } else if (aspect_ratio_idc === 255) { 13198 sar_width = eg.readBits(16); 13199 sar_height = eg.readBits(16); 13200 } 13201 } 13202 const overscan_info_present_flag = eg.readBoolean(); 13203 if (overscan_info_present_flag) { 13204 eg.readBoolean(); 13205 } 13206 const video_signal_type_present_flag = eg.readBoolean(); 13207 if (video_signal_type_present_flag) { 13208 eg.readBits(3); 13209 eg.readBoolean(); 13210 const colour_description_present_flag = eg.readBoolean(); 13211 if (colour_description_present_flag) { 13212 eg.readUByte(); 13213 eg.readUByte(); 13214 eg.readUByte(); 13215 } 13216 } 13217 const chroma_loc_info_present_flag = eg.readBoolean(); 13218 if (chroma_loc_info_present_flag) { 13219 eg.readUEG(); 13220 eg.readUEG(); 13221 } 13222 eg.readBoolean(); // neutral_chroma_indication_flag 13223 eg.readBoolean(); // field_seq_flag 13224 eg.readBoolean(); // frame_field_info_present_flag 13225 default_display_window_flag = eg.readBoolean(); 13226 if (default_display_window_flag) { 13227 eg.skipUEG(); 13228 eg.skipUEG(); 13229 eg.skipUEG(); 13230 eg.skipUEG(); 13231 } 13232 const vui_timing_info_present_flag = eg.readBoolean(); 13233 if (vui_timing_info_present_flag) { 13234 fps_den = eg.readBits(32); 13235 fps_num = eg.readBits(32); 13236 const vui_poc_proportional_to_timing_flag = eg.readBoolean(); 13237 if (vui_poc_proportional_to_timing_flag) { 13238 eg.readUEG(); 13239 } 13240 const vui_hrd_parameters_present_flag = eg.readBoolean(); 13241 if (vui_hrd_parameters_present_flag) { 13242 //const commonInfPresentFlag = true; 13243 //if (commonInfPresentFlag) { 13244 const nal_hrd_parameters_present_flag = eg.readBoolean(); 13245 const vcl_hrd_parameters_present_flag = eg.readBoolean(); 13246 let sub_pic_hrd_params_present_flag = false; 13247 if (nal_hrd_parameters_present_flag || vcl_hrd_parameters_present_flag) { 13248 sub_pic_hrd_params_present_flag = eg.readBoolean(); 13249 if (sub_pic_hrd_params_present_flag) { 13250 eg.readUByte(); 13251 eg.readBits(5); 13252 eg.readBoolean(); 13253 eg.readBits(5); 13254 } 13255 eg.readBits(4); // bit_rate_scale 13256 eg.readBits(4); // cpb_size_scale 13257 if (sub_pic_hrd_params_present_flag) { 13258 eg.readBits(4); 13259 } 13260 eg.readBits(5); 13261 eg.readBits(5); 13262 eg.readBits(5); 13263 } 13264 //} 13265 for (let i = 0; i <= max_sub_layers_minus1; i++) { 13266 fps_fixed = eg.readBoolean(); // fixed_pic_rate_general_flag 13267 const fixed_pic_rate_within_cvs_flag = fps_fixed || eg.readBoolean(); 13268 let low_delay_hrd_flag = false; 13269 if (fixed_pic_rate_within_cvs_flag) { 13270 eg.readEG(); 13271 } else { 13272 low_delay_hrd_flag = eg.readBoolean(); 13273 } 13274 const cpb_cnt = low_delay_hrd_flag ? 1 : eg.readUEG() + 1; 13275 if (nal_hrd_parameters_present_flag) { 13276 for (let j = 0; j < cpb_cnt; j++) { 13277 eg.readUEG(); 13278 eg.readUEG(); 13279 if (sub_pic_hrd_params_present_flag) { 13280 eg.readUEG(); 13281 eg.readUEG(); 13282 } 13283 eg.skipBits(1); 13284 } 13285 } 13286 if (vcl_hrd_parameters_present_flag) { 13287 for (let j = 0; j < cpb_cnt; j++) { 13288 eg.readUEG(); 13289 eg.readUEG(); 13290 if (sub_pic_hrd_params_present_flag) { 13291 eg.readUEG(); 13292 eg.readUEG(); 13293 } 13294 eg.skipBits(1); 13295 } 13296 } 13297 } 13298 } 13299 } 13300 const bitstream_restriction_flag = eg.readBoolean(); 13301 if (bitstream_restriction_flag) { 13302 eg.readBoolean(); // tiles_fixed_structure_flag 13303 eg.readBoolean(); // motion_vectors_over_pic_boundaries_flag 13304 eg.readBoolean(); // restricted_ref_pic_lists_flag 13305 min_spatial_segmentation_idc = eg.readUEG(); 13306 } 13307 } 13308 let width = pic_width_in_luma_samples, 13309 height = pic_height_in_luma_samples; 13310 if (conformance_window_flag) { 13311 let chroma_scale_w = 1, 13312 chroma_scale_h = 1; 13313 if (chroma_format_idc === 1) { 13314 // YUV 420 13315 chroma_scale_w = chroma_scale_h = 2; 13316 } else if (chroma_format_idc == 2) { 13317 // YUV 422 13318 chroma_scale_w = 2; 13319 } 13320 width = pic_width_in_luma_samples - chroma_scale_w * pic_right_offset - chroma_scale_w * pic_left_offset; 13321 height = pic_height_in_luma_samples - chroma_scale_h * pic_bottom_offset - chroma_scale_h * pic_top_offset; 13322 } 13323 const profile_space_string = general_profile_space ? ['A', 'B', 'C'][general_profile_space] : ''; 13324 const profile_compatibility_buf = general_profile_compatibility_flags_1 << 24 | general_profile_compatibility_flags_2 << 16 | general_profile_compatibility_flags_3 << 8 | general_profile_compatibility_flags_4; 13325 let profile_compatibility_rev = 0; 13326 for (let i = 0; i < 32; i++) { 13327 profile_compatibility_rev = (profile_compatibility_rev | (profile_compatibility_buf >> i & 1) << 31 - i) >>> 0; // reverse bit position (and cast as UInt32) 13328 } 13329 let profile_compatibility_flags_string = profile_compatibility_rev.toString(16); 13330 if (general_profile_idc === 1 && profile_compatibility_flags_string === '2') { 13331 profile_compatibility_flags_string = '6'; 13332 } 13333 const tier_flag_string = general_tier_flag ? 'H' : 'L'; 13334 return { 13335 codecString: `hvc1.${profile_space_string}${general_profile_idc}.${profile_compatibility_flags_string}.${tier_flag_string}${general_level_idc}.B0`, 13336 params: { 13337 general_tier_flag, 13338 general_profile_idc, 13339 general_profile_space, 13340 general_profile_compatibility_flags: [general_profile_compatibility_flags_1, general_profile_compatibility_flags_2, general_profile_compatibility_flags_3, general_profile_compatibility_flags_4], 13341 general_constraint_indicator_flags: [general_constraint_indicator_flags_1, general_constraint_indicator_flags_2, general_constraint_indicator_flags_3, general_constraint_indicator_flags_4, general_constraint_indicator_flags_5, general_constraint_indicator_flags_6], 13342 general_level_idc, 13343 bit_depth: bit_depth_luma_minus8 + 8, 13344 bit_depth_luma_minus8, 13345 bit_depth_chroma_minus8, 13346 min_spatial_segmentation_idc, 13347 chroma_format_idc: chroma_format_idc, 13348 frame_rate: { 13349 fixed: fps_fixed, 13350 fps: fps_num / fps_den 13351 } 13352 }, 13353 width, 13354 height, 13355 pixelRatio: [sar_width, sar_height] 13356 }; 13357 } 13358 readPPS(pps) { 13359 const eg = new ExpGolomb(this.ebsp2rbsp(pps)); 13360 eg.readUByte(); 13361 eg.readUByte(); 13362 eg.skipUEG(); // pic_parameter_set_id 13363 eg.skipUEG(); // seq_parameter_set_id 13364 eg.skipBits(2); // dependent_slice_segments_enabled_flag, output_flag_present_flag 13365 eg.skipBits(3); // num_extra_slice_header_bits 13366 eg.skipBits(2); // sign_data_hiding_enabled_flag, cabac_init_present_flag 13367 eg.skipUEG(); 13368 eg.skipUEG(); 13369 eg.skipEG(); // init_qp_minus26 13370 eg.skipBits(2); // constrained_intra_pred_flag, transform_skip_enabled_flag 13371 const cu_qp_delta_enabled_flag = eg.readBoolean(); 13372 if (cu_qp_delta_enabled_flag) { 13373 eg.skipUEG(); 13374 } 13375 eg.skipEG(); // cb_qp_offset 13376 eg.skipEG(); // cr_qp_offset 13377 eg.skipBits(4); // pps_slice_chroma_qp_offsets_present_flag, weighted_pred_flag, weighted_bipred_flag, transquant_bypass_enabled_flag 13378 const tiles_enabled_flag = eg.readBoolean(); 13379 const entropy_coding_sync_enabled_flag = eg.readBoolean(); 13380 let parallelismType = 1; // slice-based parallel decoding 13381 if (entropy_coding_sync_enabled_flag && tiles_enabled_flag) { 13382 parallelismType = 0; // mixed-type parallel decoding 13383 } else if (entropy_coding_sync_enabled_flag) { 13384 parallelismType = 3; // wavefront-based parallel decoding 13385 } else if (tiles_enabled_flag) { 13386 parallelismType = 2; // tile-based parallel decoding 13387 } 13388 return { 13389 parallelismType 13390 }; 13391 } 13392 matchSPS(sps1, sps2) { 13393 // compare without headers and VPS related params 13394 return String.fromCharCode.apply(null, sps1).substr(3) === String.fromCharCode.apply(null, sps2).substr(3); 13395 } 13396} 13397 13398const PACKET_LENGTH = 188; 13399class TSDemuxer { 13400 constructor(observer, config, typeSupported, logger) { 13401 this.logger = void 0; 13402 this.observer = void 0; 13403 this.config = void 0; 13404 this.typeSupported = void 0; 13405 this.sampleAes = null; 13406 this.pmtParsed = false; 13407 this.audioCodec = void 0; 13408 this.videoCodec = void 0; 13409 this._pmtId = -1; 13410 this._videoTrack = void 0; 13411 this._audioTrack = void 0; 13412 this._id3Track = void 0; 13413 this._txtTrack = void 0; 13414 this.aacOverFlow = null; 13415 this.remainderData = null; 13416 this.videoParser = void 0; 13417 this.observer = observer; 13418 this.config = config; 13419 this.typeSupported = typeSupported; 13420 this.logger = logger; 13421 this.videoParser = null; 13422 } 13423 static probe(data, logger) { 13424 const syncOffset = TSDemuxer.syncOffset(data); 13425 if (syncOffset > 0) { 13426 logger.warn(`MPEG2-TS detected but first sync word found @ offset ${syncOffset}`); 13427 } 13428 return syncOffset !== -1; 13429 } 13430 static syncOffset(data) { 13431 const length = data.length; 13432 let scanwindow = Math.min(PACKET_LENGTH * 5, length - PACKET_LENGTH) + 1; 13433 let i = 0; 13434 while (i < scanwindow) { 13435 // a TS init segment should contain at least 2 TS packets: PAT and PMT, each starting with 0x47 13436 let foundPat = false; 13437 let packetStart = -1; 13438 let tsPackets = 0; 13439 for (let j = i; j < length; j += PACKET_LENGTH) { 13440 if (data[j] === 0x47 && (length - j === PACKET_LENGTH || data[j + PACKET_LENGTH] === 0x47)) { 13441 tsPackets++; 13442 if (packetStart === -1) { 13443 packetStart = j; 13444 // First sync word found at offset, increase scan length (#5251) 13445 if (packetStart !== 0) { 13446 scanwindow = Math.min(packetStart + PACKET_LENGTH * 99, data.length - PACKET_LENGTH) + 1; 13447 } 13448 } 13449 if (!foundPat) { 13450 foundPat = parsePID(data, j) === 0; 13451 } 13452 // Sync word found at 0 with 3 packets, or found at offset least 2 packets up to scanwindow (#5501) 13453 if (foundPat && tsPackets > 1 && (packetStart === 0 && tsPackets > 2 || j + PACKET_LENGTH > scanwindow)) { 13454 return packetStart; 13455 } 13456 } else if (tsPackets) { 13457 // Exit if sync word found, but does not contain contiguous packets 13458 return -1; 13459 } else { 13460 break; 13461 } 13462 } 13463 i++; 13464 } 13465 return -1; 13466 } 13467 13468 /** 13469 * Creates a track model internal to demuxer used to drive remuxing input 13470 */ 13471 static createTrack(type, duration) { 13472 return { 13473 container: type === 'video' || type === 'audio' ? 'video/mp2t' : undefined, 13474 type, 13475 id: RemuxerTrackIdConfig[type], 13476 pid: -1, 13477 inputTimeScale: 90000, 13478 sequenceNumber: 0, 13479 samples: [], 13480 dropped: 0, 13481 duration: type === 'audio' ? duration : undefined 13482 }; 13483 } 13484 13485 /** 13486 * Initializes a new init segment on the demuxer/remuxer interface. Needed for discontinuities/track-switches (or at stream start) 13487 * Resets all internal track instances of the demuxer. 13488 */ 13489 resetInitSegment(initSegment, audioCodec, videoCodec, trackDuration) { 13490 this.pmtParsed = false; 13491 this._pmtId = -1; 13492 this._videoTrack = TSDemuxer.createTrack('video'); 13493 this._videoTrack.duration = trackDuration; 13494 this._audioTrack = TSDemuxer.createTrack('audio', trackDuration); 13495 this._id3Track = TSDemuxer.createTrack('id3'); 13496 this._txtTrack = TSDemuxer.createTrack('text'); 13497 this._audioTrack.segmentCodec = 'aac'; 13498 13499 // flush any partial content 13500 this.videoParser = null; 13501 this.aacOverFlow = null; 13502 this.remainderData = null; 13503 this.audioCodec = audioCodec; 13504 this.videoCodec = videoCodec; 13505 } 13506 resetTimeStamp() {} 13507 resetContiguity() { 13508 const { 13509 _audioTrack, 13510 _videoTrack, 13511 _id3Track 13512 } = this; 13513 if (_audioTrack) { 13514 _audioTrack.pesData = null; 13515 } 13516 if (_videoTrack) { 13517 _videoTrack.pesData = null; 13518 } 13519 if (_id3Track) { 13520 _id3Track.pesData = null; 13521 } 13522 this.aacOverFlow = null; 13523 this.remainderData = null; 13524 } 13525 demux(data, timeOffset, isSampleAes = false, flush = false) { 13526 if (!isSampleAes) { 13527 this.sampleAes = null; 13528 } 13529 let pes; 13530 const videoTrack = this._videoTrack; 13531 const audioTrack = this._audioTrack; 13532 const id3Track = this._id3Track; 13533 const textTrack = this._txtTrack; 13534 let videoPid = videoTrack.pid; 13535 let videoData = videoTrack.pesData; 13536 let audioPid = audioTrack.pid; 13537 let id3Pid = id3Track.pid; 13538 let audioData = audioTrack.pesData; 13539 let id3Data = id3Track.pesData; 13540 let unknownPID = null; 13541 let pmtParsed = this.pmtParsed; 13542 let pmtId = this._pmtId; 13543 let len = data.length; 13544 if (this.remainderData) { 13545 data = appendUint8Array(this.remainderData, data); 13546 len = data.length; 13547 this.remainderData = null; 13548 } 13549 if (len < PACKET_LENGTH && !flush) { 13550 this.remainderData = data; 13551 return { 13552 audioTrack, 13553 videoTrack, 13554 id3Track, 13555 textTrack 13556 }; 13557 } 13558 const syncOffset = Math.max(0, TSDemuxer.syncOffset(data)); 13559 len -= (len - syncOffset) % PACKET_LENGTH; 13560 if (len < data.byteLength && !flush) { 13561 this.remainderData = new Uint8Array(data.buffer, len, data.buffer.byteLength - len); 13562 } 13563 13564 // loop through TS packets 13565 let tsPacketErrors = 0; 13566 for (let start = syncOffset; start < len; start += PACKET_LENGTH) { 13567 if (data[start] === 0x47) { 13568 const stt = !!(data[start + 1] & 0x40); 13569 const pid = parsePID(data, start); 13570 const atf = (data[start + 3] & 0x30) >> 4; 13571 13572 // if an adaption field is present, its length is specified by the fifth byte of the TS packet header. 13573 let offset; 13574 if (atf > 1) { 13575 offset = start + 5 + data[start + 4]; 13576 // continue if there is only adaptation field 13577 if (offset === start + PACKET_LENGTH) { 13578 continue; 13579 } 13580 } else { 13581 offset = start + 4; 13582 } 13583 switch (pid) { 13584 case videoPid: 13585 if (stt) { 13586 if (videoData && (pes = parsePES(videoData, this.logger))) { 13587 this.readyVideoParser(videoTrack.segmentCodec); 13588 if (this.videoParser !== null) { 13589 this.videoParser.parsePES(videoTrack, textTrack, pes, false); 13590 } 13591 } 13592 videoData = { 13593 data: [], 13594 size: 0 13595 }; 13596 } 13597 if (videoData) { 13598 videoData.data.push(data.subarray(offset, start + PACKET_LENGTH)); 13599 videoData.size += start + PACKET_LENGTH - offset; 13600 } 13601 break; 13602 case audioPid: 13603 if (stt) { 13604 if (audioData && (pes = parsePES(audioData, this.logger))) { 13605 switch (audioTrack.segmentCodec) { 13606 case 'aac': 13607 this.parseAACPES(audioTrack, pes); 13608 break; 13609 case 'mp3': 13610 this.parseMPEGPES(audioTrack, pes); 13611 break; 13612 case 'ac3': 13613 { 13614 this.parseAC3PES(audioTrack, pes); 13615 } 13616 break; 13617 } 13618 } 13619 audioData = { 13620 data: [], 13621 size: 0 13622 }; 13623 } 13624 if (audioData) { 13625 audioData.data.push(data.subarray(offset, start + PACKET_LENGTH)); 13626 audioData.size += start + PACKET_LENGTH - offset; 13627 } 13628 break; 13629 case id3Pid: 13630 if (stt) { 13631 if (id3Data && (pes = parsePES(id3Data, this.logger))) { 13632 this.parseID3PES(id3Track, pes); 13633 } 13634 id3Data = { 13635 data: [], 13636 size: 0 13637 }; 13638 } 13639 if (id3Data) { 13640 id3Data.data.push(data.subarray(offset, start + PACKET_LENGTH)); 13641 id3Data.size += start + PACKET_LENGTH - offset; 13642 } 13643 break; 13644 case 0: 13645 if (stt) { 13646 offset += data[offset] + 1; 13647 } 13648 pmtId = this._pmtId = parsePAT(data, offset); 13649 // this.logger.log('PMT PID:' + this._pmtId); 13650 break; 13651 case pmtId: 13652 { 13653 if (stt) { 13654 offset += data[offset] + 1; 13655 } 13656 const parsedPIDs = parsePMT(data, offset, this.typeSupported, isSampleAes, this.observer, this.logger); 13657 13658 // only update track id if track PID found while parsing PMT 13659 // this is to avoid resetting the PID to -1 in case 13660 // track PID transiently disappears from the stream 13661 // this could happen in case of transient missing audio samples for example 13662 // NOTE this is only the PID of the track as found in TS, 13663 // but we are not using this for MP4 track IDs. 13664 videoPid = parsedPIDs.videoPid; 13665 if (videoPid > 0) { 13666 videoTrack.pid = videoPid; 13667 videoTrack.segmentCodec = parsedPIDs.segmentVideoCodec; 13668 } 13669 audioPid = parsedPIDs.audioPid; 13670 if (audioPid > 0) { 13671 audioTrack.pid = audioPid; 13672 audioTrack.segmentCodec = parsedPIDs.segmentAudioCodec; 13673 } 13674 id3Pid = parsedPIDs.id3Pid; 13675 if (id3Pid > 0) { 13676 id3Track.pid = id3Pid; 13677 } 13678 if (unknownPID !== null && !pmtParsed) { 13679 this.logger.warn(`MPEG-TS PMT found at ${start} after unknown PID '${unknownPID}'. Backtracking to sync byte @${syncOffset} to parse all TS packets.`); 13680 unknownPID = null; 13681 // we set it to -188, the += 188 in the for loop will reset start to 0 13682 start = syncOffset - 188; 13683 } 13684 pmtParsed = this.pmtParsed = true; 13685 break; 13686 } 13687 case 0x11: 13688 case 0x1fff: 13689 break; 13690 default: 13691 unknownPID = pid; 13692 break; 13693 } 13694 } else { 13695 tsPacketErrors++; 13696 } 13697 } 13698 if (tsPacketErrors > 0) { 13699 emitParsingError(this.observer, new Error(`Found ${tsPacketErrors} TS packet/s that do not start with 0x47`), undefined, this.logger); 13700 } 13701 videoTrack.pesData = videoData; 13702 audioTrack.pesData = audioData; 13703 id3Track.pesData = id3Data; 13704 const demuxResult = { 13705 audioTrack, 13706 videoTrack, 13707 id3Track, 13708 textTrack 13709 }; 13710 if (flush) { 13711 this.extractRemainingSamples(demuxResult); 13712 } 13713 return demuxResult; 13714 } 13715 flush() { 13716 const { 13717 remainderData 13718 } = this; 13719 this.remainderData = null; 13720 let result; 13721 if (remainderData) { 13722 result = this.demux(remainderData, -1, false, true); 13723 } else { 13724 result = { 13725 videoTrack: this._videoTrack, 13726 audioTrack: this._audioTrack, 13727 id3Track: this._id3Track, 13728 textTrack: this._txtTrack 13729 }; 13730 } 13731 this.extractRemainingSamples(result); 13732 if (this.sampleAes) { 13733 return this.decrypt(result, this.sampleAes); 13734 } 13735 return result; 13736 } 13737 extractRemainingSamples(demuxResult) { 13738 const { 13739 audioTrack, 13740 videoTrack, 13741 id3Track, 13742 textTrack 13743 } = demuxResult; 13744 const videoData = videoTrack.pesData; 13745 const audioData = audioTrack.pesData; 13746 const id3Data = id3Track.pesData; 13747 // try to parse last PES packets 13748 let pes; 13749 if (videoData && (pes = parsePES(videoData, this.logger))) { 13750 this.readyVideoParser(videoTrack.segmentCodec); 13751 if (this.videoParser !== null) { 13752 this.videoParser.parsePES(videoTrack, textTrack, pes, true); 13753 videoTrack.pesData = null; 13754 } 13755 } else { 13756 // either avcData null or PES truncated, keep it for next frag parsing 13757 videoTrack.pesData = videoData; 13758 } 13759 if (audioData && (pes = parsePES(audioData, this.logger))) { 13760 switch (audioTrack.segmentCodec) { 13761 case 'aac': 13762 this.parseAACPES(audioTrack, pes); 13763 break; 13764 case 'mp3': 13765 this.parseMPEGPES(audioTrack, pes); 13766 break; 13767 case 'ac3': 13768 { 13769 this.parseAC3PES(audioTrack, pes); 13770 } 13771 break; 13772 } 13773 audioTrack.pesData = null; 13774 } else { 13775 if (audioData != null && audioData.size) { 13776 this.logger.log('last AAC PES packet truncated,might overlap between fragments'); 13777 } 13778 13779 // either audioData null or PES truncated, keep it for next frag parsing 13780 audioTrack.pesData = audioData; 13781 } 13782 if (id3Data && (pes = parsePES(id3Data, this.logger))) { 13783 this.parseID3PES(id3Track, pes); 13784 id3Track.pesData = null; 13785 } else { 13786 // either id3Data null or PES truncated, keep it for next frag parsing 13787 id3Track.pesData = id3Data; 13788 } 13789 } 13790 demuxSampleAes(data, keyData, timeOffset) { 13791 const demuxResult = this.demux(data, timeOffset, true, !this.config.progressive); 13792 const sampleAes = this.sampleAes = new SampleAesDecrypter(this.observer, this.config, keyData); 13793 return this.decrypt(demuxResult, sampleAes); 13794 } 13795 readyVideoParser(codec) { 13796 if (this.videoParser === null) { 13797 if (codec === 'avc') { 13798 this.videoParser = new AvcVideoParser(); 13799 } else if (codec === 'hevc') { 13800 this.videoParser = new HevcVideoParser(); 13801 } 13802 } 13803 } 13804 decrypt(demuxResult, sampleAes) { 13805 return new Promise(resolve => { 13806 const { 13807 audioTrack, 13808 videoTrack 13809 } = demuxResult; 13810 if (audioTrack.samples && audioTrack.segmentCodec === 'aac') { 13811 sampleAes.decryptAacSamples(audioTrack.samples, 0, () => { 13812 if (videoTrack.samples) { 13813 sampleAes.decryptAvcSamples(videoTrack.samples, 0, 0, () => { 13814 resolve(demuxResult); 13815 }); 13816 } else { 13817 resolve(demuxResult); 13818 } 13819 }); 13820 } else if (videoTrack.samples) { 13821 sampleAes.decryptAvcSamples(videoTrack.samples, 0, 0, () => { 13822 resolve(demuxResult); 13823 }); 13824 } 13825 }); 13826 } 13827 destroy() { 13828 if (this.observer) { 13829 this.observer.removeAllListeners(); 13830 } 13831 // @ts-ignore 13832 this.config = this.logger = this.observer = null; 13833 this.aacOverFlow = this.videoParser = this.remainderData = this.sampleAes = null; 13834 this._videoTrack = this._audioTrack = this._id3Track = this._txtTrack = undefined; 13835 } 13836 parseAACPES(track, pes) { 13837 let startOffset = 0; 13838 const aacOverFlow = this.aacOverFlow; 13839 let data = pes.data; 13840 if (aacOverFlow) { 13841 this.aacOverFlow = null; 13842 const frameMissingBytes = aacOverFlow.missing; 13843 const sampleLength = aacOverFlow.sample.unit.byteLength; 13844 // logger.log(`AAC: append overflowing ${sampleLength} bytes to beginning of new PES`); 13845 if (frameMissingBytes === -1) { 13846 data = appendUint8Array(aacOverFlow.sample.unit, data); 13847 } else { 13848 const frameOverflowBytes = sampleLength - frameMissingBytes; 13849 aacOverFlow.sample.unit.set(data.subarray(0, frameMissingBytes), frameOverflowBytes); 13850 track.samples.push(aacOverFlow.sample); 13851 startOffset = aacOverFlow.missing; 13852 } 13853 } 13854 // look for ADTS header (0xFFFx) 13855 let offset; 13856 let len; 13857 for (offset = startOffset, len = data.length; offset < len - 1; offset++) { 13858 if (isHeader$1(data, offset)) { 13859 break; 13860 } 13861 } 13862 // if ADTS header does not start straight from the beginning of the PES payload, raise an error 13863 if (offset !== startOffset) { 13864 let reason; 13865 const recoverable = offset < len - 1; 13866 if (recoverable) { 13867 reason = `AAC PES did not start with ADTS header,offset:${offset}`; 13868 } else { 13869 reason = 'No ADTS header found in AAC PES'; 13870 } 13871 emitParsingError(this.observer, new Error(reason), recoverable, this.logger); 13872 if (!recoverable) { 13873 return; 13874 } 13875 } 13876 initTrackConfig(track, this.observer, data, offset, this.audioCodec); 13877 let pts; 13878 if (pes.pts !== undefined) { 13879 pts = pes.pts; 13880 } else if (aacOverFlow) { 13881 // if last AAC frame is overflowing, we should ensure timestamps are contiguous: 13882 // first sample PTS should be equal to last sample PTS + frameDuration 13883 const frameDuration = getFrameDuration(track.samplerate); 13884 pts = aacOverFlow.sample.pts + frameDuration; 13885 } else { 13886 this.logger.warn('[tsdemuxer]: AAC PES unknown PTS'); 13887 return; 13888 } 13889 13890 // scan for aac samples 13891 let frameIndex = 0; 13892 let frame; 13893 while (offset < len) { 13894 frame = appendFrame$2(track, data, offset, pts, frameIndex); 13895 offset += frame.length; 13896 if (!frame.missing) { 13897 frameIndex++; 13898 for (; offset < len - 1; offset++) { 13899 if (isHeader$1(data, offset)) { 13900 break; 13901 } 13902 } 13903 } else { 13904 this.aacOverFlow = frame; 13905 break; 13906 } 13907 } 13908 } 13909 parseMPEGPES(track, pes) { 13910 const data = pes.data; 13911 const length = data.length; 13912 let frameIndex = 0; 13913 let offset = 0; 13914 const pts = pes.pts; 13915 if (pts === undefined) { 13916 this.logger.warn('[tsdemuxer]: MPEG PES unknown PTS'); 13917 return; 13918 } 13919 while (offset < length) { 13920 if (isHeader(data, offset)) { 13921 const frame = appendFrame$1(track, data, offset, pts, frameIndex); 13922 if (frame) { 13923 offset += frame.length; 13924 frameIndex++; 13925 } else { 13926 // logger.log('Unable to parse Mpeg audio frame'); 13927 break; 13928 } 13929 } else { 13930 // nothing found, keep looking 13931 offset++; 13932 } 13933 } 13934 } 13935 parseAC3PES(track, pes) { 13936 { 13937 const data = pes.data; 13938 const pts = pes.pts; 13939 if (pts === undefined) { 13940 this.logger.warn('[tsdemuxer]: AC3 PES unknown PTS'); 13941 return; 13942 } 13943 const length = data.length; 13944 let frameIndex = 0; 13945 let offset = 0; 13946 let parsed; 13947 while (offset < length && (parsed = appendFrame(track, data, offset, pts, frameIndex++)) > 0) { 13948 offset += parsed; 13949 } 13950 } 13951 } 13952 parseID3PES(id3Track, pes) { 13953 if (pes.pts === undefined) { 13954 this.logger.warn('[tsdemuxer]: ID3 PES unknown PTS'); 13955 return; 13956 } 13957 const id3Sample = _extends({}, pes, { 13958 type: this._videoTrack ? MetadataSchema.emsg : MetadataSchema.audioId3, 13959 duration: Number.POSITIVE_INFINITY 13960 }); 13961 id3Track.samples.push(id3Sample); 13962 } 13963} 13964function parsePID(data, offset) { 13965 // pid is a 13-bit field starting at the last bit of TS[1] 13966 return ((data[offset + 1] & 0x1f) << 8) + data[offset + 2]; 13967} 13968function parsePAT(data, offset) { 13969 // skip the PSI header and parse the first PMT entry 13970 return (data[offset + 10] & 0x1f) << 8 | data[offset + 11]; 13971} 13972function parsePMT(data, offset, typeSupported, isSampleAes, observer, logger) { 13973 const result = { 13974 audioPid: -1, 13975 videoPid: -1, 13976 id3Pid: -1, 13977 segmentVideoCodec: 'avc', 13978 segmentAudioCodec: 'aac' 13979 }; 13980 const sectionLength = (data[offset + 1] & 0x0f) << 8 | data[offset + 2]; 13981 const tableEnd = offset + 3 + sectionLength - 4; 13982 // to determine where the table is, we have to figure out how 13983 // long the program info descriptors are 13984 const programInfoLength = (data[offset + 10] & 0x0f) << 8 | data[offset + 11]; 13985 // advance the offset to the first entry in the mapping table 13986 offset += 12 + programInfoLength; 13987 while (offset < tableEnd) { 13988 const pid = parsePID(data, offset); 13989 const esInfoLength = (data[offset + 3] & 0x0f) << 8 | data[offset + 4]; 13990 switch (data[offset]) { 13991 case 0xcf: 13992 // SAMPLE-AES AAC 13993 if (!isSampleAes) { 13994 logEncryptedSamplesFoundInUnencryptedStream('ADTS AAC', logger); 13995 break; 13996 } 13997 /* falls through */ 13998 case 0x0f: 13999 // ISO/IEC 13818-7 ADTS AAC (MPEG-2 lower bit-rate audio) 14000 // logger.log('AAC PID:' + pid); 14001 if (result.audioPid === -1) { 14002 result.audioPid = pid; 14003 } 14004 break; 14005 14006 // Packetized metadata (ID3) 14007 case 0x15: 14008 // logger.log('ID3 PID:' + pid); 14009 if (result.id3Pid === -1) { 14010 result.id3Pid = pid; 14011 } 14012 break; 14013 case 0xdb: 14014 // SAMPLE-AES AVC 14015 if (!isSampleAes) { 14016 logEncryptedSamplesFoundInUnencryptedStream('H.264', logger); 14017 break; 14018 } 14019 /* falls through */ 14020 case 0x1b: 14021 // ITU-T Rec. H.264 and ISO/IEC 14496-10 (lower bit-rate video) 14022 // logger.log('AVC PID:' + pid); 14023 if (result.videoPid === -1) { 14024 result.videoPid = pid; 14025 } 14026 break; 14027 14028 // ISO/IEC 11172-3 (MPEG-1 audio) 14029 // or ISO/IEC 13818-3 (MPEG-2 halved sample rate audio) 14030 case 0x03: 14031 case 0x04: 14032 // logger.log('MPEG PID:' + pid); 14033 if (!typeSupported.mpeg && !typeSupported.mp3) { 14034 logger.log('MPEG audio found, not supported in this browser'); 14035 } else if (result.audioPid === -1) { 14036 result.audioPid = pid; 14037 result.segmentAudioCodec = 'mp3'; 14038 } 14039 break; 14040 case 0xc1: 14041 // SAMPLE-AES AC3 14042 if (!isSampleAes) { 14043 logEncryptedSamplesFoundInUnencryptedStream('AC-3', logger); 14044 break; 14045 } 14046 /* falls through */ 14047 case 0x81: 14048 { 14049 if (!typeSupported.ac3) { 14050 logger.log('AC-3 audio found, not supported in this browser'); 14051 } else if (result.audioPid === -1) { 14052 result.audioPid = pid; 14053 result.segmentAudioCodec = 'ac3'; 14054 } 14055 } 14056 break; 14057 case 0x06: 14058 // stream_type 6 can mean a lot of different things in case of DVB. 14059 // We need to look at the descriptors. Right now, we're only interested 14060 // in AC-3 audio, so we do the descriptor parsing only when we don't have 14061 // an audio PID yet. 14062 if (result.audioPid === -1 && esInfoLength > 0) { 14063 let parsePos = offset + 5; 14064 let remaining = esInfoLength; 14065 while (remaining > 2) { 14066 const descriptorId = data[parsePos]; 14067 switch (descriptorId) { 14068 case 0x6a: 14069 // DVB Descriptor for AC-3 14070 { 14071 if (typeSupported.ac3 !== true) { 14072 logger.log('AC-3 audio found, not supported in this browser for now'); 14073 } else { 14074 result.audioPid = pid; 14075 result.segmentAudioCodec = 'ac3'; 14076 } 14077 } 14078 break; 14079 } 14080 const descriptorLen = data[parsePos + 1] + 2; 14081 parsePos += descriptorLen; 14082 remaining -= descriptorLen; 14083 } 14084 } 14085 break; 14086 case 0xc2: // SAMPLE-AES EC3 14087 /* falls through */ 14088 case 0x87: 14089 emitParsingError(observer, new Error('Unsupported EC-3 in M2TS found'), undefined, logger); 14090 return result; 14091 case 0x24: 14092 // ITU-T Rec. H.265 and ISO/IEC 23008-2 (HEVC) 14093 { 14094 if (result.videoPid === -1) { 14095 result.videoPid = pid; 14096 result.segmentVideoCodec = 'hevc'; 14097 logger.log('HEVC in M2TS found'); 14098 } 14099 } 14100 break; 14101 } 14102 // move to the next table entry 14103 // skip past the elementary stream descriptors, if present 14104 offset += esInfoLength + 5; 14105 } 14106 return result; 14107} 14108function emitParsingError(observer, error, levelRetry, logger) { 14109 logger.warn(`parsing error: ${error.message}`); 14110 observer.emit(Events.ERROR, Events.ERROR, { 14111 type: ErrorTypes.MEDIA_ERROR, 14112 details: ErrorDetails.FRAG_PARSING_ERROR, 14113 fatal: false, 14114 levelRetry, 14115 error, 14116 reason: error.message 14117 }); 14118} 14119function logEncryptedSamplesFoundInUnencryptedStream(type, logger) { 14120 logger.log(`${type} with AES-128-CBC encryption found in unencrypted stream`); 14121} 14122function parsePES(stream, logger) { 14123 let i = 0; 14124 let frag; 14125 let pesLen; 14126 let pesHdrLen; 14127 let pesPts; 14128 let pesDts; 14129 const data = stream.data; 14130 // safety check 14131 if (!stream || stream.size === 0) { 14132 return null; 14133 } 14134 14135 // we might need up to 19 bytes to read PES header 14136 // if first chunk of data is less than 19 bytes, let's merge it with following ones until we get 19 bytes 14137 // usually only one merge is needed (and this is rare ...) 14138 while (data[0].length < 19 && data.length > 1) { 14139 data[0] = appendUint8Array(data[0], data[1]); 14140 data.splice(1, 1); 14141 } 14142 // retrieve PTS/DTS from first fragment 14143 frag = data[0]; 14144 const pesPrefix = (frag[0] << 16) + (frag[1] << 8) + frag[2]; 14145 if (pesPrefix === 1) { 14146 pesLen = (frag[4] << 8) + frag[5]; 14147 // if PES parsed length is not zero and greater than total received length, stop parsing. PES might be truncated 14148 // minus 6 : PES header size 14149 if (pesLen && pesLen > stream.size - 6) { 14150 return null; 14151 } 14152 const pesFlags = frag[7]; 14153 if (pesFlags & 0xc0) { 14154 /* PES header described here : http://dvd.sourceforge.net/dvdinfo/pes-hdr.html 14155 as PTS / DTS is 33 bit we cannot use bitwise operator in JS, 14156 as Bitwise operators treat their operands as a sequence of 32 bits */ 14157 pesPts = (frag[9] & 0x0e) * 536870912 + 14158 // 1 << 29 14159 (frag[10] & 0xff) * 4194304 + 14160 // 1 << 22 14161 (frag[11] & 0xfe) * 16384 + 14162 // 1 << 14 14163 (frag[12] & 0xff) * 128 + 14164 // 1 << 7 14165 (frag[13] & 0xfe) / 2; 14166 if (pesFlags & 0x40) { 14167 pesDts = (frag[14] & 0x0e) * 536870912 + 14168 // 1 << 29 14169 (frag[15] & 0xff) * 4194304 + 14170 // 1 << 22 14171 (frag[16] & 0xfe) * 16384 + 14172 // 1 << 14 14173 (frag[17] & 0xff) * 128 + 14174 // 1 << 7 14175 (frag[18] & 0xfe) / 2; 14176 if (pesPts - pesDts > 60 * 90000) { 14177 logger.warn(`${Math.round((pesPts - pesDts) / 90000)}s delta between PTS and DTS, align them`); 14178 pesPts = pesDts; 14179 } 14180 } else { 14181 pesDts = pesPts; 14182 } 14183 } 14184 pesHdrLen = frag[8]; 14185 // 9 bytes : 6 bytes for PES header + 3 bytes for PES extension 14186 let payloadStartOffset = pesHdrLen + 9; 14187 if (stream.size <= payloadStartOffset) { 14188 return null; 14189 } 14190 stream.size -= payloadStartOffset; 14191 // reassemble PES packet 14192 const pesData = new Uint8Array(stream.size); 14193 for (let j = 0, dataLen = data.length; j < dataLen; j++) { 14194 frag = data[j]; 14195 let len = frag.byteLength; 14196 if (payloadStartOffset) { 14197 if (payloadStartOffset > len) { 14198 // trim full frag if PES header bigger than frag 14199 payloadStartOffset -= len; 14200 continue; 14201 } else { 14202 // trim partial frag if PES header smaller than frag 14203 frag = frag.subarray(payloadStartOffset); 14204 len -= payloadStartOffset; 14205 payloadStartOffset = 0; 14206 } 14207 } 14208 pesData.set(frag, i); 14209 i += len; 14210 } 14211 if (pesLen) { 14212 // payload size : remove PES header + PES extension 14213 pesLen -= pesHdrLen + 3; 14214 } 14215 return { 14216 data: pesData, 14217 pts: pesPts, 14218 dts: pesDts, 14219 len: pesLen 14220 }; 14221 } 14222 return null; 14223} 14224 14225/** 14226 * AAC helper 14227 */ 14228 14229class AAC { 14230 static getSilentFrame(codec, channelCount) { 14231 switch (codec) { 14232 case 'mp4a.40.2': 14233 if (channelCount === 1) { 14234 return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x23, 0x80]); 14235 } else if (channelCount === 2) { 14236 return new Uint8Array([0x21, 0x00, 0x49, 0x90, 0x02, 0x19, 0x00, 0x23, 0x80]); 14237 } else if (channelCount === 3) { 14238 return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x8e]); 14239 } else if (channelCount === 4) { 14240 return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x80, 0x2c, 0x80, 0x08, 0x02, 0x38]); 14241 } else if (channelCount === 5) { 14242 return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x38]); 14243 } else if (channelCount === 6) { 14244 return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x00, 0xb2, 0x00, 0x20, 0x08, 0xe0]); 14245 } 14246 break; 14247 // handle HE-AAC below (mp4a.40.5 / mp4a.40.29) 14248 default: 14249 if (channelCount === 1) { 14250 // 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 14251 return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x4e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x1c, 0x6, 0xf1, 0xc1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]); 14252 } else if (channelCount === 2) { 14253 // 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 14254 return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x5e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x0, 0x95, 0x0, 0x6, 0xf1, 0xa1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]); 14255 } else if (channelCount === 3) { 14256 // 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 14257 return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x5e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x0, 0x95, 0x0, 0x6, 0xf1, 0xa1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]); 14258 } 14259 break; 14260 } 14261 return undefined; 14262 } 14263} 14264 14265/** 14266 * Generate MP4 Box 14267 */ 14268 14269const UINT32_MAX = Math.pow(2, 32) - 1; 14270class MP4 { 14271 static init() { 14272 MP4.types = { 14273 avc1: [], 14274 // codingname 14275 avcC: [], 14276 hvc1: [], 14277 hvcC: [], 14278 btrt: [], 14279 dinf: [], 14280 dref: [], 14281 esds: [], 14282 ftyp: [], 14283 hdlr: [], 14284 mdat: [], 14285 mdhd: [], 14286 mdia: [], 14287 mfhd: [], 14288 minf: [], 14289 moof: [], 14290 moov: [], 14291 mp4a: [], 14292 '.mp3': [], 14293 dac3: [], 14294 'ac-3': [], 14295 mvex: [], 14296 mvhd: [], 14297 pasp: [], 14298 sdtp: [], 14299 stbl: [], 14300 stco: [], 14301 stsc: [], 14302 stsd: [], 14303 stsz: [], 14304 stts: [], 14305 tfdt: [], 14306 tfhd: [], 14307 traf: [], 14308 trak: [], 14309 trun: [], 14310 trex: [], 14311 tkhd: [], 14312 vmhd: [], 14313 smhd: [] 14314 }; 14315 let i; 14316 for (i in MP4.types) { 14317 if (MP4.types.hasOwnProperty(i)) { 14318 MP4.types[i] = [i.charCodeAt(0), i.charCodeAt(1), i.charCodeAt(2), i.charCodeAt(3)]; 14319 } 14320 } 14321 const videoHdlr = new Uint8Array([0x00, 14322 // version 0 14323 0x00, 0x00, 0x00, 14324 // flags 14325 0x00, 0x00, 0x00, 0x00, 14326 // pre_defined 14327 0x76, 0x69, 0x64, 0x65, 14328 // handler_type: 'vide' 14329 0x00, 0x00, 0x00, 0x00, 14330 // reserved 14331 0x00, 0x00, 0x00, 0x00, 14332 // reserved 14333 0x00, 0x00, 0x00, 0x00, 14334 // reserved 14335 0x56, 0x69, 0x64, 0x65, 0x6f, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler' 14336 ]); 14337 const audioHdlr = new Uint8Array([0x00, 14338 // version 0 14339 0x00, 0x00, 0x00, 14340 // flags 14341 0x00, 0x00, 0x00, 0x00, 14342 // pre_defined 14343 0x73, 0x6f, 0x75, 0x6e, 14344 // handler_type: 'soun' 14345 0x00, 0x00, 0x00, 0x00, 14346 // reserved 14347 0x00, 0x00, 0x00, 0x00, 14348 // reserved 14349 0x00, 0x00, 0x00, 0x00, 14350 // reserved 14351 0x53, 0x6f, 0x75, 0x6e, 0x64, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler' 14352 ]); 14353 MP4.HDLR_TYPES = { 14354 video: videoHdlr, 14355 audio: audioHdlr 14356 }; 14357 const dref = new Uint8Array([0x00, 14358 // version 0 14359 0x00, 0x00, 0x00, 14360 // flags 14361 0x00, 0x00, 0x00, 0x01, 14362 // entry_count 14363 0x00, 0x00, 0x00, 0x0c, 14364 // entry_size 14365 0x75, 0x72, 0x6c, 0x20, 14366 // 'url' type 14367 0x00, 14368 // version 0 14369 0x00, 0x00, 0x01 // entry_flags 14370 ]); 14371 const stco = new Uint8Array([0x00, 14372 // version 14373 0x00, 0x00, 0x00, 14374 // flags 14375 0x00, 0x00, 0x00, 0x00 // entry_count 14376 ]); 14377 MP4.STTS = MP4.STSC = MP4.STCO = stco; 14378 MP4.STSZ = new Uint8Array([0x00, 14379 // version 14380 0x00, 0x00, 0x00, 14381 // flags 14382 0x00, 0x00, 0x00, 0x00, 14383 // sample_size 14384 0x00, 0x00, 0x00, 0x00 // sample_count 14385 ]); 14386 MP4.VMHD = new Uint8Array([0x00, 14387 // version 14388 0x00, 0x00, 0x01, 14389 // flags 14390 0x00, 0x00, 14391 // graphicsmode 14392 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // opcolor 14393 ]); 14394 MP4.SMHD = new Uint8Array([0x00, 14395 // version 14396 0x00, 0x00, 0x00, 14397 // flags 14398 0x00, 0x00, 14399 // balance 14400 0x00, 0x00 // reserved 14401 ]); 14402 MP4.STSD = new Uint8Array([0x00, 14403 // version 0 14404 0x00, 0x00, 0x00, 14405 // flags 14406 0x00, 0x00, 0x00, 0x01]); // entry_count 14407 14408 const majorBrand = new Uint8Array([105, 115, 111, 109]); // isom 14409 const avc1Brand = new Uint8Array([97, 118, 99, 49]); // avc1 14410 const minorVersion = new Uint8Array([0, 0, 0, 1]); 14411 MP4.FTYP = MP4.box(MP4.types.ftyp, majorBrand, minorVersion, majorBrand, avc1Brand); 14412 MP4.DINF = MP4.box(MP4.types.dinf, MP4.box(MP4.types.dref, dref)); 14413 } 14414 static box(type, ...payload) { 14415 let size = 8; 14416 let i = payload.length; 14417 const len = i; 14418 // calculate the total size we need to allocate 14419 while (i--) { 14420 size += payload[i].byteLength; 14421 } 14422 const result = new Uint8Array(size); 14423 result[0] = size >> 24 & 0xff; 14424 result[1] = size >> 16 & 0xff; 14425 result[2] = size >> 8 & 0xff; 14426 result[3] = size & 0xff; 14427 result.set(type, 4); 14428 // copy the payload into the result 14429 for (i = 0, size = 8; i < len; i++) { 14430 // copy payload[i] array @ offset size 14431 result.set(payload[i], size); 14432 size += payload[i].byteLength; 14433 } 14434 return result; 14435 } 14436 static hdlr(type) { 14437 return MP4.box(MP4.types.hdlr, MP4.HDLR_TYPES[type]); 14438 } 14439 static mdat(data) { 14440 return MP4.box(MP4.types.mdat, data); 14441 } 14442 static mdhd(timescale, duration) { 14443 duration *= timescale; 14444 const upperWordDuration = Math.floor(duration / (UINT32_MAX + 1)); 14445 const lowerWordDuration = Math.floor(duration % (UINT32_MAX + 1)); 14446 return MP4.box(MP4.types.mdhd, new Uint8Array([0x01, 14447 // version 1 14448 0x00, 0x00, 0x00, 14449 // flags 14450 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 14451 // creation_time 14452 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 14453 // modification_time 14454 timescale >> 24 & 0xff, timescale >> 16 & 0xff, timescale >> 8 & 0xff, timescale & 0xff, 14455 // timescale 14456 upperWordDuration >> 24, upperWordDuration >> 16 & 0xff, upperWordDuration >> 8 & 0xff, upperWordDuration & 0xff, lowerWordDuration >> 24, lowerWordDuration >> 16 & 0xff, lowerWordDuration >> 8 & 0xff, lowerWordDuration & 0xff, 0x55, 0xc4, 14457 // 'und' language (undetermined) 14458 0x00, 0x00])); 14459 } 14460 static mdia(track) { 14461 return MP4.box(MP4.types.mdia, MP4.mdhd(track.timescale || 0, track.duration || 0), MP4.hdlr(track.type), MP4.minf(track)); 14462 } 14463 static mfhd(sequenceNumber) { 14464 return MP4.box(MP4.types.mfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, 14465 // flags 14466 sequenceNumber >> 24, sequenceNumber >> 16 & 0xff, sequenceNumber >> 8 & 0xff, sequenceNumber & 0xff // sequence_number 14467 ])); 14468 } 14469 static minf(track) { 14470 if (track.type === 'audio') { 14471 return MP4.box(MP4.types.minf, MP4.box(MP4.types.smhd, MP4.SMHD), MP4.DINF, MP4.stbl(track)); 14472 } else { 14473 return MP4.box(MP4.types.minf, MP4.box(MP4.types.vmhd, MP4.VMHD), MP4.DINF, MP4.stbl(track)); 14474 } 14475 } 14476 static moof(sn, baseMediaDecodeTime, track) { 14477 return MP4.box(MP4.types.moof, MP4.mfhd(sn), MP4.traf(track, baseMediaDecodeTime)); 14478 } 14479 static moov(tracks) { 14480 let i = tracks.length; 14481 const boxes = []; 14482 while (i--) { 14483 boxes[i] = MP4.trak(tracks[i]); 14484 } 14485 return MP4.box.apply(null, [MP4.types.moov, MP4.mvhd(tracks[0].timescale || 0, tracks[0].duration || 0)].concat(boxes).concat(MP4.mvex(tracks))); 14486 } 14487 static mvex(tracks) { 14488 let i = tracks.length; 14489 const boxes = []; 14490 while (i--) { 14491 boxes[i] = MP4.trex(tracks[i]); 14492 } 14493 return MP4.box.apply(null, [MP4.types.mvex, ...boxes]); 14494 } 14495 static mvhd(timescale, duration) { 14496 duration *= timescale; 14497 const upperWordDuration = Math.floor(duration / (UINT32_MAX + 1)); 14498 const lowerWordDuration = Math.floor(duration % (UINT32_MAX + 1)); 14499 const bytes = new Uint8Array([0x01, 14500 // version 1 14501 0x00, 0x00, 0x00, 14502 // flags 14503 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 14504 // creation_time 14505 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 14506 // modification_time 14507 timescale >> 24 & 0xff, timescale >> 16 & 0xff, timescale >> 8 & 0xff, timescale & 0xff, 14508 // timescale 14509 upperWordDuration >> 24, upperWordDuration >> 16 & 0xff, upperWordDuration >> 8 & 0xff, upperWordDuration & 0xff, lowerWordDuration >> 24, lowerWordDuration >> 16 & 0xff, lowerWordDuration >> 8 & 0xff, lowerWordDuration & 0xff, 0x00, 0x01, 0x00, 0x00, 14510 // 1.0 rate 14511 0x01, 0x00, 14512 // 1.0 volume 14513 0x00, 0x00, 14514 // reserved 14515 0x00, 0x00, 0x00, 0x00, 14516 // reserved 14517 0x00, 0x00, 0x00, 0x00, 14518 // reserved 14519 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 14520 // transformation: unity matrix 14521 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 14522 // pre_defined 14523 0xff, 0xff, 0xff, 0xff // next_track_ID 14524 ]); 14525 return MP4.box(MP4.types.mvhd, bytes); 14526 } 14527 static sdtp(track) { 14528 const samples = track.samples || []; 14529 const bytes = new Uint8Array(4 + samples.length); 14530 let i; 14531 let flags; 14532 // leave the full box header (4 bytes) all zero 14533 // write the sample table 14534 for (i = 0; i < samples.length; i++) { 14535 flags = samples[i].flags; 14536 bytes[i + 4] = flags.dependsOn << 4 | flags.isDependedOn << 2 | flags.hasRedundancy; 14537 } 14538 return MP4.box(MP4.types.sdtp, bytes); 14539 } 14540 static stbl(track) { 14541 return MP4.box(MP4.types.stbl, MP4.stsd(track), MP4.box(MP4.types.stts, MP4.STTS), MP4.box(MP4.types.stsc, MP4.STSC), MP4.box(MP4.types.stsz, MP4.STSZ), MP4.box(MP4.types.stco, MP4.STCO)); 14542 } 14543 static avc1(track) { 14544 let sps = []; 14545 let pps = []; 14546 let i; 14547 let data; 14548 let len; 14549 // assemble the SPSs 14550 14551 for (i = 0; i < track.sps.length; i++) { 14552 data = track.sps[i]; 14553 len = data.byteLength; 14554 sps.push(len >>> 8 & 0xff); 14555 sps.push(len & 0xff); 14556 14557 // SPS 14558 sps = sps.concat(Array.prototype.slice.call(data)); 14559 } 14560 14561 // assemble the PPSs 14562 for (i = 0; i < track.pps.length; i++) { 14563 data = track.pps[i]; 14564 len = data.byteLength; 14565 pps.push(len >>> 8 & 0xff); 14566 pps.push(len & 0xff); 14567 pps = pps.concat(Array.prototype.slice.call(data)); 14568 } 14569 const avcc = MP4.box(MP4.types.avcC, new Uint8Array([0x01, 14570 // version 14571 sps[3], 14572 // profile 14573 sps[4], 14574 // profile compat 14575 sps[5], 14576 // level 14577 0xfc | 3, 14578 // lengthSizeMinusOne, hard-coded to 4 bytes 14579 0xe0 | track.sps.length // 3bit reserved (111) + numOfSequenceParameterSets 14580 ].concat(sps).concat([track.pps.length // numOfPictureParameterSets 14581 ]).concat(pps))); // "PPS" 14582 const width = track.width; 14583 const height = track.height; 14584 const hSpacing = track.pixelRatio[0]; 14585 const vSpacing = track.pixelRatio[1]; 14586 return MP4.box(MP4.types.avc1, new Uint8Array([0x00, 0x00, 0x00, 14587 // reserved 14588 0x00, 0x00, 0x00, 14589 // reserved 14590 0x00, 0x01, 14591 // data_reference_index 14592 0x00, 0x00, 14593 // pre_defined 14594 0x00, 0x00, 14595 // reserved 14596 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 14597 // pre_defined 14598 width >> 8 & 0xff, width & 0xff, 14599 // width 14600 height >> 8 & 0xff, height & 0xff, 14601 // height 14602 0x00, 0x48, 0x00, 0x00, 14603 // horizresolution 14604 0x00, 0x48, 0x00, 0x00, 14605 // vertresolution 14606 0x00, 0x00, 0x00, 0x00, 14607 // reserved 14608 0x00, 0x01, 14609 // frame_count 14610 0x12, 0x64, 0x61, 0x69, 0x6c, 14611 // dailymotion/hls.js 14612 0x79, 0x6d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x68, 0x6c, 0x73, 0x2e, 0x6a, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 14613 // compressorname 14614 0x00, 0x18, 14615 // depth = 24 14616 0x11, 0x11]), 14617 // pre_defined = -1 14618 avcc, MP4.box(MP4.types.btrt, new Uint8Array([0x00, 0x1c, 0x9c, 0x80, 14619 // bufferSizeDB 14620 0x00, 0x2d, 0xc6, 0xc0, 14621 // maxBitrate 14622 0x00, 0x2d, 0xc6, 0xc0])), 14623 // avgBitrate 14624 MP4.box(MP4.types.pasp, new Uint8Array([hSpacing >> 24, 14625 // hSpacing 14626 hSpacing >> 16 & 0xff, hSpacing >> 8 & 0xff, hSpacing & 0xff, vSpacing >> 24, 14627 // vSpacing 14628 vSpacing >> 16 & 0xff, vSpacing >> 8 & 0xff, vSpacing & 0xff]))); 14629 } 14630 static esds(track) { 14631 const config = track.config; 14632 return new Uint8Array([0x00, 14633 // version 0 14634 0x00, 0x00, 0x00, 14635 // flags 14636 14637 0x03, 14638 // descriptor_type 14639 0x19, 14640 // length 14641 14642 0x00, 0x01, 14643 // es_id 14644 14645 0x00, 14646 // stream_priority 14647 14648 0x04, 14649 // descriptor_type 14650 0x11, 14651 // length 14652 0x40, 14653 // codec : mpeg4_audio 14654 0x15, 14655 // stream_type 14656 0x00, 0x00, 0x00, 14657 // buffer_size 14658 0x00, 0x00, 0x00, 0x00, 14659 // maxBitrate 14660 0x00, 0x00, 0x00, 0x00, 14661 // avgBitrate 14662 14663 0x05, 14664 // descriptor_type 14665 0x02, 14666 // length 14667 ...config, 0x06, 0x01, 0x02 // GASpecificConfig)); // length + audio config descriptor 14668 ]); 14669 } 14670 static audioStsd(track) { 14671 const samplerate = track.samplerate || 0; 14672 return new Uint8Array([0x00, 0x00, 0x00, 14673 // reserved 14674 0x00, 0x00, 0x00, 14675 // reserved 14676 0x00, 0x01, 14677 // data_reference_index 14678 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 14679 // reserved 14680 0x00, track.channelCount || 0, 14681 // channelcount 14682 0x00, 0x10, 14683 // sampleSize:16bits 14684 0x00, 0x00, 0x00, 0x00, 14685 // reserved2 14686 samplerate >> 8 & 0xff, samplerate & 0xff, 14687 // 14688 0x00, 0x00]); 14689 } 14690 static mp4a(track) { 14691 return MP4.box(MP4.types.mp4a, MP4.audioStsd(track), MP4.box(MP4.types.esds, MP4.esds(track))); 14692 } 14693 static mp3(track) { 14694 return MP4.box(MP4.types['.mp3'], MP4.audioStsd(track)); 14695 } 14696 static ac3(track) { 14697 return MP4.box(MP4.types['ac-3'], MP4.audioStsd(track), MP4.box(MP4.types.dac3, track.config)); 14698 } 14699 static stsd(track) { 14700 const { 14701 segmentCodec 14702 } = track; 14703 if (track.type === 'audio') { 14704 if (segmentCodec === 'aac') { 14705 return MP4.box(MP4.types.stsd, MP4.STSD, MP4.mp4a(track)); 14706 } 14707 if (segmentCodec === 'ac3' && track.config) { 14708 return MP4.box(MP4.types.stsd, MP4.STSD, MP4.ac3(track)); 14709 } 14710 if (segmentCodec === 'mp3' && track.codec === 'mp3') { 14711 return MP4.box(MP4.types.stsd, MP4.STSD, MP4.mp3(track)); 14712 } 14713 } else { 14714 if (track.pps && track.sps) { 14715 if (segmentCodec === 'avc') { 14716 return MP4.box(MP4.types.stsd, MP4.STSD, MP4.avc1(track)); 14717 } 14718 if (segmentCodec === 'hevc' && track.vps) { 14719 return MP4.box(MP4.types.stsd, MP4.STSD, MP4.hvc1(track)); 14720 } 14721 } else { 14722 throw new Error(`video track missing pps or sps`); 14723 } 14724 } 14725 throw new Error(`unsupported ${track.type} segment codec (${segmentCodec}/${track.codec})`); 14726 } 14727 static tkhd(track) { 14728 const id = track.id; 14729 const duration = (track.duration || 0) * (track.timescale || 0); 14730 const width = track.width || 0; 14731 const height = track.height || 0; 14732 const upperWordDuration = Math.floor(duration / (UINT32_MAX + 1)); 14733 const lowerWordDuration = Math.floor(duration % (UINT32_MAX + 1)); 14734 return MP4.box(MP4.types.tkhd, new Uint8Array([0x01, 14735 // version 1 14736 0x00, 0x00, 0x07, 14737 // flags 14738 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 14739 // creation_time 14740 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 14741 // modification_time 14742 id >> 24 & 0xff, id >> 16 & 0xff, id >> 8 & 0xff, id & 0xff, 14743 // track_ID 14744 0x00, 0x00, 0x00, 0x00, 14745 // reserved 14746 upperWordDuration >> 24, upperWordDuration >> 16 & 0xff, upperWordDuration >> 8 & 0xff, upperWordDuration & 0xff, lowerWordDuration >> 24, lowerWordDuration >> 16 & 0xff, lowerWordDuration >> 8 & 0xff, lowerWordDuration & 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 14747 // reserved 14748 0x00, 0x00, 14749 // layer 14750 0x00, 0x00, 14751 // alternate_group 14752 0x00, 0x00, 14753 // non-audio track volume 14754 0x00, 0x00, 14755 // reserved 14756 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 14757 // transformation: unity matrix 14758 width >> 8 & 0xff, width & 0xff, 0x00, 0x00, 14759 // width 14760 height >> 8 & 0xff, height & 0xff, 0x00, 0x00 // height 14761 ])); 14762 } 14763 static traf(track, baseMediaDecodeTime) { 14764 const sampleDependencyTable = MP4.sdtp(track); 14765 const id = track.id; 14766 const upperWordBaseMediaDecodeTime = Math.floor(baseMediaDecodeTime / (UINT32_MAX + 1)); 14767 const lowerWordBaseMediaDecodeTime = Math.floor(baseMediaDecodeTime % (UINT32_MAX + 1)); 14768 return MP4.box(MP4.types.traf, MP4.box(MP4.types.tfhd, new Uint8Array([0x00, 14769 // version 0 14770 0x00, 0x00, 0x00, 14771 // flags 14772 id >> 24, id >> 16 & 0xff, id >> 8 & 0xff, id & 0xff // track_ID 14773 ])), MP4.box(MP4.types.tfdt, new Uint8Array([0x01, 14774 // version 1 14775 0x00, 0x00, 0x00, 14776 // flags 14777 upperWordBaseMediaDecodeTime >> 24, upperWordBaseMediaDecodeTime >> 16 & 0xff, upperWordBaseMediaDecodeTime >> 8 & 0xff, upperWordBaseMediaDecodeTime & 0xff, lowerWordBaseMediaDecodeTime >> 24, lowerWordBaseMediaDecodeTime >> 16 & 0xff, lowerWordBaseMediaDecodeTime >> 8 & 0xff, lowerWordBaseMediaDecodeTime & 0xff])), MP4.trun(track, sampleDependencyTable.length + 16 + 14778 // tfhd 14779 20 + 14780 // tfdt 14781 8 + 14782 // traf header 14783 16 + 14784 // mfhd 14785 8 + 14786 // moof header 14787 8), 14788 // mdat header 14789 sampleDependencyTable); 14790 } 14791 14792 /** 14793 * Generate a track box. 14794 * @param track a track definition 14795 */ 14796 static trak(track) { 14797 track.duration = track.duration || 0xffffffff; 14798 return MP4.box(MP4.types.trak, MP4.tkhd(track), MP4.mdia(track)); 14799 } 14800 static trex(track) { 14801 const id = track.id; 14802 return MP4.box(MP4.types.trex, new Uint8Array([0x00, 14803 // version 0 14804 0x00, 0x00, 0x00, 14805 // flags 14806 id >> 24, id >> 16 & 0xff, id >> 8 & 0xff, id & 0xff, 14807 // track_ID 14808 0x00, 0x00, 0x00, 0x01, 14809 // default_sample_description_index 14810 0x00, 0x00, 0x00, 0x00, 14811 // default_sample_duration 14812 0x00, 0x00, 0x00, 0x00, 14813 // default_sample_size 14814 0x00, 0x01, 0x00, 0x01 // default_sample_flags 14815 ])); 14816 } 14817 static trun(track, offset) { 14818 const samples = track.samples || []; 14819 const len = samples.length; 14820 const arraylen = 12 + 16 * len; 14821 const array = new Uint8Array(arraylen); 14822 let i; 14823 let sample; 14824 let duration; 14825 let size; 14826 let flags; 14827 let cts; 14828 offset += 8 + arraylen; 14829 array.set([track.type === 'video' ? 0x01 : 0x00, 14830 // version 1 for video with signed-int sample_composition_time_offset 14831 0x00, 0x0f, 0x01, 14832 // flags 14833 len >>> 24 & 0xff, len >>> 16 & 0xff, len >>> 8 & 0xff, len & 0xff, 14834 // sample_count 14835 offset >>> 24 & 0xff, offset >>> 16 & 0xff, offset >>> 8 & 0xff, offset & 0xff // data_offset 14836 ], 0); 14837 for (i = 0; i < len; i++) { 14838 sample = samples[i]; 14839 duration = sample.duration; 14840 size = sample.size; 14841 flags = sample.flags; 14842 cts = sample.cts; 14843 array.set([duration >>> 24 & 0xff, duration >>> 16 & 0xff, duration >>> 8 & 0xff, duration & 0xff, 14844 // sample_duration 14845 size >>> 24 & 0xff, size >>> 16 & 0xff, size >>> 8 & 0xff, size & 0xff, 14846 // sample_size 14847 flags.isLeading << 2 | flags.dependsOn, flags.isDependedOn << 6 | flags.hasRedundancy << 4 | flags.paddingValue << 1 | flags.isNonSync, flags.degradPrio & 0xf0 << 8, flags.degradPrio & 0x0f, 14848 // sample_flags 14849 cts >>> 24 & 0xff, cts >>> 16 & 0xff, cts >>> 8 & 0xff, cts & 0xff // sample_composition_time_offset 14850 ], 12 + 16 * i); 14851 } 14852 return MP4.box(MP4.types.trun, array); 14853 } 14854 static initSegment(tracks) { 14855 if (!MP4.types) { 14856 MP4.init(); 14857 } 14858 const movie = MP4.moov(tracks); 14859 const result = appendUint8Array(MP4.FTYP, movie); 14860 return result; 14861 } 14862 static hvc1(track) { 14863 const ps = track.params; 14864 const units = [track.vps, track.sps, track.pps]; 14865 const NALuLengthSize = 4; 14866 const config = new Uint8Array([0x01, ps.general_profile_space << 6 | (ps.general_tier_flag ? 32 : 0) | ps.general_profile_idc, ps.general_profile_compatibility_flags[0], ps.general_profile_compatibility_flags[1], ps.general_profile_compatibility_flags[2], ps.general_profile_compatibility_flags[3], ps.general_constraint_indicator_flags[0], ps.general_constraint_indicator_flags[1], ps.general_constraint_indicator_flags[2], ps.general_constraint_indicator_flags[3], ps.general_constraint_indicator_flags[4], ps.general_constraint_indicator_flags[5], ps.general_level_idc, 240 | ps.min_spatial_segmentation_idc >> 8, 255 & ps.min_spatial_segmentation_idc, 252 | ps.parallelismType, 252 | ps.chroma_format_idc, 248 | ps.bit_depth_luma_minus8, 248 | ps.bit_depth_chroma_minus8, 0x00, parseInt(ps.frame_rate.fps), NALuLengthSize - 1 | ps.temporal_id_nested << 2 | ps.num_temporal_layers << 3 | (ps.frame_rate.fixed ? 64 : 0), units.length]); 14867 14868 // compute hvcC size in bytes 14869 let length = config.length; 14870 for (let i = 0; i < units.length; i += 1) { 14871 length += 3; 14872 for (let j = 0; j < units[i].length; j += 1) { 14873 length += 2 + units[i][j].length; 14874 } 14875 } 14876 const hvcC = new Uint8Array(length); 14877 hvcC.set(config, 0); 14878 length = config.length; 14879 // append parameter set units: one vps, one or more sps and pps 14880 const iMax = units.length - 1; 14881 for (let i = 0; i < units.length; i += 1) { 14882 hvcC.set(new Uint8Array([32 + i | (i === iMax ? 128 : 0), 0x00, units[i].length]), length); 14883 length += 3; 14884 for (let j = 0; j < units[i].length; j += 1) { 14885 hvcC.set(new Uint8Array([units[i][j].length >> 8, units[i][j].length & 255]), length); 14886 length += 2; 14887 hvcC.set(units[i][j], length); 14888 length += units[i][j].length; 14889 } 14890 } 14891 const hvcc = MP4.box(MP4.types.hvcC, hvcC); 14892 const width = track.width; 14893 const height = track.height; 14894 const hSpacing = track.pixelRatio[0]; 14895 const vSpacing = track.pixelRatio[1]; 14896 return MP4.box(MP4.types.hvc1, new Uint8Array([0x00, 0x00, 0x00, 14897 // reserved 14898 0x00, 0x00, 0x00, 14899 // reserved 14900 0x00, 0x01, 14901 // data_reference_index 14902 0x00, 0x00, 14903 // pre_defined 14904 0x00, 0x00, 14905 // reserved 14906 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 14907 // pre_defined 14908 width >> 8 & 0xff, width & 0xff, 14909 // width 14910 height >> 8 & 0xff, height & 0xff, 14911 // height 14912 0x00, 0x48, 0x00, 0x00, 14913 // horizresolution 14914 0x00, 0x48, 0x00, 0x00, 14915 // vertresolution 14916 0x00, 0x00, 0x00, 0x00, 14917 // reserved 14918 0x00, 0x01, 14919 // frame_count 14920 0x12, 0x64, 0x61, 0x69, 0x6c, 14921 // dailymotion/hls.js 14922 0x79, 0x6d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x68, 0x6c, 0x73, 0x2e, 0x6a, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 14923 // compressorname 14924 0x00, 0x18, 14925 // depth = 24 14926 0x11, 0x11]), 14927 // pre_defined = -1 14928 hvcc, MP4.box(MP4.types.btrt, new Uint8Array([0x00, 0x1c, 0x9c, 0x80, 14929 // bufferSizeDB 14930 0x00, 0x2d, 0xc6, 0xc0, 14931 // maxBitrate 14932 0x00, 0x2d, 0xc6, 0xc0])), 14933 // avgBitrate 14934 MP4.box(MP4.types.pasp, new Uint8Array([hSpacing >> 24, 14935 // hSpacing 14936 hSpacing >> 16 & 0xff, hSpacing >> 8 & 0xff, hSpacing & 0xff, vSpacing >> 24, 14937 // vSpacing 14938 vSpacing >> 16 & 0xff, vSpacing >> 8 & 0xff, vSpacing & 0xff]))); 14939 } 14940} 14941MP4.types = void 0; 14942MP4.HDLR_TYPES = void 0; 14943MP4.STTS = void 0; 14944MP4.STSC = void 0; 14945MP4.STCO = void 0; 14946MP4.STSZ = void 0; 14947MP4.VMHD = void 0; 14948MP4.SMHD = void 0; 14949MP4.STSD = void 0; 14950MP4.FTYP = void 0; 14951MP4.DINF = void 0; 14952 14953const MPEG_TS_CLOCK_FREQ_HZ = 90000; 14954function toTimescaleFromBase(baseTime, destScale, srcBase = 1, round = false) { 14955 const result = baseTime * destScale * srcBase; // equivalent to `(value * scale) / (1 / base)` 14956 return round ? Math.round(result) : result; 14957} 14958function toTimescaleFromScale(baseTime, destScale, srcScale = 1, round = false) { 14959 return toTimescaleFromBase(baseTime, destScale, 1 / srcScale, round); 14960} 14961function toMsFromMpegTsClock(baseTime, round = false) { 14962 return toTimescaleFromBase(baseTime, 1000, 1 / MPEG_TS_CLOCK_FREQ_HZ, round); 14963} 14964function toMpegTsClockFromTimescale(baseTime, srcScale = 1) { 14965 return toTimescaleFromBase(baseTime, MPEG_TS_CLOCK_FREQ_HZ, 1 / srcScale); 14966} 14967function timestampToString(timestamp) { 14968 const { 14969 baseTime, 14970 timescale, 14971 trackId 14972 } = timestamp; 14973 return `${baseTime / timescale} (${baseTime}/${timescale}) trackId: ${trackId}`; 14974} 14975 14976const MAX_SILENT_FRAME_DURATION = 10 * 1000; // 10 seconds 14977const AAC_SAMPLES_PER_FRAME = 1024; 14978const MPEG_AUDIO_SAMPLE_PER_FRAME = 1152; 14979const AC3_SAMPLES_PER_FRAME = 1536; 14980let chromeVersion = null; 14981let safariWebkitVersion = null; 14982function createMp4Sample(isKeyframe, duration, size, cts) { 14983 return { 14984 duration, 14985 size, 14986 cts, 14987 flags: { 14988 isLeading: 0, 14989 isDependedOn: 0, 14990 hasRedundancy: 0, 14991 degradPrio: 0, 14992 dependsOn: isKeyframe ? 2 : 1, 14993 isNonSync: isKeyframe ? 0 : 1 14994 } 14995 }; 14996} 14997class MP4Remuxer extends Logger { 14998 constructor(observer, config, typeSupported, logger) { 14999 super('mp4-remuxer', logger); 15000 this.observer = void 0; 15001 this.config = void 0; 15002 this.typeSupported = void 0; 15003 this.ISGenerated = false; 15004 this._initPTS = null; 15005 this._initDTS = null; 15006 this.nextVideoTs = null; 15007 this.nextAudioTs = null; 15008 this.videoSampleDuration = null; 15009 this.isAudioContiguous = false; 15010 this.isVideoContiguous = false; 15011 this.videoTrackConfig = void 0; 15012 this.observer = observer; 15013 this.config = config; 15014 this.typeSupported = typeSupported; 15015 this.ISGenerated = false; 15016 if (chromeVersion === null) { 15017 const userAgent = navigator.userAgent || ''; 15018 const result = userAgent.match(/Chrome\/(\d+)/i); 15019 chromeVersion = result ? parseInt(result[1]) : 0; 15020 } 15021 if (safariWebkitVersion === null) { 15022 const result = navigator.userAgent.match(/Safari\/(\d+)/i); 15023 safariWebkitVersion = result ? parseInt(result[1]) : 0; 15024 } 15025 } 15026 destroy() { 15027 // @ts-ignore 15028 this.config = this.videoTrackConfig = this._initPTS = this._initDTS = null; 15029 } 15030 resetTimeStamp(defaultTimeStamp) { 15031 const initPTS = this._initPTS; 15032 if (!initPTS || !defaultTimeStamp || defaultTimeStamp.trackId !== initPTS.trackId || defaultTimeStamp.baseTime !== initPTS.baseTime || defaultTimeStamp.timescale !== initPTS.timescale) { 15033 this.log(`Reset initPTS: ${initPTS ? timestampToString(initPTS) : initPTS} > ${defaultTimeStamp ? timestampToString(defaultTimeStamp) : defaultTimeStamp}`); 15034 } 15035 this._initPTS = this._initDTS = defaultTimeStamp; 15036 } 15037 resetNextTimestamp() { 15038 this.log('reset next timestamp'); 15039 this.isVideoContiguous = false; 15040 this.isAudioContiguous = false; 15041 } 15042 resetInitSegment() { 15043 this.log('ISGenerated flag reset'); 15044 this.ISGenerated = false; 15045 this.videoTrackConfig = undefined; 15046 } 15047 getVideoStartPts(videoSamples) { 15048 // Get the minimum PTS value relative to the first sample's PTS, normalized for 33-bit wrapping 15049 let rolloverDetected = false; 15050 const firstPts = videoSamples[0].pts; 15051 const startPTS = videoSamples.reduce((minPTS, sample) => { 15052 let pts = sample.pts; 15053 let delta = pts - minPTS; 15054 if (delta < -4294967296) { 15055 // 2^32, see PTSNormalize for reasoning, but we're hitting a rollover here, and we don't want that to impact the timeOffset calculation 15056 rolloverDetected = true; 15057 pts = normalizePts(pts, firstPts); 15058 delta = pts - minPTS; 15059 } 15060 if (delta > 0) { 15061 return minPTS; 15062 } 15063 return pts; 15064 }, firstPts); 15065 if (rolloverDetected) { 15066 this.debug('PTS rollover detected'); 15067 } 15068 return startPTS; 15069 } 15070 remux(audioTrack, videoTrack, id3Track, textTrack, timeOffset, accurateTimeOffset, flush, playlistType) { 15071 let video; 15072 let audio; 15073 let initSegment; 15074 let text; 15075 let id3; 15076 let independent; 15077 let audioTimeOffset = timeOffset; 15078 let videoTimeOffset = timeOffset; 15079 15080 // If we're remuxing audio and video progressively, wait until we've received enough samples for each track before proceeding. 15081 // This is done to synchronize the audio and video streams. We know if the current segment will have samples if the "pid" 15082 // parameter is greater than -1. The pid is set when the PMT is parsed, which contains the tracks list. 15083 // However, if the initSegment has already been generated, or we've reached the end of a segment (flush), 15084 // then we can remux one track without waiting for the other. 15085 const hasAudio = audioTrack.pid > -1; 15086 const hasVideo = videoTrack.pid > -1; 15087 const length = videoTrack.samples.length; 15088 const enoughAudioSamples = audioTrack.samples.length > 0; 15089 const enoughVideoSamples = flush && length > 0 || length > 1; 15090 const canRemuxAvc = (!hasAudio || enoughAudioSamples) && (!hasVideo || enoughVideoSamples) || this.ISGenerated || flush; 15091 if (canRemuxAvc) { 15092 if (this.ISGenerated) { 15093 var _videoTrack$pixelRati, _config$pixelRatio, _videoTrack$pixelRati2, _config$pixelRatio2; 15094 const config = this.videoTrackConfig; 15095 if (config && (videoTrack.width !== config.width || videoTrack.height !== config.height || ((_videoTrack$pixelRati = videoTrack.pixelRatio) == null ? void 0 : _videoTrack$pixelRati[0]) !== ((_config$pixelRatio = config.pixelRatio) == null ? void 0 : _config$pixelRatio[0]) || ((_videoTrack$pixelRati2 = videoTrack.pixelRatio) == null ? void 0 : _videoTrack$pixelRati2[1]) !== ((_config$pixelRatio2 = config.pixelRatio) == null ? void 0 : _config$pixelRatio2[1])) || !config && enoughVideoSamples || this.nextAudioTs === null && enoughAudioSamples) { 15096 this.resetInitSegment(); 15097 } 15098 } 15099 if (!this.ISGenerated) { 15100 initSegment = this.generateIS(audioTrack, videoTrack, timeOffset, accurateTimeOffset); 15101 } 15102 const isVideoContiguous = this.isVideoContiguous; 15103 let firstKeyFrameIndex = -1; 15104 let firstKeyFramePTS; 15105 if (enoughVideoSamples) { 15106 firstKeyFrameIndex = findKeyframeIndex(videoTrack.samples); 15107 if (!isVideoContiguous && this.config.forceKeyFrameOnDiscontinuity) { 15108 independent = true; 15109 if (firstKeyFrameIndex > 0) { 15110 this.warn(`Dropped ${firstKeyFrameIndex} out of ${length} video samples due to a missing keyframe`); 15111 const startPTS = this.getVideoStartPts(videoTrack.samples); 15112 videoTrack.samples = videoTrack.samples.slice(firstKeyFrameIndex); 15113 videoTrack.dropped += firstKeyFrameIndex; 15114 videoTimeOffset += (videoTrack.samples[0].pts - startPTS) / videoTrack.inputTimeScale; 15115 firstKeyFramePTS = videoTimeOffset; 15116 } else if (firstKeyFrameIndex === -1) { 15117 this.warn(`No keyframe found out of ${length} video samples`); 15118 independent = false; 15119 } 15120 } 15121 } 15122 if (this.ISGenerated) { 15123 if (enoughAudioSamples && enoughVideoSamples) { 15124 // timeOffset is expected to be the offset of the first timestamp of this fragment (first DTS) 15125 // if first audio DTS is not aligned with first video DTS then we need to take that into account 15126 // when providing timeOffset to remuxAudio / remuxVideo. if we don't do that, there might be a permanent / small 15127 // drift between audio and video streams 15128 const startPTS = this.getVideoStartPts(videoTrack.samples); 15129 const tsDelta = normalizePts(audioTrack.samples[0].pts, startPTS) - startPTS; 15130 const audiovideoTimestampDelta = tsDelta / videoTrack.inputTimeScale; 15131 audioTimeOffset += Math.max(0, audiovideoTimestampDelta); 15132 videoTimeOffset += Math.max(0, -audiovideoTimestampDelta); 15133 } 15134 15135 // Purposefully remuxing audio before video, so that remuxVideo can use nextAudioPts, which is calculated in remuxAudio. 15136 if (enoughAudioSamples) { 15137 // if initSegment was generated without audio samples, regenerate it again 15138 if (!audioTrack.samplerate) { 15139 this.warn('regenerate InitSegment as audio detected'); 15140 initSegment = this.generateIS(audioTrack, videoTrack, timeOffset, accurateTimeOffset); 15141 } 15142 audio = this.remuxAudio(audioTrack, audioTimeOffset, this.isAudioContiguous, accurateTimeOffset, hasVideo || enoughVideoSamples || playlistType === PlaylistLevelType.AUDIO ? videoTimeOffset : undefined); 15143 if (enoughVideoSamples) { 15144 const audioTrackLength = audio ? audio.endPTS - audio.startPTS : 0; 15145 // if initSegment was generated without video samples, regenerate it again 15146 if (!videoTrack.inputTimeScale) { 15147 this.warn('regenerate InitSegment as video detected'); 15148 initSegment = this.generateIS(audioTrack, videoTrack, timeOffset, accurateTimeOffset); 15149 } 15150 video = this.remuxVideo(videoTrack, videoTimeOffset, isVideoContiguous, audioTrackLength); 15151 } 15152 } else if (enoughVideoSamples) { 15153 video = this.remuxVideo(videoTrack, videoTimeOffset, isVideoContiguous, 0); 15154 } 15155 if (video) { 15156 video.firstKeyFrame = firstKeyFrameIndex; 15157 video.independent = firstKeyFrameIndex !== -1; 15158 video.firstKeyFramePTS = firstKeyFramePTS; 15159 } 15160 } 15161 } 15162 15163 // Allow ID3 and text to remux, even if more audio/video samples are required 15164 if (this.ISGenerated && this._initPTS && this._initDTS) { 15165 if (id3Track.samples.length) { 15166 id3 = flushTextTrackMetadataCueSamples(id3Track, timeOffset, this._initPTS, this._initDTS); 15167 } 15168 if (textTrack.samples.length) { 15169 text = flushTextTrackUserdataCueSamples(textTrack, timeOffset, this._initPTS); 15170 } 15171 } 15172 return { 15173 audio, 15174 video, 15175 initSegment, 15176 independent, 15177 text, 15178 id3 15179 }; 15180 } 15181 computeInitPts(basetime, timescale, presentationTime, type) { 15182 const offset = Math.round(presentationTime * timescale); 15183 let timestamp = normalizePts(basetime, offset); 15184 if (timestamp < offset + timescale) { 15185 this.log(`Adjusting PTS for rollover in timeline near ${(offset - timestamp) / timescale} ${type}`); 15186 while (timestamp < offset + timescale) { 15187 timestamp += 8589934592; 15188 } 15189 } 15190 return timestamp - offset; 15191 } 15192 generateIS(audioTrack, videoTrack, timeOffset, accurateTimeOffset) { 15193 const audioSamples = audioTrack.samples; 15194 const videoSamples = videoTrack.samples; 15195 const typeSupported = this.typeSupported; 15196 const tracks = {}; 15197 const _initPTS = this._initPTS; 15198 let computePTSDTS = !_initPTS || accurateTimeOffset; 15199 let container = 'audio/mp4'; 15200 let initPTS; 15201 let initDTS; 15202 let timescale; 15203 let trackId = -1; 15204 if (computePTSDTS) { 15205 initPTS = initDTS = Infinity; 15206 } 15207 if (audioTrack.config && audioSamples.length) { 15208 // let's use audio sampling rate as MP4 time scale. 15209 // rationale is that there is a integer nb of audio frames per audio sample (1024 for AAC) 15210 // using audio sampling rate here helps having an integer MP4 frame duration 15211 // this avoids potential rounding issue and AV sync issue 15212 audioTrack.timescale = audioTrack.samplerate; 15213 switch (audioTrack.segmentCodec) { 15214 case 'mp3': 15215 if (typeSupported.mpeg) { 15216 // Chrome and Safari 15217 container = 'audio/mpeg'; 15218 audioTrack.codec = ''; 15219 } else if (typeSupported.mp3) { 15220 // Firefox 15221 audioTrack.codec = 'mp3'; 15222 } 15223 break; 15224 case 'ac3': 15225 audioTrack.codec = 'ac-3'; 15226 break; 15227 } 15228 tracks.audio = { 15229 id: 'audio', 15230 container: container, 15231 codec: audioTrack.codec, 15232 initSegment: audioTrack.segmentCodec === 'mp3' && typeSupported.mpeg ? new Uint8Array(0) : MP4.initSegment([audioTrack]), 15233 metadata: { 15234 channelCount: audioTrack.channelCount 15235 } 15236 }; 15237 if (computePTSDTS) { 15238 trackId = audioTrack.id; 15239 timescale = audioTrack.inputTimeScale; 15240 if (!_initPTS || timescale !== _initPTS.timescale) { 15241 // remember first PTS of this demuxing context. for audio, PTS = DTS 15242 initPTS = initDTS = this.computeInitPts(audioSamples[0].pts, timescale, timeOffset, 'audio'); 15243 } else { 15244 computePTSDTS = false; 15245 } 15246 } 15247 } 15248 if (videoTrack.sps && videoTrack.pps && videoSamples.length) { 15249 // let's use input time scale as MP4 video timescale 15250 // we use input time scale straight away to avoid rounding issues on frame duration / cts computation 15251 videoTrack.timescale = videoTrack.inputTimeScale; 15252 tracks.video = { 15253 id: 'main', 15254 container: 'video/mp4', 15255 codec: videoTrack.codec, 15256 initSegment: MP4.initSegment([videoTrack]), 15257 metadata: { 15258 width: videoTrack.width, 15259 height: videoTrack.height 15260 } 15261 }; 15262 if (computePTSDTS) { 15263 trackId = videoTrack.id; 15264 timescale = videoTrack.inputTimeScale; 15265 if (!_initPTS || timescale !== _initPTS.timescale) { 15266 const basePTS = this.getVideoStartPts(videoSamples); 15267 const baseDTS = normalizePts(videoSamples[0].dts, basePTS); 15268 const videoInitDTS = this.computeInitPts(baseDTS, timescale, timeOffset, 'video'); 15269 const videoInitPTS = this.computeInitPts(basePTS, timescale, timeOffset, 'video'); 15270 initDTS = Math.min(initDTS, videoInitDTS); 15271 initPTS = Math.min(initPTS, videoInitPTS); 15272 } else { 15273 computePTSDTS = false; 15274 } 15275 } 15276 this.videoTrackConfig = { 15277 width: videoTrack.width, 15278 height: videoTrack.height, 15279 pixelRatio: videoTrack.pixelRatio 15280 }; 15281 } 15282 if (Object.keys(tracks).length) { 15283 this.ISGenerated = true; 15284 if (computePTSDTS) { 15285 if (_initPTS) { 15286 this.warn(`Timestamps at playlist time: ${accurateTimeOffset ? '' : '~'}${timeOffset} ${initPTS / timescale} != initPTS: ${_initPTS.baseTime / _initPTS.timescale} (${_initPTS.baseTime}/${_initPTS.timescale}) trackId: ${_initPTS.trackId}`); 15287 } 15288 this.log(`Found initPTS at playlist time: ${timeOffset} offset: ${initPTS / timescale} (${initPTS}/${timescale}) trackId: ${trackId}`); 15289 this._initPTS = { 15290 baseTime: initPTS, 15291 timescale: timescale, 15292 trackId: trackId 15293 }; 15294 this._initDTS = { 15295 baseTime: initDTS, 15296 timescale: timescale, 15297 trackId: trackId 15298 }; 15299 } else { 15300 initPTS = timescale = undefined; 15301 } 15302 return { 15303 tracks, 15304 initPTS, 15305 timescale, 15306 trackId 15307 }; 15308 } 15309 } 15310 remuxVideo(track, timeOffset, contiguous, audioTrackLength) { 15311 const timeScale = track.inputTimeScale; 15312 const inputSamples = track.samples; 15313 const outputSamples = []; 15314 const nbSamples = inputSamples.length; 15315 const initPTS = this._initPTS; 15316 const initTime = initPTS.baseTime * timeScale / initPTS.timescale; 15317 let nextVideoTs = this.nextVideoTs; 15318 let offset = 8; 15319 let mp4SampleDuration = this.videoSampleDuration; 15320 let firstDTS; 15321 let lastDTS; 15322 let minPTS = Number.POSITIVE_INFINITY; 15323 let maxPTS = Number.NEGATIVE_INFINITY; 15324 let sortSamples = false; 15325 15326 // if parsed fragment is contiguous with last one, let's use last DTS value as reference 15327 if (!contiguous || nextVideoTs === null) { 15328 const pts = initTime + timeOffset * timeScale; 15329 const cts = inputSamples[0].pts - normalizePts(inputSamples[0].dts, inputSamples[0].pts); 15330 if (chromeVersion && nextVideoTs !== null && Math.abs(pts - cts - (nextVideoTs + initTime)) < 15000) { 15331 // treat as contigous to adjust samples that would otherwise produce video buffer gaps in Chrome 15332 contiguous = true; 15333 } else { 15334 // if not contiguous, let's use target timeOffset 15335 nextVideoTs = pts - cts - initTime; 15336 } 15337 } 15338 15339 // PTS is coded on 33bits, and can loop from -2^32 to 2^32 15340 // PTSNormalize will make PTS/DTS value monotonic, we use last known DTS value as reference value 15341 const nextVideoPts = nextVideoTs + initTime; 15342 for (let i = 0; i < nbSamples; i++) { 15343 const sample = inputSamples[i]; 15344 sample.pts = normalizePts(sample.pts, nextVideoPts); 15345 sample.dts = normalizePts(sample.dts, nextVideoPts); 15346 if (sample.dts < inputSamples[i > 0 ? i - 1 : i].dts) { 15347 sortSamples = true; 15348 } 15349 } 15350 15351 // sort video samples by DTS then PTS then demux id order 15352 if (sortSamples) { 15353 inputSamples.sort(function (a, b) { 15354 const deltadts = a.dts - b.dts; 15355 const deltapts = a.pts - b.pts; 15356 return deltadts || deltapts; 15357 }); 15358 } 15359 15360 // Get first/last DTS 15361 firstDTS = inputSamples[0].dts; 15362 lastDTS = inputSamples[inputSamples.length - 1].dts; 15363 15364 // Sample duration (as expected by trun MP4 boxes), should be the delta between sample DTS 15365 // set this constant duration as being the avg delta between consecutive DTS. 15366 const inputDuration = lastDTS - firstDTS; 15367 const averageSampleDuration = inputDuration ? Math.round(inputDuration / (nbSamples - 1)) : mp4SampleDuration || track.inputTimeScale / 30; 15368 15369 // if fragment are contiguous, detect hole/overlapping between fragments 15370 if (contiguous) { 15371 // check timestamp continuity across consecutive fragments (this is to remove inter-fragment gap/hole) 15372 const delta = firstDTS - nextVideoPts; 15373 const foundHole = delta > averageSampleDuration; 15374 const foundOverlap = delta < -1; 15375 if (foundHole || foundOverlap) { 15376 if (foundHole) { 15377 this.warn(`${(track.segmentCodec || '').toUpperCase()}: ${toMsFromMpegTsClock(delta, true)} ms (${delta}dts) hole between fragments detected at ${timeOffset.toFixed(3)}`); 15378 } else { 15379 this.warn(`${(track.segmentCodec || '').toUpperCase()}: ${toMsFromMpegTsClock(-delta, true)} ms (${delta}dts) overlapping between fragments detected at ${timeOffset.toFixed(3)}`); 15380 } 15381 if (!foundOverlap || nextVideoPts >= inputSamples[0].pts || chromeVersion) { 15382 firstDTS = nextVideoPts; 15383 const firstPTS = inputSamples[0].pts - delta; 15384 if (foundHole) { 15385 inputSamples[0].dts = firstDTS; 15386 inputSamples[0].pts = firstPTS; 15387 } else { 15388 let isPTSOrderRetained = true; 15389 for (let i = 0; i < inputSamples.length; i++) { 15390 if (inputSamples[i].dts > firstPTS && isPTSOrderRetained) { 15391 break; 15392 } 15393 const prevPTS = inputSamples[i].pts; 15394 inputSamples[i].dts -= delta; 15395 inputSamples[i].pts -= delta; 15396 15397 // check to see if this sample's PTS order has changed 15398 // relative to the next one 15399 if (i < inputSamples.length - 1) { 15400 const nextSamplePTS = inputSamples[i + 1].pts; 15401 const currentSamplePTS = inputSamples[i].pts; 15402 const currentOrder = nextSamplePTS <= currentSamplePTS; 15403 const prevOrder = nextSamplePTS <= prevPTS; 15404 isPTSOrderRetained = currentOrder == prevOrder; 15405 } 15406 } 15407 } 15408 this.log(`Video: Initial PTS/DTS adjusted: ${toMsFromMpegTsClock(firstPTS, true)}/${toMsFromMpegTsClock(firstDTS, true)}, delta: ${toMsFromMpegTsClock(delta, true)} ms`); 15409 } 15410 } 15411 } 15412 firstDTS = Math.max(0, firstDTS); 15413 let nbNalu = 0; 15414 let naluLen = 0; 15415 let dtsStep = firstDTS; 15416 for (let i = 0; i < nbSamples; i++) { 15417 // compute total/avc sample length and nb of NAL units 15418 const sample = inputSamples[i]; 15419 const units = sample.units; 15420 const nbUnits = units.length; 15421 let sampleLen = 0; 15422 for (let j = 0; j < nbUnits; j++) { 15423 sampleLen += units[j].data.length; 15424 } 15425 naluLen += sampleLen; 15426 nbNalu += nbUnits; 15427 sample.length = sampleLen; 15428 15429 // ensure sample monotonic DTS 15430 if (sample.dts < dtsStep) { 15431 sample.dts = dtsStep; 15432 dtsStep += averageSampleDuration / 4 | 0 || 1; 15433 } else { 15434 dtsStep = sample.dts; 15435 } 15436 minPTS = Math.min(sample.pts, minPTS); 15437 maxPTS = Math.max(sample.pts, maxPTS); 15438 } 15439 lastDTS = inputSamples[nbSamples - 1].dts; 15440 15441 /* concatenate the video data and construct the mdat in place 15442 (need 8 more bytes to fill length and mpdat type) */ 15443 const mdatSize = naluLen + 4 * nbNalu + 8; 15444 let mdat; 15445 try { 15446 mdat = new Uint8Array(mdatSize); 15447 } catch (err) { 15448 this.observer.emit(Events.ERROR, Events.ERROR, { 15449 type: ErrorTypes.MUX_ERROR, 15450 details: ErrorDetails.REMUX_ALLOC_ERROR, 15451 fatal: false, 15452 error: err, 15453 bytes: mdatSize, 15454 reason: `fail allocating video mdat ${mdatSize}` 15455 }); 15456 return; 15457 } 15458 const view = new DataView(mdat.buffer); 15459 view.setUint32(0, mdatSize); 15460 mdat.set(MP4.types.mdat, 4); 15461 let stretchedLastFrame = false; 15462 let minDtsDelta = Number.POSITIVE_INFINITY; 15463 let minPtsDelta = Number.POSITIVE_INFINITY; 15464 let maxDtsDelta = Number.NEGATIVE_INFINITY; 15465 let maxPtsDelta = Number.NEGATIVE_INFINITY; 15466 for (let i = 0; i < nbSamples; i++) { 15467 const VideoSample = inputSamples[i]; 15468 const VideoSampleUnits = VideoSample.units; 15469 let mp4SampleLength = 0; 15470 // convert NALU bitstream to MP4 format (prepend NALU with size field) 15471 for (let j = 0, nbUnits = VideoSampleUnits.length; j < nbUnits; j++) { 15472 const unit = VideoSampleUnits[j]; 15473 const unitData = unit.data; 15474 const unitDataLen = unit.data.byteLength; 15475 view.setUint32(offset, unitDataLen); 15476 offset += 4; 15477 mdat.set(unitData, offset); 15478 offset += unitDataLen; 15479 mp4SampleLength += 4 + unitDataLen; 15480 } 15481 15482 // expected sample duration is the Decoding Timestamp diff of consecutive samples 15483 let ptsDelta; 15484 if (i < nbSamples - 1) { 15485 mp4SampleDuration = inputSamples[i + 1].dts - VideoSample.dts; 15486 ptsDelta = inputSamples[i + 1].pts - VideoSample.pts; 15487 } else { 15488 const config = this.config; 15489 const lastFrameDuration = i > 0 ? VideoSample.dts - inputSamples[i - 1].dts : averageSampleDuration; 15490 ptsDelta = i > 0 ? VideoSample.pts - inputSamples[i - 1].pts : averageSampleDuration; 15491 if (config.stretchShortVideoTrack && this.nextAudioTs !== null) { 15492 // In some cases, a segment's audio track duration may exceed the video track duration. 15493 // Since we've already remuxed audio, and we know how long the audio track is, we look to 15494 // see if the delta to the next segment is longer than maxBufferHole. 15495 // If so, playback would potentially get stuck, so we artificially inflate 15496 // the duration of the last frame to minimize any potential gap between segments. 15497 const gapTolerance = Math.floor(config.maxBufferHole * timeScale); 15498 const deltaToFrameEnd = (audioTrackLength ? minPTS + audioTrackLength * timeScale : this.nextAudioTs + initTime) - VideoSample.pts; 15499 if (deltaToFrameEnd > gapTolerance) { 15500 // We subtract lastFrameDuration from deltaToFrameEnd to try to prevent any video 15501 // frame overlap. maxBufferHole should be >> lastFrameDuration anyway. 15502 mp4SampleDuration = deltaToFrameEnd - lastFrameDuration; 15503 if (mp4SampleDuration < 0) { 15504 mp4SampleDuration = lastFrameDuration; 15505 } else { 15506 stretchedLastFrame = true; 15507 } 15508 this.log(`It is approximately ${deltaToFrameEnd / 90} ms to the next segment; using duration ${mp4SampleDuration / 90} ms for the last video frame.`); 15509 } else { 15510 mp4SampleDuration = lastFrameDuration; 15511 } 15512 } else { 15513 mp4SampleDuration = lastFrameDuration; 15514 } 15515 } 15516 const compositionTimeOffset = Math.round(VideoSample.pts - VideoSample.dts); 15517 minDtsDelta = Math.min(minDtsDelta, mp4SampleDuration); 15518 maxDtsDelta = Math.max(maxDtsDelta, mp4SampleDuration); 15519 minPtsDelta = Math.min(minPtsDelta, ptsDelta); 15520 maxPtsDelta = Math.max(maxPtsDelta, ptsDelta); 15521 outputSamples.push(createMp4Sample(VideoSample.key, mp4SampleDuration, mp4SampleLength, compositionTimeOffset)); 15522 } 15523 if (outputSamples.length) { 15524 if (chromeVersion) { 15525 if (chromeVersion < 70) { 15526 // Chrome workaround, mark first sample as being a Random Access Point (keyframe) to avoid sourcebuffer append issue 15527 // https://code.google.com/p/chromium/issues/detail?id=229412 15528 const flags = outputSamples[0].flags; 15529 flags.dependsOn = 2; 15530 flags.isNonSync = 0; 15531 } 15532 } else if (safariWebkitVersion) { 15533 // Fix for "CNN special report, with CC" in test-streams (Safari browser only) 15534 // Ignore DTS when frame durations are irregular. Safari MSE does not handle this leading to gaps. 15535 if (maxPtsDelta - minPtsDelta < maxDtsDelta - minDtsDelta && averageSampleDuration / maxDtsDelta < 0.025 && outputSamples[0].cts === 0) { 15536 this.warn('Found irregular gaps in sample duration. Using PTS instead of DTS to determine MP4 sample duration.'); 15537 let dts = firstDTS; 15538 for (let i = 0, len = outputSamples.length; i < len; i++) { 15539 const nextDts = dts + outputSamples[i].duration; 15540 const pts = dts + outputSamples[i].cts; 15541 if (i < len - 1) { 15542 const nextPts = nextDts + outputSamples[i + 1].cts; 15543 outputSamples[i].duration = nextPts - pts; 15544 } else { 15545 outputSamples[i].duration = i ? outputSamples[i - 1].duration : averageSampleDuration; 15546 } 15547 outputSamples[i].cts = 0; 15548 dts = nextDts; 15549 } 15550 } 15551 } 15552 } 15553 // next AVC/HEVC sample DTS should be equal to last sample DTS + last sample duration (in PES timescale) 15554 mp4SampleDuration = stretchedLastFrame || !mp4SampleDuration ? averageSampleDuration : mp4SampleDuration; 15555 const endDTS = lastDTS + mp4SampleDuration; 15556 this.nextVideoTs = nextVideoTs = endDTS - initTime; 15557 this.videoSampleDuration = mp4SampleDuration; 15558 this.isVideoContiguous = true; 15559 const moof = MP4.moof(track.sequenceNumber++, firstDTS, _extends(track, { 15560 samples: outputSamples 15561 })); 15562 const type = 'video'; 15563 const data = { 15564 data1: moof, 15565 data2: mdat, 15566 startPTS: (minPTS - initTime) / timeScale, 15567 endPTS: (maxPTS + mp4SampleDuration - initTime) / timeScale, 15568 startDTS: (firstDTS - initTime) / timeScale, 15569 endDTS: nextVideoTs / timeScale, 15570 type, 15571 hasAudio: false, 15572 hasVideo: true, 15573 nb: outputSamples.length, 15574 dropped: track.dropped 15575 }; 15576 track.samples = []; 15577 track.dropped = 0; 15578 return data; 15579 } 15580 getSamplesPerFrame(track) { 15581 switch (track.segmentCodec) { 15582 case 'mp3': 15583 return MPEG_AUDIO_SAMPLE_PER_FRAME; 15584 case 'ac3': 15585 return AC3_SAMPLES_PER_FRAME; 15586 default: 15587 return AAC_SAMPLES_PER_FRAME; 15588 } 15589 } 15590 remuxAudio(track, timeOffset, contiguous, accurateTimeOffset, videoTimeOffset) { 15591 const inputTimeScale = track.inputTimeScale; 15592 const mp4timeScale = track.samplerate ? track.samplerate : inputTimeScale; 15593 const scaleFactor = inputTimeScale / mp4timeScale; 15594 const mp4SampleDuration = this.getSamplesPerFrame(track); 15595 const inputSampleDuration = mp4SampleDuration * scaleFactor; 15596 const initPTS = this._initPTS; 15597 const rawMPEG = track.segmentCodec === 'mp3' && this.typeSupported.mpeg; 15598 const outputSamples = []; 15599 const alignedWithVideo = videoTimeOffset !== undefined; 15600 let inputSamples = track.samples; 15601 let offset = rawMPEG ? 0 : 8; 15602 let nextAudioTs = this.nextAudioTs || -1; 15603 15604 // window.audioSamples ? window.audioSamples.push(inputSamples.map(s => s.pts)) : (window.audioSamples = [inputSamples.map(s => s.pts)]); 15605 15606 // for audio samples, also consider consecutive fragments as being contiguous (even if a level switch occurs), 15607 // for sake of clarity: 15608 // consecutive fragments are frags with 15609 // - less than 100ms gaps between new time offset (if accurate) and next expected PTS OR 15610 // - less than 20 audio frames distance 15611 // contiguous fragments are consecutive fragments from same quality level (same level, new SN = old SN + 1) 15612 // this helps ensuring audio continuity 15613 // and this also avoids audio glitches/cut when switching quality, or reporting wrong duration on first audio frame 15614 const initTime = initPTS.baseTime * inputTimeScale / initPTS.timescale; 15615 const timeOffsetMpegTS = initTime + timeOffset * inputTimeScale; 15616 this.isAudioContiguous = contiguous = contiguous || inputSamples.length && nextAudioTs > 0 && (accurateTimeOffset && Math.abs(timeOffsetMpegTS - (nextAudioTs + initTime)) < 9000 || Math.abs(normalizePts(inputSamples[0].pts, timeOffsetMpegTS) - (nextAudioTs + initTime)) < 20 * inputSampleDuration); 15617 15618 // compute normalized PTS 15619 inputSamples.forEach(function (sample) { 15620 sample.pts = normalizePts(sample.pts, timeOffsetMpegTS); 15621 }); 15622 if (!contiguous || nextAudioTs < 0) { 15623 const sampleCount = inputSamples.length; 15624 // filter out sample with negative PTS that are not playable anyway 15625 // if we don't remove these negative samples, they will shift all audio samples forward. 15626 // leading to audio overlap between current / next fragment 15627 inputSamples = inputSamples.filter(sample => sample.pts >= 0); 15628 if (sampleCount !== inputSamples.length) { 15629 this.warn(`Removed ${inputSamples.length - sampleCount} of ${sampleCount} samples (initPTS ${initTime} / ${inputTimeScale})`); 15630 } 15631 15632 // in case all samples have negative PTS, and have been filtered out, return now 15633 if (!inputSamples.length) { 15634 return; 15635 } 15636 if (videoTimeOffset === 0) { 15637 // Set the start to match video so that start gaps larger than inputSampleDuration are filled with silence 15638 nextAudioTs = 0; 15639 } else if (accurateTimeOffset && !alignedWithVideo) { 15640 // When not seeking, not live, and LevelDetails.PTSKnown, use fragment start as predicted next audio PTS 15641 nextAudioTs = Math.max(0, timeOffsetMpegTS - initTime); 15642 } else { 15643 // if frags are not contiguous and if we cant trust time offset, let's use first sample PTS as next audio PTS 15644 nextAudioTs = inputSamples[0].pts - initTime; 15645 } 15646 } 15647 15648 // If the audio track is missing samples, the frames seem to get "left-shifted" within the 15649 // resulting mp4 segment, causing sync issues and leaving gaps at the end of the audio segment. 15650 // In an effort to prevent this from happening, we inject frames here where there are gaps. 15651 // When possible, we inject a silent frame; when that's not possible, we duplicate the last 15652 // frame. 15653 15654 if (track.segmentCodec === 'aac') { 15655 const maxAudioFramesDrift = this.config.maxAudioFramesDrift; 15656 for (let i = 0, nextPts = nextAudioTs + initTime; i < inputSamples.length; i++) { 15657 // First, let's see how far off this frame is from where we expect it to be 15658 const sample = inputSamples[i]; 15659 const pts = sample.pts; 15660 const delta = pts - nextPts; 15661 const duration = Math.abs(1000 * delta / inputTimeScale); 15662 15663 // When remuxing with video, if we're overlapping by more than a duration, drop this sample to stay in sync 15664 if (delta <= -maxAudioFramesDrift * inputSampleDuration && alignedWithVideo) { 15665 if (i === 0) { 15666 this.warn(`Audio frame @ ${(pts / inputTimeScale).toFixed(3)}s overlaps marker by ${Math.round(1000 * delta / inputTimeScale)} ms.`); 15667 this.nextAudioTs = nextAudioTs = pts - initTime; 15668 nextPts = pts; 15669 } 15670 } // eslint-disable-line brace-style 15671 15672 // Insert missing frames if: 15673 // 1: We're more than maxAudioFramesDrift frame away 15674 // 2: Not more than MAX_SILENT_FRAME_DURATION away 15675 // 3: currentTime (aka nextPtsNorm) is not 0 15676 // 4: remuxing with video (videoTimeOffset !== undefined) 15677 else if (delta >= maxAudioFramesDrift * inputSampleDuration && duration < MAX_SILENT_FRAME_DURATION && alignedWithVideo) { 15678 let missing = Math.round(delta / inputSampleDuration); 15679 // Adjust nextPts so that silent samples are aligned with media pts. This will prevent media samples from 15680 // later being shifted if nextPts is based on timeOffset and delta is not a multiple of inputSampleDuration. 15681 nextPts = pts - missing * inputSampleDuration; 15682 while (nextPts < 0 && missing && inputSampleDuration) { 15683 missing--; 15684 nextPts += inputSampleDuration; 15685 } 15686 if (i === 0) { 15687 this.nextAudioTs = nextAudioTs = nextPts - initTime; 15688 } 15689 this.warn(`Injecting ${missing} audio frames @ ${((nextPts - initTime) / inputTimeScale).toFixed(3)}s due to ${Math.round(1000 * delta / inputTimeScale)} ms gap.`); 15690 for (let j = 0; j < missing; j++) { 15691 let fillFrame = AAC.getSilentFrame(track.parsedCodec || track.manifestCodec || track.codec, track.channelCount); 15692 if (!fillFrame) { 15693 this.log('Unable to get silent frame for given audio codec; duplicating last frame instead.'); 15694 fillFrame = sample.unit.subarray(); 15695 } 15696 inputSamples.splice(i, 0, { 15697 unit: fillFrame, 15698 pts: nextPts 15699 }); 15700 nextPts += inputSampleDuration; 15701 i++; 15702 } 15703 } 15704 sample.pts = nextPts; 15705 nextPts += inputSampleDuration; 15706 } 15707 } 15708 let firstPTS = null; 15709 let lastPTS = null; 15710 let mdat; 15711 let mdatSize = 0; 15712 let sampleLength = inputSamples.length; 15713 while (sampleLength--) { 15714 mdatSize += inputSamples[sampleLength].unit.byteLength; 15715 } 15716 for (let j = 0, _nbSamples = inputSamples.length; j < _nbSamples; j++) { 15717 const audioSample = inputSamples[j]; 15718 const unit = audioSample.unit; 15719 let pts = audioSample.pts; 15720 if (lastPTS !== null) { 15721 // If we have more than one sample, set the duration of the sample to the "real" duration; the PTS diff with 15722 // the previous sample 15723 const prevSample = outputSamples[j - 1]; 15724 prevSample.duration = Math.round((pts - lastPTS) / scaleFactor); 15725 } else { 15726 if (contiguous && track.segmentCodec === 'aac') { 15727 // set PTS/DTS to expected PTS/DTS 15728 pts = nextAudioTs + initTime; 15729 } 15730 // remember first PTS of our audioSamples 15731 firstPTS = pts; 15732 if (mdatSize > 0) { 15733 /* concatenate the audio data and construct the mdat in place 15734 (need 8 more bytes to fill length and mdat type) */ 15735 mdatSize += offset; 15736 try { 15737 mdat = new Uint8Array(mdatSize); 15738 } catch (err) { 15739 this.observer.emit(Events.ERROR, Events.ERROR, { 15740 type: ErrorTypes.MUX_ERROR, 15741 details: ErrorDetails.REMUX_ALLOC_ERROR, 15742 fatal: false, 15743 error: err, 15744 bytes: mdatSize, 15745 reason: `fail allocating audio mdat ${mdatSize}` 15746 }); 15747 return; 15748 } 15749 if (!rawMPEG) { 15750 const view = new DataView(mdat.buffer); 15751 view.setUint32(0, mdatSize); 15752 mdat.set(MP4.types.mdat, 4); 15753 } 15754 } else { 15755 // no audio samples 15756 return; 15757 } 15758 } 15759 mdat.set(unit, offset); 15760 const unitLen = unit.byteLength; 15761 offset += unitLen; 15762 // Default the sample's duration to the computed mp4SampleDuration, which will either be 1024 for AAC or 1152 for MPEG 15763 // In the case that we have 1 sample, this will be the duration. If we have more than one sample, the duration 15764 // becomes the PTS diff with the previous sample 15765 outputSamples.push(createMp4Sample(true, mp4SampleDuration, unitLen, 0)); 15766 lastPTS = pts; 15767 } 15768 15769 // We could end up with no audio samples if all input samples were overlapping with the previously remuxed ones 15770 const nbSamples = outputSamples.length; 15771 if (!nbSamples) { 15772 return; 15773 } 15774 15775 // The next audio sample PTS should be equal to last sample PTS + duration 15776 const lastSample = outputSamples[outputSamples.length - 1]; 15777 nextAudioTs = lastPTS - initTime; 15778 this.nextAudioTs = nextAudioTs + scaleFactor * lastSample.duration; 15779 15780 // Set the track samples from inputSamples to outputSamples before remuxing 15781 const moof = rawMPEG ? new Uint8Array(0) : MP4.moof(track.sequenceNumber++, firstPTS / scaleFactor, _extends({}, track, { 15782 samples: outputSamples 15783 })); 15784 15785 // Clear the track samples. This also clears the samples array in the demuxer, since the reference is shared 15786 track.samples = []; 15787 const start = (firstPTS - initTime) / inputTimeScale; 15788 const end = this.nextAudioTs / inputTimeScale; 15789 const type = 'audio'; 15790 const audioData = { 15791 data1: moof, 15792 data2: mdat, 15793 startPTS: start, 15794 endPTS: end, 15795 startDTS: start, 15796 endDTS: end, 15797 type, 15798 hasAudio: true, 15799 hasVideo: false, 15800 nb: nbSamples 15801 }; 15802 this.isAudioContiguous = true; 15803 return audioData; 15804 } 15805} 15806function normalizePts(value, reference) { 15807 let offset; 15808 if (reference === null) { 15809 return value; 15810 } 15811 if (reference < value) { 15812 // - 2^33 15813 offset = -8589934592; 15814 } else { 15815 // + 2^33 15816 offset = 8589934592; 15817 } 15818 /* PTS is 33bit (from 0 to 2^33 -1) 15819 if diff between value and reference is bigger than half of the amplitude (2^32) then it means that 15820 PTS looping occured. fill the gap */ 15821 while (Math.abs(value - reference) > 4294967296) { 15822 value += offset; 15823 } 15824 return value; 15825} 15826function findKeyframeIndex(samples) { 15827 for (let i = 0; i < samples.length; i++) { 15828 if (samples[i].key) { 15829 return i; 15830 } 15831 } 15832 return -1; 15833} 15834function flushTextTrackMetadataCueSamples(track, timeOffset, initPTS, initDTS) { 15835 const length = track.samples.length; 15836 if (!length) { 15837 return; 15838 } 15839 const inputTimeScale = track.inputTimeScale; 15840 for (let index = 0; index < length; index++) { 15841 const sample = track.samples[index]; 15842 // setting id3 pts, dts to relative time 15843 // using this._initPTS and this._initDTS to calculate relative time 15844 sample.pts = normalizePts(sample.pts - initPTS.baseTime * inputTimeScale / initPTS.timescale, timeOffset * inputTimeScale) / inputTimeScale; 15845 sample.dts = normalizePts(sample.dts - initDTS.baseTime * inputTimeScale / initDTS.timescale, timeOffset * inputTimeScale) / inputTimeScale; 15846 } 15847 const samples = track.samples; 15848 track.samples = []; 15849 return { 15850 samples 15851 }; 15852} 15853function flushTextTrackUserdataCueSamples(track, timeOffset, initPTS) { 15854 const length = track.samples.length; 15855 if (!length) { 15856 return; 15857 } 15858 const inputTimeScale = track.inputTimeScale; 15859 for (let index = 0; index < length; index++) { 15860 const sample = track.samples[index]; 15861 // setting text pts, dts to relative time 15862 // using this._initPTS and this._initDTS to calculate relative time 15863 sample.pts = normalizePts(sample.pts - initPTS.baseTime * inputTimeScale / initPTS.timescale, timeOffset * inputTimeScale) / inputTimeScale; 15864 } 15865 track.samples.sort((a, b) => a.pts - b.pts); 15866 const samples = track.samples; 15867 track.samples = []; 15868 return { 15869 samples 15870 }; 15871} 15872 15873class PassThroughRemuxer extends Logger { 15874 constructor(observer, config, typeSupported, logger) { 15875 super('passthrough-remuxer', logger); 15876 this.emitInitSegment = false; 15877 this.audioCodec = void 0; 15878 this.videoCodec = void 0; 15879 this.initData = void 0; 15880 this.initPTS = null; 15881 this.initTracks = void 0; 15882 this.lastEndTime = null; 15883 this.isVideoContiguous = false; 15884 } 15885 destroy() {} 15886 resetTimeStamp(defaultInitPTS) { 15887 this.lastEndTime = null; 15888 const initPTS = this.initPTS; 15889 if (initPTS && defaultInitPTS) { 15890 if (initPTS.baseTime === defaultInitPTS.baseTime && initPTS.timescale === defaultInitPTS.timescale) { 15891 return; 15892 } 15893 } 15894 this.initPTS = defaultInitPTS; 15895 } 15896 resetNextTimestamp() { 15897 this.isVideoContiguous = false; 15898 this.lastEndTime = null; 15899 } 15900 resetInitSegment(initSegment, audioCodec, videoCodec, decryptdata) { 15901 this.audioCodec = audioCodec; 15902 this.videoCodec = videoCodec; 15903 this.generateInitSegment(initSegment, decryptdata); 15904 this.emitInitSegment = true; 15905 } 15906 generateInitSegment(initSegment, decryptdata) { 15907 let { 15908 audioCodec, 15909 videoCodec 15910 } = this; 15911 if (!(initSegment != null && initSegment.byteLength)) { 15912 this.initTracks = undefined; 15913 this.initData = undefined; 15914 return; 15915 } 15916 const { 15917 audio, 15918 video 15919 } = this.initData = parseInitSegment(initSegment); 15920 if (decryptdata) { 15921 patchEncyptionData(initSegment, decryptdata); 15922 } else { 15923 const eitherTrack = audio || video; 15924 if (eitherTrack != null && eitherTrack.encrypted) { 15925 this.warn(`Init segment with encrypted track with has no key ("${eitherTrack.codec}")!`); 15926 } 15927 } 15928 15929 // Get codec from initSegment 15930 if (audio) { 15931 audioCodec = getParsedTrackCodec(audio, ElementaryStreamTypes.AUDIO, this); 15932 } 15933 if (video) { 15934 videoCodec = getParsedTrackCodec(video, ElementaryStreamTypes.VIDEO, this); 15935 } 15936 const tracks = {}; 15937 if (audio && video) { 15938 tracks.audiovideo = { 15939 container: 'video/mp4', 15940 codec: audioCodec + ',' + videoCodec, 15941 supplemental: video.supplemental, 15942 encrypted: video.encrypted, 15943 initSegment, 15944 id: 'main' 15945 }; 15946 } else if (audio) { 15947 tracks.audio = { 15948 container: 'audio/mp4', 15949 codec: audioCodec, 15950 encrypted: audio.encrypted, 15951 initSegment, 15952 id: 'audio' 15953 }; 15954 } else if (video) { 15955 tracks.video = { 15956 container: 'video/mp4', 15957 codec: videoCodec, 15958 supplemental: video.supplemental, 15959 encrypted: video.encrypted, 15960 initSegment, 15961 id: 'main' 15962 }; 15963 } else { 15964 this.warn('initSegment does not contain moov or trak boxes.'); 15965 } 15966 this.initTracks = tracks; 15967 } 15968 remux(audioTrack, videoTrack, id3Track, textTrack, timeOffset, accurateTimeOffset) { 15969 var _initData, _initData2; 15970 let { 15971 initPTS, 15972 lastEndTime 15973 } = this; 15974 const result = { 15975 audio: undefined, 15976 video: undefined, 15977 text: textTrack, 15978 id3: id3Track, 15979 initSegment: undefined 15980 }; 15981 15982 // If we haven't yet set a lastEndDTS, or it was reset, set it to the provided timeOffset. We want to use the 15983 // lastEndDTS over timeOffset whenever possible; during progressive playback, the media source will not update 15984 // the media duration (which is what timeOffset is provided as) before we need to process the next chunk. 15985 if (!isFiniteNumber(lastEndTime)) { 15986 lastEndTime = this.lastEndTime = timeOffset || 0; 15987 } 15988 15989 // The binary segment data is added to the videoTrack in the mp4demuxer. We don't check to see if the data is only 15990 // audio or video (or both); adding it to video was an arbitrary choice. 15991 const data = videoTrack.samples; 15992 if (!data.length) { 15993 return result; 15994 } 15995 const initSegment = { 15996 initPTS: undefined, 15997 timescale: undefined, 15998 trackId: undefined 15999 }; 16000 let initData = this.initData; 16001 if (!((_initData = initData) != null && _initData.length)) { 16002 this.generateInitSegment(data); 16003 initData = this.initData; 16004 } 16005 if (!((_initData2 = initData) != null && _initData2.length)) { 16006 // We can't remux if the initSegment could not be generated 16007 this.warn('Failed to generate initSegment.'); 16008 return result; 16009 } 16010 if (this.emitInitSegment) { 16011 initSegment.tracks = this.initTracks; 16012 this.emitInitSegment = false; 16013 } 16014 const trackSampleData = getSampleData(data, initData, this); 16015 const audioSampleTimestamps = initData.audio ? trackSampleData[initData.audio.id] : null; 16016 const videoSampleTimestamps = initData.video ? trackSampleData[initData.video.id] : null; 16017 const videoStartTime = toStartEndOrDefault(videoSampleTimestamps, Infinity); 16018 const audioStartTime = toStartEndOrDefault(audioSampleTimestamps, Infinity); 16019 const videoEndTime = toStartEndOrDefault(videoSampleTimestamps, 0, true); 16020 const audioEndTime = toStartEndOrDefault(audioSampleTimestamps, 0, true); 16021 let decodeTime = timeOffset; 16022 let duration = 0; 16023 const syncOnAudio = audioSampleTimestamps && (!videoSampleTimestamps || !initPTS && audioStartTime < videoStartTime || initPTS && initPTS.trackId === initData.audio.id); 16024 const baseOffsetSamples = syncOnAudio ? audioSampleTimestamps : videoSampleTimestamps; 16025 if (baseOffsetSamples) { 16026 const timescale = baseOffsetSamples.timescale; 16027 const baseTime = baseOffsetSamples.start - timeOffset * timescale; 16028 const trackId = syncOnAudio ? initData.audio.id : initData.video.id; 16029 decodeTime = baseOffsetSamples.start / timescale; 16030 duration = syncOnAudio ? audioEndTime - audioStartTime : videoEndTime - videoStartTime; 16031 if ((accurateTimeOffset || !initPTS) && (isInvalidInitPts(initPTS, decodeTime, timeOffset, duration) || timescale !== initPTS.timescale)) { 16032 if (initPTS) { 16033 this.warn(`Timestamps at playlist time: ${accurateTimeOffset ? '' : '~'}${timeOffset} ${baseTime / timescale} != initPTS: ${initPTS.baseTime / initPTS.timescale} (${initPTS.baseTime}/${initPTS.timescale}) trackId: ${initPTS.trackId}`); 16034 } 16035 this.log(`Found initPTS at playlist time: ${timeOffset} offset: ${decodeTime - timeOffset} (${baseTime}/${timescale}) trackId: ${trackId}`); 16036 initPTS = null; 16037 initSegment.initPTS = baseTime; 16038 initSegment.timescale = timescale; 16039 initSegment.trackId = trackId; 16040 } 16041 } else { 16042 this.warn(`No audio or video samples found for initPTS at playlist time: ${timeOffset}`); 16043 } 16044 if (!initPTS) { 16045 if (!initSegment.timescale || initSegment.trackId === undefined || initSegment.initPTS === undefined) { 16046 this.warn('Could not set initPTS'); 16047 initSegment.initPTS = decodeTime; 16048 initSegment.timescale = 1; 16049 initSegment.trackId = -1; 16050 } 16051 this.initPTS = initPTS = { 16052 baseTime: initSegment.initPTS, 16053 timescale: initSegment.timescale, 16054 trackId: initSegment.trackId 16055 }; 16056 } else { 16057 initSegment.initPTS = initPTS.baseTime; 16058 initSegment.timescale = initPTS.timescale; 16059 initSegment.trackId = initPTS.trackId; 16060 } 16061 const startTime = decodeTime - initPTS.baseTime / initPTS.timescale; 16062 const endTime = startTime + duration; 16063 if (duration > 0) { 16064 this.lastEndTime = endTime; 16065 } else { 16066 this.warn('Duration parsed from mp4 should be greater than zero'); 16067 this.resetNextTimestamp(); 16068 } 16069 const hasAudio = !!initData.audio; 16070 const hasVideo = !!initData.video; 16071 let type = ''; 16072 if (hasAudio) { 16073 type += 'audio'; 16074 } 16075 if (hasVideo) { 16076 type += 'video'; 16077 } 16078 const encrypted = (initData.audio ? initData.audio.encrypted : false) || (initData.video ? initData.video.encrypted : false); 16079 const track = { 16080 data1: data, 16081 startPTS: startTime, 16082 startDTS: startTime, 16083 endPTS: endTime, 16084 endDTS: endTime, 16085 type, 16086 hasAudio, 16087 hasVideo, 16088 nb: 1, 16089 dropped: 0, 16090 encrypted 16091 }; 16092 result.audio = hasAudio && !hasVideo ? track : undefined; 16093 result.video = hasVideo ? track : undefined; 16094 const videoSampleCount = videoSampleTimestamps == null ? void 0 : videoSampleTimestamps.sampleCount; 16095 if (videoSampleCount) { 16096 const firstKeyFrame = videoSampleTimestamps.keyFrameIndex; 16097 const independent = firstKeyFrame !== -1; 16098 track.nb = videoSampleCount; 16099 track.dropped = firstKeyFrame === 0 || this.isVideoContiguous ? 0 : independent ? firstKeyFrame : videoSampleCount; 16100 track.independent = independent; 16101 track.firstKeyFrame = firstKeyFrame; 16102 if (independent && videoSampleTimestamps.keyFrameStart) { 16103 track.firstKeyFramePTS = (videoSampleTimestamps.keyFrameStart - initPTS.baseTime) / initPTS.timescale; 16104 } 16105 if (!this.isVideoContiguous) { 16106 result.independent = independent; 16107 } 16108 this.isVideoContiguous || (this.isVideoContiguous = independent); 16109 if (track.dropped) { 16110 this.warn(`fmp4 does not start with IDR: firstIDR ${firstKeyFrame}/${videoSampleCount} dropped: ${track.dropped} start: ${track.firstKeyFramePTS || 'NA'}`); 16111 } 16112 } 16113 result.initSegment = initSegment; 16114 result.id3 = flushTextTrackMetadataCueSamples(id3Track, timeOffset, initPTS, initPTS); 16115 if (textTrack.samples.length) { 16116 result.text = flushTextTrackUserdataCueSamples(textTrack, timeOffset, initPTS); 16117 } 16118 return result; 16119 } 16120} 16121function toStartEndOrDefault(trackTimes, defaultValue, end = false) { 16122 return (trackTimes == null ? void 0 : trackTimes.start) !== undefined ? (trackTimes.start + (end ? trackTimes.duration : 0)) / trackTimes.timescale : defaultValue; 16123} 16124function isInvalidInitPts(initPTS, startDTS, timeOffset, duration) { 16125 if (initPTS === null) { 16126 return true; 16127 } 16128 // InitPTS is invalid when distance from program would be more than segment duration or a minimum of one second 16129 const minDuration = Math.max(duration, 1); 16130 const startTime = startDTS - initPTS.baseTime / initPTS.timescale; 16131 return Math.abs(startTime - timeOffset) > minDuration; 16132} 16133function getParsedTrackCodec(track, type, logger) { 16134 const parsedCodec = track.codec; 16135 if (parsedCodec && parsedCodec.length > 4) { 16136 return parsedCodec; 16137 } 16138 if (type === ElementaryStreamTypes.AUDIO) { 16139 if (parsedCodec === 'ec-3' || parsedCodec === 'ac-3' || parsedCodec === 'alac') { 16140 return parsedCodec; 16141 } 16142 if (parsedCodec === 'fLaC' || parsedCodec === 'Opus') { 16143 // Opting not to get `preferManagedMediaSource` from player config for isSupported() check for simplicity 16144 const preferManagedMediaSource = false; 16145 return getCodecCompatibleName(parsedCodec, preferManagedMediaSource); 16146 } 16147 logger.warn(`Unhandled audio codec "${parsedCodec}" in mp4 MAP`); 16148 return parsedCodec || 'mp4a'; 16149 } 16150 // Provide defaults based on codec type 16151 // This allows for some playback of some fmp4 playlists without CODECS defined in manifest 16152 logger.warn(`Unhandled video codec "${parsedCodec}" in mp4 MAP`); 16153 return parsedCodec || 'avc1'; 16154} 16155 16156let now; 16157// performance.now() not available on WebWorker, at least on Safari Desktop 16158try { 16159 now = self.performance.now.bind(self.performance); 16160} catch (err) { 16161 now = Date.now; 16162} 16163const muxConfig = [{ 16164 demux: MP4Demuxer, 16165 remux: PassThroughRemuxer 16166}, { 16167 demux: TSDemuxer, 16168 remux: MP4Remuxer 16169}, { 16170 demux: AACDemuxer, 16171 remux: MP4Remuxer 16172}, { 16173 demux: MP3Demuxer, 16174 remux: MP4Remuxer 16175}]; 16176{ 16177 muxConfig.splice(2, 0, { 16178 demux: AC3Demuxer, 16179 remux: MP4Remuxer 16180 }); 16181} 16182class Transmuxer { 16183 constructor(observer, typeSupported, config, vendor, id, logger) { 16184 this.asyncResult = false; 16185 this.logger = void 0; 16186 this.observer = void 0; 16187 this.typeSupported = void 0; 16188 this.config = void 0; 16189 this.id = void 0; 16190 this.demuxer = void 0; 16191 this.remuxer = void 0; 16192 this.decrypter = void 0; 16193 this.probe = void 0; 16194 this.decryptionPromise = null; 16195 this.transmuxConfig = void 0; 16196 this.currentTransmuxState = void 0; 16197 this.observer = observer; 16198 this.typeSupported = typeSupported; 16199 this.config = config; 16200 this.id = id; 16201 this.logger = logger; 16202 } 16203 configure(transmuxConfig) { 16204 this.transmuxConfig = transmuxConfig; 16205 if (this.decrypter) { 16206 this.decrypter.reset(); 16207 } 16208 } 16209 push(data, decryptdata, chunkMeta, state) { 16210 const stats = chunkMeta.transmuxing; 16211 stats.executeStart = now(); 16212 let uintData = new Uint8Array(data); 16213 const { 16214 currentTransmuxState, 16215 transmuxConfig 16216 } = this; 16217 if (state) { 16218 this.currentTransmuxState = state; 16219 } 16220 const { 16221 contiguous, 16222 discontinuity, 16223 trackSwitch, 16224 accurateTimeOffset, 16225 timeOffset, 16226 initSegmentChange 16227 } = state || currentTransmuxState; 16228 const { 16229 audioCodec, 16230 videoCodec, 16231 defaultInitPts, 16232 duration, 16233 initSegmentData 16234 } = transmuxConfig; 16235 const keyData = getEncryptionType(uintData, decryptdata); 16236 if (keyData && isFullSegmentEncryption(keyData.method)) { 16237 const decrypter = this.getDecrypter(); 16238 const aesMode = getAesModeFromFullSegmentMethod(keyData.method); 16239 16240 // Software decryption is synchronous; webCrypto is not 16241 if (decrypter.isSync()) { 16242 // Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached 16243 // data is handled in the flush() call 16244 let decryptedData = decrypter.softwareDecrypt(uintData, keyData.key.buffer, keyData.iv.buffer, aesMode); 16245 // For Low-Latency HLS Parts, decrypt in place, since part parsing is expected on push progress 16246 const loadingParts = chunkMeta.part > -1; 16247 if (loadingParts) { 16248 const _data = decrypter.flush(); 16249 decryptedData = _data ? _data.buffer : _data; 16250 } 16251 if (!decryptedData) { 16252 stats.executeEnd = now(); 16253 return emptyResult(chunkMeta); 16254 } 16255 uintData = new Uint8Array(decryptedData); 16256 } else { 16257 this.asyncResult = true; 16258 this.decryptionPromise = decrypter.webCryptoDecrypt(uintData, keyData.key.buffer, keyData.iv.buffer, aesMode).then(decryptedData => { 16259 // Calling push here is important; if flush() is called while this is still resolving, this ensures that 16260 // the decrypted data has been transmuxed 16261 const result = this.push(decryptedData, null, chunkMeta); 16262 this.decryptionPromise = null; 16263 return result; 16264 }); 16265 return this.decryptionPromise; 16266 } 16267 } 16268 const resetMuxers = this.needsProbing(discontinuity, trackSwitch); 16269 if (resetMuxers) { 16270 const error = this.configureTransmuxer(uintData); 16271 if (error) { 16272 this.logger.warn(`[transmuxer] ${error.message}`); 16273 this.observer.emit(Events.ERROR, Events.ERROR, { 16274 type: ErrorTypes.MEDIA_ERROR, 16275 details: ErrorDetails.FRAG_PARSING_ERROR, 16276 fatal: false, 16277 error, 16278 reason: error.message 16279 }); 16280 stats.executeEnd = now(); 16281 return emptyResult(chunkMeta); 16282 } 16283 } 16284 if (discontinuity || trackSwitch || initSegmentChange || resetMuxers) { 16285 this.resetInitSegment(initSegmentData, audioCodec, videoCodec, duration, decryptdata); 16286 } 16287 if (discontinuity || initSegmentChange || resetMuxers) { 16288 this.resetInitialTimestamp(defaultInitPts); 16289 } 16290 if (!contiguous) { 16291 this.resetContiguity(); 16292 } 16293 const result = this.transmux(uintData, keyData, timeOffset, accurateTimeOffset, chunkMeta); 16294 this.asyncResult = isPromise(result); 16295 const currentState = this.currentTransmuxState; 16296 currentState.contiguous = true; 16297 currentState.discontinuity = false; 16298 currentState.trackSwitch = false; 16299 stats.executeEnd = now(); 16300 return result; 16301 } 16302 16303 // Due to data caching, flush calls can produce more than one TransmuxerResult (hence the Array type) 16304 flush(chunkMeta) { 16305 const stats = chunkMeta.transmuxing; 16306 stats.executeStart = now(); 16307 const { 16308 decrypter, 16309 currentTransmuxState, 16310 decryptionPromise 16311 } = this; 16312 if (decryptionPromise) { 16313 this.asyncResult = true; 16314 // Upon resolution, the decryption promise calls push() and returns its TransmuxerResult up the stack. Therefore 16315 // only flushing is required for async decryption 16316 return decryptionPromise.then(() => { 16317 return this.flush(chunkMeta); 16318 }); 16319 } 16320 const transmuxResults = []; 16321 const { 16322 timeOffset 16323 } = currentTransmuxState; 16324 if (decrypter) { 16325 // The decrypter may have data cached, which needs to be demuxed. In this case we'll have two TransmuxResults 16326 // This happens in the case that we receive only 1 push call for a segment (either for non-progressive downloads, 16327 // or for progressive downloads with small segments) 16328 const decryptedData = decrypter.flush(); 16329 if (decryptedData) { 16330 // Push always returns a TransmuxerResult if decryptdata is null 16331 transmuxResults.push(this.push(decryptedData.buffer, null, chunkMeta)); 16332 } 16333 } 16334 const { 16335 demuxer, 16336 remuxer 16337 } = this; 16338 if (!demuxer || !remuxer) { 16339 // If probing failed, then Hls.js has been given content its not able to handle 16340 stats.executeEnd = now(); 16341 const emptyResults = [emptyResult(chunkMeta)]; 16342 if (this.asyncResult) { 16343 return Promise.resolve(emptyResults); 16344 } 16345 return emptyResults; 16346 } 16347 const demuxResultOrPromise = demuxer.flush(timeOffset); 16348 if (isPromise(demuxResultOrPromise)) { 16349 this.asyncResult = true; 16350 // Decrypt final SAMPLE-AES samples 16351 return demuxResultOrPromise.then(demuxResult => { 16352 this.flushRemux(transmuxResults, demuxResult, chunkMeta); 16353 return transmuxResults; 16354 }); 16355 } 16356 this.flushRemux(transmuxResults, demuxResultOrPromise, chunkMeta); 16357 if (this.asyncResult) { 16358 return Promise.resolve(transmuxResults); 16359 } 16360 return transmuxResults; 16361 } 16362 flushRemux(transmuxResults, demuxResult, chunkMeta) { 16363 const { 16364 audioTrack, 16365 videoTrack, 16366 id3Track, 16367 textTrack 16368 } = demuxResult; 16369 const { 16370 accurateTimeOffset, 16371 timeOffset 16372 } = this.currentTransmuxState; 16373 this.logger.log(`[transmuxer.ts]: Flushed ${this.id} sn: ${chunkMeta.sn}${chunkMeta.part > -1 ? ' part: ' + chunkMeta.part : ''} of ${this.id === PlaylistLevelType.MAIN ? 'level' : 'track'} ${chunkMeta.level}`); 16374 const remuxResult = this.remuxer.remux(audioTrack, videoTrack, id3Track, textTrack, timeOffset, accurateTimeOffset, true, this.id); 16375 transmuxResults.push({ 16376 remuxResult, 16377 chunkMeta 16378 }); 16379 chunkMeta.transmuxing.executeEnd = now(); 16380 } 16381 resetInitialTimestamp(defaultInitPts) { 16382 const { 16383 demuxer, 16384 remuxer 16385 } = this; 16386 if (!demuxer || !remuxer) { 16387 return; 16388 } 16389 demuxer.resetTimeStamp(defaultInitPts); 16390 remuxer.resetTimeStamp(defaultInitPts); 16391 } 16392 resetContiguity() { 16393 const { 16394 demuxer, 16395 remuxer 16396 } = this; 16397 if (!demuxer || !remuxer) { 16398 return; 16399 } 16400 demuxer.resetContiguity(); 16401 remuxer.resetNextTimestamp(); 16402 } 16403 resetInitSegment(initSegmentData, audioCodec, videoCodec, trackDuration, decryptdata) { 16404 const { 16405 demuxer, 16406 remuxer 16407 } = this; 16408 if (!demuxer || !remuxer) { 16409 return; 16410 } 16411 demuxer.resetInitSegment(initSegmentData, audioCodec, videoCodec, trackDuration); 16412 remuxer.resetInitSegment(initSegmentData, audioCodec, videoCodec, decryptdata); 16413 } 16414 destroy() { 16415 if (this.demuxer) { 16416 this.demuxer.destroy(); 16417 this.demuxer = undefined; 16418 } 16419 if (this.remuxer) { 16420 this.remuxer.destroy(); 16421 this.remuxer = undefined; 16422 } 16423 } 16424 transmux(data, keyData, timeOffset, accurateTimeOffset, chunkMeta) { 16425 let result; 16426 if (keyData && keyData.method === 'SAMPLE-AES') { 16427 result = this.transmuxSampleAes(data, keyData, timeOffset, accurateTimeOffset, chunkMeta); 16428 } else { 16429 result = this.transmuxUnencrypted(data, timeOffset, accurateTimeOffset, chunkMeta); 16430 } 16431 return result; 16432 } 16433 transmuxUnencrypted(data, timeOffset, accurateTimeOffset, chunkMeta) { 16434 const { 16435 audioTrack, 16436 videoTrack, 16437 id3Track, 16438 textTrack 16439 } = this.demuxer.demux(data, timeOffset, false, !this.config.progressive); 16440 const remuxResult = this.remuxer.remux(audioTrack, videoTrack, id3Track, textTrack, timeOffset, accurateTimeOffset, false, this.id); 16441 return { 16442 remuxResult, 16443 chunkMeta 16444 }; 16445 } 16446 transmuxSampleAes(data, decryptData, timeOffset, accurateTimeOffset, chunkMeta) { 16447 return this.demuxer.demuxSampleAes(data, decryptData, timeOffset).then(demuxResult => { 16448 const remuxResult = this.remuxer.remux(demuxResult.audioTrack, demuxResult.videoTrack, demuxResult.id3Track, demuxResult.textTrack, timeOffset, accurateTimeOffset, false, this.id); 16449 return { 16450 remuxResult, 16451 chunkMeta 16452 }; 16453 }); 16454 } 16455 configureTransmuxer(data) { 16456 const { 16457 config, 16458 observer, 16459 typeSupported 16460 } = this; 16461 // probe for content type 16462 let mux; 16463 for (let i = 0, len = muxConfig.length; i < len; i++) { 16464 var _muxConfig$i$demux; 16465 if ((_muxConfig$i$demux = muxConfig[i].demux) != null && _muxConfig$i$demux.probe(data, this.logger)) { 16466 mux = muxConfig[i]; 16467 break; 16468 } 16469 } 16470 if (!mux) { 16471 return new Error('Failed to find demuxer by probing fragment data'); 16472 } 16473 // so let's check that current remuxer and demuxer are still valid 16474 const demuxer = this.demuxer; 16475 const remuxer = this.remuxer; 16476 const Remuxer = mux.remux; 16477 const Demuxer = mux.demux; 16478 if (!remuxer || !(remuxer instanceof Remuxer)) { 16479 this.remuxer = new Remuxer(observer, config, typeSupported, this.logger); 16480 } 16481 if (!demuxer || !(demuxer instanceof Demuxer)) { 16482 this.demuxer = new Demuxer(observer, config, typeSupported, this.logger); 16483 this.probe = Demuxer.probe; 16484 } 16485 } 16486 needsProbing(discontinuity, trackSwitch) { 16487 // in case of continuity change, or track switch 16488 // we might switch from content type (AAC container to TS container, or TS to fmp4 for example) 16489 return !this.demuxer || !this.remuxer || discontinuity || trackSwitch; 16490 } 16491 getDecrypter() { 16492 let decrypter = this.decrypter; 16493 if (!decrypter) { 16494 decrypter = this.decrypter = new Decrypter(this.config); 16495 } 16496 return decrypter; 16497 } 16498} 16499function getEncryptionType(data, decryptData) { 16500 let encryptionType = null; 16501 if (data.byteLength > 0 && (decryptData == null ? void 0 : decryptData.key) != null && decryptData.iv !== null && decryptData.method != null) { 16502 encryptionType = decryptData; 16503 } 16504 return encryptionType; 16505} 16506const emptyResult = chunkMeta => ({ 16507 remuxResult: {}, 16508 chunkMeta 16509}); 16510function isPromise(p) { 16511 return 'then' in p && p.then instanceof Function; 16512} 16513class TransmuxConfig { 16514 constructor(audioCodec, videoCodec, initSegmentData, duration, defaultInitPts) { 16515 this.audioCodec = void 0; 16516 this.videoCodec = void 0; 16517 this.initSegmentData = void 0; 16518 this.duration = void 0; 16519 this.defaultInitPts = void 0; 16520 this.audioCodec = audioCodec; 16521 this.videoCodec = videoCodec; 16522 this.initSegmentData = initSegmentData; 16523 this.duration = duration; 16524 this.defaultInitPts = defaultInitPts || null; 16525 } 16526} 16527class TransmuxState { 16528 constructor(discontinuity, contiguous, accurateTimeOffset, trackSwitch, timeOffset, initSegmentChange) { 16529 this.discontinuity = void 0; 16530 this.contiguous = void 0; 16531 this.accurateTimeOffset = void 0; 16532 this.trackSwitch = void 0; 16533 this.timeOffset = void 0; 16534 this.initSegmentChange = void 0; 16535 this.discontinuity = discontinuity; 16536 this.contiguous = contiguous; 16537 this.accurateTimeOffset = accurateTimeOffset; 16538 this.trackSwitch = trackSwitch; 16539 this.timeOffset = timeOffset; 16540 this.initSegmentChange = initSegmentChange; 16541 } 16542} 16543 16544let transmuxerInstanceCount = 0; 16545class TransmuxerInterface { 16546 constructor(_hls, id, onTransmuxComplete, onFlush) { 16547 this.error = null; 16548 this.hls = void 0; 16549 this.id = void 0; 16550 this.instanceNo = transmuxerInstanceCount++; 16551 this.observer = void 0; 16552 this.frag = null; 16553 this.part = null; 16554 this.useWorker = void 0; 16555 this.workerContext = null; 16556 this.transmuxer = null; 16557 this.onTransmuxComplete = void 0; 16558 this.onFlush = void 0; 16559 this.onWorkerMessage = event => { 16560 const data = event.data; 16561 const hls = this.hls; 16562 if (!hls || !(data != null && data.event) || data.instanceNo !== this.instanceNo) { 16563 return; 16564 } 16565 switch (data.event) { 16566 case 'init': 16567 { 16568 var _this$workerContext; 16569 const objectURL = (_this$workerContext = this.workerContext) == null ? void 0 : _this$workerContext.objectURL; 16570 if (objectURL) { 16571 // revoke the Object URL that was used to create transmuxer worker, so as not to leak it 16572 self.URL.revokeObjectURL(objectURL); 16573 } 16574 break; 16575 } 16576 case 'transmuxComplete': 16577 { 16578 this.handleTransmuxComplete(data.data); 16579 break; 16580 } 16581 case 'flush': 16582 { 16583 this.onFlush(data.data); 16584 break; 16585 } 16586 16587 // pass logs from the worker thread to the main logger 16588 case 'workerLog': 16589 { 16590 if (hls.logger[data.data.logType]) { 16591 hls.logger[data.data.logType](data.data.message); 16592 } 16593 break; 16594 } 16595 default: 16596 { 16597 data.data = data.data || {}; 16598 data.data.frag = this.frag; 16599 data.data.part = this.part; 16600 data.data.id = this.id; 16601 hls.trigger(data.event, data.data); 16602 break; 16603 } 16604 } 16605 }; 16606 this.onWorkerError = event => { 16607 if (!this.hls) { 16608 return; 16609 } 16610 const error = new Error(`${event.message} (${event.filename}:${event.lineno})`); 16611 this.hls.config.enableWorker = false; 16612 this.hls.logger.warn(`Error in "${this.id}" Web Worker, fallback to inline`); 16613 this.hls.trigger(Events.ERROR, { 16614 type: ErrorTypes.OTHER_ERROR, 16615 details: ErrorDetails.INTERNAL_EXCEPTION, 16616 fatal: false, 16617 event: 'demuxerWorker', 16618 error 16619 }); 16620 }; 16621 const config = _hls.config; 16622 this.hls = _hls; 16623 this.id = id; 16624 this.useWorker = !!config.enableWorker; 16625 this.onTransmuxComplete = onTransmuxComplete; 16626 this.onFlush = onFlush; 16627 const forwardMessage = (ev, data) => { 16628 data = data || {}; 16629 data.frag = this.frag || undefined; 16630 if (ev === Events.ERROR) { 16631 data = data; 16632 data.parent = this.id; 16633 data.part = this.part; 16634 this.error = data.error; 16635 } 16636 this.hls.trigger(ev, data); 16637 }; 16638 16639 // forward events to main thread 16640 this.observer = new EventEmitter(); 16641 this.observer.on(Events.FRAG_DECRYPTED, forwardMessage); 16642 this.observer.on(Events.ERROR, forwardMessage); 16643 const m2tsTypeSupported = getM2TSSupportedAudioTypes(config.preferManagedMediaSource); 16644 if (this.useWorker && typeof Worker !== 'undefined') { 16645 const logger = this.hls.logger; 16646 const canCreateWorker = config.workerPath || hasUMDWorker(); 16647 if (canCreateWorker) { 16648 try { 16649 if (config.workerPath) { 16650 logger.log(`loading Web Worker ${config.workerPath} for "${id}"`); 16651 this.workerContext = loadWorker(config.workerPath); 16652 } else { 16653 logger.log(`injecting Web Worker for "${id}"`); 16654 this.workerContext = injectWorker(); 16655 } 16656 const { 16657 worker 16658 } = this.workerContext; 16659 worker.addEventListener('message', this.onWorkerMessage); 16660 worker.addEventListener('error', this.onWorkerError); 16661 worker.postMessage({ 16662 instanceNo: this.instanceNo, 16663 cmd: 'init', 16664 typeSupported: m2tsTypeSupported, 16665 id, 16666 config: stringify(config) 16667 }); 16668 } catch (err) { 16669 logger.warn(`Error setting up "${id}" Web Worker, fallback to inline`, err); 16670 this.terminateWorker(); 16671 this.error = null; 16672 this.transmuxer = new Transmuxer(this.observer, m2tsTypeSupported, config, '', id, _hls.logger); 16673 } 16674 return; 16675 } 16676 } 16677 this.transmuxer = new Transmuxer(this.observer, m2tsTypeSupported, config, '', id, _hls.logger); 16678 } 16679 reset() { 16680 this.frag = null; 16681 this.part = null; 16682 if (this.workerContext) { 16683 const instanceNo = this.instanceNo; 16684 this.instanceNo = transmuxerInstanceCount++; 16685 const config = this.hls.config; 16686 const m2tsTypeSupported = getM2TSSupportedAudioTypes(config.preferManagedMediaSource); 16687 this.workerContext.worker.postMessage({ 16688 instanceNo: this.instanceNo, 16689 cmd: 'reset', 16690 resetNo: instanceNo, 16691 typeSupported: m2tsTypeSupported, 16692 id: this.id, 16693 config: stringify(config) 16694 }); 16695 } 16696 } 16697 terminateWorker() { 16698 if (this.workerContext) { 16699 const { 16700 worker 16701 } = this.workerContext; 16702 this.workerContext = null; 16703 worker.removeEventListener('message', this.onWorkerMessage); 16704 worker.removeEventListener('error', this.onWorkerError); 16705 removeWorkerFromStore(this.hls.config.workerPath); 16706 } 16707 } 16708 destroy() { 16709 if (this.workerContext) { 16710 this.terminateWorker(); 16711 // @ts-ignore 16712 this.onWorkerMessage = this.onWorkerError = null; 16713 } else { 16714 const transmuxer = this.transmuxer; 16715 if (transmuxer) { 16716 transmuxer.destroy(); 16717 this.transmuxer = null; 16718 } 16719 } 16720 const observer = this.observer; 16721 if (observer) { 16722 observer.removeAllListeners(); 16723 } 16724 this.frag = null; 16725 this.part = null; 16726 // @ts-ignore 16727 this.observer = null; 16728 // @ts-ignore 16729 this.hls = null; 16730 } 16731 push(data, initSegmentData, audioCodec, videoCodec, frag, part, duration, accurateTimeOffset, chunkMeta, defaultInitPTS) { 16732 var _frag$initSegment, _lastFrag$initSegment; 16733 chunkMeta.transmuxing.start = self.performance.now(); 16734 const { 16735 instanceNo, 16736 transmuxer 16737 } = this; 16738 const timeOffset = part ? part.start : frag.start; 16739 // TODO: push "clear-lead" decrypt data for unencrypted fragments in streams with encrypted ones 16740 const decryptdata = frag.decryptdata; 16741 const lastFrag = this.frag; 16742 const discontinuity = !(lastFrag && frag.cc === lastFrag.cc); 16743 const trackSwitch = !(lastFrag && chunkMeta.level === lastFrag.level); 16744 const snDiff = lastFrag ? chunkMeta.sn - lastFrag.sn : -1; 16745 const partDiff = this.part ? chunkMeta.part - this.part.index : -1; 16746 const progressive = snDiff === 0 && chunkMeta.id > 1 && chunkMeta.id === (lastFrag == null ? void 0 : lastFrag.stats.chunkCount); 16747 const contiguous = !trackSwitch && (snDiff === 1 || snDiff === 0 && (partDiff === 1 || progressive && partDiff <= 0)); 16748 const now = self.performance.now(); 16749 if (trackSwitch || snDiff || frag.stats.parsing.start === 0) { 16750 frag.stats.parsing.start = now; 16751 } 16752 if (part && (partDiff || !contiguous)) { 16753 part.stats.parsing.start = now; 16754 } 16755 const initSegmentChange = !(lastFrag && ((_frag$initSegment = frag.initSegment) == null ? void 0 : _frag$initSegment.url) === ((_lastFrag$initSegment = lastFrag.initSegment) == null ? void 0 : _lastFrag$initSegment.url)); 16756 const state = new TransmuxState(discontinuity, contiguous, accurateTimeOffset, trackSwitch, timeOffset, initSegmentChange); 16757 if (!contiguous || discontinuity || initSegmentChange) { 16758 this.hls.logger.log(`[transmuxer-interface]: Starting new transmux session for ${frag.type} sn: ${chunkMeta.sn}${chunkMeta.part > -1 ? ' part: ' + chunkMeta.part : ''} ${this.id === PlaylistLevelType.MAIN ? 'level' : 'track'}: ${chunkMeta.level} id: ${chunkMeta.id} 16759 discontinuity: ${discontinuity} 16760 trackSwitch: ${trackSwitch} 16761 contiguous: ${contiguous} 16762 accurateTimeOffset: ${accurateTimeOffset} 16763 timeOffset: ${timeOffset} 16764 initSegmentChange: ${initSegmentChange}`); 16765 const config = new TransmuxConfig(audioCodec, videoCodec, initSegmentData, duration, defaultInitPTS); 16766 this.configureTransmuxer(config); 16767 } 16768 this.frag = frag; 16769 this.part = part; 16770 16771 // Frags with sn of 'initSegment' are not transmuxed 16772 if (this.workerContext) { 16773 // post fragment payload as transferable objects for ArrayBuffer (no copy) 16774 this.workerContext.worker.postMessage({ 16775 instanceNo, 16776 cmd: 'demux', 16777 data, 16778 decryptdata, 16779 chunkMeta, 16780 state 16781 }, data instanceof ArrayBuffer ? [data] : []); 16782 } else if (transmuxer) { 16783 const transmuxResult = transmuxer.push(data, decryptdata, chunkMeta, state); 16784 if (isPromise(transmuxResult)) { 16785 transmuxResult.then(data => { 16786 this.handleTransmuxComplete(data); 16787 }).catch(error => { 16788 this.transmuxerError(error, chunkMeta, 'transmuxer-interface push error'); 16789 }); 16790 } else { 16791 this.handleTransmuxComplete(transmuxResult); 16792 } 16793 } 16794 } 16795 flush(chunkMeta) { 16796 chunkMeta.transmuxing.start = self.performance.now(); 16797 const { 16798 instanceNo, 16799 transmuxer 16800 } = this; 16801 if (this.workerContext) { 16802 this.workerContext.worker.postMessage({ 16803 instanceNo, 16804 cmd: 'flush', 16805 chunkMeta 16806 }); 16807 } else if (transmuxer) { 16808 const transmuxResult = transmuxer.flush(chunkMeta); 16809 if (isPromise(transmuxResult)) { 16810 transmuxResult.then(data => { 16811 this.handleFlushResult(data, chunkMeta); 16812 }).catch(error => { 16813 this.transmuxerError(error, chunkMeta, 'transmuxer-interface flush error'); 16814 }); 16815 } else { 16816 this.handleFlushResult(transmuxResult, chunkMeta); 16817 } 16818 } 16819 } 16820 transmuxerError(error, chunkMeta, reason) { 16821 if (!this.hls) { 16822 return; 16823 } 16824 this.error = error; 16825 this.hls.trigger(Events.ERROR, { 16826 type: ErrorTypes.MEDIA_ERROR, 16827 details: ErrorDetails.FRAG_PARSING_ERROR, 16828 chunkMeta, 16829 frag: this.frag || undefined, 16830 part: this.part || undefined, 16831 fatal: false, 16832 error, 16833 err: error, 16834 reason 16835 }); 16836 } 16837 handleFlushResult(results, chunkMeta) { 16838 results.forEach(result => { 16839 this.handleTransmuxComplete(result); 16840 }); 16841 this.onFlush(chunkMeta); 16842 } 16843 configureTransmuxer(config) { 16844 const { 16845 instanceNo, 16846 transmuxer 16847 } = this; 16848 if (this.workerContext) { 16849 this.workerContext.worker.postMessage({ 16850 instanceNo, 16851 cmd: 'configure', 16852 config 16853 }); 16854 } else if (transmuxer) { 16855 transmuxer.configure(config); 16856 } 16857 } 16858 handleTransmuxComplete(result) { 16859 result.chunkMeta.transmuxing.end = self.performance.now(); 16860 this.onTransmuxComplete(result); 16861 } 16862} 16863 16864const TICK_INTERVAL$3 = 100; // how often to tick in ms 16865 16866class AudioStreamController extends BaseStreamController { 16867 constructor(hls, fragmentTracker, keyLoader) { 16868 super(hls, fragmentTracker, keyLoader, 'audio-stream-controller', PlaylistLevelType.AUDIO); 16869 this.mainAnchor = null; 16870 this.mainFragLoading = null; 16871 this.audioOnly = false; 16872 this.bufferedTrack = null; 16873 this.switchingTrack = null; 16874 this.trackId = -1; 16875 this.waitingData = null; 16876 this.mainDetails = null; 16877 this.flushing = false; 16878 this.bufferFlushed = false; 16879 this.cachedTrackLoadedData = null; 16880 this.registerListeners(); 16881 } 16882 onHandlerDestroying() { 16883 this.unregisterListeners(); 16884 super.onHandlerDestroying(); 16885 this.resetItem(); 16886 } 16887 resetItem() { 16888 this.mainDetails = this.mainAnchor = this.mainFragLoading = this.bufferedTrack = this.switchingTrack = this.waitingData = this.cachedTrackLoadedData = null; 16889 } 16890 registerListeners() { 16891 super.registerListeners(); 16892 const { 16893 hls 16894 } = this; 16895 hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this); 16896 hls.on(Events.AUDIO_TRACKS_UPDATED, this.onAudioTracksUpdated, this); 16897 hls.on(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); 16898 hls.on(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this); 16899 hls.on(Events.BUFFER_RESET, this.onBufferReset, this); 16900 hls.on(Events.BUFFER_CREATED, this.onBufferCreated, this); 16901 hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 16902 hls.on(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); 16903 hls.on(Events.INIT_PTS_FOUND, this.onInitPtsFound, this); 16904 hls.on(Events.FRAG_LOADING, this.onFragLoading, this); 16905 hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this); 16906 } 16907 unregisterListeners() { 16908 const { 16909 hls 16910 } = this; 16911 if (!hls) { 16912 return; 16913 } 16914 super.unregisterListeners(); 16915 hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this); 16916 hls.off(Events.AUDIO_TRACKS_UPDATED, this.onAudioTracksUpdated, this); 16917 hls.off(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); 16918 hls.off(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this); 16919 hls.off(Events.BUFFER_RESET, this.onBufferReset, this); 16920 hls.off(Events.BUFFER_CREATED, this.onBufferCreated, this); 16921 hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 16922 hls.off(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); 16923 hls.off(Events.INIT_PTS_FOUND, this.onInitPtsFound, this); 16924 hls.off(Events.FRAG_LOADING, this.onFragLoading, this); 16925 hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this); 16926 } 16927 16928 // INIT_PTS_FOUND is triggered when the video track parsed in the stream-controller has a new PTS value 16929 onInitPtsFound(event, { 16930 frag, 16931 id, 16932 initPTS, 16933 timescale, 16934 trackId 16935 }) { 16936 // Always update the new INIT PTS 16937 // Can change due level switch 16938 if (id === PlaylistLevelType.MAIN) { 16939 const cc = frag.cc; 16940 const inFlightFrag = this.fragCurrent; 16941 this.initPTS[cc] = { 16942 baseTime: initPTS, 16943 timescale, 16944 trackId 16945 }; 16946 this.log(`InitPTS for cc: ${cc} found from main: ${initPTS / timescale} (${initPTS}/${timescale}) trackId: ${trackId}`); 16947 this.mainAnchor = frag; 16948 // If we are waiting, tick immediately to unblock audio fragment transmuxing 16949 if (this.state === State.WAITING_INIT_PTS) { 16950 const waitingData = this.waitingData; 16951 if (!waitingData && !this.loadingParts || waitingData && waitingData.frag.cc !== cc) { 16952 this.syncWithAnchor(frag, waitingData == null ? void 0 : waitingData.frag); 16953 } 16954 } else if (!this.hls.hasEnoughToStart && inFlightFrag && inFlightFrag.cc !== cc) { 16955 inFlightFrag.abortRequests(); 16956 this.syncWithAnchor(frag, inFlightFrag); 16957 } else if (this.state === State.IDLE) { 16958 this.tick(); 16959 } 16960 } 16961 } 16962 getLoadPosition() { 16963 if (!this.startFragRequested && this.nextLoadPosition >= 0) { 16964 return this.nextLoadPosition; 16965 } 16966 return super.getLoadPosition(); 16967 } 16968 syncWithAnchor(mainAnchor, waitingToAppend) { 16969 var _this$mainFragLoading; 16970 // Drop waiting fragment if videoTrackCC has changed since waitingFragment was set and initPTS was not found 16971 const mainFragLoading = ((_this$mainFragLoading = this.mainFragLoading) == null ? void 0 : _this$mainFragLoading.frag) || null; 16972 if (waitingToAppend) { 16973 if ((mainFragLoading == null ? void 0 : mainFragLoading.cc) === waitingToAppend.cc) { 16974 // Wait for loading frag to complete and INIT_PTS_FOUND 16975 return; 16976 } 16977 } 16978 const targetDiscontinuity = (mainFragLoading || mainAnchor).cc; 16979 const trackDetails = this.getLevelDetails(); 16980 const pos = this.getLoadPosition(); 16981 const syncFrag = findNearestWithCC(trackDetails, targetDiscontinuity, pos); 16982 // Only stop waiting for audioFrag.cc if an audio segment of the same discontinuity domain (cc) is found 16983 if (syncFrag) { 16984 this.log(`Syncing with main frag at ${syncFrag.start} cc ${syncFrag.cc}`); 16985 this.startFragRequested = false; 16986 this.nextLoadPosition = syncFrag.start; 16987 this.resetLoadingState(); 16988 if (this.state === State.IDLE) { 16989 this.doTickIdle(); 16990 } 16991 } 16992 } 16993 startLoad(startPosition, skipSeekToStartPosition) { 16994 if (!this.levels) { 16995 this.startPosition = startPosition; 16996 this.state = State.STOPPED; 16997 return; 16998 } 16999 const lastCurrentTime = this.lastCurrentTime; 17000 this.stopLoad(); 17001 this.setInterval(TICK_INTERVAL$3); 17002 if (lastCurrentTime > 0 && startPosition === -1) { 17003 this.log(`Override startPosition with lastCurrentTime @${lastCurrentTime.toFixed(3)}`); 17004 startPosition = lastCurrentTime; 17005 this.state = State.IDLE; 17006 } else { 17007 this.state = State.WAITING_TRACK; 17008 } 17009 this.nextLoadPosition = this.lastCurrentTime = startPosition + this.timelineOffset; 17010 this.startPosition = skipSeekToStartPosition ? -1 : startPosition; 17011 this.tick(); 17012 } 17013 doTick() { 17014 switch (this.state) { 17015 case State.IDLE: 17016 this.doTickIdle(); 17017 break; 17018 case State.WAITING_TRACK: 17019 { 17020 const { 17021 levels, 17022 trackId 17023 } = this; 17024 const currenTrack = levels == null ? void 0 : levels[trackId]; 17025 const details = currenTrack == null ? void 0 : currenTrack.details; 17026 if (details && !this.waitForLive(currenTrack)) { 17027 if (this.waitForCdnTuneIn(details)) { 17028 break; 17029 } 17030 this.state = State.WAITING_INIT_PTS; 17031 } 17032 break; 17033 } 17034 case State.FRAG_LOADING_WAITING_RETRY: 17035 { 17036 this.checkRetryDate(); 17037 break; 17038 } 17039 case State.WAITING_INIT_PTS: 17040 { 17041 // Ensure we don't get stuck in the WAITING_INIT_PTS state if the waiting frag CC doesn't match any initPTS 17042 const waitingData = this.waitingData; 17043 if (waitingData) { 17044 const { 17045 frag, 17046 part, 17047 cache, 17048 complete 17049 } = waitingData; 17050 const mainAnchor = this.mainAnchor; 17051 if (this.initPTS[frag.cc] !== undefined) { 17052 this.waitingData = null; 17053 this.state = State.FRAG_LOADING; 17054 const payload = cache.flush().buffer; 17055 const data = { 17056 frag, 17057 part, 17058 payload, 17059 networkDetails: null 17060 }; 17061 this._handleFragmentLoadProgress(data); 17062 if (complete) { 17063 super._handleFragmentLoadComplete(data); 17064 } 17065 } else if (mainAnchor && mainAnchor.cc !== waitingData.frag.cc) { 17066 this.syncWithAnchor(mainAnchor, waitingData.frag); 17067 } 17068 } else { 17069 this.state = State.IDLE; 17070 } 17071 } 17072 } 17073 this.onTickEnd(); 17074 } 17075 resetLoadingState() { 17076 const waitingData = this.waitingData; 17077 if (waitingData) { 17078 this.fragmentTracker.removeFragment(waitingData.frag); 17079 this.waitingData = null; 17080 } 17081 super.resetLoadingState(); 17082 } 17083 onTickEnd() { 17084 const { 17085 media 17086 } = this; 17087 if (!(media != null && media.readyState)) { 17088 // Exit early if we don't have media or if the media hasn't buffered anything yet (readyState 0) 17089 return; 17090 } 17091 this.lastCurrentTime = media.currentTime; 17092 } 17093 doTickIdle() { 17094 var _this$mainFragLoading2; 17095 const { 17096 hls, 17097 levels, 17098 media, 17099 trackId 17100 } = this; 17101 const config = hls.config; 17102 17103 // 1. if buffering is suspended 17104 // 2. if video not attached AND 17105 // start fragment already requested OR start frag prefetch not enabled 17106 // 3. if tracks or track not loaded and selected 17107 // then exit loop 17108 // => if media not attached but start frag prefetch is enabled and start frag not requested yet, we will not exit loop 17109 if (!this.buffering || !media && !this.primaryPrefetch && (this.startFragRequested || !config.startFragPrefetch) || !(levels != null && levels[trackId])) { 17110 return; 17111 } 17112 const levelInfo = levels[trackId];
vendor: 5,300 bytes, lines 17113-17241
17113 const trackDetails = levelInfo.details; 17114 if (!trackDetails || this.waitForLive(levelInfo) || this.waitForCdnTuneIn(trackDetails)) { 17115 this.state = State.WAITING_TRACK; 17116 this.startFragRequested = false; 17117 return; 17118 } 17119 const bufferable = this.mediaBuffer ? this.mediaBuffer : this.media; 17120 if (this.bufferFlushed && bufferable) { 17121 this.bufferFlushed = false; 17122 this.afterBufferFlushed(bufferable, ElementaryStreamTypes.AUDIO, PlaylistLevelType.AUDIO); 17123 } 17124 const bufferInfo = this.getFwdBufferInfo(bufferable, PlaylistLevelType.AUDIO); 17125 if (bufferInfo === null) { 17126 return; 17127 } 17128 if (!this.switchingTrack && this._streamEnded(bufferInfo, trackDetails)) { 17129 hls.trigger(Events.BUFFER_EOS, { 17130 type: 'audio' 17131 }); 17132 this.state = State.ENDED; 17133 return; 17134 } 17135 const bufferLen = bufferInfo.len; 17136 const maxBufLen = hls.maxBufferLength; 17137 const fragments = trackDetails.fragments; 17138 const start = fragments[0].start; 17139 const loadPosition = this.getLoadPosition(); 17140 const targetBufferTime = this.flushing ? loadPosition : bufferInfo.end; 17141 if (this.switchingTrack && media) { 17142 const pos = loadPosition; 17143 // if currentTime (pos) is less than alt audio playlist start time, it means that alt audio is ahead of currentTime 17144 if (trackDetails.PTSKnown && pos < start) { 17145 // if everything is buffered from pos to start or if audio buffer upfront, let's seek to start 17146 if (bufferInfo.end > start || bufferInfo.nextStart) { 17147 this.log('Alt audio track ahead of main track, seek to start of alt audio track'); 17148 media.currentTime = start + 0.05; 17149 } 17150 } 17151 } 17152 17153 // if buffer length is less than maxBufLen, or near the end, find a fragment to load 17154 if (bufferLen >= maxBufLen && !this.switchingTrack && targetBufferTime < fragments[fragments.length - 1].start) { 17155 return; 17156 } 17157 let frag = this.getNextFragment(targetBufferTime, trackDetails); 17158 // Avoid loop loading by using nextLoadPosition set for backtracking and skipping consecutive GAP tags 17159 if (frag && this.isLoopLoading(frag, targetBufferTime)) { 17160 frag = this.getNextFragmentLoopLoading(frag, trackDetails, bufferInfo, PlaylistLevelType.MAIN, maxBufLen); 17161 } 17162 if (!frag) { 17163 this.bufferFlushed = true; 17164 return; 17165 } 17166 if (this.exceedsMaxBuffer(bufferInfo, maxBufLen, frag)) { 17167 return; 17168 } 17169 17170 // Request audio segments up to one fragment ahead of main stream-controller 17171 let mainFragLoading = ((_this$mainFragLoading2 = this.mainFragLoading) == null ? void 0 : _this$mainFragLoading2.frag) || null; 17172 if (!this.audioOnly && this.startFragRequested && mainFragLoading && isMediaFragment(frag) && !frag.endList && (!trackDetails.live || !this.loadingParts && targetBufferTime < this.hls.liveSyncPosition)) { 17173 if (this.fragmentTracker.getState(mainFragLoading) === FragmentState.OK) { 17174 this.mainFragLoading = mainFragLoading = null; 17175 } 17176 if (mainFragLoading && isMediaFragment(mainFragLoading)) { 17177 if (frag.start > mainFragLoading.end) { 17178 // Get buffered frag at target position from tracker (loaded out of sequence) 17179 const mainFragAtPos = this.fragmentTracker.getFragAtPos(targetBufferTime, PlaylistLevelType.MAIN); 17180 if (mainFragAtPos && mainFragAtPos.end > mainFragLoading.end) { 17181 mainFragLoading = mainFragAtPos; 17182 this.mainFragLoading = { 17183 frag: mainFragAtPos, 17184 targetBufferTime: null 17185 }; 17186 } 17187 } 17188 const atBufferSyncLimit = frag.start > mainFragLoading.end; 17189 if (atBufferSyncLimit) { 17190 return; 17191 } 17192 } 17193 } 17194 this.loadFragment(frag, levelInfo, targetBufferTime); 17195 } 17196 onMediaDetaching(event, data) { 17197 this.bufferFlushed = this.flushing = false; 17198 super.onMediaDetaching(event, data); 17199 } 17200 onAudioTracksUpdated(event, { 17201 audioTracks 17202 }) { 17203 // Reset tranxmuxer is essential for large context switches (Content Steering) 17204 this.resetTransmuxer(); 17205 this.levels = audioTracks.map(mediaPlaylist => new Level(mediaPlaylist)); 17206 } 17207 onAudioTrackSwitching(event, data) { 17208 // if any URL found on new audio track, it is an alternate audio track 17209 const altAudio = !!data.url; 17210 this.trackId = data.id; 17211 const { 17212 fragCurrent 17213 } = this; 17214 if (fragCurrent) { 17215 fragCurrent.abortRequests(); 17216 this.removeUnbufferedFrags(fragCurrent.start); 17217 } 17218 this.resetLoadingState(); 17219 17220 // should we switch tracks ? 17221 if (altAudio) { 17222 this.switchingTrack = data; 17223 // main audio track are handled by stream-controller, just do something if switching to alt audio track 17224 this.flushAudioIfNeeded(data); 17225 if (this.state !== State.STOPPED) { 17226 // switching to audio track, start timer if not already started 17227 this.setInterval(TICK_INTERVAL$3); 17228 this.state = State.IDLE; 17229 this.tick(); 17230 } 17231 } else { 17232 // destroy useless transmuxer when switching audio to main 17233 this.resetTransmuxer(); 17234 this.switchingTrack = null; 17235 this.bufferedTrack = data; 17236 this.clearInterval(); 17237 } 17238 } 17239 onManifestLoading() { 17240 super.onManifestLoading(); 17241 this.bufferFlushed = this.flushing = this.audioOnly = false;
vendor: 40,021 bytes, lines 17242-18308
17242 this.resetItem(); 17243 this.trackId = -1; 17244 } 17245 onLevelLoaded(event, data) { 17246 this.mainDetails = data.details; 17247 const cachedTrackLoadedData = this.cachedTrackLoadedData; 17248 if (cachedTrackLoadedData) { 17249 this.cachedTrackLoadedData = null; 17250 this.onAudioTrackLoaded(Events.AUDIO_TRACK_LOADED, cachedTrackLoadedData); 17251 } 17252 } 17253 onAudioTrackLoaded(event, data) { 17254 var _trackLevel$details; 17255 const { 17256 levels 17257 } = this; 17258 const { 17259 details: newDetails, 17260 id: trackId, 17261 groupId, 17262 track 17263 } = data; 17264 if (!levels) { 17265 this.warn(`Audio tracks reset while loading track ${trackId} "${track.name}" of "${groupId}"`); 17266 return; 17267 } 17268 const mainDetails = this.mainDetails; 17269 if (!mainDetails || newDetails.endCC > mainDetails.endCC || mainDetails.expired) { 17270 this.cachedTrackLoadedData = data; 17271 if (this.state !== State.STOPPED) { 17272 this.state = State.WAITING_TRACK; 17273 } 17274 return; 17275 } 17276 this.cachedTrackLoadedData = null; 17277 this.log(`Audio track ${trackId} "${track.name}" of "${groupId}" loaded [${newDetails.startSN},${newDetails.endSN}]${newDetails.lastPartSn ? `[part-${newDetails.lastPartSn}-${newDetails.lastPartIndex}]` : ''},duration:${newDetails.totalduration}`); 17278 const trackLevel = levels[trackId]; 17279 let sliding = 0; 17280 if (newDetails.live || (_trackLevel$details = trackLevel.details) != null && _trackLevel$details.live) { 17281 this.checkLiveUpdate(newDetails); 17282 if (newDetails.deltaUpdateFailed) { 17283 return; 17284 } 17285 if (trackLevel.details) { 17286 var _this$levelLastLoaded; 17287 sliding = this.alignPlaylists(newDetails, trackLevel.details, (_this$levelLastLoaded = this.levelLastLoaded) == null ? void 0 : _this$levelLastLoaded.details); 17288 } 17289 if (!newDetails.alignedSliding) { 17290 // Align audio rendition with the "main" playlist on discontinuity change 17291 // or program-date-time (PDT) 17292 alignDiscontinuities(newDetails, mainDetails); 17293 if (!newDetails.alignedSliding) { 17294 alignMediaPlaylistByPDT(newDetails, mainDetails); 17295 } 17296 sliding = newDetails.fragmentStart; 17297 } 17298 } 17299 trackLevel.details = newDetails; 17300 this.levelLastLoaded = trackLevel; 17301 17302 // compute start position if we are aligned with the main playlist 17303 if (!this.startFragRequested) { 17304 this.setStartPosition(mainDetails, sliding); 17305 } 17306 this.hls.trigger(Events.AUDIO_TRACK_UPDATED, { 17307 details: newDetails, 17308 id: trackId, 17309 groupId: data.groupId 17310 }); 17311 17312 // only switch back to IDLE state if we were waiting for track to start downloading a new fragment 17313 if (this.state === State.WAITING_TRACK && !this.waitForCdnTuneIn(newDetails)) { 17314 this.state = State.IDLE; 17315 } 17316 17317 // trigger handler right now 17318 this.tick(); 17319 } 17320 _handleFragmentLoadProgress(data) { 17321 var _frag$initSegment; 17322 const frag = data.frag; 17323 const { 17324 part, 17325 payload 17326 } = data; 17327 const { 17328 config, 17329 trackId, 17330 levels 17331 } = this; 17332 if (!levels) { 17333 this.warn(`Audio tracks were reset while fragment load was in progress. Fragment ${frag.sn} of level ${frag.level} will not be buffered`); 17334 return; 17335 } 17336 const track = levels[trackId]; 17337 if (!track) { 17338 this.warn('Audio track is undefined on fragment load progress'); 17339 return; 17340 } 17341 const details = track.details; 17342 if (!details) { 17343 this.warn('Audio track details undefined on fragment load progress'); 17344 this.removeUnbufferedFrags(frag.start); 17345 return; 17346 } 17347 const audioCodec = config.defaultAudioCodec || track.audioCodec || 'mp4a.40.2'; 17348 let transmuxer = this.transmuxer; 17349 if (!transmuxer) { 17350 transmuxer = this.transmuxer = new TransmuxerInterface(this.hls, PlaylistLevelType.AUDIO, this._handleTransmuxComplete.bind(this), this._handleTransmuxerFlush.bind(this)); 17351 } 17352 17353 // Check if we have video initPTS 17354 // If not we need to wait for it 17355 const initPTS = this.initPTS[frag.cc]; 17356 const initSegmentData = (_frag$initSegment = frag.initSegment) == null ? void 0 : _frag$initSegment.data; 17357 if (initPTS !== undefined) { 17358 // this.log(`Transmuxing ${sn} of [${details.startSN} ,${details.endSN}],track ${trackId}`); 17359 // time Offset is accurate if level PTS is known, or if playlist is not sliding (not live) 17360 const accurateTimeOffset = false; // details.PTSKnown || !details.live; 17361 const partIndex = part ? part.index : -1; 17362 const partial = partIndex !== -1; 17363 const chunkMeta = new ChunkMetadata(frag.level, frag.sn, frag.stats.chunkCount, payload.byteLength, partIndex, partial); 17364 transmuxer.push(payload, initSegmentData, audioCodec, '', frag, part, details.totalduration, accurateTimeOffset, chunkMeta, initPTS); 17365 } else { 17366 this.log(`Unknown video PTS for cc ${frag.cc}, waiting for video PTS before demuxing audio frag ${frag.sn} of [${details.startSN} ,${details.endSN}],track ${trackId}`); 17367 const { 17368 cache 17369 } = this.waitingData = this.waitingData || { 17370 frag, 17371 part, 17372 cache: new ChunkCache(), 17373 complete: false 17374 }; 17375 cache.push(new Uint8Array(payload)); 17376 if (this.state !== State.STOPPED) { 17377 this.state = State.WAITING_INIT_PTS; 17378 } 17379 } 17380 } 17381 _handleFragmentLoadComplete(fragLoadedData) { 17382 if (this.waitingData) { 17383 this.waitingData.complete = true; 17384 return; 17385 } 17386 super._handleFragmentLoadComplete(fragLoadedData); 17387 } 17388 onBufferReset(/* event: Events.BUFFER_RESET */ 17389 ) { 17390 // reset reference to sourcebuffers 17391 this.mediaBuffer = null; 17392 } 17393 onBufferCreated(event, data) { 17394 this.bufferFlushed = this.flushing = false; 17395 const audioTrack = data.tracks.audio; 17396 if (audioTrack) { 17397 this.mediaBuffer = audioTrack.buffer || null; 17398 } 17399 } 17400 onFragLoading(event, data) { 17401 if (!this.audioOnly && data.frag.type === PlaylistLevelType.MAIN && isMediaFragment(data.frag)) { 17402 this.mainFragLoading = data; 17403 if (this.state === State.IDLE) { 17404 this.tick(); 17405 } 17406 } 17407 } 17408 onFragBuffered(event, data) { 17409 const { 17410 frag, 17411 part 17412 } = data; 17413 if (frag.type !== PlaylistLevelType.AUDIO) { 17414 if (!this.audioOnly && frag.type === PlaylistLevelType.MAIN && !frag.elementaryStreams.video && !frag.elementaryStreams.audiovideo) { 17415 this.audioOnly = true; 17416 this.mainFragLoading = null; 17417 } 17418 return; 17419 } 17420 if (this.fragContextChanged(frag)) { 17421 // If a level switch was requested while a fragment was buffering, it will emit the FRAG_BUFFERED event upon completion 17422 // Avoid setting state back to IDLE or concluding the audio switch; otherwise, the switched-to track will not buffer 17423 this.warn(`Fragment ${frag.sn}${part ? ' p: ' + part.index : ''} of level ${frag.level} finished buffering, but was aborted. state: ${this.state}, audioSwitch: ${this.switchingTrack ? this.switchingTrack.name : 'false'}`); 17424 return; 17425 } 17426 if (isMediaFragment(frag)) { 17427 this.fragPrevious = frag; 17428 const track = this.switchingTrack; 17429 if (track) { 17430 this.bufferedTrack = track; 17431 this.switchingTrack = null; 17432 this.hls.trigger(Events.AUDIO_TRACK_SWITCHED, _objectSpread2({}, track)); 17433 } 17434 } 17435 this.fragBufferedComplete(frag, part); 17436 if (this.media) { 17437 this.tick(); 17438 } 17439 } 17440 onError(event, data) { 17441 var _data$context; 17442 if (data.fatal) { 17443 this.state = State.ERROR; 17444 return; 17445 } 17446 switch (data.details) { 17447 case ErrorDetails.FRAG_GAP: 17448 case ErrorDetails.FRAG_PARSING_ERROR: 17449 case ErrorDetails.FRAG_DECRYPT_ERROR: 17450 case ErrorDetails.FRAG_LOAD_ERROR: 17451 case ErrorDetails.FRAG_LOAD_TIMEOUT: 17452 case ErrorDetails.KEY_LOAD_ERROR: 17453 case ErrorDetails.KEY_LOAD_TIMEOUT: 17454 this.onFragmentOrKeyLoadError(PlaylistLevelType.AUDIO, data); 17455 break; 17456 case ErrorDetails.AUDIO_TRACK_LOAD_ERROR: 17457 case ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT: 17458 case ErrorDetails.LEVEL_PARSING_ERROR: 17459 // in case of non fatal error while loading track, if not retrying to load track, switch back to IDLE 17460 if (!data.levelRetry && this.state === State.WAITING_TRACK && ((_data$context = data.context) == null ? void 0 : _data$context.type) === PlaylistContextType.AUDIO_TRACK) { 17461 this.state = State.IDLE; 17462 } 17463 break; 17464 case ErrorDetails.BUFFER_ADD_CODEC_ERROR: 17465 case ErrorDetails.BUFFER_APPEND_ERROR: 17466 if (data.parent !== 'audio') { 17467 return; 17468 } 17469 if (!this.reduceLengthAndFlushBuffer(data)) { 17470 this.resetLoadingState(); 17471 } 17472 break; 17473 case ErrorDetails.BUFFER_FULL_ERROR: 17474 if (data.parent !== 'audio') { 17475 return; 17476 } 17477 if (this.reduceLengthAndFlushBuffer(data)) { 17478 this.bufferedTrack = null; 17479 super.flushMainBuffer(0, Number.POSITIVE_INFINITY, 'audio'); 17480 } 17481 break; 17482 case ErrorDetails.INTERNAL_EXCEPTION: 17483 this.recoverWorkerError(data); 17484 break; 17485 } 17486 } 17487 onBufferFlushing(event, { 17488 type 17489 }) { 17490 if (type !== ElementaryStreamTypes.VIDEO) { 17491 this.flushing = true; 17492 } 17493 } 17494 onBufferFlushed(event, { 17495 type 17496 }) { 17497 if (type !== ElementaryStreamTypes.VIDEO) { 17498 this.flushing = false; 17499 this.bufferFlushed = true; 17500 if (this.state === State.ENDED) { 17501 this.state = State.IDLE; 17502 } 17503 const mediaBuffer = this.mediaBuffer || this.media; 17504 if (mediaBuffer) { 17505 this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.AUDIO); 17506 this.tick(); 17507 } 17508 } 17509 } 17510 _handleTransmuxComplete(transmuxResult) { 17511 var _id3$samples; 17512 const id = 'audio'; 17513 const { 17514 hls 17515 } = this; 17516 const { 17517 remuxResult, 17518 chunkMeta 17519 } = transmuxResult; 17520 const context = this.getCurrentContext(chunkMeta); 17521 if (!context) { 17522 this.resetWhenMissingContext(chunkMeta); 17523 return; 17524 } 17525 const { 17526 frag, 17527 part, 17528 level 17529 } = context; 17530 const { 17531 details 17532 } = level; 17533 const { 17534 audio, 17535 text, 17536 id3, 17537 initSegment 17538 } = remuxResult; 17539 17540 // Check if the current fragment has been aborted. We check this by first seeing if we're still playing the current level. 17541 // If we are, subsequently check if the currently loading fragment (fragCurrent) has changed. 17542 if (this.fragContextChanged(frag) || !details) { 17543 this.fragmentTracker.removeFragment(frag); 17544 return; 17545 } 17546 this.state = State.PARSING; 17547 if (this.switchingTrack && audio) { 17548 this.completeAudioSwitch(this.switchingTrack); 17549 } 17550 if (initSegment != null && initSegment.tracks) { 17551 const mapFragment = frag.initSegment || frag; 17552 if (this.unhandledEncryptionError(initSegment, frag)) { 17553 return; 17554 } 17555 this._bufferInitSegment(level, initSegment.tracks, mapFragment, chunkMeta); 17556 hls.trigger(Events.FRAG_PARSING_INIT_SEGMENT, { 17557 frag: mapFragment, 17558 id, 17559 tracks: initSegment.tracks 17560 }); 17561 // Only flush audio from old audio tracks when PTS is known on new audio track 17562 } 17563 if (audio) { 17564 const { 17565 startPTS, 17566 endPTS, 17567 startDTS, 17568 endDTS 17569 } = audio; 17570 if (part) { 17571 part.elementaryStreams[ElementaryStreamTypes.AUDIO] = { 17572 startPTS, 17573 endPTS, 17574 startDTS, 17575 endDTS 17576 }; 17577 } 17578 frag.setElementaryStreamInfo(ElementaryStreamTypes.AUDIO, startPTS, endPTS, startDTS, endDTS); 17579 this.bufferFragmentData(audio, frag, part, chunkMeta); 17580 } 17581 if (id3 != null && (_id3$samples = id3.samples) != null && _id3$samples.length) { 17582 const emittedID3 = _extends({ 17583 id, 17584 frag, 17585 details 17586 }, id3); 17587 hls.trigger(Events.FRAG_PARSING_METADATA, emittedID3); 17588 } 17589 if (text) { 17590 const emittedText = _extends({ 17591 id, 17592 frag, 17593 details 17594 }, text); 17595 hls.trigger(Events.FRAG_PARSING_USERDATA, emittedText); 17596 } 17597 } 17598 _bufferInitSegment(currentLevel, tracks, frag, chunkMeta) { 17599 if (this.state !== State.PARSING) { 17600 return; 17601 } 17602 // delete any video track found on audio transmuxer 17603 if (tracks.video) { 17604 delete tracks.video; 17605 } 17606 if (tracks.audiovideo) { 17607 delete tracks.audiovideo; 17608 } 17609 17610 // include levelCodec in audio and video tracks 17611 if (!tracks.audio) { 17612 return; 17613 } 17614 const track = tracks.audio; 17615 track.id = PlaylistLevelType.AUDIO; 17616 const variantAudioCodecs = currentLevel.audioCodec; 17617 this.log(`Init audio buffer, container:${track.container}, codecs[level/parsed]=[${variantAudioCodecs}/${track.codec}]`); 17618 // SourceBuffer will use track.levelCodec if defined 17619 if (variantAudioCodecs && variantAudioCodecs.split(',').length === 1) { 17620 track.levelCodec = variantAudioCodecs; 17621 } 17622 this.hls.trigger(Events.BUFFER_CODECS, tracks); 17623 const initSegment = track.initSegment; 17624 if (initSegment != null && initSegment.byteLength) { 17625 const segment = { 17626 type: 'audio', 17627 frag, 17628 part: null, 17629 chunkMeta, 17630 parent: frag.type, 17631 data: initSegment 17632 }; 17633 this.hls.trigger(Events.BUFFER_APPENDING, segment); 17634 } 17635 // trigger handler right now 17636 this.tickImmediate(); 17637 } 17638 loadFragment(frag, track, targetBufferTime) { 17639 // only load if fragment is not loaded or if in audio switch 17640 const fragState = this.fragmentTracker.getState(frag); 17641 17642 // we force a frag loading in audio switch as fragment tracker might not have evicted previous frags in case of quick audio switch 17643 if (this.switchingTrack || fragState === FragmentState.NOT_LOADED || fragState === FragmentState.PARTIAL) { 17644 var _track$details; 17645 if (!isMediaFragment(frag)) { 17646 this._loadInitSegment(frag, track); 17647 } else if ((_track$details = track.details) != null && _track$details.live && !this.initPTS[frag.cc]) { 17648 this.log(`Waiting for video PTS in continuity counter ${frag.cc} of live stream before loading audio fragment ${frag.sn} of level ${this.trackId}`); 17649 this.state = State.WAITING_INIT_PTS; 17650 const mainDetails = this.mainDetails; 17651 if (mainDetails && mainDetails.fragmentStart !== track.details.fragmentStart) { 17652 alignMediaPlaylistByPDT(track.details, mainDetails); 17653 } 17654 } else { 17655 super.loadFragment(frag, track, targetBufferTime); 17656 } 17657 } else { 17658 this.clearTrackerIfNeeded(frag); 17659 } 17660 } 17661 flushAudioIfNeeded(switchingTrack) { 17662 if (this.media && this.bufferedTrack) { 17663 const { 17664 name, 17665 lang, 17666 assocLang, 17667 characteristics, 17668 audioCodec, 17669 channels 17670 } = this.bufferedTrack; 17671 if (!matchesOption({ 17672 name, 17673 lang, 17674 assocLang, 17675 characteristics, 17676 audioCodec, 17677 channels 17678 }, switchingTrack, audioMatchPredicate)) { 17679 if (useAlternateAudio(switchingTrack.url, this.hls)) { 17680 this.log('Switching audio track : flushing all audio'); 17681 super.flushMainBuffer(0, Number.POSITIVE_INFINITY, 'audio'); 17682 this.bufferedTrack = null; 17683 } else { 17684 // Main is being buffered. Set bufferedTrack so that it is flushed when switching back to alt-audio 17685 this.bufferedTrack = switchingTrack; 17686 } 17687 } 17688 } 17689 } 17690 completeAudioSwitch(switchingTrack) { 17691 const { 17692 hls 17693 } = this; 17694 this.flushAudioIfNeeded(switchingTrack); 17695 this.bufferedTrack = switchingTrack; 17696 this.switchingTrack = null; 17697 hls.trigger(Events.AUDIO_TRACK_SWITCHED, _objectSpread2({}, switchingTrack)); 17698 } 17699} 17700 17701class BasePlaylistController extends Logger { 17702 constructor(hls, logPrefix) { 17703 super(logPrefix, hls.logger); 17704 this.hls = void 0; 17705 this.canLoad = false; 17706 this.timer = -1; 17707 this.hls = hls; 17708 } 17709 destroy() { 17710 this.clearTimer(); 17711 // @ts-ignore 17712 this.hls = this.log = this.warn = null; 17713 } 17714 clearTimer() { 17715 if (this.timer !== -1) { 17716 self.clearTimeout(this.timer); 17717 this.timer = -1; 17718 } 17719 } 17720 startLoad() { 17721 this.canLoad = true; 17722 this.loadPlaylist(); 17723 } 17724 stopLoad() { 17725 this.canLoad = false; 17726 this.clearTimer(); 17727 } 17728 switchParams(playlistUri, previous, current) { 17729 const renditionReports = previous == null ? void 0 : previous.renditionReports; 17730 if (renditionReports) { 17731 let foundIndex = -1; 17732 for (let i = 0; i < renditionReports.length; i++) { 17733 const attr = renditionReports[i]; 17734 let uri; 17735 try { 17736 uri = new self.URL(attr.URI, previous.url).href; 17737 } catch (error) { 17738 this.warn(`Could not construct new URL for Rendition Report: ${error}`); 17739 uri = attr.URI || ''; 17740 } 17741 // Use exact match. Otherwise, the last partial match, if any, will be used 17742 // (Playlist URI includes a query string that the Rendition Report does not) 17743 if (uri === playlistUri) { 17744 foundIndex = i; 17745 break; 17746 } else if (uri === playlistUri.substring(0, uri.length)) { 17747 foundIndex = i; 17748 } 17749 } 17750 if (foundIndex !== -1) { 17751 const attr = renditionReports[foundIndex]; 17752 const msn = parseInt(attr['LAST-MSN']) || previous.lastPartSn; 17753 let part = parseInt(attr['LAST-PART']) || previous.lastPartIndex; 17754 if (this.hls.config.lowLatencyMode) { 17755 const currentGoal = Math.min(previous.age - previous.partTarget, previous.targetduration); 17756 if (part >= 0 && currentGoal > previous.partTarget) { 17757 part += 1; 17758 } 17759 } 17760 const skip = current && getSkipValue(current); 17761 return new HlsUrlParameters(msn, part >= 0 ? part : undefined, skip); 17762 } 17763 } 17764 } 17765 loadPlaylist(hlsUrlParameters) { 17766 // Loading is handled by the subclasses 17767 this.clearTimer(); 17768 } 17769 loadingPlaylist(playlist, hlsUrlParameters) { 17770 // Loading is handled by the subclasses 17771 this.clearTimer(); 17772 } 17773 shouldLoadPlaylist(playlist) { 17774 return this.canLoad && !!playlist && !!playlist.url && (!playlist.details || playlist.details.live); 17775 } 17776 getUrlWithDirectives(uri, hlsUrlParameters) { 17777 if (hlsUrlParameters) { 17778 try { 17779 return hlsUrlParameters.addDirectives(uri); 17780 } catch (error) { 17781 this.warn(`Could not construct new URL with HLS Delivery Directives: ${error}`); 17782 } 17783 } 17784 return uri; 17785 } 17786 playlistLoaded(index, data, previousDetails) { 17787 const { 17788 details, 17789 stats 17790 } = data; 17791 17792 // Set last updated date-time 17793 const now = self.performance.now(); 17794 const elapsed = stats.loading.first ? Math.max(0, now - stats.loading.first) : 0; 17795 details.advancedDateTime = Date.now() - elapsed; 17796 17797 // shift fragment starts with timelineOffset 17798 const timelineOffset = this.hls.config.timelineOffset; 17799 if (timelineOffset !== details.appliedTimelineOffset) { 17800 const offset = Math.max(timelineOffset || 0, 0); 17801 details.appliedTimelineOffset = offset; 17802 details.fragments.forEach(frag => { 17803 frag.setStart(frag.playlistOffset + offset); 17804 }); 17805 } 17806 17807 // if current playlist is a live playlist, arm a timer to reload it 17808 if (details.live || previousDetails != null && previousDetails.live) { 17809 const levelOrTrack = 'levelInfo' in data ? data.levelInfo : data.track; 17810 details.reloaded(previousDetails); 17811 // Merge live playlists to adjust fragment starts and fill in delta playlist skipped segments 17812 if (previousDetails && details.fragments.length > 0) { 17813 mergeDetails(previousDetails, details, this); 17814 const error = details.playlistParsingError; 17815 if (error) { 17816 this.warn(error); 17817 const hls = this.hls; 17818 if (!hls.config.ignorePlaylistParsingErrors) { 17819 var _details$fragments$; 17820 const { 17821 networkDetails 17822 } = data; 17823 hls.trigger(Events.ERROR, { 17824 type: ErrorTypes.NETWORK_ERROR, 17825 details: ErrorDetails.LEVEL_PARSING_ERROR, 17826 fatal: false, 17827 url: details.url, 17828 error, 17829 reason: error.message, 17830 level: data.level || undefined, 17831 parent: (_details$fragments$ = details.fragments[0]) == null ? void 0 : _details$fragments$.type, 17832 networkDetails, 17833 stats 17834 }); 17835 return; 17836 } 17837 details.playlistParsingError = null; 17838 } 17839 } 17840 if (details.requestScheduled === -1) { 17841 details.requestScheduled = stats.loading.start; 17842 } 17843 const bufferInfo = this.hls.mainForwardBufferInfo; 17844 const position = bufferInfo ? bufferInfo.end - bufferInfo.len : 0; 17845 const distanceToLiveEdgeMs = (details.edge - position) * 1000; 17846 const reloadInterval = computeReloadInterval(details, distanceToLiveEdgeMs); 17847 if (details.requestScheduled + reloadInterval < now) { 17848 details.requestScheduled = now; 17849 } else { 17850 details.requestScheduled += reloadInterval; 17851 } 17852 this.log(`live playlist ${index} ${details.advanced ? 'REFRESHED ' + details.lastPartSn + '-' + details.lastPartIndex : details.updated ? 'UPDATED' : 'MISSED'}`); 17853 if (!this.canLoad || !details.live) { 17854 return; 17855 } 17856 let deliveryDirectives; 17857 let msn = undefined; 17858 let part = undefined; 17859 if (details.canBlockReload && details.endSN && details.advanced) { 17860 // Load level with LL-HLS delivery directives 17861 const lowLatencyMode = this.hls.config.lowLatencyMode; 17862 const lastPartSn = details.lastPartSn; 17863 const endSn = details.endSN; 17864 const lastPartIndex = details.lastPartIndex; 17865 const hasParts = lastPartIndex !== -1; 17866 const atLastPartOfSegment = lastPartSn === endSn; 17867 if (hasParts) { 17868 // When low latency mode is disabled, request the last part of the next segment 17869 if (atLastPartOfSegment) { 17870 msn = endSn + 1; 17871 part = lowLatencyMode ? 0 : lastPartIndex; 17872 } else { 17873 msn = lastPartSn; 17874 part = lowLatencyMode ? lastPartIndex + 1 : details.maxPartIndex; 17875 } 17876 } else { 17877 msn = endSn + 1; 17878 } 17879 // Low-Latency CDN Tune-in: "age" header and time since load indicates we're behind by more than one part 17880 // Update directives to obtain the Playlist that has the estimated additional duration of media 17881 const lastAdvanced = details.age; 17882 const cdnAge = lastAdvanced + details.ageHeader; 17883 let currentGoal = Math.min(cdnAge - details.partTarget, details.targetduration * 1.5); 17884 if (currentGoal > 0) { 17885 if (cdnAge > details.targetduration * 3) { 17886 // Omit segment and part directives when the last response was more than 3 target durations ago, 17887 this.log(`Playlist last advanced ${lastAdvanced.toFixed(2)}s ago. Omitting segment and part directives.`); 17888 msn = undefined; 17889 part = undefined; 17890 } else if (previousDetails != null && previousDetails.tuneInGoal && cdnAge - details.partTarget > previousDetails.tuneInGoal) { 17891 // If we attempted to get the next or latest playlist update, but currentGoal increased, 17892 // then we either can't catchup, or the "age" header cannot be trusted. 17893 this.warn(`CDN Tune-in goal increased from: ${previousDetails.tuneInGoal} to: ${currentGoal} with playlist age: ${details.age}`); 17894 currentGoal = 0; 17895 } else { 17896 const segments = Math.floor(currentGoal / details.targetduration); 17897 msn += segments; 17898 if (part !== undefined) { 17899 const parts = Math.round(currentGoal % details.targetduration / details.partTarget); 17900 part += parts; 17901 } 17902 this.log(`CDN Tune-in age: ${details.ageHeader}s last advanced ${lastAdvanced.toFixed(2)}s goal: ${currentGoal} skip sn ${segments} to part ${part}`); 17903 } 17904 details.tuneInGoal = currentGoal; 17905 } 17906 deliveryDirectives = this.getDeliveryDirectives(details, data.deliveryDirectives, msn, part); 17907 if (lowLatencyMode || !atLastPartOfSegment) { 17908 details.requestScheduled = now; 17909 this.loadingPlaylist(levelOrTrack, deliveryDirectives); 17910 return; 17911 } 17912 } else if (details.canBlockReload || details.canSkipUntil) { 17913 deliveryDirectives = this.getDeliveryDirectives(details, data.deliveryDirectives, msn, part); 17914 } 17915 if (deliveryDirectives && msn !== undefined && details.canBlockReload) { 17916 details.requestScheduled = stats.loading.first + Math.max(reloadInterval - elapsed * 2, reloadInterval / 2); 17917 } 17918 this.scheduleLoading(levelOrTrack, deliveryDirectives, details); 17919 } else { 17920 this.clearTimer(); 17921 } 17922 } 17923 scheduleLoading(levelOrTrack, deliveryDirectives, updatedDetails) { 17924 const details = updatedDetails || levelOrTrack.details; 17925 if (!details) { 17926 this.loadingPlaylist(levelOrTrack, deliveryDirectives); 17927 return; 17928 } 17929 const now = self.performance.now(); 17930 const requestScheduled = details.requestScheduled; 17931 if (now >= requestScheduled) { 17932 this.loadingPlaylist(levelOrTrack, deliveryDirectives); 17933 return; 17934 } 17935 const estimatedTimeUntilUpdate = requestScheduled - now; 17936 this.log(`reload live playlist ${levelOrTrack.name || levelOrTrack.bitrate + 'bps'} in ${Math.round(estimatedTimeUntilUpdate)} ms`); 17937 this.clearTimer(); 17938 this.timer = self.setTimeout(() => this.loadingPlaylist(levelOrTrack, deliveryDirectives), estimatedTimeUntilUpdate); 17939 } 17940 getDeliveryDirectives(details, previousDeliveryDirectives, msn, part) { 17941 let skip = getSkipValue(details); 17942 if (previousDeliveryDirectives != null && previousDeliveryDirectives.skip && details.deltaUpdateFailed) { 17943 msn = previousDeliveryDirectives.msn; 17944 part = previousDeliveryDirectives.part; 17945 skip = HlsSkip.No; 17946 } 17947 return new HlsUrlParameters(msn, part, skip); 17948 } 17949 checkRetry(errorEvent) { 17950 const errorDetails = errorEvent.details; 17951 const isTimeout = isTimeoutError(errorEvent); 17952 const errorAction = errorEvent.errorAction; 17953 const { 17954 action, 17955 retryCount = 0, 17956 retryConfig 17957 } = errorAction || {}; 17958 const retry = !!errorAction && !!retryConfig && (action === NetworkErrorAction.RetryRequest || !errorAction.resolved && action === NetworkErrorAction.SendAlternateToPenaltyBox); 17959 if (retry) { 17960 var _errorEvent$context; 17961 if (retryCount >= retryConfig.maxNumRetry) { 17962 return false; 17963 } 17964 if (isTimeout && (_errorEvent$context = errorEvent.context) != null && _errorEvent$context.deliveryDirectives) { 17965 // The LL-HLS request already timed out so retry immediately 17966 this.warn(`Retrying playlist loading ${retryCount + 1}/${retryConfig.maxNumRetry} after "${errorDetails}" without delivery-directives`); 17967 this.loadPlaylist(); 17968 } else { 17969 const delay = getRetryDelay(retryConfig, retryCount); 17970 // Schedule level/track reload 17971 this.clearTimer(); 17972 this.timer = self.setTimeout(() => this.loadPlaylist(), delay); 17973 this.warn(`Retrying playlist loading ${retryCount + 1}/${retryConfig.maxNumRetry} after "${errorDetails}" in ${delay}ms`); 17974 } 17975 // `levelRetry = true` used to inform other controllers that a retry is happening 17976 errorEvent.levelRetry = true; 17977 errorAction.resolved = true; 17978 } 17979 return retry; 17980 } 17981} 17982 17983function subtitleOptionsIdentical(trackList1, trackList2) { 17984 if (trackList1.length !== trackList2.length) { 17985 return false; 17986 } 17987 for (let i = 0; i < trackList1.length; i++) { 17988 if (!mediaAttributesIdentical(trackList1[i].attrs, trackList2[i].attrs)) { 17989 return false; 17990 } 17991 } 17992 return true; 17993} 17994function mediaAttributesIdentical(attrs1, attrs2, customAttributes) { 17995 // Media options with the same rendition ID must be bit identical 17996 const stableRenditionId = attrs1['STABLE-RENDITION-ID']; 17997 if (stableRenditionId && !customAttributes) { 17998 return stableRenditionId === attrs2['STABLE-RENDITION-ID']; 17999 } 18000 // When rendition ID is not present, compare attributes 18001 return !(customAttributes || ['LANGUAGE', 'NAME', 'CHARACTERISTICS', 'AUTOSELECT', 'DEFAULT', 'FORCED', 'ASSOC-LANGUAGE']).some(subtitleAttribute => attrs1[subtitleAttribute] !== attrs2[subtitleAttribute]); 18002} 18003function subtitleTrackMatchesTextTrack(subtitleTrack, textTrack) { 18004 return textTrack.label.toLowerCase() === subtitleTrack.name.toLowerCase() && (!textTrack.language || textTrack.language.toLowerCase() === (subtitleTrack.lang || '').toLowerCase()); 18005} 18006 18007class AudioTrackController extends BasePlaylistController { 18008 constructor(hls) { 18009 super(hls, 'audio-track-controller'); 18010 this.tracks = []; 18011 this.groupIds = null; 18012 this.tracksInGroup = []; 18013 this.trackId = -1; 18014 this.currentTrack = null; 18015 this.selectDefaultTrack = true; 18016 this.registerListeners(); 18017 } 18018 registerListeners() { 18019 const { 18020 hls 18021 } = this; 18022 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 18023 hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this); 18024 hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this); 18025 hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); 18026 hls.on(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this); 18027 hls.on(Events.ERROR, this.onError, this); 18028 } 18029 unregisterListeners() { 18030 const { 18031 hls 18032 } = this; 18033 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 18034 hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this); 18035 hls.off(Events.LEVEL_LOADING, this.onLevelLoading, this); 18036 hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); 18037 hls.off(Events.AUDIO_TRACK_LOADED, this.onAudioTrackLoaded, this); 18038 hls.off(Events.ERROR, this.onError, this); 18039 } 18040 destroy() { 18041 this.unregisterListeners(); 18042 this.tracks.length = 0; 18043 this.tracksInGroup.length = 0; 18044 this.currentTrack = null; 18045 super.destroy(); 18046 } 18047 onManifestLoading() { 18048 this.tracks = []; 18049 this.tracksInGroup = []; 18050 this.groupIds = null; 18051 this.currentTrack = null; 18052 this.trackId = -1; 18053 this.selectDefaultTrack = true; 18054 } 18055 onManifestParsed(event, data) { 18056 this.tracks = data.audioTracks || []; 18057 } 18058 onAudioTrackLoaded(event, data) { 18059 const { 18060 id, 18061 groupId, 18062 details 18063 } = data; 18064 const trackInActiveGroup = this.tracksInGroup[id]; 18065 if (!trackInActiveGroup || trackInActiveGroup.groupId !== groupId) { 18066 this.warn(`Audio track with id:${id} and group:${groupId} not found in active group ${trackInActiveGroup == null ? void 0 : trackInActiveGroup.groupId}`); 18067 return; 18068 } 18069 const curDetails = trackInActiveGroup.details; 18070 trackInActiveGroup.details = data.details; 18071 this.log(`Audio track ${id} "${trackInActiveGroup.name}" lang:${trackInActiveGroup.lang} group:${groupId} loaded [${details.startSN}-${details.endSN}]`); 18072 if (id === this.trackId) { 18073 this.playlistLoaded(id, data, curDetails); 18074 } 18075 } 18076 onLevelLoading(event, data) { 18077 this.switchLevel(data.level); 18078 } 18079 onLevelSwitching(event, data) { 18080 this.switchLevel(data.level); 18081 } 18082 switchLevel(levelIndex) { 18083 const levelInfo = this.hls.levels[levelIndex]; 18084 if (!levelInfo) { 18085 return; 18086 } 18087 const audioGroups = levelInfo.audioGroups || null; 18088 const currentGroups = this.groupIds; 18089 let currentTrack = this.currentTrack; 18090 if (!audioGroups || (currentGroups == null ? void 0 : currentGroups.length) !== (audioGroups == null ? void 0 : audioGroups.length) || audioGroups != null && audioGroups.some(groupId => (currentGroups == null ? void 0 : currentGroups.indexOf(groupId)) === -1)) { 18091 this.groupIds = audioGroups; 18092 this.trackId = -1; 18093 this.currentTrack = null; 18094 const audioTracks = this.tracks.filter(track => !audioGroups || audioGroups.indexOf(track.groupId) !== -1); 18095 if (audioTracks.length) { 18096 // Disable selectDefaultTrack if there are no default tracks 18097 if (this.selectDefaultTrack && !audioTracks.some(track => track.default)) { 18098 this.selectDefaultTrack = false; 18099 } 18100 // track.id should match hls.audioTracks index 18101 audioTracks.forEach((track, i) => { 18102 track.id = i; 18103 }); 18104 } else if (!currentTrack && !this.tracksInGroup.length) { 18105 // Do not dispatch AUDIO_TRACKS_UPDATED when there were and are no tracks 18106 return; 18107 } 18108 this.tracksInGroup = audioTracks; 18109 18110 // Find preferred track 18111 const audioPreference = this.hls.config.audioPreference; 18112 if (!currentTrack && audioPreference) { 18113 const groupIndex = findMatchingOption(audioPreference, audioTracks, audioMatchPredicate); 18114 if (groupIndex > -1) { 18115 currentTrack = audioTracks[groupIndex]; 18116 } else { 18117 const allIndex = findMatchingOption(audioPreference, this.tracks); 18118 currentTrack = this.tracks[allIndex]; 18119 } 18120 } 18121 18122 // Select initial track 18123 let trackId = this.findTrackId(currentTrack); 18124 if (trackId === -1 && currentTrack) { 18125 trackId = this.findTrackId(null); 18126 } 18127 18128 // Dispatch events and load track if needed 18129 const audioTracksUpdated = { 18130 audioTracks 18131 }; 18132 this.log(`Updating audio tracks, ${audioTracks.length} track(s) found in group(s): ${audioGroups == null ? void 0 : audioGroups.join(',')}`); 18133 this.hls.trigger(Events.AUDIO_TRACKS_UPDATED, audioTracksUpdated); 18134 const selectedTrackId = this.trackId; 18135 if (trackId !== -1 && selectedTrackId === -1) { 18136 this.setAudioTrack(trackId); 18137 } else if (audioTracks.length && selectedTrackId === -1) { 18138 var _this$groupIds; 18139 const error = new Error(`No audio track selected for current audio group-ID(s): ${(_this$groupIds = this.groupIds) == null ? void 0 : _this$groupIds.join(',')} track count: ${audioTracks.length}`); 18140 this.warn(error.message); 18141 this.hls.trigger(Events.ERROR, { 18142 type: ErrorTypes.MEDIA_ERROR, 18143 details: ErrorDetails.AUDIO_TRACK_LOAD_ERROR, 18144 fatal: true, 18145 error 18146 }); 18147 } 18148 } 18149 } 18150 onError(event, data) { 18151 if (data.fatal || !data.context) { 18152 return; 18153 } 18154 if (data.context.type === PlaylistContextType.AUDIO_TRACK && data.context.id === this.trackId && (!this.groupIds || this.groupIds.indexOf(data.context.groupId) !== -1)) { 18155 this.checkRetry(data); 18156 } 18157 } 18158 get allAudioTracks() { 18159 return this.tracks; 18160 } 18161 get audioTracks() { 18162 return this.tracksInGroup; 18163 } 18164 get audioTrack() { 18165 return this.trackId; 18166 } 18167 set audioTrack(newId) { 18168 // If audio track is selected from API then don't choose from the manifest default track 18169 this.selectDefaultTrack = false; 18170 this.setAudioTrack(newId); 18171 } 18172 setAudioOption(audioOption) { 18173 const hls = this.hls; 18174 hls.config.audioPreference = audioOption; 18175 if (audioOption) { 18176 const allAudioTracks = this.allAudioTracks; 18177 this.selectDefaultTrack = false; 18178 if (allAudioTracks.length) { 18179 // First see if current option matches (no switch op) 18180 const currentTrack = this.currentTrack; 18181 if (currentTrack && matchesOption(audioOption, currentTrack, audioMatchPredicate)) { 18182 return currentTrack; 18183 } 18184 // Find option in available tracks (tracksInGroup) 18185 const groupIndex = findMatchingOption(audioOption, this.tracksInGroup, audioMatchPredicate); 18186 if (groupIndex > -1) { 18187 const track = this.tracksInGroup[groupIndex]; 18188 this.setAudioTrack(groupIndex); 18189 return track; 18190 } else if (currentTrack) { 18191 // Find option in nearest level audio group 18192 let searchIndex = hls.loadLevel; 18193 if (searchIndex === -1) { 18194 searchIndex = hls.firstAutoLevel; 18195 } 18196 const switchIndex = findClosestLevelWithAudioGroup(audioOption, hls.levels, allAudioTracks, searchIndex, audioMatchPredicate); 18197 if (switchIndex === -1) { 18198 // could not find matching variant 18199 return null; 18200 } 18201 // and switch level to acheive the audio group switch 18202 hls.nextLoadLevel = switchIndex; 18203 } 18204 if (audioOption.channels || audioOption.audioCodec) { 18205 // Could not find a match with codec / channels predicate 18206 // Find a match without channels or codec 18207 const withoutCodecAndChannelsMatch = findMatchingOption(audioOption, allAudioTracks); 18208 if (withoutCodecAndChannelsMatch > -1) { 18209 return allAudioTracks[withoutCodecAndChannelsMatch]; 18210 } 18211 } 18212 } 18213 } 18214 return null; 18215 } 18216 setAudioTrack(newId) { 18217 const tracks = this.tracksInGroup; 18218 18219 // check if level idx is valid 18220 if (newId < 0 || newId >= tracks.length) { 18221 this.warn(`Invalid audio track id: ${newId}`); 18222 return; 18223 } 18224 this.selectDefaultTrack = false; 18225 const lastTrack = this.currentTrack; 18226 const track = tracks[newId]; 18227 const trackLoaded = track.details && !track.details.live; 18228 if (newId === this.trackId && track === lastTrack && trackLoaded) { 18229 return; 18230 } 18231 this.log(`Switching to audio-track ${newId} "${track.name}" lang:${track.lang} group:${track.groupId} channels:${track.channels}`); 18232 this.trackId = newId; 18233 this.currentTrack = track; 18234 this.hls.trigger(Events.AUDIO_TRACK_SWITCHING, _objectSpread2({}, track)); 18235 // Do not reload track unless live 18236 if (trackLoaded) { 18237 return; 18238 } 18239 const hlsUrlParameters = this.switchParams(track.url, lastTrack == null ? void 0 : lastTrack.details, track.details); 18240 this.loadPlaylist(hlsUrlParameters); 18241 } 18242 findTrackId(currentTrack) { 18243 const audioTracks = this.tracksInGroup; 18244 for (let i = 0; i < audioTracks.length; i++) { 18245 const track = audioTracks[i]; 18246 if (this.selectDefaultTrack && !track.default) { 18247 continue; 18248 } 18249 if (!currentTrack || matchesOption(currentTrack, track, audioMatchPredicate)) { 18250 return i; 18251 } 18252 } 18253 if (currentTrack) { 18254 const { 18255 name, 18256 lang, 18257 assocLang, 18258 characteristics, 18259 audioCodec, 18260 channels 18261 } = currentTrack; 18262 for (let i = 0; i < audioTracks.length; i++) { 18263 const track = audioTracks[i]; 18264 if (matchesOption({ 18265 name, 18266 lang, 18267 assocLang, 18268 characteristics, 18269 audioCodec, 18270 channels 18271 }, track, audioMatchPredicate)) { 18272 return i; 18273 } 18274 } 18275 for (let i = 0; i < audioTracks.length; i++) { 18276 const track = audioTracks[i]; 18277 if (mediaAttributesIdentical(currentTrack.attrs, track.attrs, ['LANGUAGE', 'ASSOC-LANGUAGE', 'CHARACTERISTICS'])) { 18278 return i; 18279 } 18280 } 18281 for (let i = 0; i < audioTracks.length; i++) { 18282 const track = audioTracks[i]; 18283 if (mediaAttributesIdentical(currentTrack.attrs, track.attrs, ['LANGUAGE'])) { 18284 return i; 18285 } 18286 } 18287 } 18288 return -1; 18289 } 18290 loadPlaylist(hlsUrlParameters) { 18291 super.loadPlaylist(); 18292 const audioTrack = this.currentTrack; 18293 if (!this.shouldLoadPlaylist(audioTrack)) { 18294 return; 18295 } 18296 // Do not load audio rendition with URI matching main variant URI 18297 if (useAlternateAudio(audioTrack.url, this.hls)) { 18298 this.scheduleLoading(audioTrack, hlsUrlParameters); 18299 } 18300 } 18301 loadingPlaylist(audioTrack, hlsUrlParameters) { 18302 super.loadingPlaylist(audioTrack, hlsUrlParameters); 18303 const id = audioTrack.id; 18304 const groupId = audioTrack.groupId; 18305 const url = this.getUrlWithDirectives(audioTrack.url, hlsUrlParameters); 18306 const details = audioTrack.details; 18307 const age = details == null ? void 0 : details.age; 18308 this.log(`Loading audio-track ${id} "${audioTrack.name}" lang:${audioTrack.lang} group:${groupId}${(hlsUrlParameters == null ? void 0 : hlsUrlParameters.msn) !== undefined ? ' at sn ' + hlsUrlParameters.msn + ' part ' + hlsUrlParameters.part : ''}${age && details.live ? ' age ' + age.toFixed(1) + (details.type ? ' ' + details.type ||
183080
vendor: 191,085 bytes, lines 18308-23967
18308 : '') : ''} ${url}`); 18309 this.hls.trigger(Events.AUDIO_TRACK_LOADING, { 18310 url, 18311 id, 18312 groupId, 18313 deliveryDirectives: hlsUrlParameters || null, 18314 track: audioTrack 18315 }); 18316 } 18317} 18318 18319class BufferOperationQueue { 18320 constructor(sourceBufferReference) { 18321 this.tracks = void 0; 18322 this.queues = { 18323 video: [], 18324 audio: [], 18325 audiovideo: [] 18326 }; 18327 this.tracks = sourceBufferReference; 18328 } 18329 destroy() { 18330 this.tracks = this.queues = null; 18331 } 18332 append(operation, type, pending) { 18333 if (this.queues === null || this.tracks === null) { 18334 return; 18335 } 18336 const queue = this.queues[type]; 18337 queue.push(operation); 18338 if (queue.length === 1 && !pending) { 18339 this.executeNext(type); 18340 } 18341 } 18342 appendBlocker(type) { 18343 return new Promise(resolve => { 18344 const operation = { 18345 label: 'async-blocker', 18346 execute: resolve, 18347 onStart: () => {}, 18348 onComplete: () => {}, 18349 onError: () => {} 18350 }; 18351 this.append(operation, type); 18352 }); 18353 } 18354 prependBlocker(type) { 18355 return new Promise(resolve => { 18356 if (this.queues) { 18357 const operation = { 18358 label: 'async-blocker-prepend', 18359 execute: resolve, 18360 onStart: () => {}, 18361 onComplete: () => {}, 18362 onError: () => {} 18363 }; 18364 this.queues[type].unshift(operation); 18365 } 18366 }); 18367 } 18368 removeBlockers() { 18369 if (this.queues === null) { 18370 return; 18371 } 18372 [this.queues.video, this.queues.audio, this.queues.audiovideo].forEach(queue => { 18373 var _queue$; 18374 const label = (_queue$ = queue[0]) == null ? void 0 : _queue$.label; 18375 if (label === 'async-blocker' || label === 'async-blocker-prepend') { 18376 queue[0].execute(); 18377 queue.splice(0, 1); 18378 } 18379 }); 18380 } 18381 unblockAudio(op) { 18382 if (this.queues === null) { 18383 return; 18384 } 18385 const queue = this.queues.audio; 18386 if (queue[0] === op) { 18387 this.shiftAndExecuteNext('audio'); 18388 } 18389 } 18390 executeNext(type) { 18391 if (this.queues === null || this.tracks === null) { 18392 return; 18393 } 18394 const queue = this.queues[type]; 18395 if (queue.length) { 18396 const operation = queue[0]; 18397 try { 18398 // Operations are expected to result in an 'updateend' event being fired. If not, the queue will lock. Operations 18399 // which do not end with this event must call _onSBUpdateEnd manually 18400 operation.execute(); 18401 } catch (error) { 18402 var _this$tracks$type; 18403 operation.onError(error); 18404 if (this.queues === null || this.tracks === null) { 18405 return; 18406 } 18407 18408 // Only shift the current operation off, otherwise the updateend handler will do this for us 18409 const sb = (_this$tracks$type = this.tracks[type]) == null ? void 0 : _this$tracks$type.buffer; 18410 if (!(sb != null && sb.updating)) { 18411 this.shiftAndExecuteNext(type); 18412 } 18413 } 18414 } 18415 } 18416 shiftAndExecuteNext(type) { 18417 if (this.queues === null) { 18418 return; 18419 } 18420 this.queues[type].shift(); 18421 this.executeNext(type); 18422 } 18423 current(type) { 18424 var _this$queues; 18425 return ((_this$queues = this.queues) == null ? void 0 : _this$queues[type][0]) || null; 18426 } 18427 toString() { 18428 const { 18429 queues, 18430 tracks 18431 } = this; 18432 if (queues === null || tracks === null) { 18433 return `<destroyed>`; 18434 } 18435 return ` 18436${this.list('video')} 18437${this.list('audio')} 18438${this.list('audiovideo')}}`; 18439 } 18440 list(type) { 18441 var _this$queues2, _this$tracks; 18442 return (_this$queues2 = this.queues) != null && _this$queues2[type] || (_this$tracks = this.tracks) != null && _this$tracks[type] ? `${type}: (${this.listSbInfo(type)}) ${this.listOps(type)}` : ''; 18443 } 18444 listSbInfo(type) { 18445 var _this$tracks2; 18446 const track = (_this$tracks2 = this.tracks) == null ? void 0 : _this$tracks2[type]; 18447 const sb = track == null ? void 0 : track.buffer; 18448 if (!sb) { 18449 return 'none'; 18450 } 18451 return `SourceBuffer${sb.updating ? ' updating' : ''}${track.ended ? ' ended' : ''}${track.ending ? ' ending' : ''}`; 18452 } 18453 listOps(type) { 18454 var _this$queues3; 18455 return ((_this$queues3 = this.queues) == null ? void 0 : _this$queues3[type].map(op => op.label).join(', ')) || ''; 18456 } 18457} 18458 18459const VIDEO_CODEC_PROFILE_REPLACE = /(avc[1234]|hvc1|hev1|dvh[1e]|vp09|av01)(?:\.[^.,]+)+/; 18460const TRACK_REMOVED_ERROR_NAME = 'HlsJsTrackRemovedError'; 18461class HlsJsTrackRemovedError extends Error { 18462 constructor(message) { 18463 super(message); 18464 this.name = TRACK_REMOVED_ERROR_NAME; 18465 } 18466} 18467class BufferController extends Logger { 18468 constructor(hls, fragmentTracker) { 18469 super('buffer-controller', hls.logger); 18470 this.hls = void 0; 18471 this.fragmentTracker = void 0; 18472 // The level details used to determine duration, target-duration and live 18473 this.details = null; 18474 // cache the self generated object url to detect hijack of video tag 18475 this._objectUrl = null; 18476 // A queue of buffer operations which require the SourceBuffer to not be updating upon execution 18477 this.operationQueue = null; 18478 // The total number track codecs expected before any sourceBuffers are created (2: audio and video or 1: audiovideo | audio | video) 18479 this.bufferCodecEventsTotal = 0; 18480 // A reference to the attached media element 18481 this.media = null; 18482 // A reference to the active media source 18483 this.mediaSource = null; 18484 // Last MP3 audio chunk appended 18485 this.lastMpegAudioChunk = null; 18486 // Audio fragment blocked from appending until corresponding video appends or context changes 18487 this.blockedAudioAppend = null; 18488 // Keep track of video append position for unblocking audio 18489 this.lastVideoAppendEnd = 0; 18490 // Whether or not to use ManagedMediaSource API and append source element to media element. 18491 this.appendSource = void 0; 18492 // Transferred MediaSource information used to detmerine if duration end endstream may be appended 18493 this.transferData = void 0; 18494 // Directives used to override default MediaSource handling 18495 this.overrides = void 0; 18496 // Error counters 18497 this.appendErrors = { 18498 audio: 0, 18499 video: 0, 18500 audiovideo: 0 18501 }; 18502 // Record of required or created buffers by type. SourceBuffer is stored in Track.buffer once created. 18503 this.tracks = {}; 18504 // Array of SourceBuffer type and SourceBuffer (or null). One entry per TrackSet in this.tracks. 18505 this.sourceBuffers = [[null, null], [null, null]]; 18506 this._onEndStreaming = event => { 18507 var _this$mediaSource; 18508 if (!this.hls) { 18509 return; 18510 } 18511 if (((_this$mediaSource = this.mediaSource) == null ? void 0 : _this$mediaSource.readyState) !== 'open') { 18512 return; 18513 } 18514 this.hls.pauseBuffering(); 18515 }; 18516 this._onStartStreaming = event => { 18517 if (!this.hls) { 18518 return; 18519 } 18520 this.hls.resumeBuffering(); 18521 }; 18522 // Keep as arrow functions so that we can directly reference these functions directly as event listeners 18523 this._onMediaSourceOpen = e => { 18524 const { 18525 media, 18526 mediaSource 18527 } = this; 18528 if (e) { 18529 this.log('Media source opened'); 18530 } 18531 if (!media || !mediaSource) { 18532 return; 18533 } 18534 // once received, don't listen anymore to sourceopen event 18535 mediaSource.removeEventListener('sourceopen', this._onMediaSourceOpen); 18536 media.removeEventListener('emptied', this._onMediaEmptied); 18537 this.updateDuration(); 18538 this.hls.trigger(Events.MEDIA_ATTACHED, { 18539 media, 18540 mediaSource: mediaSource 18541 }); 18542 if (this.mediaSource !== null) { 18543 this.checkPendingTracks(); 18544 } 18545 }; 18546 this._onMediaSourceClose = () => { 18547 this.log('Media source closed'); 18548 }; 18549 this._onMediaSourceEnded = () => { 18550 this.log('Media source ended'); 18551 }; 18552 this._onMediaEmptied = () => { 18553 const { 18554 mediaSrc, 18555 _objectUrl 18556 } = this; 18557 if (mediaSrc !== _objectUrl) { 18558 this.error(`Media element src was set while attaching MediaSource (${_objectUrl} > ${mediaSrc})`); 18559 } 18560 }; 18561 this.hls = hls; 18562 this.fragmentTracker = fragmentTracker; 18563 this.appendSource = isManagedMediaSource(getMediaSource(hls.config.preferManagedMediaSource)); 18564 this.initTracks(); 18565 this.registerListeners(); 18566 } 18567 hasSourceTypes() { 18568 return Object.keys(this.tracks).length > 0; 18569 } 18570 destroy() { 18571 this.unregisterListeners(); 18572 this.details = null; 18573 this.lastMpegAudioChunk = this.blockedAudioAppend = null; 18574 this.transferData = this.overrides = undefined; 18575 if (this.operationQueue) { 18576 this.operationQueue.destroy(); 18577 this.operationQueue = null; 18578 } 18579 // @ts-ignore 18580 this.hls = this.fragmentTracker = null; 18581 // @ts-ignore 18582 this._onMediaSourceOpen = this._onMediaSourceClose = null; 18583 // @ts-ignore 18584 this._onMediaSourceEnded = null; 18585 // @ts-ignore 18586 this._onStartStreaming = this._onEndStreaming = null; 18587 } 18588 registerListeners() { 18589 const { 18590 hls 18591 } = this; 18592 hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 18593 hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 18594 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 18595 hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this); 18596 hls.on(Events.BUFFER_RESET, this.onBufferReset, this); 18597 hls.on(Events.BUFFER_APPENDING, this.onBufferAppending, this); 18598 hls.on(Events.BUFFER_CODECS, this.onBufferCodecs, this); 18599 hls.on(Events.BUFFER_EOS, this.onBufferEos, this); 18600 hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 18601 hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this); 18602 hls.on(Events.FRAG_PARSED, this.onFragParsed, this); 18603 hls.on(Events.FRAG_CHANGED, this.onFragChanged, this); 18604 hls.on(Events.ERROR, this.onError, this); 18605 } 18606 unregisterListeners() { 18607 const { 18608 hls 18609 } = this; 18610 hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 18611 hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 18612 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 18613 hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this); 18614 hls.off(Events.BUFFER_RESET, this.onBufferReset, this); 18615 hls.off(Events.BUFFER_APPENDING, this.onBufferAppending, this); 18616 hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this); 18617 hls.off(Events.BUFFER_EOS, this.onBufferEos, this); 18618 hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 18619 hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this); 18620 hls.off(Events.FRAG_PARSED, this.onFragParsed, this); 18621 hls.off(Events.FRAG_CHANGED, this.onFragChanged, this); 18622 hls.off(Events.ERROR, this.onError, this); 18623 } 18624 transferMedia() { 18625 const { 18626 media, 18627 mediaSource 18628 } = this; 18629 if (!media) { 18630 return null; 18631 } 18632 const tracks = {}; 18633 if (this.operationQueue) { 18634 const updating = this.isUpdating(); 18635 if (!updating) { 18636 this.operationQueue.removeBlockers(); 18637 } 18638 const queued = this.isQueued(); 18639 if (updating || queued) { 18640 this.warn(`Transfering MediaSource with${queued ? ' operations in queue' : ''}${updating ? ' updating SourceBuffer(s)' : ''} ${this.operationQueue}`); 18641 } 18642 this.operationQueue.destroy(); 18643 } 18644 const transferData = this.transferData; 18645 if (!this.sourceBufferCount && transferData && transferData.mediaSource === mediaSource) { 18646 _extends(tracks, transferData.tracks); 18647 } else { 18648 this.sourceBuffers.forEach(tuple => { 18649 const [type] = tuple; 18650 if (type) { 18651 tracks[type] = _extends({}, this.tracks[type]); 18652 this.removeBuffer(type); 18653 } 18654 tuple[0] = tuple[1] = null; 18655 }); 18656 } 18657 return { 18658 media, 18659 mediaSource, 18660 tracks 18661 }; 18662 } 18663 initTracks() { 18664 const tracks = {}; 18665 this.sourceBuffers = [[null, null], [null, null]]; 18666 this.tracks = tracks; 18667 this.resetQueue(); 18668 this.resetAppendErrors(); 18669 this.lastMpegAudioChunk = this.blockedAudioAppend = null; 18670 this.lastVideoAppendEnd = 0; 18671 } 18672 onManifestLoading() { 18673 this.bufferCodecEventsTotal = 0; 18674 this.details = null; 18675 } 18676 onManifestParsed(event, data) { 18677 var _this$transferData; 18678 // in case of alt audio 2 BUFFER_CODECS events will be triggered, one per stream controller 18679 // sourcebuffers will be created all at once when the expected nb of tracks will be reached 18680 // in case alt audio is not used, only one BUFFER_CODEC event will be fired from main stream controller 18681 // it will contain the expected nb of source buffers, no need to compute it 18682 let codecEvents = 2; 18683 if (data.audio && !data.video || !data.altAudio) { 18684 codecEvents = 1; 18685 } 18686 this.bufferCodecEventsTotal = codecEvents; 18687 this.log(`${codecEvents} bufferCodec event(s) expected.`); 18688 if ((_this$transferData = this.transferData) != null && _this$transferData.mediaSource && this.sourceBufferCount && codecEvents) { 18689 this.bufferCreated(); 18690 } 18691 } 18692 onMediaAttaching(event, data) { 18693 const media = this.media = data.media; 18694 this.transferData = this.overrides = undefined; 18695 const MediaSource = getMediaSource(this.appendSource); 18696 if (MediaSource) { 18697 const transferringMedia = !!data.mediaSource; 18698 if (transferringMedia || data.overrides) { 18699 this.transferData = data; 18700 this.overrides = data.overrides; 18701 } 18702 const ms = this.mediaSource = data.mediaSource || new MediaSource(); 18703 this.assignMediaSource(ms); 18704 if (transferringMedia) { 18705 this._objectUrl = media.src; 18706 this.attachTransferred(); 18707 } else { 18708 // cache the locally generated object url 18709 const objectUrl = this._objectUrl = self.URL.createObjectURL(ms); 18710 // link video and media Source 18711 if (this.appendSource) { 18712 try { 18713 media.removeAttribute('src'); 18714 // ManagedMediaSource will not open without disableRemotePlayback set to false or source alternatives 18715 const MMS = self.ManagedMediaSource; 18716 media.disableRemotePlayback = media.disableRemotePlayback || MMS && ms instanceof MMS; 18717 removeSourceChildren(media); 18718 addSource(media, objectUrl); 18719 media.load(); 18720 } catch (error) { 18721 media.src = objectUrl; 18722 } 18723 } else { 18724 media.src = objectUrl; 18725 } 18726 } 18727 media.addEventListener('emptied', this._onMediaEmptied); 18728 } 18729 } 18730 assignMediaSource(ms) { 18731 var _this$transferData2, _ms$constructor; 18732 this.log(`${((_this$transferData2 = this.transferData) == null ? void 0 : _this$transferData2.mediaSource) === ms ? 'transferred' : 'created'} media source: ${(_ms$constructor = ms.constructor) == null ? void 0 : _ms$constructor.name}`); 18733 // MediaSource listeners are arrow functions with a lexical scope, and do not need to be bound 18734 ms.addEventListener('sourceopen', this._onMediaSourceOpen); 18735 ms.addEventListener('sourceended', this._onMediaSourceEnded); 18736 ms.addEventListener('sourceclose', this._onMediaSourceClose); 18737 if (this.appendSource) { 18738 ms.addEventListener('startstreaming', this._onStartStreaming); 18739 ms.addEventListener('endstreaming', this._onEndStreaming); 18740 } 18741 } 18742 attachTransferred() { 18743 const media = this.media; 18744 const data = this.transferData; 18745 if (!data || !media) { 18746 return; 18747 } 18748 const requiredTracks = this.tracks; 18749 const transferredTracks = data.tracks; 18750 const trackNames = transferredTracks ? Object.keys(transferredTracks) : null; 18751 const trackCount = trackNames ? trackNames.length : 0; 18752 const mediaSourceOpenCallback = () => { 18753 // eslint-disable-next-line @typescript-eslint/no-floating-promises 18754 Promise.resolve().then(() => { 18755 if (this.media && this.mediaSourceOpenOrEnded) { 18756 this._onMediaSourceOpen(); 18757 } 18758 }); 18759 }; 18760 if (transferredTracks && trackNames && trackCount) { 18761 if (!this.tracksReady) { 18762 // Wait for CODECS event(s) 18763 this.hls.config.startFragPrefetch = true; 18764 this.log(`attachTransferred: waiting for SourceBuffer track info`); 18765 return; 18766 } 18767 this.log(`attachTransferred: (bufferCodecEventsTotal ${this.bufferCodecEventsTotal}) 18768required tracks: ${stringify(requiredTracks, (key, value) => key === 'initSegment' ? undefined : value)}; 18769transfer tracks: ${stringify(transferredTracks, (key, value) => key === 'initSegment' ? undefined : value)}}`); 18770 if (!isCompatibleTrackChange(transferredTracks, requiredTracks)) { 18771 // destroy attaching media source 18772 data.mediaSource = null; 18773 data.tracks = undefined; 18774 const currentTime = media.currentTime; 18775 const details = this.details; 18776 const startTime = Math.max(currentTime, (details == null ? void 0 : details.fragments[0].start) || 0); 18777 if (startTime - currentTime > 1) { 18778 this.log(`attachTransferred: waiting for playback to reach new tracks start time ${currentTime} -> ${startTime}`); 18779 return; 18780 } 18781 this.warn(`attachTransferred: resetting MediaSource for incompatible tracks ("${Object.keys(transferredTracks)}"->"${Object.keys(requiredTracks)}") start time: ${startTime} currentTime: ${currentTime}`); 18782 this.onMediaDetaching(Events.MEDIA_DETACHING, {}); 18783 this.onMediaAttaching(Events.MEDIA_ATTACHING, data); 18784 media.currentTime = startTime; 18785 return; 18786 } 18787 this.transferData = undefined; 18788 trackNames.forEach(trackName => { 18789 const type = trackName; 18790 const track = transferredTracks[type]; 18791 if (track) { 18792 const sb = track.buffer; 18793 if (sb) { 18794 // Purge fragment tracker of ejected segments for existing buffer 18795 const fragmentTracker = this.fragmentTracker; 18796 const playlistType = track.id; 18797 if (fragmentTracker.hasFragments(playlistType) || fragmentTracker.hasParts(playlistType)) { 18798 const bufferedTimeRanges = BufferHelper.getBuffered(sb); 18799 fragmentTracker.detectEvictedFragments(type, bufferedTimeRanges, playlistType, null, true); 18800 } 18801 // Transfer SourceBuffer 18802 const sbIndex = sourceBufferNameToIndex(type); 18803 const sbTuple = [type, sb]; 18804 this.sourceBuffers[sbIndex] = sbTuple; 18805 if (sb.updating && this.operationQueue) { 18806 // eslint-disable-next-line @typescript-eslint/no-floating-promises 18807 this.operationQueue.prependBlocker(type); 18808 } 18809 this.trackSourceBuffer(type, track); 18810 } 18811 } 18812 }); 18813 mediaSourceOpenCallback(); 18814 this.bufferCreated(); 18815 } else { 18816 this.log(`attachTransferred: MediaSource w/o SourceBuffers`); 18817 mediaSourceOpenCallback(); 18818 } 18819 } 18820 get mediaSourceOpenOrEnded() { 18821 var _this$mediaSource2; 18822 const readyState = (_this$mediaSource2 = this.mediaSource) == null ? void 0 : _this$mediaSource2.readyState; 18823 return readyState === 'open' || readyState === 'ended'; 18824 } 18825 onMediaDetaching(event, data) { 18826 const transferringMedia = !!data.transferMedia; 18827 this.transferData = this.overrides = undefined; 18828 const { 18829 media, 18830 mediaSource, 18831 _objectUrl 18832 } = this; 18833 if (mediaSource) { 18834 this.log(`media source ${transferringMedia ? 'transferring' : 'detaching'}`); 18835 if (transferringMedia) { 18836 // Detach SourceBuffers without removing from MediaSource 18837 // and leave `tracks` (required SourceBuffers configuration) 18838 this.sourceBuffers.forEach(([type]) => { 18839 if (type) { 18840 this.removeBuffer(type); 18841 } 18842 }); 18843 this.resetQueue(); 18844 } else { 18845 if (this.mediaSourceOpenOrEnded) { 18846 const open = mediaSource.readyState === 'open'; 18847 try { 18848 const sourceBuffers = mediaSource.sourceBuffers; 18849 for (let i = sourceBuffers.length; i--;) { 18850 if (open) { 18851 sourceBuffers[i].abort(); 18852 } 18853 mediaSource.removeSourceBuffer(sourceBuffers[i]); 18854 } 18855 if (open) { 18856 // endOfStream could trigger exception if any sourcebuffer is in updating state 18857 // we don't really care about checking sourcebuffer state here, 18858 // as we are anyway detaching the MediaSource 18859 // let's just avoid this exception to propagate 18860 mediaSource.endOfStream(); 18861 } 18862 } catch (err) { 18863 this.warn(`onMediaDetaching: ${err.message} while calling endOfStream`); 18864 } 18865 } 18866 // Clean up the SourceBuffers by invoking onBufferReset 18867 if (this.sourceBufferCount) { 18868 this.onBufferReset(); 18869 } 18870 } 18871 mediaSource.removeEventListener('sourceopen', this._onMediaSourceOpen); 18872 mediaSource.removeEventListener('sourceended', this._onMediaSourceEnded); 18873 mediaSource.removeEventListener('sourceclose', this._onMediaSourceClose); 18874 if (this.appendSource) { 18875 mediaSource.removeEventListener('startstreaming', this._onStartStreaming); 18876 mediaSource.removeEventListener('endstreaming', this._onEndStreaming); 18877 } 18878 this.mediaSource = null; 18879 this._objectUrl = null; 18880 } 18881 18882 // Detach properly the MediaSource from the HTMLMediaElement as 18883 // suggested in https://github.com/w3c/media-source/issues/53. 18884 if (media) { 18885 media.removeEventListener('emptied', this._onMediaEmptied); 18886 if (!transferringMedia) { 18887 if (_objectUrl) { 18888 self.URL.revokeObjectURL(_objectUrl); 18889 } 18890 18891 // clean up video tag src only if it's our own url. some external libraries might 18892 // hijack the video tag and change its 'src' without destroying the Hls instance first 18893 if (this.mediaSrc === _objectUrl) { 18894 media.removeAttribute('src'); 18895 if (this.appendSource) { 18896 removeSourceChildren(media); 18897 } 18898 media.load(); 18899 } else { 18900 this.warn('media|source.src was changed by a third party - skip cleanup'); 18901 } 18902 } 18903 this.media = null; 18904 } 18905 this.hls.trigger(Events.MEDIA_DETACHED, data); 18906 } 18907 onBufferReset() { 18908 this.sourceBuffers.forEach(([type]) => { 18909 if (type) { 18910 this.resetBuffer(type); 18911 } 18912 }); 18913 this.initTracks(); 18914 } 18915 resetBuffer(type) { 18916 var _this$tracks$type; 18917 const sb = (_this$tracks$type = this.tracks[type]) == null ? void 0 : _this$tracks$type.buffer; 18918 this.removeBuffer(type); 18919 if (sb) { 18920 try { 18921 var _this$mediaSource3; 18922 if ((_this$mediaSource3 = this.mediaSource) != null && _this$mediaSource3.sourceBuffers.length) { 18923 this.mediaSource.removeSourceBuffer(sb); 18924 } 18925 } catch (err) { 18926 this.warn(`onBufferReset ${type}`, err); 18927 } 18928 } 18929 delete this.tracks[type]; 18930 } 18931 removeBuffer(type) { 18932 this.removeBufferListeners(type); 18933 this.sourceBuffers[sourceBufferNameToIndex(type)] = [null, null]; 18934 const track = this.tracks[type]; 18935 if (track) { 18936 track.buffer = undefined; 18937 } 18938 } 18939 resetQueue() { 18940 if (this.operationQueue) { 18941 this.operationQueue.destroy(); 18942 } 18943 this.operationQueue = new BufferOperationQueue(this.tracks); 18944 } 18945 onBufferCodecs(event, data) { 18946 var _data$audio; 18947 const tracks = this.tracks; 18948 const trackNames = Object.keys(data); 18949 this.log(`BUFFER_CODECS: "${trackNames}" (current SB count ${this.sourceBufferCount})`); 18950 const unmuxedToMuxed = 'audiovideo' in data && (tracks.audio || tracks.video) || tracks.audiovideo && ('audio' in data || 'video' in data); 18951 const muxedToUnmuxed = !unmuxedToMuxed && this.sourceBufferCount && this.media && trackNames.some(sbName => !tracks[sbName]); 18952 if (unmuxedToMuxed || muxedToUnmuxed) { 18953 this.warn(`Unsupported transition between "${Object.keys(tracks)}" and "${trackNames}" SourceBuffers`); 18954 // Do not add incompatible track ('audiovideo' <-> 'video'/'audio'). 18955 // Allow following onBufferAppending handle to trigger BUFFER_APPEND_ERROR. 18956 // This will either be resolved by level switch or could be handled with recoverMediaError(). 18957 return; 18958 } 18959 trackNames.forEach(trackName => { 18960 var _this$transferData3, _trackCodec; 18961 const parsedTrack = data[trackName]; 18962 const { 18963 id, 18964 codec, 18965 levelCodec, 18966 container, 18967 metadata, 18968 supplemental 18969 } = parsedTrack; 18970 let track = tracks[trackName]; 18971 const transferredTrack = (_this$transferData3 = this.transferData) == null || (_this$transferData3 = _this$transferData3.tracks) == null ? void 0 : _this$transferData3[trackName]; 18972 const sbTrack = transferredTrack != null && transferredTrack.buffer ? transferredTrack : track; 18973 const sbCodec = (sbTrack == null ? void 0 : sbTrack.pendingCodec) || (sbTrack == null ? void 0 : sbTrack.codec); 18974 const trackLevelCodec = sbTrack == null ? void 0 : sbTrack.levelCodec; 18975 if (!track) { 18976 track = tracks[trackName] = { 18977 buffer: undefined, 18978 listeners: [], 18979 codec, 18980 supplemental, 18981 container, 18982 levelCodec, 18983 metadata, 18984 id 18985 }; 18986 } 18987 // check if SourceBuffer codec needs to change 18988 const currentCodecFull = pickMostCompleteCodecName(sbCodec, trackLevelCodec); 18989 const currentCodec = currentCodecFull == null ? void 0 : currentCodecFull.replace(VIDEO_CODEC_PROFILE_REPLACE, '$1'); 18990 let trackCodec = pickMostCompleteCodecName(codec, levelCodec); 18991 const nextCodec = (_trackCodec = trackCodec) == null ? void 0 : _trackCodec.replace(VIDEO_CODEC_PROFILE_REPLACE, '$1'); 18992 if (trackCodec && currentCodecFull && currentCodec !== nextCodec) { 18993 if (trackName.slice(0, 5) === 'audio') { 18994 trackCodec = getCodecCompatibleName(trackCodec, this.appendSource); 18995 } 18996 this.log(`switching codec ${sbCodec} to ${trackCodec}`); 18997 if (trackCodec !== (track.pendingCodec || track.codec)) { 18998 track.pendingCodec = trackCodec; 18999 } 19000 track.container = container; 19001 this.appendChangeType(trackName, container, trackCodec); 19002 } 19003 }); 19004 if (this.tracksReady || this.sourceBufferCount) { 19005 data.tracks = this.sourceBufferTracks; 19006 } 19007 19008 // if sourcebuffers already created, do nothing ... 19009 if (this.sourceBufferCount) { 19010 return; 19011 } 19012 if (this.bufferCodecEventsTotal > 1 && !this.tracks.video && !data.video && ((_data$audio = data.audio) == null ? void 0 : _data$audio.id) === 'main') { 19013 // MVP is missing CODECS and only audio was found in main segment (#7524) 19014 this.log(`Main audio-only`); 19015 this.bufferCodecEventsTotal = 1; 19016 } 19017 if (this.mediaSourceOpenOrEnded) { 19018 this.checkPendingTracks(); 19019 } 19020 } 19021 get sourceBufferTracks() { 19022 return Object.keys(this.tracks).reduce((baseTracks, type) => { 19023 const track = this.tracks[type]; 19024 baseTracks[type] = { 19025 id: track.id, 19026 container: track.container, 19027 codec: track.codec, 19028 levelCodec: track.levelCodec 19029 }; 19030 return baseTracks; 19031 }, {}); 19032 } 19033 appendChangeType(type, container, codec) { 19034 const mimeType = `${container};codecs=${codec}`; 19035 const operation = { 19036 label: `change-type=${mimeType}`, 19037 execute: () => { 19038 const track = this.tracks[type]; 19039 if (track) { 19040 const sb = track.buffer; 19041 if (sb != null && sb.changeType) { 19042 this.log(`changing ${type} sourceBuffer type to ${mimeType}`); 19043 sb.changeType(mimeType); 19044 track.codec = codec; 19045 track.container = container; 19046 } 19047 } 19048 this.shiftAndExecuteNext(type); 19049 }, 19050 onStart: () => {}, 19051 onComplete: () => {}, 19052 onError: error => { 19053 this.warn(`Failed to change ${type} SourceBuffer type`, error); 19054 } 19055 }; 19056 this.append(operation, type, this.isPending(this.tracks[type])); 19057 } 19058 blockAudio(partOrFrag) { 19059 var _this$fragmentTracker; 19060 const pStart = partOrFrag.start; 19061 const pTime = pStart + partOrFrag.duration * 0.05; 19062 const atGap = ((_this$fragmentTracker = this.fragmentTracker.getAppendedFrag(pStart, PlaylistLevelType.MAIN)) == null ? void 0 : _this$fragmentTracker.gap) === true; 19063 if (atGap) { 19064 return; 19065 } 19066 const op = { 19067 label: 'block-audio', 19068 execute: () => { 19069 var _this$fragmentTracker2; 19070 const videoTrack = this.tracks.video; 19071 if (this.lastVideoAppendEnd > pTime || videoTrack != null && videoTrack.buffer && BufferHelper.isBuffered(videoTrack.buffer, pTime) || ((_this$fragmentTracker2 = this.fragmentTracker.getAppendedFrag(pTime, PlaylistLevelType.MAIN)) == null ? void 0 : _this$fragmentTracker2.gap) === true) { 19072 this.blockedAudioAppend = null; 19073 this.shiftAndExecuteNext('audio'); 19074 } 19075 }, 19076 onStart: () => {}, 19077 onComplete: () => {}, 19078 onError: error => { 19079 this.warn('Error executing block-audio operation', error); 19080 } 19081 }; 19082 this.blockedAudioAppend = { 19083 op, 19084 frag: partOrFrag 19085 }; 19086 this.append(op, 'audio', true); 19087 } 19088 unblockAudio() { 19089 const { 19090 blockedAudioAppend, 19091 operationQueue 19092 } = this; 19093 if (blockedAudioAppend && operationQueue) { 19094 this.blockedAudioAppend = null; 19095 operationQueue.unblockAudio(blockedAudioAppend.op); 19096 } 19097 } 19098 onBufferAppending(event, eventData) { 19099 const { 19100 tracks 19101 } = this; 19102 const { 19103 data, 19104 type, 19105 parent, 19106 frag, 19107 part, 19108 chunkMeta, 19109 offset 19110 } = eventData; 19111 const chunkStats = chunkMeta.buffering[type]; 19112 const { 19113 sn, 19114 cc 19115 } = frag; 19116 const bufferAppendingStart = self.performance.now(); 19117 chunkStats.start = bufferAppendingStart; 19118 const fragBuffering = frag.stats.buffering; 19119 const partBuffering = part ? part.stats.buffering : null; 19120 if (fragBuffering.start === 0) { 19121 fragBuffering.start = bufferAppendingStart; 19122 } 19123 if (partBuffering && partBuffering.start === 0) { 19124 partBuffering.start = bufferAppendingStart; 19125 } 19126 19127 // TODO: Only update timestampOffset when audio/mpeg fragment or part is not contiguous with previously appended 19128 // Adjusting `SourceBuffer.timestampOffset` (desired point in the timeline where the next frames should be appended) 19129 // in Chrome browser when we detect MPEG audio container and time delta between level PTS and `SourceBuffer.timestampOffset` 19130 // is greater than 100ms (this is enough to handle seek for VOD or level change for LIVE videos). 19131 // More info here: https://github.com/video-dev/hls.js/issues/332#issuecomment-257986486 19132 const audioTrack = tracks.audio; 19133 let checkTimestampOffset = false; 19134 if (type === 'audio' && (audioTrack == null ? void 0 : audioTrack.container) === 'audio/mpeg') { 19135 checkTimestampOffset = !this.lastMpegAudioChunk || chunkMeta.id === 1 || this.lastMpegAudioChunk.sn !== chunkMeta.sn; 19136 this.lastMpegAudioChunk = chunkMeta; 19137 } 19138 19139 // Block audio append until overlapping video append 19140 const videoTrack = tracks.video; 19141 const videoSb = videoTrack == null ? void 0 : videoTrack.buffer; 19142 if (videoSb && sn !== 'initSegment') { 19143 const partOrFrag = part || frag; 19144 const blockedAudioAppend = this.blockedAudioAppend; 19145 if (type === 'audio' && parent !== 'main' && !this.blockedAudioAppend && !(videoTrack.ending || videoTrack.ended)) { 19146 const pStart = partOrFrag.start; 19147 const pTime = pStart + partOrFrag.duration * 0.05; 19148 const vbuffered = videoSb.buffered; 19149 const vappending = this.currentOp('video'); 19150 if (!vbuffered.length && !vappending) { 19151 // wait for video before appending audio 19152 this.blockAudio(partOrFrag); 19153 } else if (!vappending && !BufferHelper.isBuffered(videoSb, pTime) && this.lastVideoAppendEnd < pTime) { 19154 // audio is ahead of video 19155 this.blockAudio(partOrFrag); 19156 } 19157 } else if (type === 'video') { 19158 const videoAppendEnd = partOrFrag.end; 19159 if (blockedAudioAppend) { 19160 const audioStart = blockedAudioAppend.frag.start; 19161 if (videoAppendEnd > audioStart || videoAppendEnd < this.lastVideoAppendEnd || BufferHelper.isBuffered(videoSb, audioStart)) { 19162 this.unblockAudio(); 19163 } 19164 } 19165 this.lastVideoAppendEnd = videoAppendEnd; 19166 } 19167 } 19168 const fragStart = (part || frag).start; 19169 const operation = { 19170 label: `append-${type}`, 19171 execute: () => { 19172 var _this$tracks$type2; 19173 chunkStats.executeStart = self.performance.now(); 19174 const sb = (_this$tracks$type2 = this.tracks[type]) == null ? void 0 : _this$tracks$type2.buffer; 19175 if (sb) { 19176 if (checkTimestampOffset) { 19177 this.updateTimestampOffset(sb, fragStart, 0.1, type, sn, cc); 19178 } else if (offset !== undefined && isFiniteNumber(offset)) { 19179 this.updateTimestampOffset(sb, offset, 0.000001, type, sn, cc); 19180 } 19181 } 19182 this.appendExecutor(data, type); 19183 }, 19184 onStart: () => { 19185 // logger.debug(`[buffer-controller]: ${type} SourceBuffer updatestart`); 19186 }, 19187 onComplete: () => { 19188 // logger.debug(`[buffer-controller]: ${type} SourceBuffer updateend`); 19189 const end = self.performance.now(); 19190 chunkStats.executeEnd = chunkStats.end = end; 19191 if (fragBuffering.first === 0) { 19192 fragBuffering.first = end; 19193 } 19194 if (partBuffering && partBuffering.first === 0) { 19195 partBuffering.first = end; 19196 } 19197 const timeRanges = {}; 19198 this.sourceBuffers.forEach(([type, sb]) => { 19199 if (type) { 19200 timeRanges[type] = BufferHelper.getBuffered(sb); 19201 } 19202 }); 19203 this.appendErrors[type] = 0; 19204 if (type === 'audio' || type === 'video') { 19205 this.appendErrors.audiovideo = 0; 19206 } else { 19207 this.appendErrors.audio = 0; 19208 this.appendErrors.video = 0; 19209 } 19210 this.hls.trigger(Events.BUFFER_APPENDED, { 19211 type, 19212 frag, 19213 part, 19214 chunkMeta, 19215 parent: frag.type, 19216 timeRanges 19217 }); 19218 }, 19219 onError: error => { 19220 var _this$media; 19221 // in case any error occured while appending, put back segment in segments table 19222 const event = { 19223 type: ErrorTypes.MEDIA_ERROR, 19224 parent: frag.type, 19225 details: ErrorDetails.BUFFER_APPEND_ERROR, 19226 sourceBufferName: type, 19227 frag, 19228 part, 19229 chunkMeta, 19230 error, 19231 err: error, 19232 fatal: false 19233 }; 19234 const mediaError = (_this$media = this.media) == null ? void 0 : _this$media.error; 19235 if (error.code === DOMException.QUOTA_EXCEEDED_ERR || error.name == 'QuotaExceededError' || `quota` in error) { 19236 // QuotaExceededError: http://www.w3.org/TR/html5/infrastructure.html#quotaexceedederror 19237 // let's stop appending any segments, and report BUFFER_FULL_ERROR error 19238 event.details = ErrorDetails.BUFFER_FULL_ERROR; 19239 } else if (error.code === DOMException.INVALID_STATE_ERR && this.mediaSourceOpenOrEnded && !mediaError) { 19240 // Allow retry for "Failed to execute 'appendBuffer' on 'SourceBuffer': This SourceBuffer is still processing" errors 19241 event.errorAction = createDoNothingErrorAction(true); 19242 } else if (error.name === TRACK_REMOVED_ERROR_NAME && this.sourceBufferCount === 0) { 19243 // Do nothing if sourceBuffers were removed (media is detached and append was not aborted) 19244 event.errorAction = createDoNothingErrorAction(true); 19245 } else { 19246 const appendErrorCount = ++this.appendErrors[type]; 19247 /* with UHD content, we could get loop of quota exceeded error until 19248 browser is able to evict some data from sourcebuffer. Retrying can help recover. 19249 */ 19250 this.warn(`Failed ${appendErrorCount}/${this.hls.config.appendErrorMaxRetry} times to append segment in "${type}" sourceBuffer (${mediaError ? mediaError : 'no media error'})`); 19251 if (appendErrorCount >= this.hls.config.appendErrorMaxRetry || !!mediaError) { 19252 event.fatal = true; 19253 } 19254 } 19255 this.hls.trigger(Events.ERROR, event); 19256 } 19257 }; 19258 this.log(`queuing "${type}" append sn: ${sn}${part ? ' p: ' + part.index : ''} of ${frag.type === PlaylistLevelType.MAIN ? 'level' : 'track'} ${frag.level} cc: ${cc}`); 19259 this.append(operation, type, this.isPending(this.tracks[type])); 19260 } 19261 getFlushOp(type, start, end) { 19262 this.log(`queuing "${type}" remove ${start}-${end}`); 19263 return { 19264 label: 'remove', 19265 execute: () => { 19266 this.removeExecutor(type, start, end); 19267 }, 19268 onStart: () => { 19269 // logger.debug(`[buffer-controller]: Started flushing ${data.startOffset} -> ${data.endOffset} for ${type} Source Buffer`); 19270 }, 19271 onComplete: () => { 19272 // logger.debug(`[buffer-controller]: Finished flushing ${data.startOffset} -> ${data.endOffset} for ${type} Source Buffer`); 19273 this.hls.trigger(Events.BUFFER_FLUSHED, { 19274 type 19275 }); 19276 }, 19277 onError: error => { 19278 this.warn(`Failed to remove ${start}-${end} from "${type}" SourceBuffer`, error); 19279 } 19280 }; 19281 } 19282 onBufferFlushing(event, data) { 19283 const { 19284 type, 19285 startOffset, 19286 endOffset 19287 } = data; 19288 if (type) { 19289 this.append(this.getFlushOp(type, startOffset, endOffset), type); 19290 } else { 19291 this.sourceBuffers.forEach(([type]) => { 19292 if (type) { 19293 this.append(this.getFlushOp(type, startOffset, endOffset), type); 19294 } 19295 }); 19296 } 19297 } 19298 onFragParsed(event, data) { 19299 const { 19300 frag, 19301 part 19302 } = data; 19303 const buffersAppendedTo = []; 19304 const elementaryStreams = part ? part.elementaryStreams : frag.elementaryStreams; 19305 if (elementaryStreams[ElementaryStreamTypes.AUDIOVIDEO]) { 19306 buffersAppendedTo.push('audiovideo'); 19307 } else { 19308 if (elementaryStreams[ElementaryStreamTypes.AUDIO]) { 19309 buffersAppendedTo.push('audio'); 19310 } 19311 if (elementaryStreams[ElementaryStreamTypes.VIDEO]) { 19312 buffersAppendedTo.push('video'); 19313 } 19314 } 19315 const onUnblocked = () => { 19316 const now = self.performance.now(); 19317 frag.stats.buffering.end = now; 19318 if (part) { 19319 part.stats.buffering.end = now; 19320 } 19321 const stats = part ? part.stats : frag.stats; 19322 this.hls.trigger(Events.FRAG_BUFFERED, { 19323 frag, 19324 part, 19325 stats, 19326 id: frag.type 19327 }); 19328 }; 19329 if (buffersAppendedTo.length === 0) { 19330 this.warn(`Fragments must have at least one ElementaryStreamType set. type: ${frag.type} level: ${frag.level} sn: ${frag.sn}`); 19331 } 19332 this.blockBuffers(onUnblocked, buffersAppendedTo).catch(error => { 19333 this.warn(`Fragment buffered callback ${error}`); 19334 this.stepOperationQueue(this.sourceBufferTypes); 19335 }); 19336 } 19337 onFragChanged(event, data) { 19338 this.trimBuffers(); 19339 } 19340 get bufferedToEnd() { 19341 return this.sourceBufferCount > 0 && !this.sourceBuffers.some(([type]) => { 19342 if (type) { 19343 const track = this.tracks[type]; 19344 if (track) { 19345 return !track.ended || track.ending; 19346 } 19347 } 19348 return false; 19349 }); 19350 } 19351 19352 // on BUFFER_EOS mark matching sourcebuffer(s) as "ending" and "ended" and queue endOfStream after remaining operations(s) 19353 // an undefined data.type will mark all buffers as EOS. 19354 onBufferEos(event, data) { 19355 var _this$overrides; 19356 this.sourceBuffers.forEach(([type]) => { 19357 if (type) { 19358 const track = this.tracks[type]; 19359 if (!data.type || data.type === type) { 19360 track.ending = true; 19361 if (!track.ended) { 19362 track.ended = true; 19363 this.log(`${type} buffer reached EOS`); 19364 } 19365 } 19366 } 19367 }); 19368 const allowEndOfStream = ((_this$overrides = this.overrides) == null ? void 0 : _this$overrides.endOfStream) !== false; 19369 const allTracksEnding = this.sourceBufferCount > 0 && !this.sourceBuffers.some(([type]) => { 19370 var _this$tracks$type3; 19371 return type && !((_this$tracks$type3 = this.tracks[type]) != null && _this$tracks$type3.ended); 19372 }); 19373 if (allTracksEnding) { 19374 if (allowEndOfStream) { 19375 this.log(`Queueing EOS`); 19376 this.blockUntilOpen(() => { 19377 this.tracksEnded(); 19378 const { 19379 mediaSource 19380 } = this; 19381 if (!mediaSource || mediaSource.readyState !== 'open') { 19382 if (mediaSource) { 19383 this.log(`Could not call mediaSource.endOfStream(). mediaSource.readyState: ${mediaSource.readyState}`); 19384 } 19385 return; 19386 } 19387 this.log(`Calling mediaSource.endOfStream()`); 19388 // Allow this to throw and be caught by the enqueueing function 19389 mediaSource.endOfStream(); 19390 this.hls.trigger(Events.BUFFERED_TO_END, undefined); 19391 }); 19392 } else { 19393 this.tracksEnded(); 19394 this.hls.trigger(Events.BUFFERED_TO_END, undefined); 19395 } 19396 } else if (data.type === 'video') { 19397 // Make sure pending audio appends are unblocked when video reaches end 19398 this.unblockAudio(); 19399 } 19400 } 19401 tracksEnded() { 19402 this.sourceBuffers.forEach(([type]) => { 19403 if (type !== null) { 19404 const track = this.tracks[type]; 19405 if (track) { 19406 track.ending = false; 19407 } 19408 } 19409 }); 19410 } 19411 onLevelUpdated(event, { 19412 details 19413 }) { 19414 if (!details.fragments.length) { 19415 return; 19416 } 19417 this.details = details; 19418 this.updateDuration(); 19419 } 19420 updateDuration() { 19421 this.blockUntilOpen(() => { 19422 const durationAndRange = this.getDurationAndRange(); 19423 if (!durationAndRange) { 19424 return; 19425 } 19426 this.updateMediaSource(durationAndRange); 19427 }); 19428 } 19429 onError(event, data) { 19430 if (data.details === ErrorDetails.BUFFER_APPEND_ERROR && data.frag) { 19431 var _data$errorAction; 19432 const nextAutoLevel = (_data$errorAction = data.errorAction) == null ? void 0 : _data$errorAction.nextAutoLevel; 19433 if (isFiniteNumber(nextAutoLevel) && nextAutoLevel !== data.frag.level) { 19434 this.resetAppendErrors(); 19435 } 19436 } 19437 } 19438 resetAppendErrors() { 19439 this.appendErrors = { 19440 audio: 0, 19441 video: 0, 19442 audiovideo: 0 19443 }; 19444 } 19445 trimBuffers() { 19446 const { 19447 hls, 19448 details, 19449 media 19450 } = this; 19451 if (!media || details === null) { 19452 return; 19453 } 19454 if (!this.sourceBufferCount) { 19455 return; 19456 } 19457 const config = hls.config; 19458 const currentTime = media.currentTime; 19459 const targetDuration = details.levelTargetDuration; 19460 19461 // Support for deprecated liveBackBufferLength 19462 const backBufferLength = details.live && config.liveBackBufferLength !== null ? config.liveBackBufferLength : config.backBufferLength; 19463 if (isFiniteNumber(backBufferLength) && backBufferLength >= 0) { 19464 const maxBackBufferLength = Math.max(backBufferLength, targetDuration); 19465 const targetBackBufferPosition = Math.floor(currentTime / targetDuration) * targetDuration - maxBackBufferLength; 19466 this.flushBackBuffer(currentTime, targetDuration, targetBackBufferPosition); 19467 } 19468 const frontBufferFlushThreshold = config.frontBufferFlushThreshold; 19469 if (isFiniteNumber(frontBufferFlushThreshold) && frontBufferFlushThreshold > 0) { 19470 const frontBufferLength = Math.max(config.maxBufferLength, frontBufferFlushThreshold); 19471 const maxFrontBufferLength = Math.max(frontBufferLength, targetDuration); 19472 const targetFrontBufferPosition = Math.floor(currentTime / targetDuration) * targetDuration + maxFrontBufferLength; 19473 this.flushFrontBuffer(currentTime, targetDuration, targetFrontBufferPosition); 19474 } 19475 } 19476 flushBackBuffer(currentTime, targetDuration, targetBackBufferPosition) { 19477 this.sourceBuffers.forEach(([type, sb]) => { 19478 if (sb) { 19479 const buffered = BufferHelper.getBuffered(sb); 19480 // when target buffer start exceeds actual buffer start 19481 if (buffered.length > 0 && targetBackBufferPosition > buffered.start(0)) { 19482 var _this$details; 19483 this.hls.trigger(Events.BACK_BUFFER_REACHED, { 19484 bufferEnd: targetBackBufferPosition 19485 }); 19486 19487 // Support for deprecated event: 19488 const track = this.tracks[type]; 19489 if ((_this$details = this.details) != null && _this$details.live) { 19490 this.hls.trigger(Events.LIVE_BACK_BUFFER_REACHED, { 19491 bufferEnd: targetBackBufferPosition 19492 }); 19493 } else if (track != null && track.ended) { 19494 this.log(`Cannot flush ${type} back buffer while SourceBuffer is in ended state`); 19495 return; 19496 } 19497 this.hls.trigger(Events.BUFFER_FLUSHING, { 19498 startOffset: 0, 19499 endOffset: targetBackBufferPosition, 19500 type 19501 }); 19502 } 19503 } 19504 }); 19505 } 19506 flushFrontBuffer(currentTime, targetDuration, targetFrontBufferPosition) { 19507 this.sourceBuffers.forEach(([type, sb]) => { 19508 if (sb) { 19509 const buffered = BufferHelper.getBuffered(sb); 19510 const numBufferedRanges = buffered.length; 19511 // The buffer is either empty or contiguous 19512 if (numBufferedRanges < 2) { 19513 return; 19514 } 19515 const bufferStart = buffered.start(numBufferedRanges - 1); 19516 const bufferEnd = buffered.end(numBufferedRanges - 1); 19517 // No flush if we can tolerate the current buffer length or the current buffer range we would flush is contiguous with current position 19518 if (targetFrontBufferPosition > bufferStart || currentTime >= bufferStart && currentTime <= bufferEnd) { 19519 return; 19520 } 19521 this.hls.trigger(Events.BUFFER_FLUSHING, { 19522 startOffset: bufferStart, 19523 endOffset: Infinity, 19524 type 19525 }); 19526 } 19527 }); 19528 } 19529 19530 /** 19531 * Update Media Source duration to current level duration or override to Infinity if configuration parameter 19532 * 'liveDurationInfinity` is set to `true` 19533 * More details: https://github.com/video-dev/hls.js/issues/355 19534 */ 19535 getDurationAndRange() { 19536 var _this$overrides2; 19537 const { 19538 details, 19539 mediaSource 19540 } = this; 19541 if (!details || !this.media || (mediaSource == null ? void 0 : mediaSource.readyState) !== 'open') { 19542 return null; 19543 } 19544 const playlistEnd = details.edge; 19545 if (details.live && this.hls.config.liveDurationInfinity) { 19546 const len = details.fragments.length; 19547 if (len && !!mediaSource.setLiveSeekableRange) { 19548 const start = Math.max(0, details.fragmentStart); 19549 const end = Math.max(start, playlistEnd); 19550 return { 19551 duration: Infinity, 19552 start, 19553 end 19554 }; 19555 } 19556 return { 19557 duration: Infinity 19558 }; 19559 } 19560 const overrideDuration = (_this$overrides2 = this.overrides) == null ? void 0 : _this$overrides2.duration; 19561 if (overrideDuration) { 19562 if (!isFiniteNumber(overrideDuration)) { 19563 return null; 19564 } 19565 return { 19566 duration: overrideDuration 19567 }; 19568 } 19569 const mediaDuration = this.media.duration; 19570 const msDuration = isFiniteNumber(mediaSource.duration) ? mediaSource.duration : 0; 19571 if (playlistEnd > msDuration && playlistEnd > mediaDuration || !isFiniteNumber(mediaDuration)) { 19572 return { 19573 duration: playlistEnd 19574 }; 19575 } 19576 return null; 19577 } 19578 updateMediaSource({ 19579 duration, 19580 start, 19581 end 19582 }) { 19583 const mediaSource = this.mediaSource; 19584 if (!this.media || !mediaSource || mediaSource.readyState !== 'open') { 19585 return; 19586 } 19587 if (mediaSource.duration !== duration) { 19588 if (isFiniteNumber(duration)) { 19589 this.log(`Updating MediaSource duration to ${duration.toFixed(3)}`); 19590 } 19591 mediaSource.duration = duration; 19592 } 19593 if (start !== undefined && end !== undefined) { 19594 this.log(`MediaSource duration is set to ${mediaSource.duration}. Setting seekable range to ${start}-${end}.`); 19595 mediaSource.setLiveSeekableRange(start, end); 19596 } 19597 } 19598 get tracksReady() { 19599 const pendingTrackCount = this.pendingTrackCount; 19600 return pendingTrackCount > 0 && (pendingTrackCount >= this.bufferCodecEventsTotal || this.isPending(this.tracks.audiovideo)); 19601 } 19602 checkPendingTracks() { 19603 const { 19604 bufferCodecEventsTotal, 19605 pendingTrackCount, 19606 tracks 19607 } = this; 19608 this.log(`checkPendingTracks (pending: ${pendingTrackCount} codec events expected: ${bufferCodecEventsTotal}) ${stringify(tracks)}`); 19609 // Check if we've received all of the expected bufferCodec events. When none remain, create all the sourceBuffers at once. 19610 // This is important because the MSE spec allows implementations to throw QuotaExceededErrors if creating new sourceBuffers after 19611 // data has been appended to existing ones. 19612 // 2 tracks is the max (one for audio, one for video). If we've reach this max go ahead and create the buffers. 19613 if (this.tracksReady) { 19614 var _this$transferData4; 19615 const transferredTracks = (_this$transferData4 = this.transferData) == null ? void 0 : _this$transferData4.tracks; 19616 if (transferredTracks && Object.keys(transferredTracks).length) { 19617 this.attachTransferred(); 19618 } else { 19619 // ok, let's create them now ! 19620 this.createSourceBuffers(); 19621 } 19622 } 19623 } 19624 bufferCreated() { 19625 if (this.sourceBufferCount) { 19626 const tracks = {}; 19627 this.sourceBuffers.forEach(([type, buffer]) => { 19628 if (type) { 19629 const track = this.tracks[type]; 19630 tracks[type] = { 19631 buffer, 19632 container: track.container, 19633 codec: track.codec, 19634 supplemental: track.supplemental, 19635 levelCodec: track.levelCodec, 19636 id: track.id, 19637 metadata: track.metadata 19638 }; 19639 } 19640 }); 19641 this.hls.trigger(Events.BUFFER_CREATED, { 19642 tracks 19643 }); 19644 this.log(`SourceBuffers created. Running queue: ${this.operationQueue}`); 19645 this.sourceBuffers.forEach(([type]) => { 19646 this.executeNext(type); 19647 }); 19648 } else { 19649 const error = new Error('could not create source buffer for media codec(s)'); 19650 this.hls.trigger(Events.ERROR, { 19651 type: ErrorTypes.MEDIA_ERROR, 19652 details: ErrorDetails.BUFFER_INCOMPATIBLE_CODECS_ERROR, 19653 fatal: true, 19654 error, 19655 reason: error.message 19656 }); 19657 } 19658 } 19659 createSourceBuffers() { 19660 const { 19661 tracks, 19662 sourceBuffers, 19663 mediaSource 19664 } = this; 19665 if (!mediaSource) { 19666 throw new Error('createSourceBuffers called when mediaSource was null'); 19667 } 19668 for (const trackName in tracks) { 19669 const type = trackName; 19670 const track = tracks[type]; 19671 if (this.isPending(track)) { 19672 const codec = this.getTrackCodec(track, type); 19673 const mimeType = `${track.container};codecs=${codec}`; 19674 track.codec = codec; 19675 this.log(`creating sourceBuffer(${mimeType})${this.currentOp(type) ? ' Queued' : ''} ${stringify(track)}`); 19676 try { 19677 const sb = mediaSource.addSourceBuffer(mimeType); 19678 const sbIndex = sourceBufferNameToIndex(type); 19679 const sbTuple = [type, sb]; 19680 sourceBuffers[sbIndex] = sbTuple; 19681 track.buffer = sb; 19682 } catch (error) { 19683 var _this$operationQueue; 19684 this.error(`error while trying to add sourceBuffer: ${error.message}`); 19685 // remove init segment from queue and delete track info 19686 this.shiftAndExecuteNext(type); 19687 (_this$operationQueue = this.operationQueue) == null || _this$operationQueue.removeBlockers(); 19688 delete this.tracks[type]; 19689 this.hls.trigger(Events.ERROR, { 19690 type: ErrorTypes.MEDIA_ERROR, 19691 details: ErrorDetails.BUFFER_ADD_CODEC_ERROR, 19692 fatal: false, 19693 error, 19694 sourceBufferName: type, 19695 mimeType: mimeType, 19696 parent: track.id 19697 }); 19698 return; 19699 } 19700 this.trackSourceBuffer(type, track); 19701 } 19702 } 19703 this.bufferCreated(); 19704 } 19705 getTrackCodec(track, trackName) { 19706 // Use supplemental video codec when supported when adding SourceBuffer (#5558) 19707 const supplementalCodec = track.supplemental; 19708 let trackCodec = track.codec; 19709 if (supplementalCodec && (trackName === 'video' || trackName === 'audiovideo') && areCodecsMediaSourceSupported(supplementalCodec, 'video')) { 19710 trackCodec = replaceVideoCodec(trackCodec, supplementalCodec); 19711 } 19712 const codec = pickMostCompleteCodecName(trackCodec, track.levelCodec); 19713 if (codec) { 19714 if (trackName.slice(0, 5) === 'audio') { 19715 return getCodecCompatibleName(codec, this.appendSource); 19716 } 19717 return codec; 19718 } 19719 return ''; 19720 } 19721 trackSourceBuffer(type, track) { 19722 const buffer = track.buffer; 19723 if (!buffer) { 19724 return; 19725 } 19726 const codec = this.getTrackCodec(track, type); 19727 this.tracks[type] = { 19728 buffer, 19729 codec, 19730 container: track.container, 19731 levelCodec: track.levelCodec, 19732 supplemental: track.supplemental, 19733 metadata: track.metadata, 19734 id: track.id, 19735 listeners: [] 19736 }; 19737 this.removeBufferListeners(type); 19738 this.addBufferListener(type, 'updatestart', this.onSBUpdateStart); 19739 this.addBufferListener(type, 'updateend', this.onSBUpdateEnd); 19740 this.addBufferListener(type, 'error', this.onSBUpdateError); 19741 // ManagedSourceBuffer bufferedchange event 19742 if (this.appendSource) { 19743 this.addBufferListener(type, 'bufferedchange', (type, event) => { 19744 // If media was ejected check for a change. Added ranges are redundant with changes on 'updateend' event. 19745 const removedRanges = event.removedRanges; 19746 if (removedRanges != null && removedRanges.length) { 19747 this.hls.trigger(Events.BUFFER_FLUSHED, { 19748 type: type 19749 }); 19750 } 19751 }); 19752 } 19753 } 19754 get mediaSrc() { 19755 var _this$media2, _this$media2$querySel; 19756 const media = ((_this$media2 = this.media) == null || (_this$media2$querySel = _this$media2.querySelector) == null ? void 0 : _this$media2$querySel.call(_this$media2, 'source')) || this.media; 19757 return media == null ? void 0 : media.src; 19758 } 19759 onSBUpdateStart(type) { 19760 const operation = this.currentOp(type); 19761 if (!operation) { 19762 return; 19763 } 19764 operation.onStart(); 19765 } 19766 onSBUpdateEnd(type) { 19767 var _this$mediaSource4; 19768 if (((_this$mediaSource4 = this.mediaSource) == null ? void 0 : _this$mediaSource4.readyState) === 'closed') { 19769 this.resetBuffer(type); 19770 return; 19771 } 19772 const operation = this.currentOp(type); 19773 if (!operation) { 19774 return; 19775 } 19776 operation.onComplete(); 19777 this.shiftAndExecuteNext(type); 19778 } 19779 onSBUpdateError(type, event) { 19780 var _this$mediaSource5; 19781 const error = new Error(`${type} SourceBuffer error. MediaSource readyState: ${(_this$mediaSource5 = this.mediaSource) == null ? void 0 : _this$mediaSource5.readyState}`); 19782 this.error(`${error}`, event); 19783 // according to http://www.w3.org/TR/media-source/#sourcebuffer-append-error 19784 // SourceBuffer errors are not necessarily fatal; if so, the HTMLMediaElement will fire an error event 19785 this.hls.trigger(Events.ERROR, { 19786 type: ErrorTypes.MEDIA_ERROR, 19787 details: ErrorDetails.BUFFER_APPENDING_ERROR, 19788 sourceBufferName: type, 19789 error, 19790 fatal: false 19791 }); 19792 // updateend is always fired after error, so we'll allow that to shift the current operation off of the queue 19793 const operation = this.currentOp(type); 19794 if (operation) { 19795 operation.onError(error); 19796 } 19797 } 19798 updateTimestampOffset(sb, timestampOffset, tolerance, type, sn, cc) { 19799 const delta = timestampOffset - sb.timestampOffset; 19800 if (Math.abs(delta) >= tolerance) { 19801 this.log(`Updating ${type} SourceBuffer timestampOffset to ${timestampOffset} (sn: ${sn} cc: ${cc})`); 19802 sb.timestampOffset = timestampOffset; 19803 } 19804 } 19805 19806 // This method must result in an updateend event; if remove is not called, onSBUpdateEnd must be called manually 19807 removeExecutor(type, startOffset, endOffset) { 19808 const { 19809 media, 19810 mediaSource 19811 } = this; 19812 const track = this.tracks[type]; 19813 const sb = track == null ? void 0 : track.buffer; 19814 if (!media || !mediaSource || !sb) { 19815 this.warn(`Attempting to remove from the ${type} SourceBuffer, but it does not exist`); 19816 this.shiftAndExecuteNext(type); 19817 return; 19818 } 19819 const mediaDuration = isFiniteNumber(media.duration) ? media.duration : Infinity; 19820 const msDuration = isFiniteNumber(mediaSource.duration) ? mediaSource.duration : Infinity; 19821 const removeStart = Math.max(0, startOffset); 19822 const removeEnd = Math.min(endOffset, mediaDuration, msDuration); 19823 if (removeEnd > removeStart && (!track.ending || track.ended)) { 19824 track.ended = false; 19825 this.log(`Removing [${removeStart},${removeEnd}] from the ${type} SourceBuffer`); 19826 sb.remove(removeStart, removeEnd); 19827 } else { 19828 // Cycle the queue 19829 this.shiftAndExecuteNext(type); 19830 } 19831 } 19832 19833 // This method must result in an updateend event; if append is not called, onSBUpdateEnd must be called manually 19834 appendExecutor(data, type) { 19835 const track = this.tracks[type]; 19836 const sb = track == null ? void 0 : track.buffer; 19837 if (!sb) { 19838 throw new HlsJsTrackRemovedError(`Attempting to append to the ${type} SourceBuffer, but it does not exist`); 19839 } 19840 track.ending = false; 19841 track.ended = false; 19842 sb.appendBuffer(data); 19843 } 19844 blockUntilOpen(callback) { 19845 if (this.isUpdating() || this.isQueued()) { 19846 this.blockBuffers(callback).catch(error => { 19847 this.warn(`SourceBuffer blocked callback ${error}`); 19848 this.stepOperationQueue(this.sourceBufferTypes); 19849 }); 19850 } else { 19851 try { 19852 callback(); 19853 } catch (error) { 19854 this.warn(`Callback run without blocking ${this.operationQueue} ${error}`); 19855 } 19856 } 19857 } 19858 isUpdating() { 19859 return this.sourceBuffers.some(([type, sb]) => type && sb.updating); 19860 } 19861 isQueued() { 19862 return this.sourceBuffers.some(([type]) => type && !!this.currentOp(type)); 19863 } 19864 isPending(track) { 19865 return !!track && !track.buffer; 19866 } 19867 19868 // Enqueues an operation to each SourceBuffer queue which, upon execution, resolves a promise. When all promises 19869 // resolve, the onUnblocked function is executed. Functions calling this method do not need to unblock the queue 19870 // upon completion, since we already do it here 19871 blockBuffers(onUnblocked, bufferNames = this.sourceBufferTypes) { 19872 if (!bufferNames.length) { 19873 this.log('Blocking operation requested, but no SourceBuffers exist'); 19874 return Promise.resolve().then(onUnblocked); 19875 } 19876 const { 19877 operationQueue 19878 } = this; 19879 19880 // logger.debug(`[buffer-controller]: Blocking ${buffers} SourceBuffer`); 19881 const blockingOperations = bufferNames.map(type => this.appendBlocker(type)); 19882 const audioBlocked = bufferNames.length > 1 && !!this.blockedAudioAppend; 19883 if (audioBlocked) { 19884 this.unblockAudio(); 19885 } 19886 return Promise.all(blockingOperations).then(result => { 19887 if (operationQueue !== this.operationQueue) { 19888 return; 19889 } 19890 // logger.debug(`[buffer-controller]: Blocking operation resolved; unblocking ${buffers} SourceBuffer`); 19891 onUnblocked(); 19892 this.stepOperationQueue(this.sourceBufferTypes); 19893 }); 19894 } 19895 stepOperationQueue(bufferNames) { 19896 bufferNames.forEach(type => { 19897 var _this$tracks$type4; 19898 const sb = (_this$tracks$type4 = this.tracks[type]) == null ? void 0 : _this$tracks$type4.buffer; 19899 // Only cycle the queue if the SB is not updating. There's a bug in Chrome which sets the SB updating flag to 19900 // true when changing the MediaSource duration (https://bugs.chromium.org/p/chromium/issues/detail?id=959359&can=2&q=mediasource%20duration) 19901 // While this is a workaround, it's probably useful to have around 19902 if (!sb || sb.updating) { 19903 return; 19904 } 19905 this.shiftAndExecuteNext(type); 19906 }); 19907 } 19908 append(operation, type, pending) { 19909 if (this.operationQueue) { 19910 this.operationQueue.append(operation, type, pending); 19911 } 19912 } 19913 appendBlocker(type) { 19914 if (this.operationQueue) { 19915 return this.operationQueue.appendBlocker(type); 19916 } 19917 } 19918 currentOp(type) { 19919 if (this.operationQueue) { 19920 return this.operationQueue.current(type); 19921 } 19922 return null; 19923 } 19924 executeNext(type) { 19925 if (type && this.operationQueue) { 19926 this.operationQueue.executeNext(type); 19927 } 19928 } 19929 shiftAndExecuteNext(type) { 19930 if (this.operationQueue) { 19931 this.operationQueue.shiftAndExecuteNext(type); 19932 } 19933 } 19934 get pendingTrackCount() { 19935 return Object.keys(this.tracks).reduce((acc, type) => acc + (this.isPending(this.tracks[type]) ? 1 : 0), 0); 19936 } 19937 get sourceBufferCount() { 19938 return this.sourceBuffers.reduce((acc, [type]) => acc + (type ? 1 : 0), 0); 19939 } 19940 get sourceBufferTypes() { 19941 return this.sourceBuffers.map(([type]) => type).filter(type => !!type); 19942 } 19943 addBufferListener(type, event, fn) { 19944 const track = this.tracks[type]; 19945 if (!track) { 19946 return; 19947 } 19948 const buffer = track.buffer; 19949 if (!buffer) { 19950 return; 19951 } 19952 const listener = fn.bind(this, type); 19953 track.listeners.push({ 19954 event, 19955 listener 19956 }); 19957 buffer.addEventListener(event, listener); 19958 } 19959 removeBufferListeners(type) { 19960 const track = this.tracks[type]; 19961 if (!track) { 19962 return; 19963 } 19964 const buffer = track.buffer; 19965 if (!buffer) { 19966 return; 19967 } 19968 track.listeners.forEach(l => { 19969 buffer.removeEventListener(l.event, l.listener); 19970 }); 19971 track.listeners.length = 0; 19972 } 19973} 19974function removeSourceChildren(node) { 19975 const sourceChildren = node.querySelectorAll('source'); 19976 [].slice.call(sourceChildren).forEach(source => { 19977 node.removeChild(source); 19978 }); 19979} 19980function addSource(media, url) { 19981 const source = self.document.createElement('source'); 19982 source.type = 'video/mp4'; 19983 source.src = url; 19984 media.appendChild(source); 19985} 19986function sourceBufferNameToIndex(type) { 19987 return type === 'audio' ? 1 : 0; 19988} 19989 19990class CapLevelController { 19991 constructor(hls) { 19992 this.hls = void 0; 19993 this.autoLevelCapping = void 0; 19994 this.firstLevel = void 0; 19995 this.media = void 0; 19996 this.restrictedLevels = void 0; 19997 this.timer = void 0; 19998 this.clientRect = void 0; 19999 this.streamController = void 0; 20000 this.hls = hls; 20001 this.autoLevelCapping = Number.POSITIVE_INFINITY; 20002 this.firstLevel = -1; 20003 this.media = null; 20004 this.restrictedLevels = []; 20005 this.timer = undefined; 20006 this.clientRect = null; 20007 this.registerListeners(); 20008 } 20009 setStreamController(streamController) { 20010 this.streamController = streamController; 20011 } 20012 destroy() { 20013 if (this.hls) { 20014 this.unregisterListener(); 20015 } 20016 if (this.timer) { 20017 this.stopCapping(); 20018 } 20019 this.media = null; 20020 this.clientRect = null; 20021 // @ts-ignore 20022 this.hls = this.streamController = null; 20023 } 20024 registerListeners() { 20025 const { 20026 hls 20027 } = this; 20028 hls.on(Events.FPS_DROP_LEVEL_CAPPING, this.onFpsDropLevelCapping, this); 20029 hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 20030 hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this); 20031 hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); 20032 hls.on(Events.BUFFER_CODECS, this.onBufferCodecs, this); 20033 hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 20034 } 20035 unregisterListener() { 20036 const { 20037 hls 20038 } = this; 20039 hls.off(Events.FPS_DROP_LEVEL_CAPPING, this.onFpsDropLevelCapping, this); 20040 hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 20041 hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this); 20042 hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); 20043 hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this); 20044 hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 20045 } 20046 onFpsDropLevelCapping(event, data) { 20047 // Don't add a restricted level more than once 20048 const level = this.hls.levels[data.droppedLevel]; 20049 if (this.isLevelAllowed(level)) { 20050 this.restrictedLevels.push({ 20051 bitrate: level.bitrate, 20052 height: level.height, 20053 width: level.width 20054 }); 20055 } 20056 } 20057 onMediaAttaching(event, data) { 20058 this.media = data.media instanceof HTMLVideoElement ? data.media : null; 20059 this.clientRect = null; 20060 if (this.timer && this.hls.levels.length) { 20061 this.detectPlayerSize(); 20062 } 20063 } 20064 onManifestParsed(event, data) { 20065 const hls = this.hls; 20066 this.restrictedLevels = []; 20067 this.firstLevel = data.firstLevel; 20068 if (hls.config.capLevelToPlayerSize && data.video) { 20069 // Start capping immediately if the manifest has signaled video codecs 20070 this.startCapping(); 20071 } 20072 } 20073 onLevelsUpdated(event, data) { 20074 if (this.timer && isFiniteNumber(this.autoLevelCapping)) { 20075 this.detectPlayerSize(); 20076 } 20077 } 20078 20079 // Only activate capping when playing a video stream; otherwise, multi-bitrate audio-only streams will be restricted 20080 // to the first level 20081 onBufferCodecs(event, data) { 20082 const hls = this.hls; 20083 if (hls.config.capLevelToPlayerSize && data.video) { 20084 // If the manifest did not signal a video codec capping has been deferred until we're certain video is present 20085 this.startCapping(); 20086 } 20087 } 20088 onMediaDetaching() { 20089 this.stopCapping(); 20090 this.media = null; 20091 } 20092 detectPlayerSize() { 20093 if (this.media) { 20094 if (this.mediaHeight <= 0 || this.mediaWidth <= 0) { 20095 this.clientRect = null; 20096 return; 20097 } 20098 const levels = this.hls.levels; 20099 if (levels.length) { 20100 const hls = this.hls; 20101 const maxLevel = this.getMaxLevel(levels.length - 1); 20102 if (maxLevel !== this.autoLevelCapping) { 20103 hls.logger.log(`Setting autoLevelCapping to ${maxLevel}: ${levels[maxLevel].height}p@${levels[maxLevel].bitrate} for media ${this.mediaWidth}x${this.mediaHeight}`); 20104 } 20105 hls.autoLevelCapping = maxLevel; 20106 if (hls.autoLevelEnabled && hls.autoLevelCapping > this.autoLevelCapping && this.streamController) { 20107 // if auto level capping has a higher value for the previous one, flush the buffer using nextLevelSwitch 20108 // usually happen when the user go to the fullscreen mode. 20109 this.streamController.nextLevelSwitch(); 20110 } 20111 this.autoLevelCapping = hls.autoLevelCapping; 20112 } 20113 } 20114 } 20115 20116 /* 20117 * returns level should be the one with the dimensions equal or greater than the media (player) dimensions (so the video will be downscaled) 20118 */ 20119 getMaxLevel(capLevelIndex) { 20120 const levels = this.hls.levels; 20121 if (!levels.length) { 20122 return -1; 20123 } 20124 const validLevels = levels.filter((level, index) => this.isLevelAllowed(level) && index <= capLevelIndex); 20125 this.clientRect = null; 20126 return CapLevelController.getMaxLevelByMediaSize(validLevels, this.mediaWidth, this.mediaHeight); 20127 } 20128 startCapping() { 20129 if (this.timer) { 20130 // Don't reset capping if started twice; this can happen if the manifest signals a video codec 20131 return; 20132 } 20133 this.autoLevelCapping = Number.POSITIVE_INFINITY; 20134 self.clearInterval(this.timer); 20135 this.timer = self.setInterval(this.detectPlayerSize.bind(this), 1000); 20136 this.detectPlayerSize(); 20137 } 20138 stopCapping() { 20139 this.restrictedLevels = []; 20140 this.firstLevel = -1; 20141 this.autoLevelCapping = Number.POSITIVE_INFINITY; 20142 if (this.timer) { 20143 self.clearInterval(this.timer); 20144 this.timer = undefined; 20145 } 20146 } 20147 getDimensions() { 20148 if (this.clientRect) { 20149 return this.clientRect; 20150 } 20151 const media = this.media; 20152 const boundsRect = { 20153 width: 0, 20154 height: 0 20155 }; 20156 if (media) { 20157 const clientRect = media.getBoundingClientRect(); 20158 boundsRect.width = clientRect.width; 20159 boundsRect.height = clientRect.height; 20160 if (!boundsRect.width && !boundsRect.height) { 20161 // When the media element has no width or height (equivalent to not being in the DOM), 20162 // then use its width and height attributes (media.width, media.height) 20163 boundsRect.width = clientRect.right - clientRect.left || media.width || 0; 20164 boundsRect.height = clientRect.bottom - clientRect.top || media.height || 0; 20165 } 20166 } 20167 this.clientRect = boundsRect; 20168 return boundsRect; 20169 } 20170 get mediaWidth() { 20171 return this.getDimensions().width * this.contentScaleFactor; 20172 } 20173 get mediaHeight() { 20174 return this.getDimensions().height * this.contentScaleFactor; 20175 } 20176 get contentScaleFactor() { 20177 let pixelRatio = 1; 20178 if (!this.hls.config.ignoreDevicePixelRatio) { 20179 try { 20180 pixelRatio = self.devicePixelRatio; 20181 } catch (e) { 20182 /* no-op */ 20183 } 20184 } 20185 return Math.min(pixelRatio, this.hls.config.maxDevicePixelRatio); 20186 } 20187 isLevelAllowed(level) { 20188 const restrictedLevels = this.restrictedLevels; 20189 return !restrictedLevels.some(restrictedLevel => { 20190 return level.bitrate === restrictedLevel.bitrate && level.width === restrictedLevel.width && level.height === restrictedLevel.height; 20191 }); 20192 } 20193 static getMaxLevelByMediaSize(levels, width, height) { 20194 if (!(levels != null && levels.length)) { 20195 return -1; 20196 } 20197 20198 // Levels can have the same dimensions but differing bandwidths - since levels are ordered, we can look to the next 20199 // to determine whether we've chosen the greatest bandwidth for the media's dimensions 20200 const atGreatestBandwidth = (curLevel, nextLevel) => { 20201 if (!nextLevel) { 20202 return true; 20203 } 20204 return curLevel.width !== nextLevel.width || curLevel.height !== nextLevel.height; 20205 }; 20206 20207 // If we run through the loop without breaking, the media's dimensions are greater than every level, so default to 20208 // the max level 20209 let maxLevelIndex = levels.length - 1; 20210 // Prevent changes in aspect-ratio from causing capping to toggle back and forth 20211 const squareSize = Math.max(width, height); 20212 for (let i = 0; i < levels.length; i += 1) { 20213 const level = levels[i]; 20214 if ((level.width >= squareSize || level.height >= squareSize) && atGreatestBandwidth(level, levels[i + 1])) { 20215 maxLevelIndex = i; 20216 break; 20217 } 20218 } 20219 return maxLevelIndex; 20220 } 20221} 20222 20223/** 20224 * Common Media Object Type 20225 * 20226 * @internal 20227 */ 20228const CmObjectType = { 20229 /** 20230 * text file, such as a manifest or playlist 20231 */ 20232 MANIFEST: 'm', 20233 /** 20234 * audio only 20235 */ 20236 AUDIO: 'a', 20237 /** 20238 * video only 20239 */ 20240 VIDEO: 'v', 20241 /** 20242 * muxed audio and video 20243 */ 20244 MUXED: 'av', 20245 /** 20246 * init segment 20247 */ 20248 INIT: 'i', 20249 /** 20250 * caption or subtitle 20251 */ 20252 CAPTION: 'c', 20253 /** 20254 * ISOBMFF timed text track 20255 */ 20256 TIMED_TEXT: 'tt', 20257 /** 20258 * cryptographic key, license or certificate. 20259 */ 20260 KEY: 'k', 20261 /** 20262 * other 20263 */ 20264 OTHER: 'o' 20265}; 20266 20267/** 20268 * Common Media Client Data Object Type 20269 * 20270 * @group CMCD 20271 * 20272 * @beta 20273 * 20274 * @enum 20275 */ 20276const CmcdObjectType = CmObjectType; 20277 20278/** 20279 * Common Media Streaming Format 20280 * 20281 * @internal 20282 */ 20283const CmStreamingFormat = { 20284 /** 20285 * HTTP Live Streaming (HLS) 20286 */ 20287 HLS: 'h'}; 20288 20289/** 20290 * Common Media Client Data Streaming Format 20291 * 20292 * @group CMCD 20293 * 20294 * @enum 20295 * 20296 * @beta 20297 */ 20298const CmcdStreamingFormat = CmStreamingFormat; 20299 20300/** 20301 * Structured Field Item 20302 * 20303 * @group Structured Field 20304 * 20305 * @beta 20306 */ 20307class SfItem { 20308 constructor(value, params) { 20309 if (Array.isArray(value)) { 20310 value = value.map(v => v instanceof SfItem ? v : new SfItem(v)); 20311 } 20312 this.value = value; 20313 this.params = params; 20314 } 20315} 20316 20317const DICT = 'Dict'; 20318 20319function format(value) { 20320 if (Array.isArray(value)) { 20321 return JSON.stringify(value); 20322 } 20323 if (value instanceof Map) { 20324 return 'Map{}'; 20325 } 20326 if (value instanceof Set) { 20327 return 'Set{}'; 20328 } 20329 if (typeof value === 'object') { 20330 return JSON.stringify(value); 20331 } 20332 return String(value); 20333} 20334function throwError(action, src, type, cause) { 20335 return new Error(`failed to ${action} "${format(src)}" as ${type}`, { 20336 cause 20337 }); 20338} 20339 20340function serializeError(src, type, cause) { 20341 return throwError('serialize', src, type, cause); 20342} 20343 20344/** 20345 * A class to represent structured field tokens when `Symbol` is not available. 20346 * 20347 * @group Structured Field 20348 * 20349 * @beta 20350 */ 20351class SfToken { 20352 constructor(description) { 20353 this.description = description; 20354 } 20355} 20356 20357const BARE_ITEM = 'Bare Item'; 20358 20359const BOOLEAN = 'Boolean'; 20360 20361// 4.1.9. Serializing a Boolean 20362// 20363// Given a Boolean as input_boolean, return an ASCII string suitable for 20364// use in a HTTP field value. 20365// 20366// 1. If input_boolean is not a boolean, fail serialization. 20367// 20368// 2. Let output be an empty string. 20369// 20370// 3. Append "?" to output. 20371// 20372// 4. If input_boolean is true, append "1" to output. 20373// 20374// 5. If input_boolean is false, append "0" to output. 20375// 20376// 6. Return output. 20377function serializeBoolean(value) { 20378 if (typeof value !== 'boolean') { 20379 throw serializeError(value, BOOLEAN); 20380 } 20381 return value ? '?1' : '?0'; 20382} 20383 20384/** 20385 * Encodes binary data to base64 20386 * 20387 * @param binary - The binary data to encode 20388 * @returns The base64 encoded string 20389 * 20390 * @group Utils 20391 * 20392 * @beta 20393 */ 20394function encodeBase64(binary) { 20395 return btoa(String.fromCharCode(...binary)); 20396} 20397 20398const BYTES = 'Byte Sequence'; 20399 20400// 4.1.8. Serializing a Byte Sequence 20401// 20402// Given a Byte Sequence as input_bytes, return an ASCII string suitable 20403// for use in a HTTP field value. 20404// 20405// 1. If input_bytes is not a sequence of bytes, fail serialization. 20406// 20407// 2. Let output be an empty string. 20408// 20409// 3. Append ":" to output. 20410// 20411// 4. Append the result of base64-encoding input_bytes as per 20412// [RFC4648], Section 4, taking account of the requirements below. 20413// 20414// 5. Append ":" to output. 20415// 20416// 6. Return output. 20417// 20418// The encoded data is required to be padded with "=", as per [RFC4648], 20419// Section 3.2. 20420// 20421// Likewise, encoded data SHOULD have pad bits set to zero, as per 20422// [RFC4648], Section 3.5, unless it is not possible to do so due to 20423// implementation constraints. 20424function serializeByteSequence(value) { 20425 if (ArrayBuffer.isView(value) === false) { 20426 throw serializeError(value, BYTES); 20427 } 20428 return `:${encodeBase64(value)}:`; 20429} 20430 20431const INTEGER = 'Integer'; 20432 20433function isInvalidInt(value) { 20434 return value < -999999999999999 || 999999999999999 < value; 20435} 20436 20437// 4.1.4. Serializing an Integer 20438// 20439// Given an Integer as input_integer, return an ASCII string suitable 20440// for use in a HTTP field value. 20441// 20442// 1. If input_integer is not an integer in the range of 20443// -999,999,999,999,999 to 999,999,999,999,999 inclusive, fail 20444// serialization. 20445// 20446// 2. Let output be an empty string. 20447// 20448// 3. If input_integer is less than (but not equal to) 0, append "-" to 20449// output. 20450// 20451// 4. Append input_integer's numeric value represented in base 10 using 20452// only decimal digits to output. 20453// 20454// 5. Return output. 20455function serializeInteger(value) { 20456 if (isInvalidInt(value)) { 20457 throw serializeError(value, INTEGER); 20458 } 20459 return value.toString(); 20460} 20461 20462// 4.1.10. Serializing a Date 20463// 20464// Given a Date as input_integer, return an ASCII string suitable for 20465// use in an HTTP field value. 20466// 1. Let output be "@". 20467// 2. Append to output the result of running Serializing an Integer 20468// with input_date (Section 4.1.4). 20469// 3. Return output. 20470function serializeDate(value) { 20471 return `@${serializeInteger(value.getTime() / 1000)}`; 20472} 20473 20474/** 20475 * This implements the rounding procedure described in step 2 of the "Serializing a Decimal" specification. 20476 * This rounding style is known as "even rounding", "banker's rounding", or "commercial rounding". 20477 * 20478 * @param value - The value to round 20479 * @param precision - The number of decimal places to round to 20480 * @returns The rounded value 20481 * 20482 * @group Utils 20483 * 20484 * @beta 20485 */ 20486function roundToEven(value, precision) { 20487 if (value < 0) { 20488 return -roundToEven(-value, precision); 20489 } 20490 const decimalShift = Math.pow(10, precision); 20491 const isEquidistant = Math.abs(value * decimalShift % 1 - 0.5) < Number.EPSILON; 20492 if (isEquidistant) { 20493 // If the tail of the decimal place is 'equidistant' we round to the nearest even value 20494 const flooredValue = Math.floor(value * decimalShift); 20495 return (flooredValue % 2 === 0 ? flooredValue : flooredValue + 1) / decimalShift; 20496 } else { 20497 // Otherwise, proceed as normal 20498 return Math.round(value * decimalShift) / decimalShift; 20499 } 20500} 20501 20502const DECIMAL = 'Decimal'; 20503 20504// 4.1.5. Serializing a Decimal 20505// 20506// Given a decimal number as input_decimal, return an ASCII string 20507// suitable for use in a HTTP field value. 20508// 20509// 1. If input_decimal is not a decimal number, fail serialization. 20510// 20511// 2. If input_decimal has more than three significant digits to the 20512// right of the decimal point, round it to three decimal places, 20513// rounding the final digit to the nearest value, or to the even 20514// value if it is equidistant. 20515// 20516// 3. If input_decimal has more than 12 significant digits to the left 20517// of the decimal point after rounding, fail serialization. 20518// 20519// 4. Let output be an empty string. 20520// 20521// 5. If input_decimal is less than (but not equal to) 0, append "-" 20522// to output. 20523// 20524// 6. Append input_decimal's integer component represented in base 10 20525// (using only decimal digits) to output; if it is zero, append 20526// "0". 20527// 20528// 7. Append "." to output. 20529// 20530// 8. If input_decimal's fractional component is zero, append "0" to 20531// output. 20532// 20533// 9. Otherwise, append the significant digits of input_decimal's 20534// fractional component represented in base 10 (using only decimal 20535// digits) to output. 20536// 20537// 10. Return output. 20538function serializeDecimal(value) { 20539 const roundedValue = roundToEven(value, 3); // round to 3 decimal places 20540 if (Math.floor(Math.abs(roundedValue)).toString().length > 12) { 20541 throw serializeError(value, DECIMAL); 20542 } 20543 const stringValue = roundedValue.toString(); 20544 return stringValue.includes('.') ? stringValue : `${stringValue}.0`; 20545} 20546 20547const STRING = 'String'; 20548 20549const STRING_REGEX = /[\x00-\x1f\x7f]+/; 20550 20551// 4.1.6. Serializing a String 20552// 20553// Given a String as input_string, return an ASCII string suitable for 20554// use in a HTTP field value. 20555// 20556// 1. Convert input_string into a sequence of ASCII characters; if 20557// conversion fails, fail serialization. 20558// 20559// 2. If input_string contains characters in the range %x00-1f or %x7f 20560// (i.e., not in VCHAR or SP), fail serialization. 20561// 20562// 3. Let output be the string DQUOTE. 20563// 20564// 4. For each character char in input_string: 20565// 20566// 1. If char is "\" or DQUOTE: 20567// 20568// 1. Append "\" to output. 20569// 20570// 2. Append char to output. 20571// 20572// 5. Append DQUOTE to output. 20573// 20574// 6. Return output. 20575function serializeString(value) { 20576 if (STRING_REGEX.test(value)) { 20577 throw serializeError(value, STRING); 20578 } 20579 return `"${value.replace(/\\/g, `\\\\`).replace(/"/g, `\\"`)}"`; 20580} 20581 20582/** 20583 * Converts a symbol to a string. 20584 * 20585 * @param symbol - The symbol to convert. 20586 * 20587 * @returns The string representation of the symbol. 20588 * 20589 * @internal 20590 */ 20591function symbolToStr(symbol) { 20592 return symbol.description || symbol.toString().slice(7, -1); 20593} 20594 20595const TOKEN = 'Token'; 20596 20597function serializeToken(token) { 20598 const value = symbolToStr(token); 20599 if (/^([a-zA-Z*])([!#$%&'*+\-.^_`|~\w:/]*)$/.test(value) === false) { 20600 throw serializeError(value, TOKEN); 20601 } 20602 return value; 20603} 20604 20605// 4.1.3.1. Serializing a Bare Item 20606// 20607// Given an Item as input_item, return an ASCII string suitable for use 20608// in a HTTP field value. 20609// 20610// 1. If input_item is an Integer, return the result of running 20611// Serializing an Integer (Section 4.1.4) with input_item. 20612// 20613// 2. If input_item is a Decimal, return the result of running 20614// Serializing a Decimal (Section 4.1.5) with input_item. 20615// 20616// 3. If input_item is a String, return the result of running 20617// Serializing a String (Section 4.1.6) with input_item. 20618// 20619// 4. If input_item is a Token, return the result of running 20620// Serializing a Token (Section 4.1.7) with input_item. 20621// 20622// 5. If input_item is a Boolean, return the result of running 20623// Serializing a Boolean (Section 4.1.9) with input_item. 20624// 20625// 6. If input_item is a Byte Sequence, return the result of running 20626// Serializing a Byte Sequence (Section 4.1.8) with input_item. 20627// 20628// 7. If input_item is a Date, return the result of running Serializing 20629// a Date (Section 4.1.10) with input_item. 20630// 20631// 8. Otherwise, fail serialization. 20632function serializeBareItem(value) { 20633 switch (typeof value) { 20634 case 'number': 20635 if (!isFiniteNumber(value)) { 20636 throw serializeError(value, BARE_ITEM); 20637 } 20638 if (Number.isInteger(value)) { 20639 return serializeInteger(value); 20640 } 20641 return serializeDecimal(value); 20642 case 'string': 20643 return serializeString(value); 20644 case 'symbol': 20645 return serializeToken(value); 20646 case 'boolean': 20647 return serializeBoolean(value); 20648 case 'object': 20649 if (value instanceof Date) { 20650 return serializeDate(value); 20651 } 20652 if (value instanceof Uint8Array) { 20653 return serializeByteSequence(value); 20654 } 20655 if (value instanceof SfToken) { 20656 return serializeToken(value); 20657 } 20658 default: 20659 // fail 20660 throw serializeError(value, BARE_ITEM); 20661 } 20662} 20663 20664const KEY = 'Key'; 20665 20666// 4.1.1.3. Serializing a Key 20667// 20668// Given a key as input_key, return an ASCII string suitable for use in 20669// a HTTP field value. 20670// 20671// 1. Convert input_key into a sequence of ASCII characters; if 20672// conversion fails, fail serialization. 20673// 20674// 2. If input_key contains characters not in lcalpha, DIGIT, "_", "-", 20675// ".", or "*" fail serialization. 20676// 20677// 3. If the first character of input_key is not lcalpha or "*", fail 20678// serialization. 20679// 20680// 4. Let output be an empty string. 20681// 20682// 5. Append input_key to output. 20683// 20684// 6. Return output. 20685function serializeKey(value) { 20686 if (/^[a-z*][a-z0-9\-_.*]*$/.test(value) === false) { 20687 throw serializeError(value, KEY); 20688 } 20689 return value; 20690} 20691 20692// 4.1.1.2. Serializing Parameters 20693// 20694// Given an ordered Dictionary as input_parameters (each member having a 20695// param_name and a param_value), return an ASCII string suitable for 20696// use in a HTTP field value. 20697// 20698// 1. Let output be an empty string. 20699// 20700// 2. For each param_name with a value of param_value in 20701// input_parameters: 20702// 20703// 1. Append ";" to output. 20704// 20705// 2. Append the result of running Serializing a Key 20706// (Section 4.1.1.3) with param_name to output. 20707// 20708// 3. If param_value is not Boolean true: 20709// 20710// 1. Append "=" to output. 20711// 20712// 2. Append the result of running Serializing a bare Item 20713// (Section 4.1.3.1) with param_value to output. 20714// 20715// 3. Return output. 20716function serializeParams(params) { 20717 if (params == null) { 20718 return ''; 20719 } 20720 return Object.entries(params).map(([key, value]) => { 20721 if (value === true) { 20722 return `;${serializeKey(key)}`; // omit true 20723 } 20724 return `;${serializeKey(key)}=${serializeBareItem(value)}`; 20725 }).join(''); 20726} 20727 20728// 4.1.3. Serializing an Item 20729// 20730// Given an Item as bare_item and Parameters as item_parameters, return 20731// an ASCII string suitable for use in a HTTP field value. 20732// 20733// 1. Let output be an empty string. 20734// 20735// 2. Append the result of running Serializing a Bare Item 20736// Section 4.1.3.1 with bare_item to output. 20737// 20738// 3. Append the result of running Serializing Parameters 20739// Section 4.1.1.2 with item_parameters to output. 20740// 20741// 4. Return output. 20742function serializeItem(value) { 20743 if (value instanceof SfItem) { 20744 return `${serializeBareItem(value.value)}${serializeParams(value.params)}`; 20745 } else { 20746 return serializeBareItem(value); 20747 } 20748} 20749 20750// 4.1.1.1. Serializing an Inner List 20751// 20752// Given an array of (member_value, parameters) tuples as inner_list, 20753// and parameters as list_parameters, return an ASCII string suitable 20754// for use in a HTTP field value. 20755// 20756// 1. Let output be the string "(". 20757// 20758// 2. For each (member_value, parameters) of inner_list: 20759// 20760// 1. Append the result of running Serializing an Item 20761// (Section 4.1.3) with (member_value, parameters) to output. 20762// 20763// 2. If more values remain in inner_list, append a single SP to 20764// output. 20765// 20766// 3. Append ")" to output. 20767// 20768// 4. Append the result of running Serializing Parameters 20769// (Section 4.1.1.2) with list_parameters to output. 20770// 20771// 5. Return output. 20772function serializeInnerList(value) { 20773 return `(${value.value.map(serializeItem).join(' ')})${serializeParams(value.params)}`; 20774} 20775 20776// 4.1.2. Serializing a Dictionary 20777// 20778// Given an ordered Dictionary as input_dictionary (each member having a 20779// member_name and a tuple value of (member_value, parameters)), return 20780// an ASCII string suitable for use in a HTTP field value. 20781// 20782// 1. Let output be an empty string. 20783// 20784// 2. For each member_name with a value of (member_value, parameters) 20785// in input_dictionary: 20786// 20787// 1. Append the result of running Serializing a Key 20788// (Section 4.1.1.3) with member's member_name to output. 20789// 20790// 2. If member_value is Boolean true: 20791// 20792// 1. Append the result of running Serializing Parameters 20793// (Section 4.1.1.2) with parameters to output. 20794// 20795// 3. Otherwise: 20796// 20797// 1. Append "=" to output. 20798// 20799// 2. If member_value is an array, append the result of running 20800// Serializing an Inner List (Section 4.1.1.1) with 20801// (member_value, parameters) to output. 20802// 20803// 3. Otherwise, append the result of running Serializing an 20804// Item (Section 4.1.3) with (member_value, parameters) to 20805// output. 20806// 20807// 4. If more members remain in input_dictionary: 20808// 20809// 1. Append "," to output. 20810// 20811// 2. Append a single SP to output. 20812// 20813// 3. Return output. 20814function serializeDict(dict, options = { 20815 whitespace: true 20816}) { 20817 if (typeof dict !== 'object' || dict == null) { 20818 throw serializeError(dict, DICT); 20819 } 20820 const entries = dict instanceof Map ? dict.entries() : Object.entries(dict); 20821 const optionalWhiteSpace = (options === null || options === void 0 ? void 0 : options.whitespace) ? ' ' : ''; 20822 return Array.from(entries).map(([key, item]) => { 20823 if (item instanceof SfItem === false) { 20824 item = new SfItem(item); 20825 } 20826 let output = serializeKey(key); 20827 if (item.value === true) { 20828 output += serializeParams(item.params); 20829 } else { 20830 output += '='; 20831 if (Array.isArray(item.value)) { 20832 output += serializeInnerList(item); 20833 } else { 20834 output += serializeItem(item); 20835 } 20836 } 20837 return output; 20838 }).join(`,${optionalWhiteSpace}`); 20839} 20840 20841/** 20842 * Encode an object into a structured field dictionary 20843 * 20844 * @param value - The structured field dictionary to encode 20845 * @param options - Encoding options 20846 * 20847 * @returns The structured field string 20848 * 20849 * @group Structured Field 20850 * 20851 * @beta 20852 */ 20853function encodeSfDict(value, options) { 20854 return serializeDict(value, options); 20855} 20856 20857/** 20858 * CMCD object header name. 20859 * 20860 * @group CMCD 20861 * 20862 * @beta 20863 */ 20864const CMCD_OBJECT = 'CMCD-Object'; 20865 20866/** 20867 * CMCD request header name. 20868 * 20869 * @group CMCD 20870 * 20871 * @beta 20872 */ 20873const CMCD_REQUEST = 'CMCD-Request'; 20874 20875/** 20876 * CMCD session header name. 20877 * 20878 * @group CMCD 20879 * 20880 * @beta 20881 */ 20882const CMCD_SESSION = 'CMCD-Session'; 20883 20884/** 20885 * CMCD status header name. 20886 * 20887 * @group CMCD 20888 * 20889 * @beta 20890 */ 20891const CMCD_STATUS = 'CMCD-Status'; 20892 20893/** 20894 * The map of CMCD keys to their appropriate header shard. 20895 * 20896 * @group CMCD 20897 * 20898 * @internal 20899 */ 20900const CMCD_HEADER_MAP = { 20901 // Object 20902 br: CMCD_OBJECT, 20903 ab: CMCD_OBJECT, 20904 d: CMCD_OBJECT, 20905 ot: CMCD_OBJECT, 20906 tb: CMCD_OBJECT, 20907 tpb: CMCD_OBJECT, 20908 lb: CMCD_OBJECT, 20909 tab: CMCD_OBJECT, 20910 lab: CMCD_OBJECT, 20911 url: CMCD_OBJECT, 20912 // Request 20913 pb: CMCD_REQUEST, 20914 bl: CMCD_REQUEST, 20915 tbl: CMCD_REQUEST, 20916 dl: CMCD_REQUEST, 20917 ltc: CMCD_REQUEST, 20918 mtp: CMCD_REQUEST, 20919 nor: CMCD_REQUEST, 20920 nrr: CMCD_REQUEST, 20921 rc: CMCD_REQUEST, 20922 sn: CMCD_REQUEST, 20923 sta: CMCD_REQUEST, 20924 su: CMCD_REQUEST, 20925 ttfb: CMCD_REQUEST, 20926 ttfbb: CMCD_REQUEST, 20927 ttlb: CMCD_REQUEST, 20928 cmsdd: CMCD_REQUEST, 20929 cmsds: CMCD_REQUEST, 20930 smrt: CMCD_REQUEST, 20931 df: CMCD_REQUEST, 20932 cs: CMCD_REQUEST, 20933 // TODO: Which header to put the `ts` field is not defined yet. 20934 ts: CMCD_REQUEST, 20935 // Session 20936 cid: CMCD_SESSION, 20937 pr: CMCD_SESSION, 20938 sf: CMCD_SESSION, 20939 sid: CMCD_SESSION, 20940 st: CMCD_SESSION, 20941 v: CMCD_SESSION, 20942 msd: CMCD_SESSION, 20943 // Status 20944 bs: CMCD_STATUS, 20945 bsd: CMCD_STATUS, 20946 cdn: CMCD_STATUS, 20947 rtp: CMCD_STATUS, 20948 bg: CMCD_STATUS, 20949 pt: CMCD_STATUS, 20950 ec: CMCD_STATUS, 20951 e: CMCD_STATUS 20952}; 20953 20954/** 20955 * CMCD header fields. 20956 * 20957 * @group CMCD 20958 * 20959 * @enum 20960 * 20961 * @beta 20962 */ 20963const CmcdHeaderField = { 20964 /** 20965 * keys whose values vary with each request. 20966 */ 20967 REQUEST: CMCD_REQUEST}; 20968 20969function createHeaderMap(headerMap) { 20970 return Object.keys(headerMap).reduce((acc, field) => { 20971 var _a; 20972 (_a = headerMap[field]) === null || _a === void 0 ? void 0 : _a.forEach(key => acc[key] = field); 20973 return acc; 20974 }, {}); 20975} 20976/** 20977 * Group a CMCD data object into header shards 20978 * 20979 * @param cmcd - The CMCD data object to convert. 20980 * @param customHeaderMap - A map of CMCD header fields to custom CMCD keys. 20981 * 20982 * @returns The CMCD header shards. 20983 * 20984 * @group CMCD 20985 * 20986 * @beta 20987 */ 20988function groupCmcdHeaders(cmcd, customHeaderMap) { 20989 const result = {}; 20990 if (!cmcd) { 20991 return result; 20992 } 20993 const keys = Object.keys(cmcd); 20994 const custom = customHeaderMap ? createHeaderMap(customHeaderMap) : {}; 20995 return keys.reduce((acc, key) => { 20996 var _a; 20997 const field = CMCD_HEADER_MAP[key] || custom[key] || CmcdHeaderField.REQUEST; 20998 const data = (_a = acc[field]) !== null && _a !== void 0 ? _a : acc[field] = {}; 20999 data[key] = cmcd[key]; 21000 return acc; 21001 }, result); 21002} 21003 21004/** 21005 * Checks if the given key is a token field. 21006 * 21007 * @param key - The key to check. 21008 * 21009 * @returns `true` if the key is a token field. 21010 * 21011 * @internal 21012 */ 21013function isTokenField(key) { 21014 return ['ot', 'sf', 'st', 'e', 'sta'].includes(key); 21015} 21016 21017/** 21018 * Checks if the given value is valid 21019 * 21020 * @param value - The value to check. 21021 * 21022 * @returns `true` if the key is a value is valid. 21023 * 21024 * @internal 21025 */ 21026function isValid(value) { 21027 if (typeof value === 'number') { 21028 return isFiniteNumber(value); 21029 } 21030 return value != null && value !== '' && value !== false; 21031} 21032 21033/** 21034 * CMCD event mode variable name. 21035 * 21036 * @group CMCD 21037 * 21038 * @beta 21039 */ 21040const CMCD_EVENT_MODE = 'event'; 21041 21042/** 21043 * Constructs a relative path from a URL. 21044 * 21045 * @param url - The destination URL 21046 * @param base - The base URL 21047 * @returns The relative path 21048 * 21049 * @group Utils 21050 * 21051 * @beta 21052 */ 21053function urlToRelativePath(url, base) { 21054 const to = new URL(url); 21055 const from = new URL(base); 21056 if (to.origin !== from.origin) { 21057 return url; 21058 } 21059 const toPath = to.pathname.split('/').slice(1); 21060 const fromPath = from.pathname.split('/').slice(1, -1); 21061 // remove common parents 21062 while (toPath[0] === fromPath[0]) { 21063 toPath.shift(); 21064 fromPath.shift(); 21065 } 21066 // add back paths 21067 while (fromPath.length) { 21068 fromPath.shift(); 21069 toPath.unshift('..'); 21070 } 21071 const relativePath = toPath.join('/'); 21072 // preserve query parameters and hash of the destination url 21073 return relativePath + to.search + to.hash; 21074} 21075 21076const toRounded = value => Math.round(value); 21077const toUrlSafe = (value, options) => { 21078 if (Array.isArray(value)) { 21079 return value.map(item => toUrlSafe(item, options)); 21080 } 21081 if (value instanceof SfItem && typeof value.value === 'string') { 21082 return new SfItem(toUrlSafe(value.value, options), value.params); 21083 } else { 21084 if (options.baseUrl) { 21085 value = urlToRelativePath(value, options.baseUrl); 21086 } 21087 return options.version === 1 ? encodeURIComponent(value) : value; 21088 } 21089}; 21090const toHundred = value => toRounded(value / 100) * 100; 21091const nor = (value, options) => { 21092 let norValue = value; 21093 if (options.version >= 2) { 21094 if (value instanceof SfItem && typeof value.value === 'string') { 21095 norValue = new SfItem([value]); 21096 } else if (typeof value === 'string') { 21097 norValue = [value]; 21098 } 21099 } 21100 return toUrlSafe(norValue, options); 21101}; 21102/** 21103 * The default formatters for CMCD values. 21104 * 21105 * @group CMCD 21106 * 21107 * @beta 21108 */ 21109const CMCD_FORMATTER_MAP = { 21110 /** 21111 * Bitrate (kbps) rounded integer 21112 */ 21113 br: toRounded, 21114 /** 21115 * Duration (milliseconds) rounded integer 21116 */ 21117 d: toRounded, 21118 /** 21119 * Buffer Length (milliseconds) rounded nearest 100ms 21120 */ 21121 bl: toHundred, 21122 /** 21123 * Deadline (milliseconds) rounded nearest 100ms 21124 */ 21125 dl: toHundred, 21126 /** 21127 * Measured Throughput (kbps) rounded nearest 100kbps 21128 */ 21129 mtp: toHundred, 21130 /** 21131 * Next Object Request URL encoded 21132 */ 21133 nor, 21134 /** 21135 * Requested maximum throughput (kbps) rounded nearest 100kbps 21136 */ 21137 rtp: toHundred, 21138 /** 21139 * Top Bitrate (kbps) rounded integer 21140 */ 21141 tb: toRounded 21142}; 21143 21144/** 21145 * CMCD request mode variable name. 21146 * 21147 * @group CMCD 21148 * 21149 * @beta 21150 */ 21151const CMCD_REQUEST_MODE = 'request'; 21152 21153/** 21154 * CMCD response mode variable name. 21155 * 21156 * @group CMCD 21157 * 21158 * @beta 21159 */ 21160const CMCD_RESPONSE_MODE = 'response'; 21161 21162/** 21163 * Defines the common keys for CMCD (Common Media Client Data) version 2. 21164 * 21165 * @group CMCD 21166 * 21167 * @beta 21168 */ 21169const CMCD_COMMON_KEYS = ['ab', 'bg', 'bl', 'br', 'bs', 'bsd', 'cdn', 'cid', 'cs', 'df', 'ec', 'lab', 'lb', 'ltc', 'msd', 'mtp', 'pb', 'pr', 'pt', 'sf', 'sid', 'sn', 'st', 'sta', 'tab', 'tb', 'tbl', 'tpb', 'ts', 'v']; 21170 21171/** 21172 * Defines the event-specific keys for CMCD (Common Media Client Data) version 2. 21173 * 21174 * @group CMCD 21175 * 21176 * @beta 21177 */ 21178const CMCD_EVENT_KEYS = ['e']; 21179 21180const CUSTOM_KEY_REGEX = /^[a-zA-Z0-9-.]+-[a-zA-Z0-9-.]+$/; 21181/** 21182 * Check if a key is a custom key. 21183 * 21184 * @param key - The key to check. 21185 * 21186 * @returns `true` if the key is a custom key, `false` otherwise. 21187 * 21188 * @group CMCD 21189 * 21190 * @beta 21191 */ 21192function isCmcdCustomKey(key) { 21193 return CUSTOM_KEY_REGEX.test(key); 21194} 21195 21196/** 21197 * Check if a key is a valid CMCD event key. 21198 * 21199 * @param key - The key to check. 21200 * 21201 * @returns `true` if the key is a valid CMCD event key, `false` otherwise. 21202 * 21203 * @group CMCD 21204 * 21205 * @beta 21206 * 21207 * @example 21208 * {@includeCode ../../test/cmcd/isCmcdEventKey.test.ts#example} 21209 */ 21210function isCmcdEventKey(key) { 21211 return CMCD_COMMON_KEYS.includes(key) || CMCD_EVENT_KEYS.includes(key) || isCmcdCustomKey(key); 21212} 21213 21214/** 21215 * Defines the request-specific keys for CMCD (Common Media Client Data) version 2. 21216 * 21217 * @group CMCD 21218 * 21219 * @beta 21220 */ 21221const CMCD_REQUEST_KEYS = ['d', 'dl', 'nor', 'ot', 'rtp', 'su']; 21222 21223/** 21224 * Check if a key is a valid CMCD request key. 21225 * 21226 * @param key - The key to check. 21227 * 21228 * @returns `true` if the key is a valid CMCD request key, `false` otherwise. 21229 * 21230 * @group CMCD 21231 * 21232 * @beta 21233 * 21234 * @example 21235 * {@includeCode ../../test/cmcd/isCmcdRequestKey.test.ts#example} 21236 */ 21237function isCmcdRequestKey(key) { 21238 return CMCD_COMMON_KEYS.includes(key) || CMCD_REQUEST_KEYS.includes(key) || isCmcdCustomKey(key); 21239} 21240 21241/** 21242 * CMCD v2 - Response-only and timing keys. 21243 * 21244 * @group CMCD 21245 * 21246 * @beta 21247 */ 21248const CMCD_RESPONSE_KEYS = ['cmsdd', 'cmsds', 'rc', 'smrt', 'ttfb', 'ttfbb', 'ttlb', 'url']; 21249 21250/** 21251 * Check if a key is a valid CMCD response key. 21252 * 21253 * @param key - The key to check. 21254 * 21255 * @returns `true` if the key is a valid CMCD request key, `false` otherwise. 21256 * 21257 * @group CMCD 21258 * 21259 * @beta 21260 * 21261 * @example 21262 * {@includeCode ../../test/cmcd/isCmcdResponseKey.test.ts#example} 21263 */ 21264function isCmcdResponseKey(key) { 21265 return CMCD_COMMON_KEYS.includes(key) || CMCD_REQUEST_KEYS.includes(key) || CMCD_RESPONSE_KEYS.includes(key) || isCmcdCustomKey(key); 21266} 21267 21268/** 21269 * Defines the keys for CMCD (Common Media Client Data) version 1. 21270 * 21271 * @group CMCD 21272 * 21273 * @beta 21274 */ 21275const CMCD_V1_KEYS = ['bl', 'br', 'bs', 'cid', 'd', 'dl', 'mtp', 'nor', 'nrr', 'ot', 'pr', 'rtp', 'sf', 'sid', 'st', 'su', 'tb', 'v']; 21276 21277/** 21278 * Filter function for CMCD v1 keys. 21279 * 21280 * @param key - The CMCD key to filter. 21281 * 21282 * @returns `true` if the key should be included, `false` otherwise. 21283 * 21284 * @group CMCD 21285 * 21286 * @beta 21287 * 21288 * @example 21289 * {@includeCode ../../test/cmcd/isCmcdV1Key.test.ts#example} 21290 */ 21291function isCmcdV1Key(key) { 21292 return CMCD_V1_KEYS.includes(key) || isCmcdCustomKey(key); 21293} 21294 21295const filterMap = { 21296 [CMCD_RESPONSE_MODE]: isCmcdResponseKey, 21297 [CMCD_EVENT_MODE]: isCmcdEventKey, 21298 [CMCD_REQUEST_MODE]: isCmcdRequestKey 21299}; 21300/** 21301 * Convert a generic object to CMCD data. 21302 * 21303 * @param obj - The CMCD object to process. 21304 * @param options - Options for encoding. 21305 * 21306 * @group CMCD 21307 * 21308 * @beta 21309 */ 21310function prepareCmcdData(obj, options = {}) { 21311 const results = {}; 21312 if (obj == null || typeof obj !== 'object') { 21313 return results; 21314 } 21315 const version = options.version || obj['v'] || 1; 21316 const reportingMode = options.reportingMode || CMCD_REQUEST_MODE; 21317 const keyFilter = version === 1 ? isCmcdV1Key : filterMap[reportingMode]; 21318 // Filter keys based on the version, reporting mode and options 21319 let keys = Object.keys(obj).filter(keyFilter); 21320 const filter = options.filter; 21321 if (typeof filter === 'function') { 21322 keys = keys.filter(filter); 21323 } 21324 // Ensure all required keys are present before sorting 21325 const needsTimestamp = reportingMode === CMCD_RESPONSE_MODE || reportingMode === CMCD_EVENT_MODE; 21326 if (needsTimestamp && !keys.includes('ts')) { 21327 keys.push('ts'); 21328 } 21329 if (version > 1 && !keys.includes('v')) { 21330 keys.push('v'); 21331 } 21332 const formatters = _extends({}, CMCD_FORMATTER_MAP, options.formatters); 21333 const formatterOptions = { 21334 version, 21335 reportingMode, 21336 baseUrl: options.baseUrl 21337 }; 21338 keys.sort().forEach(key => { 21339 let value = obj[key]; 21340 const formatter = formatters[key]; 21341 if (typeof formatter === 'function') { 21342 value = formatter(value, formatterOptions); 21343 } 21344 // Version should only be reported if not equal to 1. 21345 if (key === 'v') { 21346 if (version === 1) { 21347 return; 21348 } else { 21349 value = version; 21350 } 21351 } 21352 // Playback rate should only be sent if not equal to 1. 21353 if (key == 'pr' && value === 1) { 21354 return; 21355 } 21356 // Ensure a timestamp is set for response and event modes 21357 if (needsTimestamp && key === 'ts' && !isFiniteNumber(value)) { 21358 value = Date.now(); 21359 } 21360 // ignore invalid values 21361 if (!isValid(value)) { 21362 return; 21363 } 21364 if (isTokenField(key) && typeof value === 'string') { 21365 value = new SfToken(value); 21366 } 21367 results[key] = value; 21368 }); 21369 return results; 21370} 21371 21372/** 21373 * Convert a CMCD data object to request headers 21374 * 21375 * @param cmcd - The CMCD data object to convert. 21376 * @param options - Options for encoding the CMCD object. 21377 * 21378 * @returns The CMCD header shards. 21379 * 21380 * @group CMCD 21381 * 21382 * @beta 21383 * 21384 * @example 21385 * {@includeCode ../../test/cmcd/toCmcdHeaders.test.ts#example} 21386 */ 21387function toCmcdHeaders(cmcd, options = {}) { 21388 const result = {}; 21389 if (!cmcd) { 21390 return result; 21391 } 21392 const data = prepareCmcdData(cmcd, options); 21393 const shards = groupCmcdHeaders(data, options === null || options === void 0 ? void 0 : options.customHeaderMap); 21394 return Object.entries(shards).reduce((acc, [field, value]) => { 21395 const shard = encodeSfDict(value, { 21396 whitespace: false 21397 }); 21398 if (shard) { 21399 acc[field] = shard; 21400 } 21401 return acc; 21402 }, result); 21403} 21404 21405/** 21406 * Append CMCD query args to a header object. 21407 * 21408 * @param headers - The headers to append to. 21409 * @param cmcd - The CMCD object to append. 21410 * @param options - Encode options. 21411 * 21412 * @returns The headers with the CMCD header shards appended. 21413 * 21414 * @group CMCD 21415 * 21416 * @beta 21417 * 21418 * @example 21419 * {@includeCode ../../test/cmcd/appendCmcdHeaders.test.ts#example} 21420 */ 21421function appendCmcdHeaders(headers, cmcd, options) { 21422 return _extends(headers, toCmcdHeaders(cmcd, options)); 21423} 21424 21425/** 21426 * CMCD parameter name. 21427 * 21428 * @group CMCD 21429 * 21430 * @beta 21431 */ 21432const CMCD_PARAM = 'CMCD'; 21433 21434/** 21435 * Encode a CMCD object to a string. 21436 * 21437 * @param cmcd - The CMCD object to encode. 21438 * @param options - Options for encoding. 21439 * 21440 * @returns The encoded CMCD string. 21441 * 21442 * @group CMCD 21443 * 21444 * @beta 21445 * 21446 * @example 21447 * {@includeCode ../../test/cmcd/encodeCmcd.test.ts#example} 21448 */ 21449function encodeCmcd(cmcd, options = {}) { 21450 if (!cmcd) { 21451 return ''; 21452 } 21453 return encodeSfDict(prepareCmcdData(cmcd, options), { 21454 whitespace: false 21455 }); 21456} 21457 21458/** 21459 * Convert a CMCD data object to a URL encoded string. 21460 * 21461 * @param cmcd - The CMCD object to convert. 21462 * @param options - Options for encoding the CMCD object. 21463 * 21464 * @returns The URL encoded CMCD data. 21465 * 21466 * @group CMCD 21467 * 21468 * @beta 21469 */ 21470function toCmcdUrl(cmcd, options = {}) { 21471 if (!cmcd) { 21472 return ''; 21473 } 21474 const params = encodeCmcd(cmcd, options); 21475 return encodeURIComponent(params); 21476} 21477 21478/** 21479 * Convert a CMCD data object to a query arg. 21480 * 21481 * @param cmcd - The CMCD object to convert. 21482 * @param options - Options for encoding the CMCD object. 21483 * 21484 * @returns The CMCD query arg. 21485 * 21486 * @group CMCD 21487 * 21488 * @beta 21489 * 21490 * @example 21491 * {@includeCode ../../test/cmcd/toCmcdQuery.test.ts#example} 21492 */ 21493function toCmcdQuery(cmcd, options = {}) { 21494 if (!cmcd) { 21495 return ''; 21496 } 21497 const value = toCmcdUrl(cmcd, options); 21498 return `${CMCD_PARAM}=${value}`; 21499} 21500 21501const REGEX = /CMCD=[^&#]+/; 21502/** 21503 * Append CMCD query args to a URL. 21504 * 21505 * @param url - The URL to append to. 21506 * @param cmcd - The CMCD object to append. 21507 * @param options - Options for encoding the CMCD object. 21508 * 21509 * @returns The URL with the CMCD query args appended. 21510 * 21511 * @group CMCD 21512 * 21513 * @beta 21514 * 21515 * @example 21516 * {@includeCode ../../test/cmcd/appendCmcdQuery.test.ts#example} 21517 */ 21518function appendCmcdQuery(url, cmcd, options) { 21519 // TODO: Replace with URLSearchParams once we drop Safari < 10.1 & Chrome < 49 support. 21520 // https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams 21521 const query = toCmcdQuery(cmcd, options); 21522 if (!query) { 21523 return url; 21524 } 21525 if (REGEX.test(url)) { 21526 return url.replace(REGEX, query); 21527 } 21528 const separator = url.includes('?') ? '&' : '?'; 21529 return `${url}${separator}${query}`; 21530} 21531 21532/** 21533 * Controller to deal with Common Media Client Data (CMCD) 21534 * @see https://cdn.cta.tech/cta/media/media/resources/standards/pdfs/cta-5004-final.pdf 21535 */ 21536class CMCDController { 21537 constructor(hls) { 21538 this.hls = void 0; 21539 this.config = void 0; 21540 this.media = void 0; 21541 this.sid = void 0; 21542 this.cid = void 0; 21543 this.useHeaders = false; 21544 this.includeKeys = void 0; 21545 this.initialized = false; 21546 this.starved = false; 21547 this.buffering = true; 21548 this.audioBuffer = void 0; 21549 this.videoBuffer = void 0; 21550 this.onWaiting = () => { 21551 if (this.initialized) { 21552 this.starved = true; 21553 } 21554 this.buffering = true; 21555 }; 21556 this.onPlaying = () => { 21557 if (!this.initialized) { 21558 this.initialized = true; 21559 } 21560 this.buffering = false; 21561 }; 21562 /** 21563 * Apply CMCD data to a manifest request. 21564 */ 21565 this.applyPlaylistData = context => { 21566 try { 21567 this.apply(context, { 21568 ot: CmcdObjectType.MANIFEST, 21569 su: !this.initialized 21570 }); 21571 } catch (error) { 21572 this.hls.logger.warn('Could not generate manifest CMCD data.', error); 21573 } 21574 }; 21575 /** 21576 * Apply CMCD data to a segment request 21577 */ 21578 this.applyFragmentData = context => { 21579 try { 21580 const { 21581 frag, 21582 part 21583 } = context; 21584 const level = this.hls.levels[frag.level]; 21585 const ot = this.getObjectType(frag); 21586 const data = { 21587 d: (part || frag).duration * 1000, 21588 ot 21589 }; 21590 if (ot === CmcdObjectType.VIDEO || ot === CmcdObjectType.AUDIO || ot == CmcdObjectType.MUXED) { 21591 data.br = level.bitrate / 1000; 21592 data.tb = this.getTopBandwidth(ot) / 1000; 21593 data.bl = this.getBufferLength(ot); 21594 } 21595 const next = part ? this.getNextPart(part) : this.getNextFrag(frag); 21596 if (next != null && next.url && next.url !== frag.url) { 21597 data.nor = next.url; 21598 } 21599 this.apply(context, data); 21600 } catch (error) { 21601 this.hls.logger.warn('Could not generate segment CMCD data.', error); 21602 } 21603 }; 21604 this.hls = hls; 21605 const config = this.config = hls.config; 21606 const { 21607 cmcd 21608 } = config; 21609 if (cmcd != null) { 21610 config.pLoader = this.createPlaylistLoader(); 21611 config.fLoader = this.createFragmentLoader(); 21612 this.sid = cmcd.sessionId || hls.sessionId; 21613 this.cid = cmcd.contentId; 21614 this.useHeaders = cmcd.useHeaders === true; 21615 this.includeKeys = cmcd.includeKeys; 21616 this.registerListeners(); 21617 } 21618 } 21619 registerListeners() { 21620 const hls = this.hls; 21621 hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 21622 hls.on(Events.MEDIA_DETACHED, this.onMediaDetached, this); 21623 hls.on(Events.BUFFER_CREATED, this.onBufferCreated, this); 21624 } 21625 unregisterListeners() { 21626 const hls = this.hls; 21627 hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 21628 hls.off(Events.MEDIA_DETACHED, this.onMediaDetached, this); 21629 hls.off(Events.BUFFER_CREATED, this.onBufferCreated, this); 21630 } 21631 destroy() { 21632 this.unregisterListeners(); 21633 this.onMediaDetached(); 21634 21635 // @ts-ignore 21636 this.hls = this.config = this.audioBuffer = this.videoBuffer = null; 21637 // @ts-ignore 21638 this.onWaiting = this.onPlaying = this.media = null; 21639 } 21640 onMediaAttached(event, data) { 21641 this.media = data.media; 21642 this.media.addEventListener('waiting', this.onWaiting); 21643 this.media.addEventListener('playing', this.onPlaying); 21644 } 21645 onMediaDetached() { 21646 if (!this.media) { 21647 return; 21648 } 21649 this.media.removeEventListener('waiting', this.onWaiting); 21650 this.media.removeEventListener('playing', this.onPlaying); 21651 21652 // @ts-ignore 21653 this.media = null; 21654 } 21655 onBufferCreated(event, data) { 21656 var _data$tracks$audio, _data$tracks$video; 21657 this.audioBuffer = (_data$tracks$audio = data.tracks.audio) == null ? void 0 : _data$tracks$audio.buffer; 21658 this.videoBuffer = (_data$tracks$video = data.tracks.video) == null ? void 0 : _data$tracks$video.buffer; 21659 } 21660 /** 21661 * Create baseline CMCD data 21662 */ 21663 createData() { 21664 var _this$media; 21665 return { 21666 v: 1, 21667 sf: CmcdStreamingFormat.HLS, 21668 sid: this.sid, 21669 cid: this.cid, 21670 pr: (_this$media = this.media) == null ? void 0 : _this$media.playbackRate, 21671 mtp: this.hls.bandwidthEstimate / 1000 21672 }; 21673 } 21674 21675 /** 21676 * Apply CMCD data to a request. 21677 */ 21678 apply(context, data = {}) { 21679 // apply baseline data 21680 _extends(data, this.createData()); 21681 const isVideo = data.ot === CmcdObjectType.INIT || data.ot === CmcdObjectType.VIDEO || data.ot === CmcdObjectType.MUXED; 21682 if (this.starved && isVideo) { 21683 data.bs = true; 21684 data.su = true; 21685 this.starved = false; 21686 } 21687 if (data.su == null) { 21688 data.su = this.buffering; 21689 } 21690 21691 // TODO: Implement rtp, nrr, dl 21692 21693 const { 21694 includeKeys 21695 } = this; 21696 if (includeKeys) { 21697 data = Object.keys(data).reduce((acc, key) => { 21698 includeKeys.includes(key) && (acc[key] = data[key]); 21699 return acc; 21700 }, {}); 21701 } 21702 const options = { 21703 baseUrl: context.url 21704 }; 21705 if (this.useHeaders) { 21706 if (!context.headers) { 21707 context.headers = {}; 21708 } 21709 appendCmcdHeaders(context.headers, data, options); 21710 } else { 21711 context.url = appendCmcdQuery(context.url, data, options); 21712 } 21713 } 21714 getNextFrag(fragment) { 21715 var _this$hls$levels$frag; 21716 const levelDetails = (_this$hls$levels$frag = this.hls.levels[fragment.level]) == null ? void 0 : _this$hls$levels$frag.details; 21717 if (levelDetails) { 21718 const index = fragment.sn - levelDetails.startSN; 21719 return levelDetails.fragments[index + 1]; 21720 } 21721 return undefined; 21722 } 21723 getNextPart(part) { 21724 var _this$hls$levels$frag2; 21725 const { 21726 index, 21727 fragment 21728 } = part; 21729 const partList = (_this$hls$levels$frag2 = this.hls.levels[fragment.level]) == null || (_this$hls$levels$frag2 = _this$hls$levels$frag2.details) == null ? void 0 : _this$hls$levels$frag2.partList; 21730 if (partList) { 21731 const { 21732 sn 21733 } = fragment; 21734 for (let i = partList.length - 1; i >= 0; i--) { 21735 const p = partList[i]; 21736 if (p.index === index && p.fragment.sn === sn) { 21737 return partList[i + 1]; 21738 } 21739 } 21740 } 21741 return undefined; 21742 } 21743 21744 /** 21745 * The CMCD object type. 21746 */ 21747 getObjectType(fragment) { 21748 const { 21749 type 21750 } = fragment; 21751 if (type === 'subtitle') { 21752 return CmcdObjectType.TIMED_TEXT; 21753 } 21754 if (fragment.sn === 'initSegment') { 21755 return CmcdObjectType.INIT; 21756 } 21757 if (type === 'audio') { 21758 return CmcdObjectType.AUDIO; 21759 } 21760 if (type === 'main') { 21761 if (!this.hls.audioTracks.length) { 21762 return CmcdObjectType.MUXED; 21763 } 21764 return CmcdObjectType.VIDEO; 21765 } 21766 return undefined; 21767 } 21768 21769 /** 21770 * Get the highest bitrate. 21771 */ 21772 getTopBandwidth(type) { 21773 let bitrate = 0; 21774 let levels; 21775 const hls = this.hls; 21776 if (type === CmcdObjectType.AUDIO) { 21777 levels = hls.audioTracks; 21778 } else { 21779 const max = hls.maxAutoLevel; 21780 const len = max > -1 ? max + 1 : hls.levels.length; 21781 levels = hls.levels.slice(0, len); 21782 } 21783 levels.forEach(level => { 21784 if (level.bitrate > bitrate) { 21785 bitrate = level.bitrate; 21786 } 21787 }); 21788 return bitrate > 0 ? bitrate : NaN; 21789 } 21790 21791 /** 21792 * Get the buffer length for a media type in milliseconds 21793 */ 21794 getBufferLength(type) { 21795 const media = this.media; 21796 const buffer = type === CmcdObjectType.AUDIO ? this.audioBuffer : this.videoBuffer; 21797 if (!buffer || !media) { 21798 return NaN; 21799 } 21800 const info = BufferHelper.bufferInfo(buffer, media.currentTime, this.config.maxBufferHole); 21801 return info.len * 1000; 21802 } 21803 21804 /** 21805 * Create a playlist loader 21806 */ 21807 createPlaylistLoader() { 21808 const { 21809 pLoader 21810 } = this.config; 21811 const apply = this.applyPlaylistData; 21812 const Ctor = pLoader || this.config.loader; 21813 return class CmcdPlaylistLoader { 21814 constructor(config) { 21815 this.loader = void 0; 21816 this.loader = new Ctor(config); 21817 } 21818 get stats() { 21819 return this.loader.stats; 21820 } 21821 get context() { 21822 return this.loader.context; 21823 } 21824 destroy() { 21825 this.loader.destroy(); 21826 } 21827 abort() { 21828 this.loader.abort(); 21829 } 21830 load(context, config, callbacks) { 21831 apply(context); 21832 this.loader.load(context, config, callbacks); 21833 } 21834 }; 21835 } 21836 21837 /** 21838 * Create a playlist loader 21839 */ 21840 createFragmentLoader() { 21841 const { 21842 fLoader 21843 } = this.config; 21844 const apply = this.applyFragmentData; 21845 const Ctor = fLoader || this.config.loader; 21846 return class CmcdFragmentLoader { 21847 constructor(config) { 21848 this.loader = void 0; 21849 this.loader = new Ctor(config); 21850 } 21851 get stats() { 21852 return this.loader.stats; 21853 } 21854 get context() { 21855 return this.loader.context; 21856 } 21857 destroy() { 21858 this.loader.destroy(); 21859 } 21860 abort() { 21861 this.loader.abort(); 21862 } 21863 load(context, config, callbacks) { 21864 apply(context); 21865 this.loader.load(context, config, callbacks); 21866 } 21867 }; 21868 } 21869} 21870 21871const PATHWAY_PENALTY_DURATION_MS = 300000; 21872class ContentSteeringController extends Logger { 21873 constructor(hls) { 21874 super('content-steering', hls.logger); 21875 this.hls = void 0; 21876 this.loader = null; 21877 this.uri = null; 21878 this.pathwayId = '.'; 21879 this._pathwayPriority = null; 21880 this.timeToLoad = 300; 21881 this.reloadTimer = -1; 21882 this.updated = 0; 21883 this.started = false; 21884 this.enabled = true; 21885 this.levels = null; 21886 this.audioTracks = null; 21887 this.subtitleTracks = null; 21888 this.penalizedPathways = {}; 21889 this.hls = hls; 21890 this.registerListeners(); 21891 } 21892 registerListeners() { 21893 const hls = this.hls; 21894 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 21895 hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 21896 hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this); 21897 hls.on(Events.ERROR, this.onError, this); 21898 } 21899 unregisterListeners() { 21900 const hls = this.hls; 21901 if (!hls) { 21902 return; 21903 } 21904 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 21905 hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 21906 hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this); 21907 hls.off(Events.ERROR, this.onError, this); 21908 } 21909 pathways() { 21910 return (this.levels || []).reduce((pathways, level) => { 21911 if (pathways.indexOf(level.pathwayId) === -1) { 21912 pathways.push(level.pathwayId); 21913 } 21914 return pathways; 21915 }, []); 21916 } 21917 get pathwayPriority() { 21918 return this._pathwayPriority; 21919 } 21920 set pathwayPriority(pathwayPriority) { 21921 this.updatePathwayPriority(pathwayPriority); 21922 } 21923 startLoad() { 21924 this.started = true; 21925 this.clearTimeout(); 21926 if (this.enabled && this.uri) { 21927 if (this.updated) { 21928 const ttl = this.timeToLoad * 1000 - (performance.now() - this.updated); 21929 if (ttl > 0) { 21930 this.scheduleRefresh(this.uri, ttl); 21931 return; 21932 } 21933 } 21934 this.loadSteeringManifest(this.uri); 21935 } 21936 } 21937 stopLoad() { 21938 this.started = false; 21939 if (this.loader) { 21940 this.loader.destroy(); 21941 this.loader = null; 21942 } 21943 this.clearTimeout(); 21944 } 21945 clearTimeout() { 21946 if (this.reloadTimer !== -1) { 21947 self.clearTimeout(this.reloadTimer); 21948 this.reloadTimer = -1; 21949 } 21950 } 21951 destroy() { 21952 this.unregisterListeners(); 21953 this.stopLoad(); 21954 // @ts-ignore 21955 this.hls = null; 21956 this.levels = this.audioTracks = this.subtitleTracks = null; 21957 } 21958 removeLevel(levelToRemove) { 21959 const levels = this.levels; 21960 if (levels) { 21961 this.levels = levels.filter(level => level !== levelToRemove); 21962 } 21963 } 21964 onManifestLoading() { 21965 this.stopLoad(); 21966 this.enabled = true; 21967 this.timeToLoad = 300; 21968 this.updated = 0; 21969 this.uri = null; 21970 this.pathwayId = '.'; 21971 this.levels = this.audioTracks = this.subtitleTracks = null; 21972 } 21973 onManifestLoaded(event, data) { 21974 const { 21975 contentSteering 21976 } = data; 21977 if (contentSteering === null) { 21978 return; 21979 } 21980 this.pathwayId = contentSteering.pathwayId; 21981 this.uri = contentSteering.uri; 21982 if (this.started) { 21983 this.startLoad(); 21984 } 21985 } 21986 onManifestParsed(event, data) { 21987 this.audioTracks = data.audioTracks; 21988 this.subtitleTracks = data.subtitleTracks; 21989 } 21990 onError(event, data) { 21991 const { 21992 errorAction 21993 } = data; 21994 if ((errorAction == null ? void 0 : errorAction.action) === NetworkErrorAction.SendAlternateToPenaltyBox && errorAction.flags === ErrorActionFlags.MoveAllAlternatesMatchingHost) { 21995 const levels = this.levels; 21996 let pathwayPriority = this._pathwayPriority; 21997 let errorPathway = this.pathwayId; 21998 if (data.context) { 21999 const { 22000 groupId, 22001 pathwayId, 22002 type 22003 } = data.context; 22004 if (groupId && levels) { 22005 errorPathway = this.getPathwayForGroupId(groupId, type, errorPathway); 22006 } else if (pathwayId) { 22007 errorPathway = pathwayId; 22008 } 22009 } 22010 if (!(errorPathway in this.penalizedPathways)) { 22011 this.penalizedPathways[errorPathway] = performance.now(); 22012 } 22013 if (!pathwayPriority && levels) { 22014 // If PATHWAY-PRIORITY was not provided, list pathways for error handling 22015 pathwayPriority = this.pathways(); 22016 } 22017 if (pathwayPriority && pathwayPriority.length > 1) { 22018 this.updatePathwayPriority(pathwayPriority); 22019 errorAction.resolved = this.pathwayId !== errorPathway; 22020 } 22021 if (data.details === ErrorDetails.BUFFER_APPEND_ERROR && !data.fatal) { 22022 // Error will become fatal in buffer-controller when reaching `appendErrorMaxRetry` 22023 // Stream-controllers are expected to reduce buffer length even if this is not deemed a QuotaExceededError 22024 errorAction.resolved = true; 22025 } else if (!errorAction.resolved) { 22026 this.warn(`Could not resolve ${data.details} ("${data.error.message}") with content-steering for Pathway: ${errorPathway} levels: ${levels ? levels.length : levels} priorities: ${stringify(pathwayPriority)} penalized: ${stringify(this.penalizedPathways)}`); 22027 } 22028 } 22029 } 22030 filterParsedLevels(levels) { 22031 // Filter levels to only include those that are in the initial pathway 22032 this.levels = levels; 22033 let pathwayLevels = this.getLevelsForPathway(this.pathwayId); 22034 if (pathwayLevels.length === 0) { 22035 const pathwayId = levels[0].pathwayId; 22036 this.log(`No levels found in Pathway ${this.pathwayId}. Setting initial Pathway to "${pathwayId}"`); 22037 pathwayLevels = this.getLevelsForPathway(pathwayId); 22038 this.pathwayId = pathwayId; 22039 } 22040 if (pathwayLevels.length !== levels.length) { 22041 this.log(`Found ${pathwayLevels.length}/${levels.length} levels in Pathway "${this.pathwayId}"`); 22042 } 22043 return pathwayLevels; 22044 } 22045 getLevelsForPathway(pathwayId) { 22046 if (this.levels === null) { 22047 return []; 22048 } 22049 return this.levels.filter(level => pathwayId === level.pathwayId); 22050 } 22051 updatePathwayPriority(pathwayPriority) { 22052 this._pathwayPriority = pathwayPriority; 22053 let levels; 22054 22055 // Evaluate if we should remove the pathway from the penalized list 22056 const penalizedPathways = this.penalizedPathways; 22057 const now = performance.now(); 22058 Object.keys(penalizedPathways).forEach(pathwayId => { 22059 if (now - penalizedPathways[pathwayId] > PATHWAY_PENALTY_DURATION_MS) { 22060 delete penalizedPathways[pathwayId]; 22061 } 22062 }); 22063 for (let i = 0; i < pathwayPriority.length; i++) { 22064 const pathwayId = pathwayPriority[i]; 22065 if (pathwayId in penalizedPathways) { 22066 continue; 22067 } 22068 if (pathwayId === this.pathwayId) { 22069 return; 22070 } 22071 const selectedIndex = this.hls.nextLoadLevel; 22072 const selectedLevel = this.hls.levels[selectedIndex]; 22073 levels = this.getLevelsForPathway(pathwayId); 22074 if (levels.length > 0) { 22075 this.log(`Setting Pathway to "${pathwayId}"`); 22076 this.pathwayId = pathwayId; 22077 reassignFragmentLevelIndexes(levels); 22078 this.hls.trigger(Events.LEVELS_UPDATED, { 22079 levels 22080 }); 22081 // Set LevelController's level to trigger LEVEL_SWITCHING which loads playlist if needed 22082 const levelAfterChange = this.hls.levels[selectedIndex]; 22083 if (selectedLevel && levelAfterChange && this.levels) { 22084 if (levelAfterChange.attrs['STABLE-VARIANT-ID'] !== selectedLevel.attrs['STABLE-VARIANT-ID'] && levelAfterChange.bitrate !== selectedLevel.bitrate) { 22085 this.log(`Unstable Pathways change from bitrate ${selectedLevel.bitrate} to ${levelAfterChange.bitrate}`); 22086 } 22087 this.hls.nextLoadLevel = selectedIndex; 22088 } 22089 break; 22090 } 22091 } 22092 } 22093 getPathwayForGroupId(groupId, type, defaultPathway) { 22094 const levels = this.getLevelsForPathway(defaultPathway).concat(this.levels || []); 22095 for (let i = 0; i < levels.length; i++) { 22096 if (type === PlaylistContextType.AUDIO_TRACK && levels[i].hasAudioGroup(groupId) || type === PlaylistContextType.SUBTITLE_TRACK && levels[i].hasSubtitleGroup(groupId)) { 22097 return levels[i].pathwayId; 22098 } 22099 } 22100 return defaultPathway; 22101 } 22102 clonePathways(pathwayClones) { 22103 const levels = this.levels; 22104 if (!levels) { 22105 return; 22106 } 22107 const audioGroupCloneMap = {}; 22108 const subtitleGroupCloneMap = {}; 22109 pathwayClones.forEach(pathwayClone => { 22110 const { 22111 ID: cloneId, 22112 'BASE-ID': baseId, 22113 'URI-REPLACEMENT': uriReplacement 22114 } = pathwayClone; 22115 if (levels.some(level => level.pathwayId === cloneId)) { 22116 return; 22117 } 22118 const clonedVariants = this.getLevelsForPathway(baseId).map(baseLevel => { 22119 const attributes = new AttrList(baseLevel.attrs); 22120 attributes['PATHWAY-ID'] = cloneId; 22121 const clonedAudioGroupId = attributes.AUDIO && `${attributes.AUDIO}_clone_${cloneId}`; 22122 const clonedSubtitleGroupId = attributes.SUBTITLES && `${attributes.SUBTITLES}_clone_${cloneId}`; 22123 if (clonedAudioGroupId) { 22124 audioGroupCloneMap[attributes.AUDIO] = clonedAudioGroupId; 22125 attributes.AUDIO = clonedAudioGroupId; 22126 } 22127 if (clonedSubtitleGroupId) { 22128 subtitleGroupCloneMap[attributes.SUBTITLES] = clonedSubtitleGroupId; 22129 attributes.SUBTITLES = clonedSubtitleGroupId; 22130 } 22131 const url = performUriReplacement(baseLevel.uri, attributes['STABLE-VARIANT-ID'], 'PER-VARIANT-URIS', uriReplacement); 22132 const clonedLevel = new Level({ 22133 attrs: attributes, 22134 audioCodec: baseLevel.audioCodec, 22135 bitrate: baseLevel.bitrate, 22136 height: baseLevel.height, 22137 name: baseLevel.name, 22138 url, 22139 videoCodec: baseLevel.videoCodec, 22140 width: baseLevel.width 22141 }); 22142 if (baseLevel.audioGroups) { 22143 for (let i = 1; i < baseLevel.audioGroups.length; i++) { 22144 clonedLevel.addGroupId('audio', `${baseLevel.audioGroups[i]}_clone_${cloneId}`); 22145 } 22146 } 22147 if (baseLevel.subtitleGroups) { 22148 for (let i = 1; i < baseLevel.subtitleGroups.length; i++) { 22149 clonedLevel.addGroupId('text', `${baseLevel.subtitleGroups[i]}_clone_${cloneId}`); 22150 } 22151 } 22152 return clonedLevel; 22153 }); 22154 levels.push(...clonedVariants); 22155 cloneRenditionGroups(this.audioTracks, audioGroupCloneMap, uriReplacement, cloneId); 22156 cloneRenditionGroups(this.subtitleTracks, subtitleGroupCloneMap, uriReplacement, cloneId); 22157 }); 22158 } 22159 loadSteeringManifest(uri) { 22160 const config = this.hls.config; 22161 const Loader = config.loader; 22162 if (this.loader) { 22163 this.loader.destroy(); 22164 } 22165 this.loader = new Loader(config); 22166 let url; 22167 try { 22168 url = new self.URL(uri); 22169 } catch (error) { 22170 this.enabled = false; 22171 this.log(`Failed to parse Steering Manifest URI: ${uri}`); 22172 return; 22173 } 22174 if (url.protocol !== 'data:') { 22175 const throughput = (this.hls.bandwidthEstimate || config.abrEwmaDefaultEstimate) | 0; 22176 url.searchParams.set('_HLS_pathway', this.pathwayId); 22177 url.searchParams.set('_HLS_throughput', '' + throughput); 22178 } 22179 const context = { 22180 responseType: 'json', 22181 url: url.href 22182 }; 22183 const loadPolicy = config.steeringManifestLoadPolicy.default; 22184 const legacyRetryCompatibility = loadPolicy.errorRetry || loadPolicy.timeoutRetry || {}; 22185 const loaderConfig = { 22186 loadPolicy, 22187 timeout: loadPolicy.maxLoadTimeMs, 22188 maxRetry: legacyRetryCompatibility.maxNumRetry || 0, 22189 retryDelay: legacyRetryCompatibility.retryDelayMs || 0, 22190 maxRetryDelay: legacyRetryCompatibility.maxRetryDelayMs || 0 22191 }; 22192 const callbacks = { 22193 onSuccess: (response, stats, context, networkDetails) => { 22194 this.log(`Loaded steering manifest: "${url}"`); 22195 const steeringData = response.data; 22196 if ((steeringData == null ? void 0 : steeringData.VERSION) !== 1) { 22197 this.log(`Steering VERSION ${steeringData.VERSION} not supported!`); 22198 return; 22199 } 22200 this.updated = performance.now(); 22201 this.timeToLoad = steeringData.TTL; 22202 const { 22203 'RELOAD-URI': reloadUri, 22204 'PATHWAY-CLONES': pathwayClones, 22205 'PATHWAY-PRIORITY': pathwayPriority 22206 } = steeringData; 22207 if (reloadUri) { 22208 try { 22209 this.uri = new self.URL(reloadUri, url).href; 22210 } catch (error) { 22211 this.enabled = false; 22212 this.log(`Failed to parse Steering Manifest RELOAD-URI: ${reloadUri}`); 22213 return; 22214 } 22215 } 22216 this.scheduleRefresh(this.uri || context.url); 22217 if (pathwayClones) { 22218 this.clonePathways(pathwayClones); 22219 } 22220 const loadedSteeringData = { 22221 steeringManifest: steeringData, 22222 url: url.toString() 22223 }; 22224 this.hls.trigger(Events.STEERING_MANIFEST_LOADED, loadedSteeringData); 22225 if (pathwayPriority) { 22226 this.updatePathwayPriority(pathwayPriority); 22227 } 22228 }, 22229 onError: (error, context, networkDetails, stats) => { 22230 this.log(`Error loading steering manifest: ${error.code} ${error.text} (${context.url})`); 22231 this.stopLoad(); 22232 if (error.code === 410) { 22233 this.enabled = false; 22234 this.log(`Steering manifest ${context.url} no longer available`); 22235 return; 22236 } 22237 let ttl = this.timeToLoad * 1000; 22238 if (error.code === 429) { 22239 const loader = this.loader; 22240 if (typeof (loader == null ? void 0 : loader.getResponseHeader) === 'function') { 22241 const retryAfter = loader.getResponseHeader('Retry-After'); 22242 if (retryAfter) { 22243 ttl = parseFloat(retryAfter) * 1000; 22244 } 22245 } 22246 this.log(`Steering manifest ${context.url} rate limited`); 22247 return; 22248 } 22249 this.scheduleRefresh(this.uri || context.url, ttl); 22250 }, 22251 onTimeout: (stats, context, networkDetails) => { 22252 this.log(`Timeout loading steering manifest (${context.url})`); 22253 this.scheduleRefresh(this.uri || context.url); 22254 } 22255 }; 22256 this.log(`Requesting steering manifest: ${url}`); 22257 this.loader.load(context, loaderConfig, callbacks); 22258 } 22259 scheduleRefresh(uri, ttlMs = this.timeToLoad * 1000) { 22260 this.clearTimeout(); 22261 this.reloadTimer = self.setTimeout(() => { 22262 var _this$hls; 22263 const media = (_this$hls = this.hls) == null ? void 0 : _this$hls.media; 22264 if (media && !media.ended) { 22265 this.loadSteeringManifest(uri); 22266 return; 22267 } 22268 this.scheduleRefresh(uri, this.timeToLoad * 1000); 22269 }, ttlMs); 22270 } 22271} 22272function cloneRenditionGroups(tracks, groupCloneMap, uriReplacement, cloneId) { 22273 if (!tracks) { 22274 return; 22275 } 22276 Object.keys(groupCloneMap).forEach(audioGroupId => { 22277 const clonedTracks = tracks.filter(track => track.groupId === audioGroupId).map(track => { 22278 const clonedTrack = _extends({}, track); 22279 clonedTrack.details = undefined; 22280 clonedTrack.attrs = new AttrList(clonedTrack.attrs); 22281 clonedTrack.url = clonedTrack.attrs.URI = performUriReplacement(track.url, track.attrs['STABLE-RENDITION-ID'], 'PER-RENDITION-URIS', uriReplacement); 22282 clonedTrack.groupId = clonedTrack.attrs['GROUP-ID'] = groupCloneMap[audioGroupId]; 22283 clonedTrack.attrs['PATHWAY-ID'] = cloneId; 22284 return clonedTrack; 22285 }); 22286 tracks.push(...clonedTracks); 22287 }); 22288} 22289function performUriReplacement(uri, stableId, perOptionKey, uriReplacement) { 22290 const { 22291 HOST: host, 22292 PARAMS: params, 22293 [perOptionKey]: perOptionUris 22294 } = uriReplacement; 22295 let perVariantUri; 22296 if (stableId) { 22297 perVariantUri = perOptionUris == null ? void 0 : perOptionUris[stableId]; 22298 if (perVariantUri) { 22299 uri = perVariantUri; 22300 } 22301 } 22302 const url = new self.URL(uri); 22303 if (host && !perVariantUri) { 22304 url.host = host; 22305 } 22306 if (params) { 22307 Object.keys(params).sort().forEach(key => { 22308 if (key) { 22309 url.searchParams.set(key, params[key]); 22310 } 22311 }); 22312 } 22313 return url.href; 22314} 22315 22316/** 22317 * Controller to deal with encrypted media extensions (EME) 22318 * @see https://developer.mozilla.org/en-US/docs/Web/API/Encrypted_Media_Extensions_API 22319 * 22320 * @class 22321 * @constructor 22322 */ 22323class EMEController extends Logger { 22324 constructor(hls) { 22325 super('eme', hls.logger); 22326 this.hls = void 0; 22327 this.config = void 0; 22328 this.media = null; 22329 this.mediaResolved = void 0; 22330 this.keyFormatPromise = null; 22331 this.keySystemAccessPromises = {}; 22332 this._requestLicenseFailureCount = 0; 22333 this.mediaKeySessions = []; 22334 this.keyIdToKeySessionPromise = {}; 22335 this.mediaKeys = null; 22336 this.setMediaKeysQueue = EMEController.CDMCleanupPromise ? [EMEController.CDMCleanupPromise] : []; 22337 this.bannedKeyIds = {}; 22338 this.onMediaEncrypted = event => { 22339 const { 22340 initDataType, 22341 initData 22342 } = event; 22343 const logMessage = `"${event.type}" event: init data type: "${initDataType}"`; 22344 this.debug(logMessage); 22345 22346 // Ignore event when initData is null 22347 if (initData === null) { 22348 return; 22349 } 22350 if (!this.keyFormatPromise) { 22351 let keySystems = Object.keys(this.keySystemAccessPromises); 22352 if (!keySystems.length) { 22353 keySystems = getKeySystemsForConfig(this.config); 22354 } 22355 const keyFormats = keySystems.map(keySystemDomainToKeySystemFormat).filter(k => !!k); 22356 this.keyFormatPromise = this.getKeyFormatPromise(keyFormats); 22357 } 22358 this.keyFormatPromise.then(keySystemFormat => { 22359 const keySystem = keySystemFormatToKeySystemDomain(keySystemFormat); 22360 if (initDataType !== 'sinf' || keySystem !== KeySystems.FAIRPLAY) { 22361 this.log(`Ignoring "${event.type}" event with init data type: "${initDataType}" for selected key-system ${keySystem}`); 22362 return; 22363 } 22364 22365 // Match sinf keyId to playlist skd://keyId= 22366 let keyId; 22367 try { 22368 const json = bin2str(new Uint8Array(initData)); 22369 const sinf = base64Decode(JSON.parse(json).sinf); 22370 const tenc = parseSinf(sinf); 22371 if (!tenc) { 22372 throw new Error(`'schm' box missing or not cbcs/cenc with schi > tenc`); 22373 } 22374 keyId = new Uint8Array(tenc.subarray(8, 24)); 22375 } catch (error) { 22376 this.warn(`${logMessage} Failed to parse sinf: ${error}`); 22377 return; 22378 } 22379 const keyIdHex = arrayToHex(keyId); 22380 const { 22381 keyIdToKeySessionPromise, 22382 mediaKeySessions 22383 } = this; 22384 let keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex]; 22385 for (let i = 0; i < mediaKeySessions.length; i++) { 22386 // Match playlist key 22387 const keyContext = mediaKeySessions[i]; 22388 const decryptdata = keyContext.decryptdata; 22389 if (!decryptdata.keyId) { 22390 continue; 22391 } 22392 const oldKeyIdHex = arrayToHex(decryptdata.keyId); 22393 if (arrayValuesMatch(keyId, decryptdata.keyId) || decryptdata.uri.replace(/-/g, '').indexOf(keyIdHex) !== -1) { 22394 keySessionContextPromise = keyIdToKeySessionPromise[oldKeyIdHex]; 22395 if (!keySessionContextPromise) { 22396 continue; 22397 } 22398 if (decryptdata.pssh) { 22399 break; 22400 } 22401 delete keyIdToKeySessionPromise[oldKeyIdHex]; 22402 decryptdata.pssh = new Uint8Array(initData); 22403 decryptdata.keyId = keyId; 22404 keySessionContextPromise = keyIdToKeySessionPromise[keyIdHex] = keySessionContextPromise.then(() => { 22405 return this.generateRequestWithPreferredKeySession(keyContext, initDataType, initData, 'encrypted-event-key-match'); 22406 }); 22407 keySessionContextPromise.catch(error => this.handleError(error)); 22408 break; 22409 } 22410 } 22411 if (!keySessionContextPromise) { 22412 this.handleError(new Error(`Key ID ${keyIdHex} not encountered in playlist. Key-system sessions ${mediaKeySessions.length}.`)); 22413 } 22414 }).catch(error => this.handleError(error)); 22415 }; 22416 this.onWaitingForKey = event => { 22417 this.log(`"${event.type}" event`); 22418 }; 22419 this.hls = hls; 22420 this.config = hls.config; 22421 this.registerListeners(); 22422 } 22423 destroy() { 22424 this.onDestroying(); 22425 this.onMediaDetached(); 22426 // Remove any references that could be held in config options or callbacks 22427 const config = this.config; 22428 config.requestMediaKeySystemAccessFunc = null; 22429 config.licenseXhrSetup = config.licenseResponseCallback = undefined; 22430 config.drmSystems = config.drmSystemOptions = {}; 22431 // @ts-ignore 22432 this.hls = this.config = this.keyIdToKeySessionPromise = null; 22433 // @ts-ignore 22434 this.onMediaEncrypted = this.onWaitingForKey = null; 22435 } 22436 registerListeners() { 22437 this.hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 22438 this.hls.on(Events.MEDIA_DETACHED, this.onMediaDetached, this); 22439 this.hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 22440 this.hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 22441 this.hls.on(Events.DESTROYING, this.onDestroying, this); 22442 } 22443 unregisterListeners() { 22444 this.hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 22445 this.hls.off(Events.MEDIA_DETACHED, this.onMediaDetached, this); 22446 this.hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 22447 this.hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 22448 this.hls.off(Events.DESTROYING, this.onDestroying, this); 22449 } 22450 getLicenseServerUrl(keySystem) { 22451 const { 22452 drmSystems, 22453 widevineLicenseUrl 22454 } = this.config; 22455 const keySystemConfiguration = drmSystems == null ? void 0 : drmSystems[keySystem]; 22456 if (keySystemConfiguration) { 22457 return keySystemConfiguration.licenseUrl; 22458 } 22459 22460 // For backward compatibility 22461 if (keySystem === KeySystems.WIDEVINE && widevineLicenseUrl) { 22462 return widevineLicenseUrl; 22463 } 22464 } 22465 getLicenseServerUrlOrThrow(keySystem) { 22466 const url = this.getLicenseServerUrl(keySystem); 22467 if (url === undefined) { 22468 throw new Error(`no license server URL configured for key-system "${keySystem}"`); 22469 } 22470 return url; 22471 } 22472 getServerCertificateUrl(keySystem) { 22473 const { 22474 drmSystems 22475 } = this.config; 22476 const keySystemConfiguration = drmSystems == null ? void 0 : drmSystems[keySystem]; 22477 if (keySystemConfiguration) { 22478 return keySystemConfiguration.serverCertificateUrl; 22479 } else { 22480 this.log(`No Server Certificate in config.drmSystems["${keySystem}"]`); 22481 } 22482 } 22483 attemptKeySystemAccess(keySystemsToAttempt) { 22484 const levels = this.hls.levels; 22485 const uniqueCodec = (value, i, a) => !!value && a.indexOf(value) === i; 22486 const audioCodecs = levels.map(level => level.audioCodec).filter(uniqueCodec); 22487 const videoCodecs = levels.map(level => level.videoCodec).filter(uniqueCodec); 22488 if (audioCodecs.length + videoCodecs.length === 0) { 22489 videoCodecs.push('avc1.42e01e'); 22490 } 22491 return new Promise((resolve, reject) => { 22492 const attempt = keySystems => { 22493 const keySystem = keySystems.shift(); 22494 this.getMediaKeysPromise(keySystem, audioCodecs, videoCodecs).then(mediaKeys => resolve({ 22495 keySystem, 22496 mediaKeys 22497 })).catch(error => { 22498 if (keySystems.length) { 22499 attempt(keySystems); 22500 } else if (error instanceof EMEKeyError) { 22501 reject(error); 22502 } else { 22503 reject(new EMEKeyError({ 22504 type: ErrorTypes.KEY_SYSTEM_ERROR, 22505 details: ErrorDetails.KEY_SYSTEM_NO_ACCESS, 22506 error, 22507 fatal: true 22508 }, error.message)); 22509 } 22510 }); 22511 }; 22512 attempt(keySystemsToAttempt); 22513 }); 22514 } 22515 requestMediaKeySystemAccess(keySystem, supportedConfigurations) { 22516 const { 22517 requestMediaKeySystemAccessFunc 22518 } = this.config; 22519 if (!(typeof requestMediaKeySystemAccessFunc === 'function')) { 22520 let errMessage = `Configured requestMediaKeySystemAccess is not a function ${requestMediaKeySystemAccessFunc}`; 22521 if (requestMediaKeySystemAccess === null && self.location.protocol === 'http:') { 22522 errMessage = `navigator.requestMediaKeySystemAccess is not available over insecure protocol ${location.protocol}`; 22523 } 22524 return Promise.reject(new Error(errMessage)); 22525 } 22526 return requestMediaKeySystemAccessFunc(keySystem, supportedConfigurations); 22527 } 22528 getMediaKeysPromise(keySystem, audioCodecs, videoCodecs) { 22529 var _keySystemAccessPromi; 22530 // This can throw, but is caught in event handler callpath 22531 const mediaKeySystemConfigs = getSupportedMediaKeySystemConfigurations(keySystem, audioCodecs, videoCodecs, this.config.drmSystemOptions || {}); 22532 let keySystemAccessPromises = this.keySystemAccessPromises[keySystem]; 22533 let keySystemAccess = (_keySystemAccessPromi = keySystemAccessPromises) == null ? void 0 : _keySystemAccessPromi.keySystemAccess; 22534 if (!keySystemAccess) { 22535 this.log(`Requesting encrypted media "${keySystem}" key-system access with config: ${stringify(mediaKeySystemConfigs)}`); 22536 keySystemAccess = this.requestMediaKeySystemAccess(keySystem, mediaKeySystemConfigs); 22537 const keySystemAccessPromisesNew = keySystemAccessPromises = this.keySystemAccessPromises[keySystem] = { 22538 keySystemAccess 22539 }; 22540 keySystemAccess.catch(error => { 22541 this.log(`Failed to obtain access to key-system "${keySystem}": ${error}`); 22542 }); 22543 return keySystemAccess.then(mediaKeySystemAccess => { 22544 this.log(`Access for key-system "${mediaKeySystemAccess.keySystem}" obtained`); 22545 const certificateRequest = this.fetchServerCertificate(keySystem); 22546 this.log(`Create media-keys for "${keySystem}"`); 22547 const mediaKeys = keySystemAccessPromisesNew.mediaKeys = mediaKeySystemAccess.createMediaKeys().then(mediaKeys => { 22548 this.log(`Media-keys created for "${keySystem}"`); 22549 keySystemAccessPromisesNew.hasMediaKeys = true; 22550 return certificateRequest.then(certificate => { 22551 if (certificate) { 22552 return this.setMediaKeysServerCertificate(mediaKeys, keySystem, certificate); 22553 } 22554 return mediaKeys; 22555 }); 22556 }); 22557 mediaKeys.catch(error => { 22558 this.error(`Failed to create media-keys for "${keySystem}"}: ${error}`); 22559 }); 22560 return mediaKeys; 22561 }); 22562 } 22563 return keySystemAccess.then(() => keySystemAccessPromises.mediaKeys); 22564 } 22565 createMediaKeySessionContext({ 22566 decryptdata, 22567 keySystem, 22568 mediaKeys 22569 }) { 22570 this.log(`Creating key-system session "${keySystem}" keyId: ${arrayToHex(decryptdata.keyId || [])} keyUri: ${decryptdata.uri}`); 22571 const mediaKeysSession = mediaKeys.createSession(); 22572 const mediaKeySessionContext = { 22573 decryptdata, 22574 keySystem, 22575 mediaKeys, 22576 mediaKeysSession, 22577 keyStatus: 'status-pending' 22578 }; 22579 this.mediaKeySessions.push(mediaKeySessionContext); 22580 return mediaKeySessionContext; 22581 } 22582 renewKeySession(mediaKeySessionContext) { 22583 const decryptdata = mediaKeySessionContext.decryptdata; 22584 if (decryptdata.pssh) { 22585 const keySessionContext = this.createMediaKeySessionContext(mediaKeySessionContext); 22586 const keyId = getKeyIdString(decryptdata); 22587 const scheme = 'cenc'; 22588 this.keyIdToKeySessionPromise[keyId] = this.generateRequestWithPreferredKeySession(keySessionContext, scheme, decryptdata.pssh.buffer, 'expired'); 22589 } else { 22590 this.warn(`Could not renew expired session. Missing pssh initData.`); 22591 } 22592 // eslint-disable-next-line @typescript-eslint/no-floating-promises 22593 this.removeSession(mediaKeySessionContext); 22594 } 22595 updateKeySession(mediaKeySessionContext, data) { 22596 const keySession = mediaKeySessionContext.mediaKeysSession; 22597 this.log(`Updating key-session "${keySession.sessionId}" for keyId ${arrayToHex(mediaKeySessionContext.decryptdata.keyId || [])} 22598 } (data length: ${data.byteLength})`); 22599 return keySession.update(data); 22600 } 22601 getSelectedKeySystemFormats() { 22602 return Object.keys(this.keySystemAccessPromises).map(keySystem => ({ 22603 keySystem, 22604 hasMediaKeys: this.keySystemAccessPromises[keySystem].hasMediaKeys 22605 })).filter(({ 22606 hasMediaKeys 22607 }) => !!hasMediaKeys).map(({ 22608 keySystem 22609 }) => keySystemDomainToKeySystemFormat(keySystem)).filter(keySystem => !!keySystem); 22610 } 22611 getKeySystemAccess(keySystemsToAttempt) { 22612 return this.getKeySystemSelectionPromise(keySystemsToAttempt).then(({ 22613 keySystem, 22614 mediaKeys 22615 }) => { 22616 return this.attemptSetMediaKeys(keySystem, mediaKeys); 22617 }); 22618 } 22619 selectKeySystem(keySystemsToAttempt) { 22620 return new Promise((resolve, reject) => { 22621 this.getKeySystemSelectionPromise(keySystemsToAttempt).then(({ 22622 keySystem 22623 }) => { 22624 const keySystemFormat = keySystemDomainToKeySystemFormat(keySystem); 22625 if (keySystemFormat) { 22626 resolve(keySystemFormat); 22627 } else { 22628 reject(new Error(`Unable to find format for key-system "${keySystem}"`)); 22629 } 22630 }).catch(reject); 22631 }); 22632 } 22633 selectKeySystemFormat(frag) { 22634 const keyFormats = Object.keys(frag.levelkeys || {}); 22635 if (!this.keyFormatPromise) { 22636 this.log(`Selecting key-system from fragment (sn: ${frag.sn} ${frag.type}: ${frag.level}) key formats ${keyFormats.join(', ')}`); 22637 this.keyFormatPromise = this.getKeyFormatPromise(keyFormats); 22638 } 22639 return this.keyFormatPromise; 22640 } 22641 getKeyFormatPromise(keyFormats) { 22642 const keySystemsInConfig = getKeySystemsForConfig(this.config); 22643 const keySystemsToAttempt = keyFormats.map(keySystemFormatToKeySystemDomain).filter(value => !!value && keySystemsInConfig.indexOf(value) !== -1); 22644 return this.selectKeySystem(keySystemsToAttempt); 22645 } 22646 getKeyStatus(decryptdata) { 22647 const { 22648 mediaKeySessions 22649 } = this; 22650 for (let i = 0; i < mediaKeySessions.length; i++) { 22651 const status = getKeyStatus(decryptdata, mediaKeySessions[i]); 22652 if (status) { 22653 return status; 22654 } 22655 } 22656 return undefined; 22657 } 22658 loadKey(data) { 22659 const decryptdata = data.keyInfo.decryptdata; 22660 const keyId = getKeyIdString(decryptdata); 22661 const badStatus = this.bannedKeyIds[keyId]; 22662 if (badStatus || this.getKeyStatus(decryptdata) === 'internal-error') { 22663 const error = getKeyStatusError(badStatus || 'internal-error', decryptdata); 22664 this.handleError(error, data.frag); 22665 return Promise.reject(error); 22666 } 22667 const keyDetails = `(keyId: ${keyId} format: "${decryptdata.keyFormat}" method: ${decryptdata.method} uri: ${decryptdata.uri})`; 22668 this.log(`Starting session for key ${keyDetails}`); 22669 const keyContextPromise = this.keyIdToKeySessionPromise[keyId]; 22670 if (!keyContextPromise) { 22671 const keySessionContextPromise = this.getKeySystemForKeyPromise(decryptdata).then(({ 22672 keySystem, 22673 mediaKeys 22674 }) => { 22675 this.throwIfDestroyed(); 22676 this.log(`Handle encrypted media sn: ${data.frag.sn} ${data.frag.type}: ${data.frag.level} using key ${keyDetails}`); 22677 return this.attemptSetMediaKeys(keySystem, mediaKeys).then(() => { 22678 this.throwIfDestroyed(); 22679 return this.createMediaKeySessionContext({ 22680 keySystem, 22681 mediaKeys, 22682 decryptdata 22683 }); 22684 }); 22685 }).then(keySessionContext => { 22686 const scheme = 'cenc'; 22687 const initData = decryptdata.pssh ? decryptdata.pssh.buffer : null; 22688 return this.generateRequestWithPreferredKeySession(keySessionContext, scheme, initData, 'playlist-key'); 22689 }); 22690 keySessionContextPromise.catch(error => this.handleError(error, data.frag)); 22691 this.keyIdToKeySessionPromise[keyId] = keySessionContextPromise; 22692 return keySessionContextPromise; 22693 } 22694 22695 // Re-emit error for playlist key loading 22696 keyContextPromise.catch(error => { 22697 if (error instanceof EMEKeyError) { 22698 const errorData = _objectSpread2({}, error.data); 22699 if (this.getKeyStatus(decryptdata) === 'internal-error') { 22700 errorData.decryptdata = decryptdata; 22701 } 22702 const clonedError = new EMEKeyError(errorData, error.message); 22703 this.handleError(clonedError, data.frag); 22704 } 22705 }); 22706 return keyContextPromise; 22707 } 22708 throwIfDestroyed(message = 'Invalid state') { 22709 if (!this.hls) { 22710 throw new Error('invalid state'); 22711 } 22712 } 22713 handleError(error, frag) { 22714 if (!this.hls) { 22715 return; 22716 } 22717 if (error instanceof EMEKeyError) { 22718 if (frag) { 22719 error.data.frag = frag; 22720 } 22721 const levelKey = error.data.decryptdata; 22722 this.error(`${error.message}${levelKey ? ` (${arrayToHex(levelKey.keyId || [])})` : ''}`); 22723 this.hls.trigger(Events.ERROR, error.data); 22724 } else { 22725 this.error(error.message); 22726 this.hls.trigger(Events.ERROR, { 22727 type: ErrorTypes.KEY_SYSTEM_ERROR, 22728 details: ErrorDetails.KEY_SYSTEM_NO_KEYS, 22729 error, 22730 fatal: true 22731 }); 22732 } 22733 } 22734 getKeySystemForKeyPromise(decryptdata) { 22735 const keyId = getKeyIdString(decryptdata); 22736 const mediaKeySessionContext = this.keyIdToKeySessionPromise[keyId]; 22737 if (!mediaKeySessionContext) { 22738 const keySystem = keySystemFormatToKeySystemDomain(decryptdata.keyFormat); 22739 const keySystemsToAttempt = keySystem ? [keySystem] : getKeySystemsForConfig(this.config); 22740 return this.attemptKeySystemAccess(keySystemsToAttempt); 22741 } 22742 return mediaKeySessionContext; 22743 } 22744 getKeySystemSelectionPromise(keySystemsToAttempt) { 22745 if (!keySystemsToAttempt.length) { 22746 keySystemsToAttempt = getKeySystemsForConfig(this.config); 22747 } 22748 if (keySystemsToAttempt.length === 0) { 22749 throw new EMEKeyError({ 22750 type: ErrorTypes.KEY_SYSTEM_ERROR, 22751 details: ErrorDetails.KEY_SYSTEM_NO_CONFIGURED_LICENSE, 22752 fatal: true 22753 }, `Missing key-system license configuration options ${stringify({ 22754 drmSystems: this.config.drmSystems 22755 })}`); 22756 } 22757 return this.attemptKeySystemAccess(keySystemsToAttempt); 22758 } 22759 attemptSetMediaKeys(keySystem, mediaKeys) { 22760 this.mediaResolved = undefined; 22761 if (this.mediaKeys === mediaKeys) { 22762 return Promise.resolve(); 22763 } 22764 const queue = this.setMediaKeysQueue.slice(); 22765 this.log(`Setting media-keys for "${keySystem}"`); 22766 // Only one setMediaKeys() can run at one time, and multiple setMediaKeys() operations 22767 // can be queued for execution for multiple key sessions. 22768 const setMediaKeysPromise = Promise.all(queue).then(() => { 22769 if (!this.media) { 22770 return new Promise((resolve, reject) => { 22771 this.mediaResolved = () => { 22772 this.mediaResolved = undefined; 22773 if (!this.media) { 22774 return reject(new Error('Attempted to set mediaKeys without media element attached')); 22775 } 22776 this.mediaKeys = mediaKeys; 22777 this.media.setMediaKeys(mediaKeys).then(resolve).catch(reject); 22778 }; 22779 }); 22780 } 22781 return this.media.setMediaKeys(mediaKeys); 22782 }); 22783 this.mediaKeys = mediaKeys; 22784 this.setMediaKeysQueue.push(setMediaKeysPromise); 22785 return setMediaKeysPromise.then(() => { 22786 this.log(`Media-keys set for "${keySystem}"`); 22787 queue.push(setMediaKeysPromise); 22788 this.setMediaKeysQueue = this.setMediaKeysQueue.filter(p => queue.indexOf(p) === -1); 22789 }); 22790 } 22791 generateRequestWithPreferredKeySession(context, initDataType, initData, reason) { 22792 var _this$config$drmSyste; 22793 const generateRequestFilter = (_this$config$drmSyste = this.config.drmSystems) == null || (_this$config$drmSyste = _this$config$drmSyste[context.keySystem]) == null ? void 0 : _this$config$drmSyste.generateRequest; 22794 if (generateRequestFilter) { 22795 try { 22796 const mappedInitData = generateRequestFilter.call(this.hls, initDataType, initData, context); 22797 if (!mappedInitData) { 22798 throw new Error('Invalid response from configured generateRequest filter'); 22799 } 22800 initDataType = mappedInitData.initDataType; 22801 initData = mappedInitData.initData ? mappedInitData.initData : null; 22802 context.decryptdata.pssh = initData ? new Uint8Array(initData) : null; 22803 } catch (error) { 22804 this.warn(error.message); 22805 if (this.hls && this.hls.config.debug) { 22806 throw error; 22807 } 22808 } 22809 } 22810 if (initData === null) { 22811 this.log(`Skipping key-session request for "${reason}" (no initData)`); 22812 return Promise.resolve(context); 22813 } 22814 const keyId = getKeyIdString(context.decryptdata); 22815 const keyUri = context.decryptdata.uri; 22816 this.log(`Generating key-session request for "${reason}" keyId: ${keyId} URI: ${keyUri} (init data type: ${initDataType} length: ${initData.byteLength})`); 22817 const licenseStatus = new EventEmitter(); 22818 const onmessage = context._onmessage = event => { 22819 const keySession = context.mediaKeysSession; 22820 if (!keySession) { 22821 licenseStatus.emit('error', new Error('invalid state')); 22822 return; 22823 } 22824 const { 22825 messageType, 22826 message 22827 } = event; 22828 this.log(`"${messageType}" message event for session "${keySession.sessionId}" message size: ${message.byteLength}`); 22829 if (messageType === 'license-request' || messageType === 'license-renewal') { 22830 this.renewLicense(context, message).catch(error => { 22831 if (licenseStatus.eventNames().length) { 22832 licenseStatus.emit('error', error); 22833 } else { 22834 this.handleError(error); 22835 } 22836 }); 22837 } else if (messageType === 'license-release') { 22838 if (context.keySystem === KeySystems.FAIRPLAY) { 22839 this.updateKeySession(context, strToUtf8array('acknowledged')).then(() => this.removeSession(context)).catch(error => this.handleError(error)); 22840 } 22841 } else { 22842 this.warn(`unhandled media key message type "${messageType}"`); 22843 } 22844 }; 22845 const handleKeyStatus = (keyStatus, context) => { 22846 context.keyStatus = keyStatus; 22847 let keyError; 22848 if (keyStatus.startsWith('usable')) { 22849 licenseStatus.emit('resolved'); 22850 } else if (keyStatus === 'internal-error' || keyStatus === 'output-restricted' || keyStatus === 'output-downscaled') { 22851 keyError = getKeyStatusError(keyStatus, context.decryptdata); 22852 } else if (keyStatus === 'expired') { 22853 keyError = new Error(`key expired (keyId: ${keyId})`); 22854 } else if (keyStatus === 'released') { 22855 keyError = new Error(`key released`); 22856 } else if (keyStatus === 'status-pending') ; else { 22857 this.warn(`unhandled key status change "${keyStatus}" (keyId: ${keyId})`); 22858 } 22859 if (keyError) { 22860 if (licenseStatus.eventNames().length) { 22861 licenseStatus.emit('error', keyError); 22862 } else { 22863 this.handleError(keyError); 22864 } 22865 } 22866 }; 22867 const onkeystatuseschange = context._onkeystatuseschange = event => { 22868 const keySession = context.mediaKeysSession; 22869 if (!keySession) { 22870 licenseStatus.emit('error', new Error('invalid state')); 22871 return; 22872 } 22873 const keyStatuses = this.getKeyStatuses(context); 22874 const keyIds = Object.keys(keyStatuses); 22875 22876 // exit if all keys are status-pending 22877 if (!keyIds.some(id => keyStatuses[id] !== 'status-pending')) { 22878 return; 22879 } 22880 22881 // renew when a key status for a levelKey comes back expired 22882 if (keyStatuses[keyId] === 'expired') { 22883 // renew when a key status comes back expired 22884 this.log(`Expired key ${stringify(keyStatuses)} in key-session "${context.mediaKeysSession.sessionId}"`); 22885 this.renewKeySession(context); 22886 return; 22887 } 22888 let keyStatus = keyStatuses[keyId]; 22889 if (keyStatus) { 22890 // handle status of current key 22891 handleKeyStatus(keyStatus, context); 22892 } else { 22893 var _context$keyStatusTim; 22894 // Timeout key-status 22895 const timeout = 1000; 22896 context.keyStatusTimeouts || (context.keyStatusTimeouts = {}); 22897 (_context$keyStatusTim = context.keyStatusTimeouts)[keyId] || (_context$keyStatusTim[keyId] = self.setTimeout(() => { 22898 if (!context.mediaKeysSession || !this.mediaKeys) { 22899 return; 22900 } 22901 22902 // Find key status in another session if missing (PlayReady #7519 no key-status "single-key" setup with shared key) 22903 const sessionKeyStatus = this.getKeyStatus(context.decryptdata); 22904 if (sessionKeyStatus && sessionKeyStatus !== 'status-pending') { 22905 this.log(`No status for keyId ${keyId} in key-session "${context.mediaKeysSession.sessionId}". Using session key-status ${sessionKeyStatus} from other session.`); 22906 return handleKeyStatus(sessionKeyStatus, context); 22907 } 22908 22909 // Timeout key with internal-error 22910 this.log(`key status for ${keyId} in key-session "${context.mediaKeysSession.sessionId}" timed out after ${timeout}ms`); 22911 keyStatus = 'internal-error'; 22912 handleKeyStatus(keyStatus, context); 22913 }, timeout)); 22914 this.log(`No status for keyId ${keyId} (${stringify(keyStatuses)}).`); 22915 } 22916 }; 22917 addEventListener(context.mediaKeysSession, 'message', onmessage); 22918 addEventListener(context.mediaKeysSession, 'keystatuseschange', onkeystatuseschange); 22919 const keyUsablePromise = new Promise((resolve, reject) => { 22920 licenseStatus.on('error', reject); 22921 licenseStatus.on('resolved', resolve); 22922 }); 22923 return context.mediaKeysSession.generateRequest(initDataType, initData).then(() => { 22924 this.log(`Request generated for key-session "${context.mediaKeysSession.sessionId}" keyId: ${keyId} URI: ${keyUri}`); 22925 }).catch(error => { 22926 throw new EMEKeyError({ 22927 type: ErrorTypes.KEY_SYSTEM_ERROR, 22928 details: ErrorDetails.KEY_SYSTEM_NO_SESSION, 22929 error, 22930 decryptdata: context.decryptdata, 22931 fatal: false 22932 }, `Error generating key-session request: ${error}`); 22933 }).then(() => keyUsablePromise).catch(error => { 22934 licenseStatus.removeAllListeners(); 22935 return this.removeSession(context).then(() => { 22936 throw error; 22937 }); 22938 }).then(() => { 22939 licenseStatus.removeAllListeners(); 22940 return context; 22941 }); 22942 } 22943 getKeyStatuses(mediaKeySessionContext) { 22944 const keyStatuses = {}; 22945 mediaKeySessionContext.mediaKeysSession.keyStatuses.forEach((status, keyId) => { 22946 // keyStatuses.forEach is not standard API so the callback value looks weird on xboxone 22947 // xboxone callback(keyId, status) so we need to exchange them 22948 if (typeof keyId === 'string' && typeof status === 'object') { 22949 const temp = keyId; 22950 keyId = status; 22951 status = temp; 22952 } 22953 const keyIdArray = 'buffer' in keyId ? new Uint8Array(keyId.buffer, keyId.byteOffset, keyId.byteLength) : new Uint8Array(keyId); 22954 if (mediaKeySessionContext.keySystem === KeySystems.PLAYREADY && keyIdArray.length === 16) { 22955 // On some devices, the key ID has already been converted for endianness. 22956 // In such cases, this key ID is the one we need to cache. 22957 const originKeyIdWithStatusChange = arrayToHex(keyIdArray); 22958 // Cache the original key IDs to ensure compatibility across all cases. 22959 keyStatuses[originKeyIdWithStatusChange] = status; 22960 changeEndianness(keyIdArray); 22961 } 22962 const keyIdWithStatusChange = arrayToHex(keyIdArray); 22963 // Add to banned keys to prevent playlist usage and license requests 22964 if (status === 'internal-error') { 22965 this.bannedKeyIds[keyIdWithStatusChange] = status; 22966 } 22967 this.log(`key status change "${status}" for keyStatuses keyId: ${keyIdWithStatusChange} key-session "${mediaKeySessionContext.mediaKeysSession.sessionId}"`); 22968 keyStatuses[keyIdWithStatusChange] = status; 22969 }); 22970 return keyStatuses; 22971 } 22972 fetchServerCertificate(keySystem) { 22973 const config = this.config; 22974 const Loader = config.loader; 22975 const certLoader = new Loader(config); 22976 const url = this.getServerCertificateUrl(keySystem); 22977 if (!url) { 22978 return Promise.resolve(); 22979 } 22980 this.log(`Fetching server certificate for "${keySystem}"`); 22981 return new Promise((resolve, reject) => { 22982 const loaderContext = { 22983 responseType: 'arraybuffer', 22984 url 22985 }; 22986 const loadPolicy = config.certLoadPolicy.default; 22987 const loaderConfig = { 22988 loadPolicy, 22989 timeout: loadPolicy.maxLoadTimeMs, 22990 maxRetry: 0, 22991 retryDelay: 0, 22992 maxRetryDelay: 0 22993 }; 22994 const loaderCallbacks = { 22995 onSuccess: (response, stats, context, networkDetails) => { 22996 resolve(response.data); 22997 }, 22998 onError: (response, contex, networkDetails, stats) => { 22999 reject(new EMEKeyError({ 23000 type: ErrorTypes.KEY_SYSTEM_ERROR, 23001 details: ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED, 23002 fatal: true, 23003 networkDetails, 23004 response: _objectSpread2({ 23005 url: loaderContext.url, 23006 data: undefined 23007 }, response) 23008 }, `"${keySystem}" certificate request failed (${url}). Status: ${response.code} (${response.text})`)); 23009 }, 23010 onTimeout: (stats, context, networkDetails) => { 23011 reject(new EMEKeyError({ 23012 type: ErrorTypes.KEY_SYSTEM_ERROR, 23013 details: ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED, 23014 fatal: true, 23015 networkDetails, 23016 response: { 23017 url: loaderContext.url, 23018 data: undefined 23019 } 23020 }, `"${keySystem}" certificate request timed out (${url})`)); 23021 }, 23022 onAbort: (stats, context, networkDetails) => { 23023 reject(new Error('aborted')); 23024 } 23025 }; 23026 certLoader.load(loaderContext, loaderConfig, loaderCallbacks); 23027 }); 23028 } 23029 setMediaKeysServerCertificate(mediaKeys, keySystem, cert) { 23030 return new Promise((resolve, reject) => { 23031 mediaKeys.setServerCertificate(cert).then(success => { 23032 this.log(`setServerCertificate ${success ? 'success' : 'not supported by CDM'} (${cert.byteLength}) on "${keySystem}"`); 23033 resolve(mediaKeys); 23034 }).catch(error => { 23035 reject(new EMEKeyError({ 23036 type: ErrorTypes.KEY_SYSTEM_ERROR, 23037 details: ErrorDetails.KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED, 23038 error, 23039 fatal: true 23040 }, error.message)); 23041 }); 23042 }); 23043 } 23044 renewLicense(context, keyMessage) { 23045 return this.requestLicense(context, new Uint8Array(keyMessage)).then(data => { 23046 return this.updateKeySession(context, new Uint8Array(data)).catch(error => { 23047 throw new EMEKeyError({ 23048 type: ErrorTypes.KEY_SYSTEM_ERROR, 23049 details: ErrorDetails.KEY_SYSTEM_SESSION_UPDATE_FAILED, 23050 decryptdata: context.decryptdata, 23051 error, 23052 fatal: false 23053 }, error.message); 23054 }); 23055 }); 23056 } 23057 unpackPlayReadyKeyMessage(xhr, licenseChallenge) { 23058 // On Edge, the raw license message is UTF-16-encoded XML. We need 23059 // to unpack the Challenge element (base64-encoded string containing the 23060 // actual license request) and any HttpHeader elements (sent as request 23061 // headers). 23062 // For PlayReady CDMs, we need to dig the Challenge out of the XML. 23063 const xmlString = String.fromCharCode.apply(null, new Uint16Array(licenseChallenge.buffer)); 23064 if (!xmlString.includes('PlayReadyKeyMessage')) { 23065 // This does not appear to be a wrapped message as on Edge. Some 23066 // clients do not need this unwrapping, so we will assume this is one of 23067 // them. Note that "xml" at this point probably looks like random 23068 // garbage, since we interpreted UTF-8 as UTF-16. 23069 xhr.setRequestHeader('Content-Type', 'text/xml; charset=utf-8'); 23070 return licenseChallenge; 23071 } 23072 const keyMessageXml = new DOMParser().parseFromString(xmlString, 'application/xml'); 23073 // Set request headers. 23074 const headers = keyMessageXml.querySelectorAll('HttpHeader'); 23075 if (headers.length > 0) { 23076 let header; 23077 for (let i = 0, len = headers.length; i < len; i++) { 23078 var _header$querySelector, _header$querySelector2; 23079 header = headers[i]; 23080 const name = (_header$querySelector = header.querySelector('name')) == null ? void 0 : _header$querySelector.textContent; 23081 const value = (_header$querySelector2 = header.querySelector('value')) == null ? void 0 : _header$querySelector2.textContent; 23082 if (name && value) { 23083 xhr.setRequestHeader(name, value); 23084 } 23085 } 23086 } 23087 const challengeElement = keyMessageXml.querySelector('Challenge'); 23088 const challengeText = challengeElement == null ? void 0 : challengeElement.textContent; 23089 if (!challengeText) { 23090 throw new Error(`Cannot find <Challenge> in key message`); 23091 } 23092 return strToUtf8array(atob(challengeText)); 23093 } 23094 setupLicenseXHR(xhr, url, keysListItem, licenseChallenge) { 23095 const licenseXhrSetup = this.config.licenseXhrSetup; 23096 if (!licenseXhrSetup) { 23097 xhr.open('POST', url, true); 23098 return Promise.resolve({ 23099 xhr, 23100 licenseChallenge 23101 }); 23102 } 23103 return Promise.resolve().then(() => { 23104 if (!keysListItem.decryptdata) { 23105 throw new Error('Key removed'); 23106 } 23107 return licenseXhrSetup.call(this.hls, xhr, url, keysListItem, licenseChallenge); 23108 }).catch(error => { 23109 if (!keysListItem.decryptdata) { 23110 // Key session removed. Cancel license request. 23111 throw error; 23112 } 23113 // let's try to open before running setup 23114 xhr.open('POST', url, true); 23115 return licenseXhrSetup.call(this.hls, xhr, url, keysListItem, licenseChallenge); 23116 }).then(licenseXhrSetupResult => { 23117 // if licenseXhrSetup did not yet call open, let's do it now 23118 if (!xhr.readyState) { 23119 xhr.open('POST', url, true); 23120 } 23121 const finalLicenseChallenge = licenseXhrSetupResult ? licenseXhrSetupResult : licenseChallenge; 23122 return { 23123 xhr, 23124 licenseChallenge: finalLicenseChallenge 23125 }; 23126 }); 23127 } 23128 requestLicense(keySessionContext, licenseChallenge) { 23129 const keyLoadPolicy = this.config.keyLoadPolicy.default; 23130 return new Promise((resolve, reject) => { 23131 const url = this.getLicenseServerUrlOrThrow(keySessionContext.keySystem); 23132 this.log(`Sending license request to URL: ${url}`); 23133 const xhr = new XMLHttpRequest(); 23134 xhr.responseType = 'arraybuffer'; 23135 xhr.onreadystatechange = () => { 23136 if (!this.hls || !keySessionContext.mediaKeysSession) { 23137 return reject(new Error('invalid state')); 23138 } 23139 if (xhr.readyState === 4) { 23140 if (xhr.status === 200) { 23141 this._requestLicenseFailureCount = 0; 23142 let data = xhr.response; 23143 this.log(`License received ${data instanceof ArrayBuffer ? data.byteLength : data}`); 23144 const licenseResponseCallback = this.config.licenseResponseCallback; 23145 if (licenseResponseCallback) { 23146 try { 23147 data = licenseResponseCallback.call(this.hls, xhr, url, keySessionContext); 23148 } catch (error) { 23149 this.error(error); 23150 } 23151 } 23152 resolve(data); 23153 } else { 23154 const retryConfig = keyLoadPolicy.errorRetry; 23155 const maxNumRetry = retryConfig ? retryConfig.maxNumRetry : 0; 23156 this._requestLicenseFailureCount++; 23157 if (this._requestLicenseFailureCount > maxNumRetry || xhr.status >= 400 && xhr.status < 500) { 23158 reject(new EMEKeyError({ 23159 type: ErrorTypes.KEY_SYSTEM_ERROR, 23160 details: ErrorDetails.KEY_SYSTEM_LICENSE_REQUEST_FAILED, 23161 decryptdata: keySessionContext.decryptdata, 23162 fatal: true, 23163 networkDetails: xhr, 23164 response: { 23165 url, 23166 data: undefined, 23167 code: xhr.status, 23168 text: xhr.statusText 23169 } 23170 }, `License Request XHR failed (${url}). Status: ${xhr.status} (${xhr.statusText})`)); 23171 } else { 23172 const attemptsLeft = maxNumRetry - this._requestLicenseFailureCount + 1; 23173 this.warn(`Retrying license request, ${attemptsLeft} attempts left`); 23174 this.requestLicense(keySessionContext, licenseChallenge).then(resolve, reject); 23175 } 23176 } 23177 } 23178 }; 23179 if (keySessionContext.licenseXhr && keySessionContext.licenseXhr.readyState !== XMLHttpRequest.DONE) { 23180 keySessionContext.licenseXhr.abort(); 23181 } 23182 keySessionContext.licenseXhr = xhr; 23183 this.setupLicenseXHR(xhr, url, keySessionContext, licenseChallenge).then(({ 23184 xhr, 23185 licenseChallenge 23186 }) => { 23187 if (keySessionContext.keySystem == KeySystems.PLAYREADY) { 23188 licenseChallenge = this.unpackPlayReadyKeyMessage(xhr, licenseChallenge); 23189 } 23190 xhr.send(licenseChallenge); 23191 }).catch(reject); 23192 }); 23193 } 23194 onDestroying() { 23195 this.unregisterListeners(); 23196 this._clear(); 23197 } 23198 onMediaAttached(event, data) { 23199 if (!this.config.emeEnabled) { 23200 return; 23201 } 23202 const media = data.media; 23203 23204 // keep reference of media 23205 this.media = media; 23206 addEventListener(media, 'encrypted', this.onMediaEncrypted); 23207 addEventListener(media, 'waitingforkey', this.onWaitingForKey); 23208 const mediaResolved = this.mediaResolved; 23209 if (mediaResolved) { 23210 mediaResolved(); 23211 } else { 23212 this.mediaKeys = media.mediaKeys; 23213 } 23214 } 23215 onMediaDetached() { 23216 const media = this.media; 23217 if (media) { 23218 removeEventListener(media, 'encrypted', this.onMediaEncrypted); 23219 removeEventListener(media, 'waitingforkey', this.onWaitingForKey); 23220 this.media = null; 23221 this.mediaKeys = null; 23222 } 23223 } 23224 _clear() { 23225 var _media$setMediaKeys; 23226 this._requestLicenseFailureCount = 0; 23227 this.keyIdToKeySessionPromise = {}; 23228 this.bannedKeyIds = {}; 23229 const mediaResolved = this.mediaResolved; 23230 if (mediaResolved) { 23231 mediaResolved(); 23232 } 23233 if (!this.mediaKeys && !this.mediaKeySessions.length) { 23234 return; 23235 } 23236 const media = this.media; 23237 const mediaKeysList = this.mediaKeySessions.slice(); 23238 this.mediaKeySessions = []; 23239 this.mediaKeys = null; 23240 LevelKey.clearKeyUriToKeyIdMap(); 23241 23242 // Close all sessions and remove media keys from the video element. 23243 const keySessionCount = mediaKeysList.length; 23244 EMEController.CDMCleanupPromise = Promise.all(mediaKeysList.map(mediaKeySessionContext => this.removeSession(mediaKeySessionContext)).concat((media == null || (_media$setMediaKeys = media.setMediaKeys(null)) == null ? void 0 : _media$setMediaKeys.catch(error => { 23245 this.log(`Could not clear media keys: ${error}`); 23246 if (!this.hls) return; 23247 this.hls.trigger(Events.ERROR, { 23248 type: ErrorTypes.OTHER_ERROR, 23249 details: ErrorDetails.KEY_SYSTEM_DESTROY_MEDIA_KEYS_ERROR, 23250 fatal: false, 23251 error: new Error(`Could not clear media keys: ${error}`) 23252 }); 23253 })) || Promise.resolve())).catch(error => { 23254 this.log(`Could not close sessions and clear media keys: ${error}`); 23255 if (!this.hls) return; 23256 this.hls.trigger(Events.ERROR, { 23257 type: ErrorTypes.OTHER_ERROR, 23258 details: ErrorDetails.KEY_SYSTEM_DESTROY_CLOSE_SESSION_ERROR, 23259 fatal: false, 23260 error: new Error(`Could not close sessions and clear media keys: ${error}`) 23261 }); 23262 }).then(() => { 23263 if (keySessionCount) { 23264 this.log('finished closing key sessions and clearing media keys'); 23265 } 23266 }); 23267 } 23268 onManifestLoading() { 23269 this._clear(); 23270 } 23271 onManifestLoaded(event, { 23272 sessionKeys 23273 }) { 23274 if (!sessionKeys || !this.config.emeEnabled) { 23275 return; 23276 } 23277 if (!this.keyFormatPromise) { 23278 const keyFormats = sessionKeys.reduce((formats, sessionKey) => { 23279 if (formats.indexOf(sessionKey.keyFormat) === -1) { 23280 formats.push(sessionKey.keyFormat); 23281 } 23282 return formats; 23283 }, []); 23284 this.log(`Selecting key-system from session-keys ${keyFormats.join(', ')}`); 23285 this.keyFormatPromise = this.getKeyFormatPromise(keyFormats); 23286 } 23287 } 23288 removeSession(mediaKeySessionContext) { 23289 const { 23290 mediaKeysSession, 23291 licenseXhr, 23292 decryptdata 23293 } = mediaKeySessionContext; 23294 if (mediaKeysSession) { 23295 this.log(`Remove licenses and keys and close session "${mediaKeysSession.sessionId}" keyId: ${arrayToHex((decryptdata == null ? void 0 : decryptdata.keyId) || [])}`); 23296 if (mediaKeySessionContext._onmessage) { 23297 mediaKeysSession.removeEventListener('message', mediaKeySessionContext._onmessage); 23298 mediaKeySessionContext._onmessage = undefined; 23299 } 23300 if (mediaKeySessionContext._onkeystatuseschange) { 23301 mediaKeysSession.removeEventListener('keystatuseschange', mediaKeySessionContext._onkeystatuseschange); 23302 mediaKeySessionContext._onkeystatuseschange = undefined; 23303 } 23304 if (licenseXhr && licenseXhr.readyState !== XMLHttpRequest.DONE) { 23305 licenseXhr.abort(); 23306 } 23307 mediaKeySessionContext.mediaKeysSession = mediaKeySessionContext.decryptdata = mediaKeySessionContext.licenseXhr = undefined; 23308 const index = this.mediaKeySessions.indexOf(mediaKeySessionContext); 23309 if (index > -1) { 23310 this.mediaKeySessions.splice(index, 1); 23311 } 23312 const { 23313 keyStatusTimeouts 23314 } = mediaKeySessionContext; 23315 if (keyStatusTimeouts) { 23316 Object.keys(keyStatusTimeouts).forEach(keyId => self.clearTimeout(keyStatusTimeouts[keyId])); 23317 } 23318 const { 23319 drmSystemOptions 23320 } = this.config; 23321 const removePromise = isPersistentSessionType(drmSystemOptions) ? new Promise((resolve, reject) => { 23322 self.setTimeout(() => reject(new Error(`MediaKeySession.remove() timeout`)), 8000); 23323 mediaKeysSession.remove().then(resolve).catch(reject); 23324 }) : Promise.resolve(); 23325 return removePromise.catch(error => { 23326 this.log(`Could not remove session: ${error}`); 23327 if (!this.hls) return; 23328 this.hls.trigger(Events.ERROR, { 23329 type: ErrorTypes.OTHER_ERROR, 23330 details: ErrorDetails.KEY_SYSTEM_DESTROY_REMOVE_SESSION_ERROR, 23331 fatal: false, 23332 error: new Error(`Could not remove session: ${error}`) 23333 }); 23334 }).then(() => { 23335 return mediaKeysSession.close(); 23336 }).catch(error => { 23337 this.log(`Could not close session: ${error}`); 23338 if (!this.hls) return; 23339 this.hls.trigger(Events.ERROR, { 23340 type: ErrorTypes.OTHER_ERROR, 23341 details: ErrorDetails.KEY_SYSTEM_DESTROY_CLOSE_SESSION_ERROR, 23342 fatal: false, 23343 error: new Error(`Could not close session: ${error}`) 23344 }); 23345 }); 23346 } 23347 return Promise.resolve(); 23348 } 23349} 23350EMEController.CDMCleanupPromise = void 0; 23351function getKeyIdString(decryptdata) { 23352 if (!decryptdata) { 23353 throw new Error('Could not read keyId of undefined decryptdata'); 23354 } 23355 if (decryptdata.keyId === null) { 23356 throw new Error('keyId is null'); 23357 } 23358 return arrayToHex(decryptdata.keyId); 23359} 23360function getKeyStatus(decryptdata, keyContext) { 23361 if (decryptdata.keyId && keyContext.mediaKeysSession.keyStatuses.has(decryptdata.keyId)) { 23362 return keyContext.mediaKeysSession.keyStatuses.get(decryptdata.keyId); 23363 } 23364 if (decryptdata.matches(keyContext.decryptdata)) { 23365 return keyContext.keyStatus; 23366 } 23367 return undefined; 23368} 23369class EMEKeyError extends Error { 23370 constructor(data, message) { 23371 super(message); 23372 this.data = void 0; 23373 data.error || (data.error = new Error(message)); 23374 this.data = data; 23375 data.err = data.error; 23376 } 23377} 23378function getKeyStatusError(keyStatus, decryptdata) { 23379 const outputRestricted = keyStatus === 'output-restricted'; 23380 const details = outputRestricted ? ErrorDetails.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED : ErrorDetails.KEY_SYSTEM_STATUS_INTERNAL_ERROR; 23381 return new EMEKeyError({ 23382 type: ErrorTypes.KEY_SYSTEM_ERROR, 23383 details, 23384 fatal: false, 23385 decryptdata 23386 }, outputRestricted ? 'HDCP level output restricted' : `key status changed to "${keyStatus}"`); 23387} 23388 23389class FPSController { 23390 constructor(hls) { 23391 this.hls = void 0; 23392 this.isVideoPlaybackQualityAvailable = false; 23393 this.timer = void 0; 23394 this.media = null; 23395 this.lastTime = void 0; 23396 this.lastDroppedFrames = 0; 23397 this.lastDecodedFrames = 0; 23398 // stream controller must be provided as a dependency! 23399 this.streamController = void 0; 23400 this.hls = hls; 23401 this.registerListeners(); 23402 } 23403 setStreamController(streamController) { 23404 this.streamController = streamController; 23405 } 23406 registerListeners() { 23407 this.hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 23408 this.hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 23409 } 23410 unregisterListeners() { 23411 this.hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 23412 this.hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 23413 } 23414 destroy() { 23415 if (this.timer) { 23416 clearInterval(this.timer); 23417 } 23418 this.unregisterListeners(); 23419 this.isVideoPlaybackQualityAvailable = false; 23420 this.media = null; 23421 } 23422 onMediaAttaching(event, data) { 23423 const config = this.hls.config; 23424 if (config.capLevelOnFPSDrop) { 23425 const media = data.media instanceof self.HTMLVideoElement ? data.media : null; 23426 this.media = media; 23427 if (media && typeof media.getVideoPlaybackQuality === 'function') { 23428 this.isVideoPlaybackQualityAvailable = true; 23429 } 23430 self.clearInterval(this.timer); 23431 this.timer = self.setInterval(this.checkFPSInterval.bind(this), config.fpsDroppedMonitoringPeriod); 23432 } 23433 } 23434 onMediaDetaching() { 23435 this.media = null; 23436 } 23437 checkFPS(video, decodedFrames, droppedFrames) { 23438 const currentTime = performance.now(); 23439 if (decodedFrames) { 23440 if (this.lastTime) { 23441 const currentPeriod = currentTime - this.lastTime; 23442 const currentDropped = droppedFrames - this.lastDroppedFrames; 23443 const currentDecoded = decodedFrames - this.lastDecodedFrames; 23444 const droppedFPS = 1000 * currentDropped / currentPeriod; 23445 const hls = this.hls; 23446 hls.trigger(Events.FPS_DROP, { 23447 currentDropped: currentDropped, 23448 currentDecoded: currentDecoded, 23449 totalDroppedFrames: droppedFrames 23450 }); 23451 if (droppedFPS > 0) { 23452 // hls.logger.log('checkFPS : droppedFPS/decodedFPS:' + droppedFPS/(1000 * currentDecoded / currentPeriod)); 23453 if (currentDropped > hls.config.fpsDroppedMonitoringThreshold * currentDecoded) { 23454 let currentLevel = hls.currentLevel; 23455 hls.logger.warn('drop FPS ratio greater than max allowed value for currentLevel: ' + currentLevel); 23456 if (currentLevel > 0 && (hls.autoLevelCapping === -1 || hls.autoLevelCapping >= currentLevel)) { 23457 currentLevel = currentLevel - 1; 23458 hls.trigger(Events.FPS_DROP_LEVEL_CAPPING, { 23459 level: currentLevel, 23460 droppedLevel: hls.currentLevel 23461 }); 23462 hls.autoLevelCapping = currentLevel; 23463 this.streamController.nextLevelSwitch(); 23464 } 23465 } 23466 } 23467 } 23468 this.lastTime = currentTime; 23469 this.lastDroppedFrames = droppedFrames; 23470 this.lastDecodedFrames = decodedFrames; 23471 } 23472 } 23473 checkFPSInterval() { 23474 const video = this.media; 23475 if (video) { 23476 if (this.isVideoPlaybackQualityAvailable) { 23477 const videoPlaybackQuality = video.getVideoPlaybackQuality(); 23478 this.checkFPS(video, videoPlaybackQuality.totalVideoFrames, videoPlaybackQuality.droppedVideoFrames); 23479 } else { 23480 // HTMLVideoElement doesn't include the webkit types 23481 this.checkFPS(video, video.webkitDecodedFrameCount, video.webkitDroppedFrameCount); 23482 } 23483 } 23484 } 23485} 23486 23487function sendAddTrackEvent(track, videoEl) { 23488 let event; 23489 try { 23490 event = new Event('addtrack'); 23491 } catch (err) { 23492 // for IE11 23493 event = document.createEvent('Event'); 23494 event.initEvent('addtrack', false, false); 23495 } 23496 event.track = track; 23497 videoEl.dispatchEvent(event); 23498} 23499function addCueToTrack(track, cue) { 23500 // Sometimes there are cue overlaps on segmented vtts so the same 23501 // cue can appear more than once in different vtt files. 23502 // This avoid showing duplicated cues with same timecode and text. 23503 const mode = track.mode; 23504 if (mode === 'disabled') { 23505 track.mode = 'hidden'; 23506 } 23507 if (track.cues && !track.cues.getCueById(cue.id)) { 23508 try { 23509 track.addCue(cue); 23510 if (!track.cues.getCueById(cue.id)) { 23511 throw new Error(`addCue is failed for: ${cue}`); 23512 } 23513 } catch (err) { 23514 logger.debug(`[texttrack-utils]: ${err}`); 23515 try { 23516 const textTrackCue = new self.TextTrackCue(cue.startTime, cue.endTime, cue.text); 23517 textTrackCue.id = cue.id; 23518 track.addCue(textTrackCue); 23519 } catch (err2) { 23520 logger.debug(`[texttrack-utils]: Legacy TextTrackCue fallback failed: ${err2}`); 23521 } 23522 } 23523 } 23524 if (mode === 'disabled') { 23525 track.mode = mode; 23526 } 23527} 23528function clearCurrentCues(track, enterHandler) { 23529 // When track.mode is disabled, track.cues will be null. 23530 // To guarantee the removal of cues, we need to temporarily 23531 // change the mode to hidden 23532 const mode = track.mode; 23533 if (mode === 'disabled') { 23534 track.mode = 'hidden'; 23535 } 23536 if (track.cues) { 23537 for (let i = track.cues.length; i--;) { 23538 if (enterHandler) { 23539 track.cues[i].removeEventListener('enter', enterHandler); 23540 } 23541 track.removeCue(track.cues[i]); 23542 } 23543 } 23544 if (mode === 'disabled') { 23545 track.mode = mode; 23546 } 23547} 23548function removeCuesInRange(track, start, end, predicate) { 23549 const mode = track.mode; 23550 if (mode === 'disabled') { 23551 track.mode = 'hidden'; 23552 } 23553 if (track.cues && track.cues.length > 0) { 23554 const cues = getCuesInRange(track.cues, start, end); 23555 for (let i = 0; i < cues.length; i++) { 23556 if (!predicate || predicate(cues[i])) { 23557 track.removeCue(cues[i]); 23558 } 23559 } 23560 } 23561 if (mode === 'disabled') { 23562 track.mode = mode; 23563 } 23564} 23565 23566// Find first cue starting at or after given time. 23567// Modified version of binary search O(log(n)). 23568function getFirstCueIndexFromTime(cues, time) { 23569 // If first cue starts at or after time, start there 23570 if (time <= cues[0].startTime) { 23571 return 0; 23572 } 23573 // If the last cue ends before time there is no overlap 23574 const len = cues.length - 1; 23575 if (time > cues[len].endTime) { 23576 return -1; 23577 } 23578 let left = 0; 23579 let right = len; 23580 let mid; 23581 while (left <= right) { 23582 mid = Math.floor((right + left) / 2); 23583 if (time < cues[mid].startTime) { 23584 right = mid - 1; 23585 } else if (time > cues[mid].startTime && left < len) { 23586 left = mid + 1; 23587 } else { 23588 // If it's not lower or higher, it must be equal. 23589 return mid; 23590 } 23591 } 23592 // At this point, left and right have swapped. 23593 // No direct match was found, left or right element must be the closest. Check which one has the smallest diff. 23594 return cues[left].startTime - time < time - cues[right].startTime ? left : right; 23595} 23596function getCuesInRange(cues, start, end) { 23597 const cuesFound = []; 23598 const firstCueInRange = getFirstCueIndexFromTime(cues, start); 23599 if (firstCueInRange > -1) { 23600 for (let i = firstCueInRange, len = cues.length; i < len; i++) { 23601 const cue = cues[i]; 23602 if (cue.startTime >= start && cue.endTime <= end) { 23603 cuesFound.push(cue); 23604 } else if (cue.startTime > end) { 23605 return cuesFound; 23606 } 23607 } 23608 } 23609 return cuesFound; 23610} 23611function filterSubtitleTracks(textTrackList) { 23612 const tracks = []; 23613 for (let i = 0; i < textTrackList.length; i++) { 23614 const track = textTrackList[i]; 23615 // Edge adds a track without a label; we don't want to use it 23616 if ((track.kind === 'subtitles' || track.kind === 'captions') && track.label) { 23617 tracks.push(textTrackList[i]); 23618 } 23619 } 23620 return tracks; 23621} 23622 23623class SubtitleTrackController extends BasePlaylistController { 23624 constructor(hls) { 23625 super(hls, 'subtitle-track-controller'); 23626 this.media = null; 23627 this.tracks = []; 23628 this.groupIds = null; 23629 this.tracksInGroup = []; 23630 this.trackId = -1; 23631 this.currentTrack = null; 23632 this.selectDefaultTrack = true; 23633 this.queuedDefaultTrack = -1; 23634 this.useTextTrackPolling = false; 23635 this.subtitlePollingInterval = -1; 23636 this._subtitleDisplay = true; 23637 this.asyncPollTrackChange = () => this.pollTrackChange(0); 23638 this.onTextTracksChanged = () => { 23639 if (!this.useTextTrackPolling) { 23640 self.clearInterval(this.subtitlePollingInterval); 23641 } 23642 // Media is undefined when switching streams via loadSource() 23643 if (!this.media || !this.hls.config.renderTextTracksNatively) { 23644 return; 23645 } 23646 let textTrack = null; 23647 const tracks = filterSubtitleTracks(this.media.textTracks); 23648 for (let i = 0; i < tracks.length; i++) { 23649 if (tracks[i].mode === 'hidden') { 23650 // Do not break in case there is a following track with showing. 23651 textTrack = tracks[i]; 23652 } else if (tracks[i].mode === 'showing') { 23653 textTrack = tracks[i]; 23654 break; 23655 } 23656 } 23657 23658 // Find internal track index for TextTrack 23659 const trackId = this.findTrackForTextTrack(textTrack); 23660 if (this.subtitleTrack !== trackId) { 23661 this.setSubtitleTrack(trackId); 23662 } 23663 }; 23664 this.registerListeners(); 23665 } 23666 destroy() { 23667 this.unregisterListeners(); 23668 this.tracks.length = 0; 23669 this.tracksInGroup.length = 0; 23670 this.currentTrack = null; 23671 // @ts-ignore 23672 this.onTextTracksChanged = this.asyncPollTrackChange = null; 23673 super.destroy(); 23674 } 23675 get subtitleDisplay() { 23676 return this._subtitleDisplay; 23677 } 23678 set subtitleDisplay(value) { 23679 this._subtitleDisplay = value; 23680 if (this.trackId > -1) { 23681 this.toggleTrackModes(); 23682 } 23683 } 23684 registerListeners() { 23685 const { 23686 hls 23687 } = this; 23688 hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 23689 hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 23690 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 23691 hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this); 23692 hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this); 23693 hls.on(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); 23694 hls.on(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this); 23695 hls.on(Events.ERROR, this.onError, this); 23696 } 23697 unregisterListeners() { 23698 const { 23699 hls 23700 } = this; 23701 hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 23702 hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 23703 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 23704 hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this); 23705 hls.off(Events.LEVEL_LOADING, this.onLevelLoading, this); 23706 hls.off(Events.LEVEL_SWITCHING, this.onLevelSwitching, this); 23707 hls.off(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this); 23708 hls.off(Events.ERROR, this.onError, this); 23709 } 23710 23711 // Listen for subtitle track change, then extract the current track ID. 23712 onMediaAttached(event, data) { 23713 this.media = data.media; 23714 if (!this.media) { 23715 return; 23716 } 23717 if (this.queuedDefaultTrack > -1) { 23718 this.subtitleTrack = this.queuedDefaultTrack; 23719 this.queuedDefaultTrack = -1; 23720 } 23721 this.useTextTrackPolling = !(this.media.textTracks && 'onchange' in this.media.textTracks); 23722 if (this.useTextTrackPolling) { 23723 this.pollTrackChange(500); 23724 } else { 23725 this.media.textTracks.addEventListener('change', this.asyncPollTrackChange); 23726 } 23727 } 23728 pollTrackChange(timeout) { 23729 self.clearInterval(this.subtitlePollingInterval); 23730 this.subtitlePollingInterval = self.setInterval(this.onTextTracksChanged, timeout); 23731 } 23732 onMediaDetaching(event, data) { 23733 const media = this.media; 23734 if (!media) { 23735 return; 23736 } 23737 const transferringMedia = !!data.transferMedia; 23738 self.clearInterval(this.subtitlePollingInterval); 23739 if (!this.useTextTrackPolling) { 23740 media.textTracks.removeEventListener('change', this.asyncPollTrackChange); 23741 } 23742 if (this.trackId > -1) { 23743 this.queuedDefaultTrack = this.trackId; 23744 } 23745 23746 // Disable all subtitle tracks before detachment so when reattached only tracks in that content are enabled. 23747 this.subtitleTrack = -1; 23748 this.media = null; 23749 if (transferringMedia) { 23750 return; 23751 } 23752 const textTracks = filterSubtitleTracks(media.textTracks); 23753 // Clear loaded cues on media detachment from tracks 23754 textTracks.forEach(track => { 23755 clearCurrentCues(track); 23756 }); 23757 } 23758 onManifestLoading() { 23759 this.tracks = []; 23760 this.groupIds = null; 23761 this.tracksInGroup = []; 23762 this.trackId = -1; 23763 this.currentTrack = null; 23764 this.selectDefaultTrack = true; 23765 } 23766 23767 // Fired whenever a new manifest is loaded. 23768 onManifestParsed(event, data) { 23769 this.tracks = data.subtitleTracks; 23770 } 23771 onSubtitleTrackLoaded(event, data) { 23772 const { 23773 id, 23774 groupId, 23775 details 23776 } = data; 23777 const trackInActiveGroup = this.tracksInGroup[id]; 23778 if (!trackInActiveGroup || trackInActiveGroup.groupId !== groupId) { 23779 this.warn(`Subtitle track with id:${id} and group:${groupId} not found in active group ${trackInActiveGroup == null ? void 0 : trackInActiveGroup.groupId}`); 23780 return; 23781 } 23782 const curDetails = trackInActiveGroup.details; 23783 trackInActiveGroup.details = data.details; 23784 this.log(`Subtitle track ${id} "${trackInActiveGroup.name}" lang:${trackInActiveGroup.lang} group:${groupId} loaded [${details.startSN}-${details.endSN}]`); 23785 if (id === this.trackId) { 23786 this.playlistLoaded(id, data, curDetails); 23787 } 23788 } 23789 onLevelLoading(event, data) { 23790 this.switchLevel(data.level); 23791 } 23792 onLevelSwitching(event, data) { 23793 this.switchLevel(data.level); 23794 } 23795 switchLevel(levelIndex) { 23796 const levelInfo = this.hls.levels[levelIndex]; 23797 if (!levelInfo) { 23798 return; 23799 } 23800 const subtitleGroups = levelInfo.subtitleGroups || null; 23801 const currentGroups = this.groupIds; 23802 let currentTrack = this.currentTrack; 23803 if (!subtitleGroups || (currentGroups == null ? void 0 : currentGroups.length) !== (subtitleGroups == null ? void 0 : subtitleGroups.length) || subtitleGroups != null && subtitleGroups.some(groupId => (currentGroups == null ? void 0 : currentGroups.indexOf(groupId)) === -1)) { 23804 this.groupIds = subtitleGroups; 23805 this.trackId = -1; 23806 this.currentTrack = null; 23807 const subtitleTracks = this.tracks.filter(track => !subtitleGroups || subtitleGroups.indexOf(track.groupId) !== -1); 23808 if (subtitleTracks.length) { 23809 // Disable selectDefaultTrack if there are no default tracks 23810 if (this.selectDefaultTrack && !subtitleTracks.some(track => track.default)) { 23811 this.selectDefaultTrack = false; 23812 } 23813 // track.id should match hls.audioTracks index 23814 subtitleTracks.forEach((track, i) => { 23815 track.id = i; 23816 }); 23817 } else if (!currentTrack && !this.tracksInGroup.length) { 23818 // Do not dispatch SUBTITLE_TRACKS_UPDATED when there were and are no tracks 23819 return; 23820 } 23821 this.tracksInGroup = subtitleTracks; 23822 23823 // Find preferred track 23824 const subtitlePreference = this.hls.config.subtitlePreference; 23825 if (!currentTrack && subtitlePreference) { 23826 this.selectDefaultTrack = false; 23827 const groupIndex = findMatchingOption(subtitlePreference, subtitleTracks); 23828 if (groupIndex > -1) { 23829 currentTrack = subtitleTracks[groupIndex]; 23830 } else { 23831 const allIndex = findMatchingOption(subtitlePreference, this.tracks); 23832 currentTrack = this.tracks[allIndex]; 23833 } 23834 } 23835 23836 // Select initial track 23837 let trackId = this.findTrackId(currentTrack); 23838 if (trackId === -1 && currentTrack) { 23839 trackId = this.findTrackId(null); 23840 } 23841 23842 // Dispatch events and load track if needed 23843 const subtitleTracksUpdated = { 23844 subtitleTracks 23845 }; 23846 this.log(`Updating subtitle tracks, ${subtitleTracks.length} track(s) found in "${subtitleGroups == null ? void 0 : subtitleGroups.join(',')}" group-id`); 23847 this.hls.trigger(Events.SUBTITLE_TRACKS_UPDATED, subtitleTracksUpdated); 23848 if (trackId !== -1 && this.trackId === -1) { 23849 this.setSubtitleTrack(trackId); 23850 } 23851 } 23852 } 23853 findTrackId(currentTrack) { 23854 const tracks = this.tracksInGroup; 23855 const selectDefault = this.selectDefaultTrack; 23856 for (let i = 0; i < tracks.length; i++) { 23857 const track = tracks[i]; 23858 if (selectDefault && !track.default || !selectDefault && !currentTrack) { 23859 continue; 23860 } 23861 if (!currentTrack || matchesOption(track, currentTrack)) { 23862 return i; 23863 } 23864 } 23865 if (currentTrack) { 23866 for (let i = 0; i < tracks.length; i++) { 23867 const track = tracks[i]; 23868 if (mediaAttributesIdentical(currentTrack.attrs, track.attrs, ['LANGUAGE', 'ASSOC-LANGUAGE', 'CHARACTERISTICS'])) { 23869 return i; 23870 } 23871 } 23872 for (let i = 0; i < tracks.length; i++) { 23873 const track = tracks[i]; 23874 if (mediaAttributesIdentical(currentTrack.attrs, track.attrs, ['LANGUAGE'])) { 23875 return i; 23876 } 23877 } 23878 } 23879 return -1; 23880 } 23881 findTrackForTextTrack(textTrack) { 23882 if (textTrack) { 23883 const tracks = this.tracksInGroup; 23884 for (let i = 0; i < tracks.length; i++) { 23885 const track = tracks[i]; 23886 if (subtitleTrackMatchesTextTrack(track, textTrack)) { 23887 return i; 23888 } 23889 } 23890 } 23891 return -1; 23892 } 23893 onError(event, data) { 23894 if (data.fatal || !data.context) { 23895 return; 23896 } 23897 if (data.context.type === PlaylistContextType.SUBTITLE_TRACK && data.context.id === this.trackId && (!this.groupIds || this.groupIds.indexOf(data.context.groupId) !== -1)) { 23898 this.checkRetry(data); 23899 } 23900 } 23901 get allSubtitleTracks() { 23902 return this.tracks; 23903 } 23904 23905 /** get alternate subtitle tracks list from playlist **/ 23906 get subtitleTracks() { 23907 return this.tracksInGroup; 23908 } 23909 23910 /** get/set index of the selected subtitle track (based on index in subtitle track lists) **/ 23911 get subtitleTrack() { 23912 return this.trackId; 23913 } 23914 set subtitleTrack(newId) { 23915 this.selectDefaultTrack = false; 23916 this.setSubtitleTrack(newId); 23917 } 23918 setSubtitleOption(subtitleOption) { 23919 this.hls.config.subtitlePreference = subtitleOption; 23920 if (subtitleOption) { 23921 if (subtitleOption.id === -1) { 23922 this.setSubtitleTrack(-1); 23923 return null; 23924 } 23925 const allSubtitleTracks = this.allSubtitleTracks; 23926 this.selectDefaultTrack = false; 23927 if (allSubtitleTracks.length) { 23928 // First see if current option matches (no switch op) 23929 const currentTrack = this.currentTrack; 23930 if (currentTrack && matchesOption(subtitleOption, currentTrack)) { 23931 return currentTrack; 23932 } 23933 // Find option in current group 23934 const groupIndex = findMatchingOption(subtitleOption, this.tracksInGroup); 23935 if (groupIndex > -1) { 23936 const track = this.tracksInGroup[groupIndex]; 23937 this.setSubtitleTrack(groupIndex); 23938 return track; 23939 } else if (currentTrack) { 23940 // If this is not the initial selection return null 23941 // option should have matched one in active group 23942 return null; 23943 } else { 23944 // Find the option in all tracks for initial selection 23945 const allIndex = findMatchingOption(subtitleOption, allSubtitleTracks); 23946 if (allIndex > -1) { 23947 return allSubtitleTracks[allIndex]; 23948 } 23949 } 23950 } 23951 } 23952 return null; 23953 } 23954 loadPlaylist(hlsUrlParameters) { 23955 super.loadPlaylist(); 23956 if (this.shouldLoadPlaylist(this.currentTrack)) { 23957 this.scheduleLoading(this.currentTrack, hlsUrlParameters); 23958 } 23959 } 23960 loadingPlaylist(currentTrack, hlsUrlParameters) { 23961 super.loadingPlaylist(currentTrack, hlsUrlParameters); 23962 const id = currentTrack.id; 23963 const groupId = currentTrack.groupId; 23964 const url = this.getUrlWithDirectives(currentTrack.url, hlsUrlParameters); 23965 const details = currentTrack.details; 23966 const age = details == null ? void 0 : details.age; 23967 this.log(`Loading subtitle ${id} "${currentTrack.name}" lang:${currentTrack.lang} group:${groupId}${(hlsUrlParameters == null ? void 0 : hlsUrlParameters.msn) !== undefined ? ' at sn ' + hlsUrlParameters.msn + ' part ' + hlsUrlParameters.part : ''}${age && details.live ? ' age ' + age.toFixed(1) + (details.type ? ' ' + details.type ||
239670
vendor: 41,995 bytes, lines 23967-25193
23967 : '') : ''} ${url}`); 23968 this.hls.trigger(Events.SUBTITLE_TRACK_LOADING, { 23969 url, 23970 id, 23971 groupId, 23972 deliveryDirectives: hlsUrlParameters || null, 23973 track: currentTrack 23974 }); 23975 } 23976 23977 /** 23978 * Disables the old subtitleTrack and sets current mode on the next subtitleTrack. 23979 * This operates on the DOM textTracks. 23980 * A value of -1 will disable all subtitle tracks. 23981 */ 23982 toggleTrackModes() { 23983 const { 23984 media 23985 } = this; 23986 if (!media) { 23987 return; 23988 } 23989 const textTracks = filterSubtitleTracks(media.textTracks); 23990 const currentTrack = this.currentTrack; 23991 let nextTrack; 23992 if (currentTrack) { 23993 nextTrack = textTracks.filter(textTrack => subtitleTrackMatchesTextTrack(currentTrack, textTrack))[0]; 23994 if (!nextTrack) { 23995 this.warn(`Unable to find subtitle TextTrack with name "${currentTrack.name}" and language "${currentTrack.lang}"`); 23996 } 23997 } 23998 [].slice.call(textTracks).forEach(track => { 23999 if (track.mode !== 'disabled' && track !== nextTrack) { 24000 track.mode = 'disabled'; 24001 } 24002 }); 24003 if (nextTrack) { 24004 const mode = this.subtitleDisplay ? 'showing' : 'hidden'; 24005 if (nextTrack.mode !== mode) { 24006 nextTrack.mode = mode; 24007 } 24008 } 24009 } 24010 24011 /** 24012 * This method is responsible for validating the subtitle index and periodically reloading if live. 24013 * Dispatches the SUBTITLE_TRACK_SWITCH event, which instructs the subtitle-stream-controller to load the selected track. 24014 */ 24015 setSubtitleTrack(newId) { 24016 const tracks = this.tracksInGroup; 24017 24018 // setting this.subtitleTrack will trigger internal logic 24019 // if media has not been attached yet, it will fail 24020 // we keep a reference to the default track id 24021 // and we'll set subtitleTrack when onMediaAttached is triggered 24022 if (!this.media) { 24023 this.queuedDefaultTrack = newId; 24024 return; 24025 } 24026 24027 // exit if track id as already set or invalid 24028 if (newId < -1 || newId >= tracks.length || !isFiniteNumber(newId)) { 24029 this.warn(`Invalid subtitle track id: ${newId}`); 24030 return; 24031 } 24032 this.selectDefaultTrack = false; 24033 const lastTrack = this.currentTrack; 24034 const track = tracks[newId] || null; 24035 this.trackId = newId; 24036 this.currentTrack = track; 24037 this.toggleTrackModes(); 24038 if (!track) { 24039 // switch to -1 24040 this.hls.trigger(Events.SUBTITLE_TRACK_SWITCH, { 24041 id: newId 24042 }); 24043 return; 24044 } 24045 const trackLoaded = !!track.details && !track.details.live; 24046 if (newId === this.trackId && track === lastTrack && trackLoaded) { 24047 return; 24048 } 24049 this.log(`Switching to subtitle-track ${newId}` + (track ? ` "${track.name}" lang:${track.lang} group:${track.groupId}` : '')); 24050 const { 24051 id, 24052 groupId = '', 24053 name, 24054 type, 24055 url 24056 } = track; 24057 this.hls.trigger(Events.SUBTITLE_TRACK_SWITCH, { 24058 id, 24059 groupId, 24060 name, 24061 type, 24062 url 24063 }); 24064 const hlsUrlParameters = this.switchParams(track.url, lastTrack == null ? void 0 : lastTrack.details, track.details); 24065 this.loadPlaylist(hlsUrlParameters); 24066 } 24067} 24068 24069/** 24070 * Generate a random v4 UUID 24071 * 24072 * @returns A random v4 UUID 24073 * 24074 * @group Utils 24075 * 24076 * @beta 24077 */ 24078function uuid() { 24079 try { 24080 return crypto.randomUUID(); 24081 } catch (error) { 24082 try { 24083 const url = URL.createObjectURL(new Blob()); 24084 const uuid = url.toString(); 24085 URL.revokeObjectURL(url); 24086 return uuid.slice(uuid.lastIndexOf('/') + 1); 24087 } catch (error) { 24088 let dt = new Date().getTime(); 24089 const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { 24090 const r = (dt + Math.random() * 16) % 16 | 0; 24091 dt = Math.floor(dt / 16); 24092 return (c == 'x' ? r : r & 0x3 | 0x8).toString(16); 24093 }); 24094 return uuid; 24095 } 24096 } 24097} 24098 24099// From https://github.com/darkskyapp/string-hash 24100function hash(text) { 24101 let hash = 5381; 24102 let i = text.length; 24103 while (i) { 24104 hash = hash * 33 ^ text.charCodeAt(--i); 24105 } 24106 return (hash >>> 0).toString(); 24107} 24108 24109const ALIGNED_END_THRESHOLD_SECONDS = 0.025; 24110let TimelineOccupancy = /*#__PURE__*/function (TimelineOccupancy) { 24111 TimelineOccupancy[TimelineOccupancy["Point"] = 0] = "Point"; 24112 TimelineOccupancy[TimelineOccupancy["Range"] = 1] = "Range"; 24113 return TimelineOccupancy; 24114}({}); 24115function generateAssetIdentifier(interstitial, uri, assetListIndex) { 24116 return `${interstitial.identifier}-${assetListIndex + 1}-${hash(uri)}`; 24117} 24118class InterstitialEvent { 24119 constructor(dateRange, base) { 24120 this.base = void 0; 24121 this._duration = null; 24122 this._timelineStart = null; 24123 this.appendInPlaceDisabled = void 0; 24124 this.appendInPlaceStarted = void 0; 24125 this.dateRange = void 0; 24126 this.hasPlayed = false; 24127 this.cumulativeDuration = 0; 24128 this.resumeOffset = NaN; 24129 this.playoutLimit = NaN; 24130 this.restrictions = { 24131 skip: false, 24132 jump: false 24133 }; 24134 this.snapOptions = { 24135 out: false, 24136 in: false 24137 }; 24138 this.assetList = []; 24139 this.assetListLoader = void 0; 24140 this.assetListResponse = null; 24141 this.resumeAnchor = void 0; 24142 this.error = void 0; 24143 this.resetOnResume = void 0; 24144 this.base = base; 24145 this.dateRange = dateRange; 24146 this.setDateRange(dateRange); 24147 } 24148 setDateRange(dateRange) { 24149 this.dateRange = dateRange; 24150 this.resumeOffset = dateRange.attr.optionalFloat('X-RESUME-OFFSET', this.resumeOffset); 24151 this.playoutLimit = dateRange.attr.optionalFloat('X-PLAYOUT-LIMIT', this.playoutLimit); 24152 this.restrictions = dateRange.attr.enumeratedStringList('X-RESTRICT', this.restrictions); 24153 this.snapOptions = dateRange.attr.enumeratedStringList('X-SNAP', this.snapOptions); 24154 } 24155 reset() { 24156 var _this$assetListLoader; 24157 this.appendInPlaceStarted = false; 24158 (_this$assetListLoader = this.assetListLoader) == null || _this$assetListLoader.destroy(); 24159 this.assetListLoader = undefined; 24160 if (!this.supplementsPrimary) { 24161 this.assetListResponse = null; 24162 this.assetList = []; 24163 this._duration = null; 24164 } 24165 // `error?` is reset when seeking back over interstitial `startOffset` 24166 // using `schedule.resetErrorsInRange(start, end)`. 24167 } 24168 isAssetPastPlayoutLimit(assetIndex) { 24169 var _this$assetList$asset; 24170 if (assetIndex > 0 && assetIndex >= this.assetList.length) { 24171 return true; 24172 } 24173 const playoutLimit = this.playoutLimit; 24174 if (assetIndex <= 0 || isNaN(playoutLimit)) { 24175 return false; 24176 } 24177 if (playoutLimit === 0) { 24178 return true; 24179 } 24180 const assetOffset = ((_this$assetList$asset = this.assetList[assetIndex]) == null ? void 0 : _this$assetList$asset.startOffset) || 0; 24181 return assetOffset > playoutLimit; 24182 } 24183 findAssetIndex(asset) { 24184 const index = this.assetList.indexOf(asset); 24185 return index; 24186 } 24187 get identifier() { 24188 return this.dateRange.id; 24189 } 24190 get startDate() { 24191 return this.dateRange.startDate; 24192 } 24193 get startTime() { 24194 // Primary media timeline start time 24195 const startTime = this.dateRange.startTime; 24196 if (this.snapOptions.out) { 24197 const frag = this.dateRange.tagAnchor; 24198 if (frag) { 24199 return getSnapToFragmentTime(startTime, frag); 24200 } 24201 } 24202 return startTime; 24203 } 24204 get startOffset() { 24205 return this.cue.pre ? 0 : this.startTime; 24206 } 24207 get startIsAligned() { 24208 if (this.startTime === 0 || this.snapOptions.out) { 24209 return true; 24210 } 24211 const frag = this.dateRange.tagAnchor; 24212 if (frag) { 24213 const startTime = this.dateRange.startTime; 24214 const snappedStart = getSnapToFragmentTime(startTime, frag); 24215 return startTime - snappedStart < 0.1; 24216 } 24217 return false; 24218 } 24219 get resumptionOffset() { 24220 const resumeOffset = this.resumeOffset; 24221 const offset = isFiniteNumber(resumeOffset) ? resumeOffset : this.duration; 24222 return this.cumulativeDuration + offset; 24223 } 24224 get resumeTime() { 24225 // Primary media timeline resumption time 24226 const resumeTime = this.startOffset + this.resumptionOffset; 24227 if (this.snapOptions.in) { 24228 const frag = this.resumeAnchor; 24229 if (frag) { 24230 return getSnapToFragmentTime(resumeTime, frag); 24231 } 24232 } 24233 return resumeTime; 24234 } 24235 get appendInPlace() { 24236 if (this.appendInPlaceStarted) { 24237 return true; 24238 } 24239 if (this.appendInPlaceDisabled) { 24240 return false; 24241 } 24242 if (!this.cue.once && !this.cue.pre && 24243 // preroll starts at startPosition before startPosition is known (live) 24244 this.startIsAligned && (isNaN(this.playoutLimit) && isNaN(this.resumeOffset) || this.resumeOffset && this.duration && Math.abs(this.resumeOffset - this.duration) < ALIGNED_END_THRESHOLD_SECONDS)) { 24245 return true; 24246 } 24247 return false; 24248 } 24249 set appendInPlace(value) { 24250 if (this.appendInPlaceStarted) { 24251 this.resetOnResume = !value; 24252 return; 24253 } 24254 this.appendInPlaceDisabled = !value; 24255 } 24256 24257 // Extended timeline start time 24258 get timelineStart() { 24259 if (this._timelineStart !== null) { 24260 return this._timelineStart; 24261 } 24262 return this.startTime; 24263 } 24264 set timelineStart(value) { 24265 this._timelineStart = value; 24266 } 24267 get duration() { 24268 const playoutLimit = this.playoutLimit; 24269 let duration; 24270 if (this._duration !== null) { 24271 duration = this._duration; 24272 } else if (this.dateRange.duration) { 24273 duration = this.dateRange.duration; 24274 } else { 24275 duration = this.dateRange.plannedDuration || 0; 24276 } 24277 if (!isNaN(playoutLimit) && playoutLimit < duration) { 24278 duration = playoutLimit; 24279 } 24280 return duration; 24281 } 24282 set duration(value) { 24283 this._duration = value; 24284 } 24285 get cue() { 24286 return this.dateRange.cue; 24287 } 24288 get timelineOccupancy() { 24289 if (this.dateRange.attr['X-TIMELINE-OCCUPIES'] === 'RANGE') { 24290 return TimelineOccupancy.Range; 24291 } 24292 return TimelineOccupancy.Point; 24293 } 24294 get supplementsPrimary() { 24295 return this.dateRange.attr['X-TIMELINE-STYLE'] === 'PRIMARY'; 24296 } 24297 get contentMayVary() { 24298 return this.dateRange.attr['X-CONTENT-MAY-VARY'] !== 'NO'; 24299 } 24300 get assetUrl() { 24301 return this.dateRange.attr['X-ASSET-URI']; 24302 } 24303 get assetListUrl() { 24304 return this.dateRange.attr['X-ASSET-LIST']; 24305 } 24306 get baseUrl() { 24307 return this.base.url; 24308 } 24309 get assetListLoaded() { 24310 return this.assetList.length > 0 || this.assetListResponse !== null; 24311 } 24312 toString() { 24313 return eventToString(this); 24314 } 24315} 24316function getSnapToFragmentTime(time, frag) { 24317 return time - frag.start < frag.duration / 2 && !(Math.abs(time - (frag.start + frag.duration)) < ALIGNED_END_THRESHOLD_SECONDS) ? frag.start : frag.start + frag.duration; 24318} 24319function getInterstitialUrl(uri, sessionId, baseUrl) { 24320 const url = new self.URL(uri, baseUrl); 24321 if (url.protocol !== 'data:') { 24322 url.searchParams.set('_HLS_primary_id', sessionId); 24323 } 24324 return url; 24325} 24326function getNextAssetIndex(interstitial, assetListIndex) { 24327 while ((_interstitial$assetLi = interstitial.assetList[++assetListIndex]) != null && _interstitial$assetLi.error) { 24328 var _interstitial$assetLi; 24329 } /* no-op */ 24330 return assetListIndex; 24331} 24332function eventToString(interstitial) { 24333 return `["${interstitial.identifier}" ${interstitial.cue.pre ? '<pre>' : interstitial.cue.post ? '<post>' : ''}${interstitial.timelineStart.toFixed(2)}-${interstitial.resumeTime.toFixed(2)}]`; 24334} 24335function eventAssetToString(asset) { 24336 const start = asset.timelineStart; 24337 const duration = asset.duration || 0; 24338 return `["${asset.identifier}" ${start.toFixed(2)}-${(start + duration).toFixed(2)}]`; 24339} 24340 24341class HlsAssetPlayer { 24342 constructor(HlsPlayerClass, userConfig, interstitial, assetItem) { 24343 this.hls = void 0; 24344 this.interstitial = void 0; 24345 this.assetItem = void 0; 24346 this.tracks = null; 24347 this.hasDetails = false; 24348 this.mediaAttached = null; 24349 this._currentTime = void 0; 24350 this._bufferedEosTime = void 0; 24351 this.checkPlayout = () => { 24352 if (this.reachedPlayout(this.currentTime) && this.hls) { 24353 this.hls.trigger(Events.PLAYOUT_LIMIT_REACHED, {}); 24354 } 24355 }; 24356 const hls = this.hls = new HlsPlayerClass(userConfig); 24357 this.interstitial = interstitial; 24358 this.assetItem = assetItem; 24359 const detailsLoaded = () => { 24360 this.hasDetails = true; 24361 }; 24362 hls.once(Events.LEVEL_LOADED, detailsLoaded); 24363 hls.once(Events.AUDIO_TRACK_LOADED, detailsLoaded); 24364 hls.once(Events.SUBTITLE_TRACK_LOADED, detailsLoaded); 24365 hls.on(Events.MEDIA_ATTACHING, (name, { 24366 media 24367 }) => { 24368 this.removeMediaListeners(); 24369 this.mediaAttached = media; 24370 const event = this.interstitial; 24371 if (event.playoutLimit) { 24372 media.addEventListener('timeupdate', this.checkPlayout); 24373 if (this.appendInPlace) { 24374 hls.on(Events.BUFFER_APPENDED, () => { 24375 const bufferedEnd = this.bufferedEnd; 24376 if (this.reachedPlayout(bufferedEnd)) { 24377 this._bufferedEosTime = bufferedEnd; 24378 hls.trigger(Events.BUFFERED_TO_END, undefined); 24379 } 24380 }); 24381 } 24382 } 24383 }); 24384 } 24385 get appendInPlace() { 24386 return this.interstitial.appendInPlace; 24387 } 24388 loadSource() { 24389 const hls = this.hls; 24390 if (!hls) { 24391 return; 24392 } 24393 if (!hls.url) { 24394 let uri = this.assetItem.uri; 24395 try { 24396 uri = getInterstitialUrl(uri, hls.config.primarySessionId || '').href; 24397 } catch (error) { 24398 // Ignore error parsing ASSET_URI or adding _HLS_primary_id to it. The 24399 // issue should surface as an INTERSTITIAL_ASSET_ERROR loading the asset. 24400 } 24401 hls.loadSource(uri); 24402 } else if (hls.levels.length && !hls.started) { 24403 hls.startLoad(-1, true); 24404 } 24405 } 24406 bufferedInPlaceToEnd(media) { 24407 var _this$hls; 24408 if (!this.appendInPlace) { 24409 return false; 24410 } 24411 if ((_this$hls = this.hls) != null && _this$hls.bufferedToEnd) { 24412 return true; 24413 } 24414 if (!media) { 24415 return false; 24416 } 24417 const duration = Math.min(this._bufferedEosTime || Infinity, this.duration); 24418 const start = this.timelineOffset; 24419 const bufferInfo = BufferHelper.bufferInfo(media, start, 0); 24420 const bufferedEnd = this.getAssetTime(bufferInfo.end); 24421 return bufferedEnd >= duration - 0.02; 24422 } 24423 reachedPlayout(time) { 24424 const interstitial = this.interstitial; 24425 const playoutLimit = interstitial.playoutLimit; 24426 return this.startOffset + time >= playoutLimit; 24427 } 24428 get destroyed() { 24429 var _this$hls2; 24430 return !((_this$hls2 = this.hls) != null && _this$hls2.userConfig); 24431 } 24432 get assetId() { 24433 return this.assetItem.identifier; 24434 } 24435 get interstitialId() { 24436 return this.assetItem.parentIdentifier; 24437 } 24438 get media() { 24439 var _this$hls3; 24440 return ((_this$hls3 = this.hls) == null ? void 0 : _this$hls3.media) || null; 24441 } 24442 get bufferedEnd() { 24443 const media = this.media || this.mediaAttached; 24444 if (!media) { 24445 if (this._bufferedEosTime) { 24446 return this._bufferedEosTime; 24447 } 24448 return this.currentTime; 24449 } 24450 const bufferInfo = BufferHelper.bufferInfo(media, media.currentTime, 0.001); 24451 return this.getAssetTime(bufferInfo.end); 24452 } 24453 get currentTime() { 24454 const media = this.media || this.mediaAttached; 24455 if (!media) { 24456 return this._currentTime || 0; 24457 } 24458 return this.getAssetTime(media.currentTime); 24459 } 24460 get duration() { 24461 const duration = this.assetItem.duration; 24462 if (!duration) { 24463 return 0; 24464 } 24465 const playoutLimit = this.interstitial.playoutLimit; 24466 if (playoutLimit) { 24467 const assetPlayout = playoutLimit - this.startOffset; 24468 if (assetPlayout > 0 && assetPlayout < duration) { 24469 return assetPlayout; 24470 } 24471 } 24472 return duration; 24473 } 24474 get remaining() { 24475 const duration = this.duration; 24476 if (!duration) { 24477 return 0; 24478 } 24479 return Math.max(0, duration - this.currentTime); 24480 } 24481 get startOffset() { 24482 return this.assetItem.startOffset; 24483 } 24484 get timelineOffset() { 24485 var _this$hls4; 24486 return ((_this$hls4 = this.hls) == null ? void 0 : _this$hls4.config.timelineOffset) || 0; 24487 } 24488 set timelineOffset(value) { 24489 const timelineOffset = this.timelineOffset; 24490 if (value !== timelineOffset) { 24491 const diff = value - timelineOffset; 24492 if (Math.abs(diff) > 1 / 90000 && this.hls) { 24493 if (this.hasDetails) { 24494 throw new Error(`Cannot set timelineOffset after playlists are loaded`); 24495 } 24496 this.hls.config.timelineOffset = value; 24497 } 24498 } 24499 } 24500 getAssetTime(time) { 24501 const timelineOffset = this.timelineOffset; 24502 const duration = this.duration; 24503 return Math.min(Math.max(0, time - timelineOffset), duration); 24504 } 24505 removeMediaListeners() { 24506 const media = this.mediaAttached; 24507 if (media) { 24508 this._currentTime = media.currentTime; 24509 this.bufferSnapShot(); 24510 media.removeEventListener('timeupdate', this.checkPlayout); 24511 } 24512 } 24513 bufferSnapShot() { 24514 if (this.mediaAttached) { 24515 var _this$hls5; 24516 if ((_this$hls5 = this.hls) != null && _this$hls5.bufferedToEnd) { 24517 this._bufferedEosTime = this.bufferedEnd; 24518 } 24519 } 24520 } 24521 destroy() { 24522 this.removeMediaListeners(); 24523 if (this.hls) { 24524 this.hls.destroy(); 24525 } 24526 this.hls = null; 24527 // @ts-ignore 24528 this.tracks = this.mediaAttached = this.checkPlayout = null; 24529 } 24530 attachMedia(data) { 24531 var _this$hls6; 24532 this.loadSource(); 24533 (_this$hls6 = this.hls) == null || _this$hls6.attachMedia(data); 24534 } 24535 detachMedia() { 24536 var _this$hls7; 24537 this.removeMediaListeners(); 24538 this.mediaAttached = null; 24539 (_this$hls7 = this.hls) == null || _this$hls7.detachMedia(); 24540 } 24541 resumeBuffering() { 24542 var _this$hls8; 24543 (_this$hls8 = this.hls) == null || _this$hls8.resumeBuffering(); 24544 } 24545 pauseBuffering() { 24546 var _this$hls9; 24547 (_this$hls9 = this.hls) == null || _this$hls9.pauseBuffering(); 24548 } 24549 transferMedia() { 24550 var _this$hls0; 24551 this.bufferSnapShot(); 24552 return ((_this$hls0 = this.hls) == null ? void 0 : _this$hls0.transferMedia()) || null; 24553 } 24554 resetDetails() { 24555 const hls = this.hls; 24556 if (hls && this.hasDetails) { 24557 hls.stopLoad(); 24558 const deleteDetails = obj => delete obj.details; 24559 hls.levels.forEach(deleteDetails); 24560 hls.allAudioTracks.forEach(deleteDetails); 24561 hls.allSubtitleTracks.forEach(deleteDetails); 24562 this.hasDetails = false; 24563 } 24564 } 24565 on(event, listener, context) { 24566 var _this$hls1; 24567 (_this$hls1 = this.hls) == null || _this$hls1.on(event, listener); 24568 } 24569 once(event, listener, context) { 24570 var _this$hls10; 24571 (_this$hls10 = this.hls) == null || _this$hls10.once(event, listener); 24572 } 24573 off(event, listener, context) { 24574 var _this$hls11; 24575 (_this$hls11 = this.hls) == null || _this$hls11.off(event, listener); 24576 } 24577 toString() { 24578 var _this$hls12; 24579 return `HlsAssetPlayer: ${eventAssetToString(this.assetItem)} ${(_this$hls12 = this.hls) == null ? void 0 : _this$hls12.sessionId} ${this.appendInPlace ? 'append-in-place' : ''}`; 24580 } 24581} 24582 24583const ABUTTING_THRESHOLD_SECONDS = 0.033; 24584class InterstitialsSchedule extends Logger { 24585 constructor(onScheduleUpdate, logger) { 24586 super('interstitials-sched', logger); 24587 this.onScheduleUpdate = void 0; 24588 this.eventMap = {}; 24589 this.events = null; 24590 this.items = null; 24591 this.durations = { 24592 primary: 0, 24593 playout: 0, 24594 integrated: 0 24595 }; 24596 this.onScheduleUpdate = onScheduleUpdate; 24597 } 24598 destroy() { 24599 this.reset(); 24600 // @ts-ignore 24601 this.onScheduleUpdate = null; 24602 } 24603 reset() { 24604 this.eventMap = {}; 24605 this.setDurations(0, 0, 0); 24606 if (this.events) { 24607 this.events.forEach(interstitial => interstitial.reset()); 24608 } 24609 this.events = this.items = null; 24610 } 24611 resetErrorsInRange(start, end) { 24612 if (this.events) { 24613 return this.events.reduce((count, interstitial) => { 24614 if (start <= interstitial.startOffset && end > interstitial.startOffset) { 24615 delete interstitial.error; 24616 return count + 1; 24617 } 24618 return count; 24619 }, 0); 24620 } 24621 return 0; 24622 } 24623 get duration() { 24624 const items = this.items; 24625 return items ? items[items.length - 1].end : 0; 24626 } 24627 get length() { 24628 return this.items ? this.items.length : 0; 24629 } 24630 getEvent(identifier) { 24631 return identifier ? this.eventMap[identifier] || null : null; 24632 } 24633 hasEvent(identifier) { 24634 return identifier in this.eventMap; 24635 } 24636 findItemIndex(item, time) { 24637 if (item.event) { 24638 // Find Event Item 24639 return this.findEventIndex(item.event.identifier); 24640 } 24641 // Find Primary Item 24642 let index = -1; 24643 if (item.nextEvent) { 24644 index = this.findEventIndex(item.nextEvent.identifier) - 1; 24645 } else if (item.previousEvent) { 24646 index = this.findEventIndex(item.previousEvent.identifier) + 1; 24647 } 24648 const items = this.items; 24649 if (items) { 24650 if (!items[index]) { 24651 if (time === undefined) { 24652 time = item.start; 24653 } 24654 index = this.findItemIndexAtTime(time); 24655 } 24656 // Only return index of a Primary Item 24657 while (index >= 0 && (_items$index = items[index]) != null && _items$index.event) { 24658 var _items$index; 24659 // If index found is an interstitial it is not a valid result as it should have been matched up top 24660 // decrement until result is negative (not found) or a primary segment 24661 index--; 24662 } 24663 } 24664 return index; 24665 } 24666 findItemIndexAtTime(timelinePos, timelineType) { 24667 const items = this.items; 24668 if (items) { 24669 for (let i = 0; i < items.length; i++) { 24670 let timeRange = items[i]; 24671 if (timelineType && timelineType !== 'primary') { 24672 timeRange = timeRange[timelineType]; 24673 } 24674 if (timelinePos === timeRange.start || timelinePos > timeRange.start && timelinePos < timeRange.end) { 24675 return i; 24676 } 24677 } 24678 } 24679 return -1; 24680 } 24681 findJumpRestrictedIndex(startIndex, endIndex) { 24682 const items = this.items; 24683 if (items) { 24684 for (let i = startIndex; i <= endIndex; i++) { 24685 if (!items[i]) { 24686 break; 24687 } 24688 const event = items[i].event; 24689 if (event != null && event.restrictions.jump && !event.appendInPlace) { 24690 return i; 24691 } 24692 } 24693 } 24694 return -1; 24695 } 24696 findEventIndex(identifier) { 24697 const items = this.items; 24698 if (items) { 24699 for (let i = items.length; i--;) { 24700 var _items$i$event; 24701 if (((_items$i$event = items[i].event) == null ? void 0 : _items$i$event.identifier) === identifier) { 24702 return i; 24703 } 24704 } 24705 } 24706 return -1; 24707 } 24708 findAssetIndex(event, timelinePos) { 24709 const assetList = event.assetList; 24710 const length = assetList.length; 24711 if (length > 1) { 24712 for (let i = 0; i < length; i++) { 24713 const asset = assetList[i]; 24714 if (!asset.error) { 24715 const timelineStart = asset.timelineStart; 24716 if (timelinePos === timelineStart || timelinePos > timelineStart && (timelinePos < timelineStart + (asset.duration || 0) || i === length - 1)) { 24717 return i; 24718 } 24719 } 24720 } 24721 } 24722 return 0; 24723 } 24724 get assetIdAtEnd() { 24725 var _this$items; 24726 const interstitialAtEnd = (_this$items = this.items) == null || (_this$items = _this$items[this.length - 1]) == null ? void 0 : _this$items.event; 24727 if (interstitialAtEnd) { 24728 const assetList = interstitialAtEnd.assetList; 24729 const assetAtEnd = assetList[assetList.length - 1]; 24730 if (assetAtEnd) { 24731 return assetAtEnd.identifier; 24732 } 24733 } 24734 return null; 24735 } 24736 parseInterstitialDateRanges(mediaSelection, enableAppendInPlace) { 24737 const details = mediaSelection.main.details; 24738 const { 24739 dateRanges 24740 } = details; 24741 const previousInterstitialEvents = this.events; 24742 const interstitialEvents = this.parseDateRanges(dateRanges, { 24743 url: details.url 24744 }, enableAppendInPlace); 24745 const ids = Object.keys(dateRanges); 24746 const removedInterstitials = previousInterstitialEvents ? previousInterstitialEvents.filter(event => !ids.includes(event.identifier)) : []; 24747 if (interstitialEvents.length) { 24748 // pre-rolls, post-rolls, and events with the same start time are played in playlist tag order 24749 // all other events are ordered by start time 24750 interstitialEvents.sort((a, b) => { 24751 const aPre = a.cue.pre; 24752 const aPost = a.cue.post; 24753 const bPre = b.cue.pre; 24754 const bPost = b.cue.post; 24755 if (aPre && !bPre) { 24756 return -1; 24757 } 24758 if (bPre && !aPre) { 24759 return 1; 24760 } 24761 if (aPost && !bPost) { 24762 return 1; 24763 } 24764 if (bPost && !aPost) { 24765 return -1; 24766 } 24767 if (!aPre && !bPre && !aPost && !bPost) { 24768 const startA = a.startTime; 24769 const startB = b.startTime; 24770 if (startA !== startB) { 24771 return startA - startB; 24772 } 24773 } 24774 return a.dateRange.tagOrder - b.dateRange.tagOrder; 24775 }); 24776 } 24777 this.events = interstitialEvents; 24778 24779 // Clear removed DateRanges from buffered list (kills playback of active Interstitials) 24780 removedInterstitials.forEach(interstitial => { 24781 this.removeEvent(interstitial); 24782 }); 24783 this.updateSchedule(mediaSelection, removedInterstitials); 24784 } 24785 updateSchedule(mediaSelection, removedInterstitials = [], forceUpdate = false) { 24786 const events = this.events || []; 24787 if (events.length || removedInterstitials.length || this.length < 2) { 24788 const currentItems = this.items; 24789 const updatedItems = this.parseSchedule(events, mediaSelection); 24790 const updated = forceUpdate || removedInterstitials.length || (currentItems == null ? void 0 : currentItems.length) !== updatedItems.length || updatedItems.some((item, i) => { 24791 return Math.abs(item.playout.start - currentItems[i].playout.start) > 0.005 || Math.abs(item.playout.end - currentItems[i].playout.end) > 0.005; 24792 }); 24793 if (updated) { 24794 this.items = updatedItems; 24795 // call interstitials-controller onScheduleUpdated() 24796 this.onScheduleUpdate(removedInterstitials, currentItems); 24797 } 24798 } 24799 } 24800 parseDateRanges(dateRanges, baseData, enableAppendInPlace) { 24801 const interstitialEvents = []; 24802 const ids = Object.keys(dateRanges); 24803 for (let i = 0; i < ids.length; i++) { 24804 const id = ids[i]; 24805 const dateRange = dateRanges[id]; 24806 if (dateRange.isInterstitial) { 24807 let interstitial = this.eventMap[id]; 24808 if (interstitial) { 24809 // Update InterstitialEvent already parsed and mapped 24810 // This retains already loaded duration and loaded asset list info 24811 interstitial.setDateRange(dateRange); 24812 } else { 24813 interstitial = new InterstitialEvent(dateRange, baseData); 24814 this.eventMap[id] = interstitial; 24815 if (enableAppendInPlace === false) { 24816 interstitial.appendInPlace = enableAppendInPlace; 24817 } 24818 } 24819 interstitialEvents.push(interstitial); 24820 } 24821 } 24822 return interstitialEvents; 24823 } 24824 parseSchedule(interstitialEvents, mediaSelection) { 24825 const schedule = []; 24826 const details = mediaSelection.main.details; 24827 const primaryDuration = details.live ? Infinity : details.edge; 24828 let playoutDuration = 0; 24829 24830 // Filter events that have errored from the schedule (Primary fallback) 24831 interstitialEvents = interstitialEvents.filter(event => !event.error && !(event.cue.once && event.hasPlayed)); 24832 if (interstitialEvents.length) { 24833 // Update Schedule 24834 this.resolveOffsets(interstitialEvents, mediaSelection); 24835 24836 // Populate Schedule with Interstitial Event and Primary Segment Items 24837 let primaryPosition = 0; 24838 let integratedTime = 0; 24839 interstitialEvents.forEach((interstitial, i) => { 24840 const preroll = interstitial.cue.pre; 24841 const postroll = interstitial.cue.post; 24842 const previousEvent = interstitialEvents[i - 1] || null; 24843 const appendInPlace = interstitial.appendInPlace; 24844 const eventStart = postroll ? primaryDuration : interstitial.startOffset; 24845 const interstitialDuration = interstitial.duration; 24846 const timelineDuration = interstitial.timelineOccupancy === TimelineOccupancy.Range ? interstitialDuration : 0; 24847 const resumptionOffset = interstitial.resumptionOffset; 24848 const inSameStartTimeSequence = (previousEvent == null ? void 0 : previousEvent.startTime) === eventStart; 24849 const start = eventStart + interstitial.cumulativeDuration; 24850 let end = appendInPlace ? start + interstitialDuration : eventStart + resumptionOffset; 24851 if (preroll || !postroll && eventStart <= 0) { 24852 // preroll or in-progress midroll 24853 const integratedStart = integratedTime; 24854 integratedTime += timelineDuration; 24855 interstitial.timelineStart = start; 24856 const playoutStart = playoutDuration; 24857 playoutDuration += interstitialDuration; 24858 schedule.push({ 24859 event: interstitial, 24860 start, 24861 end, 24862 playout: { 24863 start: playoutStart, 24864 end: playoutDuration 24865 }, 24866 integrated: { 24867 start: integratedStart, 24868 end: integratedTime 24869 } 24870 }); 24871 } else if (eventStart <= primaryDuration) { 24872 if (!inSameStartTimeSequence) { 24873 const segmentDuration = eventStart - primaryPosition; 24874 // Do not schedule a primary segment if interstitials are abutting by less than ABUTTING_THRESHOLD_SECONDS 24875 if (segmentDuration > ABUTTING_THRESHOLD_SECONDS) { 24876 // primary segment 24877 const timelineStart = primaryPosition; 24878 const _integratedStart = integratedTime; 24879 integratedTime += segmentDuration; 24880 const _playoutStart = playoutDuration; 24881 playoutDuration += segmentDuration; 24882 const primarySegment = { 24883 previousEvent: interstitialEvents[i - 1] || null, 24884 nextEvent: interstitial, 24885 start: timelineStart, 24886 end: timelineStart + segmentDuration, 24887 playout: { 24888 start: _playoutStart, 24889 end: playoutDuration 24890 }, 24891 integrated: { 24892 start: _integratedStart, 24893 end: integratedTime 24894 } 24895 }; 24896 schedule.push(primarySegment); 24897 } else if (segmentDuration > 0 && previousEvent) { 24898 // Add previous event `resumeTime` (based on duration or resumeOffset) so that it ends aligned with this one 24899 previousEvent.cumulativeDuration += segmentDuration; 24900 schedule[schedule.length - 1].end = eventStart; 24901 } 24902 } 24903 // midroll / postroll 24904 if (postroll) { 24905 end = start; 24906 } 24907 interstitial.timelineStart = start; 24908 const integratedStart = integratedTime; 24909 integratedTime += timelineDuration; 24910 const playoutStart = playoutDuration; 24911 playoutDuration += interstitialDuration; 24912 schedule.push({ 24913 event: interstitial, 24914 start, 24915 end, 24916 playout: { 24917 start: playoutStart, 24918 end: playoutDuration 24919 }, 24920 integrated: { 24921 start: integratedStart, 24922 end: integratedTime 24923 } 24924 }); 24925 } else { 24926 // Interstitial starts after end of primary VOD - not included in schedule 24927 return; 24928 } 24929 const resumeTime = interstitial.resumeTime; 24930 if (postroll || resumeTime > primaryDuration) { 24931 primaryPosition = primaryDuration; 24932 } else { 24933 primaryPosition = resumeTime; 24934 } 24935 }); 24936 if (primaryPosition < primaryDuration) { 24937 var _schedule; 24938 // last primary segment 24939 const timelineStart = primaryPosition; 24940 const integratedStart = integratedTime; 24941 const segmentDuration = primaryDuration - primaryPosition; 24942 integratedTime += segmentDuration; 24943 const playoutStart = playoutDuration; 24944 playoutDuration += segmentDuration; 24945 schedule.push({ 24946 previousEvent: ((_schedule = schedule[schedule.length - 1]) == null ? void 0 : _schedule.event) || null, 24947 nextEvent: null, 24948 start: primaryPosition, 24949 end: timelineStart + segmentDuration, 24950 playout: { 24951 start: playoutStart, 24952 end: playoutDuration 24953 }, 24954 integrated: { 24955 start: integratedStart, 24956 end: integratedTime 24957 } 24958 }); 24959 } 24960 this.setDurations(primaryDuration, playoutDuration, integratedTime); 24961 } else { 24962 // no interstials - schedule is one primary segment 24963 const start = 0; 24964 schedule.push({ 24965 previousEvent: null, 24966 nextEvent: null, 24967 start, 24968 end: primaryDuration, 24969 playout: { 24970 start, 24971 end: primaryDuration 24972 }, 24973 integrated: { 24974 start, 24975 end: primaryDuration 24976 } 24977 }); 24978 this.setDurations(primaryDuration, primaryDuration, primaryDuration); 24979 } 24980 return schedule; 24981 } 24982 setDurations(primary, playout, integrated) { 24983 this.durations = { 24984 primary, 24985 playout, 24986 integrated 24987 }; 24988 } 24989 resolveOffsets(interstitialEvents, mediaSelection) { 24990 const details = mediaSelection.main.details; 24991 const primaryDuration = details.live ? Infinity : details.edge; 24992 24993 // First resolve cumulative resumption offsets for Interstitials that start at the same DateTime 24994 let cumulativeDuration = 0; 24995 let lastScheduledStart = -1; 24996 interstitialEvents.forEach((interstitial, i) => { 24997 const preroll = interstitial.cue.pre; 24998 const postroll = interstitial.cue.post; 24999 const eventStart = preroll ? 0 : postroll ? primaryDuration : interstitial.startTime; 25000 this.updateAssetDurations(interstitial); 25001 25002 // X-RESUME-OFFSET values of interstitials scheduled at the same time are cumulative 25003 const inSameStartTimeSequence = lastScheduledStart === eventStart; 25004 if (inSameStartTimeSequence) { 25005 interstitial.cumulativeDuration = cumulativeDuration; 25006 } else { 25007 cumulativeDuration = 0; 25008 lastScheduledStart = eventStart; 25009 } 25010 if (!postroll && interstitial.snapOptions.in) { 25011 // FIXME: Include audio playlist in snapping 25012 interstitial.resumeAnchor = findFragmentByPTS(null, details.fragments, interstitial.startOffset + interstitial.resumptionOffset, 0, 0) || undefined; 25013 } 25014 // Check if primary fragments align with resumption offset and disable appendInPlace if they do not 25015 if (interstitial.appendInPlace && !interstitial.appendInPlaceStarted) { 25016 const alignedSegmentStart = this.primaryCanResumeInPlaceAt(interstitial, mediaSelection); 25017 if (!alignedSegmentStart) { 25018 interstitial.appendInPlace = false; 25019 } 25020 } 25021 if (!interstitial.appendInPlace && i + 1 < interstitialEvents.length) { 25022 // abutting Interstitials must use the same MediaSource strategy, this applies to all whether or not they are back to back: 25023 const timeBetween = interstitialEvents[i + 1].startTime - interstitialEvents[i].resumeTime; 25024 if (timeBetween < ABUTTING_THRESHOLD_SECONDS) { 25025 interstitialEvents[i + 1].appendInPlace = false; 25026 if (interstitialEvents[i + 1].appendInPlace) { 25027 this.warn(`Could not change append strategy for abutting event ${interstitial}`); 25028 } 25029 } 25030 } 25031 // Update cumulativeDuration for next abutting interstitial with the same start date 25032 const resumeOffset = isFiniteNumber(interstitial.resumeOffset) ? interstitial.resumeOffset : interstitial.duration; 25033 cumulativeDuration += resumeOffset; 25034 }); 25035 } 25036 primaryCanResumeInPlaceAt(interstitial, mediaSelection) { 25037 const resumeTime = interstitial.resumeTime; 25038 const resumesInPlaceAt = interstitial.startTime + interstitial.resumptionOffset; 25039 if (Math.abs(resumeTime - resumesInPlaceAt) > ALIGNED_END_THRESHOLD_SECONDS) { 25040 this.log(`"${interstitial.identifier}" resumption ${resumeTime} not aligned with estimated timeline end ${resumesInPlaceAt}`); 25041 return false; 25042 } 25043 const playlists = Object.keys(mediaSelection); 25044 return !playlists.some(playlistType => { 25045 const details = mediaSelection[playlistType].details; 25046 const playlistEnd = details.edge; 25047 if (resumeTime >= playlistEnd) { 25048 // Live playback - resumption segments are not yet available 25049 this.log(`"${interstitial.identifier}" resumption ${resumeTime} past ${playlistType} playlist end ${playlistEnd}`); 25050 // Assume alignment is possible (or reset can take place) 25051 return false; 25052 } 25053 const startFragment = findFragmentByPTS(null, details.fragments, resumeTime); 25054 if (!startFragment) { 25055 this.log(`"${interstitial.identifier}" resumption ${resumeTime} does not align with any fragments in ${playlistType} playlist (${details.fragStart}-${details.fragmentEnd})`); 25056 return true; 25057 } 25058 const allowance = playlistType === 'audio' ? 0.175 : 0; 25059 const alignedWithSegment = Math.abs(startFragment.start - resumeTime) < ALIGNED_END_THRESHOLD_SECONDS + allowance || Math.abs(startFragment.end - resumeTime) < ALIGNED_END_THRESHOLD_SECONDS + allowance; 25060 if (!alignedWithSegment) { 25061 this.log(`"${interstitial.identifier}" resumption ${resumeTime} not aligned with ${playlistType} fragment bounds (${startFragment.start}-${startFragment.end} sn: ${startFragment.sn} cc: ${startFragment.cc})`); 25062 return true; 25063 } 25064 return false; 25065 }); 25066 } 25067 updateAssetDurations(interstitial) { 25068 if (!interstitial.assetListLoaded) { 25069 return; 25070 } 25071 const eventStart = interstitial.timelineStart; 25072 let sumDuration = 0; 25073 let hasUnknownDuration = false; 25074 let hasErrors = false; 25075 for (let i = 0; i < interstitial.assetList.length; i++) { 25076 const asset = interstitial.assetList[i]; 25077 const timelineStart = eventStart + sumDuration; 25078 asset.startOffset = sumDuration; 25079 asset.timelineStart = timelineStart; 25080 hasUnknownDuration || (hasUnknownDuration = asset.duration === null); 25081 hasErrors || (hasErrors = !!asset.error); 25082 const duration = asset.error ? 0 : asset.duration || 0; 25083 sumDuration += duration; 25084 } 25085 // Use the sum of known durations when it is greater than the stated duration 25086 if (hasUnknownDuration && !hasErrors) { 25087 interstitial.duration = Math.max(sumDuration, interstitial.duration); 25088 } else { 25089 interstitial.duration = sumDuration; 25090 } 25091 } 25092 removeEvent(interstitial) { 25093 interstitial.reset(); 25094 delete this.eventMap[interstitial.identifier]; 25095 } 25096} 25097function segmentToString(segment) { 25098 return `[${segment.event ? '"' + segment.event.identifier + '"' : 'primary'}: ${segment.start.toFixed(2)}-${segment.end.toFixed(2)}]`; 25099} 25100 25101class AssetListLoader { 25102 constructor(hls) { 25103 this.hls = void 0; 25104 this.hls = hls; 25105 } 25106 destroy() { 25107 // @ts-ignore 25108 this.hls = null; 25109 } 25110 loadAssetList(interstitial, hlsStartOffset) { 25111 const assetListUrl = interstitial.assetListUrl; 25112 let url; 25113 try { 25114 url = getInterstitialUrl(assetListUrl, this.hls.sessionId, interstitial.baseUrl); 25115 } catch (error) { 25116 const errorData = this.assignAssetListError(interstitial, ErrorDetails.ASSET_LIST_LOAD_ERROR, error, assetListUrl); 25117 this.hls.trigger(Events.ERROR, errorData); 25118 return; 25119 } 25120 if (hlsStartOffset && url.protocol !== 'data:') { 25121 url.searchParams.set('_HLS_start_offset', '' + hlsStartOffset); 25122 } 25123 const config = this.hls.config; 25124 const Loader = config.loader; 25125 const loader = new Loader(config); 25126 const context = { 25127 responseType: 'json', 25128 url: url.href 25129 }; 25130 const loadPolicy = config.interstitialAssetListLoadPolicy.default; 25131 const loaderConfig = { 25132 loadPolicy, 25133 timeout: loadPolicy.maxLoadTimeMs, 25134 maxRetry: 0, 25135 retryDelay: 0, 25136 maxRetryDelay: 0 25137 }; 25138 const callbacks = { 25139 onSuccess: (response, stats, context, networkDetails) => { 25140 const assetListResponse = response.data; 25141 const assets = assetListResponse == null ? void 0 : assetListResponse.ASSETS; 25142 if (!Array.isArray(assets)) { 25143 const errorData = this.assignAssetListError(interstitial, ErrorDetails.ASSET_LIST_PARSING_ERROR, new Error(`Invalid interstitial asset list`), context.url, stats, networkDetails); 25144 this.hls.trigger(Events.ERROR, errorData); 25145 return; 25146 } 25147 interstitial.assetListResponse = assetListResponse; 25148 this.hls.trigger(Events.ASSET_LIST_LOADED, { 25149 event: interstitial, 25150 assetListResponse, 25151 networkDetails 25152 }); 25153 }, 25154 onError: (error, context, networkDetails, stats) => { 25155 const errorData = this.assignAssetListError(interstitial, ErrorDetails.ASSET_LIST_LOAD_ERROR, new Error(`Error loading X-ASSET-LIST: HTTP status ${error.code} ${error.text} (${context.url})`), context.url, stats, networkDetails); 25156 this.hls.trigger(Events.ERROR, errorData); 25157 }, 25158 onTimeout: (stats, context, networkDetails) => { 25159 const errorData = this.assignAssetListError(interstitial, ErrorDetails.ASSET_LIST_LOAD_TIMEOUT, new Error(`Timeout loading X-ASSET-LIST (${context.url})`), context.url, stats, networkDetails); 25160 this.hls.trigger(Events.ERROR, errorData); 25161 } 25162 }; 25163 loader.load(context, loaderConfig, callbacks); 25164 this.hls.trigger(Events.ASSET_LIST_LOADING, { 25165 event: interstitial 25166 }); 25167 return loader; 25168 } 25169 assignAssetListError(interstitial, details, error, url, stats, networkDetails) { 25170 interstitial.error = error; 25171 return { 25172 type: ErrorTypes.NETWORK_ERROR, 25173 details, 25174 fatal: false, 25175 interstitial, 25176 url, 25177 error, 25178 networkDetails, 25179 stats 25180 }; 25181 } 25182} 25183 25184function playWithCatch(media) { 25185 var _media$play; 25186 media == null || (_media$play = media.play()) == null || _media$play.catch(() => { 25187 /* no-op */ 25188 }); 25189} 25190function timelineMessage(label, time) { 25191 return `[${label}] Advancing timeline position to ${time}`; 25192} 25193class InterstitialsController extends Logger {
vendor: 5,755 bytes, lines 25194-25347
25194 constructor(hls, HlsPlayerClass) { 25195 super('interstitials', hls.logger); 25196 this.HlsPlayerClass = void 0; 25197 this.hls = void 0; 25198 this.assetListLoader = void 0; 25199 // Last updated LevelDetails 25200 this.mediaSelection = null; 25201 this.altSelection = null; 25202 // Media and MediaSource/SourceBuffers 25203 this.media = null; 25204 this.detachedData = null; 25205 this.requiredTracks = null; 25206 // Public Interface for Interstitial playback state and control 25207 this.manager = null; 25208 // Interstitial Asset Players 25209 this.playerQueue = []; 25210 // Timeline position tracking 25211 this.bufferedPos = -1; 25212 this.timelinePos = -1; 25213 // Schedule 25214 this.schedule = void 0; 25215 // Schedule playback and buffering state 25216 this.playingItem = null; 25217 this.bufferingItem = null; 25218 this.waitingItem = null; 25219 this.endedItem = null; 25220 this.playingAsset = null; 25221 this.endedAsset = null; 25222 this.bufferingAsset = null; 25223 this.shouldPlay = false; 25224 this.onPlay = () => { 25225 this.shouldPlay = true; 25226 }; 25227 this.onPause = () => { 25228 this.shouldPlay = false; 25229 }; 25230 this.onSeeking = () => { 25231 const currentTime = this.currentTime; 25232 if (currentTime === undefined || this.playbackDisabled || !this.schedule) { 25233 return; 25234 } 25235 const diff = currentTime - this.timelinePos; 25236 const roundingError = Math.abs(diff) < 1 / 705600000; // one flick 25237 if (roundingError) { 25238 return; 25239 } 25240 const backwardSeek = diff <= -0.01; 25241 if (this.timelinePos === -1 && !this.effectivePlayingItem) { 25242 this.checkStart(); 25243 } 25244 this.timelinePos = currentTime; 25245 this.bufferedPos = currentTime; 25246 25247 // Check if seeking out of an item 25248 const playingItem = this.playingItem; 25249 if (!playingItem) { 25250 this.checkBuffer(); 25251 return; 25252 } 25253 if (backwardSeek) { 25254 const resetCount = this.schedule.resetErrorsInRange(currentTime, currentTime - diff); 25255 if (resetCount) { 25256 this.updateSchedule(true); 25257 } 25258 } 25259 this.checkBuffer(); 25260 if (backwardSeek && currentTime < playingItem.start || currentTime >= playingItem.end) { 25261 var _this$media; 25262 const playingIndex = this.findItemIndex(playingItem); 25263 let scheduleIndex = this.schedule.findItemIndexAtTime(currentTime); 25264 if (scheduleIndex === -1) { 25265 scheduleIndex = playingIndex + (backwardSeek ? -1 : 1); 25266 this.log(`seeked ${backwardSeek ? 'back ' : ''}to position not covered by schedule ${currentTime} (resolving from ${playingIndex} to ${scheduleIndex})`); 25267 } 25268 if (!this.isInterstitial(playingItem) && (_this$media = this.media) != null && _this$media.paused) { 25269 this.shouldPlay = false; 25270 } 25271 if (!backwardSeek) { 25272 // check if an Interstitial between the current item and target item has an X-RESTRICT JUMP restriction 25273 if (scheduleIndex > playingIndex) { 25274 const jumpIndex = this.schedule.findJumpRestrictedIndex(playingIndex + 1, scheduleIndex); 25275 if (jumpIndex > playingIndex) { 25276 this.setSchedulePosition(jumpIndex); 25277 return; 25278 } 25279 } 25280 } 25281 this.setSchedulePosition(scheduleIndex); 25282 return; 25283 } 25284 // Check if seeking out of an asset (assumes same item following above check) 25285 const playingAsset = this.playingAsset; 25286 if (!playingAsset) { 25287 // restart Interstitial at end 25288 if (this.playingLastItem && this.isInterstitial(playingItem)) { 25289 const restartAsset = playingItem.event.assetList[0]; 25290 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 25291 if (restartAsset) { 25292 this.endedItem = this.playingItem; 25293 this.playingItem = null; 25294 this.setScheduleToAssetAtTime(currentTime, restartAsset); 25295 } 25296 } 25297 return; 25298 } 25299 const start = playingAsset.timelineStart; 25300 const duration = playingAsset.duration || 0; 25301 if (backwardSeek && currentTime < start || currentTime >= start + duration) { 25302 var _playingItem$event; 25303 if ((_playingItem$event = playingItem.event) != null && _playingItem$event.appendInPlace) { 25304 // Return SourceBuffer(s) to primary player and flush 25305 this.clearAssetPlayers(playingItem.event, playingItem); 25306 this.flushFrontBuffer(currentTime); 25307 } 25308 this.setScheduleToAssetAtTime(currentTime, playingAsset); 25309 } 25310 }; 25311 this.onTimeupdate = () => { 25312 const currentTime = this.currentTime; 25313 if (currentTime === undefined || this.playbackDisabled) { 25314 return; 25315 } 25316 if (this.timelinePos === -1 && !this.effectivePlayingItem) { 25317 this.checkStart(); 25318 } 25319 25320 // Only allow timeupdate to advance primary position, seeking is used for jumping back 25321 // this prevents primaryPos from being reset to 0 after re-attach 25322 if (currentTime > this.timelinePos) { 25323 this.timelinePos = currentTime; 25324 if (currentTime > this.bufferedPos) { 25325 this.checkBuffer(); 25326 } 25327 } else { 25328 return; 25329 } 25330 25331 // Check if playback has entered the next item 25332 const playingItem = this.playingItem; 25333 if (!playingItem || this.playingLastItem) { 25334 return; 25335 } 25336 if (currentTime >= playingItem.end) { 25337 this.timelinePos = playingItem.end; 25338 const playingIndex = this.findItemIndex(playingItem); 25339 this.setSchedulePosition(playingIndex + 1); 25340 } 25341 // Check if playback has entered the next asset 25342 const playingAsset = this.playingAsset; 25343 if (!playingAsset) { 25344 return; 25345 } 25346 const end = playingAsset.timelineStart + (playingAsset.duration || 0); 25347 if (currentTime >
vendor: 25,891 bytes, lines 25347-25970
25347= end) { 25348 this.setScheduleToAssetAtTime(currentTime, playingAsset); 25349 } 25350 }; 25351 // Schedule update callback 25352 this.onScheduleUpdate = (removedInterstitials, previousItems) => { 25353 const schedule = this.schedule; 25354 if (!schedule) { 25355 return; 25356 } 25357 const playingItem = this.playingItem; 25358 const interstitialEvents = schedule.events || []; 25359 const scheduleItems = schedule.items || []; 25360 const durations = schedule.durations; 25361 const removedIds = removedInterstitials.map(interstitial => interstitial.identifier); 25362 const interstitialsUpdated = !!(interstitialEvents.length || removedIds.length); 25363 if (interstitialsUpdated || previousItems) { 25364 this.log(`INTERSTITIALS_UPDATED (${interstitialEvents.length}): ${interstitialEvents} 25365Schedule: ${scheduleItems.map(seg => segmentToString(seg))} pos: ${this.timelinePos}`); 25366 } 25367 if (removedIds.length) { 25368 this.log(`Removed events ${removedIds}`); 25369 } 25370 25371 // Update schedule item references 25372 // Do not replace Interstitial playingItem without a match - used for INTERSTITIAL_ASSET_ENDED and INTERSTITIAL_ENDED 25373 let updatedPlayingItem = null; 25374 let updatedBufferingItem = null; 25375 if (playingItem) { 25376 updatedPlayingItem = this.updateItem(playingItem, this.timelinePos); 25377 if (this.itemsMatch(playingItem, updatedPlayingItem)) { 25378 this.playingItem = updatedPlayingItem; 25379 } else { 25380 this.waitingItem = this.endedItem = null; 25381 } 25382 } 25383 // Clear waitingItem if it has been removed from the schedule 25384 this.waitingItem = this.updateItem(this.waitingItem); 25385 this.endedItem = this.updateItem(this.endedItem); 25386 // Do not replace Interstitial bufferingItem without a match - used for transfering media element or source 25387 const bufferingItem = this.bufferingItem; 25388 if (bufferingItem) { 25389 updatedBufferingItem = this.updateItem(bufferingItem, this.bufferedPos); 25390 if (this.itemsMatch(bufferingItem, updatedBufferingItem)) { 25391 this.bufferingItem = updatedBufferingItem; 25392 } else if (bufferingItem.event) { 25393 // Interstitial removed from schedule (Live -> VOD or other scenario where Start Date is outside the range of VOD Playlist) 25394 this.bufferingItem = this.playingItem; 25395 this.clearInterstitial(bufferingItem.event, null); 25396 } 25397 } 25398 removedInterstitials.forEach(interstitial => { 25399 interstitial.assetList.forEach(asset => { 25400 this.clearAssetPlayer(asset.identifier, null); 25401 }); 25402 }); 25403 this.playerQueue.forEach(player => { 25404 if (player.interstitial.appendInPlace) { 25405 const timelineStart = player.assetItem.timelineStart; 25406 const diff = player.timelineOffset - timelineStart; 25407 if (diff) { 25408 try { 25409 player.timelineOffset = timelineStart; 25410 } catch (e) { 25411 if (Math.abs(diff) > ALIGNED_END_THRESHOLD_SECONDS) { 25412 this.warn(`${e} ("${player.assetId}" ${player.timelineOffset}->${timelineStart})`); 25413 } 25414 } 25415 } 25416 } 25417 }); 25418 if (interstitialsUpdated || previousItems) { 25419 this.hls.trigger(Events.INTERSTITIALS_UPDATED, { 25420 events: interstitialEvents.slice(0), 25421 schedule: scheduleItems.slice(0), 25422 durations, 25423 removedIds 25424 }); 25425 if (this.isInterstitial(playingItem) && removedIds.includes(playingItem.event.identifier)) { 25426 this.warn(`Interstitial "${playingItem.event.identifier}" removed while playing`); 25427 this.primaryFallback(playingItem.event); 25428 return; 25429 } 25430 if (playingItem) { 25431 this.trimInPlace(updatedPlayingItem, playingItem); 25432 } 25433 if (bufferingItem && updatedBufferingItem !== updatedPlayingItem) { 25434 this.trimInPlace(updatedBufferingItem, bufferingItem); 25435 } 25436 25437 // Check if buffered to new Interstitial event boundary 25438 // (Live update publishes Interstitial with new segment) 25439 this.checkBuffer(); 25440 } 25441 }; 25442 this.hls = hls; 25443 this.HlsPlayerClass = HlsPlayerClass; 25444 this.assetListLoader = new AssetListLoader(hls); 25445 this.schedule = new InterstitialsSchedule(this.onScheduleUpdate, hls.logger); 25446 this.registerListeners(); 25447 } 25448 registerListeners() { 25449 const hls = this.hls; 25450 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 25451 if (hls) { 25452 hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 25453 hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 25454 hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 25455 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 25456 hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this); 25457 hls.on(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); 25458 hls.on(Events.AUDIO_TRACK_UPDATED, this.onAudioTrackUpdated, this); 25459 hls.on(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this); 25460 hls.on(Events.SUBTITLE_TRACK_UPDATED, this.onSubtitleTrackUpdated, this); 25461 hls.on(Events.EVENT_CUE_ENTER, this.onInterstitialCueEnter, this); 25462 hls.on(Events.ASSET_LIST_LOADED, this.onAssetListLoaded, this); 25463 hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this); 25464 hls.on(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); 25465 hls.on(Events.BUFFERED_TO_END, this.onBufferedToEnd, this); 25466 hls.on(Events.MEDIA_ENDED, this.onMediaEnded, this); 25467 hls.on(Events.ERROR, this.onError, this); 25468 hls.on(Events.DESTROYING, this.onDestroying, this); 25469 } 25470 } 25471 unregisterListeners() { 25472 const hls = this.hls; 25473 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 25474 if (hls) { 25475 hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 25476 hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 25477 hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 25478 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 25479 hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this); 25480 hls.off(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); 25481 hls.off(Events.AUDIO_TRACK_UPDATED, this.onAudioTrackUpdated, this); 25482 hls.off(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this); 25483 hls.off(Events.SUBTITLE_TRACK_UPDATED, this.onSubtitleTrackUpdated, this); 25484 hls.off(Events.EVENT_CUE_ENTER, this.onInterstitialCueEnter, this); 25485 hls.off(Events.ASSET_LIST_LOADED, this.onAssetListLoaded, this); 25486 hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this); 25487 hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this); 25488 hls.off(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); 25489 hls.off(Events.BUFFERED_TO_END, this.onBufferedToEnd, this); 25490 hls.off(Events.MEDIA_ENDED, this.onMediaEnded, this); 25491 hls.off(Events.ERROR, this.onError, this); 25492 hls.off(Events.DESTROYING, this.onDestroying, this); 25493 } 25494 } 25495 startLoad() { 25496 // TODO: startLoad - check for waitingItem and retry by resetting schedule 25497 this.resumeBuffering(); 25498 } 25499 stopLoad() { 25500 // TODO: stopLoad - stop all scheule.events[].assetListLoader?.abort() then delete the loaders 25501 this.pauseBuffering(); 25502 } 25503 resumeBuffering() { 25504 var _this$getBufferingPla; 25505 (_this$getBufferingPla = this.getBufferingPlayer()) == null || _this$getBufferingPla.resumeBuffering(); 25506 } 25507 pauseBuffering() { 25508 var _this$getBufferingPla2; 25509 (_this$getBufferingPla2 = this.getBufferingPlayer()) == null || _this$getBufferingPla2.pauseBuffering(); 25510 } 25511 destroy() { 25512 this.unregisterListeners(); 25513 this.stopLoad(); 25514 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 25515 if (this.assetListLoader) { 25516 this.assetListLoader.destroy(); 25517 } 25518 this.emptyPlayerQueue(); 25519 this.clearScheduleState(); 25520 if (this.schedule) { 25521 this.schedule.destroy(); 25522 } 25523 this.media = this.detachedData = this.mediaSelection = this.requiredTracks = this.altSelection = this.schedule = this.manager = null; 25524 // @ts-ignore 25525 this.hls = this.HlsPlayerClass = this.log = null; 25526 // @ts-ignore 25527 this.assetListLoader = null; 25528 // @ts-ignore 25529 this.onPlay = this.onPause = this.onSeeking = this.onTimeupdate = null; 25530 // @ts-ignore 25531 this.onScheduleUpdate = null; 25532 } 25533 onDestroying() { 25534 const media = this.primaryMedia || this.media; 25535 if (media) { 25536 this.removeMediaListeners(media); 25537 } 25538 } 25539 removeMediaListeners(media) { 25540 removeEventListener(media, 'play', this.onPlay); 25541 removeEventListener(media, 'pause', this.onPause); 25542 removeEventListener(media, 'seeking', this.onSeeking); 25543 removeEventListener(media, 'timeupdate', this.onTimeupdate); 25544 } 25545 onMediaAttaching(event, data) { 25546 const media = this.media = data.media; 25547 addEventListener(media, 'seeking', this.onSeeking); 25548 addEventListener(media, 'timeupdate', this.onTimeupdate); 25549 addEventListener(media, 'play', this.onPlay); 25550 addEventListener(media, 'pause', this.onPause); 25551 } 25552 onMediaAttached(event, data) { 25553 const playingItem = this.effectivePlayingItem; 25554 const detachedMedia = this.detachedData; 25555 this.detachedData = null; 25556 if (playingItem === null) { 25557 this.checkStart(); 25558 } else if (!detachedMedia) { 25559 // Resume schedule after detached externally 25560 this.clearScheduleState(); 25561 const playingIndex = this.findItemIndex(playingItem); 25562 this.setSchedulePosition(playingIndex); 25563 } 25564 } 25565 clearScheduleState() { 25566 this.log(`clear schedule state`); 25567 this.playingItem = this.bufferingItem = this.waitingItem = this.endedItem = this.playingAsset = this.endedAsset = this.bufferingAsset = null; 25568 } 25569 onMediaDetaching(event, data) { 25570 const transferringMedia = !!data.transferMedia; 25571 const media = this.media; 25572 this.media = null; 25573 if (transferringMedia) { 25574 return; 25575 } 25576 if (media) { 25577 this.removeMediaListeners(media); 25578 } 25579 // If detachMedia is called while in an Interstitial, detach the asset player as well and reset the schedule position 25580 if (this.detachedData) { 25581 const player = this.getBufferingPlayer(); 25582 if (player) { 25583 this.log(`Removing schedule state for detachedData and ${player}`); 25584 this.playingAsset = this.endedAsset = this.bufferingAsset = this.bufferingItem = this.waitingItem = this.detachedData = null; 25585 player.detachMedia(); 25586 } 25587 this.shouldPlay = false; 25588 } 25589 } 25590 get interstitialsManager() { 25591 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 25592 if (!this.hls) { 25593 return null; 25594 } 25595 if (this.manager) { 25596 return this.manager; 25597 } 25598 const c = this; 25599 const effectiveBufferingItem = () => c.bufferingItem || c.waitingItem; 25600 const getAssetPlayer = asset => asset ? c.getAssetPlayer(asset.identifier) : asset; 25601 const getMappedTime = (item, timelineType, asset, controllerField, assetPlayerField) => { 25602 if (item) { 25603 let time = item[timelineType].start; 25604 const interstitial = item.event; 25605 if (interstitial) { 25606 if (timelineType === 'playout' || interstitial.timelineOccupancy !== TimelineOccupancy.Point) { 25607 const assetPlayer = getAssetPlayer(asset); 25608 if ((assetPlayer == null ? void 0 : assetPlayer.interstitial) === interstitial) { 25609 time += assetPlayer.assetItem.startOffset + assetPlayer[assetPlayerField]; 25610 } 25611 } 25612 } else { 25613 const value = controllerField === 'bufferedPos' ? getBufferedEnd() : c[controllerField]; 25614 time += value - item.start; 25615 } 25616 return time; 25617 } 25618 return 0; 25619 }; 25620 const findMappedTime = (primaryTime, timelineType) => { 25621 var _c$schedule; 25622 if (primaryTime !== 0 && timelineType !== 'primary' && (_c$schedule = c.schedule) != null && _c$schedule.length) { 25623 var _c$schedule$items; 25624 const index = c.schedule.findItemIndexAtTime(primaryTime); 25625 const item = (_c$schedule$items = c.schedule.items) == null ? void 0 : _c$schedule$items[index]; 25626 if (item) { 25627 const diff = item[timelineType].start - item.start; 25628 return primaryTime + diff; 25629 } 25630 } 25631 return primaryTime; 25632 }; 25633 const getBufferedEnd = () => { 25634 const value = c.bufferedPos; 25635 if (value === Number.MAX_VALUE) { 25636 return getMappedDuration('primary'); 25637 } 25638 return Math.max(value, 0); 25639 }; 25640 const getMappedDuration = timelineType => { 25641 var _c$primaryDetails, _c$schedule2; 25642 if ((_c$primaryDetails = c.primaryDetails) != null && _c$primaryDetails.live) { 25643 // return end of last event item or playlist 25644 return c.primaryDetails.edge; 25645 } 25646 return ((_c$schedule2 = c.schedule) == null ? void 0 : _c$schedule2.durations[timelineType]) || 0; 25647 }; 25648 const seekTo = (time, timelineType) => { 25649 var _item$event, _c$schedule$items2; 25650 const item = c.effectivePlayingItem; 25651 if (item != null && (_item$event = item.event) != null && _item$event.restrictions.skip || !c.schedule) { 25652 return; 25653 } 25654 c.log(`seek to ${time} "${timelineType}"`); 25655 const playingItem = c.effectivePlayingItem; 25656 const targetIndex = c.schedule.findItemIndexAtTime(time, timelineType); 25657 const targetItem = (_c$schedule$items2 = c.schedule.items) == null ? void 0 : _c$schedule$items2[targetIndex]; 25658 const bufferingPlayer = c.getBufferingPlayer(); 25659 const bufferingInterstitial = bufferingPlayer == null ? void 0 : bufferingPlayer.interstitial; 25660 const appendInPlace = bufferingInterstitial == null ? void 0 : bufferingInterstitial.appendInPlace; 25661 const seekInItem = playingItem && c.itemsMatch(playingItem, targetItem); 25662 if (playingItem && (appendInPlace || seekInItem)) { 25663 // seek in asset player or primary media (appendInPlace) 25664 const assetPlayer = getAssetPlayer(c.playingAsset); 25665 const media = (assetPlayer == null ? void 0 : assetPlayer.media) || c.primaryMedia; 25666 if (media) { 25667 const currentTime = timelineType === 'primary' ? media.currentTime : getMappedTime(playingItem, timelineType, c.playingAsset, 'timelinePos', 'currentTime'); 25668 const diff = time - currentTime; 25669 const seekToTime = (appendInPlace ? currentTime : media.currentTime) + diff; 25670 if (seekToTime >= 0 && (!assetPlayer || appendInPlace || seekToTime <= assetPlayer.duration)) { 25671 media.currentTime = seekToTime; 25672 return; 25673 } 25674 } 25675 } 25676 // seek out of item or asset 25677 if (targetItem) { 25678 let seekToTime = time; 25679 if (timelineType !== 'primary') { 25680 const primarySegmentStart = targetItem[timelineType].start; 25681 const diff = time - primarySegmentStart; 25682 seekToTime = targetItem.start + diff; 25683 } 25684 const targetIsPrimary = !c.isInterstitial(targetItem); 25685 if ((!c.isInterstitial(playingItem) || playingItem.event.appendInPlace) && (targetIsPrimary || targetItem.event.appendInPlace)) { 25686 const media = c.media || (appendInPlace ? bufferingPlayer == null ? void 0 : bufferingPlayer.media : null); 25687 if (media) { 25688 media.currentTime = seekToTime; 25689 } 25690 } else if (playingItem) { 25691 // check if an Interstitial between the current item and target item has an X-RESTRICT JUMP restriction 25692 const playingIndex = c.findItemIndex(playingItem); 25693 if (targetIndex > playingIndex) { 25694 const jumpIndex = c.schedule.findJumpRestrictedIndex(playingIndex + 1, targetIndex); 25695 if (jumpIndex > playingIndex) { 25696 c.setSchedulePosition(jumpIndex); 25697 return; 25698 } 25699 } 25700 let assetIndex = 0; 25701 if (targetIsPrimary) { 25702 c.timelinePos = seekToTime; 25703 c.checkBuffer(); 25704 } else { 25705 const assetList = targetItem.event.assetList; 25706 const eventTime = time - (targetItem[timelineType] || targetItem).start; 25707 for (let i = assetList.length; i--;) { 25708 const asset = assetList[i]; 25709 if (asset.duration && eventTime >= asset.startOffset && eventTime < asset.startOffset + asset.duration) { 25710 assetIndex = i; 25711 break; 25712 } 25713 } 25714 } 25715 c.setSchedulePosition(targetIndex, assetIndex); 25716 } 25717 } 25718 }; 25719 const getActiveInterstitial = () => { 25720 const playingItem = c.effectivePlayingItem; 25721 if (c.isInterstitial(playingItem)) { 25722 return playingItem; 25723 } 25724 const bufferingItem = effectiveBufferingItem(); 25725 if (c.isInterstitial(bufferingItem)) { 25726 return bufferingItem; 25727 } 25728 return null; 25729 }; 25730 const interstitialPlayer = { 25731 get bufferedEnd() { 25732 const interstitialItem = effectiveBufferingItem(); 25733 const bufferingItem = c.bufferingItem; 25734 if (bufferingItem && bufferingItem === interstitialItem) { 25735 var _c$bufferingAsset; 25736 return getMappedTime(bufferingItem, 'playout', c.bufferingAsset, 'bufferedPos', 'bufferedEnd') - bufferingItem.playout.start || ((_c$bufferingAsset = c.bufferingAsset) == null ? void 0 : _c$bufferingAsset.startOffset) || 0; 25737 } 25738 return 0; 25739 }, 25740 get currentTime() { 25741 const interstitialItem = getActiveInterstitial(); 25742 const playingItem = c.effectivePlayingItem; 25743 if (playingItem && playingItem === interstitialItem) { 25744 return getMappedTime(playingItem, 'playout', c.effectivePlayingAsset, 'timelinePos', 'currentTime') - playingItem.playout.start; 25745 } 25746 return 0; 25747 }, 25748 set currentTime(time) { 25749 const interstitialItem = getActiveInterstitial(); 25750 const playingItem = c.effectivePlayingItem; 25751 if (playingItem && playingItem === interstitialItem) { 25752 seekTo(time + playingItem.playout.start, 'playout'); 25753 } 25754 }, 25755 get duration() { 25756 const interstitialItem = getActiveInterstitial(); 25757 if (interstitialItem) { 25758 return interstitialItem.playout.end - interstitialItem.playout.start; 25759 } 25760 return 0; 25761 }, 25762 get assetPlayers() { 25763 var _getActiveInterstitia; 25764 const assetList = (_getActiveInterstitia = getActiveInterstitial()) == null ? void 0 : _getActiveInterstitia.event.assetList; 25765 if (assetList) { 25766 return assetList.map(asset => c.getAssetPlayer(asset.identifier)); 25767 } 25768 return []; 25769 }, 25770 get playingIndex() { 25771 var _getActiveInterstitia2; 25772 const interstitial = (_getActiveInterstitia2 = getActiveInterstitial()) == null ? void 0 : _getActiveInterstitia2.event; 25773 if (interstitial && c.effectivePlayingAsset) { 25774 return interstitial.findAssetIndex(c.effectivePlayingAsset); 25775 } 25776 return -1; 25777 }, 25778 get scheduleItem() { 25779 return getActiveInterstitial(); 25780 } 25781 }; 25782 return this.manager = { 25783 get events() { 25784 var _c$schedule3; 25785 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 25786 return ((_c$schedule3 = c.schedule) == null || (_c$schedule3 = _c$schedule3.events) == null ? void 0 : _c$schedule3.slice(0)) || []; 25787 }, 25788 get schedule() { 25789 var _c$schedule4; 25790 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 25791 return ((_c$schedule4 = c.schedule) == null || (_c$schedule4 = _c$schedule4.items) == null ? void 0 : _c$schedule4.slice(0)) || []; 25792 }, 25793 get interstitialPlayer() { 25794 if (getActiveInterstitial()) { 25795 return interstitialPlayer; 25796 } 25797 return null; 25798 }, 25799 get playerQueue() { 25800 return c.playerQueue.slice(0); 25801 }, 25802 get bufferingAsset() { 25803 return c.bufferingAsset; 25804 }, 25805 get bufferingItem() { 25806 return effectiveBufferingItem(); 25807 }, 25808 get bufferingIndex() { 25809 const item = effectiveBufferingItem(); 25810 return c.findItemIndex(item); 25811 }, 25812 get playingAsset() { 25813 return c.effectivePlayingAsset; 25814 }, 25815 get playingItem() { 25816 return c.effectivePlayingItem; 25817 }, 25818 get playingIndex() { 25819 const item = c.effectivePlayingItem; 25820 return c.findItemIndex(item); 25821 }, 25822 primary: { 25823 get bufferedEnd() { 25824 return getBufferedEnd(); 25825 }, 25826 get currentTime() { 25827 const timelinePos = c.timelinePos; 25828 return timelinePos > 0 ? timelinePos : 0; 25829 }, 25830 set currentTime(time) { 25831 seekTo(time, 'primary'); 25832 }, 25833 get duration() { 25834 return getMappedDuration('primary'); 25835 }, 25836 get seekableStart() { 25837 var _c$primaryDetails2; 25838 return ((_c$primaryDetails2 = c.primaryDetails) == null ? void 0 : _c$primaryDetails2.fragmentStart) || 0; 25839 } 25840 }, 25841 integrated: { 25842 get bufferedEnd() { 25843 return getMappedTime(effectiveBufferingItem(), 'integrated', c.bufferingAsset, 'bufferedPos', 'bufferedEnd'); 25844 }, 25845 get currentTime() { 25846 return getMappedTime(c.effectivePlayingItem, 'integrated', c.effectivePlayingAsset, 'timelinePos', 'currentTime'); 25847 }, 25848 set currentTime(time) { 25849 seekTo(time, 'integrated'); 25850 }, 25851 get duration() { 25852 return getMappedDuration('integrated'); 25853 }, 25854 get seekableStart() { 25855 var _c$primaryDetails3; 25856 return findMappedTime(((_c$primaryDetails3 = c.primaryDetails) == null ? void 0 : _c$primaryDetails3.fragmentStart) || 0, 'integrated'); 25857 } 25858 }, 25859 skip: () => { 25860 const item = c.effectivePlayingItem; 25861 const event = item == null ? void 0 : item.event; 25862 if (event && !event.restrictions.skip) { 25863 const index = c.findItemIndex(item); 25864 if (event.appendInPlace) { 25865 const time = item.playout.start + item.event.duration; 25866 seekTo(time + 0.001, 'playout'); 25867 } else { 25868 c.advanceAfterAssetEnded(event, index, Infinity); 25869 } 25870 } 25871 } 25872 }; 25873 } 25874 25875 // Schedule getters 25876 get effectivePlayingItem() { 25877 return this.waitingItem || this.playingItem || this.endedItem; 25878 } 25879 get effectivePlayingAsset() { 25880 return this.playingAsset || this.endedAsset; 25881 } 25882 get playingLastItem() { 25883 var _this$schedule; 25884 const playingItem = this.playingItem; 25885 const items = (_this$schedule = this.schedule) == null ? void 0 : _this$schedule.items; 25886 if (!this.playbackStarted || !playingItem || !items) { 25887 return false; 25888 } 25889 return this.findItemIndex(playingItem) === items.length - 1; 25890 } 25891 get playbackStarted() { 25892 return this.effectivePlayingItem !== null; 25893 } 25894 25895 // Media getters and event callbacks 25896 get currentTime() { 25897 var _this$bufferingItem, _media; 25898 if (this.mediaSelection === null) { 25899 // Do not advance before schedule is known 25900 return undefined; 25901 } 25902 // Ignore currentTime when detached for Interstitial playback with source reset 25903 const queuedForPlayback = this.waitingItem || this.playingItem; 25904 if (this.isInterstitial(queuedForPlayback) && !queuedForPlayback.event.appendInPlace) { 25905 return undefined; 25906 } 25907 let media = this.media; 25908 if (!media && (_this$bufferingItem = this.bufferingItem) != null && (_this$bufferingItem = _this$bufferingItem.event) != null && _this$bufferingItem.appendInPlace) { 25909 // Observe detached media currentTime when appending in place 25910 media = this.primaryMedia; 25911 } 25912 const currentTime = (_media = media) == null ? void 0 : _media.currentTime; 25913 if (currentTime === undefined || !isFiniteNumber(currentTime)) { 25914 return undefined; 25915 } 25916 return currentTime; 25917 } 25918 get primaryMedia() { 25919 var _this$detachedData; 25920 return this.media || ((_this$detachedData = this.detachedData) == null ? void 0 : _this$detachedData.media) || null; 25921 } 25922 isInterstitial(item) { 25923 return !!(item != null && item.event); 25924 } 25925 retreiveMediaSource(assetId, toSegment) { 25926 const player = this.getAssetPlayer(assetId); 25927 if (player) { 25928 this.transferMediaFromPlayer(player, toSegment); 25929 } 25930 } 25931 transferMediaFromPlayer(player, toSegment) { 25932 const appendInPlace = player.interstitial.appendInPlace; 25933 const playerMedia = player.media; 25934 if (appendInPlace && playerMedia === this.primaryMedia) { 25935 this.bufferingAsset = null; 25936 if (!toSegment || this.isInterstitial(toSegment) && !toSegment.event.appendInPlace) { 25937 // MediaSource cannot be transfered back to an Interstitial that requires a source reset 25938 // no-op when toSegment is undefined 25939 if (toSegment && playerMedia) { 25940 this.detachedData = { 25941 media: playerMedia 25942 }; 25943 return; 25944 } 25945 } 25946 const attachMediaSourceData = player.transferMedia(); 25947 this.log(`transfer MediaSource from ${player} ${stringify(attachMediaSourceData)}`); 25948 this.detachedData = attachMediaSourceData; 25949 } else if (toSegment && playerMedia) { 25950 this.shouldPlay || (this.shouldPlay = !playerMedia.paused); 25951 } 25952 } 25953 transferMediaTo(player, media) { 25954 var _this$detachedData2, _attachMediaSourceDat; 25955 if (player.media === media) { 25956 return; 25957 } 25958 let attachMediaSourceData = null; 25959 const primaryPlayer = this.hls; 25960 const isAssetPlayer = player !== primaryPlayer; 25961 const appendInPlace = isAssetPlayer && player.interstitial.appendInPlace; 25962 const detachedMediaSource = (_this$detachedData2 = this.detachedData) == null ? void 0 : _this$detachedData2.mediaSource; 25963 let logFromSource; 25964 if (primaryPlayer.media) { 25965 if (appendInPlace) { 25966 attachMediaSourceData = primaryPlayer.transferMedia(); 25967 this.detachedData = attachMediaSourceData; 25968 } 25969 logFromSource = `Primary`; 25970 } else if (detachedMediaSource) {
vendor: 6,499 bytes, lines 25971-26118
25971 const bufferingPlayer = this.getBufferingPlayer(); 25972 if (bufferingPlayer) { 25973 attachMediaSourceData = bufferingPlayer.transferMedia(); 25974 logFromSource = `${bufferingPlayer}`; 25975 } else { 25976 logFromSource = `detached MediaSource`; 25977 } 25978 } else { 25979 logFromSource = `detached media`; 25980 } 25981 if (!attachMediaSourceData) { 25982 if (detachedMediaSource) { 25983 attachMediaSourceData = this.detachedData; 25984 this.log(`using detachedData: MediaSource ${stringify(attachMediaSourceData)}`); 25985 } else if (!this.detachedData || primaryPlayer.media === media) { 25986 // Keep interstitial media transition consistent 25987 const playerQueue = this.playerQueue; 25988 if (playerQueue.length > 1) { 25989 playerQueue.forEach(queuedPlayer => { 25990 if (isAssetPlayer && queuedPlayer.interstitial.appendInPlace !== appendInPlace) { 25991 const interstitial = queuedPlayer.interstitial; 25992 this.clearInterstitial(queuedPlayer.interstitial, null); 25993 interstitial.appendInPlace = false; // setter may be a no-op; 25994 // `appendInPlace` getter may still return `true` after insterstitial streaming has begun in that mode. 25995 if (interstitial.appendInPlace) { 25996 this.warn(`Could not change append strategy for queued assets ${interstitial}`); 25997 } 25998 } 25999 }); 26000 } 26001 this.hls.detachMedia(); 26002 this.detachedData = { 26003 media 26004 }; 26005 } 26006 } 26007 const transferring = attachMediaSourceData && 'mediaSource' in attachMediaSourceData && ((_attachMediaSourceDat = attachMediaSourceData.mediaSource) == null ? void 0 : _attachMediaSourceDat.readyState) !== 'closed'; 26008 const dataToAttach = transferring && attachMediaSourceData ? attachMediaSourceData : media; 26009 this.log(`${transferring ? 'transfering MediaSource' : 'attaching media'} to ${isAssetPlayer ? player : 'Primary'} from ${logFromSource} (media.currentTime: ${media.currentTime})`); 26010 const schedule = this.schedule; 26011 if (dataToAttach === attachMediaSourceData && schedule) { 26012 const isAssetAtEndOfSchedule = isAssetPlayer && player.assetId === schedule.assetIdAtEnd; 26013 // Prevent asset players from marking EoS on transferred MediaSource 26014 dataToAttach.overrides = { 26015 duration: schedule.duration, 26016 endOfStream: !isAssetPlayer || isAssetAtEndOfSchedule, 26017 cueRemoval: !isAssetPlayer 26018 }; 26019 } 26020 player.attachMedia(dataToAttach); 26021 } 26022 onInterstitialCueEnter() { 26023 this.onTimeupdate(); 26024 } 26025 // Scheduling methods 26026 checkStart() { 26027 const schedule = this.schedule; 26028 const interstitialEvents = schedule == null ? void 0 : schedule.events; 26029 if (!interstitialEvents || this.playbackDisabled || !this.media) { 26030 return; 26031 } 26032 // Check buffered to pre-roll 26033 if (this.bufferedPos === -1) { 26034 this.bufferedPos = 0; 26035 } 26036 // Start stepping through schedule when playback begins for the first time and we have a pre-roll 26037 const timelinePos = this.timelinePos; 26038 const effectivePlayingItem = this.effectivePlayingItem; 26039 if (timelinePos === -1) { 26040 const startPosition = this.hls.startPosition; 26041 this.timelinePos = startPosition; 26042 if (interstitialEvents.length === 0) { 26043 this.setSchedulePosition(0); 26044 } else if (interstitialEvents[0].cue.pre) { 26045 this.log(timelineMessage('checkStart (preroll)', startPosition)); 26046 const index = schedule.findEventIndex(interstitialEvents[0].identifier); 26047 this.setSchedulePosition(index); 26048 } else if (startPosition >= 0 || !this.primaryLive) { 26049 this.log(timelineMessage('checkStart', startPosition)); 26050 const start = this.timelinePos = startPosition > 0 ? startPosition : 0; 26051 const index = schedule.findItemIndexAtTime(start); 26052 this.setSchedulePosition(index); 26053 } else if (this.hls.liveSyncPosition === 0) { 26054 this.setSchedulePosition(0); 26055 } else { 26056 this.log('[checkStart] waiting for live start'); 26057 } 26058 } else if (effectivePlayingItem && !this.playingItem) { 26059 this.log(timelineMessage('checkStart (playing item)', effectivePlayingItem.start)); 26060 const index = schedule.findItemIndex(effectivePlayingItem); 26061 this.setSchedulePosition(index); 26062 } 26063 } 26064 advanceAssetBuffering(item, assetItem) { 26065 const interstitial = item.event; 26066 const assetListIndex = interstitial.findAssetIndex(assetItem); 26067 const nextAssetIndex = getNextAssetIndex(interstitial, assetListIndex); 26068 if (!interstitial.isAssetPastPlayoutLimit(nextAssetIndex)) { 26069 this.bufferedToEvent(item, nextAssetIndex); 26070 } else if (this.schedule) { 26071 var _this$schedule$items; 26072 const nextItem = (_this$schedule$items = this.schedule.items) == null ? void 0 : _this$schedule$items[this.findItemIndex(item) + 1]; 26073 if (nextItem) { 26074 this.bufferedToItem(nextItem); 26075 } 26076 } 26077 } 26078 advanceAfterAssetEnded(interstitial, index, assetListIndex) { 26079 const nextAssetIndex = getNextAssetIndex(interstitial, assetListIndex); 26080 if (!interstitial.isAssetPastPlayoutLimit(nextAssetIndex)) { 26081 // Advance to next asset list item 26082 if (interstitial.appendInPlace) { 26083 const assetItem = interstitial.assetList[nextAssetIndex]; 26084 if (assetItem) { 26085 this.advanceInPlace(assetItem.timelineStart); 26086 } 26087 } 26088 this.setSchedulePosition(index, nextAssetIndex); 26089 } else if (this.schedule) { 26090 // Advance to next schedule segment 26091 // check if we've reached the end of the program 26092 const scheduleItems = this.schedule.items; 26093 if (scheduleItems) { 26094 const nextIndex = index + 1; 26095 const scheduleLength = scheduleItems.length; 26096 if (nextIndex >= scheduleLength) { 26097 this.setSchedulePosition(-1); 26098 return; 26099 } 26100 const resumptionTime = interstitial.resumeTime; 26101 if (this.timelinePos < resumptionTime) { 26102 this.log(timelineMessage('advanceAfterAssetEnded', resumptionTime)); 26103 this.timelinePos = resumptionTime; 26104 if (interstitial.appendInPlace) { 26105 this.advanceInPlace(resumptionTime); 26106 } 26107 this.checkBuffer(this.bufferedPos < resumptionTime); 26108 } 26109 this.setSchedulePosition(nextIndex); 26110 } 26111 } 26112 } 26113 setScheduleToAssetAtTime(time, playingAsset) { 26114 const schedule = this.schedule; 26115 if (!schedule) { 26116 return; 26117 } 26118 const parentIdentifier = playingAsset.parentIdentifier;
vendor: 248,144 bytes, lines 26119-33372
26119 const interstitial = schedule.getEvent(parentIdentifier); 26120 if (interstitial) { 26121 const itemIndex = schedule.findEventIndex(parentIdentifier); 26122 const assetListIndex = schedule.findAssetIndex(interstitial, time); 26123 this.advanceAfterAssetEnded(interstitial, itemIndex, assetListIndex - 1); 26124 } 26125 } 26126 setSchedulePosition(index, assetListIndex) { 26127 var _this$schedule2; 26128 const scheduleItems = (_this$schedule2 = this.schedule) == null ? void 0 : _this$schedule2.items; 26129 if (!scheduleItems || this.playbackDisabled) { 26130 return; 26131 } 26132 const scheduledItem = index >= 0 ? scheduleItems[index] : null; 26133 this.log(`setSchedulePosition ${index}, ${assetListIndex} (${scheduledItem ? segmentToString(scheduledItem) : scheduledItem}) pos: ${this.timelinePos}`); 26134 // Cleanup current item / asset 26135 const currentItem = this.waitingItem || this.playingItem; 26136 const playingLastItem = this.playingLastItem; 26137 if (this.isInterstitial(currentItem)) { 26138 const interstitial = currentItem.event; 26139 const playingAsset = this.playingAsset; 26140 const assetId = playingAsset == null ? void 0 : playingAsset.identifier; 26141 const player = assetId ? this.getAssetPlayer(assetId) : null; 26142 if (player && assetId && (!this.eventItemsMatch(currentItem, scheduledItem) || assetListIndex !== undefined && assetId !== interstitial.assetList[assetListIndex].identifier)) { 26143 var _this$detachedData3; 26144 const playingAssetListIndex = interstitial.findAssetIndex(playingAsset); 26145 this.log(`INTERSTITIAL_ASSET_ENDED ${playingAssetListIndex + 1}/${interstitial.assetList.length} ${eventAssetToString(playingAsset)}`); 26146 this.endedAsset = playingAsset; 26147 this.playingAsset = null; 26148 this.hls.trigger(Events.INTERSTITIAL_ASSET_ENDED, { 26149 asset: playingAsset, 26150 assetListIndex: playingAssetListIndex, 26151 event: interstitial, 26152 schedule: scheduleItems.slice(0), 26153 scheduleIndex: index, 26154 player 26155 }); 26156 if (currentItem !== this.playingItem) { 26157 // Schedule change occured on INTERSTITIAL_ASSET_ENDED 26158 if (this.itemsMatch(currentItem, this.playingItem) && 26159 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 26160 !this.playingAsset // INTERSTITIAL_ASSET_ENDED side-effect 26161 ) { 26162 this.advanceAfterAssetEnded(interstitial, this.findItemIndex(this.playingItem), playingAssetListIndex); 26163 } 26164 // Navigation occured on INTERSTITIAL_ASSET_ENDED 26165 return; 26166 } 26167 this.retreiveMediaSource(assetId, scheduledItem); 26168 if (player.media && !((_this$detachedData3 = this.detachedData) != null && _this$detachedData3.mediaSource)) { 26169 player.detachMedia(); 26170 } 26171 } 26172 if (!this.eventItemsMatch(currentItem, scheduledItem)) { 26173 this.endedItem = currentItem; 26174 this.playingItem = null; 26175 this.log(`INTERSTITIAL_ENDED ${interstitial} ${segmentToString(currentItem)}`); 26176 interstitial.hasPlayed = true; 26177 this.hls.trigger(Events.INTERSTITIAL_ENDED, { 26178 event: interstitial, 26179 schedule: scheduleItems.slice(0), 26180 scheduleIndex: index 26181 }); 26182 // Exiting an Interstitial 26183 if (interstitial.cue.once) { 26184 var _this$schedule3; 26185 // Remove interstitial with CUE attribute value of ONCE after it has played 26186 this.updateSchedule(); 26187 const updatedScheduleItems = (_this$schedule3 = this.schedule) == null ? void 0 : _this$schedule3.items; 26188 if (scheduledItem && updatedScheduleItems) { 26189 const updatedIndex = this.findItemIndex(scheduledItem); 26190 this.advanceSchedule(updatedIndex, updatedScheduleItems, assetListIndex, currentItem, playingLastItem); 26191 } 26192 return; 26193 } 26194 } 26195 } 26196 this.advanceSchedule(index, scheduleItems, assetListIndex, currentItem, playingLastItem); 26197 } 26198 advanceSchedule(index, scheduleItems, assetListIndex, currentItem, playedLastItem) { 26199 const schedule = this.schedule; 26200 if (!schedule) { 26201 return; 26202 } 26203 const scheduledItem = scheduleItems[index] || null; 26204 const media = this.primaryMedia; 26205 // Cleanup out of range Interstitials 26206 const playerQueue = this.playerQueue; 26207 if (playerQueue.length) { 26208 playerQueue.forEach(player => { 26209 const interstitial = player.interstitial; 26210 const queuedIndex = schedule.findEventIndex(interstitial.identifier); 26211 if (queuedIndex < index || queuedIndex > index + 1) { 26212 this.clearInterstitial(interstitial, scheduledItem); 26213 } 26214 }); 26215 } 26216 // Setup scheduled item 26217 if (this.isInterstitial(scheduledItem)) { 26218 this.timelinePos = Math.min(Math.max(this.timelinePos, scheduledItem.start), scheduledItem.end); 26219 // Handle Interstitial 26220 const interstitial = scheduledItem.event; 26221 // find asset index 26222 if (assetListIndex === undefined) { 26223 assetListIndex = schedule.findAssetIndex(interstitial, this.timelinePos); 26224 const assetIndexCandidate = getNextAssetIndex(interstitial, assetListIndex - 1); 26225 if (interstitial.isAssetPastPlayoutLimit(assetIndexCandidate) || interstitial.appendInPlace && this.timelinePos === scheduledItem.end) { 26226 this.advanceAfterAssetEnded(interstitial, index, assetListIndex); 26227 return; 26228 } 26229 assetListIndex = assetIndexCandidate; 26230 } 26231 // Ensure Interstitial is enqueued 26232 const waitingItem = this.waitingItem; 26233 if (!this.assetsBuffered(scheduledItem, media)) { 26234 this.setBufferingItem(scheduledItem); 26235 } 26236 let player = this.preloadAssets(interstitial, assetListIndex); 26237 if (!this.eventItemsMatch(scheduledItem, waitingItem || currentItem)) { 26238 this.waitingItem = scheduledItem; 26239 this.log(`INTERSTITIAL_STARTED ${segmentToString(scheduledItem)} ${interstitial.appendInPlace ? 'append in place' : ''}`); 26240 this.hls.trigger(Events.INTERSTITIAL_STARTED, { 26241 event: interstitial, 26242 schedule: scheduleItems.slice(0), 26243 scheduleIndex: index 26244 }); 26245 } 26246 if (!interstitial.assetListLoaded) { 26247 // Waiting at end of primary content segment 26248 // Expect setSchedulePosition to be called again once ASSET-LIST is loaded 26249 this.log(`Waiting for ASSET-LIST to complete loading ${interstitial}`); 26250 return; 26251 } 26252 if (interstitial.assetListLoader) { 26253 interstitial.assetListLoader.destroy(); 26254 interstitial.assetListLoader = undefined; 26255 } 26256 if (!media) { 26257 this.log(`Waiting for attachMedia to start Interstitial ${interstitial}`); 26258 return; 26259 } 26260 // Update schedule and asset list position now that it can start 26261 this.waitingItem = this.endedItem = null; 26262 this.playingItem = scheduledItem; 26263 26264 // If asset-list is empty or missing asset index, advance to next item 26265 const assetItem = interstitial.assetList[assetListIndex]; 26266 if (!assetItem) { 26267 this.advanceAfterAssetEnded(interstitial, index, assetListIndex || 0); 26268 return; 26269 } 26270 26271 // Start Interstitial Playback 26272 if (!player) { 26273 player = this.getAssetPlayer(assetItem.identifier); 26274 } 26275 if (player === null || player.destroyed) { 26276 const assetListLength = interstitial.assetList.length; 26277 this.warn(`asset ${assetListIndex + 1}/${assetListLength} player destroyed ${interstitial}`); 26278 player = this.createAssetPlayer(interstitial, assetItem, assetListIndex); 26279 player.loadSource(); 26280 } 26281 if (!this.eventItemsMatch(scheduledItem, this.bufferingItem)) { 26282 if (interstitial.appendInPlace && this.isAssetBuffered(assetItem)) { 26283 return; 26284 } 26285 } 26286 this.startAssetPlayer(player, assetListIndex, scheduleItems, index, media); 26287 if (this.shouldPlay) { 26288 playWithCatch(player.media); 26289 } 26290 } else if (scheduledItem) { 26291 this.resumePrimary(scheduledItem, index, currentItem); 26292 if (this.shouldPlay) { 26293 playWithCatch(this.hls.media); 26294 } 26295 } else if (playedLastItem && this.isInterstitial(currentItem)) { 26296 // Maintain playingItem state at end of schedule (setSchedulePosition(-1) called to end program) 26297 // this allows onSeeking handler to update schedule position 26298 this.endedItem = null; 26299 this.playingItem = currentItem; 26300 if (!currentItem.event.appendInPlace) { 26301 // Media must be re-attached to resume primary schedule if not sharing source 26302 this.attachPrimary(schedule.durations.primary, null); 26303 } 26304 } 26305 } 26306 get playbackDisabled() { 26307 return this.hls.config.enableInterstitialPlayback === false; 26308 } 26309 get primaryDetails() { 26310 var _this$mediaSelection; 26311 return (_this$mediaSelection = this.mediaSelection) == null ? void 0 : _this$mediaSelection.main.details; 26312 } 26313 get primaryLive() { 26314 var _this$primaryDetails; 26315 return !!((_this$primaryDetails = this.primaryDetails) != null && _this$primaryDetails.live); 26316 } 26317 resumePrimary(scheduledItem, index, fromItem) { 26318 var _this$detachedData4, _this$schedule4; 26319 this.playingItem = scheduledItem; 26320 this.playingAsset = this.endedAsset = null; 26321 this.waitingItem = this.endedItem = null; 26322 this.bufferedToItem(scheduledItem); 26323 this.log(`resuming ${segmentToString(scheduledItem)}`); 26324 if (!((_this$detachedData4 = this.detachedData) != null && _this$detachedData4.mediaSource)) { 26325 let timelinePos = this.timelinePos; 26326 if (timelinePos < scheduledItem.start || timelinePos >= scheduledItem.end) { 26327 timelinePos = this.getPrimaryResumption(scheduledItem, index); 26328 this.log(timelineMessage('resumePrimary', timelinePos)); 26329 this.timelinePos = timelinePos; 26330 } 26331 this.attachPrimary(timelinePos, scheduledItem); 26332 } 26333 if (!fromItem) { 26334 return; 26335 } 26336 const scheduleItems = (_this$schedule4 = this.schedule) == null ? void 0 : _this$schedule4.items; 26337 if (!scheduleItems) { 26338 return; 26339 } 26340 this.log(`INTERSTITIALS_PRIMARY_RESUMED ${segmentToString(scheduledItem)}`); 26341 this.hls.trigger(Events.INTERSTITIALS_PRIMARY_RESUMED, { 26342 schedule: scheduleItems.slice(0), 26343 scheduleIndex: index 26344 }); 26345 this.checkBuffer(); 26346 } 26347 getPrimaryResumption(scheduledItem, index) { 26348 const itemStart = scheduledItem.start; 26349 if (this.primaryLive) { 26350 const details = this.primaryDetails; 26351 if (index === 0) { 26352 return this.hls.startPosition; 26353 } else if (details && (itemStart < details.fragmentStart || itemStart > details.edge)) { 26354 return this.hls.liveSyncPosition || -1; 26355 } 26356 } 26357 return itemStart; 26358 } 26359 isAssetBuffered(asset) { 26360 const player = this.getAssetPlayer(asset.identifier); 26361 if (player != null && player.hls) { 26362 return player.hls.bufferedToEnd; 26363 } 26364 const bufferInfo = BufferHelper.bufferInfo(this.primaryMedia, this.timelinePos, 0); 26365 return bufferInfo.end + 1 >= asset.timelineStart + (asset.duration || 0); 26366 } 26367 attachPrimary(timelinePos, item, skipSeekToStartPosition) { 26368 if (item) { 26369 this.setBufferingItem(item); 26370 } else { 26371 this.bufferingItem = this.playingItem; 26372 } 26373 this.bufferingAsset = null; 26374 const media = this.primaryMedia; 26375 if (!media) { 26376 return; 26377 } 26378 const hls = this.hls; 26379 if (hls.media) { 26380 this.checkBuffer(); 26381 } else { 26382 this.transferMediaTo(hls, media); 26383 if (skipSeekToStartPosition) { 26384 this.startLoadingPrimaryAt(timelinePos, skipSeekToStartPosition); 26385 } 26386 } 26387 if (!skipSeekToStartPosition) { 26388 // Set primary position to resume time 26389 this.log(timelineMessage('attachPrimary', timelinePos)); 26390 this.timelinePos = timelinePos; 26391 this.startLoadingPrimaryAt(timelinePos, skipSeekToStartPosition); 26392 } 26393 } 26394 startLoadingPrimaryAt(timelinePos, skipSeekToStartPosition) { 26395 var _hls$mainForwardBuffe; 26396 const hls = this.hls; 26397 if (!hls.loadingEnabled || !hls.media || Math.abs((((_hls$mainForwardBuffe = hls.mainForwardBufferInfo) == null ? void 0 : _hls$mainForwardBuffe.start) || hls.media.currentTime) - timelinePos) > 0.5) { 26398 hls.startLoad(timelinePos, skipSeekToStartPosition); 26399 } else if (!hls.bufferingEnabled) { 26400 hls.resumeBuffering(); 26401 } 26402 } 26403 26404 // HLS.js event callbacks 26405 onManifestLoading() { 26406 var _this$schedule5; 26407 this.stopLoad(); 26408 (_this$schedule5 = this.schedule) == null || _this$schedule5.reset(); 26409 this.emptyPlayerQueue(); 26410 this.clearScheduleState(); 26411 this.shouldPlay = false; 26412 this.bufferedPos = this.timelinePos = -1; 26413 this.mediaSelection = this.altSelection = this.manager = this.requiredTracks = null; 26414 // BUFFER_CODECS listener added here for buffer-controller to handle it first where it adds tracks 26415 this.hls.off(Events.BUFFER_CODECS, this.onBufferCodecs, this); 26416 this.hls.on(Events.BUFFER_CODECS, this.onBufferCodecs, this); 26417 } 26418 onLevelUpdated(event, data) { 26419 if (data.level === -1 || !this.schedule) { 26420 // level was removed 26421 return; 26422 } 26423 const main = this.hls.levels[data.level]; 26424 if (!main.details) { 26425 return; 26426 } 26427 const currentSelection = _objectSpread2(_objectSpread2({}, this.mediaSelection || this.altSelection), {}, { 26428 main 26429 }); 26430 this.mediaSelection = currentSelection; 26431 this.schedule.parseInterstitialDateRanges(currentSelection, this.hls.config.interstitialAppendInPlace); 26432 if (!this.effectivePlayingItem && this.schedule.items) { 26433 this.checkStart(); 26434 } 26435 } 26436 onAudioTrackUpdated(event, data) { 26437 const audio = this.hls.audioTracks[data.id]; 26438 const previousSelection = this.mediaSelection; 26439 if (!previousSelection) { 26440 this.altSelection = _objectSpread2(_objectSpread2({}, this.altSelection), {}, { 26441 audio 26442 }); 26443 return; 26444 } 26445 const currentSelection = _objectSpread2(_objectSpread2({}, previousSelection), {}, { 26446 audio 26447 }); 26448 this.mediaSelection = currentSelection; 26449 } 26450 onSubtitleTrackUpdated(event, data) { 26451 const subtitles = this.hls.subtitleTracks[data.id]; 26452 const previousSelection = this.mediaSelection; 26453 if (!previousSelection) { 26454 this.altSelection = _objectSpread2(_objectSpread2({}, this.altSelection), {}, { 26455 subtitles 26456 }); 26457 return; 26458 } 26459 const currentSelection = _objectSpread2(_objectSpread2({}, previousSelection), {}, { 26460 subtitles 26461 }); 26462 this.mediaSelection = currentSelection; 26463 } 26464 onAudioTrackSwitching(event, data) { 26465 const audioOption = getBasicSelectionOption(data); 26466 this.playerQueue.forEach(({ 26467 hls 26468 }) => hls && (hls.setAudioOption(data) || hls.setAudioOption(audioOption))); 26469 } 26470 onSubtitleTrackSwitch(event, data) { 26471 const subtitleOption = getBasicSelectionOption(data); 26472 this.playerQueue.forEach(({ 26473 hls 26474 }) => hls && (hls.setSubtitleOption(data) || data.id !== -1 && hls.setSubtitleOption(subtitleOption))); 26475 } 26476 onBufferCodecs(event, data) { 26477 const requiredTracks = data.tracks; 26478 if (requiredTracks) { 26479 this.requiredTracks = requiredTracks; 26480 } 26481 } 26482 onBufferAppended(event, data) { 26483 this.checkBuffer(); 26484 } 26485 onBufferFlushed(event, data) { 26486 const playingItem = this.playingItem; 26487 if (playingItem && !this.itemsMatch(playingItem, this.bufferingItem) && !this.isInterstitial(playingItem)) { 26488 const timelinePos = this.timelinePos; 26489 this.bufferedPos = timelinePos; 26490 this.checkBuffer(); 26491 } 26492 } 26493 onBufferedToEnd(event) { 26494 if (!this.schedule) { 26495 return; 26496 } 26497 // Buffered to post-roll 26498 const interstitialEvents = this.schedule.events; 26499 if (this.bufferedPos < Number.MAX_VALUE && interstitialEvents) { 26500 for (let i = 0; i < interstitialEvents.length; i++) { 26501 const interstitial = interstitialEvents[i]; 26502 if (interstitial.cue.post) { 26503 var _this$schedule$items2; 26504 const scheduleIndex = this.schedule.findEventIndex(interstitial.identifier); 26505 const item = (_this$schedule$items2 = this.schedule.items) == null ? void 0 : _this$schedule$items2[scheduleIndex]; 26506 if (this.isInterstitial(item) && this.eventItemsMatch(item, this.bufferingItem)) { 26507 this.bufferedToItem(item, 0); 26508 } 26509 break; 26510 } 26511 } 26512 this.bufferedPos = Number.MAX_VALUE; 26513 } 26514 } 26515 onMediaEnded(event) { 26516 const playingItem = this.playingItem; 26517 if (!this.playingLastItem && playingItem) { 26518 const playingIndex = this.findItemIndex(playingItem); 26519 this.setSchedulePosition(playingIndex + 1); 26520 } else { 26521 this.shouldPlay = false; 26522 } 26523 } 26524 updateItem(previousItem, time) { 26525 var _this$schedule6; 26526 // find item in this.schedule.items; 26527 const items = (_this$schedule6 = this.schedule) == null ? void 0 : _this$schedule6.items; 26528 if (previousItem && items) { 26529 const index = this.findItemIndex(previousItem, time); 26530 return items[index] || null; 26531 } 26532 return null; 26533 } 26534 trimInPlace(updatedItem, itemBeforeUpdate) { 26535 if (this.isInterstitial(updatedItem) && updatedItem.event.appendInPlace && itemBeforeUpdate.end - updatedItem.end > 0.25) { 26536 updatedItem.event.assetList.forEach((asset, index) => { 26537 if (updatedItem.event.isAssetPastPlayoutLimit(index)) { 26538 this.clearAssetPlayer(asset.identifier, null); 26539 } 26540 }); 26541 const flushStart = updatedItem.end + 0.25; 26542 const bufferInfo = BufferHelper.bufferInfo(this.primaryMedia, flushStart, 0); 26543 if (bufferInfo.end > flushStart || (bufferInfo.nextStart || 0) > flushStart) { 26544 this.log(`trim buffered interstitial ${segmentToString(updatedItem)} (was ${segmentToString(itemBeforeUpdate)})`); 26545 const skipSeekToStartPosition = true; 26546 this.attachPrimary(flushStart, null, skipSeekToStartPosition); 26547 this.flushFrontBuffer(flushStart); 26548 } 26549 } 26550 } 26551 itemsMatch(a, b) { 26552 return !!b && (a === b || a.event && b.event && this.eventItemsMatch(a, b) || !a.event && !b.event && this.findItemIndex(a) === this.findItemIndex(b)); 26553 } 26554 eventItemsMatch(a, b) { 26555 var _b$event; 26556 return !!b && (a === b || a.event.identifier === ((_b$event = b.event) == null ? void 0 : _b$event.identifier)); 26557 } 26558 findItemIndex(item, time) { 26559 return item && this.schedule ? this.schedule.findItemIndex(item, time) : -1; 26560 } 26561 updateSchedule(forceUpdate = false) { 26562 var _this$schedule7; 26563 const mediaSelection = this.mediaSelection; 26564 if (!mediaSelection) { 26565 return; 26566 } 26567 (_this$schedule7 = this.schedule) == null || _this$schedule7.updateSchedule(mediaSelection, [], forceUpdate); 26568 } 26569 26570 // Schedule buffer control 26571 checkBuffer(starved) { 26572 var _this$schedule8; 26573 const items = (_this$schedule8 = this.schedule) == null ? void 0 : _this$schedule8.items; 26574 if (!items) { 26575 return; 26576 } 26577 // Find when combined forward buffer change reaches next schedule segment 26578 const bufferInfo = BufferHelper.bufferInfo(this.primaryMedia, this.timelinePos, 0); 26579 if (starved) { 26580 this.bufferedPos = this.timelinePos; 26581 } 26582 starved || (starved = bufferInfo.len < 1); 26583 this.updateBufferedPos(bufferInfo.end, items, starved); 26584 } 26585 updateBufferedPos(bufferEnd, items, bufferIsEmpty) { 26586 const schedule = this.schedule; 26587 const bufferingItem = this.bufferingItem; 26588 if (this.bufferedPos > bufferEnd || !schedule) { 26589 return; 26590 } 26591 if (items.length === 1 && this.itemsMatch(items[0], bufferingItem)) { 26592 this.bufferedPos = bufferEnd; 26593 return; 26594 } 26595 const playingItem = this.playingItem; 26596 const playingIndex = this.findItemIndex(playingItem); 26597 let bufferEndIndex = schedule.findItemIndexAtTime(bufferEnd); 26598 if (this.bufferedPos < bufferEnd) { 26599 var _nextItemToBuffer$eve; 26600 const bufferingIndex = this.findItemIndex(bufferingItem); 26601 const nextToBufferIndex = Math.min(bufferingIndex + 1, items.length - 1); 26602 const nextItemToBuffer = items[nextToBufferIndex]; 26603 if (bufferEndIndex === -1 && bufferingItem && bufferEnd >= bufferingItem.end || (_nextItemToBuffer$eve = nextItemToBuffer.event) != null && _nextItemToBuffer$eve.appendInPlace && bufferEnd + 0.01 >= nextItemToBuffer.start) { 26604 bufferEndIndex = nextToBufferIndex; 26605 } 26606 if (this.isInterstitial(bufferingItem)) { 26607 const interstitial = bufferingItem.event; 26608 if (nextToBufferIndex - playingIndex > 1 && interstitial.appendInPlace === false) { 26609 // do not advance buffering item past Interstitial that requires source reset 26610 return; 26611 } 26612 if (interstitial.assetList.length === 0 && interstitial.assetListLoader) { 26613 // do not advance buffering item past Interstitial loading asset-list 26614 return; 26615 } 26616 } 26617 this.bufferedPos = bufferEnd; 26618 if (bufferEndIndex > bufferingIndex && bufferEndIndex > playingIndex) { 26619 this.bufferedToItem(nextItemToBuffer); 26620 } else { 26621 // allow more time than distance from edge for assets to load 26622 const details = this.primaryDetails; 26623 if (this.primaryLive && details && bufferEnd > details.edge - details.targetduration && nextItemToBuffer.start < details.edge + this.hls.config.interstitialLiveLookAhead && this.isInterstitial(nextItemToBuffer)) { 26624 this.preloadAssets(nextItemToBuffer.event, 0); 26625 } 26626 } 26627 } else if (bufferIsEmpty && playingItem && !this.itemsMatch(playingItem, bufferingItem)) { 26628 if (bufferEndIndex === playingIndex) { 26629 this.bufferedToItem(playingItem); 26630 } else if (bufferEndIndex === playingIndex + 1) { 26631 this.bufferedToItem(items[bufferEndIndex]); 26632 } 26633 } 26634 } 26635 assetsBuffered(item, media) { 26636 const assetList = item.event.assetList; 26637 if (assetList.length === 0) { 26638 return false; 26639 } 26640 return !item.event.assetList.some(asset => { 26641 const player = this.getAssetPlayer(asset.identifier); 26642 return !(player != null && player.bufferedInPlaceToEnd(media)); 26643 }); 26644 } 26645 setBufferingItem(item) { 26646 const bufferingLast = this.bufferingItem; 26647 const schedule = this.schedule; 26648 if (!this.itemsMatch(item, bufferingLast) && schedule) { 26649 const { 26650 items, 26651 events 26652 } = schedule; 26653 if (!items || !events) { 26654 return bufferingLast; 26655 } 26656 const isInterstitial = this.isInterstitial(item); 26657 const bufferingPlayer = this.getBufferingPlayer(); 26658 this.bufferingItem = item; 26659 this.bufferedPos = Math.max(item.start, Math.min(item.end, this.timelinePos)); 26660 const timeRemaining = bufferingPlayer ? bufferingPlayer.remaining : bufferingLast ? bufferingLast.end - this.timelinePos : 0; 26661 this.log(`INTERSTITIALS_BUFFERED_TO_BOUNDARY ${segmentToString(item)}` + (bufferingLast ? ` (${timeRemaining.toFixed(2)} remaining)` : '')); 26662 if (!this.playbackDisabled) { 26663 if (isInterstitial) { 26664 const bufferIndex = schedule.findAssetIndex(item.event, this.bufferedPos); 26665 // primary fragment loading will exit early in base-stream-controller while `bufferingItem` is set to an Interstitial block 26666 item.event.assetList.forEach((asset, i) => { 26667 const player = this.getAssetPlayer(asset.identifier); 26668 if (player) { 26669 if (i === bufferIndex) { 26670 player.loadSource(); 26671 } 26672 player.resumeBuffering(); 26673 } 26674 }); 26675 } else { 26676 this.hls.resumeBuffering(); 26677 this.playerQueue.forEach(player => player.pauseBuffering()); 26678 } 26679 } 26680 this.hls.trigger(Events.INTERSTITIALS_BUFFERED_TO_BOUNDARY, { 26681 events: events.slice(0), 26682 schedule: items.slice(0), 26683 bufferingIndex: this.findItemIndex(item), 26684 playingIndex: this.findItemIndex(this.playingItem) 26685 }); 26686 } else if (this.bufferingItem !== item) { 26687 this.bufferingItem = item; 26688 } 26689 return bufferingLast; 26690 } 26691 bufferedToItem(item, assetListIndex = 0) { 26692 const bufferingLast = this.setBufferingItem(item); 26693 if (this.playbackDisabled) { 26694 return; 26695 } 26696 if (this.isInterstitial(item)) { 26697 // Ensure asset list is loaded 26698 this.bufferedToEvent(item, assetListIndex); 26699 } else if (bufferingLast !== null) { 26700 // If primary player is detached, it is also stopped, restart loading at primary position 26701 this.bufferingAsset = null; 26702 const detachedData = this.detachedData; 26703 if (detachedData) { 26704 if (detachedData.mediaSource) { 26705 const skipSeekToStartPosition = true; 26706 this.attachPrimary(item.start, item, skipSeekToStartPosition); 26707 } else { 26708 this.preloadPrimary(item); 26709 } 26710 } else { 26711 // If not detached seek to resumption point 26712 this.preloadPrimary(item); 26713 } 26714 } 26715 } 26716 preloadPrimary(item) { 26717 const index = this.findItemIndex(item); 26718 const timelinePos = this.getPrimaryResumption(item, index); 26719 this.startLoadingPrimaryAt(timelinePos); 26720 } 26721 bufferedToEvent(item, assetListIndex) { 26722 const interstitial = item.event; 26723 const neverLoaded = interstitial.assetList.length === 0 && !interstitial.assetListLoader; 26724 const playOnce = interstitial.cue.once; 26725 if (neverLoaded || !playOnce) { 26726 // Buffered to Interstitial boundary 26727 const player = this.preloadAssets(interstitial, assetListIndex); 26728 if (player != null && player.interstitial.appendInPlace) { 26729 const media = this.primaryMedia; 26730 if (media) { 26731 this.bufferAssetPlayer(player, media); 26732 } 26733 } 26734 } 26735 } 26736 preloadAssets(interstitial, assetListIndex) { 26737 const uri = interstitial.assetUrl; 26738 const assetListLength = interstitial.assetList.length; 26739 const neverLoaded = assetListLength === 0 && !interstitial.assetListLoader; 26740 const playOnce = interstitial.cue.once; 26741 if (neverLoaded) { 26742 const timelineStart = interstitial.timelineStart; 26743 if (interstitial.appendInPlace) { 26744 var _playingItem$nextEven; 26745 const playingItem = this.playingItem; 26746 if (!this.isInterstitial(playingItem) && (playingItem == null || (_playingItem$nextEven = playingItem.nextEvent) == null ? void 0 : _playingItem$nextEven.identifier) === interstitial.identifier) { 26747 this.flushFrontBuffer(timelineStart + 0.25); 26748 } 26749 } 26750 let hlsStartOffset; 26751 let liveStartPosition = 0; 26752 if (!this.playingItem && this.primaryLive) { 26753 liveStartPosition = this.hls.startPosition; 26754 if (liveStartPosition === -1) { 26755 liveStartPosition = this.hls.liveSyncPosition || 0; 26756 } 26757 } 26758 if (liveStartPosition && !(interstitial.cue.pre || interstitial.cue.post)) { 26759 const startOffset = liveStartPosition - timelineStart; 26760 if (startOffset > 0) { 26761 hlsStartOffset = Math.round(startOffset * 1000) / 1000; 26762 } 26763 } 26764 this.log(`Load interstitial asset ${assetListIndex + 1}/${uri ? 1 : assetListLength} ${interstitial}${hlsStartOffset ? ` live-start: ${liveStartPosition} start-offset: ${hlsStartOffset}` : ''}`); 26765 if (uri) { 26766 return this.createAsset(interstitial, 0, 0, timelineStart, interstitial.duration, uri); 26767 } 26768 const assetListLoader = this.assetListLoader.loadAssetList(interstitial, hlsStartOffset); 26769 if (assetListLoader) { 26770 interstitial.assetListLoader = assetListLoader; 26771 } 26772 } else if (!playOnce && assetListLength) { 26773 // Re-buffered to Interstitial boundary, re-create asset player(s) 26774 for (let i = assetListIndex; i < assetListLength; i++) { 26775 const _asset = interstitial.assetList[i]; 26776 const playerIndex = this.getAssetPlayerQueueIndex(_asset.identifier); 26777 if ((playerIndex === -1 || this.playerQueue[playerIndex].destroyed) && !_asset.error) { 26778 this.createAssetPlayer(interstitial, _asset, i); 26779 } 26780 } 26781 const asset = interstitial.assetList[assetListIndex]; 26782 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 26783 if (asset) { 26784 const player = this.getAssetPlayer(asset.identifier); 26785 if (player) { 26786 player.loadSource(); 26787 } 26788 return player; 26789 } 26790 } 26791 return null; 26792 } 26793 flushFrontBuffer(startOffset) { 26794 // Force queued flushing of all buffers 26795 const requiredTracks = this.requiredTracks; 26796 if (!requiredTracks) { 26797 return; 26798 } 26799 this.log(`Removing front buffer starting at ${startOffset}`); 26800 const sourceBufferNames = Object.keys(requiredTracks); 26801 sourceBufferNames.forEach(type => { 26802 this.hls.trigger(Events.BUFFER_FLUSHING, { 26803 startOffset, 26804 endOffset: Infinity, 26805 type 26806 }); 26807 }); 26808 } 26809 26810 // Interstitial Asset Player control 26811 getAssetPlayerQueueIndex(assetId) { 26812 const playerQueue = this.playerQueue; 26813 for (let i = 0; i < playerQueue.length; i++) { 26814 if (assetId === playerQueue[i].assetId) { 26815 return i; 26816 } 26817 } 26818 return -1; 26819 } 26820 getAssetPlayer(assetId) { 26821 const index = this.getAssetPlayerQueueIndex(assetId); 26822 return this.playerQueue[index] || null; 26823 } 26824 getBufferingPlayer() { 26825 const { 26826 playerQueue, 26827 primaryMedia 26828 } = this; 26829 if (primaryMedia) { 26830 for (let i = 0; i < playerQueue.length; i++) { 26831 if (playerQueue[i].media === primaryMedia) { 26832 return playerQueue[i]; 26833 } 26834 } 26835 } 26836 return null; 26837 } 26838 createAsset(interstitial, assetListIndex, startOffset, timelineStart, duration, uri) { 26839 const assetItem = { 26840 parentIdentifier: interstitial.identifier, 26841 identifier: generateAssetIdentifier(interstitial, uri, assetListIndex), 26842 duration, 26843 startOffset, 26844 timelineStart, 26845 uri 26846 }; 26847 return this.createAssetPlayer(interstitial, assetItem, assetListIndex); 26848 } 26849 createAssetPlayer(interstitial, assetItem, assetListIndex) { 26850 const primary = this.hls; 26851 const userConfig = primary.userConfig; 26852 let videoPreference = userConfig.videoPreference; 26853 const currentLevel = primary.loadLevelObj || primary.levels[primary.currentLevel]; 26854 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 26855 if (videoPreference || currentLevel) { 26856 videoPreference = _extends({}, videoPreference); 26857 if (currentLevel.videoCodec) { 26858 videoPreference.videoCodec = currentLevel.videoCodec; 26859 } 26860 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 26861 if (currentLevel.videoRange) { 26862 videoPreference.allowedVideoRanges = [currentLevel.videoRange]; 26863 } 26864 } 26865 const selectedAudio = primary.audioTracks[primary.audioTrack]; 26866 const selectedSubtitle = primary.subtitleTracks[primary.subtitleTrack]; 26867 let startPosition = 0; 26868 if (this.primaryLive || interstitial.appendInPlace) { 26869 const timePastStart = this.timelinePos - assetItem.timelineStart; 26870 if (timePastStart > 1) { 26871 const duration = assetItem.duration; 26872 if (duration && timePastStart < duration) { 26873 startPosition = timePastStart; 26874 } 26875 } 26876 } 26877 const assetId = assetItem.identifier; 26878 const playerConfig = _objectSpread2(_objectSpread2({}, userConfig), {}, { 26879 maxMaxBufferLength: Math.min(180, primary.config.maxMaxBufferLength), 26880 autoStartLoad: true, 26881 startFragPrefetch: true, 26882 primarySessionId: primary.sessionId, 26883 assetPlayerId: assetId, 26884 abrEwmaDefaultEstimate: primary.bandwidthEstimate, 26885 interstitialsController: undefined, 26886 startPosition, 26887 liveDurationInfinity: false, 26888 testBandwidth: false, 26889 videoPreference, 26890 audioPreference: selectedAudio || userConfig.audioPreference, 26891 subtitlePreference: selectedSubtitle || userConfig.subtitlePreference 26892 }); 26893 // TODO: limit maxMaxBufferLength in asset players to prevent QEE 26894 if (interstitial.appendInPlace) { 26895 interstitial.appendInPlaceStarted = true; 26896 if (assetItem.timelineStart) { 26897 playerConfig.timelineOffset = assetItem.timelineStart; 26898 } 26899 } 26900 const cmcd = playerConfig.cmcd; 26901 if (cmcd != null && cmcd.sessionId && cmcd.contentId) { 26902 playerConfig.cmcd = _extends({}, cmcd, { 26903 contentId: hash(assetItem.uri) 26904 }); 26905 } 26906 if (this.getAssetPlayer(assetId)) { 26907 this.warn(`Duplicate date range identifier ${interstitial} and asset ${assetId}`); 26908 } 26909 const player = new HlsAssetPlayer(this.HlsPlayerClass, playerConfig, interstitial, assetItem); 26910 this.playerQueue.push(player); 26911 interstitial.assetList[assetListIndex] = assetItem; 26912 // Listen for LevelDetails and PTS change to update duration 26913 let initialDuration = true; 26914 const updateAssetPlayerDetails = details => { 26915 if (details.live) { 26916 var _this$schedule9; 26917 const error = new Error(`Interstitials MUST be VOD assets ${interstitial}`); 26918 const errorData = { 26919 fatal: true, 26920 type: ErrorTypes.OTHER_ERROR, 26921 details: ErrorDetails.INTERSTITIAL_ASSET_ITEM_ERROR, 26922 error 26923 }; 26924 const scheduleIndex = ((_this$schedule9 = this.schedule) == null ? void 0 : _this$schedule9.findEventIndex(interstitial.identifier)) || -1; 26925 this.handleAssetItemError(errorData, interstitial, scheduleIndex, assetListIndex, error.message); 26926 return; 26927 } 26928 // Get time at end of last fragment 26929 const duration = details.edge - details.fragmentStart; 26930 const currentAssetDuration = assetItem.duration; 26931 if (initialDuration || currentAssetDuration === null || duration > currentAssetDuration) { 26932 initialDuration = false; 26933 this.log(`Interstitial asset "${assetId}" duration change ${currentAssetDuration} > ${duration}`); 26934 assetItem.duration = duration; 26935 // Update schedule with new event and asset duration 26936 this.updateSchedule(); 26937 } 26938 }; 26939 player.on(Events.LEVEL_UPDATED, (event, { 26940 details 26941 }) => updateAssetPlayerDetails(details)); 26942 player.on(Events.LEVEL_PTS_UPDATED, (event, { 26943 details 26944 }) => updateAssetPlayerDetails(details)); 26945 player.on(Events.EVENT_CUE_ENTER, () => this.onInterstitialCueEnter()); 26946 const onBufferCodecs = (event, data) => { 26947 const inQueuPlayer = this.getAssetPlayer(assetId); 26948 if (inQueuPlayer && data.tracks) { 26949 inQueuPlayer.off(Events.BUFFER_CODECS, onBufferCodecs); 26950 inQueuPlayer.tracks = data.tracks; 26951 const media = this.primaryMedia; 26952 if (this.bufferingAsset === inQueuPlayer.assetItem && media && !inQueuPlayer.media) { 26953 this.bufferAssetPlayer(inQueuPlayer, media); 26954 } 26955 } 26956 }; 26957 player.on(Events.BUFFER_CODECS, onBufferCodecs); 26958 const bufferedToEnd = () => { 26959 var _this$schedule$items3; 26960 const inQueuPlayer = this.getAssetPlayer(assetId); 26961 this.log(`buffered to end of asset ${inQueuPlayer}`); 26962 if (!inQueuPlayer || !this.schedule) { 26963 return; 26964 } 26965 // Preload at end of asset 26966 const scheduleIndex = this.schedule.findEventIndex(interstitial.identifier); 26967 const item = (_this$schedule$items3 = this.schedule.items) == null ? void 0 : _this$schedule$items3[scheduleIndex]; 26968 if (this.isInterstitial(item)) { 26969 this.advanceAssetBuffering(item, assetItem); 26970 } 26971 }; 26972 player.on(Events.BUFFERED_TO_END, bufferedToEnd); 26973 const endedWithAssetIndex = assetIndex => { 26974 return () => { 26975 const inQueuPlayer = this.getAssetPlayer(assetId); 26976 if (!inQueuPlayer || !this.schedule) { 26977 return; 26978 } 26979 this.shouldPlay = true; 26980 const scheduleIndex = this.schedule.findEventIndex(interstitial.identifier); 26981 this.advanceAfterAssetEnded(interstitial, scheduleIndex, assetIndex); 26982 }; 26983 }; 26984 player.once(Events.MEDIA_ENDED, endedWithAssetIndex(assetListIndex)); 26985 player.once(Events.PLAYOUT_LIMIT_REACHED, endedWithAssetIndex(Infinity)); 26986 player.on(Events.ERROR, (event, data) => { 26987 if (!this.schedule) { 26988 return; 26989 } 26990 const inQueuPlayer = this.getAssetPlayer(assetId); 26991 if (data.details === ErrorDetails.BUFFER_STALLED_ERROR) { 26992 if (inQueuPlayer != null && inQueuPlayer.appendInPlace) { 26993 this.handleInPlaceStall(interstitial); 26994 return; 26995 } 26996 this.onTimeupdate(); 26997 this.checkBuffer(true); 26998 return; 26999 } 27000 this.handleAssetItemError(data, interstitial, this.schedule.findEventIndex(interstitial.identifier), assetListIndex, `Asset player error ${data.error} ${interstitial}`); 27001 }); 27002 player.on(Events.DESTROYING, () => { 27003 const inQueuPlayer = this.getAssetPlayer(assetId); 27004 if (!inQueuPlayer || !this.schedule) { 27005 return; 27006 } 27007 const error = new Error(`Asset player destroyed unexpectedly ${assetId}`); 27008 const errorData = { 27009 fatal: true, 27010 type: ErrorTypes.OTHER_ERROR, 27011 details: ErrorDetails.INTERSTITIAL_ASSET_ITEM_ERROR, 27012 error 27013 }; 27014 this.handleAssetItemError(errorData, interstitial, this.schedule.findEventIndex(interstitial.identifier), assetListIndex, error.message); 27015 }); 27016 this.log(`INTERSTITIAL_ASSET_PLAYER_CREATED ${eventAssetToString(assetItem)}`); 27017 this.hls.trigger(Events.INTERSTITIAL_ASSET_PLAYER_CREATED, { 27018 asset: assetItem, 27019 assetListIndex, 27020 event: interstitial, 27021 player 27022 }); 27023 return player; 27024 } 27025 clearInterstitial(interstitial, toSegment) { 27026 this.clearAssetPlayers(interstitial, toSegment); 27027 // Remove asset list and resolved duration 27028 interstitial.reset(); 27029 } 27030 clearAssetPlayers(interstitial, toSegment) { 27031 interstitial.assetList.forEach(asset => { 27032 this.clearAssetPlayer(asset.identifier, toSegment); 27033 }); 27034 } 27035 resetAssetPlayer(assetId) { 27036 // Reset asset player so that it's timeline can be adjusted without reloading the MVP 27037 const playerIndex = this.getAssetPlayerQueueIndex(assetId); 27038 if (playerIndex !== -1) { 27039 this.log(`reset asset player "${assetId}" after error`); 27040 const player = this.playerQueue[playerIndex]; 27041 this.transferMediaFromPlayer(player, null); 27042 player.resetDetails(); 27043 } 27044 } 27045 clearAssetPlayer(assetId, toSegment) { 27046 const playerIndex = this.getAssetPlayerQueueIndex(assetId); 27047 if (playerIndex !== -1) { 27048 const player = this.playerQueue[playerIndex]; 27049 this.log(`clear ${player} toSegment: ${toSegment ? segmentToString(toSegment) : toSegment}`); 27050 this.transferMediaFromPlayer(player, toSegment); 27051 this.playerQueue.splice(playerIndex, 1); 27052 player.destroy(); 27053 } 27054 } 27055 emptyPlayerQueue() { 27056 let player; 27057 while (player = this.playerQueue.pop()) { 27058 player.destroy(); 27059 } 27060 this.playerQueue = []; 27061 } 27062 startAssetPlayer(player, assetListIndex, scheduleItems, scheduleIndex, media) { 27063 const { 27064 interstitial, 27065 assetItem, 27066 assetId 27067 } = player; 27068 const assetListLength = interstitial.assetList.length; 27069 const playingAsset = this.playingAsset; 27070 this.endedAsset = null; 27071 this.playingAsset = assetItem; 27072 if (!playingAsset || playingAsset.identifier !== assetId) { 27073 if (playingAsset) { 27074 // Exiting another Interstitial asset 27075 this.clearAssetPlayer(playingAsset.identifier, scheduleItems[scheduleIndex]); 27076 delete playingAsset.error; 27077 } 27078 this.log(`INTERSTITIAL_ASSET_STARTED ${assetListIndex + 1}/${assetListLength} ${eventAssetToString(assetItem)}`); 27079 this.hls.trigger(Events.INTERSTITIAL_ASSET_STARTED, { 27080 asset: assetItem, 27081 assetListIndex, 27082 event: interstitial, 27083 schedule: scheduleItems.slice(0), 27084 scheduleIndex, 27085 player 27086 }); 27087 } 27088 27089 // detach media and attach to interstitial player if it does not have another element attached 27090 this.bufferAssetPlayer(player, media); 27091 } 27092 bufferAssetPlayer(player, media) { 27093 var _this$schedule$items4, _this$detachedData5; 27094 if (!this.schedule) { 27095 return; 27096 } 27097 const { 27098 interstitial, 27099 assetItem 27100 } = player; 27101 const scheduleIndex = this.schedule.findEventIndex(interstitial.identifier); 27102 const item = (_this$schedule$items4 = this.schedule.items) == null ? void 0 : _this$schedule$items4[scheduleIndex]; 27103 if (!item) { 27104 return; 27105 } 27106 player.loadSource(); 27107 this.setBufferingItem(item); 27108 this.bufferingAsset = assetItem; 27109 const bufferingPlayer = this.getBufferingPlayer(); 27110 if (bufferingPlayer === player) { 27111 return; 27112 } 27113 const appendInPlaceNext = interstitial.appendInPlace; 27114 if (appendInPlaceNext && (bufferingPlayer == null ? void 0 : bufferingPlayer.interstitial.appendInPlace) === false) { 27115 // Media is detached and not available to append in place 27116 return; 27117 } 27118 const activeTracks = (bufferingPlayer == null ? void 0 : bufferingPlayer.tracks) || ((_this$detachedData5 = this.detachedData) == null ? void 0 : _this$detachedData5.tracks) || this.requiredTracks; 27119 if (appendInPlaceNext && assetItem !== this.playingAsset) { 27120 // Do not buffer another item if tracks are unknown or incompatible 27121 if (!player.tracks) { 27122 this.log(`Waiting for track info before buffering ${player}`); 27123 return; 27124 } 27125 if (activeTracks && !isCompatibleTrackChange(activeTracks, player.tracks)) { 27126 const error = new Error(`Asset ${eventAssetToString(assetItem)} SourceBuffer tracks ('${Object.keys(player.tracks)}') are not compatible with primary content tracks ('${Object.keys(activeTracks)}')`); 27127 const errorData = { 27128 fatal: true, 27129 type: ErrorTypes.OTHER_ERROR, 27130 details: ErrorDetails.INTERSTITIAL_ASSET_ITEM_ERROR, 27131 error 27132 }; 27133 const assetListIndex = interstitial.findAssetIndex(assetItem); 27134 this.handleAssetItemError(errorData, interstitial, scheduleIndex, assetListIndex, error.message); 27135 return; 27136 } 27137 } 27138 this.transferMediaTo(player, media); 27139 } 27140 handleInPlaceStall(interstitial) { 27141 const schedule = this.schedule; 27142 const media = this.primaryMedia; 27143 if (!schedule || !media) { 27144 return; 27145 } 27146 const currentTime = media.currentTime; 27147 const foundAssetIndex = schedule.findAssetIndex(interstitial, currentTime); 27148 const stallingAsset = interstitial.assetList[foundAssetIndex]; 27149 if (stallingAsset) { 27150 const player = this.getAssetPlayer(stallingAsset.identifier); 27151 if (player) { 27152 const assetCurrentTime = player.currentTime || currentTime - stallingAsset.timelineStart; 27153 const distanceFromEnd = player.duration - assetCurrentTime; 27154 this.warn(`Stalled at ${assetCurrentTime} of ${assetCurrentTime + distanceFromEnd} in ${player} ${interstitial} (media.currentTime: ${currentTime})`); 27155 if (assetCurrentTime && (distanceFromEnd / media.playbackRate < 0.5 || player.bufferedInPlaceToEnd(media)) && player.hls) { 27156 const scheduleIndex = schedule.findEventIndex(interstitial.identifier); 27157 this.advanceAfterAssetEnded(interstitial, scheduleIndex, foundAssetIndex); 27158 } 27159 } 27160 } 27161 } 27162 advanceInPlace(time) { 27163 const media = this.primaryMedia; 27164 if (media && media.currentTime < time) { 27165 media.currentTime = time; 27166 } 27167 } 27168 handleAssetItemError(data, interstitial, scheduleIndex, assetListIndex, errorMessage) { 27169 if (data.details === ErrorDetails.BUFFER_STALLED_ERROR) { 27170 return; 27171 } 27172 const assetItem = interstitial.assetList[assetListIndex] || null; 27173 this.warn(`INTERSTITIAL_ASSET_ERROR ${assetItem ? eventAssetToString(assetItem) : assetItem} ${data.error}`); 27174 if (!this.schedule) { 27175 return; 27176 } 27177 const assetId = (assetItem == null ? void 0 : assetItem.identifier) || ''; 27178 const playerIndex = this.getAssetPlayerQueueIndex(assetId); 27179 const player = this.playerQueue[playerIndex] || null; 27180 const items = this.schedule.items; 27181 const interstitialAssetError = _extends({}, data, { 27182 fatal: false, 27183 errorAction: createDoNothingErrorAction(true), 27184 asset: assetItem, 27185 assetListIndex, 27186 event: interstitial, 27187 schedule: items, 27188 scheduleIndex, 27189 player 27190 }); 27191 this.hls.trigger(Events.INTERSTITIAL_ASSET_ERROR, interstitialAssetError); 27192 if (!data.fatal) { 27193 return; 27194 } 27195 const playingAsset = this.playingAsset; 27196 const bufferingAsset = this.bufferingAsset; 27197 const error = new Error(errorMessage); 27198 if (assetItem) { 27199 this.clearAssetPlayer(assetId, null); 27200 assetItem.error = error; 27201 } 27202 27203 // If all assets in interstitial fail, mark the interstitial with an error 27204 if (!interstitial.assetList.some(asset => !asset.error)) { 27205 interstitial.error = error; 27206 } else { 27207 // Reset level details and reload/parse media playlists to align with updated schedule 27208 for (let i = assetListIndex; i < interstitial.assetList.length; i++) { 27209 this.resetAssetPlayer(interstitial.assetList[i].identifier); 27210 } 27211 } 27212 this.updateSchedule(true); 27213 if (interstitial.error) { 27214 this.primaryFallback(interstitial); 27215 } else if (playingAsset && playingAsset.identifier === assetId) { 27216 this.advanceAfterAssetEnded(interstitial, scheduleIndex, assetListIndex); 27217 } else if (bufferingAsset && bufferingAsset.identifier === assetId && this.isInterstitial(this.bufferingItem)) { 27218 this.advanceAssetBuffering(this.bufferingItem, bufferingAsset); 27219 } 27220 } 27221 primaryFallback(interstitial) { 27222 // Fallback to Primary by on current or future events by updating schedule to skip errored interstitials/assets 27223 const flushStart = interstitial.timelineStart; 27224 const playingItem = this.effectivePlayingItem; 27225 let timelinePos = this.timelinePos; 27226 // Update schedule now that interstitial/assets are flagged with `error` for fallback 27227 if (playingItem) { 27228 this.log(`Fallback to primary from event "${interstitial.identifier}" start: ${flushStart} pos: ${timelinePos} playing: ${segmentToString(playingItem)} error: ${interstitial.error}`); 27229 if (timelinePos === -1) { 27230 timelinePos = this.hls.startPosition; 27231 } 27232 const newPlayingItem = this.updateItem(playingItem, timelinePos); 27233 if (this.itemsMatch(playingItem, newPlayingItem)) { 27234 this.clearInterstitial(interstitial, null); 27235 } 27236 if (interstitial.appendInPlace) { 27237 this.attachPrimary(flushStart, null); 27238 this.flushFrontBuffer(flushStart); 27239 } 27240 } else if (timelinePos === -1) { 27241 this.checkStart(); 27242 return; 27243 } 27244 if (!this.schedule) { 27245 return; 27246 } 27247 const scheduleIndex = this.schedule.findItemIndexAtTime(timelinePos); 27248 this.setSchedulePosition(scheduleIndex); 27249 } 27250 27251 // Asset List loading 27252 onAssetListLoaded(event, data) { 27253 var _this$schedule0, _this$bufferingItem2; 27254 const interstitial = data.event; 27255 const interstitialId = interstitial.identifier; 27256 const assets = data.assetListResponse.ASSETS; 27257 if (!((_this$schedule0 = this.schedule) != null && _this$schedule0.hasEvent(interstitialId))) { 27258 // Interstitial with id was removed 27259 return; 27260 } 27261 const eventStart = interstitial.timelineStart; 27262 const previousDuration = interstitial.duration; 27263 let sumDuration = 0; 27264 assets.forEach((asset, assetListIndex) => { 27265 const duration = parseFloat(asset.DURATION); 27266 this.createAsset(interstitial, assetListIndex, sumDuration, eventStart + sumDuration, duration, asset.URI); 27267 sumDuration += duration; 27268 }); 27269 interstitial.duration = sumDuration; 27270 this.log(`Loaded asset-list with duration: ${sumDuration} (was: ${previousDuration}) ${interstitial}`); 27271 const waitingItem = this.waitingItem; 27272 const waitingForItem = (waitingItem == null ? void 0 : waitingItem.event.identifier) === interstitialId; 27273 27274 // Update schedule now that asset.DURATION(s) are parsed 27275 this.updateSchedule(); 27276 const bufferingEvent = (_this$bufferingItem2 = this.bufferingItem) == null ? void 0 : _this$bufferingItem2.event; 27277 27278 // If buffer reached Interstitial, start buffering first asset 27279 if (waitingForItem) { 27280 var _this$schedule$items5; 27281 // Advance schedule when waiting for asset list data to play 27282 const scheduleIndex = this.schedule.findEventIndex(interstitialId); 27283 const item = (_this$schedule$items5 = this.schedule.items) == null ? void 0 : _this$schedule$items5[scheduleIndex]; 27284 if (item) { 27285 if (!this.playingItem && this.timelinePos > item.end) { 27286 // Abandon if new duration is reduced enough to land playback in primary start 27287 const index = this.schedule.findItemIndexAtTime(this.timelinePos); 27288 if (index !== scheduleIndex) { 27289 interstitial.error = new Error(`Interstitial ${assets.length ? 'no longer within playback range' : 'asset-list is empty'} ${this.timelinePos} ${interstitial}`); 27290 this.log(interstitial.error.message); 27291 this.updateSchedule(true); 27292 this.primaryFallback(interstitial); 27293 return; 27294 } 27295 } 27296 this.setBufferingItem(item); 27297 } 27298 this.setSchedulePosition(scheduleIndex); 27299 } else if ((bufferingEvent == null ? void 0 : bufferingEvent.identifier) === interstitialId) { 27300 const assetItem = interstitial.assetList[0]; 27301 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 27302 if (assetItem) { 27303 const player = this.getAssetPlayer(assetItem.identifier); 27304 if (bufferingEvent.appendInPlace) { 27305 // If buffering (but not playback) has reached this item transfer media-source 27306 const media = this.primaryMedia; 27307 if (player && media) { 27308 this.bufferAssetPlayer(player, media); 27309 } 27310 } else if (player) { 27311 player.loadSource(); 27312 } 27313 } 27314 } 27315 } 27316 onError(event, data) { 27317 if (!this.schedule) { 27318 return; 27319 } 27320 switch (data.details) { 27321 case ErrorDetails.ASSET_LIST_PARSING_ERROR: 27322 case ErrorDetails.ASSET_LIST_LOAD_ERROR: 27323 case ErrorDetails.ASSET_LIST_LOAD_TIMEOUT: 27324 { 27325 const interstitial = data.interstitial; 27326 if (interstitial) { 27327 this.updateSchedule(true); 27328 this.primaryFallback(interstitial); 27329 } 27330 break; 27331 } 27332 case ErrorDetails.BUFFER_STALLED_ERROR: 27333 { 27334 const stallingItem = this.endedItem || this.waitingItem || this.playingItem; 27335 if (this.isInterstitial(stallingItem) && stallingItem.event.appendInPlace) { 27336 this.handleInPlaceStall(stallingItem.event); 27337 return; 27338 } 27339 this.log(`Primary player stall @${this.timelinePos} bufferedPos: ${this.bufferedPos}`); 27340 this.onTimeupdate(); 27341 this.checkBuffer(true); 27342 break; 27343 } 27344 } 27345 } 27346} 27347 27348const TICK_INTERVAL$2 = 500; // how often to tick in ms 27349 27350class SubtitleStreamController extends BaseStreamController { 27351 constructor(hls, fragmentTracker, keyLoader) { 27352 super(hls, fragmentTracker, keyLoader, 'subtitle-stream-controller', PlaylistLevelType.SUBTITLE); 27353 this.currentTrackId = -1; 27354 this.tracksBuffered = []; 27355 this.mainDetails = null; 27356 this.registerListeners(); 27357 } 27358 onHandlerDestroying() { 27359 this.unregisterListeners(); 27360 super.onHandlerDestroying(); 27361 this.mainDetails = null; 27362 } 27363 registerListeners() { 27364 super.registerListeners(); 27365 const { 27366 hls 27367 } = this; 27368 hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this); 27369 hls.on(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this); 27370 hls.on(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this); 27371 hls.on(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this); 27372 hls.on(Events.SUBTITLE_FRAG_PROCESSED, this.onSubtitleFragProcessed, this); 27373 hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 27374 } 27375 unregisterListeners() { 27376 super.unregisterListeners(); 27377 const { 27378 hls 27379 } = this; 27380 hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this); 27381 hls.off(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this); 27382 hls.off(Events.SUBTITLE_TRACK_SWITCH, this.onSubtitleTrackSwitch, this); 27383 hls.off(Events.SUBTITLE_TRACK_LOADED, this.onSubtitleTrackLoaded, this); 27384 hls.off(Events.SUBTITLE_FRAG_PROCESSED, this.onSubtitleFragProcessed, this); 27385 hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 27386 } 27387 startLoad(startPosition, skipSeekToStartPosition) { 27388 this.stopLoad(); 27389 this.state = State.IDLE; 27390 this.setInterval(TICK_INTERVAL$2); 27391 this.nextLoadPosition = this.lastCurrentTime = startPosition + this.timelineOffset; 27392 this.startPosition = skipSeekToStartPosition ? -1 : startPosition; 27393 this.tick(); 27394 } 27395 onManifestLoading() { 27396 super.onManifestLoading(); 27397 this.mainDetails = null; 27398 } 27399 onMediaDetaching(event, data) { 27400 this.tracksBuffered = []; 27401 super.onMediaDetaching(event, data); 27402 } 27403 onLevelLoaded(event, data) { 27404 this.mainDetails = data.details; 27405 } 27406 onSubtitleFragProcessed(event, data) { 27407 const { 27408 frag, 27409 success 27410 } = data; 27411 if (!this.fragContextChanged(frag)) { 27412 if (isMediaFragment(frag)) { 27413 this.fragPrevious = frag; 27414 } 27415 this.state = State.IDLE; 27416 } 27417 if (!success) { 27418 return; 27419 } 27420 const buffered = this.tracksBuffered[this.currentTrackId]; 27421 if (!buffered) { 27422 return; 27423 } 27424 27425 // Create/update a buffered array matching the interface used by BufferHelper.bufferedInfo 27426 // so we can re-use the logic used to detect how much has been buffered 27427 let timeRange; 27428 const fragStart = frag.start; 27429 for (let i = 0; i < buffered.length; i++) { 27430 if (fragStart >= buffered[i].start && fragStart <= buffered[i].end) { 27431 timeRange = buffered[i]; 27432 break; 27433 } 27434 } 27435 const fragEnd = frag.start + frag.duration; 27436 if (timeRange) { 27437 timeRange.end = fragEnd; 27438 } else { 27439 timeRange = { 27440 start: fragStart, 27441 end: fragEnd 27442 }; 27443 buffered.push(timeRange); 27444 } 27445 this.fragmentTracker.fragBuffered(frag); 27446 this.fragBufferedComplete(frag, null); 27447 if (this.media) { 27448 this.tick(); 27449 } 27450 } 27451 onBufferFlushing(event, data) { 27452 const { 27453 startOffset, 27454 endOffset 27455 } = data; 27456 if (startOffset === 0 && endOffset !== Number.POSITIVE_INFINITY) { 27457 const endOffsetSubtitles = endOffset - 1; 27458 if (endOffsetSubtitles <= 0) { 27459 return; 27460 } 27461 data.endOffsetSubtitles = Math.max(0, endOffsetSubtitles); 27462 this.tracksBuffered.forEach(buffered => { 27463 for (let i = 0; i < buffered.length;) { 27464 if (buffered[i].end <= endOffsetSubtitles) { 27465 buffered.shift(); 27466 continue; 27467 } else if (buffered[i].start < endOffsetSubtitles) { 27468 buffered[i].start = endOffsetSubtitles; 27469 } else { 27470 break; 27471 } 27472 i++; 27473 } 27474 }); 27475 this.fragmentTracker.removeFragmentsInRange(startOffset, endOffsetSubtitles, PlaylistLevelType.SUBTITLE); 27476 } 27477 } 27478 27479 // If something goes wrong, proceed to next frag, if we were processing one. 27480 onError(event, data) { 27481 const frag = data.frag; 27482 if ((frag == null ? void 0 : frag.type) === PlaylistLevelType.SUBTITLE) { 27483 if (data.details === ErrorDetails.FRAG_GAP) { 27484 this.fragmentTracker.fragBuffered(frag, true); 27485 } 27486 if (this.fragCurrent) { 27487 this.fragCurrent.abortRequests(); 27488 } 27489 if (this.state !== State.STOPPED) { 27490 this.state = State.IDLE; 27491 } 27492 } 27493 } 27494 27495 // Got all new subtitle levels. 27496 onSubtitleTracksUpdated(event, { 27497 subtitleTracks 27498 }) { 27499 if (this.levels && subtitleOptionsIdentical(this.levels, subtitleTracks)) { 27500 this.levels = subtitleTracks.map(mediaPlaylist => new Level(mediaPlaylist)); 27501 return; 27502 } 27503 this.tracksBuffered = []; 27504 this.levels = subtitleTracks.map(mediaPlaylist => { 27505 const level = new Level(mediaPlaylist); 27506 this.tracksBuffered[level.id] = []; 27507 return level; 27508 }); 27509 this.fragmentTracker.removeFragmentsInRange(0, Number.POSITIVE_INFINITY, PlaylistLevelType.SUBTITLE); 27510 this.fragPrevious = null; 27511 this.mediaBuffer = null; 27512 } 27513 onSubtitleTrackSwitch(event, data) { 27514 var _this$levels; 27515 this.currentTrackId = data.id; 27516 if (!((_this$levels = this.levels) != null && _this$levels.length) || this.currentTrackId === -1) { 27517 this.clearInterval(); 27518 return; 27519 } 27520 27521 // Check if track has the necessary details to load fragments 27522 const currentTrack = this.levels[this.currentTrackId]; 27523 if (currentTrack != null && currentTrack.details) { 27524 this.mediaBuffer = this.mediaBufferTimeRanges; 27525 } else { 27526 this.mediaBuffer = null; 27527 } 27528 if (currentTrack && this.state !== State.STOPPED) { 27529 this.setInterval(TICK_INTERVAL$2); 27530 } 27531 } 27532 27533 // Got a new set of subtitle fragments. 27534 onSubtitleTrackLoaded(event, data) { 27535 var _track$details; 27536 const { 27537 currentTrackId, 27538 levels 27539 } = this; 27540 const { 27541 details: newDetails, 27542 id: trackId 27543 } = data; 27544 if (!levels) { 27545 this.warn(`Subtitle tracks were reset while loading level ${trackId}`); 27546 return; 27547 } 27548 const track = levels[trackId]; 27549 if (trackId >= levels.length || !track) { 27550 return; 27551 } 27552 this.log(`Subtitle track ${trackId} loaded [${newDetails.startSN},${newDetails.endSN}]${newDetails.lastPartSn ? `[part-${newDetails.lastPartSn}-${newDetails.lastPartIndex}]` : ''},duration:${newDetails.totalduration}`); 27553 this.mediaBuffer = this.mediaBufferTimeRanges; 27554 let sliding = 0; 27555 if (newDetails.live || (_track$details = track.details) != null && _track$details.live) { 27556 if (newDetails.deltaUpdateFailed) { 27557 return; 27558 } 27559 const mainDetails = this.mainDetails; 27560 if (!mainDetails) { 27561 this.startFragRequested = false; 27562 return; 27563 } 27564 const mainSlidingStartFragment = mainDetails.fragments[0]; 27565 if (!track.details) { 27566 if (newDetails.hasProgramDateTime && mainDetails.hasProgramDateTime) { 27567 alignMediaPlaylistByPDT(newDetails, mainDetails); 27568 sliding = newDetails.fragmentStart; 27569 } else if (mainSlidingStartFragment) { 27570 // line up live playlist with main so that fragments in range are loaded 27571 sliding = mainSlidingStartFragment.start; 27572 addSliding(newDetails, sliding); 27573 } 27574 } else { 27575 var _this$levelLastLoaded; 27576 sliding = this.alignPlaylists(newDetails, track.details, (_this$levelLastLoaded = this.levelLastLoaded) == null ? void 0 : _this$levelLastLoaded.details); 27577 if (sliding === 0 && mainSlidingStartFragment) { 27578 // realign with main when there is no overlap with last refresh 27579 sliding = mainSlidingStartFragment.start; 27580 addSliding(newDetails, sliding); 27581 } 27582 } 27583 // compute start position if we are aligned with the main playlist 27584 if (mainDetails && !this.startFragRequested) { 27585 this.setStartPosition(mainDetails, sliding); 27586 } 27587 } 27588 track.details = newDetails; 27589 this.levelLastLoaded = track; 27590 if (trackId !== currentTrackId) { 27591 return; 27592 } 27593 this.hls.trigger(Events.SUBTITLE_TRACK_UPDATED, { 27594 details: newDetails, 27595 id: trackId, 27596 groupId: data.groupId 27597 }); 27598 27599 // trigger handler right now 27600 this.tick(); 27601 27602 // If playlist is misaligned because of bad PDT or drift, delete details to resync with main on reload 27603 if (newDetails.live && !this.fragCurrent && this.media && this.state === State.IDLE) { 27604 const foundFrag = findFragmentByPTS(null, newDetails.fragments, this.media.currentTime, 0); 27605 if (!foundFrag) { 27606 this.warn('Subtitle playlist not aligned with playback'); 27607 track.details = undefined; 27608 } 27609 } 27610 } 27611 _handleFragmentLoadComplete(fragLoadedData) { 27612 const { 27613 frag, 27614 payload 27615 } = fragLoadedData; 27616 const decryptData = frag.decryptdata; 27617 const hls = this.hls; 27618 if (this.fragContextChanged(frag)) { 27619 return; 27620 } 27621 // check to see if the payload needs to be decrypted 27622 if (payload && payload.byteLength > 0 && decryptData != null && decryptData.key && decryptData.iv && isFullSegmentEncryption(decryptData.method)) { 27623 const startTime = performance.now(); 27624 // decrypt the subtitles 27625 this.decrypter.decrypt(new Uint8Array(payload), decryptData.key.buffer, decryptData.iv.buffer, getAesModeFromFullSegmentMethod(decryptData.method)).catch(err => { 27626 hls.trigger(Events.ERROR, { 27627 type: ErrorTypes.MEDIA_ERROR, 27628 details: ErrorDetails.FRAG_DECRYPT_ERROR, 27629 fatal: false, 27630 error: err, 27631 reason: err.message, 27632 frag 27633 }); 27634 throw err; 27635 }).then(decryptedData => { 27636 const endTime = performance.now(); 27637 hls.trigger(Events.FRAG_DECRYPTED, { 27638 frag, 27639 payload: decryptedData, 27640 stats: { 27641 tstart: startTime, 27642 tdecrypt: endTime 27643 } 27644 }); 27645 }).catch(err => { 27646 this.warn(`${err.name}: ${err.message}`); 27647 this.state = State.IDLE; 27648 }); 27649 } 27650 } 27651 doTick() { 27652 if (!this.media) { 27653 this.state = State.IDLE; 27654 return; 27655 } 27656 if (this.state === State.IDLE) { 27657 const { 27658 currentTrackId, 27659 levels 27660 } = this; 27661 const track = levels == null ? void 0 : levels[currentTrackId]; 27662 if (!track || !levels.length || !track.details) { 27663 return; 27664 } 27665 if (this.waitForLive(track)) { 27666 return; 27667 } 27668 const { 27669 config 27670 } = this; 27671 const currentTime = this.getLoadPosition(); 27672 const bufferedInfo = BufferHelper.bufferedInfo(this.tracksBuffered[this.currentTrackId] || [], currentTime, config.maxBufferHole); 27673 const { 27674 end: targetBufferTime, 27675 len: bufferLen 27676 } = bufferedInfo; 27677 const trackDetails = track.details; 27678 const maxBufLen = this.hls.maxBufferLength + trackDetails.levelTargetDuration; 27679 if (bufferLen > maxBufLen) { 27680 return; 27681 } 27682 const fragments = trackDetails.fragments; 27683 const fragLen = fragments.length; 27684 const end = trackDetails.edge; 27685 let foundFrag = null; 27686 const fragPrevious = this.fragPrevious; 27687 if (targetBufferTime < end) { 27688 const tolerance = config.maxFragLookUpTolerance; 27689 const lookupTolerance = targetBufferTime > end - tolerance ? 0 : tolerance; 27690 foundFrag = findFragmentByPTS(fragPrevious, fragments, Math.max(fragments[0].start, targetBufferTime), lookupTolerance); 27691 if (!foundFrag && fragPrevious && fragPrevious.start < fragments[0].start) { 27692 foundFrag = fragments[0]; 27693 } 27694 } else { 27695 foundFrag = fragments[fragLen - 1]; 27696 } 27697 foundFrag = this.filterReplacedPrimary(foundFrag, track.details); 27698 if (!foundFrag) { 27699 return; 27700 } 27701 // Load earlier fragment in same discontinuity to make up for misaligned playlists and cues that extend beyond end of segment 27702 const curSNIdx = foundFrag.sn - trackDetails.startSN; 27703 const prevFrag = fragments[curSNIdx - 1]; 27704 if (prevFrag && prevFrag.cc === foundFrag.cc && this.fragmentTracker.getState(prevFrag) === FragmentState.NOT_LOADED) { 27705 foundFrag = prevFrag; 27706 } 27707 if (this.fragmentTracker.getState(foundFrag) === FragmentState.NOT_LOADED) { 27708 // only load if fragment is not loaded 27709 const fragToLoad = this.mapToInitFragWhenRequired(foundFrag); 27710 if (fragToLoad) { 27711 this.loadFragment(fragToLoad, track, targetBufferTime); 27712 } 27713 } 27714 } 27715 } 27716 loadFragment(frag, level, targetBufferTime) { 27717 if (!isMediaFragment(frag)) { 27718 this._loadInitSegment(frag, level); 27719 } else { 27720 super.loadFragment(frag, level, targetBufferTime); 27721 } 27722 } 27723 get mediaBufferTimeRanges() { 27724 return new BufferableInstance(this.tracksBuffered[this.currentTrackId] || []); 27725 } 27726} 27727class BufferableInstance { 27728 constructor(timeranges) { 27729 this.buffered = void 0; 27730 const getRange = (name, index, length) => { 27731 index = index >>> 0; 27732 if (index > length - 1) { 27733 throw new DOMException(`Failed to execute '${name}' on 'TimeRanges': The index provided (${index}) is greater than the maximum bound (${length})`); 27734 } 27735 return timeranges[index][name]; 27736 }; 27737 this.buffered = { 27738 get length() { 27739 return timeranges.length; 27740 }, 27741 end(index) { 27742 return getRange('end', index, timeranges.length); 27743 }, 27744 start(index) { 27745 return getRange('start', index, timeranges.length); 27746 } 27747 }; 27748 } 27749} 27750 27751/** 27752 * 27753 * This code was ported from the dash.js project at: 27754 * https://github.com/Dash-Industry-Forum/dash.js/blob/development/externals/cea608-parser.js 27755 * https://github.com/Dash-Industry-Forum/dash.js/commit/8269b26a761e0853bb21d78780ed945144ecdd4d#diff-71bc295a2d6b6b7093a1d3290d53a4b2 27756 * 27757 * The original copyright appears below: 27758 * 27759 * The copyright in this software is being made available under the BSD License, 27760 * included below. This software may be subject to other third party and contributor 27761 * rights, including patent rights, and no such rights are granted under this license. 27762 * 27763 * Copyright (c) 2015-2016, DASH Industry Forum. 27764 * All rights reserved. 27765 * 27766 * Redistribution and use in source and binary forms, with or without modification, 27767 * are permitted provided that the following conditions are met: 27768 * 1. Redistributions of source code must retain the above copyright notice, this 27769 * list of conditions and the following disclaimer. 27770 * * Redistributions in binary form must reproduce the above copyright notice, 27771 * this list of conditions and the following disclaimer in the documentation and/or 27772 * other materials provided with the distribution. 27773 * 2. Neither the name of Dash Industry Forum nor the names of its 27774 * contributors may be used to endorse or promote products derived from this software 27775 * without specific prior written permission. 27776 * 27777 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY 27778 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 27779 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 27780 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 27781 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 27782 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 27783 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 27784 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27785 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27786 * POSSIBILITY OF SUCH DAMAGE. 27787 */ 27788/** 27789 * Exceptions from regular ASCII. CodePoints are mapped to UTF-16 codes 27790 */ 27791 27792const specialCea608CharsCodes = { 27793 0x2a: 0xe1, 27794 // lowercase a, acute accent 27795 0x5c: 0xe9, 27796 // lowercase e, acute accent 27797 0x5e: 0xed, 27798 // lowercase i, acute accent 27799 0x5f: 0xf3, 27800 // lowercase o, acute accent 27801 0x60: 0xfa, 27802 // lowercase u, acute accent 27803 0x7b: 0xe7, 27804 // lowercase c with cedilla 27805 0x7c: 0xf7, 27806 // division symbol 27807 0x7d: 0xd1, 27808 // uppercase N tilde 27809 0x7e: 0xf1, 27810 // lowercase n tilde 27811 0x7f: 0x2588, 27812 // Full block 27813 // THIS BLOCK INCLUDES THE 16 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS 27814 // THAT COME FROM HI BYTE=0x11 AND LOW BETWEEN 0x30 AND 0x3F 27815 // THIS MEANS THAT \x50 MUST BE ADDED TO THE VALUES 27816 0x80: 0xae, 27817 // Registered symbol (R) 27818 0x81: 0xb0, 27819 // degree sign 27820 0x82: 0xbd, 27821 // 1/2 symbol 27822 0x83: 0xbf, 27823 // Inverted (open) question mark 27824 0x84: 0x2122, 27825 // Trademark symbol (TM) 27826 0x85: 0xa2, 27827 // Cents symbol 27828 0x86: 0xa3, 27829 // Pounds sterling 27830 0x87: 0x266a, 27831 // Music 8'th note 27832 0x88: 0xe0, 27833 // lowercase a, grave accent 27834 0x89: 0x20, 27835 // transparent space (regular) 27836 0x8a: 0xe8, 27837 // lowercase e, grave accent 27838 0x8b: 0xe2, 27839 // lowercase a, circumflex accent 27840 0x8c: 0xea, 27841 // lowercase e, circumflex accent 27842 0x8d: 0xee, 27843 // lowercase i, circumflex accent 27844 0x8e: 0xf4, 27845 // lowercase o, circumflex accent 27846 0x8f: 0xfb, 27847 // lowercase u, circumflex accent 27848 // THIS BLOCK INCLUDES THE 32 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS 27849 // THAT COME FROM HI BYTE=0x12 AND LOW BETWEEN 0x20 AND 0x3F 27850 0x90: 0xc1, 27851 // capital letter A with acute 27852 0x91: 0xc9, 27853 // capital letter E with acute 27854 0x92: 0xd3, 27855 // capital letter O with acute 27856 0x93: 0xda, 27857 // capital letter U with acute 27858 0x94: 0xdc, 27859 // capital letter U with diaresis 27860 0x95: 0xfc, 27861 // lowercase letter U with diaeresis 27862 0x96: 0x2018, 27863 // opening single quote 27864 0x97: 0xa1, 27865 // inverted exclamation mark 27866 0x98: 0x2a, 27867 // asterisk 27868 0x99: 0x2019, 27869 // closing single quote 27870 0x9a: 0x2501, 27871 // box drawings heavy horizontal 27872 0x9b: 0xa9, 27873 // copyright sign 27874 0x9c: 0x2120, 27875 // Service mark 27876 0x9d: 0x2022, 27877 // (round) bullet 27878 0x9e: 0x201c, 27879 // Left double quotation mark 27880 0x9f: 0x201d, 27881 // Right double quotation mark 27882 0xa0: 0xc0, 27883 // uppercase A, grave accent 27884 0xa1: 0xc2, 27885 // uppercase A, circumflex 27886 0xa2: 0xc7, 27887 // uppercase C with cedilla 27888 0xa3: 0xc8, 27889 // uppercase E, grave accent 27890 0xa4: 0xca, 27891 // uppercase E, circumflex 27892 0xa5: 0xcb, 27893 // capital letter E with diaresis 27894 0xa6: 0xeb, 27895 // lowercase letter e with diaresis 27896 0xa7: 0xce, 27897 // uppercase I, circumflex 27898 0xa8: 0xcf, 27899 // uppercase I, with diaresis 27900 0xa9: 0xef, 27901 // lowercase i, with diaresis 27902 0xaa: 0xd4, 27903 // uppercase O, circumflex 27904 0xab: 0xd9, 27905 // uppercase U, grave accent 27906 0xac: 0xf9, 27907 // lowercase u, grave accent 27908 0xad: 0xdb, 27909 // uppercase U, circumflex 27910 0xae: 0xab, 27911 // left-pointing double angle quotation mark 27912 0xaf: 0xbb, 27913 // right-pointing double angle quotation mark 27914 // THIS BLOCK INCLUDES THE 32 EXTENDED (TWO-BYTE) LINE 21 CHARACTERS 27915 // THAT COME FROM HI BYTE=0x13 AND LOW BETWEEN 0x20 AND 0x3F 27916 0xb0: 0xc3, 27917 // Uppercase A, tilde 27918 0xb1: 0xe3, 27919 // Lowercase a, tilde 27920 0xb2: 0xcd, 27921 // Uppercase I, acute accent 27922 0xb3: 0xcc, 27923 // Uppercase I, grave accent 27924 0xb4: 0xec, 27925 // Lowercase i, grave accent 27926 0xb5: 0xd2, 27927 // Uppercase O, grave accent 27928 0xb6: 0xf2, 27929 // Lowercase o, grave accent 27930 0xb7: 0xd5, 27931 // Uppercase O, tilde 27932 0xb8: 0xf5, 27933 // Lowercase o, tilde 27934 0xb9: 0x7b, 27935 // Open curly brace 27936 0xba: 0x7d, 27937 // Closing curly brace 27938 0xbb: 0x5c, 27939 // Backslash 27940 0xbc: 0x5e, 27941 // Caret 27942 0xbd: 0x5f, 27943 // Underscore 27944 0xbe: 0x7c, 27945 // Pipe (vertical line) 27946 0xbf: 0x223c, 27947 // Tilde operator 27948 0xc0: 0xc4, 27949 // Uppercase A, umlaut 27950 0xc1: 0xe4, 27951 // Lowercase A, umlaut 27952 0xc2: 0xd6, 27953 // Uppercase O, umlaut 27954 0xc3: 0xf6, 27955 // Lowercase o, umlaut 27956 0xc4: 0xdf, 27957 // Esszett (sharp S) 27958 0xc5: 0xa5, 27959 // Yen symbol 27960 0xc6: 0xa4, 27961 // Generic currency sign 27962 0xc7: 0x2503, 27963 // Box drawings heavy vertical 27964 0xc8: 0xc5, 27965 // Uppercase A, ring 27966 0xc9: 0xe5, 27967 // Lowercase A, ring 27968 0xca: 0xd8, 27969 // Uppercase O, stroke 27970 0xcb: 0xf8, 27971 // Lowercase o, strok 27972 0xcc: 0x250f, 27973 // Box drawings heavy down and right 27974 0xcd: 0x2513, 27975 // Box drawings heavy down and left 27976 0xce: 0x2517, 27977 // Box drawings heavy up and right 27978 0xcf: 0x251b // Box drawings heavy up and left 27979}; 27980 27981/** 27982 * Utils 27983 */ 27984const getCharForByte = byte => String.fromCharCode(specialCea608CharsCodes[byte] || byte); 27985const NR_ROWS = 15; 27986const NR_COLS = 100; 27987// Tables to look up row from PAC data 27988const rowsLowCh1 = { 27989 0x11: 1, 27990 0x12: 3, 27991 0x15: 5, 27992 0x16: 7, 27993 0x17: 9, 27994 0x10: 11, 27995 0x13: 12, 27996 0x14: 14 27997}; 27998const rowsHighCh1 = { 27999 0x11: 2, 28000 0x12: 4, 28001 0x15: 6, 28002 0x16: 8, 28003 0x17: 10, 28004 0x13: 13, 28005 0x14: 15 28006}; 28007const rowsLowCh2 = { 28008 0x19: 1, 28009 0x1a: 3, 28010 0x1d: 5, 28011 0x1e: 7, 28012 0x1f: 9, 28013 0x18: 11, 28014 0x1b: 12, 28015 0x1c: 14 28016}; 28017const rowsHighCh2 = { 28018 0x19: 2, 28019 0x1a: 4, 28020 0x1d: 6, 28021 0x1e: 8, 28022 0x1f: 10, 28023 0x1b: 13, 28024 0x1c: 15 28025}; 28026const backgroundColors = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta', 'black', 'transparent']; 28027class CaptionsLogger { 28028 constructor() { 28029 this.time = null; 28030 this.verboseLevel = 0; 28031 } 28032 log(severity, msg) { 28033 if (this.verboseLevel >= severity) { 28034 const m = typeof msg === 'function' ? msg() : msg; 28035 logger.log(`${this.time} [${severity}] ${m}`); 28036 } 28037 } 28038} 28039const numArrayToHexArray = function numArrayToHexArray(numArray) { 28040 const hexArray = []; 28041 for (let j = 0; j < numArray.length; j++) { 28042 hexArray.push(numArray[j].toString(16)); 28043 } 28044 return hexArray; 28045}; 28046class PenState { 28047 constructor() { 28048 this.foreground = 'white'; 28049 this.underline = false; 28050 this.italics = false; 28051 this.background = 'black'; 28052 this.flash = false; 28053 } 28054 reset() { 28055 this.foreground = 'white'; 28056 this.underline = false; 28057 this.italics = false; 28058 this.background = 'black'; 28059 this.flash = false; 28060 } 28061 setStyles(styles) { 28062 const attribs = ['foreground', 'underline', 'italics', 'background', 'flash']; 28063 for (let i = 0; i < attribs.length; i++) { 28064 const style = attribs[i]; 28065 if (styles.hasOwnProperty(style)) { 28066 this[style] = styles[style]; 28067 } 28068 } 28069 } 28070 isDefault() { 28071 return this.foreground === 'white' && !this.underline && !this.italics && this.background === 'black' && !this.flash; 28072 } 28073 equals(other) { 28074 return this.foreground === other.foreground && this.underline === other.underline && this.italics === other.italics && this.background === other.background && this.flash === other.flash; 28075 } 28076 copy(newPenState) { 28077 this.foreground = newPenState.foreground; 28078 this.underline = newPenState.underline; 28079 this.italics = newPenState.italics; 28080 this.background = newPenState.background; 28081 this.flash = newPenState.flash; 28082 } 28083 toString() { 28084 return 'color=' + this.foreground + ', underline=' + this.underline + ', italics=' + this.italics + ', background=' + this.background + ', flash=' + this.flash; 28085 } 28086} 28087 28088/** 28089 * Unicode character with styling and background. 28090 * @constructor 28091 */ 28092class StyledUnicodeChar { 28093 constructor() { 28094 this.uchar = ' '; 28095 this.penState = new PenState(); 28096 } 28097 reset() { 28098 this.uchar = ' '; 28099 this.penState.reset(); 28100 } 28101 setChar(uchar, newPenState) { 28102 this.uchar = uchar; 28103 this.penState.copy(newPenState); 28104 } 28105 setPenState(newPenState) { 28106 this.penState.copy(newPenState); 28107 } 28108 equals(other) { 28109 return this.uchar === other.uchar && this.penState.equals(other.penState); 28110 } 28111 copy(newChar) { 28112 this.uchar = newChar.uchar; 28113 this.penState.copy(newChar.penState); 28114 } 28115 isEmpty() { 28116 return this.uchar === ' ' && this.penState.isDefault(); 28117 } 28118} 28119 28120/** 28121 * CEA-608 row consisting of NR_COLS instances of StyledUnicodeChar. 28122 * @constructor 28123 */ 28124class Row { 28125 constructor(logger) { 28126 this.chars = []; 28127 this.pos = 0; 28128 this.currPenState = new PenState(); 28129 this.cueStartTime = null; 28130 this.logger = void 0; 28131 for (let i = 0; i < NR_COLS; i++) { 28132 this.chars.push(new StyledUnicodeChar()); 28133 } 28134 this.logger = logger; 28135 } 28136 equals(other) { 28137 for (let i = 0; i < NR_COLS; i++) { 28138 if (!this.chars[i].equals(other.chars[i])) { 28139 return false; 28140 } 28141 } 28142 return true; 28143 } 28144 copy(other) { 28145 for (let i = 0; i < NR_COLS; i++) { 28146 this.chars[i].copy(other.chars[i]); 28147 } 28148 } 28149 isEmpty() { 28150 let empty = true; 28151 for (let i = 0; i < NR_COLS; i++) { 28152 if (!this.chars[i].isEmpty()) { 28153 empty = false; 28154 break; 28155 } 28156 } 28157 return empty; 28158 } 28159 28160 /** 28161 * Set the cursor to a valid column. 28162 */ 28163 setCursor(absPos) { 28164 if (this.pos !== absPos) { 28165 this.pos = absPos; 28166 } 28167 if (this.pos < 0) { 28168 this.logger.log(3, 'Negative cursor position ' + this.pos); 28169 this.pos = 0; 28170 } else if (this.pos > NR_COLS) { 28171 this.logger.log(3, 'Too large cursor position ' + this.pos); 28172 this.pos = NR_COLS; 28173 } 28174 } 28175 28176 /** 28177 * Move the cursor relative to current position. 28178 */ 28179 moveCursor(relPos) { 28180 const newPos = this.pos + relPos; 28181 if (relPos > 1) { 28182 for (let i = this.pos + 1; i < newPos + 1; i++) { 28183 this.chars[i].setPenState(this.currPenState); 28184 } 28185 } 28186 this.setCursor(newPos); 28187 } 28188 28189 /** 28190 * Backspace, move one step back and clear character. 28191 */ 28192 backSpace() { 28193 this.moveCursor(-1); 28194 this.chars[this.pos].setChar(' ', this.currPenState); 28195 } 28196 insertChar(byte) { 28197 if (byte >= 0x90) { 28198 // Extended char 28199 this.backSpace(); 28200 } 28201 const char = getCharForByte(byte); 28202 if (this.pos >= NR_COLS) { 28203 this.logger.log(0, () => 'Cannot insert ' + byte.toString(16) + ' (' + char + ') at position ' + this.pos + '. Skipping it!'); 28204 return; 28205 } 28206 this.chars[this.pos].setChar(char, this.currPenState); 28207 this.moveCursor(1); 28208 } 28209 clearFromPos(startPos) { 28210 let i; 28211 for (i = startPos; i < NR_COLS; i++) { 28212 this.chars[i].reset(); 28213 } 28214 } 28215 clear() { 28216 this.clearFromPos(0); 28217 this.pos = 0; 28218 this.currPenState.reset(); 28219 } 28220 clearToEndOfRow() { 28221 this.clearFromPos(this.pos); 28222 } 28223 getTextString() { 28224 const chars = []; 28225 let empty = true; 28226 for (let i = 0; i < NR_COLS; i++) { 28227 const char = this.chars[i].uchar; 28228 if (char !== ' ') { 28229 empty = false; 28230 } 28231 chars.push(char); 28232 } 28233 if (empty) { 28234 return ''; 28235 } else { 28236 return chars.join(''); 28237 } 28238 } 28239 setPenStyles(styles) { 28240 this.currPenState.setStyles(styles); 28241 const currChar = this.chars[this.pos]; 28242 currChar.setPenState(this.currPenState); 28243 } 28244} 28245 28246/** 28247 * Keep a CEA-608 screen of 32x15 styled characters 28248 * @constructor 28249 */ 28250class CaptionScreen { 28251 constructor(logger) { 28252 this.rows = []; 28253 this.currRow = NR_ROWS - 1; 28254 this.nrRollUpRows = null; 28255 this.lastOutputScreen = null; 28256 this.logger = void 0; 28257 for (let i = 0; i < NR_ROWS; i++) { 28258 this.rows.push(new Row(logger)); 28259 } 28260 this.logger = logger; 28261 } 28262 reset() { 28263 for (let i = 0; i < NR_ROWS; i++) { 28264 this.rows[i].clear(); 28265 } 28266 this.currRow = NR_ROWS - 1; 28267 } 28268 equals(other) { 28269 let equal = true; 28270 for (let i = 0; i < NR_ROWS; i++) { 28271 if (!this.rows[i].equals(other.rows[i])) { 28272 equal = false; 28273 break; 28274 } 28275 } 28276 return equal; 28277 } 28278 copy(other) { 28279 for (let i = 0; i < NR_ROWS; i++) { 28280 this.rows[i].copy(other.rows[i]); 28281 } 28282 } 28283 isEmpty() { 28284 let empty = true; 28285 for (let i = 0; i < NR_ROWS; i++) { 28286 if (!this.rows[i].isEmpty()) { 28287 empty = false; 28288 break; 28289 } 28290 } 28291 return empty; 28292 } 28293 backSpace() { 28294 const row = this.rows[this.currRow]; 28295 row.backSpace(); 28296 } 28297 clearToEndOfRow() { 28298 const row = this.rows[this.currRow]; 28299 row.clearToEndOfRow(); 28300 } 28301 28302 /** 28303 * Insert a character (without styling) in the current row. 28304 */ 28305 insertChar(char) { 28306 const row = this.rows[this.currRow]; 28307 row.insertChar(char); 28308 } 28309 setPen(styles) { 28310 const row = this.rows[this.currRow]; 28311 row.setPenStyles(styles); 28312 } 28313 moveCursor(relPos) { 28314 const row = this.rows[this.currRow]; 28315 row.moveCursor(relPos); 28316 } 28317 setCursor(absPos) { 28318 this.logger.log(2, 'setCursor: ' + absPos); 28319 const row = this.rows[this.currRow]; 28320 row.setCursor(absPos); 28321 } 28322 setPAC(pacData) { 28323 this.logger.log(2, () => 'pacData = ' + stringify(pacData)); 28324 let newRow = pacData.row - 1; 28325 if (this.nrRollUpRows && newRow < this.nrRollUpRows - 1) { 28326 newRow = this.nrRollUpRows - 1; 28327 } 28328 28329 // Make sure this only affects Roll-up Captions by checking this.nrRollUpRows 28330 if (this.nrRollUpRows && this.currRow !== newRow) { 28331 // clear all rows first 28332 for (let i = 0; i < NR_ROWS; i++) { 28333 this.rows[i].clear(); 28334 } 28335 28336 // Copy this.nrRollUpRows rows from lastOutputScreen and place it in the newRow location 28337 // topRowIndex - the start of rows to copy (inclusive index) 28338 const topRowIndex = this.currRow + 1 - this.nrRollUpRows; 28339 // We only copy if the last position was already shown. 28340 // We use the cueStartTime value to check this. 28341 const lastOutputScreen = this.lastOutputScreen; 28342 if (lastOutputScreen) { 28343 const prevLineTime = lastOutputScreen.rows[topRowIndex].cueStartTime; 28344 const time = this.logger.time; 28345 if (prevLineTime !== null && time !== null && prevLineTime < time) { 28346 for (let i = 0; i < this.nrRollUpRows; i++) { 28347 this.rows[newRow - this.nrRollUpRows + i + 1].copy(lastOutputScreen.rows[topRowIndex + i]); 28348 } 28349 } 28350 } 28351 } 28352 this.currRow = newRow; 28353 const row = this.rows[this.currRow]; 28354 if (pacData.indent !== null) { 28355 const indent = pacData.indent; 28356 const prevPos = Math.max(indent - 1, 0); 28357 row.setCursor(pacData.indent); 28358 pacData.color = row.chars[prevPos].penState.foreground; 28359 } 28360 const styles = { 28361 foreground: pacData.color, 28362 underline: pacData.underline, 28363 italics: pacData.italics, 28364 background: 'black', 28365 flash: false 28366 }; 28367 this.setPen(styles); 28368 } 28369 28370 /** 28371 * Set background/extra foreground, but first do back_space, and then insert space (backwards compatibility). 28372 */ 28373 setBkgData(bkgData) { 28374 this.logger.log(2, () => 'bkgData = ' + stringify(bkgData)); 28375 this.backSpace(); 28376 this.setPen(bkgData); 28377 this.insertChar(0x20); // Space 28378 } 28379 setRollUpRows(nrRows) { 28380 this.nrRollUpRows = nrRows; 28381 } 28382 rollUp() { 28383 if (this.nrRollUpRows === null) { 28384 this.logger.log(3, 'roll_up but nrRollUpRows not set yet'); 28385 return; // Not properly setup 28386 } 28387 this.logger.log(1, () => this.getDisplayText()); 28388 const topRowIndex = this.currRow + 1 - this.nrRollUpRows; 28389 const topRow = this.rows.splice(topRowIndex, 1)[0]; 28390 topRow.clear(); 28391 this.rows.splice(this.currRow, 0, topRow); 28392 this.logger.log(2, 'Rolling up'); 28393 // this.logger.log(VerboseLevel.TEXT, this.get_display_text()) 28394 } 28395 28396 /** 28397 * Get all non-empty rows with as unicode text. 28398 */ 28399 getDisplayText(asOneRow) { 28400 asOneRow = asOneRow || false; 28401 const displayText = []; 28402 let text = ''; 28403 let rowNr = -1; 28404 for (let i = 0; i < NR_ROWS; i++) { 28405 const rowText = this.rows[i].getTextString(); 28406 if (rowText) { 28407 rowNr = i + 1; 28408 if (asOneRow) { 28409 displayText.push('Row ' + rowNr + ": '" + rowText + "'"); 28410 } else { 28411 displayText.push(rowText.trim()); 28412 } 28413 } 28414 } 28415 if (displayText.length > 0) { 28416 if (asOneRow) { 28417 text = '[' + displayText.join(' | ') + ']'; 28418 } else { 28419 text = displayText.join('\n'); 28420 } 28421 } 28422 return text; 28423 } 28424 getTextAndFormat() { 28425 return this.rows; 28426 } 28427} 28428 28429// var modes = ['MODE_ROLL-UP', 'MODE_POP-ON', 'MODE_PAINT-ON', 'MODE_TEXT']; 28430 28431class Cea608Channel { 28432 constructor(channelNumber, outputFilter, logger) { 28433 this.chNr = void 0; 28434 this.outputFilter = void 0; 28435 this.mode = void 0; 28436 this.verbose = void 0; 28437 this.displayedMemory = void 0; 28438 this.nonDisplayedMemory = void 0; 28439 this.lastOutputScreen = void 0; 28440 this.currRollUpRow = void 0; 28441 this.writeScreen = void 0; 28442 this.cueStartTime = void 0; 28443 this.logger = void 0; 28444 this.chNr = channelNumber; 28445 this.outputFilter = outputFilter; 28446 this.mode = null; 28447 this.verbose = 0; 28448 this.displayedMemory = new CaptionScreen(logger); 28449 this.nonDisplayedMemory = new CaptionScreen(logger); 28450 this.lastOutputScreen = new CaptionScreen(logger); 28451 this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1]; 28452 this.writeScreen = this.displayedMemory; 28453 this.mode = null; 28454 this.cueStartTime = null; // Keeps track of where a cue started. 28455 this.logger = logger; 28456 } 28457 reset() { 28458 this.mode = null; 28459 this.displayedMemory.reset(); 28460 this.nonDisplayedMemory.reset(); 28461 this.lastOutputScreen.reset(); 28462 this.outputFilter.reset(); 28463 this.currRollUpRow = this.displayedMemory.rows[NR_ROWS - 1]; 28464 this.writeScreen = this.displayedMemory; 28465 this.mode = null; 28466 this.cueStartTime = null; 28467 } 28468 getHandler() { 28469 return this.outputFilter; 28470 } 28471 setHandler(newHandler) { 28472 this.outputFilter = newHandler; 28473 } 28474 setPAC(pacData) { 28475 this.writeScreen.setPAC(pacData); 28476 } 28477 setBkgData(bkgData) { 28478 this.writeScreen.setBkgData(bkgData); 28479 } 28480 setMode(newMode) { 28481 if (newMode === this.mode) { 28482 return; 28483 } 28484 this.mode = newMode; 28485 this.logger.log(2, () => 'MODE=' + newMode); 28486 if (this.mode === 'MODE_POP-ON') { 28487 this.writeScreen = this.nonDisplayedMemory; 28488 } else { 28489 this.writeScreen = this.displayedMemory; 28490 this.writeScreen.reset(); 28491 } 28492 if (this.mode !== 'MODE_ROLL-UP') { 28493 this.displayedMemory.nrRollUpRows = null; 28494 this.nonDisplayedMemory.nrRollUpRows = null; 28495 } 28496 this.mode = newMode; 28497 } 28498 insertChars(chars) { 28499 for (let i = 0; i < chars.length; i++) { 28500 this.writeScreen.insertChar(chars[i]); 28501 } 28502 const screen = this.writeScreen === this.displayedMemory ? 'DISP' : 'NON_DISP'; 28503 this.logger.log(2, () => screen + ': ' + this.writeScreen.getDisplayText(true)); 28504 if (this.mode === 'MODE_PAINT-ON' || this.mode === 'MODE_ROLL-UP') { 28505 this.logger.log(1, () => 'DISPLAYED: ' + this.displayedMemory.getDisplayText(true)); 28506 this.outputDataUpdate(); 28507 } 28508 } 28509 ccRCL() { 28510 // Resume Caption Loading (switch mode to Pop On) 28511 this.logger.log(2, 'RCL - Resume Caption Loading'); 28512 this.setMode('MODE_POP-ON'); 28513 } 28514 ccBS() { 28515 // BackSpace 28516 this.logger.log(2, 'BS - BackSpace'); 28517 if (this.mode === 'MODE_TEXT') { 28518 return; 28519 } 28520 this.writeScreen.backSpace(); 28521 if (this.writeScreen === this.displayedMemory) { 28522 this.outputDataUpdate(); 28523 } 28524 } 28525 ccAOF() { 28526 // Reserved (formerly Alarm Off) 28527 } 28528 ccAON() { 28529 // Reserved (formerly Alarm On) 28530 } 28531 ccDER() { 28532 // Delete to End of Row 28533 this.logger.log(2, 'DER- Delete to End of Row'); 28534 this.writeScreen.clearToEndOfRow(); 28535 this.outputDataUpdate(); 28536 } 28537 ccRU(nrRows) { 28538 // Roll-Up Captions-2,3,or 4 Rows 28539 this.logger.log(2, 'RU(' + nrRows + ') - Roll Up'); 28540 this.writeScreen = this.displayedMemory; 28541 this.setMode('MODE_ROLL-UP'); 28542 this.writeScreen.setRollUpRows(nrRows); 28543 } 28544 ccFON() { 28545 // Flash On 28546 this.logger.log(2, 'FON - Flash On'); 28547 this.writeScreen.setPen({ 28548 flash: true 28549 }); 28550 } 28551 ccRDC() { 28552 // Resume Direct Captioning (switch mode to PaintOn) 28553 this.logger.log(2, 'RDC - Resume Direct Captioning'); 28554 this.setMode('MODE_PAINT-ON'); 28555 } 28556 ccTR() { 28557 // Text Restart in text mode (not supported, however) 28558 this.logger.log(2, 'TR'); 28559 this.setMode('MODE_TEXT'); 28560 } 28561 ccRTD() { 28562 // Resume Text Display in Text mode (not supported, however) 28563 this.logger.log(2, 'RTD'); 28564 this.setMode('MODE_TEXT'); 28565 } 28566 ccEDM() { 28567 // Erase Displayed Memory 28568 this.logger.log(2, 'EDM - Erase Displayed Memory'); 28569 this.displayedMemory.reset(); 28570 this.outputDataUpdate(true); 28571 } 28572 ccCR() { 28573 // Carriage Return 28574 this.logger.log(2, 'CR - Carriage Return'); 28575 this.writeScreen.rollUp(); 28576 this.outputDataUpdate(true); 28577 } 28578 ccENM() { 28579 // Erase Non-Displayed Memory 28580 this.logger.log(2, 'ENM - Erase Non-displayed Memory'); 28581 this.nonDisplayedMemory.reset(); 28582 } 28583 ccEOC() { 28584 // End of Caption (Flip Memories) 28585 this.logger.log(2, 'EOC - End Of Caption'); 28586 if (this.mode === 'MODE_POP-ON') { 28587 const tmp = this.displayedMemory; 28588 this.displayedMemory = this.nonDisplayedMemory; 28589 this.nonDisplayedMemory = tmp; 28590 this.writeScreen = this.nonDisplayedMemory; 28591 this.logger.log(1, () => 'DISP: ' + this.displayedMemory.getDisplayText()); 28592 } 28593 this.outputDataUpdate(true); 28594 } 28595 ccTO(nrCols) { 28596 // Tab Offset 1,2, or 3 columns 28597 this.logger.log(2, 'TO(' + nrCols + ') - Tab Offset'); 28598 this.writeScreen.moveCursor(nrCols); 28599 } 28600 ccMIDROW(secondByte) { 28601 // Parse MIDROW command 28602 const styles = { 28603 flash: false 28604 }; 28605 styles.underline = secondByte % 2 === 1; 28606 styles.italics = secondByte >= 0x2e; 28607 if (!styles.italics) { 28608 const colorIndex = Math.floor(secondByte / 2) - 0x10; 28609 const colors = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta']; 28610 styles.foreground = colors[colorIndex]; 28611 } else { 28612 styles.foreground = 'white'; 28613 } 28614 this.logger.log(2, 'MIDROW: ' + stringify(styles)); 28615 this.writeScreen.setPen(styles); 28616 } 28617 outputDataUpdate(dispatch = false) { 28618 const time = this.logger.time; 28619 if (time === null) { 28620 return; 28621 } 28622 if (this.outputFilter) { 28623 if (this.cueStartTime === null && !this.displayedMemory.isEmpty()) { 28624 // Start of a new cue 28625 this.cueStartTime = time; 28626 } else { 28627 if (!this.displayedMemory.equals(this.lastOutputScreen)) { 28628 this.outputFilter.newCue(this.cueStartTime, time, this.lastOutputScreen); 28629 if (dispatch && this.outputFilter.dispatchCue) { 28630 this.outputFilter.dispatchCue(); 28631 } 28632 this.cueStartTime = this.displayedMemory.isEmpty() ? null : time; 28633 } 28634 } 28635 this.lastOutputScreen.copy(this.displayedMemory); 28636 } 28637 } 28638 cueSplitAtTime(t) { 28639 if (this.outputFilter) { 28640 if (!this.displayedMemory.isEmpty()) { 28641 if (this.outputFilter.newCue) { 28642 this.outputFilter.newCue(this.cueStartTime, t, this.displayedMemory); 28643 } 28644 this.cueStartTime = t; 28645 } 28646 } 28647 } 28648} 28649 28650// Will be 1 or 2 when parsing captions 28651 28652class Cea608Parser { 28653 constructor(field, out1, out2) { 28654 this.channels = void 0; 28655 this.currentChannel = 0; 28656 this.cmdHistory = createCmdHistory(); 28657 this.logger = void 0; 28658 const logger = this.logger = new CaptionsLogger(); 28659 this.channels = [null, new Cea608Channel(field, out1, logger), new Cea608Channel(field + 1, out2, logger)]; 28660 } 28661 getHandler(channel) { 28662 return this.channels[channel].getHandler(); 28663 } 28664 setHandler(channel, newHandler) { 28665 this.channels[channel].setHandler(newHandler); 28666 } 28667 28668 /** 28669 * Add data for time t in forms of list of bytes (unsigned ints). The bytes are treated as pairs. 28670 */ 28671 addData(time, byteList) { 28672 this.logger.time = time; 28673 for (let i = 0; i < byteList.length; i += 2) { 28674 const a = byteList[i] & 0x7f; 28675 const b = byteList[i + 1] & 0x7f; 28676 let cmdFound = false; 28677 let charsFound = null; 28678 if (a === 0 && b === 0) { 28679 continue; 28680 } else { 28681 this.logger.log(3, () => '[' + numArrayToHexArray([byteList[i], byteList[i + 1]]) + '] -> (' + numArrayToHexArray([a, b]) + ')'); 28682 } 28683 const cmdHistory = this.cmdHistory; 28684 const isControlCode = a >= 0x10 && a <= 0x1f; 28685 if (isControlCode) { 28686 // Skip redundant control codes 28687 if (hasCmdRepeated(a, b, cmdHistory)) { 28688 setLastCmd(null, null, cmdHistory); 28689 this.logger.log(3, () => 'Repeated command (' + numArrayToHexArray([a, b]) + ') is dropped'); 28690 continue; 28691 } 28692 setLastCmd(a, b, this.cmdHistory); 28693 cmdFound = this.parseCmd(a, b); 28694 if (!cmdFound) { 28695 cmdFound = this.parseMidrow(a, b); 28696 } 28697 if (!cmdFound) { 28698 cmdFound = this.parsePAC(a, b); 28699 } 28700 if (!cmdFound) { 28701 cmdFound = this.parseBackgroundAttributes(a, b); 28702 } 28703 } else { 28704 setLastCmd(null, null, cmdHistory); 28705 } 28706 if (!cmdFound) { 28707 charsFound = this.parseChars(a, b); 28708 if (charsFound) { 28709 const currChNr = this.currentChannel; 28710 if (currChNr && currChNr > 0) { 28711 const channel = this.channels[currChNr]; 28712 channel.insertChars(charsFound); 28713 } else { 28714 this.logger.log(2, 'No channel found yet. TEXT-MODE?'); 28715 } 28716 } 28717 } 28718 if (!cmdFound && !charsFound) { 28719 this.logger.log(2, () => "Couldn't parse cleaned data " + numArrayToHexArray([a, b]) + ' orig: ' + numArrayToHexArray([byteList[i], byteList[i + 1]])); 28720 } 28721 } 28722 } 28723 28724 /** 28725 * Parse Command. 28726 * @returns True if a command was found 28727 */ 28728 parseCmd(a, b) { 28729 const cond1 = (a === 0x14 || a === 0x1c || a === 0x15 || a === 0x1d) && b >= 0x20 && b <= 0x2f; 28730 const cond2 = (a === 0x17 || a === 0x1f) && b >= 0x21 && b <= 0x23; 28731 if (!(cond1 || cond2)) { 28732 return false; 28733 } 28734 const chNr = a === 0x14 || a === 0x15 || a === 0x17 ? 1 : 2; 28735 const channel = this.channels[chNr]; 28736 if (a === 0x14 || a === 0x15 || a === 0x1c || a === 0x1d) { 28737 if (b === 0x20) { 28738 channel.ccRCL(); 28739 } else if (b === 0x21) { 28740 channel.ccBS(); 28741 } else if (b === 0x22) { 28742 channel.ccAOF(); 28743 } else if (b === 0x23) { 28744 channel.ccAON(); 28745 } else if (b === 0x24) { 28746 channel.ccDER(); 28747 } else if (b === 0x25) { 28748 channel.ccRU(2); 28749 } else if (b === 0x26) { 28750 channel.ccRU(3); 28751 } else if (b === 0x27) { 28752 channel.ccRU(4); 28753 } else if (b === 0x28) { 28754 channel.ccFON(); 28755 } else if (b === 0x29) { 28756 channel.ccRDC(); 28757 } else if (b === 0x2a) { 28758 channel.ccTR(); 28759 } else if (b === 0x2b) { 28760 channel.ccRTD(); 28761 } else if (b === 0x2c) { 28762 channel.ccEDM(); 28763 } else if (b === 0x2d) { 28764 channel.ccCR(); 28765 } else if (b === 0x2e) { 28766 channel.ccENM(); 28767 } else if (b === 0x2f) { 28768 channel.ccEOC(); 28769 } 28770 } else { 28771 // a == 0x17 || a == 0x1F 28772 channel.ccTO(b - 0x20); 28773 } 28774 this.currentChannel = chNr; 28775 return true; 28776 } 28777 28778 /** 28779 * Parse midrow styling command 28780 */ 28781 parseMidrow(a, b) { 28782 let chNr = 0; 28783 if ((a === 0x11 || a === 0x19) && b >= 0x20 && b <= 0x2f) { 28784 if (a === 0x11) { 28785 chNr = 1; 28786 } else { 28787 chNr = 2; 28788 } 28789 if (chNr !== this.currentChannel) { 28790 this.logger.log(0, 'Mismatch channel in midrow parsing'); 28791 return false; 28792 } 28793 const channel = this.channels[chNr]; 28794 if (!channel) { 28795 return false; 28796 } 28797 channel.ccMIDROW(b); 28798 this.logger.log(3, () => 'MIDROW (' + numArrayToHexArray([a, b]) + ')'); 28799 return true; 28800 } 28801 return false; 28802 } 28803 28804 /** 28805 * Parse Preable Access Codes (Table 53). 28806 * @returns {Boolean} Tells if PAC found 28807 */ 28808 parsePAC(a, b) { 28809 let row; 28810 const case1 = (a >= 0x11 && a <= 0x17 || a >= 0x19 && a <= 0x1f) && b >= 0x40 && b <= 0x7f; 28811 const case2 = (a === 0x10 || a === 0x18) && b >= 0x40 && b <= 0x5f; 28812 if (!(case1 || case2)) { 28813 return false; 28814 } 28815 const chNr = a <= 0x17 ? 1 : 2; 28816 if (b >= 0x40 && b <= 0x5f) { 28817 row = chNr === 1 ? rowsLowCh1[a] : rowsLowCh2[a]; 28818 } else { 28819 // 0x60 <= b <= 0x7F 28820 row = chNr === 1 ? rowsHighCh1[a] : rowsHighCh2[a]; 28821 } 28822 const channel = this.channels[chNr]; 28823 if (!channel) { 28824 return false; 28825 } 28826 channel.setPAC(this.interpretPAC(row, b)); 28827 this.currentChannel = chNr; 28828 return true; 28829 } 28830 28831 /** 28832 * Interpret the second byte of the pac, and return the information. 28833 * @returns pacData with style parameters 28834 */ 28835 interpretPAC(row, byte) { 28836 let pacIndex; 28837 const pacData = { 28838 color: null, 28839 italics: false, 28840 indent: null, 28841 underline: false, 28842 row: row 28843 }; 28844 if (byte > 0x5f) { 28845 pacIndex = byte - 0x60; 28846 } else { 28847 pacIndex = byte - 0x40; 28848 } 28849 pacData.underline = (pacIndex & 1) === 1; 28850 if (pacIndex <= 0xd) { 28851 pacData.color = ['white', 'green', 'blue', 'cyan', 'red', 'yellow', 'magenta', 'white'][Math.floor(pacIndex / 2)]; 28852 } else if (pacIndex <= 0xf) { 28853 pacData.italics = true; 28854 pacData.color = 'white'; 28855 } else { 28856 pacData.indent = Math.floor((pacIndex - 0x10) / 2) * 4; 28857 } 28858 return pacData; // Note that row has zero offset. The spec uses 1. 28859 } 28860 28861 /** 28862 * Parse characters. 28863 * @returns An array with 1 to 2 codes corresponding to chars, if found. null otherwise. 28864 */ 28865 parseChars(a, b) { 28866 let channelNr; 28867 let charCodes = null; 28868 let charCode1 = null; 28869 if (a >= 0x19) { 28870 channelNr = 2; 28871 charCode1 = a - 8; 28872 } else { 28873 channelNr = 1; 28874 charCode1 = a; 28875 } 28876 if (charCode1 >= 0x11 && charCode1 <= 0x13) { 28877 // Special character 28878 let oneCode; 28879 if (charCode1 === 0x11) { 28880 oneCode = b + 0x50; 28881 } else if (charCode1 === 0x12) { 28882 oneCode = b + 0x70; 28883 } else { 28884 oneCode = b + 0x90; 28885 } 28886 this.logger.log(2, () => "Special char '" + getCharForByte(oneCode) + "' in channel " + channelNr); 28887 charCodes = [oneCode]; 28888 } else if (a >= 0x20 && a <= 0x7f) { 28889 charCodes = b === 0 ? [a] : [a, b]; 28890 } 28891 if (charCodes) { 28892 this.logger.log(3, () => 'Char codes = ' + numArrayToHexArray(charCodes).join(',')); 28893 } 28894 return charCodes; 28895 } 28896 28897 /** 28898 * Parse extended background attributes as well as new foreground color black. 28899 * @returns True if background attributes are found 28900 */ 28901 parseBackgroundAttributes(a, b) { 28902 const case1 = (a === 0x10 || a === 0x18) && b >= 0x20 && b <= 0x2f; 28903 const case2 = (a === 0x17 || a === 0x1f) && b >= 0x2d && b <= 0x2f; 28904 if (!(case1 || case2)) { 28905 return false; 28906 } 28907 let index; 28908 const bkgData = {}; 28909 if (a === 0x10 || a === 0x18) { 28910 index = Math.floor((b - 0x20) / 2); 28911 bkgData.background = backgroundColors[index]; 28912 if (b % 2 === 1) { 28913 bkgData.background = bkgData.background + '_semi'; 28914 } 28915 } else if (b === 0x2d) { 28916 bkgData.background = 'transparent'; 28917 } else { 28918 bkgData.foreground = 'black'; 28919 if (b === 0x2f) { 28920 bkgData.underline = true; 28921 } 28922 } 28923 const chNr = a <= 0x17 ? 1 : 2; 28924 const channel = this.channels[chNr]; 28925 channel.setBkgData(bkgData); 28926 return true; 28927 } 28928 28929 /** 28930 * Reset state of parser and its channels. 28931 */ 28932 reset() { 28933 for (let i = 0; i < Object.keys(this.channels).length; i++) { 28934 const channel = this.channels[i]; 28935 if (channel) { 28936 channel.reset(); 28937 } 28938 } 28939 setLastCmd(null, null, this.cmdHistory); 28940 } 28941 28942 /** 28943 * Trigger the generation of a cue, and the start of a new one if displayScreens are not empty. 28944 */ 28945 cueSplitAtTime(t) { 28946 for (let i = 0; i < this.channels.length; i++) { 28947 const channel = this.channels[i]; 28948 if (channel) { 28949 channel.cueSplitAtTime(t); 28950 } 28951 } 28952 } 28953} 28954function setLastCmd(a, b, cmdHistory) { 28955 cmdHistory.a = a; 28956 cmdHistory.b = b; 28957} 28958function hasCmdRepeated(a, b, cmdHistory) { 28959 return cmdHistory.a === a && cmdHistory.b === b; 28960} 28961function createCmdHistory() { 28962 return { 28963 a: null, 28964 b: null 28965 }; 28966} 28967 28968/** 28969 * Copyright 2013 vtt.js Contributors 28970 * 28971 * Licensed under the Apache License, Version 2.0 (the 'License'); 28972 * you may not use this file except in compliance with the License. 28973 * You may obtain a copy of the License at 28974 * 28975 * http://www.apache.org/licenses/LICENSE-2.0 28976 * 28977 * Unless required by applicable law or agreed to in writing, software 28978 * distributed under the License is distributed on an 'AS IS' BASIS, 28979 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28980 * See the License for the specific language governing permissions and 28981 * limitations under the License. 28982 */ 28983 28984var VTTCue = (function () { 28985 if (optionalSelf != null && optionalSelf.VTTCue) { 28986 return self.VTTCue; 28987 } 28988 const AllowedDirections = ['', 'lr', 'rl']; 28989 const AllowedAlignments = ['start', 'middle', 'end', 'left', 'right']; 28990 function isAllowedValue(allowed, value) { 28991 if (typeof value !== 'string') { 28992 return false; 28993 } 28994 // necessary for assuring the generic conforms to the Array interface 28995 if (!Array.isArray(allowed)) { 28996 return false; 28997 } 28998 // reset the type so that the next narrowing works well 28999 const lcValue = value.toLowerCase(); 29000 // use the allow list to narrow the type to a specific subset of strings 29001 if (~allowed.indexOf(lcValue)) { 29002 return lcValue; 29003 } 29004 return false; 29005 } 29006 function findDirectionSetting(value) { 29007 return isAllowedValue(AllowedDirections, value); 29008 } 29009 function findAlignSetting(value) { 29010 return isAllowedValue(AllowedAlignments, value); 29011 } 29012 function extend(obj, ...rest) { 29013 let i = 1; 29014 for (; i < arguments.length; i++) { 29015 const cobj = arguments[i]; 29016 for (const p in cobj) { 29017 obj[p] = cobj[p]; 29018 } 29019 } 29020 return obj; 29021 } 29022 function VTTCue(startTime, endTime, text) { 29023 const cue = this; 29024 const baseObj = { 29025 enumerable: true 29026 }; 29027 /** 29028 * Shim implementation specific properties. These properties are not in 29029 * the spec. 29030 */ 29031 29032 // Lets us know when the VTTCue's data has changed in such a way that we need 29033 // to recompute its display state. This lets us compute its display state 29034 // lazily. 29035 cue.hasBeenReset = false; 29036 29037 /** 29038 * VTTCue and TextTrackCue properties 29039 * http://dev.w3.org/html5/webvtt/#vttcue-interface 29040 */ 29041 29042 let _id = ''; 29043 let _pauseOnExit = false; 29044 let _startTime = startTime; 29045 let _endTime = endTime; 29046 let _text = text; 29047 let _region = null; 29048 let _vertical = ''; 29049 let _snapToLines = true; 29050 let _line = 'auto'; 29051 let _lineAlign = 'start'; 29052 let _position = 50; 29053 let _positionAlign = 'middle'; 29054 let _size = 50; 29055 let _align = 'middle'; 29056 Object.defineProperty(cue, 'id', extend({}, baseObj, { 29057 get: function () { 29058 return _id; 29059 }, 29060 set: function (value) { 29061 _id = '' + value; 29062 } 29063 })); 29064 Object.defineProperty(cue, 'pauseOnExit', extend({}, baseObj, { 29065 get: function () { 29066 return _pauseOnExit; 29067 }, 29068 set: function (value) { 29069 _pauseOnExit = !!value; 29070 } 29071 })); 29072 Object.defineProperty(cue, 'startTime', extend({}, baseObj, { 29073 get: function () { 29074 return _startTime; 29075 }, 29076 set: function (value) { 29077 if (typeof value !== 'number') { 29078 throw new TypeError('Start time must be set to a number.'); 29079 } 29080 _startTime = value; 29081 this.hasBeenReset = true; 29082 } 29083 })); 29084 Object.defineProperty(cue, 'endTime', extend({}, baseObj, { 29085 get: function () { 29086 return _endTime; 29087 }, 29088 set: function (value) { 29089 if (typeof value !== 'number') { 29090 throw new TypeError('End time must be set to a number.'); 29091 } 29092 _endTime = value; 29093 this.hasBeenReset = true; 29094 } 29095 })); 29096 Object.defineProperty(cue, 'text', extend({}, baseObj, { 29097 get: function () { 29098 return _text; 29099 }, 29100 set: function (value) { 29101 _text = '' + value; 29102 this.hasBeenReset = true; 29103 } 29104 })); 29105 29106 // todo: implement VTTRegion polyfill? 29107 Object.defineProperty(cue, 'region', extend({}, baseObj, { 29108 get: function () { 29109 return _region; 29110 }, 29111 set: function (value) { 29112 _region = value; 29113 this.hasBeenReset = true; 29114 } 29115 })); 29116 Object.defineProperty(cue, 'vertical', extend({}, baseObj, { 29117 get: function () { 29118 return _vertical; 29119 }, 29120 set: function (value) { 29121 const setting = findDirectionSetting(value); 29122 // Have to check for false because the setting an be an empty string. 29123 if (setting === false) { 29124 throw new SyntaxError('An invalid or illegal string was specified.'); 29125 } 29126 _vertical = setting; 29127 this.hasBeenReset = true; 29128 } 29129 })); 29130 Object.defineProperty(cue, 'snapToLines', extend({}, baseObj, { 29131 get: function () { 29132 return _snapToLines; 29133 }, 29134 set: function (value) { 29135 _snapToLines = !!value; 29136 this.hasBeenReset = true; 29137 } 29138 })); 29139 Object.defineProperty(cue, 'line', extend({}, baseObj, { 29140 get: function () { 29141 return _line; 29142 }, 29143 set: function (value) { 29144 if (typeof value !== 'number' && value !== 'auto') { 29145 throw new SyntaxError('An invalid number or illegal string was specified.'); 29146 } 29147 _line = value; 29148 this.hasBeenReset = true; 29149 } 29150 })); 29151 Object.defineProperty(cue, 'lineAlign', extend({}, baseObj, { 29152 get: function () { 29153 return _lineAlign; 29154 }, 29155 set: function (value) { 29156 const setting = findAlignSetting(value); 29157 if (!setting) { 29158 throw new SyntaxError('An invalid or illegal string was specified.'); 29159 } 29160 _lineAlign = setting; 29161 this.hasBeenReset = true; 29162 } 29163 })); 29164 Object.defineProperty(cue, 'position', extend({}, baseObj, { 29165 get: function () { 29166 return _position; 29167 }, 29168 set: function (value) { 29169 if (value < 0 || value > 100) { 29170 throw new Error('Position must be between 0 and 100.'); 29171 } 29172 _position = value; 29173 this.hasBeenReset = true; 29174 } 29175 })); 29176 Object.defineProperty(cue, 'positionAlign', extend({}, baseObj, { 29177 get: function () { 29178 return _positionAlign; 29179 }, 29180 set: function (value) { 29181 const setting = findAlignSetting(value); 29182 if (!setting) { 29183 throw new SyntaxError('An invalid or illegal string was specified.'); 29184 } 29185 _positionAlign = setting; 29186 this.hasBeenReset = true; 29187 } 29188 })); 29189 Object.defineProperty(cue, 'size', extend({}, baseObj, { 29190 get: function () { 29191 return _size; 29192 }, 29193 set: function (value) { 29194 if (value < 0 || value > 100) { 29195 throw new Error('Size must be between 0 and 100.'); 29196 } 29197 _size = value; 29198 this.hasBeenReset = true; 29199 } 29200 })); 29201 Object.defineProperty(cue, 'align', extend({}, baseObj, { 29202 get: function () { 29203 return _align; 29204 }, 29205 set: function (value) { 29206 const setting = findAlignSetting(value); 29207 if (!setting) { 29208 throw new SyntaxError('An invalid or illegal string was specified.'); 29209 } 29210 _align = setting; 29211 this.hasBeenReset = true; 29212 } 29213 })); 29214 29215 /** 29216 * Other <track> spec defined properties 29217 */ 29218 29219 // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#text-track-cue-display-state 29220 cue.displayState = undefined; 29221 } 29222 29223 /** 29224 * VTTCue methods 29225 */ 29226 29227 VTTCue.prototype.getCueAsHTML = function () { 29228 // Assume WebVTT.convertCueToDOMTree is on the global. 29229 const WebVTT = self.WebVTT; 29230 return WebVTT.convertCueToDOMTree(self, this.text); 29231 }; 29232 // this is a polyfill hack 29233 return VTTCue; 29234})(); 29235 29236/* 29237 * Source: https://github.com/mozilla/vtt.js/blob/master/dist/vtt.js 29238 */ 29239 29240class StringDecoder { 29241 decode(data, options) { 29242 if (!data) { 29243 return ''; 29244 } 29245 if (typeof data !== 'string') { 29246 throw new Error('Error - expected string data.'); 29247 } 29248 return decodeURIComponent(encodeURIComponent(data)); 29249 } 29250} 29251 29252// Try to parse input as a time stamp. 29253function parseTimeStamp(input) { 29254 function computeSeconds(h, m, s, f) { 29255 return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + parseFloat(f || 0); 29256 } 29257 const m = input.match(/^(?:(\d+):)?(\d{2}):(\d{2})(\.\d+)?/); 29258 if (!m) { 29259 return null; 29260 } 29261 if (parseFloat(m[2]) > 59) { 29262 // Timestamp takes the form of [hours]:[minutes].[milliseconds] 29263 // First position is hours as it's over 59. 29264 return computeSeconds(m[2], m[3], 0, m[4]); 29265 } 29266 // Timestamp takes the form of [hours (optional)]:[minutes]:[seconds].[milliseconds] 29267 return computeSeconds(m[1], m[2], m[3], m[4]); 29268} 29269 29270// A settings object holds key/value pairs and will ignore anything but the first 29271// assignment to a specific key. 29272class Settings { 29273 constructor() { 29274 this.values = Object.create(null); 29275 } 29276 // Only accept the first assignment to any key. 29277 set(k, v) { 29278 if (!this.get(k) && v !== '') { 29279 this.values[k] = v; 29280 } 29281 } 29282 // Return the value for a key, or a default value. 29283 // If 'defaultKey' is passed then 'dflt' is assumed to be an object with 29284 // a number of possible default values as properties where 'defaultKey' is 29285 // the key of the property that will be chosen; otherwise it's assumed to be 29286 // a single value. 29287 get(k, dflt, defaultKey) { 29288 if (defaultKey) { 29289 return this.has(k) ? this.values[k] : dflt[defaultKey]; 29290 } 29291 return this.has(k) ? this.values[k] : dflt; 29292 } 29293 // Check whether we have a value for a key. 29294 has(k) { 29295 return k in this.values; 29296 } 29297 // Accept a setting if its one of the given alternatives. 29298 alt(k, v, a) { 29299 for (let n = 0; n < a.length; ++n) { 29300 if (v === a[n]) { 29301 this.set(k, v); 29302 break; 29303 } 29304 } 29305 } 29306 // Accept a setting if its a valid (signed) integer. 29307 integer(k, v) { 29308 if (/^-?\d+$/.test(v)) { 29309 // integer 29310 this.set(k, parseInt(v, 10)); 29311 } 29312 } 29313 // Accept a setting if its a valid percentage. 29314 percent(k, v) { 29315 if (/^([\d]{1,3})(\.[\d]*)?%$/.test(v)) { 29316 const percent = parseFloat(v); 29317 if (percent >= 0 && percent <= 100) { 29318 this.set(k, percent); 29319 return true; 29320 } 29321 } 29322 return false; 29323 } 29324} 29325 29326// Helper function to parse input into groups separated by 'groupDelim', and 29327// interpret each group as a key/value pair separated by 'keyValueDelim'. 29328function parseOptions(input, callback, keyValueDelim, groupDelim) { 29329 const groups = groupDelim ? input.split(groupDelim) : [input]; 29330 for (const i in groups) { 29331 if (typeof groups[i] !== 'string') { 29332 continue; 29333 } 29334 const kv = groups[i].split(keyValueDelim); 29335 if (kv.length !== 2) { 29336 continue; 29337 } 29338 const k = kv[0]; 29339 const v = kv[1]; 29340 callback(k, v); 29341 } 29342} 29343const defaults = new VTTCue(0, 0, ''); 29344// 'middle' was changed to 'center' in the spec: https://github.com/w3c/webvtt/pull/244 29345// Safari doesn't yet support this change, but FF and Chrome do. 29346const center = defaults.align === 'middle' ? 'middle' : 'center'; 29347function parseCue(input, cue, regionList) { 29348 // Remember the original input if we need to throw an error. 29349 const oInput = input; 29350 // 4.1 WebVTT timestamp 29351 function consumeTimeStamp() { 29352 const ts = parseTimeStamp(input); 29353 if (ts === null) { 29354 throw new Error('Malformed timestamp: ' + oInput); 29355 } 29356 29357 // Remove time stamp from input. 29358 input = input.replace(/^[^\sa-zA-Z-]+/, ''); 29359 return ts; 29360 } 29361 29362 // 4.4.2 WebVTT cue settings 29363 function consumeCueSettings(input, cue) { 29364 const settings = new Settings(); 29365 parseOptions(input, function (k, v) { 29366 let vals; 29367 switch (k) { 29368 case 'region': 29369 // Find the last region we parsed with the same region id. 29370 for (let i = regionList.length - 1; i >= 0; i--) { 29371 if (regionList[i].id === v) { 29372 settings.set(k, regionList[i].region); 29373 break; 29374 } 29375 } 29376 break; 29377 case 'vertical': 29378 settings.alt(k, v, ['rl', 'lr']); 29379 break; 29380 case 'line': 29381 vals = v.split(','); 29382 settings.integer(k, vals[0]); 29383 if (settings.percent(k, vals[0])) { 29384 settings.set('snapToLines', false); 29385 } 29386 settings.alt(k, vals[0], ['auto']); 29387 if (vals.length === 2) { 29388 settings.alt('lineAlign', vals[1], ['start', center, 'end']); 29389 } 29390 break; 29391 case 'position': 29392 vals = v.split(','); 29393 settings.percent(k, vals[0]); 29394 if (vals.length === 2) { 29395 settings.alt('positionAlign', vals[1], ['start', center, 'end', 'line-left', 'line-right', 'auto']); 29396 } 29397 break; 29398 case 'size': 29399 settings.percent(k, v); 29400 break; 29401 case 'align': 29402 settings.alt(k, v, ['start', center, 'end', 'left', 'right']); 29403 break; 29404 } 29405 }, /:/, /\s/); 29406 29407 // Apply default values for any missing fields. 29408 cue.region = settings.get('region', null); 29409 cue.vertical = settings.get('vertical', ''); 29410 let line = settings.get('line', 'auto'); 29411 if (line === 'auto' && defaults.line === -1) { 29412 // set numeric line number for Safari 29413 line = -1; 29414 } 29415 cue.line = line; 29416 cue.lineAlign = settings.get('lineAlign', 'start'); 29417 cue.snapToLines = settings.get('snapToLines', true); 29418 cue.size = settings.get('size', 100); 29419 cue.align = settings.get('align', center); 29420 let position = settings.get('position', 'auto'); 29421 if (position === 'auto' && defaults.position === 50) { 29422 // set numeric position for Safari 29423 position = cue.align === 'start' || cue.align === 'left' ? 0 : cue.align === 'end' || cue.align === 'right' ? 100 : 50; 29424 } 29425 cue.position = position; 29426 } 29427 function skipWhitespace() { 29428 input = input.replace(/^\s+/, ''); 29429 } 29430 29431 // 4.1 WebVTT cue timings. 29432 skipWhitespace(); 29433 cue.startTime = consumeTimeStamp(); // (1) collect cue start time 29434 skipWhitespace(); 29435 if (input.slice(0, 3) !== '-->') { 29436 // (3) next characters must match '-->' 29437 throw new Error("Malformed time stamp (time stamps must be separated by '-->'): " + oInput); 29438 } 29439 input = input.slice(3); 29440 skipWhitespace(); 29441 cue.endTime = consumeTimeStamp(); // (5) collect cue end time 29442 29443 // 4.1 WebVTT cue settings list. 29444 skipWhitespace(); 29445 consumeCueSettings(input, cue); 29446} 29447function fixLineBreaks(input) { 29448 return input.replace(/<br(?: \/)?>/gi, '\n'); 29449} 29450class VTTParser { 29451 constructor() { 29452 this.state = 'INITIAL'; 29453 this.buffer = ''; 29454 this.decoder = new StringDecoder(); 29455 this.regionList = []; 29456 this.cue = null; 29457 this.oncue = void 0; 29458 this.onparsingerror = void 0; 29459 this.onflush = void 0; 29460 } 29461 parse(data) { 29462 const _this = this; 29463 29464 // If there is no data then we won't decode it, but will just try to parse 29465 // whatever is in buffer already. This may occur in circumstances, for 29466 // example when flush() is called. 29467 if (data) { 29468 // Try to decode the data that we received. 29469 _this.buffer += _this.decoder.decode(data, { 29470 stream: true 29471 }); 29472 } 29473 function collectNextLine() { 29474 let buffer = _this.buffer; 29475 let pos = 0; 29476 buffer = fixLineBreaks(buffer); 29477 while (pos < buffer.length && buffer[pos] !== '\r' && buffer[pos] !== '\n') { 29478 ++pos; 29479 } 29480 const line = buffer.slice(0, pos); 29481 // Advance the buffer early in case we fail below. 29482 if (buffer[pos] === '\r') { 29483 ++pos; 29484 } 29485 if (buffer[pos] === '\n') { 29486 ++pos; 29487 } 29488 _this.buffer = buffer.slice(pos); 29489 return line; 29490 } 29491 29492 // 3.2 WebVTT metadata header syntax 29493 function parseHeader(input) { 29494 parseOptions(input, function (k, v) { 29495 // switch (k) { 29496 // case 'region': 29497 // 3.3 WebVTT region metadata header syntax 29498 // console.log('parse region', v); 29499 // parseRegion(v); 29500 // break; 29501 // } 29502 }, /:/); 29503 } 29504 29505 // 5.1 WebVTT file parsing. 29506 try { 29507 let line = ''; 29508 if (_this.state === 'INITIAL') { 29509 // We can't start parsing until we have the first line. 29510 if (!/\r\n|\n/.test(_this.buffer)) { 29511 return this; 29512 } 29513 line = collectNextLine(); 29514 // strip of UTF-8 BOM if any 29515 // https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8 29516 const m = line.match(/^()?WEBVTT([ \t].*)?$/); 29517 if (!(m != null && m[0])) { 29518 throw new Error('Malformed WebVTT signature.'); 29519 } 29520 _this.state = 'HEADER'; 29521 } 29522 let alreadyCollectedLine = false; 29523 while (_this.buffer) { 29524 // We can't parse a line until we have the full line. 29525 if (!/\r\n|\n/.test(_this.buffer)) { 29526 return this; 29527 } 29528 if (!alreadyCollectedLine) { 29529 line = collectNextLine(); 29530 } else { 29531 alreadyCollectedLine = false; 29532 } 29533 switch (_this.state) { 29534 case 'HEADER': 29535 // 13-18 - Allow a header (metadata) under the WEBVTT line. 29536 if (/:/.test(line)) { 29537 parseHeader(line); 29538 } else if (!line) { 29539 // An empty line terminates the header and starts the body (cues). 29540 _this.state = 'ID'; 29541 } 29542 continue; 29543 case 'NOTE': 29544 // Ignore NOTE blocks. 29545 if (!line) { 29546 _this.state = 'ID'; 29547 } 29548 continue; 29549 case 'ID': 29550 // Check for the start of NOTE blocks. 29551 if (/^NOTE($|[ \t])/.test(line)) { 29552 _this.state = 'NOTE'; 29553 break; 29554 } 29555 // 19-29 - Allow any number of line terminators, then initialize new cue values. 29556 if (!line) { 29557 continue; 29558 } 29559 _this.cue = new VTTCue(0, 0, ''); 29560 _this.state = 'CUE'; 29561 // 30-39 - Check if self line contains an optional identifier or timing data. 29562 if (line.indexOf('-->') === -1) { 29563 _this.cue.id = line; 29564 continue; 29565 } 29566 // Process line as start of a cue. 29567 /* falls through */ 29568 case 'CUE': 29569 // 40 - Collect cue timings and settings. 29570 if (!_this.cue) { 29571 _this.state = 'BADCUE'; 29572 continue; 29573 } 29574 try { 29575 parseCue(line, _this.cue, _this.regionList); 29576 } catch (e) { 29577 // In case of an error ignore rest of the cue. 29578 _this.cue = null; 29579 _this.state = 'BADCUE'; 29580 continue; 29581 } 29582 _this.state = 'CUETEXT'; 29583 continue; 29584 case 'CUETEXT': 29585 { 29586 const hasSubstring = line.indexOf('-->') !== -1; 29587 // 34 - If we have an empty line then report the cue. 29588 // 35 - If we have the special substring '-->' then report the cue, 29589 // but do not collect the line as we need to process the current 29590 // one as a new cue. 29591 if (!line || hasSubstring && (alreadyCollectedLine = true)) { 29592 // We are done parsing self cue. 29593 if (_this.oncue && _this.cue) { 29594 _this.oncue(_this.cue); 29595 } 29596 _this.cue = null; 29597 _this.state = 'ID'; 29598 continue; 29599 } 29600 if (_this.cue === null) { 29601 continue; 29602 } 29603 if (_this.cue.text) { 29604 _this.cue.text += '\n'; 29605 } 29606 _this.cue.text += line; 29607 } 29608 continue; 29609 case 'BADCUE': 29610 // 54-62 - Collect and discard the remaining cue. 29611 if (!line) { 29612 _this.state = 'ID'; 29613 } 29614 } 29615 } 29616 } catch (e) { 29617 // If we are currently parsing a cue, report what we have. 29618 if (_this.state === 'CUETEXT' && _this.cue && _this.oncue) { 29619 _this.oncue(_this.cue); 29620 } 29621 _this.cue = null; 29622 // Enter BADWEBVTT state if header was not parsed correctly otherwise 29623 // another exception occurred so enter BADCUE state. 29624 _this.state = _this.state === 'INITIAL' ? 'BADWEBVTT' : 'BADCUE'; 29625 } 29626 return this; 29627 } 29628 flush() { 29629 const _this = this; 29630 try { 29631 // Finish decoding the stream. 29632 // _this.buffer += _this.decoder.decode(); 29633 // Synthesize the end of the current cue or region. 29634 if (_this.cue || _this.state === 'HEADER') { 29635 _this.buffer += '\n\n'; 29636 _this.parse(); 29637 } 29638 // If we've flushed, parsed, and we're still on the INITIAL state then 29639 // that means we don't have enough of the stream to parse the first 29640 // line. 29641 if (_this.state === 'INITIAL' || _this.state === 'BADWEBVTT') { 29642 throw new Error('Malformed WebVTT signature.'); 29643 } 29644 } catch (e) { 29645 if (_this.onparsingerror) { 29646 _this.onparsingerror(e); 29647 } 29648 } 29649 if (_this.onflush) { 29650 _this.onflush(); 29651 } 29652 return this; 29653 } 29654} 29655 29656const LINEBREAKS = /\r\n|\n\r|\n|\r/g; 29657 29658// String.prototype.startsWith is not supported in IE11 29659const startsWith = function startsWith(inputString, searchString, position = 0) { 29660 return inputString.slice(position, position + searchString.length) === searchString; 29661}; 29662const cueString2millis = function cueString2millis(timeString) { 29663 let ts = parseInt(timeString.slice(-3)); 29664 const secs = parseInt(timeString.slice(-6, -4)); 29665 const mins = parseInt(timeString.slice(-9, -7)); 29666 const hours = timeString.length > 9 ? parseInt(timeString.substring(0, timeString.indexOf(':'))) : 0; 29667 if (!isFiniteNumber(ts) || !isFiniteNumber(secs) || !isFiniteNumber(mins) || !isFiniteNumber(hours)) { 29668 throw Error(`Malformed X-TIMESTAMP-MAP: Local:${timeString}`); 29669 } 29670 ts += 1000 * secs; 29671 ts += 60 * 1000 * mins; 29672 ts += 60 * 60 * 1000 * hours; 29673 return ts; 29674}; 29675 29676// Create a unique hash id for a cue based on start/end times and text. 29677// This helps timeline-controller to avoid showing repeated captions. 29678function generateCueId(startTime, endTime, text) { 29679 return hash(startTime.toString()) + hash(endTime.toString()) + hash(text); 29680} 29681const calculateOffset = function calculateOffset(vttCCs, cc, presentationTime) { 29682 let currCC = vttCCs[cc]; 29683 let prevCC = vttCCs[currCC.prevCC]; 29684 29685 // This is the first discontinuity or cues have been processed since the last discontinuity 29686 // Offset = current discontinuity time 29687 if (!prevCC || !prevCC.new && currCC.new) { 29688 vttCCs.ccOffset = vttCCs.presentationOffset = currCC.start; 29689 currCC.new = false; 29690 return; 29691 } 29692 29693 // There have been discontinuities since cues were last parsed. 29694 // Offset = time elapsed 29695 while ((_prevCC = prevCC) != null && _prevCC.new) { 29696 var _prevCC; 29697 vttCCs.ccOffset += currCC.start - prevCC.start; 29698 currCC.new = false; 29699 currCC = prevCC; 29700 prevCC = vttCCs[currCC.prevCC]; 29701 } 29702 vttCCs.presentationOffset = presentationTime; 29703}; 29704function parseWebVTT(vttByteArray, initPTS, vttCCs, cc, timeOffset, callBack, errorCallBack) { 29705 const parser = new VTTParser(); 29706 // Convert byteArray into string, replacing any somewhat exotic linefeeds with "\n", then split on that character. 29707 // Uint8Array.prototype.reduce is not implemented in IE11 29708 const vttLines = utf8ArrayToStr(new Uint8Array(vttByteArray)).trim().replace(LINEBREAKS, '\n').split('\n'); 29709 const cues = []; 29710 const init90kHz = initPTS ? toMpegTsClockFromTimescale(initPTS.baseTime, initPTS.timescale) : 0; 29711 let cueTime = '00:00.000'; 29712 let timestampMapMPEGTS = 0; 29713 let timestampMapLOCAL = 0; 29714 let parsingError; 29715 let inHeader = true; 29716 parser.oncue = function (cue) { 29717 // Adjust cue timing; clamp cues to start no earlier than - and drop cues that don't end after - 0 on timeline. 29718 const currCC = vttCCs[cc]; 29719 let cueOffset = vttCCs.ccOffset; 29720 29721 // Calculate subtitle PTS offset 29722 const webVttMpegTsMapOffset = (timestampMapMPEGTS - init90kHz) / 90000; 29723 29724 // Update offsets for new discontinuities 29725 if (currCC != null && currCC.new) { 29726 if (timestampMapLOCAL !== undefined) { 29727 // When local time is provided, offset = discontinuity start time - local time 29728 cueOffset = vttCCs.ccOffset = currCC.start; 29729 } else { 29730 calculateOffset(vttCCs, cc, webVttMpegTsMapOffset); 29731 } 29732 } 29733 if (webVttMpegTsMapOffset) { 29734 if (!initPTS) { 29735 parsingError = new Error('Missing initPTS for VTT MPEGTS'); 29736 return; 29737 } 29738 // If we have MPEGTS, offset = presentation time + discontinuity offset 29739 cueOffset = webVttMpegTsMapOffset - vttCCs.presentationOffset; 29740 } 29741 const duration = cue.endTime - cue.startTime; 29742 const startTime = normalizePts((cue.startTime + cueOffset - timestampMapLOCAL) * 90000, timeOffset * 90000) / 90000; 29743 cue.startTime = Math.max(startTime, 0); 29744 cue.endTime = Math.max(startTime + duration, 0); 29745 29746 //trim trailing webvtt block whitespaces 29747 const text = cue.text.trim(); 29748 29749 // Fix encoding of special characters 29750 cue.text = decodeURIComponent(encodeURIComponent(text)); 29751 29752 // If the cue was not assigned an id from the VTT file (line above the content), create one. 29753 if (!cue.id) { 29754 cue.id = generateCueId(cue.startTime, cue.endTime, text); 29755 } 29756 if (cue.endTime > 0) { 29757 cues.push(cue); 29758 } 29759 }; 29760 parser.onparsingerror = function (error) { 29761 parsingError = error; 29762 }; 29763 parser.onflush = function () { 29764 if (parsingError) { 29765 errorCallBack(parsingError); 29766 return; 29767 } 29768 callBack(cues); 29769 }; 29770 29771 // Go through contents line by line. 29772 vttLines.forEach(line => { 29773 if (inHeader) { 29774 // Look for X-TIMESTAMP-MAP in header. 29775 if (startsWith(line, 'X-TIMESTAMP-MAP=')) { 29776 // Once found, no more are allowed anyway, so stop searching. 29777 inHeader = false; 29778 // Extract LOCAL and MPEGTS. 29779 line.slice(16).split(',').forEach(timestamp => { 29780 if (startsWith(timestamp, 'LOCAL:')) { 29781 cueTime = timestamp.slice(6); 29782 } else if (startsWith(timestamp, 'MPEGTS:')) { 29783 timestampMapMPEGTS = parseInt(timestamp.slice(7)); 29784 } 29785 }); 29786 try { 29787 // Convert cue time to seconds 29788 timestampMapLOCAL = cueString2millis(cueTime) / 1000; 29789 } catch (error) { 29790 parsingError = error; 29791 } 29792 // Return without parsing X-TIMESTAMP-MAP line. 29793 return; 29794 } else if (line === '') { 29795 inHeader = false; 29796 } 29797 } 29798 // Parse line by default. 29799 parser.parse(line + '\n'); 29800 }); 29801 parser.flush(); 29802} 29803 29804const IMSC1_CODEC = 'stpp.ttml.im1t'; 29805 29806// Time format: h:m:s:frames(.subframes) 29807const HMSF_REGEX = /^(\d{2,}):(\d{2}):(\d{2}):(\d{2})\.?(\d+)?$/; 29808 29809// Time format: hours, minutes, seconds, milliseconds, frames, ticks 29810const TIME_UNIT_REGEX = /^(\d*(?:\.\d*)?)(h|m|s|ms|f|t)$/; 29811const textAlignToLineAlign = { 29812 left: 'start', 29813 center: 'center', 29814 right: 'end', 29815 start: 'start', 29816 end: 'end' 29817}; 29818function parseIMSC1(payload, initPTS, callBack, errorCallBack) { 29819 const results = findBox(new Uint8Array(payload), ['mdat']); 29820 if (results.length === 0) { 29821 errorCallBack(new Error('Could not parse IMSC1 mdat')); 29822 return; 29823 } 29824 const ttmlList = results.map(mdat => utf8ArrayToStr(mdat)); 29825 const syncTime = toTimescaleFromScale(initPTS.baseTime, 1, initPTS.timescale); 29826 try { 29827 ttmlList.forEach(ttml => callBack(parseTTML(ttml, syncTime))); 29828 } catch (error) { 29829 errorCallBack(error); 29830 } 29831} 29832function parseTTML(ttml, syncTime) { 29833 const parser = new DOMParser(); 29834 const xmlDoc = parser.parseFromString(ttml, 'text/xml'); 29835 const tt = xmlDoc.getElementsByTagName('tt')[0]; 29836 if (!tt) { 29837 throw new Error('Invalid ttml'); 29838 } 29839 const defaultRateInfo = { 29840 frameRate: 30, 29841 subFrameRate: 1, 29842 frameRateMultiplier: 0, 29843 tickRate: 0 29844 }; 29845 const rateInfo = Object.keys(defaultRateInfo).reduce((result, key) => { 29846 result[key] = tt.getAttribute(`ttp:${key}`) || defaultRateInfo[key]; 29847 return result; 29848 }, {}); 29849 const trim = tt.getAttribute('xml:space') !== 'preserve'; 29850 const styleElements = collectionToDictionary(getElementCollection(tt, 'styling', 'style')); 29851 const regionElements = collectionToDictionary(getElementCollection(tt, 'layout', 'region')); 29852 const cueElements = getElementCollection(tt, 'body', '[begin]'); 29853 return [].map.call(cueElements, cueElement => { 29854 const cueText = getTextContent(cueElement, trim); 29855 if (!cueText || !cueElement.hasAttribute('begin')) { 29856 return null; 29857 } 29858 const startTime = parseTtmlTime(cueElement.getAttribute('begin'), rateInfo); 29859 const duration = parseTtmlTime(cueElement.getAttribute('dur'), rateInfo); 29860 let endTime = parseTtmlTime(cueElement.getAttribute('end'), rateInfo); 29861 if (startTime === null) { 29862 throw timestampParsingError(cueElement); 29863 } 29864 if (endTime === null) { 29865 if (duration === null) { 29866 throw timestampParsingError(cueElement); 29867 } 29868 endTime = startTime + duration; 29869 } 29870 const cue = new VTTCue(startTime - syncTime, endTime - syncTime, cueText); 29871 cue.id = generateCueId(cue.startTime, cue.endTime, cue.text); 29872 const region = regionElements[cueElement.getAttribute('region')]; 29873 const style = styleElements[cueElement.getAttribute('style')]; 29874 29875 // Apply styles to cue 29876 const styles = getTtmlStyles(region, style, styleElements); 29877 const { 29878 textAlign 29879 } = styles; 29880 if (textAlign) { 29881 // cue.positionAlign not settable in FF~2016 29882 const lineAlign = textAlignToLineAlign[textAlign]; 29883 if (lineAlign) { 29884 cue.lineAlign = lineAlign; 29885 } 29886 cue.align = textAlign; 29887 } 29888 _extends(cue, styles); 29889 return cue; 29890 }).filter(cue => cue !== null); 29891} 29892function getElementCollection(fromElement, parentName, childName) { 29893 const parent = fromElement.getElementsByTagName(parentName)[0]; 29894 if (parent) { 29895 return [].slice.call(parent.querySelectorAll(childName)); 29896 } 29897 return []; 29898} 29899function collectionToDictionary(elementsWithId) { 29900 return elementsWithId.reduce((dict, element) => { 29901 const id = element.getAttribute('xml:id'); 29902 if (id) { 29903 dict[id] = element; 29904 } 29905 return dict; 29906 }, {}); 29907} 29908function getTextContent(element, trim) { 29909 return [].slice.call(element.childNodes).reduce((str, node, i) => { 29910 var _node$childNodes; 29911 if (node.nodeName === 'br' && i) { 29912 return str + '\n'; 29913 } 29914 if ((_node$childNodes = node.childNodes) != null && _node$childNodes.length) { 29915 return getTextContent(node, trim); 29916 } else if (trim) { 29917 return str + node.textContent.trim().replace(/\s+/g, ' '); 29918 } 29919 return str + node.textContent; 29920 }, ''); 29921} 29922function getTtmlStyles(region, style, styleElements) { 29923 const ttsNs = 'http://www.w3.org/ns/ttml#styling'; 29924 let regionStyle = null; 29925 const styleAttributes = ['displayAlign', 'textAlign', 'color', 'backgroundColor', 'fontSize', 'fontFamily' 29926 // 'fontWeight', 29927 // 'lineHeight', 29928 // 'wrapOption', 29929 // 'fontStyle', 29930 // 'direction', 29931 // 'writingMode' 29932 ]; 29933 const regionStyleName = region != null && region.hasAttribute('style') ? region.getAttribute('style') : null; 29934 if (regionStyleName && styleElements.hasOwnProperty(regionStyleName)) { 29935 regionStyle = styleElements[regionStyleName]; 29936 } 29937 return styleAttributes.reduce((styles, name) => { 29938 const value = getAttributeNS(style, ttsNs, name) || getAttributeNS(region, ttsNs, name) || getAttributeNS(regionStyle, ttsNs, name); 29939 if (value) { 29940 styles[name] = value; 29941 } 29942 return styles; 29943 }, {}); 29944} 29945function getAttributeNS(element, ns, name) { 29946 if (!element) { 29947 return null; 29948 } 29949 return element.hasAttributeNS(ns, name) ? element.getAttributeNS(ns, name) : null; 29950} 29951function timestampParsingError(node) { 29952 return new Error(`Could not parse ttml timestamp ${node}`); 29953} 29954function parseTtmlTime(timeAttributeValue, rateInfo) { 29955 if (!timeAttributeValue) { 29956 return null; 29957 } 29958 let seconds = parseTimeStamp(timeAttributeValue); 29959 if (seconds === null) { 29960 if (HMSF_REGEX.test(timeAttributeValue)) { 29961 seconds = parseHoursMinutesSecondsFrames(timeAttributeValue, rateInfo); 29962 } else if (TIME_UNIT_REGEX.test(timeAttributeValue)) { 29963 seconds = parseTimeUnits(timeAttributeValue, rateInfo); 29964 } 29965 } 29966 return seconds; 29967} 29968function parseHoursMinutesSecondsFrames(timeAttributeValue, rateInfo) { 29969 const m = HMSF_REGEX.exec(timeAttributeValue); 29970 const frames = (m[4] | 0) + (m[5] | 0) / rateInfo.subFrameRate; 29971 return (m[1] | 0) * 3600 + (m[2] | 0) * 60 + (m[3] | 0) + frames / rateInfo.frameRate; 29972} 29973function parseTimeUnits(timeAttributeValue, rateInfo) { 29974 const m = TIME_UNIT_REGEX.exec(timeAttributeValue); 29975 const value = Number(m[1]); 29976 const unit = m[2]; 29977 switch (unit) { 29978 case 'h': 29979 return value * 3600; 29980 case 'm': 29981 return value * 60; 29982 case 'ms': 29983 return value * 1000; 29984 case 'f': 29985 return value / rateInfo.frameRate; 29986 case 't': 29987 return value / rateInfo.tickRate; 29988 } 29989 return value; 29990} 29991 29992class OutputFilter { 29993 constructor(timelineController, trackName) { 29994 this.timelineController = void 0; 29995 this.cueRanges = []; 29996 this.trackName = void 0; 29997 this.startTime = null; 29998 this.endTime = null; 29999 this.screen = null; 30000 this.timelineController = timelineController; 30001 this.trackName = trackName; 30002 } 30003 dispatchCue() { 30004 if (this.startTime === null) { 30005 return; 30006 } 30007 this.timelineController.addCues(this.trackName, this.startTime, this.endTime, this.screen, this.cueRanges); 30008 this.startTime = null; 30009 } 30010 newCue(startTime, endTime, screen) { 30011 if (this.startTime === null || this.startTime > startTime) { 30012 this.startTime = startTime; 30013 } 30014 this.endTime = endTime; 30015 this.screen = screen; 30016 this.timelineController.createCaptionsTrack(this.trackName); 30017 } 30018 reset() { 30019 this.cueRanges = []; 30020 this.startTime = null; 30021 } 30022} 30023 30024class TimelineController { 30025 constructor(hls) { 30026 this.hls = void 0; 30027 this.media = null; 30028 this.config = void 0; 30029 this.enabled = true; 30030 this.Cues = void 0; 30031 this.textTracks = []; 30032 this.tracks = []; 30033 this.initPTS = []; 30034 this.unparsedVttFrags = []; 30035 this.captionsTracks = {}; 30036 this.nonNativeCaptionsTracks = {}; 30037 this.cea608Parser1 = void 0; 30038 this.cea608Parser2 = void 0; 30039 this.lastCc = -1; 30040 // Last video (CEA-608) fragment CC 30041 this.lastSn = -1; 30042 // Last video (CEA-608) fragment MSN 30043 this.lastPartIndex = -1; 30044 // Last video (CEA-608) fragment Part Index 30045 this.prevCC = -1; 30046 // Last subtitle fragment CC 30047 this.vttCCs = newVTTCCs(); 30048 this.captionsProperties = void 0; 30049 this.hls = hls; 30050 this.config = hls.config; 30051 this.Cues = hls.config.cueHandler; 30052 this.captionsProperties = { 30053 textTrack1: { 30054 label: this.config.captionsTextTrack1Label, 30055 languageCode: this.config.captionsTextTrack1LanguageCode 30056 }, 30057 textTrack2: { 30058 label: this.config.captionsTextTrack2Label, 30059 languageCode: this.config.captionsTextTrack2LanguageCode 30060 }, 30061 textTrack3: { 30062 label: this.config.captionsTextTrack3Label, 30063 languageCode: this.config.captionsTextTrack3LanguageCode 30064 }, 30065 textTrack4: { 30066 label: this.config.captionsTextTrack4Label, 30067 languageCode: this.config.captionsTextTrack4LanguageCode 30068 } 30069 }; 30070 hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 30071 hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 30072 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 30073 hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 30074 hls.on(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this); 30075 hls.on(Events.FRAG_LOADING, this.onFragLoading, this); 30076 hls.on(Events.FRAG_LOADED, this.onFragLoaded, this); 30077 hls.on(Events.FRAG_PARSING_USERDATA, this.onFragParsingUserdata, this); 30078 hls.on(Events.FRAG_DECRYPTED, this.onFragDecrypted, this); 30079 hls.on(Events.INIT_PTS_FOUND, this.onInitPtsFound, this); 30080 hls.on(Events.SUBTITLE_TRACKS_CLEARED, this.onSubtitleTracksCleared, this); 30081 hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 30082 } 30083 destroy() { 30084 const { 30085 hls 30086 } = this; 30087 hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 30088 hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 30089 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 30090 hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 30091 hls.off(Events.SUBTITLE_TRACKS_UPDATED, this.onSubtitleTracksUpdated, this); 30092 hls.off(Events.FRAG_LOADING, this.onFragLoading, this); 30093 hls.off(Events.FRAG_LOADED, this.onFragLoaded, this); 30094 hls.off(Events.FRAG_PARSING_USERDATA, this.onFragParsingUserdata, this); 30095 hls.off(Events.FRAG_DECRYPTED, this.onFragDecrypted, this); 30096 hls.off(Events.INIT_PTS_FOUND, this.onInitPtsFound, this); 30097 hls.off(Events.SUBTITLE_TRACKS_CLEARED, this.onSubtitleTracksCleared, this); 30098 hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 30099 // @ts-ignore 30100 this.hls = this.config = this.media = null; 30101 this.cea608Parser1 = this.cea608Parser2 = undefined; 30102 } 30103 initCea608Parsers() { 30104 const channel1 = new OutputFilter(this, 'textTrack1'); 30105 const channel2 = new OutputFilter(this, 'textTrack2'); 30106 const channel3 = new OutputFilter(this, 'textTrack3'); 30107 const channel4 = new OutputFilter(this, 'textTrack4'); 30108 this.cea608Parser1 = new Cea608Parser(1, channel1, channel2); 30109 this.cea608Parser2 = new Cea608Parser(3, channel3, channel4); 30110 } 30111 addCues(trackName, startTime, endTime, screen, cueRanges) { 30112 // skip cues which overlap more than 50% with previously parsed time ranges 30113 let merged = false; 30114 for (let i = cueRanges.length; i--;) { 30115 const cueRange = cueRanges[i]; 30116 const overlap = intersection(cueRange[0], cueRange[1], startTime, endTime); 30117 if (overlap >= 0) { 30118 cueRange[0] = Math.min(cueRange[0], startTime); 30119 cueRange[1] = Math.max(cueRange[1], endTime); 30120 merged = true; 30121 if (overlap / (endTime - startTime) > 0.5) { 30122 return; 30123 } 30124 } 30125 } 30126 if (!merged) { 30127 cueRanges.push([startTime, endTime]); 30128 } 30129 if (this.config.renderTextTracksNatively) { 30130 const track = this.captionsTracks[trackName]; 30131 this.Cues.newCue(track, startTime, endTime, screen); 30132 } else { 30133 const cues = this.Cues.newCue(null, startTime, endTime, screen); 30134 this.hls.trigger(Events.CUES_PARSED, { 30135 type: 'captions', 30136 cues, 30137 track: trackName 30138 }); 30139 } 30140 } 30141 30142 // Triggered when an initial PTS is found; used for synchronisation of WebVTT. 30143 onInitPtsFound(event, { 30144 frag, 30145 id, 30146 initPTS, 30147 timescale, 30148 trackId 30149 }) { 30150 const { 30151 unparsedVttFrags 30152 } = this; 30153 if (id === PlaylistLevelType.MAIN) { 30154 this.initPTS[frag.cc] = { 30155 baseTime: initPTS, 30156 timescale, 30157 trackId 30158 }; 30159 } 30160 30161 // Due to asynchronous processing, initial PTS may arrive later than the first VTT fragments are loaded. 30162 // Parse any unparsed fragments upon receiving the initial PTS. 30163 if (unparsedVttFrags.length) { 30164 this.unparsedVttFrags = []; 30165 unparsedVttFrags.forEach(data => { 30166 if (this.initPTS[data.frag.cc]) { 30167 this.onFragLoaded(Events.FRAG_LOADED, data); 30168 } else { 30169 this.hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { 30170 success: false, 30171 frag: data.frag, 30172 error: new Error('Subtitle discontinuity domain does not match main') 30173 }); 30174 } 30175 }); 30176 } 30177 } 30178 getExistingTrack(label, language) { 30179 const { 30180 media 30181 } = this; 30182 if (media) { 30183 for (let i = 0; i < media.textTracks.length; i++) { 30184 const textTrack = media.textTracks[i]; 30185 if (canReuseVttTextTrack(textTrack, { 30186 name: label, 30187 lang: language, 30188 characteristics: 'transcribes-spoken-dialog,describes-music-and-sound'})) { 30189 return textTrack; 30190 } 30191 } 30192 } 30193 return null; 30194 } 30195 createCaptionsTrack(trackName) { 30196 if (this.config.renderTextTracksNatively) { 30197 this.createNativeTrack(trackName); 30198 } else { 30199 this.createNonNativeTrack(trackName); 30200 } 30201 } 30202 createNativeTrack(trackName) { 30203 if (this.captionsTracks[trackName]) { 30204 return; 30205 } 30206 const { 30207 captionsProperties, 30208 captionsTracks, 30209 media 30210 } = this; 30211 const { 30212 label, 30213 languageCode 30214 } = captionsProperties[trackName]; 30215 // Enable reuse of existing text track. 30216 const existingTrack = this.getExistingTrack(label, languageCode); 30217 if (!existingTrack) { 30218 const textTrack = this.createTextTrack('captions', label, languageCode); 30219 if (textTrack) { 30220 // Set a special property on the track so we know it's managed by Hls.js 30221 textTrack[trackName] = true; 30222 captionsTracks[trackName] = textTrack; 30223 } 30224 } else { 30225 captionsTracks[trackName] = existingTrack; 30226 clearCurrentCues(captionsTracks[trackName]); 30227 sendAddTrackEvent(captionsTracks[trackName], media); 30228 } 30229 } 30230 createNonNativeTrack(trackName) { 30231 if (this.nonNativeCaptionsTracks[trackName]) { 30232 return; 30233 } 30234 // Create a list of a single track for the provider to consume 30235 const trackProperties = this.captionsProperties[trackName]; 30236 if (!trackProperties) { 30237 return; 30238 } 30239 const label = trackProperties.label; 30240 const track = { 30241 _id: trackName, 30242 label, 30243 kind: 'captions', 30244 default: trackProperties.media ? !!trackProperties.media.default : false, 30245 closedCaptions: trackProperties.media 30246 }; 30247 this.nonNativeCaptionsTracks[trackName] = track; 30248 this.hls.trigger(Events.NON_NATIVE_TEXT_TRACKS_FOUND, { 30249 tracks: [track] 30250 }); 30251 } 30252 createTextTrack(kind, label, lang) { 30253 const media = this.media; 30254 if (!media) { 30255 return; 30256 } 30257 return media.addTextTrack(kind, label, lang); 30258 } 30259 onMediaAttaching(event, data) { 30260 this.media = data.media; 30261 if (!data.mediaSource) { 30262 this._cleanTracks(); 30263 } 30264 } 30265 onMediaDetaching(event, data) { 30266 const transferringMedia = !!data.transferMedia; 30267 this.media = null; 30268 if (transferringMedia) { 30269 return; 30270 } 30271 const { 30272 captionsTracks 30273 } = this; 30274 Object.keys(captionsTracks).forEach(trackName => { 30275 clearCurrentCues(captionsTracks[trackName]); 30276 delete captionsTracks[trackName]; 30277 }); 30278 this.nonNativeCaptionsTracks = {}; 30279 } 30280 onManifestLoading() { 30281 // Detect discontinuity in video fragment (CEA-608) parsing 30282 this.lastCc = -1; 30283 this.lastSn = -1; 30284 this.lastPartIndex = -1; 30285 // Detect discontinuity in subtitle manifests 30286 this.prevCC = -1; 30287 this.vttCCs = newVTTCCs(); 30288 // Reset tracks 30289 this._cleanTracks(); 30290 this.tracks = []; 30291 this.captionsTracks = {}; 30292 this.nonNativeCaptionsTracks = {}; 30293 this.textTracks = []; 30294 this.unparsedVttFrags = []; 30295 this.initPTS = []; 30296 if (this.cea608Parser1 && this.cea608Parser2) { 30297 this.cea608Parser1.reset(); 30298 this.cea608Parser2.reset(); 30299 } 30300 } 30301 _cleanTracks() { 30302 // clear outdated subtitles 30303 const { 30304 media 30305 } = this; 30306 if (!media) { 30307 return; 30308 } 30309 const textTracks = media.textTracks; 30310 if (textTracks) { 30311 for (let i = 0; i < textTracks.length; i++) { 30312 clearCurrentCues(textTracks[i]); 30313 } 30314 } 30315 } 30316 onSubtitleTracksUpdated(event, data) { 30317 const tracks = data.subtitleTracks || []; 30318 const hasIMSC1 = tracks.some(track => track.textCodec === IMSC1_CODEC); 30319 if (this.config.enableWebVTT || hasIMSC1 && this.config.enableIMSC1) { 30320 const listIsIdentical = subtitleOptionsIdentical(this.tracks, tracks); 30321 if (listIsIdentical) { 30322 this.tracks = tracks; 30323 return; 30324 } 30325 this.textTracks = []; 30326 this.tracks = tracks; 30327 if (this.config.renderTextTracksNatively) { 30328 const media = this.media; 30329 const inUseTracks = media ? filterSubtitleTracks(media.textTracks) : null; 30330 this.tracks.forEach((track, index) => { 30331 // Reuse tracks with the same label and lang, but do not reuse 608/708 tracks 30332 let textTrack; 30333 if (inUseTracks) { 30334 let inUseTrack = null; 30335 for (let i = 0; i < inUseTracks.length; i++) { 30336 if (inUseTracks[i] && canReuseVttTextTrack(inUseTracks[i], track)) { 30337 inUseTrack = inUseTracks[i]; 30338 inUseTracks[i] = null; 30339 break; 30340 } 30341 } 30342 if (inUseTrack) { 30343 textTrack = inUseTrack; 30344 } 30345 } 30346 if (textTrack) { 30347 clearCurrentCues(textTrack); 30348 } else { 30349 const textTrackKind = captionsOrSubtitlesFromCharacteristics(track); 30350 textTrack = this.createTextTrack(textTrackKind, track.name, track.lang); 30351 if (textTrack) { 30352 textTrack.mode = 'disabled'; 30353 } 30354 } 30355 if (textTrack) { 30356 this.textTracks.push(textTrack); 30357 } 30358 }); 30359 // Warn when video element has captions or subtitle TextTracks carried over from another source 30360 if (inUseTracks != null && inUseTracks.length) { 30361 const unusedTextTracks = inUseTracks.filter(t => t !== null).map(t => t.label); 30362 if (unusedTextTracks.length) { 30363 this.hls.logger.warn(`Media element contains unused subtitle tracks: ${unusedTextTracks.join(', ')}. Replace media element for each source to clear TextTracks and captions menu.`); 30364 } 30365 } 30366 } else if (this.tracks.length) { 30367 // Create a list of tracks for the provider to consume 30368 const tracksList = this.tracks.map(track => { 30369 return { 30370 label: track.name, 30371 kind: track.type.toLowerCase(), 30372 default: track.default, 30373 subtitleTrack: track 30374 }; 30375 }); 30376 this.hls.trigger(Events.NON_NATIVE_TEXT_TRACKS_FOUND, { 30377 tracks: tracksList 30378 }); 30379 } 30380 } 30381 } 30382 onManifestLoaded(event, data) { 30383 if (this.config.enableCEA708Captions && data.captions) { 30384 data.captions.forEach(captionsTrack => { 30385 const instreamIdMatch = /(?:CC|SERVICE)([1-4])/.exec(captionsTrack.instreamId); 30386 if (!instreamIdMatch) { 30387 return; 30388 } 30389 const trackName = `textTrack${instreamIdMatch[1]}`; 30390 const trackProperties = this.captionsProperties[trackName]; 30391 if (!trackProperties) { 30392 return; 30393 } 30394 trackProperties.label = captionsTrack.name; 30395 if (captionsTrack.lang) { 30396 // optional attribute 30397 trackProperties.languageCode = captionsTrack.lang; 30398 } 30399 trackProperties.media = captionsTrack; 30400 }); 30401 } 30402 } 30403 closedCaptionsForLevel(frag) { 30404 const level = this.hls.levels[frag.level]; 30405 return level == null ? void 0 : level.attrs['CLOSED-CAPTIONS']; 30406 } 30407 onFragLoading(event, data) { 30408 // if this frag isn't contiguous, clear the parser so cues with bad start/end times aren't added to the textTrack 30409 if (this.enabled && data.frag.type === PlaylistLevelType.MAIN) { 30410 var _data$part$index, _data$part; 30411 const { 30412 cea608Parser1, 30413 cea608Parser2, 30414 lastSn 30415 } = this; 30416 const { 30417 cc, 30418 sn 30419 } = data.frag; 30420 const partIndex = (_data$part$index = (_data$part = data.part) == null ? void 0 : _data$part.index) != null ? _data$part$index : -1; 30421 if (cea608Parser1 && cea608Parser2) { 30422 if (sn !== lastSn + 1 || sn === lastSn && partIndex !== this.lastPartIndex + 1 || cc !== this.lastCc) { 30423 cea608Parser1.reset(); 30424 cea608Parser2.reset(); 30425 } 30426 } 30427 this.lastCc = cc; 30428 this.lastSn = sn; 30429 this.lastPartIndex = partIndex; 30430 } 30431 } 30432 onFragLoaded(event, data) { 30433 const { 30434 frag, 30435 payload 30436 } = data; 30437 if (frag.type === PlaylistLevelType.SUBTITLE) { 30438 // If fragment is subtitle type, parse as WebVTT. 30439 if (payload.byteLength) { 30440 const decryptData = frag.decryptdata; 30441 // fragment after decryption has a stats object 30442 const decrypted = 'stats' in data; 30443 // If the subtitles are not encrypted, parse VTTs now. Otherwise, we need to wait. 30444 if (decryptData == null || !decryptData.encrypted || decrypted) { 30445 const trackPlaylistMedia = this.tracks[frag.level]; 30446 const vttCCs = this.vttCCs; 30447 if (!vttCCs[frag.cc]) { 30448 vttCCs[frag.cc] = { 30449 start: frag.start, 30450 prevCC: this.prevCC, 30451 new: true 30452 }; 30453 this.prevCC = frag.cc; 30454 } 30455 if (trackPlaylistMedia && trackPlaylistMedia.textCodec === IMSC1_CODEC) { 30456 this._parseIMSC1(frag, payload); 30457 } else { 30458 this._parseVTTs(data); 30459 } 30460 } 30461 } else { 30462 // In case there is no payload, finish unsuccessfully. 30463 this.hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { 30464 success: false, 30465 frag, 30466 error: new Error('Empty subtitle payload') 30467 }); 30468 } 30469 } 30470 } 30471 _parseIMSC1(frag, payload) { 30472 const hls = this.hls; 30473 parseIMSC1(payload, this.initPTS[frag.cc], cues => { 30474 this._appendCues(cues, frag.level); 30475 hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { 30476 success: true, 30477 frag: frag 30478 }); 30479 }, error => { 30480 hls.logger.log(`Failed to parse IMSC1: ${error}`); 30481 hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { 30482 success: false, 30483 frag: frag, 30484 error 30485 }); 30486 }); 30487 } 30488 _parseVTTs(data) { 30489 var _frag$initSegment; 30490 const { 30491 frag, 30492 payload 30493 } = data; 30494 // We need an initial synchronisation PTS. Store fragments as long as none has arrived 30495 const { 30496 initPTS, 30497 unparsedVttFrags 30498 } = this; 30499 const maxAvCC = initPTS.length - 1; 30500 if (!initPTS[frag.cc] && maxAvCC === -1) { 30501 unparsedVttFrags.push(data); 30502 return; 30503 } 30504 const hls = this.hls; 30505 // Parse the WebVTT file contents. 30506 const payloadWebVTT = (_frag$initSegment = frag.initSegment) != null && _frag$initSegment.data ? appendUint8Array(frag.initSegment.data, new Uint8Array(payload)).buffer : payload; 30507 parseWebVTT(payloadWebVTT, this.initPTS[frag.cc], this.vttCCs, frag.cc, frag.start, cues => { 30508 this._appendCues(cues, frag.level); 30509 hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { 30510 success: true, 30511 frag: frag 30512 }); 30513 }, error => { 30514 const missingInitPTS = error.message === 'Missing initPTS for VTT MPEGTS'; 30515 if (missingInitPTS) { 30516 unparsedVttFrags.push(data); 30517 } else { 30518 this._fallbackToIMSC1(frag, payload); 30519 } 30520 // Something went wrong while parsing. Trigger event with success false. 30521 hls.logger.log(`Failed to parse VTT cue: ${error}`); 30522 if (missingInitPTS && maxAvCC > frag.cc) { 30523 return; 30524 } 30525 hls.trigger(Events.SUBTITLE_FRAG_PROCESSED, { 30526 success: false, 30527 frag: frag, 30528 error 30529 }); 30530 }); 30531 } 30532 _fallbackToIMSC1(frag, payload) { 30533 // If textCodec is unknown, try parsing as IMSC1. Set textCodec based on the result 30534 const trackPlaylistMedia = this.tracks[frag.level]; 30535 if (!trackPlaylistMedia.textCodec) { 30536 parseIMSC1(payload, this.initPTS[frag.cc], () => { 30537 trackPlaylistMedia.textCodec = IMSC1_CODEC; 30538 this._parseIMSC1(frag, payload); 30539 }, () => { 30540 trackPlaylistMedia.textCodec = 'wvtt'; 30541 }); 30542 } 30543 } 30544 _appendCues(cues, fragLevel) { 30545 const hls = this.hls; 30546 if (this.config.renderTextTracksNatively) { 30547 const textTrack = this.textTracks[fragLevel]; 30548 // WebVTTParser.parse is an async method and if the currently selected text track mode is set to "disabled" 30549 // before parsing is done then don't try to access currentTrack.cues.getCueById as cues will be null 30550 // and trying to access getCueById method of cues will throw an exception 30551 // Because we check if the mode is disabled, we can force check `cues` below. They can't be null. 30552 if (!textTrack || textTrack.mode === 'disabled') { 30553 return; 30554 } 30555 cues.forEach(cue => addCueToTrack(textTrack, cue)); 30556 } else { 30557 const currentTrack = this.tracks[fragLevel]; 30558 if (!currentTrack) { 30559 return; 30560 } 30561 const track = currentTrack.default ? 'default' : 'subtitles' + fragLevel; 30562 hls.trigger(Events.CUES_PARSED, { 30563 type: 'subtitles', 30564 cues, 30565 track 30566 }); 30567 } 30568 } 30569 onFragDecrypted(event, data) { 30570 const { 30571 frag 30572 } = data; 30573 if (frag.type === PlaylistLevelType.SUBTITLE) { 30574 this.onFragLoaded(Events.FRAG_LOADED, data); 30575 } 30576 } 30577 onSubtitleTracksCleared() { 30578 this.tracks = []; 30579 this.captionsTracks = {}; 30580 } 30581 onFragParsingUserdata(event, data) { 30582 if (!this.enabled || !this.config.enableCEA708Captions) { 30583 return; 30584 } 30585 const { 30586 frag, 30587 samples 30588 } = data; 30589 if (frag.type === PlaylistLevelType.MAIN && this.closedCaptionsForLevel(frag) === 'NONE') { 30590 return; 30591 } 30592 // If the event contains captions (found in the bytes property), push all bytes into the parser immediately 30593 // It will create the proper timestamps based on the PTS value 30594 for (let i = 0; i < samples.length; i++) { 30595 const ccBytes = samples[i].bytes; 30596 if (ccBytes) { 30597 if (!this.cea608Parser1) { 30598 this.initCea608Parsers(); 30599 } 30600 const ccdatas = this.extractCea608Data(ccBytes); 30601 this.cea608Parser1.addData(samples[i].pts, ccdatas[0]); 30602 this.cea608Parser2.addData(samples[i].pts, ccdatas[1]); 30603 } 30604 } 30605 } 30606 onBufferFlushing(event, { 30607 startOffset, 30608 endOffset, 30609 endOffsetSubtitles, 30610 type 30611 }) { 30612 const { 30613 media 30614 } = this; 30615 if (!media || media.currentTime < endOffset) { 30616 return; 30617 } 30618 // Clear 608 caption cues from the captions TextTracks when the video back buffer is flushed 30619 // Forward cues are never removed because we can loose streamed 608 content from recent fragments 30620 if (!type || type === 'video') { 30621 const { 30622 captionsTracks 30623 } = this; 30624 Object.keys(captionsTracks).forEach(trackName => removeCuesInRange(captionsTracks[trackName], startOffset, endOffset)); 30625 } 30626 if (this.config.renderTextTracksNatively) { 30627 // Clear VTT/IMSC1 subtitle cues from the subtitle TextTracks when the back buffer is flushed 30628 if (startOffset === 0 && endOffsetSubtitles !== undefined) { 30629 const { 30630 textTracks 30631 } = this; 30632 Object.keys(textTracks).forEach(trackName => removeCuesInRange(textTracks[trackName], startOffset, endOffsetSubtitles)); 30633 } 30634 } 30635 } 30636 extractCea608Data(byteArray) { 30637 const actualCCBytes = [[], []]; 30638 const count = byteArray[0] & 0x1f; 30639 let position = 2; 30640 for (let j = 0; j < count; j++) { 30641 const tmpByte = byteArray[position++]; 30642 const ccbyte1 = 0x7f & byteArray[position++]; 30643 const ccbyte2 = 0x7f & byteArray[position++]; 30644 if (ccbyte1 === 0 && ccbyte2 === 0) { 30645 continue; 30646 } 30647 const ccValid = (0x04 & tmpByte) !== 0; // Support all four channels 30648 if (ccValid) { 30649 const ccType = 0x03 & tmpByte; 30650 if (0x00 /* CEA608 field1*/ === ccType || 0x01 /* CEA608 field2*/ === ccType) { 30651 // Exclude CEA708 CC data. 30652 actualCCBytes[ccType].push(ccbyte1); 30653 actualCCBytes[ccType].push(ccbyte2); 30654 } 30655 } 30656 } 30657 return actualCCBytes; 30658 } 30659} 30660function captionsOrSubtitlesFromCharacteristics(track) { 30661 if (track.characteristics) { 30662 if (/transcribes-spoken-dialog/gi.test(track.characteristics) && /describes-music-and-sound/gi.test(track.characteristics)) { 30663 return 'captions'; 30664 } 30665 } 30666 return 'subtitles'; 30667} 30668function canReuseVttTextTrack(inUseTrack, manifestTrack) { 30669 return !!inUseTrack && inUseTrack.kind === captionsOrSubtitlesFromCharacteristics(manifestTrack) && subtitleTrackMatchesTextTrack(manifestTrack, inUseTrack); 30670} 30671function intersection(x1, x2, y1, y2) { 30672 return Math.min(x2, y2) - Math.max(x1, y1); 30673} 30674function newVTTCCs() { 30675 return { 30676 ccOffset: 0, 30677 presentationOffset: 0, 30678 0: { 30679 start: 0, 30680 prevCC: -1, 30681 new: true 30682 } 30683 }; 30684} 30685 30686const WHITESPACE_CHAR = /\s/; 30687const Cues = { 30688 newCue(track, startTime, endTime, captionScreen) { 30689 const result = []; 30690 let row; 30691 // the type data states this is VTTCue, but it can potentially be a TextTrackCue on old browsers 30692 let cue; 30693 let indenting; 30694 let indent; 30695 let text; 30696 const Cue = self.VTTCue || self.TextTrackCue; 30697 for (let r = 0; r < captionScreen.rows.length; r++) { 30698 row = captionScreen.rows[r]; 30699 indenting = true; 30700 indent = 0; 30701 text = ''; 30702 if (!row.isEmpty()) { 30703 var _track$cues; 30704 for (let c = 0; c < row.chars.length; c++) { 30705 if (WHITESPACE_CHAR.test(row.chars[c].uchar) && indenting) { 30706 indent++; 30707 } else { 30708 text += row.chars[c].uchar; 30709 indenting = false; 30710 } 30711 } 30712 // To be used for cleaning-up orphaned roll-up captions 30713 row.cueStartTime = startTime; 30714 30715 // Give a slight bump to the endTime if it's equal to startTime to avoid a SyntaxError in IE 30716 if (startTime === endTime) { 30717 endTime += 0.0001; 30718 } 30719 if (indent >= 16) { 30720 indent--; 30721 } else { 30722 indent++; 30723 } 30724 const cueText = fixLineBreaks(text.trim()); 30725 const id = generateCueId(startTime, endTime, cueText); 30726 30727 // If this cue already exists in the track do not push it 30728 if (!(track != null && (_track$cues = track.cues) != null && _track$cues.getCueById(id))) { 30729 cue = new Cue(startTime, endTime, cueText); 30730 cue.id = id; 30731 cue.line = r + 1; 30732 cue.align = 'left'; 30733 // Clamp the position between 10 and 80 percent (CEA-608 PAC indent code) 30734 // https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#positioning-in-cea-608 30735 // Firefox throws an exception and captions break with out of bounds 0-100 values 30736 cue.position = 10 + Math.min(80, Math.floor(indent * 8 / 32) * 10); 30737 result.push(cue); 30738 } 30739 } 30740 } 30741 if (track && result.length) { 30742 // Sort bottom cues in reverse order so that they render in line order when overlapping in Chrome 30743 result.sort((cueA, cueB) => { 30744 if (cueA.line === 'auto' || cueB.line === 'auto') { 30745 return 0; 30746 } 30747 if (cueA.line > 8 && cueB.line > 8) { 30748 return cueB.line - cueA.line; 30749 } 30750 return cueA.line - cueB.line; 30751 }); 30752 result.forEach(cue => addCueToTrack(track, cue)); 30753 } 30754 return result; 30755 } 30756}; 30757 30758function fetchSupported() { 30759 if ( 30760 // @ts-ignore 30761 self.fetch && self.AbortController && self.ReadableStream && self.Request) { 30762 try { 30763 new self.ReadableStream({}); // eslint-disable-line no-new 30764 return true; 30765 } catch (e) { 30766 /* noop */ 30767 } 30768 } 30769 return false; 30770} 30771const BYTERANGE = /(\d+)-(\d+)\/(\d+)/; 30772class FetchLoader { 30773 constructor(config) { 30774 this.fetchSetup = void 0; 30775 this.requestTimeout = void 0; 30776 this.request = null; 30777 this.response = null; 30778 this.controller = void 0; 30779 this.context = null; 30780 this.config = null; 30781 this.callbacks = null; 30782 this.stats = void 0; 30783 this.loader = null; 30784 this.fetchSetup = config.fetchSetup || getRequest; 30785 this.controller = new self.AbortController(); 30786 this.stats = new LoadStats(); 30787 } 30788 destroy() { 30789 this.loader = this.callbacks = this.context = this.config = this.request = null; 30790 this.abortInternal(); 30791 this.response = null; 30792 // @ts-ignore 30793 this.fetchSetup = this.controller = this.stats = null; 30794 } 30795 abortInternal() { 30796 if (this.controller && !this.stats.loading.end) { 30797 this.stats.aborted = true; 30798 this.controller.abort(); 30799 } 30800 } 30801 abort() { 30802 var _this$callbacks; 30803 this.abortInternal(); 30804 if ((_this$callbacks = this.callbacks) != null && _this$callbacks.onAbort) { 30805 this.callbacks.onAbort(this.stats, this.context, this.response); 30806 } 30807 } 30808 load(context, config, callbacks) { 30809 const stats = this.stats; 30810 if (stats.loading.start) { 30811 throw new Error('Loader can only be used once.'); 30812 } 30813 stats.loading.start = self.performance.now(); 30814 const initParams = getRequestParameters(context, this.controller.signal); 30815 const isArrayBuffer = context.responseType === 'arraybuffer'; 30816 const LENGTH = isArrayBuffer ? 'byteLength' : 'length'; 30817 const { 30818 maxTimeToFirstByteMs, 30819 maxLoadTimeMs 30820 } = config.loadPolicy; 30821 this.context = context; 30822 this.config = config; 30823 this.callbacks = callbacks; 30824 this.request = this.fetchSetup(context, initParams); 30825 self.clearTimeout(this.requestTimeout); 30826 config.timeout = maxTimeToFirstByteMs && isFiniteNumber(maxTimeToFirstByteMs) ? maxTimeToFirstByteMs : maxLoadTimeMs; 30827 this.requestTimeout = self.setTimeout(() => { 30828 if (this.callbacks) { 30829 this.abortInternal(); 30830 this.callbacks.onTimeout(stats, context, this.response); 30831 } 30832 }, config.timeout); 30833 const fetchPromise = isPromise(this.request) ? this.request.then(self.fetch) : self.fetch(this.request); 30834 fetchPromise.then(response => { 30835 var _this$callbacks2; 30836 this.response = this.loader = response; 30837 const first = Math.max(self.performance.now(), stats.loading.start); 30838 self.clearTimeout(this.requestTimeout); 30839 config.timeout = maxLoadTimeMs; 30840 this.requestTimeout = self.setTimeout(() => { 30841 if (this.callbacks) { 30842 this.abortInternal(); 30843 this.callbacks.onTimeout(stats, context, this.response); 30844 } 30845 }, maxLoadTimeMs - (first - stats.loading.start)); 30846 if (!response.ok) { 30847 const { 30848 status, 30849 statusText 30850 } = response; 30851 throw new FetchError(statusText || 'fetch, bad network response', status, response); 30852 } 30853 stats.loading.first = first; 30854 stats.total = getContentLength(response.headers) || stats.total; 30855 const onProgress = (_this$callbacks2 = this.callbacks) == null ? void 0 : _this$callbacks2.onProgress; 30856 if (onProgress && isFiniteNumber(config.highWaterMark)) { 30857 return this.loadProgressively(response, stats, context, config.highWaterMark, onProgress); 30858 } 30859 if (isArrayBuffer) { 30860 return response.arrayBuffer(); 30861 } 30862 if (context.responseType === 'json') { 30863 return response.json(); 30864 } 30865 return response.text(); 30866 }).then(responseData => { 30867 var _this$callbacks3, _this$callbacks4; 30868 const response = this.response; 30869 if (!response) { 30870 throw new Error('loader destroyed'); 30871 } 30872 self.clearTimeout(this.requestTimeout); 30873 stats.loading.end = Math.max(self.performance.now(), stats.loading.first); 30874 const total = responseData[LENGTH]; 30875 if (total) { 30876 stats.loaded = stats.total = total; 30877 } 30878 const loaderResponse = { 30879 url: response.url, 30880 data: responseData, 30881 code: response.status 30882 }; 30883 const onProgress = (_this$callbacks3 = this.callbacks) == null ? void 0 : _this$callbacks3.onProgress; 30884 if (onProgress && !isFiniteNumber(config.highWaterMark)) { 30885 onProgress(stats, context, responseData, response); 30886 } 30887 (_this$callbacks4 = this.callbacks) == null || _this$callbacks4.onSuccess(loaderResponse, stats, context, response); 30888 }).catch(error => { 30889 var _this$callbacks5; 30890 self.clearTimeout(this.requestTimeout); 30891 if (stats.aborted) { 30892 return; 30893 } 30894 // CORS errors result in an undefined code. Set it to 0 here to align with XHR's behavior 30895 // when destroying, 'error' itself can be undefined 30896 const code = !error ? 0 : error.code || 0; 30897 const text = !error ? null : error.message; 30898 (_this$callbacks5 = this.callbacks) == null || _this$callbacks5.onError({ 30899 code, 30900 text 30901 }, context, error ? error.details : null, stats); 30902 }); 30903 } 30904 getCacheAge() { 30905 let result = null; 30906 if (this.response) { 30907 const ageHeader = this.response.headers.get('age'); 30908 result = ageHeader ? parseFloat(ageHeader) : null; 30909 } 30910 return result; 30911 } 30912 getResponseHeader(name) { 30913 return this.response ? this.response.headers.get(name) : null; 30914 } 30915 loadProgressively(response, stats, context, highWaterMark = 0, onProgress) { 30916 const chunkCache = new ChunkCache(); 30917 const reader = response.body.getReader(); 30918 const pump = () => { 30919 return reader.read().then(data => { 30920 if (data.done) { 30921 if (chunkCache.dataLength) { 30922 onProgress(stats, context, chunkCache.flush().buffer, response); 30923 } 30924 return Promise.resolve(new ArrayBuffer(0)); 30925 } 30926 const chunk = data.value; 30927 const len = chunk.length; 30928 stats.loaded += len; 30929 if (len < highWaterMark || chunkCache.dataLength) { 30930 // The current chunk is too small to to be emitted or the cache already has data 30931 // Push it to the cache 30932 chunkCache.push(chunk); 30933 if (chunkCache.dataLength >= highWaterMark) { 30934 // flush in order to join the typed arrays 30935 onProgress(stats, context, chunkCache.flush().buffer, response); 30936 } 30937 } else { 30938 // If there's nothing cached already, and the chache is large enough 30939 // just emit the progress event 30940 onProgress(stats, context, chunk.buffer, response); 30941 } 30942 return pump(); 30943 }).catch(() => { 30944 /* aborted */ 30945 return Promise.reject(); 30946 }); 30947 }; 30948 return pump(); 30949 } 30950} 30951function getRequestParameters(context, signal) { 30952 const initParams = { 30953 method: 'GET', 30954 mode: 'cors', 30955 credentials: 'same-origin', 30956 signal, 30957 headers: new self.Headers(_extends({}, context.headers)) 30958 }; 30959 if (context.rangeEnd) { 30960 initParams.headers.set('Range', 'bytes=' + context.rangeStart + '-' + String(context.rangeEnd - 1)); 30961 } 30962 return initParams; 30963} 30964function getByteRangeLength(byteRangeHeader) { 30965 const result = BYTERANGE.exec(byteRangeHeader); 30966 if (result) { 30967 return parseInt(result[2]) - parseInt(result[1]) + 1; 30968 } 30969} 30970function getContentLength(headers) { 30971 const contentRange = headers.get('Content-Range'); 30972 if (contentRange) { 30973 const byteRangeLength = getByteRangeLength(contentRange); 30974 if (isFiniteNumber(byteRangeLength)) { 30975 return byteRangeLength; 30976 } 30977 } 30978 const contentLength = headers.get('Content-Length'); 30979 if (contentLength) { 30980 return parseInt(contentLength); 30981 } 30982} 30983function getRequest(context, initParams) { 30984 return new self.Request(context.url, initParams); 30985} 30986class FetchError extends Error { 30987 constructor(message, code, details) { 30988 super(message); 30989 this.code = void 0; 30990 this.details = void 0; 30991 this.code = code; 30992 this.details = details; 30993 } 30994} 30995 30996const AGE_HEADER_LINE_REGEX = /^age:\s*[\d.]+\s*$/im; 30997class XhrLoader { 30998 constructor(config) { 30999 this.xhrSetup = void 0; 31000 this.requestTimeout = void 0; 31001 this.retryTimeout = void 0; 31002 this.retryDelay = void 0; 31003 this.config = null; 31004 this.callbacks = null; 31005 this.context = null; 31006 this.loader = null; 31007 this.stats = void 0; 31008 this.xhrSetup = config ? config.xhrSetup || null : null; 31009 this.stats = new LoadStats(); 31010 this.retryDelay = 0; 31011 } 31012 destroy() { 31013 this.callbacks = null; 31014 this.abortInternal(); 31015 this.loader = null; 31016 this.config = null; 31017 this.context = null; 31018 this.xhrSetup = null; 31019 } 31020 abortInternal() { 31021 const loader = this.loader; 31022 self.clearTimeout(this.requestTimeout); 31023 self.clearTimeout(this.retryTimeout); 31024 if (loader) { 31025 loader.onreadystatechange = null; 31026 loader.onprogress = null; 31027 if (loader.readyState !== 4) { 31028 this.stats.aborted = true; 31029 loader.abort(); 31030 } 31031 } 31032 } 31033 abort() { 31034 var _this$callbacks; 31035 this.abortInternal(); 31036 if ((_this$callbacks = this.callbacks) != null && _this$callbacks.onAbort) { 31037 this.callbacks.onAbort(this.stats, this.context, this.loader); 31038 } 31039 } 31040 load(context, config, callbacks) { 31041 if (this.stats.loading.start) { 31042 throw new Error('Loader can only be used once.'); 31043 } 31044 this.stats.loading.start = self.performance.now(); 31045 this.context = context; 31046 this.config = config; 31047 this.callbacks = callbacks; 31048 this.loadInternal(); 31049 } 31050 loadInternal() { 31051 const { 31052 config, 31053 context 31054 } = this; 31055 if (!config || !context) { 31056 return; 31057 } 31058 const xhr = this.loader = new self.XMLHttpRequest(); 31059 const stats = this.stats; 31060 stats.loading.first = 0; 31061 stats.loaded = 0; 31062 stats.aborted = false; 31063 const xhrSetup = this.xhrSetup; 31064 if (xhrSetup) { 31065 Promise.resolve().then(() => { 31066 if (this.loader !== xhr || this.stats.aborted) return; 31067 return xhrSetup(xhr, context.url); 31068 }).catch(error => { 31069 if (this.loader !== xhr || this.stats.aborted) return; 31070 xhr.open('GET', context.url, true); 31071 return xhrSetup(xhr, context.url); 31072 }).then(() => { 31073 if (this.loader !== xhr || this.stats.aborted) return; 31074 this.openAndSendXhr(xhr, context, config); 31075 }).catch(error => { 31076 var _this$callbacks2; 31077 // IE11 throws an exception on xhr.open if attempting to access an HTTP resource over HTTPS 31078 (_this$callbacks2 = this.callbacks) == null || _this$callbacks2.onError({ 31079 code: xhr.status, 31080 text: error.message 31081 }, context, xhr, stats); 31082 return; 31083 }); 31084 } else { 31085 this.openAndSendXhr(xhr, context, config); 31086 } 31087 } 31088 openAndSendXhr(xhr, context, config) { 31089 if (!xhr.readyState) { 31090 xhr.open('GET', context.url, true); 31091 } 31092 const headers = context.headers; 31093 const { 31094 maxTimeToFirstByteMs, 31095 maxLoadTimeMs 31096 } = config.loadPolicy; 31097 if (headers) { 31098 for (const header in headers) { 31099 xhr.setRequestHeader(header, headers[header]); 31100 } 31101 } 31102 if (context.rangeEnd) { 31103 xhr.setRequestHeader('Range', 'bytes=' + context.rangeStart + '-' + (context.rangeEnd - 1)); 31104 } 31105 xhr.onreadystatechange = this.readystatechange.bind(this); 31106 xhr.onprogress = this.loadprogress.bind(this); 31107 xhr.responseType = context.responseType; 31108 // setup timeout before we perform request 31109 self.clearTimeout(this.requestTimeout); 31110 config.timeout = maxTimeToFirstByteMs && isFiniteNumber(maxTimeToFirstByteMs) ? maxTimeToFirstByteMs : maxLoadTimeMs; 31111 this.requestTimeout = self.setTimeout(this.loadtimeout.bind(this), config.timeout); 31112 xhr.send(); 31113 } 31114 readystatechange() { 31115 const { 31116 context, 31117 loader: xhr, 31118 stats 31119 } = this; 31120 if (!context || !xhr) { 31121 return; 31122 } 31123 const readyState = xhr.readyState; 31124 const config = this.config; 31125 31126 // don't proceed if xhr has been aborted 31127 if (stats.aborted) { 31128 return; 31129 } 31130 31131 // >= HEADERS_RECEIVED 31132 if (readyState >= 2) { 31133 if (stats.loading.first === 0) { 31134 stats.loading.first = Math.max(self.performance.now(), stats.loading.start); 31135 // readyState >= 2 AND readyState !==4 (readyState = HEADERS_RECEIVED || LOADING) rearm timeout as xhr not finished yet 31136 if (config.timeout !== config.loadPolicy.maxLoadTimeMs) { 31137 self.clearTimeout(this.requestTimeout); 31138 config.timeout = config.loadPolicy.maxLoadTimeMs; 31139 this.requestTimeout = self.setTimeout(this.loadtimeout.bind(this), config.loadPolicy.maxLoadTimeMs - (stats.loading.first - stats.loading.start)); 31140 } 31141 } 31142 if (readyState === 4) { 31143 self.clearTimeout(this.requestTimeout); 31144 xhr.onreadystatechange = null; 31145 xhr.onprogress = null; 31146 const status = xhr.status; 31147 // http status between 200 to 299 are all successful 31148 const useResponseText = xhr.responseType === 'text' ? xhr.responseText : null; 31149 if (status >= 200 && status < 300) { 31150 const data = useResponseText != null ? useResponseText : xhr.response; 31151 if (data != null) { 31152 var _this$callbacks3, _this$callbacks4; 31153 stats.loading.end = Math.max(self.performance.now(), stats.loading.first); 31154 const len = xhr.responseType === 'arraybuffer' ? data.byteLength : data.length; 31155 stats.loaded = stats.total = len; 31156 stats.bwEstimate = stats.total * 8000 / (stats.loading.end - stats.loading.first); 31157 const onProgress = (_this$callbacks3 = this.callbacks) == null ? void 0 : _this$callbacks3.onProgress; 31158 if (onProgress) { 31159 onProgress(stats, context, data, xhr); 31160 } 31161 const _response = { 31162 url: xhr.responseURL, 31163 data: data, 31164 code: status 31165 }; 31166 (_this$callbacks4 = this.callbacks) == null || _this$callbacks4.onSuccess(_response, stats, context, xhr); 31167 return; 31168 } 31169 } 31170 31171 // Handle bad status or nullish response 31172 const retryConfig = config.loadPolicy.errorRetry; 31173 const retryCount = stats.retry; 31174 // if max nb of retries reached or if http status between 400 and 499 (such error cannot be recovered, retrying is useless), return error 31175 const response = { 31176 url: context.url, 31177 data: undefined, 31178 code: status 31179 }; 31180 if (shouldRetry(retryConfig, retryCount, false, response)) { 31181 this.retry(retryConfig); 31182 } else { 31183 var _this$callbacks5; 31184 logger.error(`${status} while loading ${context.url}`); 31185 (_this$callbacks5 = this.callbacks) == null || _this$callbacks5.onError({ 31186 code: status, 31187 text: xhr.statusText 31188 }, context, xhr, stats); 31189 } 31190 } 31191 } 31192 } 31193 loadtimeout() { 31194 if (!this.config) return; 31195 const retryConfig = this.config.loadPolicy.timeoutRetry; 31196 const retryCount = this.stats.retry; 31197 if (shouldRetry(retryConfig, retryCount, true)) { 31198 this.retry(retryConfig); 31199 } else { 31200 var _this$context; 31201 logger.warn(`timeout while loading ${(_this$context = this.context) == null ? void 0 : _this$context.url}`); 31202 const callbacks = this.callbacks; 31203 if (callbacks) { 31204 this.abortInternal(); 31205 callbacks.onTimeout(this.stats, this.context, this.loader); 31206 } 31207 } 31208 } 31209 retry(retryConfig) { 31210 const { 31211 context, 31212 stats 31213 } = this; 31214 this.retryDelay = getRetryDelay(retryConfig, stats.retry); 31215 stats.retry++; 31216 logger.warn(`${status ? 'HTTP Status ' + status : 'Timeout'} while loading ${context == null ? void 0 : context.url}, retrying ${stats.retry}/${retryConfig.maxNumRetry} in ${this.retryDelay}ms`); 31217 // abort and reset internal state 31218 this.abortInternal(); 31219 this.loader = null; 31220 // schedule retry 31221 self.clearTimeout(this.retryTimeout); 31222 this.retryTimeout = self.setTimeout(this.loadInternal.bind(this), this.retryDelay); 31223 } 31224 loadprogress(event) { 31225 const stats = this.stats; 31226 stats.loaded = event.loaded; 31227 if (event.lengthComputable) { 31228 stats.total = event.total; 31229 } 31230 } 31231 getCacheAge() { 31232 let result = null; 31233 if (this.loader && AGE_HEADER_LINE_REGEX.test(this.loader.getAllResponseHeaders())) { 31234 const ageHeader = this.loader.getResponseHeader('age'); 31235 result = ageHeader ? parseFloat(ageHeader) : null; 31236 } 31237 return result; 31238 } 31239 getResponseHeader(name) { 31240 if (this.loader && new RegExp(`^${name}:\\s*[\\d.]+\\s*$`, 'im').test(this.loader.getAllResponseHeaders())) { 31241 return this.loader.getResponseHeader(name); 31242 } 31243 return null; 31244 } 31245} 31246 31247/** 31248 * @deprecated use fragLoadPolicy.default 31249 */ 31250 31251/** 31252 * @deprecated use manifestLoadPolicy.default and playlistLoadPolicy.default 31253 */ 31254 31255const defaultLoadPolicy = { 31256 maxTimeToFirstByteMs: 8000, 31257 maxLoadTimeMs: 20000, 31258 timeoutRetry: null, 31259 errorRetry: null 31260}; 31261 31262/** 31263 * @ignore 31264 * If possible, keep hlsDefaultConfig shallow 31265 * It is cloned whenever a new Hls instance is created, by keeping the config 31266 * shallow the properties are cloned, and we don't end up manipulating the default 31267 */ 31268const hlsDefaultConfig = _objectSpread2(_objectSpread2({ 31269 autoStartLoad: true, 31270 // used by stream-controller 31271 startPosition: -1, 31272 // used by stream-controller 31273 defaultAudioCodec: undefined, 31274 // used by stream-controller 31275 debug: false, 31276 // used by logger 31277 capLevelOnFPSDrop: false, 31278 // used by fps-controller 31279 capLevelToPlayerSize: false, 31280 // used by cap-level-controller 31281 ignoreDevicePixelRatio: false, 31282 // used by cap-level-controller 31283 maxDevicePixelRatio: Number.POSITIVE_INFINITY, 31284 // used by cap-level-controller 31285 preferManagedMediaSource: true, 31286 initialLiveManifestSize: 1, 31287 // used by stream-controller 31288 maxBufferLength: 30, 31289 // used by stream-controller 31290 backBufferLength: Infinity, 31291 // used by buffer-controller 31292 frontBufferFlushThreshold: Infinity, 31293 startOnSegmentBoundary: false, 31294 // used by stream-controller 31295 maxBufferSize: 60 * 1000 * 1000, 31296 // used by stream-controller 31297 maxFragLookUpTolerance: 0.25, 31298 // used by stream-controller 31299 maxBufferHole: 0.1, 31300 // used by stream-controller and gap-controller 31301 detectStallWithCurrentTimeMs: 1250, 31302 // used by gap-controller 31303 highBufferWatchdogPeriod: 2, 31304 // used by gap-controller 31305 nudgeOffset: 0.1, 31306 // used by gap-controller 31307 nudgeMaxRetry: 3, 31308 // used by gap-controller 31309 nudgeOnVideoHole: true, 31310 // used by gap-controller 31311 liveSyncMode: 'edge', 31312 // used by stream-controller 31313 liveSyncDurationCount: 3, 31314 // used by latency-controller 31315 liveSyncOnStallIncrease: 1, 31316 // used by latency-controller 31317 liveMaxLatencyDurationCount: Infinity, 31318 // used by latency-controller 31319 liveSyncDuration: undefined, 31320 // used by latency-controller 31321 liveMaxLatencyDuration: undefined, 31322 // used by latency-controller 31323 maxLiveSyncPlaybackRate: 1, 31324 // used by latency-controller 31325 liveDurationInfinity: false, 31326 // used by buffer-controller 31327 /** 31328 * @deprecated use backBufferLength 31329 */ 31330 liveBackBufferLength: null, 31331 // used by buffer-controller 31332 maxMaxBufferLength: 600, 31333 // used by stream-controller 31334 enableWorker: true, 31335 // used by transmuxer 31336 workerPath: null, 31337 // used by transmuxer 31338 enableSoftwareAES: true, 31339 // used by decrypter 31340 startLevel: undefined, 31341 // used by level-controller 31342 startFragPrefetch: false, 31343 // used by stream-controller 31344 fpsDroppedMonitoringPeriod: 5000, 31345 // used by fps-controller 31346 fpsDroppedMonitoringThreshold: 0.2, 31347 // used by fps-controller 31348 appendErrorMaxRetry: 3, 31349 // used by buffer-controller 31350 ignorePlaylistParsingErrors: false, 31351 loader: XhrLoader, 31352 // loader: FetchLoader, 31353 fLoader: undefined, 31354 // used by fragment-loader 31355 pLoader: undefined, 31356 // used by playlist-loader 31357 xhrSetup: undefined, 31358 // used by xhr-loader 31359 licenseXhrSetup: undefined, 31360 // used by eme-controller 31361 licenseResponseCallback: undefined, 31362 // used by eme-controller 31363 abrController: AbrController, 31364 bufferController: BufferController, 31365 capLevelController: CapLevelController, 31366 errorController: ErrorController, 31367 fpsController: FPSController, 31368 stretchShortVideoTrack: false, 31369 // used by mp4-remuxer 31370 maxAudioFramesDrift: 1, 31371 // used by mp4-remuxer 31372 forceKeyFrameOnDiscontinuity: true, 31373 // used by ts-demuxer 31374 abrEwmaFastLive: 3, 31375 // used by abr-controller 31376 abrEwmaSlowLive: 9, 31377 // used by abr-controller 31378 abrEwmaFastVoD: 3, 31379 // used by abr-controller 31380 abrEwmaSlowVoD: 9, 31381 // used by abr-controller 31382 abrEwmaDefaultEstimate: 5e5, 31383 // 500 kbps // used by abr-controller 31384 abrEwmaDefaultEstimateMax: 5e6, 31385 // 5 mbps 31386 abrBandWidthFactor: 0.95, 31387 // used by abr-controller 31388 abrBandWidthUpFactor: 0.7, 31389 // used by abr-controller 31390 abrMaxWithRealBitrate: false, 31391 // used by abr-controller 31392 maxStarvationDelay: 4, 31393 // used by abr-controller 31394 maxLoadingDelay: 4, 31395 // used by abr-controller 31396 minAutoBitrate: 0, 31397 // used by hls 31398 emeEnabled: false, 31399 // used by eme-controller 31400 widevineLicenseUrl: undefined, 31401 // used by eme-controller 31402 drmSystems: {}, 31403 // used by eme-controller 31404 drmSystemOptions: {}, 31405 // used by eme-controller 31406 requestMediaKeySystemAccessFunc: requestMediaKeySystemAccess , 31407 // used by eme-controller 31408 requireKeySystemAccessOnStart: false, 31409 // used by eme-controller 31410 testBandwidth: true, 31411 progressive: false, 31412 lowLatencyMode: true, 31413 cmcd: undefined, 31414 enableDateRangeMetadataCues: true, 31415 enableEmsgMetadataCues: true, 31416 enableEmsgKLVMetadata: false, 31417 enableID3MetadataCues: true, 31418 enableInterstitialPlayback: true, 31419 interstitialAppendInPlace: true, 31420 interstitialLiveLookAhead: 10, 31421 useMediaCapabilities: true, 31422 preserveManualLevelOnError: false, 31423 certLoadPolicy: { 31424 default: defaultLoadPolicy 31425 }, 31426 keyLoadPolicy: { 31427 default: { 31428 maxTimeToFirstByteMs: 8000, 31429 maxLoadTimeMs: 20000, 31430 timeoutRetry: { 31431 maxNumRetry: 1, 31432 retryDelayMs: 1000, 31433 maxRetryDelayMs: 20000, 31434 backoff: 'linear' 31435 }, 31436 errorRetry: { 31437 maxNumRetry: 8, 31438 retryDelayMs: 1000, 31439 maxRetryDelayMs: 20000, 31440 backoff: 'linear' 31441 } 31442 } 31443 }, 31444 manifestLoadPolicy: { 31445 default: { 31446 maxTimeToFirstByteMs: Infinity, 31447 maxLoadTimeMs: 20000, 31448 timeoutRetry: { 31449 maxNumRetry: 2, 31450 retryDelayMs: 0, 31451 maxRetryDelayMs: 0 31452 }, 31453 errorRetry: { 31454 maxNumRetry: 1, 31455 retryDelayMs: 1000, 31456 maxRetryDelayMs: 8000 31457 } 31458 } 31459 }, 31460 playlistLoadPolicy: { 31461 default: { 31462 maxTimeToFirstByteMs: 10000, 31463 maxLoadTimeMs: 20000, 31464 timeoutRetry: { 31465 maxNumRetry: 2, 31466 retryDelayMs: 0, 31467 maxRetryDelayMs: 0 31468 }, 31469 errorRetry: { 31470 maxNumRetry: 2, 31471 retryDelayMs: 1000, 31472 maxRetryDelayMs: 8000 31473 } 31474 } 31475 }, 31476 fragLoadPolicy: { 31477 default: { 31478 maxTimeToFirstByteMs: 10000, 31479 maxLoadTimeMs: 120000, 31480 timeoutRetry: { 31481 maxNumRetry: 4, 31482 retryDelayMs: 0, 31483 maxRetryDelayMs: 0 31484 }, 31485 errorRetry: { 31486 maxNumRetry: 6, 31487 retryDelayMs: 1000, 31488 maxRetryDelayMs: 8000 31489 } 31490 } 31491 }, 31492 steeringManifestLoadPolicy: { 31493 default: { 31494 maxTimeToFirstByteMs: 10000, 31495 maxLoadTimeMs: 20000, 31496 timeoutRetry: { 31497 maxNumRetry: 2, 31498 retryDelayMs: 0, 31499 maxRetryDelayMs: 0 31500 }, 31501 errorRetry: { 31502 maxNumRetry: 1, 31503 retryDelayMs: 1000, 31504 maxRetryDelayMs: 8000 31505 } 31506 } 31507 }, 31508 interstitialAssetListLoadPolicy: { 31509 default: { 31510 maxTimeToFirstByteMs: 10000, 31511 maxLoadTimeMs: 30000, 31512 timeoutRetry: { 31513 maxNumRetry: 0, 31514 retryDelayMs: 0, 31515 maxRetryDelayMs: 0 31516 }, 31517 errorRetry: { 31518 maxNumRetry: 0, 31519 retryDelayMs: 1000, 31520 maxRetryDelayMs: 8000 31521 } 31522 } 31523 }, 31524 // These default settings are deprecated in favor of the above policies 31525 // and are maintained for backwards compatibility 31526 manifestLoadingTimeOut: 10000, 31527 manifestLoadingMaxRetry: 1, 31528 manifestLoadingRetryDelay: 1000, 31529 manifestLoadingMaxRetryTimeout: 64000, 31530 levelLoadingTimeOut: 10000, 31531 levelLoadingMaxRetry: 4, 31532 levelLoadingRetryDelay: 1000, 31533 levelLoadingMaxRetryTimeout: 64000, 31534 fragLoadingTimeOut: 20000, 31535 fragLoadingMaxRetry: 6, 31536 fragLoadingRetryDelay: 1000, 31537 fragLoadingMaxRetryTimeout: 64000 31538}, timelineConfig()), {}, { 31539 subtitleStreamController: SubtitleStreamController , 31540 subtitleTrackController: SubtitleTrackController , 31541 timelineController: TimelineController , 31542 audioStreamController: AudioStreamController , 31543 audioTrackController: AudioTrackController , 31544 emeController: EMEController , 31545 cmcdController: CMCDController , 31546 contentSteeringController: ContentSteeringController , 31547 interstitialsController: InterstitialsController 31548}); 31549function timelineConfig() { 31550 return { 31551 cueHandler: Cues, 31552 // used by timeline-controller 31553 enableWebVTT: true, 31554 // used by timeline-controller 31555 enableIMSC1: true, 31556 // used by timeline-controller 31557 enableCEA708Captions: true, 31558 // used by timeline-controller 31559 captionsTextTrack1Label: 'English', 31560 // used by timeline-controller 31561 captionsTextTrack1LanguageCode: 'en', 31562 // used by timeline-controller 31563 captionsTextTrack2Label: 'Spanish', 31564 // used by timeline-controller 31565 captionsTextTrack2LanguageCode: 'es', 31566 // used by timeline-controller 31567 captionsTextTrack3Label: 'Unknown CC', 31568 // used by timeline-controller 31569 captionsTextTrack3LanguageCode: '', 31570 // used by timeline-controller 31571 captionsTextTrack4Label: 'Unknown CC', 31572 // used by timeline-controller 31573 captionsTextTrack4LanguageCode: '', 31574 // used by timeline-controller 31575 renderTextTracksNatively: true 31576 }; 31577} 31578 31579/** 31580 * @ignore 31581 */ 31582function mergeConfig(defaultConfig, userConfig, logger) { 31583 if ((userConfig.liveSyncDurationCount || userConfig.liveMaxLatencyDurationCount) && (userConfig.liveSyncDuration || userConfig.liveMaxLatencyDuration)) { 31584 throw new Error("Illegal hls.js config: don't mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration"); 31585 } 31586 if (userConfig.liveMaxLatencyDurationCount !== undefined && (userConfig.liveSyncDurationCount === undefined || userConfig.liveMaxLatencyDurationCount <= userConfig.liveSyncDurationCount)) { 31587 throw new Error('Illegal hls.js config: "liveMaxLatencyDurationCount" must be greater than "liveSyncDurationCount"'); 31588 } 31589 if (userConfig.liveMaxLatencyDuration !== undefined && (userConfig.liveSyncDuration === undefined || userConfig.liveMaxLatencyDuration <= userConfig.liveSyncDuration)) { 31590 throw new Error('Illegal hls.js config: "liveMaxLatencyDuration" must be greater than "liveSyncDuration"'); 31591 } 31592 const defaultsCopy = deepCpy(defaultConfig); 31593 31594 // Backwards compatibility with deprecated config values 31595 const deprecatedSettingTypes = ['manifest', 'level', 'frag']; 31596 const deprecatedSettings = ['TimeOut', 'MaxRetry', 'RetryDelay', 'MaxRetryTimeout']; 31597 deprecatedSettingTypes.forEach(type => { 31598 const policyName = `${type === 'level' ? 'playlist' : type}LoadPolicy`; 31599 const policyNotSet = userConfig[policyName] === undefined; 31600 const report = []; 31601 deprecatedSettings.forEach(setting => { 31602 const deprecatedSetting = `${type}Loading${setting}`; 31603 const value = userConfig[deprecatedSetting]; 31604 if (value !== undefined && policyNotSet) { 31605 report.push(deprecatedSetting); 31606 const settings = defaultsCopy[policyName].default; 31607 userConfig[policyName] = { 31608 default: settings 31609 }; 31610 switch (setting) { 31611 case 'TimeOut': 31612 settings.maxLoadTimeMs = value; 31613 settings.maxTimeToFirstByteMs = value; 31614 break; 31615 case 'MaxRetry': 31616 settings.errorRetry.maxNumRetry = value; 31617 settings.timeoutRetry.maxNumRetry = value; 31618 break; 31619 case 'RetryDelay': 31620 settings.errorRetry.retryDelayMs = value; 31621 settings.timeoutRetry.retryDelayMs = value; 31622 break; 31623 case 'MaxRetryTimeout': 31624 settings.errorRetry.maxRetryDelayMs = value; 31625 settings.timeoutRetry.maxRetryDelayMs = value; 31626 break; 31627 } 31628 } 31629 }); 31630 if (report.length) { 31631 logger.warn(`hls.js config: "${report.join('", "')}" setting(s) are deprecated, use "${policyName}": ${stringify(userConfig[policyName])}`); 31632 } 31633 }); 31634 return _objectSpread2(_objectSpread2({}, defaultsCopy), userConfig); 31635} 31636function deepCpy(obj) { 31637 if (obj && typeof obj === 'object') { 31638 if (Array.isArray(obj)) { 31639 return obj.map(deepCpy); 31640 } 31641 return Object.keys(obj).reduce((result, key) => { 31642 result[key] = deepCpy(obj[key]); 31643 return result; 31644 }, {}); 31645 } 31646 return obj; 31647} 31648 31649/** 31650 * @ignore 31651 */ 31652function enableStreamingMode(config, logger) { 31653 const currentLoader = config.loader; 31654 if (currentLoader !== FetchLoader && currentLoader !== XhrLoader) { 31655 // If a developer has configured their own loader, respect that choice 31656 logger.log('[config]: Custom loader detected, cannot enable progressive streaming'); 31657 config.progressive = false; 31658 } else { 31659 const canStreamProgressively = fetchSupported(); 31660 if (canStreamProgressively) { 31661 config.loader = FetchLoader; 31662 config.progressive = true; 31663 config.enableSoftwareAES = true; 31664 logger.log('[config]: Progressive streaming enabled, using FetchLoader'); 31665 } 31666 } 31667} 31668 31669const MAX_START_GAP_JUMP = 2.0; 31670const SKIP_BUFFER_HOLE_STEP_SECONDS = 0.1; 31671const SKIP_BUFFER_RANGE_START = 0.05; 31672const TICK_INTERVAL$1 = 100; 31673class GapController extends TaskLoop { 31674 constructor(hls, fragmentTracker) { 31675 super('gap-controller', hls.logger); 31676 this.hls = void 0; 31677 this.fragmentTracker = void 0; 31678 this.media = null; 31679 this.mediaSource = void 0; 31680 this.nudgeRetry = 0; 31681 this.stallReported = false; 31682 this.stalled = null; 31683 this.moved = false; 31684 this.seeking = false; 31685 this.buffered = {}; 31686 this.lastCurrentTime = 0; 31687 this.ended = 0; 31688 this.waiting = 0; 31689 this.onMediaPlaying = () => { 31690 this.ended = 0; 31691 this.waiting = 0; 31692 }; 31693 this.onMediaWaiting = () => { 31694 var _this$media; 31695 if ((_this$media = this.media) != null && _this$media.seeking) { 31696 return; 31697 } 31698 this.waiting = self.performance.now(); 31699 this.tick(); 31700 }; 31701 this.onMediaEnded = () => { 31702 if (this.hls) { 31703 var _this$media2; 31704 // ended is set when triggering MEDIA_ENDED so that we do not trigger it again on stall or on tick with media.ended 31705 this.ended = ((_this$media2 = this.media) == null ? void 0 : _this$media2.currentTime) || 1; 31706 this.hls.trigger(Events.MEDIA_ENDED, { 31707 stalled: false 31708 }); 31709 } 31710 }; 31711 this.hls = hls; 31712 this.fragmentTracker = fragmentTracker; 31713 this.registerListeners(); 31714 } 31715 registerListeners() { 31716 const { 31717 hls 31718 } = this; 31719 if (hls) { 31720 hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 31721 hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 31722 hls.on(Events.BUFFER_APPENDED, this.onBufferAppended, this); 31723 } 31724 } 31725 unregisterListeners() { 31726 const { 31727 hls 31728 } = this; 31729 if (hls) { 31730 hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 31731 hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 31732 hls.off(Events.BUFFER_APPENDED, this.onBufferAppended, this); 31733 } 31734 } 31735 destroy() { 31736 super.destroy(); 31737 this.unregisterListeners(); 31738 this.media = this.hls = this.fragmentTracker = null; 31739 this.mediaSource = undefined; 31740 } 31741 onMediaAttached(event, data) { 31742 this.setInterval(TICK_INTERVAL$1); 31743 this.mediaSource = data.mediaSource; 31744 const media = this.media = data.media; 31745 addEventListener(media, 'playing', this.onMediaPlaying); 31746 addEventListener(media, 'waiting', this.onMediaWaiting); 31747 addEventListener(media, 'ended', this.onMediaEnded); 31748 } 31749 onMediaDetaching(event, data) { 31750 this.clearInterval(); 31751 const { 31752 media 31753 } = this; 31754 if (media) { 31755 removeEventListener(media, 'playing', this.onMediaPlaying); 31756 removeEventListener(media, 'waiting', this.onMediaWaiting); 31757 removeEventListener(media, 'ended', this.onMediaEnded); 31758 this.media = null; 31759 } 31760 this.mediaSource = undefined; 31761 } 31762 onBufferAppended(event, data) { 31763 this.buffered = data.timeRanges; 31764 } 31765 get hasBuffered() { 31766 return Object.keys(this.buffered).length > 0; 31767 } 31768 tick() { 31769 var _this$media3; 31770 if (!((_this$media3 = this.media) != null && _this$media3.readyState) || !this.hasBuffered) { 31771 return; 31772 } 31773 const currentTime = this.media.currentTime; 31774 this.poll(currentTime, this.lastCurrentTime); 31775 this.lastCurrentTime = currentTime; 31776 } 31777 31778 /** 31779 * Checks if the playhead is stuck within a gap, and if so, attempts to free it. 31780 * A gap is an unbuffered range between two buffered ranges (or the start and the first buffered range). 31781 * 31782 * @param lastCurrentTime - Previously read playhead position 31783 */ 31784 poll(currentTime, lastCurrentTime) { 31785 var _this$hls, _this$hls2; 31786 const config = (_this$hls = this.hls) == null ? void 0 : _this$hls.config; 31787 if (!config) { 31788 return; 31789 } 31790 const media = this.media; 31791 if (!media) { 31792 return; 31793 } 31794 const { 31795 seeking 31796 } = media; 31797 const seeked = this.seeking && !seeking; 31798 const beginSeek = !this.seeking && seeking; 31799 const pausedEndedOrHalted = media.paused && !seeking || media.ended || media.playbackRate === 0; 31800 this.seeking = seeking; 31801 31802 // The playhead is moving, no-op 31803 if (currentTime !== lastCurrentTime) { 31804 if (lastCurrentTime) { 31805 this.ended = 0; 31806 } 31807 this.moved = true; 31808 if (!seeking) { 31809 this.nudgeRetry = 0; 31810 // When crossing between buffered video time ranges, but not audio, flush pipeline with seek (Chrome) 31811 if (config.nudgeOnVideoHole && !pausedEndedOrHalted && currentTime > lastCurrentTime) { 31812 this.nudgeOnVideoHole(currentTime, lastCurrentTime); 31813 } 31814 } 31815 if (this.waiting === 0) { 31816 this.stallResolved(currentTime); 31817 } 31818 return; 31819 } 31820 31821 // Clear stalled state when beginning or finishing seeking so that we don't report stalls coming out of a seek 31822 if (beginSeek || seeked) { 31823 if (seeked) { 31824 this.stallResolved(currentTime); 31825 } 31826 return; 31827 } 31828 31829 // The playhead should not be moving 31830 if (pausedEndedOrHalted) { 31831 this.nudgeRetry = 0; 31832 this.stallResolved(currentTime); 31833 // Fire MEDIA_ENDED to workaround event not being dispatched by browser 31834 if (!this.ended && media.ended && this.hls) { 31835 this.ended = currentTime || 1; 31836 this.hls.trigger(Events.MEDIA_ENDED, { 31837 stalled: false 31838 }); 31839 } 31840 return; 31841 } 31842 if (!BufferHelper.getBuffered(media).length) { 31843 this.nudgeRetry = 0; 31844 return; 31845 } 31846 31847 // Resolve stalls at buffer holes using the main buffer, whose ranges are the intersections of the A/V sourcebuffers 31848 const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0); 31849 const nextStart = bufferInfo.nextStart || 0; 31850 const fragmentTracker = this.fragmentTracker; 31851 if (seeking && fragmentTracker && this.hls) { 31852 // Is there a fragment loading/parsing/appending before currentTime? 31853 const inFlightDependency = getInFlightDependency(this.hls.inFlightFragments, currentTime); 31854 31855 // Waiting for seeking in a buffered range to complete 31856 const hasEnoughBuffer = bufferInfo.len > MAX_START_GAP_JUMP; 31857 // Next buffered range is too far ahead to jump to while still seeking 31858 const noBufferHole = !nextStart || inFlightDependency || nextStart - currentTime > MAX_START_GAP_JUMP && !fragmentTracker.getPartialFragment(currentTime); 31859 if (hasEnoughBuffer || noBufferHole) { 31860 return; 31861 } 31862 // Reset moved state when seeking to a point in or before a gap/hole 31863 this.moved = false; 31864 } 31865 31866 // Skip start gaps if we haven't played, but the last poll detected the start of a stall 31867 // The addition poll gives the browser a chance to jump the gap for us 31868 const levelDetails = (_this$hls2 = this.hls) == null ? void 0 : _this$hls2.latestLevelDetails; 31869 if (!this.moved && this.stalled !== null && fragmentTracker) { 31870 // There is no playable buffer (seeked, waiting for buffer) 31871 const isBuffered = bufferInfo.len > 0; 31872 if (!isBuffered && !nextStart) { 31873 return; 31874 } 31875 // Jump start gaps within jump threshold 31876 const startJump = Math.max(nextStart, bufferInfo.start || 0) - currentTime; 31877 31878 // When joining a live stream with audio tracks, account for live playlist window sliding by allowing 31879 // a larger jump over start gaps caused by the audio-stream-controller buffering a start fragment 31880 // that begins over 1 target duration after the video start position. 31881 const isLive = !!(levelDetails != null && levelDetails.live); 31882 const maxStartGapJump = isLive ? levelDetails.targetduration * 2 : MAX_START_GAP_JUMP; 31883 const appended = appendedFragAtPosition(currentTime, fragmentTracker); 31884 if (startJump > 0 && (startJump <= maxStartGapJump || appended)) { 31885 if (!media.paused) { 31886 this._trySkipBufferHole(appended); 31887 } 31888 return; 31889 } 31890 } 31891 31892 // Start tracking stall time 31893 const detectStallWithCurrentTimeMs = config.detectStallWithCurrentTimeMs; 31894 const tnow = self.performance.now(); 31895 const tWaiting = this.waiting; 31896 let stalled = this.stalled; 31897 if (stalled === null) { 31898 // Use time of recent "waiting" event 31899 if (tWaiting > 0 && tnow - tWaiting < detectStallWithCurrentTimeMs) { 31900 stalled = this.stalled = tWaiting; 31901 } else { 31902 this.stalled = tnow; 31903 return; 31904 } 31905 } 31906 const stalledDuration = tnow - stalled; 31907 if (!seeking && (stalledDuration >= detectStallWithCurrentTimeMs || tWaiting) && this.hls) { 31908 var _this$mediaSource; 31909 // Dispatch MEDIA_ENDED when media.ended/ended event is not signalled at end of stream 31910 if (((_this$mediaSource = this.mediaSource) == null ? void 0 : _this$mediaSource.readyState) === 'ended' && !(levelDetails != null && levelDetails.live) && Math.abs(currentTime - ((levelDetails == null ? void 0 : levelDetails.edge) || 0)) < 1) { 31911 if (this.ended) { 31912 return; 31913 } 31914 this.ended = currentTime || 1; 31915 this.hls.trigger(Events.MEDIA_ENDED, { 31916 stalled: true 31917 }); 31918 return; 31919 } 31920 // Report stalling after trying to fix 31921 this._reportStall(bufferInfo); 31922 if (!this.media || !this.hls) { 31923 return; 31924 } 31925 } 31926 const bufferedWithHoles = BufferHelper.bufferInfo(media, currentTime, config.maxBufferHole); 31927 this._tryFixBufferStall(bufferedWithHoles, stalledDuration, currentTime); 31928 } 31929 stallResolved(currentTime) { 31930 const stalled = this.stalled; 31931 if (stalled && this.hls) { 31932 this.stalled = null; 31933 // The playhead is now moving, but was previously stalled 31934 if (this.stallReported) { 31935 const stalledDuration = self.performance.now() - stalled; 31936 this.log(`playback not stuck anymore @${currentTime}, after ${Math.round(stalledDuration)}ms`); 31937 this.stallReported = false; 31938 this.waiting = 0; 31939 this.hls.trigger(Events.STALL_RESOLVED, {}); 31940 } 31941 } 31942 } 31943 nudgeOnVideoHole(currentTime, lastCurrentTime) { 31944 var _this$buffered$audio; 31945 // Chrome will play one second past a hole in video buffered time ranges without rendering any video from the subsequent range and then stall as long as audio is buffered: 31946 // https://github.com/video-dev/hls.js/issues/5631 31947 // https://issues.chromium.org/issues/40280613#comment10 31948 // Detect the potential for this situation and proactively seek to flush the video pipeline once the playhead passes the start of the video hole. 31949 // When there are audio and video buffers and currentTime is past the end of the first video buffered range... 31950 const videoSourceBuffered = this.buffered.video; 31951 if (this.hls && this.media && this.fragmentTracker && (_this$buffered$audio = this.buffered.audio) != null && _this$buffered$audio.length && videoSourceBuffered && videoSourceBuffered.length > 1 && currentTime > videoSourceBuffered.end(0)) { 31952 // and audio is buffered at the playhead 31953 const audioBufferInfo = BufferHelper.bufferedInfo(BufferHelper.timeRangesToArray(this.buffered.audio), currentTime, 0); 31954 if (audioBufferInfo.len > 1 && lastCurrentTime >= audioBufferInfo.start) { 31955 const videoTimes = BufferHelper.timeRangesToArray(videoSourceBuffered); 31956 const lastBufferedIndex = BufferHelper.bufferedInfo(videoTimes, lastCurrentTime, 0).bufferedIndex; 31957 // nudge when crossing into another video buffered range (hole). 31958 if (lastBufferedIndex > -1 && lastBufferedIndex < videoTimes.length - 1) { 31959 const bufferedIndex = BufferHelper.bufferedInfo(videoTimes, currentTime, 0).bufferedIndex; 31960 const holeStart = videoTimes[lastBufferedIndex].end; 31961 const holeEnd = videoTimes[lastBufferedIndex + 1].start; 31962 if ((bufferedIndex === -1 || bufferedIndex > lastBufferedIndex) && holeEnd - holeStart < 1 && 31963 // `maxBufferHole` may be too small and setting it to 0 should not disable this feature 31964 currentTime - holeStart < 2) { 31965 const error = new Error(`nudging playhead to flush pipeline after video hole. currentTime: ${currentTime} hole: ${holeStart} -> ${holeEnd} buffered index: ${bufferedIndex}`); 31966 this.warn(error.message); 31967 // Magic number to flush the pipeline without interuption to audio playback: 31968 this.media.currentTime += 0.000001; 31969 let frag = appendedFragAtPosition(currentTime, this.fragmentTracker); 31970 if (frag && 'fragment' in frag) { 31971 frag = frag.fragment; 31972 } else if (!frag) { 31973 frag = undefined; 31974 } 31975 const bufferInfo = BufferHelper.bufferInfo(this.media, currentTime, 0); 31976 this.hls.trigger(Events.ERROR, { 31977 type: ErrorTypes.MEDIA_ERROR, 31978 details: ErrorDetails.BUFFER_SEEK_OVER_HOLE, 31979 fatal: false, 31980 error, 31981 reason: error.message, 31982 frag, 31983 buffer: bufferInfo.len, 31984 bufferInfo 31985 }); 31986 } 31987 } 31988 } 31989 } 31990 } 31991 31992 /** 31993 * Detects and attempts to fix known buffer stalling issues. 31994 * @param bufferInfo - The properties of the current buffer. 31995 * @param stalledDurationMs - The amount of time Hls.js has been stalling for. 31996 * @private 31997 */ 31998 _tryFixBufferStall(bufferInfo, stalledDurationMs, currentTime) { 31999 var _this$hls3, _this$hls4; 32000 const { 32001 fragmentTracker, 32002 media 32003 } = this; 32004 const config = (_this$hls3 = this.hls) == null ? void 0 : _this$hls3.config; 32005 if (!media || !fragmentTracker || !config) { 32006 return; 32007 } 32008 const levelDetails = (_this$hls4 = this.hls) == null ? void 0 : _this$hls4.latestLevelDetails; 32009 const appended = appendedFragAtPosition(currentTime, fragmentTracker); 32010 if (appended || levelDetails != null && levelDetails.live && currentTime < levelDetails.fragmentStart) { 32011 // Try to skip over the buffer hole caused by a partial fragment 32012 // This method isn't limited by the size of the gap between buffered ranges 32013 const targetTime = this._trySkipBufferHole(appended); 32014 // we return here in this case, meaning 32015 // the branch below only executes when we haven't seeked to a new position 32016 if (targetTime || !this.media) { 32017 return; 32018 } 32019 } 32020 32021 // if we haven't had to skip over a buffer hole of a partial fragment 32022 // we may just have to "nudge" the playlist as the browser decoding/rendering engine 32023 // needs to cross some sort of threshold covering all source-buffers content 32024 // to start playing properly. 32025 const bufferedRanges = bufferInfo.buffered; 32026 const adjacentTraversal = this.adjacentTraversal(bufferInfo, currentTime); 32027 if ((bufferedRanges && bufferedRanges.length > 1 && bufferInfo.len > config.maxBufferHole || bufferInfo.nextStart && (bufferInfo.nextStart - currentTime < config.maxBufferHole || adjacentTraversal)) && (stalledDurationMs > config.highBufferWatchdogPeriod * 1000 || this.waiting)) { 32028 this.warn('Trying to nudge playhead over buffer-hole'); 32029 // Try to nudge currentTime over a buffer hole if we've been stalling for the configured amount of seconds 32030 // We only try to jump the hole if it's under the configured size 32031 this._tryNudgeBuffer(bufferInfo); 32032 } 32033 } 32034 adjacentTraversal(bufferInfo, currentTime) { 32035 const fragmentTracker = this.fragmentTracker; 32036 const nextStart = bufferInfo.nextStart; 32037 if (fragmentTracker && nextStart) { 32038 const current = fragmentTracker.getFragAtPos(currentTime, PlaylistLevelType.MAIN); 32039 const next = fragmentTracker.getFragAtPos(nextStart, PlaylistLevelType.MAIN); 32040 if (current && next) { 32041 return next.sn - current.sn < 2; 32042 } 32043 } 32044 return false; 32045 } 32046 32047 /** 32048 * Triggers a BUFFER_STALLED_ERROR event, but only once per stall period. 32049 * @param bufferLen - The playhead distance from the end of the current buffer segment. 32050 * @private 32051 */ 32052 _reportStall(bufferInfo) { 32053 const { 32054 hls, 32055 media, 32056 stallReported, 32057 stalled 32058 } = this; 32059 if (!stallReported && stalled !== null && media && hls) { 32060 // Report stalled error once 32061 this.stallReported = true; 32062 const error = new Error(`Playback stalling at @${media.currentTime} due to low buffer (${stringify(bufferInfo)})`); 32063 this.warn(error.message); 32064 hls.trigger(Events.ERROR, { 32065 type: ErrorTypes.MEDIA_ERROR, 32066 details: ErrorDetails.BUFFER_STALLED_ERROR, 32067 fatal: false, 32068 error, 32069 buffer: bufferInfo.len, 32070 bufferInfo, 32071 stalled: { 32072 start: stalled 32073 } 32074 }); 32075 } 32076 } 32077 32078 /** 32079 * Attempts to fix buffer stalls by jumping over known gaps caused by partial fragments 32080 * @param appended - The fragment or part found at the current time (where playback is stalling). 32081 * @private 32082 */ 32083 _trySkipBufferHole(appended) { 32084 var _this$hls5; 32085 const { 32086 fragmentTracker, 32087 media 32088 } = this; 32089 const config = (_this$hls5 = this.hls) == null ? void 0 : _this$hls5.config; 32090 if (!media || !fragmentTracker || !config) { 32091 return 0; 32092 } 32093 32094 // Check if currentTime is between unbuffered regions of partial fragments 32095 const currentTime = media.currentTime; 32096 const bufferInfo = BufferHelper.bufferInfo(media, currentTime, 0); 32097 const startTime = currentTime < bufferInfo.start ? bufferInfo.start : bufferInfo.nextStart; 32098 if (startTime && this.hls) { 32099 const bufferStarved = bufferInfo.len <= config.maxBufferHole; 32100 const waiting = bufferInfo.len > 0 && bufferInfo.len < 1 && media.readyState < 3; 32101 const gapLength = startTime - currentTime; 32102 if (gapLength > 0 && (bufferStarved || waiting)) { 32103 // Only allow large gaps to be skipped if it is a start gap, or all fragments in skip range are partial 32104 if (gapLength > config.maxBufferHole) { 32105 let startGap = false; 32106 if (currentTime === 0) { 32107 const startFrag = fragmentTracker.getAppendedFrag(0, PlaylistLevelType.MAIN); 32108 if (startFrag && startTime < startFrag.end) { 32109 startGap = true; 32110 } 32111 } 32112 if (!startGap && appended) { 32113 var _this$hls$loadLevelOb; 32114 // Do not seek when selected variant playlist is unloaded 32115 if (!((_this$hls$loadLevelOb = this.hls.loadLevelObj) != null && _this$hls$loadLevelOb.details)) { 32116 return 0; 32117 } 32118 // Do not seek when required fragments are inflight or appending 32119 const inFlightDependency = getInFlightDependency(this.hls.inFlightFragments, startTime); 32120 if (inFlightDependency) { 32121 return 0; 32122 } 32123 // Do not seek if we can't walk tracked fragments to end of gap 32124 let moreToLoad = false; 32125 let pos = appended.end; 32126 while (pos < startTime) { 32127 const provisioned = appendedFragAtPosition(pos, fragmentTracker); 32128 if (provisioned) { 32129 pos += provisioned.duration; 32130 } else { 32131 moreToLoad = true; 32132 break; 32133 } 32134 } 32135 if (moreToLoad) { 32136 return 0; 32137 } 32138 } 32139 } 32140 const targetTime = Math.max(startTime + SKIP_BUFFER_RANGE_START, currentTime + SKIP_BUFFER_HOLE_STEP_SECONDS); 32141 this.warn(`skipping hole, adjusting currentTime from ${currentTime} to ${targetTime}`); 32142 this.moved = true; 32143 media.currentTime = targetTime; 32144 if (!(appended != null && appended.gap)) { 32145 const error = new Error(`fragment loaded with buffer holes, seeking from ${currentTime} to ${targetTime}`); 32146 const errorData = { 32147 type: ErrorTypes.MEDIA_ERROR, 32148 details: ErrorDetails.BUFFER_SEEK_OVER_HOLE, 32149 fatal: false, 32150 error, 32151 reason: error.message, 32152 buffer: bufferInfo.len, 32153 bufferInfo 32154 }; 32155 if (appended) { 32156 if ('fragment' in appended) { 32157 errorData.part = appended; 32158 } else { 32159 errorData.frag = appended; 32160 } 32161 } 32162 this.hls.trigger(Events.ERROR, errorData); 32163 } 32164 return targetTime; 32165 } 32166 } 32167 return 0; 32168 } 32169 32170 /** 32171 * Attempts to fix buffer stalls by advancing the mediaElement's current time by a small amount. 32172 * @private 32173 */ 32174 _tryNudgeBuffer(bufferInfo) { 32175 const { 32176 hls, 32177 media, 32178 nudgeRetry 32179 } = this; 32180 const config = hls == null ? void 0 : hls.config; 32181 if (!media || !config) { 32182 return 0; 32183 } 32184 const currentTime = media.currentTime; 32185 this.nudgeRetry++; 32186 if (nudgeRetry < config.nudgeMaxRetry) { 32187 const targetTime = currentTime + (nudgeRetry + 1) * config.nudgeOffset; 32188 // playback stalled in buffered area ... let's nudge currentTime to try to overcome this 32189 const error = new Error(`Nudging 'currentTime' from ${currentTime} to ${targetTime}`); 32190 this.warn(error.message); 32191 media.currentTime = targetTime; 32192 hls.trigger(Events.ERROR, { 32193 type: ErrorTypes.MEDIA_ERROR, 32194 details: ErrorDetails.BUFFER_NUDGE_ON_STALL, 32195 error, 32196 fatal: false, 32197 buffer: bufferInfo.len, 32198 bufferInfo 32199 }); 32200 } else { 32201 const error = new Error(`Playhead still not moving while enough data buffered @${currentTime} after ${config.nudgeMaxRetry} nudges`); 32202 this.error(error.message); 32203 hls.trigger(Events.ERROR, { 32204 type: ErrorTypes.MEDIA_ERROR, 32205 details: ErrorDetails.BUFFER_STALLED_ERROR, 32206 error, 32207 fatal: true, 32208 buffer: bufferInfo.len, 32209 bufferInfo 32210 }); 32211 } 32212 } 32213} 32214function getInFlightDependency(inFlightFragments, currentTime) { 32215 const main = inFlight(inFlightFragments.main); 32216 if (main && main.start <= currentTime) { 32217 return main; 32218 } 32219 const audio = inFlight(inFlightFragments.audio); 32220 if (audio && audio.start <= currentTime) { 32221 return audio; 32222 } 32223 return null; 32224} 32225function inFlight(inFlightData) { 32226 if (!inFlightData) { 32227 return null; 32228 } 32229 switch (inFlightData.state) { 32230 case State.IDLE: 32231 case State.STOPPED: 32232 case State.ENDED: 32233 case State.ERROR: 32234 return null; 32235 } 32236 return inFlightData.frag; 32237} 32238function appendedFragAtPosition(pos, fragmentTracker) { 32239 return fragmentTracker.getAppendedFrag(pos, PlaylistLevelType.MAIN) || fragmentTracker.getPartialFragment(pos); 32240} 32241 32242const MIN_CUE_DURATION = 0.25; 32243function getCueClass() { 32244 if (typeof self === 'undefined') return undefined; 32245 return self.VTTCue || self.TextTrackCue; 32246} 32247function createCueWithDataFields(Cue, startTime, endTime, data, type) { 32248 let cue = new Cue(startTime, endTime, ''); 32249 try { 32250 cue.value = data; 32251 if (type) { 32252 cue.type = type; 32253 } 32254 } catch (e) { 32255 cue = new Cue(startTime, endTime, stringify(type ? _objectSpread2({ 32256 type 32257 }, data) : data)); 32258 } 32259 return cue; 32260} 32261 32262// VTTCue latest draft allows an infinite duration, fallback 32263// to MAX_VALUE if necessary 32264const MAX_CUE_ENDTIME = (() => { 32265 const Cue = getCueClass(); 32266 try { 32267 Cue && new Cue(0, Number.POSITIVE_INFINITY, ''); 32268 } catch (e) { 32269 return Number.MAX_VALUE; 32270 } 32271 return Number.POSITIVE_INFINITY; 32272})(); 32273class ID3TrackController { 32274 constructor(hls) { 32275 this.hls = void 0; 32276 this.id3Track = null; 32277 this.media = null; 32278 this.dateRangeCuesAppended = {}; 32279 this.removeCues = true; 32280 this.assetCue = void 0; 32281 this.onEventCueEnter = () => { 32282 if (!this.hls) { 32283 return; 32284 } 32285 this.hls.trigger(Events.EVENT_CUE_ENTER, {}); 32286 }; 32287 this.hls = hls; 32288 this._registerListeners(); 32289 } 32290 destroy() { 32291 this._unregisterListeners(); 32292 this.id3Track = null; 32293 this.media = null; 32294 this.dateRangeCuesAppended = {}; 32295 // @ts-ignore 32296 this.hls = this.onEventCueEnter = null; 32297 } 32298 _registerListeners() { 32299 const { 32300 hls 32301 } = this; 32302 if (hls) { 32303 hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 32304 hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 32305 hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 32306 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 32307 hls.on(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this); 32308 hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 32309 hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this); 32310 hls.on(Events.LEVEL_PTS_UPDATED, this.onLevelPtsUpdated, this); 32311 } 32312 } 32313 _unregisterListeners() { 32314 const { 32315 hls 32316 } = this; 32317 if (hls) { 32318 hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this); 32319 hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 32320 hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 32321 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 32322 hls.off(Events.FRAG_PARSING_METADATA, this.onFragParsingMetadata, this); 32323 hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this); 32324 hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this); 32325 hls.off(Events.LEVEL_PTS_UPDATED, this.onLevelPtsUpdated, this); 32326 } 32327 } 32328 // Add ID3 metatadata text track. 32329 onMediaAttaching(event, data) { 32330 var _data$overrides; 32331 this.media = data.media; 32332 if (((_data$overrides = data.overrides) == null ? void 0 : _data$overrides.cueRemoval) === false) { 32333 this.removeCues = false; 32334 } 32335 } 32336 onMediaAttached() { 32337 var _this$hls; 32338 const details = (_this$hls = this.hls) == null ? void 0 : _this$hls.latestLevelDetails; 32339 if (details) { 32340 this.updateDateRangeCues(details); 32341 } 32342 } 32343 onMediaDetaching(event, data) { 32344 this.media = null; 32345 const transferringMedia = !!data.transferMedia; 32346 if (transferringMedia) { 32347 return; 32348 } 32349 if (this.id3Track) { 32350 if (this.removeCues) { 32351 clearCurrentCues(this.id3Track, this.onEventCueEnter); 32352 } 32353 this.id3Track = null; 32354 } 32355 this.dateRangeCuesAppended = {}; 32356 } 32357 onManifestLoading() { 32358 this.dateRangeCuesAppended = {}; 32359 } 32360 createTrack(media) { 32361 const track = this.getID3Track(media.textTracks); 32362 track.mode = 'hidden'; 32363 return track; 32364 } 32365 getID3Track(textTracks) { 32366 if (!this.media) { 32367 return; 32368 } 32369 for (let i = 0; i < textTracks.length; i++) { 32370 const textTrack = textTracks[i]; 32371 if (textTrack.kind === 'metadata' && textTrack.label === 'id3') { 32372 // send 'addtrack' when reusing the textTrack for metadata, 32373 // same as what we do for captions 32374 sendAddTrackEvent(textTrack, this.media); 32375 return textTrack; 32376 } 32377 } 32378 return this.media.addTextTrack('metadata', 'id3'); 32379 } 32380 onFragParsingMetadata(event, data) { 32381 if (!this.media || !this.hls) { 32382 return; 32383 } 32384 const { 32385 enableEmsgMetadataCues, 32386 enableID3MetadataCues 32387 } = this.hls.config; 32388 if (!enableEmsgMetadataCues && !enableID3MetadataCues) { 32389 return; 32390 } 32391 const { 32392 samples 32393 } = data; 32394 32395 // create track dynamically 32396 if (!this.id3Track) { 32397 this.id3Track = this.createTrack(this.media); 32398 } 32399 const Cue = getCueClass(); 32400 if (!Cue) { 32401 return; 32402 } 32403 for (let i = 0; i < samples.length; i++) { 32404 const type = samples[i].type; 32405 if (type === MetadataSchema.emsg && !enableEmsgMetadataCues || !enableID3MetadataCues) { 32406 continue; 32407 } 32408 const frames = getId3Frames(samples[i].data); 32409 const startTime = samples[i].pts; 32410 let endTime = startTime + samples[i].duration; 32411 if (endTime > MAX_CUE_ENDTIME) { 32412 endTime = MAX_CUE_ENDTIME; 32413 } 32414 const timeDiff = endTime - startTime; 32415 if (timeDiff <= 0) { 32416 endTime = startTime + MIN_CUE_DURATION; 32417 } 32418 for (let j = 0; j < frames.length; j++) { 32419 const frame = frames[j]; 32420 // Safari doesn't put the timestamp frame in the TextTrack 32421 if (!isId3TimestampFrame(frame)) { 32422 // add a bounds to any unbounded cues 32423 this.updateId3CueEnds(startTime, type); 32424 const cue = createCueWithDataFields(Cue, startTime, endTime, frame, type); 32425 if (cue) { 32426 this.id3Track.addCue(cue); 32427 } 32428 } 32429 } 32430 } 32431 } 32432 updateId3CueEnds(startTime, type) { 32433 var _this$id3Track; 32434 const cues = (_this$id3Track = this.id3Track) == null ? void 0 : _this$id3Track.cues; 32435 if (cues) { 32436 for (let i = cues.length; i--;) { 32437 const cue = cues[i]; 32438 if (cue.type === type && cue.startTime < startTime && cue.endTime === MAX_CUE_ENDTIME) { 32439 cue.endTime = startTime; 32440 } 32441 } 32442 } 32443 } 32444 onBufferFlushing(event, { 32445 startOffset, 32446 endOffset, 32447 type 32448 }) { 32449 const { 32450 id3Track, 32451 hls 32452 } = this; 32453 if (!hls) { 32454 return; 32455 } 32456 const { 32457 config: { 32458 enableEmsgMetadataCues, 32459 enableID3MetadataCues 32460 } 32461 } = hls; 32462 if (id3Track && (enableEmsgMetadataCues || enableID3MetadataCues)) { 32463 let predicate; 32464 if (type === 'audio') { 32465 predicate = cue => cue.type === MetadataSchema.audioId3 && enableID3MetadataCues; 32466 } else if (type === 'video') { 32467 predicate = cue => cue.type === MetadataSchema.emsg && enableEmsgMetadataCues; 32468 } else { 32469 predicate = cue => cue.type === MetadataSchema.audioId3 && enableID3MetadataCues || cue.type === MetadataSchema.emsg && enableEmsgMetadataCues; 32470 } 32471 removeCuesInRange(id3Track, startOffset, endOffset, predicate); 32472 } 32473 } 32474 onLevelUpdated(event, { 32475 details 32476 }) { 32477 this.updateDateRangeCues(details, true); 32478 } 32479 onLevelPtsUpdated(event, data) { 32480 if (Math.abs(data.drift) > 0.01) { 32481 this.updateDateRangeCues(data.details); 32482 } 32483 } 32484 updateDateRangeCues(details, removeOldCues) { 32485 if (!this.hls || !this.media) { 32486 return; 32487 } 32488 const { 32489 assetPlayerId, 32490 timelineOffset, 32491 enableDateRangeMetadataCues, 32492 interstitialsController 32493 } = this.hls.config; 32494 if (!enableDateRangeMetadataCues) { 32495 return; 32496 } 32497 const Cue = getCueClass(); 32498 if (assetPlayerId && timelineOffset && !interstitialsController) { 32499 const { 32500 fragmentStart, 32501 fragmentEnd 32502 } = details; 32503 let cue = this.assetCue; 32504 if (cue) { 32505 cue.startTime = fragmentStart; 32506 cue.endTime = fragmentEnd; 32507 } else if (Cue) { 32508 cue = this.assetCue = createCueWithDataFields(Cue, fragmentStart, fragmentEnd, { 32509 assetPlayerId: this.hls.config.assetPlayerId 32510 }, 'hlsjs.interstitial.asset'); 32511 if (cue) { 32512 cue.id = assetPlayerId; 32513 this.id3Track || (this.id3Track = this.createTrack(this.media)); 32514 this.id3Track.addCue(cue); 32515 cue.addEventListener('enter', this.onEventCueEnter); 32516 } 32517 } 32518 } 32519 if (!details.hasProgramDateTime) { 32520 return; 32521 } 32522 const { 32523 id3Track 32524 } = this; 32525 const { 32526 dateRanges 32527 } = details; 32528 const ids = Object.keys(dateRanges); 32529 let dateRangeCuesAppended = this.dateRangeCuesAppended; 32530 // Remove cues from track not found in details.dateRanges 32531 if (id3Track && removeOldCues) { 32532 var _id3Track$cues; 32533 if ((_id3Track$cues = id3Track.cues) != null && _id3Track$cues.length) { 32534 const idsToRemove = Object.keys(dateRangeCuesAppended).filter(id => !ids.includes(id)); 32535 for (let i = idsToRemove.length; i--;) { 32536 var _dateRangeCuesAppende; 32537 const id = idsToRemove[i]; 32538 const cues = (_dateRangeCuesAppende = dateRangeCuesAppended[id]) == null ? void 0 : _dateRangeCuesAppende.cues; 32539 delete dateRangeCuesAppended[id]; 32540 if (cues) { 32541 Object.keys(cues).forEach(key => { 32542 const cue = cues[key]; 32543 if (cue) { 32544 cue.removeEventListener('enter', this.onEventCueEnter); 32545 try { 32546 id3Track.removeCue(cue); 32547 } catch (e) { 32548 /* no-op */ 32549 } 32550 } 32551 }); 32552 } 32553 } 32554 } else { 32555 dateRangeCuesAppended = this.dateRangeCuesAppended = {}; 32556 } 32557 } 32558 // Exit if the playlist does not have Date Ranges or does not have Program Date Time 32559 const lastFragment = details.fragments[details.fragments.length - 1]; 32560 if (ids.length === 0 || !isFiniteNumber(lastFragment == null ? void 0 : lastFragment.programDateTime)) { 32561 return; 32562 } 32563 this.id3Track || (this.id3Track = this.createTrack(this.media)); 32564 for (let i = 0; i < ids.length; i++) { 32565 const id = ids[i]; 32566 const dateRange = dateRanges[id]; 32567 const startTime = dateRange.startTime; 32568 32569 // Process DateRanges to determine end-time (known DURATION, END-DATE, or END-ON-NEXT) 32570 const appendedDateRangeCues = dateRangeCuesAppended[id]; 32571 const cues = (appendedDateRangeCues == null ? void 0 : appendedDateRangeCues.cues) || {}; 32572 let durationKnown = (appendedDateRangeCues == null ? void 0 : appendedDateRangeCues.durationKnown) || false; 32573 let endTime = MAX_CUE_ENDTIME; 32574 const { 32575 duration, 32576 endDate 32577 } = dateRange; 32578 if (endDate && duration !== null) { 32579 endTime = startTime + duration; 32580 durationKnown = true; 32581 } else if (dateRange.endOnNext && !durationKnown) { 32582 const nextDateRangeWithSameClass = ids.reduce((candidateDateRange, id) => { 32583 if (id !== dateRange.id) { 32584 const otherDateRange = dateRanges[id]; 32585 if (otherDateRange.class === dateRange.class && otherDateRange.startDate > dateRange.startDate && (!candidateDateRange || dateRange.startDate < candidateDateRange.startDate)) { 32586 return otherDateRange; 32587 } 32588 } 32589 return candidateDateRange; 32590 }, null); 32591 if (nextDateRangeWithSameClass) { 32592 endTime = nextDateRangeWithSameClass.startTime; 32593 durationKnown = true; 32594 } 32595 } 32596 32597 // Create TextTrack Cues for each MetadataGroup Item (select DateRange attribute) 32598 // This is to emulate Safari HLS playback handling of DateRange tags 32599 const attributes = Object.keys(dateRange.attr); 32600 for (let j = 0; j < attributes.length; j++) { 32601 const key = attributes[j]; 32602 if (!isDateRangeCueAttribute(key)) { 32603 continue; 32604 } 32605 const cue = cues[key]; 32606 if (cue) { 32607 if (durationKnown && !(appendedDateRangeCues != null && appendedDateRangeCues.durationKnown)) { 32608 cue.endTime = endTime; 32609 } else if (Math.abs(cue.startTime - startTime) > 0.01) { 32610 cue.startTime = startTime; 32611 cue.endTime = endTime; 32612 } 32613 } else if (Cue) { 32614 let data = dateRange.attr[key]; 32615 if (isSCTE35Attribute(key)) { 32616 data = hexToArrayBuffer(data); 32617 } 32618 const payload = { 32619 key, 32620 data 32621 }; 32622 const _cue = createCueWithDataFields(Cue, startTime, endTime, payload, MetadataSchema.dateRange); 32623 if (_cue) { 32624 _cue.id = id; 32625 this.id3Track.addCue(_cue); 32626 cues[key] = _cue; 32627 if (interstitialsController) { 32628 if (key === 'X-ASSET-LIST' || key === 'X-ASSET-URL') { 32629 _cue.addEventListener('enter', this.onEventCueEnter); 32630 } 32631 } 32632 } 32633 } 32634 } 32635 32636 // Keep track of processed DateRanges by ID for updating cues with new DateRange tag attributes 32637 dateRangeCuesAppended[id] = { 32638 cues, 32639 dateRange, 32640 durationKnown 32641 }; 32642 } 32643 } 32644} 32645 32646class LatencyController { 32647 constructor(hls) { 32648 this.hls = void 0; 32649 this.config = void 0; 32650 this.media = null; 32651 this.currentTime = 0; 32652 this.stallCount = 0; 32653 this._latency = null; 32654 this._targetLatencyUpdated = false; 32655 this.onTimeupdate = () => { 32656 const { 32657 media 32658 } = this; 32659 const levelDetails = this.levelDetails; 32660 if (!media || !levelDetails) { 32661 return; 32662 } 32663 this.currentTime = media.currentTime; 32664 const latency = this.computeLatency(); 32665 if (latency === null) { 32666 return; 32667 } 32668 this._latency = latency; 32669 32670 // Adapt playbackRate to meet target latency in low-latency mode 32671 const { 32672 lowLatencyMode, 32673 maxLiveSyncPlaybackRate 32674 } = this.config; 32675 if (!lowLatencyMode || maxLiveSyncPlaybackRate === 1 || !levelDetails.live) { 32676 return; 32677 } 32678 const targetLatency = this.targetLatency; 32679 if (targetLatency === null) { 32680 return; 32681 } 32682 const distanceFromTarget = latency - targetLatency; 32683 // Only adjust playbackRate when within one target duration of targetLatency 32684 // and more than one second from under-buffering. 32685 // Playback further than one target duration from target can be considered DVR playback. 32686 const liveMinLatencyDuration = Math.min(this.maxLatency, targetLatency + levelDetails.targetduration); 32687 const inLiveRange = distanceFromTarget < liveMinLatencyDuration; 32688 if (inLiveRange && distanceFromTarget > 0.05 && this.forwardBufferLength > 1) { 32689 const max = Math.min(2, Math.max(1.0, maxLiveSyncPlaybackRate)); 32690 const rate = Math.round(2 / (1 + Math.exp(-0.75 * distanceFromTarget - this.edgeStalled)) * 20) / 20; 32691 const playbackRate = Math.min(max, Math.max(1, rate)); 32692 this.changeMediaPlaybackRate(media, playbackRate); 32693 } else if (media.playbackRate !== 1 && media.playbackRate !== 0) { 32694 this.changeMediaPlaybackRate(media, 1); 32695 } 32696 }; 32697 this.hls = hls; 32698 this.config = hls.config; 32699 this.registerListeners(); 32700 } 32701 get levelDetails() { 32702 var _this$hls; 32703 return ((_this$hls = this.hls) == null ? void 0 : _this$hls.latestLevelDetails) || null; 32704 } 32705 get latency() { 32706 return this._latency || 0; 32707 } 32708 get maxLatency() { 32709 const { 32710 config 32711 } = this; 32712 if (config.liveMaxLatencyDuration !== undefined) { 32713 return config.liveMaxLatencyDuration; 32714 } 32715 const levelDetails = this.levelDetails; 32716 return levelDetails ? config.liveMaxLatencyDurationCount * levelDetails.targetduration : 0; 32717 } 32718 get targetLatency() { 32719 const levelDetails = this.levelDetails; 32720 if (levelDetails === null || this.hls === null) { 32721 return null; 32722 } 32723 const { 32724 holdBack, 32725 partHoldBack, 32726 targetduration 32727 } = levelDetails; 32728 const { 32729 liveSyncDuration, 32730 liveSyncDurationCount, 32731 lowLatencyMode 32732 } = this.config; 32733 const userConfig = this.hls.userConfig; 32734 let targetLatency = lowLatencyMode ? partHoldBack || holdBack : holdBack; 32735 if (this._targetLatencyUpdated || userConfig.liveSyncDuration || userConfig.liveSyncDurationCount || targetLatency === 0) { 32736 targetLatency = liveSyncDuration !== undefined ? liveSyncDuration : liveSyncDurationCount * targetduration; 32737 } 32738 const maxLiveSyncOnStallIncrease = targetduration; 32739 return targetLatency + Math.min(this.stallCount * this.config.liveSyncOnStallIncrease, maxLiveSyncOnStallIncrease); 32740 } 32741 set targetLatency(latency) { 32742 this.stallCount = 0; 32743 this.config.liveSyncDuration = latency; 32744 this._targetLatencyUpdated = true; 32745 } 32746 get liveSyncPosition() { 32747 const liveEdge = this.estimateLiveEdge(); 32748 const targetLatency = this.targetLatency; 32749 if (liveEdge === null || targetLatency === null) { 32750 return null; 32751 } 32752 const levelDetails = this.levelDetails; 32753 if (levelDetails === null) { 32754 return null; 32755 } 32756 const edge = levelDetails.edge; 32757 const syncPosition = liveEdge - targetLatency - this.edgeStalled; 32758 const min = edge - levelDetails.totalduration; 32759 const max = edge - (this.config.lowLatencyMode && levelDetails.partTarget || levelDetails.targetduration); 32760 return Math.min(Math.max(min, syncPosition), max); 32761 } 32762 get drift() { 32763 const levelDetails = this.levelDetails; 32764 if (levelDetails === null) { 32765 return 1; 32766 } 32767 return levelDetails.drift; 32768 } 32769 get edgeStalled() { 32770 const levelDetails = this.levelDetails; 32771 if (levelDetails === null) { 32772 return 0; 32773 } 32774 const maxLevelUpdateAge = (this.config.lowLatencyMode && levelDetails.partTarget || levelDetails.targetduration) * 3; 32775 return Math.max(levelDetails.age - maxLevelUpdateAge, 0); 32776 } 32777 get forwardBufferLength() { 32778 const { 32779 media 32780 } = this; 32781 const levelDetails = this.levelDetails; 32782 if (!media || !levelDetails) { 32783 return 0; 32784 } 32785 const bufferedRanges = media.buffered.length; 32786 return (bufferedRanges ? media.buffered.end(bufferedRanges - 1) : levelDetails.edge) - this.currentTime; 32787 } 32788 destroy() { 32789 this.unregisterListeners(); 32790 this.onMediaDetaching(); 32791 this.hls = null; 32792 } 32793 registerListeners() { 32794 const { 32795 hls 32796 } = this; 32797 if (!hls) { 32798 return; 32799 } 32800 hls.on(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 32801 hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 32802 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 32803 hls.on(Events.LEVEL_UPDATED, this.onLevelUpdated, this); 32804 hls.on(Events.ERROR, this.onError, this); 32805 } 32806 unregisterListeners() { 32807 const { 32808 hls 32809 } = this; 32810 if (!hls) { 32811 return; 32812 } 32813 hls.off(Events.MEDIA_ATTACHED, this.onMediaAttached, this); 32814 hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this); 32815 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 32816 hls.off(Events.LEVEL_UPDATED, this.onLevelUpdated, this); 32817 hls.off(Events.ERROR, this.onError, this); 32818 } 32819 onMediaAttached(event, data) { 32820 this.media = data.media; 32821 this.media.addEventListener('timeupdate', this.onTimeupdate); 32822 } 32823 onMediaDetaching() { 32824 if (this.media) { 32825 this.media.removeEventListener('timeupdate', this.onTimeupdate); 32826 this.media = null; 32827 } 32828 } 32829 onManifestLoading() { 32830 this._latency = null; 32831 this.stallCount = 0; 32832 } 32833 onLevelUpdated(event, { 32834 details 32835 }) { 32836 if (details.advanced) { 32837 this.onTimeupdate(); 32838 } 32839 if (!details.live && this.media) { 32840 this.media.removeEventListener('timeupdate', this.onTimeupdate); 32841 } 32842 } 32843 onError(event, data) { 32844 var _this$levelDetails; 32845 if (data.details !== ErrorDetails.BUFFER_STALLED_ERROR) { 32846 return; 32847 } 32848 this.stallCount++; 32849 if (this.hls && (_this$levelDetails = this.levelDetails) != null && _this$levelDetails.live) { 32850 this.hls.logger.warn('[latency-controller]: Stall detected, adjusting target latency'); 32851 } 32852 } 32853 changeMediaPlaybackRate(media, playbackRate) { 32854 var _this$hls2, _this$targetLatency; 32855 if (media.playbackRate === playbackRate) { 32856 return; 32857 } 32858 (_this$hls2 = this.hls) == null || _this$hls2.logger.debug(`[latency-controller]: latency=${this.latency.toFixed(3)}, targetLatency=${(_this$targetLatency = this.targetLatency) == null ? void 0 : _this$targetLatency.toFixed(3)}, forwardBufferLength=${this.forwardBufferLength.toFixed(3)}: adjusting playback rate from ${media.playbackRate} to ${playbackRate}`); 32859 media.playbackRate = playbackRate; 32860 } 32861 estimateLiveEdge() { 32862 const levelDetails = this.levelDetails; 32863 if (levelDetails === null) { 32864 return null; 32865 } 32866 return levelDetails.edge + levelDetails.age; 32867 } 32868 computeLatency() { 32869 const liveEdge = this.estimateLiveEdge(); 32870 if (liveEdge === null) { 32871 return null; 32872 } 32873 return liveEdge - this.currentTime; 32874 } 32875} 32876 32877class LevelController extends BasePlaylistController { 32878 constructor(hls, contentSteeringController) { 32879 super(hls, 'level-controller'); 32880 this._levels = []; 32881 this._firstLevel = -1; 32882 this._maxAutoLevel = -1; 32883 this._startLevel = void 0; 32884 this.currentLevel = null; 32885 this.currentLevelIndex = -1; 32886 this.manualLevelIndex = -1; 32887 this.steering = void 0; 32888 this.onParsedComplete = void 0; 32889 this.steering = contentSteeringController; 32890 this._registerListeners(); 32891 } 32892 _registerListeners() { 32893 const { 32894 hls 32895 } = this; 32896 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 32897 hls.on(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 32898 hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this); 32899 hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); 32900 hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this); 32901 hls.on(Events.ERROR, this.onError, this); 32902 } 32903 _unregisterListeners() { 32904 const { 32905 hls 32906 } = this; 32907 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 32908 hls.off(Events.MANIFEST_LOADED, this.onManifestLoaded, this); 32909 hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this); 32910 hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); 32911 hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this); 32912 hls.off(Events.ERROR, this.onError, this); 32913 } 32914 destroy() { 32915 this._unregisterListeners(); 32916 this.steering = null; 32917 this.resetLevels(); 32918 super.destroy(); 32919 } 32920 stopLoad() { 32921 const levels = this._levels; 32922 32923 // clean up live level details to force reload them, and reset load errors 32924 levels.forEach(level => { 32925 level.loadError = 0; 32926 level.fragmentError = 0; 32927 }); 32928 super.stopLoad(); 32929 } 32930 resetLevels() { 32931 this._startLevel = undefined; 32932 this.manualLevelIndex = -1; 32933 this.currentLevelIndex = -1; 32934 this.currentLevel = null; 32935 this._levels = []; 32936 this._maxAutoLevel = -1; 32937 } 32938 onManifestLoading(event, data) { 32939 this.resetLevels(); 32940 } 32941 onManifestLoaded(event, data) { 32942 const preferManagedMediaSource = this.hls.config.preferManagedMediaSource; 32943 const levels = []; 32944 const redundantSet = {}; 32945 const generatePathwaySet = {}; 32946 let resolutionFound = false; 32947 let videoCodecFound = false; 32948 let audioCodecFound = false; 32949 data.levels.forEach(levelParsed => { 32950 const attributes = levelParsed.attrs; 32951 let { 32952 audioCodec, 32953 videoCodec 32954 } = levelParsed; 32955 if (audioCodec) { 32956 // Returns empty and set to undefined for 'mp4a.40.34' with fallback to 'audio/mpeg' SourceBuffer 32957 levelParsed.audioCodec = audioCodec = getCodecCompatibleName(audioCodec, preferManagedMediaSource) || undefined; 32958 } 32959 if (videoCodec) { 32960 videoCodec = levelParsed.videoCodec = convertAVC1ToAVCOTI(videoCodec); 32961 } 32962 32963 // only keep levels with supported audio/video codecs 32964 const { 32965 width, 32966 height, 32967 unknownCodecs 32968 } = levelParsed; 32969 const unknownUnsupportedCodecCount = (unknownCodecs == null ? void 0 : unknownCodecs.length) || 0; 32970 resolutionFound || (resolutionFound = !!(width && height)); 32971 videoCodecFound || (videoCodecFound = !!videoCodec); 32972 audioCodecFound || (audioCodecFound = !!audioCodec); 32973 if (unknownUnsupportedCodecCount || audioCodec && !this.isAudioSupported(audioCodec) || videoCodec && !this.isVideoSupported(videoCodec)) { 32974 this.log(`Some or all CODECS not supported "${attributes.CODECS}"`); 32975 return; 32976 } 32977 const { 32978 CODECS, 32979 'FRAME-RATE': FRAMERATE, 32980 'HDCP-LEVEL': HDCP, 32981 'PATHWAY-ID': PATHWAY, 32982 RESOLUTION, 32983 'VIDEO-RANGE': VIDEO_RANGE 32984 } = attributes; 32985 const contentSteeringPrefix = `${PATHWAY || '.'}-`; 32986 const levelKey = `${contentSteeringPrefix}${levelParsed.bitrate}-${RESOLUTION}-${FRAMERATE}-${CODECS}-${VIDEO_RANGE}-${HDCP}`; 32987 if (!redundantSet[levelKey]) { 32988 const level = this.createLevel(levelParsed); 32989 redundantSet[levelKey] = level; 32990 generatePathwaySet[levelKey] = 1; 32991 levels.push(level); 32992 } else if (redundantSet[levelKey].uri !== levelParsed.url && !levelParsed.attrs['PATHWAY-ID']) { 32993 // Assign Pathway IDs to Redundant Streams (default Pathways is ".". Redundant Streams "..", "...", and so on.) 32994 // Content Steering controller to handles Pathway fallback on error 32995 const pathwayCount = generatePathwaySet[levelKey] += 1; 32996 levelParsed.attrs['PATHWAY-ID'] = new Array(pathwayCount + 1).join('.'); 32997 const level = this.createLevel(levelParsed); 32998 redundantSet[levelKey] = level; 32999 levels.push(level); 33000 } else { 33001 redundantSet[levelKey].addGroupId('audio', attributes.AUDIO); 33002 redundantSet[levelKey].addGroupId('text', attributes.SUBTITLES); 33003 } 33004 }); 33005 this.filterAndSortMediaOptions(levels, data, resolutionFound, videoCodecFound, audioCodecFound); 33006 } 33007 createLevel(levelParsed) { 33008 const level = new Level(levelParsed); 33009 const supplemental = levelParsed.supplemental; 33010 if (supplemental != null && supplemental.videoCodec && !this.isVideoSupported(supplemental.videoCodec)) { 33011 const error = new Error(`SUPPLEMENTAL-CODECS not supported "${supplemental.videoCodec}"`); 33012 this.log(error.message); 33013 level.supportedResult = getUnsupportedResult(error, []); 33014 } 33015 return level; 33016 } 33017 isAudioSupported(codec) { 33018 return areCodecsMediaSourceSupported(codec, 'audio', this.hls.config.preferManagedMediaSource); 33019 } 33020 isVideoSupported(codec) { 33021 return areCodecsMediaSourceSupported(codec, 'video', this.hls.config.preferManagedMediaSource); 33022 } 33023 filterAndSortMediaOptions(filteredLevels, data, resolutionFound, videoCodecFound, audioCodecFound) { 33024 var _data$stats; 33025 let audioTracks = []; 33026 let subtitleTracks = []; 33027 let levels = filteredLevels; 33028 const statsParsing = ((_data$stats = data.stats) == null ? void 0 : _data$stats.parsing) || {}; 33029 33030 // remove audio-only and invalid video-range levels if we also have levels with video codecs or RESOLUTION signalled 33031 if ((resolutionFound || videoCodecFound) && audioCodecFound) { 33032 levels = levels.filter(({ 33033 videoCodec, 33034 videoRange, 33035 width, 33036 height 33037 }) => (!!videoCodec || !!(width && height)) && isVideoRange(videoRange)); 33038 } 33039 if (levels.length === 0) { 33040 // Dispatch error after MANIFEST_LOADED is done propagating 33041 // eslint-disable-next-line @typescript-eslint/no-floating-promises 33042 Promise.resolve().then(() => { 33043 if (this.hls) { 33044 let message = 'no level with compatible codecs found in manifest'; 33045 let reason = message; 33046 if (data.levels.length) { 33047 reason = `one or more CODECS in variant not supported: ${stringify(data.levels.map(level => level.attrs.CODECS).filter((value, index, array) => array.indexOf(value) === index))}`; 33048 this.warn(reason); 33049 message += ` (${reason})`; 33050 } 33051 const error = new Error(message); 33052 this.hls.trigger(Events.ERROR, { 33053 type: ErrorTypes.MEDIA_ERROR, 33054 details: ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR, 33055 fatal: true, 33056 url: data.url, 33057 error, 33058 reason 33059 }); 33060 } 33061 }); 33062 statsParsing.end = performance.now(); 33063 return; 33064 } 33065 if (data.audioTracks) { 33066 audioTracks = data.audioTracks.filter(track => !track.audioCodec || this.isAudioSupported(track.audioCodec)); 33067 // Assign ids after filtering as array indices by group-id 33068 assignTrackIdsByGroup(audioTracks); 33069 } 33070 if (data.subtitles) { 33071 subtitleTracks = data.subtitles; 33072 assignTrackIdsByGroup(subtitleTracks); 33073 } 33074 // start bitrate is the first bitrate of the manifest 33075 const unsortedLevels = levels.slice(0); 33076 // sort levels from lowest to highest 33077 levels.sort((a, b) => { 33078 if (a.attrs['HDCP-LEVEL'] !== b.attrs['HDCP-LEVEL']) { 33079 return (a.attrs['HDCP-LEVEL'] || '') > (b.attrs['HDCP-LEVEL'] || '') ? 1 : -1; 33080 } 33081 // sort on height before bitrate for cap-level-controller 33082 if (resolutionFound && a.height !== b.height) { 33083 return a.height - b.height; 33084 } 33085 if (a.frameRate !== b.frameRate) { 33086 return a.frameRate - b.frameRate; 33087 } 33088 if (a.videoRange !== b.videoRange) { 33089 return VideoRangeValues.indexOf(a.videoRange) - VideoRangeValues.indexOf(b.videoRange); 33090 } 33091 if (a.videoCodec !== b.videoCodec) { 33092 const valueA = videoCodecPreferenceValue(a.videoCodec); 33093 const valueB = videoCodecPreferenceValue(b.videoCodec); 33094 if (valueA !== valueB) { 33095 return valueB - valueA; 33096 } 33097 } 33098 if (a.uri === b.uri && a.codecSet !== b.codecSet) { 33099 const valueA = codecsSetSelectionPreferenceValue(a.codecSet); 33100 const valueB = codecsSetSelectionPreferenceValue(b.codecSet); 33101 if (valueA !== valueB) { 33102 return valueB - valueA; 33103 } 33104 } 33105 if (a.averageBitrate !== b.averageBitrate) { 33106 return a.averageBitrate - b.averageBitrate; 33107 } 33108 return 0; 33109 }); 33110 let firstLevelInPlaylist = unsortedLevels[0]; 33111 if (this.steering) { 33112 levels = this.steering.filterParsedLevels(levels); 33113 if (levels.length !== unsortedLevels.length) { 33114 for (let i = 0; i < unsortedLevels.length; i++) { 33115 if (unsortedLevels[i].pathwayId === levels[0].pathwayId) { 33116 firstLevelInPlaylist = unsortedLevels[i]; 33117 break; 33118 } 33119 } 33120 } 33121 } 33122 this._levels = levels; 33123 33124 // find index of first level in sorted levels 33125 for (let i = 0; i < levels.length; i++) { 33126 if (levels[i] === firstLevelInPlaylist) { 33127 var _this$hls$userConfig; 33128 this._firstLevel = i; 33129 const firstLevelBitrate = firstLevelInPlaylist.bitrate; 33130 const bandwidthEstimate = this.hls.bandwidthEstimate; 33131 this.log(`manifest loaded, ${levels.length} level(s) found, first bitrate: ${firstLevelBitrate}`); 33132 // Update default bwe to first variant bitrate as long it has not been configured or set 33133 if (((_this$hls$userConfig = this.hls.userConfig) == null ? void 0 : _this$hls$userConfig.abrEwmaDefaultEstimate) === undefined) { 33134 const startingBwEstimate = Math.min(firstLevelBitrate, this.hls.config.abrEwmaDefaultEstimateMax); 33135 if (startingBwEstimate > bandwidthEstimate && bandwidthEstimate === this.hls.abrEwmaDefaultEstimate) { 33136 this.hls.bandwidthEstimate = startingBwEstimate; 33137 } 33138 } 33139 break; 33140 } 33141 } 33142 33143 // Audio is only alternate if manifest include a URI along with the audio group tag, 33144 // and this is not an audio-only stream where levels contain audio-only 33145 const audioOnly = audioCodecFound && !videoCodecFound; 33146 const config = this.hls.config; 33147 const altAudioEnabled = !!(config.audioStreamController && config.audioTrackController); 33148 const edata = { 33149 levels, 33150 audioTracks, 33151 subtitleTracks, 33152 sessionData: data.sessionData, 33153 sessionKeys: data.sessionKeys, 33154 firstLevel: this._firstLevel, 33155 stats: data.stats, 33156 audio: audioCodecFound, 33157 video: videoCodecFound, 33158 altAudio: altAudioEnabled && !audioOnly && audioTracks.some(t => !!t.url) 33159 }; 33160 statsParsing.end = performance.now(); 33161 this.hls.trigger(Events.MANIFEST_PARSED, edata); 33162 } 33163 get levels() { 33164 if (this._levels.length === 0) { 33165 return null; 33166 } 33167 return this._levels; 33168 } 33169 get loadLevelObj() { 33170 return this.currentLevel; 33171 } 33172 get level() { 33173 return this.currentLevelIndex; 33174 } 33175 set level(newLevel) { 33176 const levels = this._levels; 33177 if (levels.length === 0) { 33178 return; 33179 } 33180 // check if level idx is valid 33181 if (newLevel < 0 || newLevel >= levels.length) { 33182 // invalid level id given, trigger error 33183 const error = new Error('invalid level idx'); 33184 const fatal = newLevel < 0; 33185 this.hls.trigger(Events.ERROR, { 33186 type: ErrorTypes.OTHER_ERROR, 33187 details: ErrorDetails.LEVEL_SWITCH_ERROR, 33188 level: newLevel, 33189 fatal, 33190 error, 33191 reason: error.message 33192 }); 33193 if (fatal) { 33194 return; 33195 } 33196 newLevel = Math.min(newLevel, levels.length - 1); 33197 } 33198 const lastLevelIndex = this.currentLevelIndex; 33199 const lastLevel = this.currentLevel; 33200 const lastPathwayId = lastLevel ? lastLevel.attrs['PATHWAY-ID'] : undefined; 33201 const level = levels[newLevel]; 33202 const pathwayId = level.attrs['PATHWAY-ID']; 33203 this.currentLevelIndex = newLevel; 33204 this.currentLevel = level; 33205 if (lastLevelIndex === newLevel && lastLevel && lastPathwayId === pathwayId) { 33206 return; 33207 } 33208 this.log(`Switching to level ${newLevel} (${level.height ? level.height + 'p ' : ''}${level.videoRange ? level.videoRange + ' ' : ''}${level.codecSet ? level.codecSet + ' ' : ''}@${level.bitrate})${pathwayId ? ' with Pathway ' + pathwayId : ''} from level ${lastLevelIndex}${lastPathwayId ? ' with Pathway ' + lastPathwayId : ''}`); 33209 const levelSwitchingData = { 33210 level: newLevel, 33211 attrs: level.attrs, 33212 details: level.details, 33213 bitrate: level.bitrate, 33214 averageBitrate: level.averageBitrate, 33215 maxBitrate: level.maxBitrate, 33216 realBitrate: level.realBitrate, 33217 width: level.width, 33218 height: level.height, 33219 codecSet: level.codecSet, 33220 audioCodec: level.audioCodec, 33221 videoCodec: level.videoCodec, 33222 audioGroups: level.audioGroups, 33223 subtitleGroups: level.subtitleGroups, 33224 loaded: level.loaded, 33225 loadError: level.loadError, 33226 fragmentError: level.fragmentError, 33227 name: level.name, 33228 id: level.id, 33229 uri: level.uri, 33230 url: level.url, 33231 urlId: 0, 33232 audioGroupIds: level.audioGroupIds, 33233 textGroupIds: level.textGroupIds 33234 }; 33235 this.hls.trigger(Events.LEVEL_SWITCHING, levelSwitchingData); 33236 // check if we need to load playlist for this level 33237 const levelDetails = level.details; 33238 if (!levelDetails || levelDetails.live) { 33239 // level not retrieved yet, or live playlist we need to (re)load it 33240 const hlsUrlParameters = this.switchParams(level.uri, lastLevel == null ? void 0 : lastLevel.details, levelDetails); 33241 this.loadPlaylist(hlsUrlParameters); 33242 } 33243 } 33244 get manualLevel() { 33245 return this.manualLevelIndex; 33246 } 33247 set manualLevel(newLevel) { 33248 this.manualLevelIndex = newLevel; 33249 if (this._startLevel === undefined) { 33250 this._startLevel = newLevel; 33251 } 33252 if (newLevel !== -1) { 33253 this.level = newLevel; 33254 } 33255 } 33256 get firstLevel() { 33257 return this._firstLevel; 33258 } 33259 set firstLevel(newLevel) { 33260 this._firstLevel = newLevel; 33261 } 33262 get startLevel() { 33263 // Setting hls.startLevel (this._startLevel) overrides config.startLevel 33264 if (this._startLevel === undefined) { 33265 const configStartLevel = this.hls.config.startLevel; 33266 if (configStartLevel !== undefined) { 33267 return configStartLevel; 33268 } 33269 return this.hls.firstAutoLevel; 33270 } 33271 return this._startLevel; 33272 } 33273 set startLevel(newLevel) { 33274 this._startLevel = newLevel; 33275 } 33276 get pathways() { 33277 if (this.steering) { 33278 return this.steering.pathways(); 33279 } 33280 return []; 33281 } 33282 get pathwayPriority() { 33283 if (this.steering) { 33284 return this.steering.pathwayPriority; 33285 } 33286 return null; 33287 } 33288 set pathwayPriority(pathwayPriority) { 33289 if (this.steering) { 33290 const pathwaysList = this.steering.pathways(); 33291 const filteredPathwayPriority = pathwayPriority.filter(pathwayId => { 33292 return pathwaysList.indexOf(pathwayId) !== -1; 33293 }); 33294 if (pathwayPriority.length < 1) { 33295 this.warn(`pathwayPriority ${pathwayPriority} should contain at least one pathway from list: ${pathwaysList}`); 33296 return; 33297 } 33298 this.steering.pathwayPriority = filteredPathwayPriority; 33299 } 33300 } 33301 onError(event, data) { 33302 if (data.fatal || !data.context) { 33303 return; 33304 } 33305 if (data.context.type === PlaylistContextType.LEVEL && data.context.level === this.level) { 33306 this.checkRetry(data); 33307 } 33308 } 33309 33310 // reset errors on the successful load of a fragment 33311 onFragBuffered(event, { 33312 frag 33313 }) { 33314 if (frag !== undefined && frag.type === PlaylistLevelType.MAIN) { 33315 const el = frag.elementaryStreams; 33316 if (!Object.keys(el).some(type => !!el[type])) { 33317 return; 33318 } 33319 const level = this._levels[frag.level]; 33320 if (level != null && level.loadError) { 33321 this.log(`Resetting level error count of ${level.loadError} on frag buffered`); 33322 level.loadError = 0; 33323 } 33324 } 33325 } 33326 onLevelLoaded(event, data) { 33327 var _data$deliveryDirecti2; 33328 const { 33329 level, 33330 details 33331 } = data; 33332 const curLevel = data.levelInfo; 33333 if (!curLevel) { 33334 var _data$deliveryDirecti; 33335 this.warn(`Invalid level index ${level}`); 33336 if ((_data$deliveryDirecti = data.deliveryDirectives) != null && _data$deliveryDirecti.skip) { 33337 details.deltaUpdateFailed = true; 33338 } 33339 return; 33340 } 33341 33342 // only process level loaded events matching with expected level or prior to switch when media playlist is loaded directly 33343 if (curLevel === this.currentLevel || data.withoutMultiVariant) { 33344 // reset level load error counter on successful level loaded only if there is no issues with fragments 33345 if (curLevel.fragmentError === 0) { 33346 curLevel.loadError = 0; 33347 } 33348 // Ignore matching details populated by loading a Media Playlist directly 33349 let previousDetails = curLevel.details; 33350 if (previousDetails === data.details && previousDetails.advanced) { 33351 previousDetails = undefined; 33352 } 33353 this.playlistLoaded(level, data, previousDetails); 33354 } else if ((_data$deliveryDirecti2 = data.deliveryDirectives) != null && _data$deliveryDirecti2.skip) { 33355 // received a delta playlist update that cannot be merged 33356 details.deltaUpdateFailed = true; 33357 } 33358 } 33359 loadPlaylist(hlsUrlParameters) { 33360 super.loadPlaylist(); 33361 if (this.shouldLoadPlaylist(this.currentLevel)) { 33362 this.scheduleLoading(this.currentLevel, hlsUrlParameters); 33363 } 33364 } 33365 loadingPlaylist(currentLevel, hlsUrlParameters) { 33366 super.loadingPlaylist(currentLevel, hlsUrlParameters); 33367 const url = this.getUrlWithDirectives(currentLevel.uri, hlsUrlParameters); 33368 const currentLevelIndex = this.currentLevelIndex; 33369 const pathwayId = currentLevel.attrs['PATHWAY-ID']; 33370 const details = currentLevel.details; 33371 const age = details == null ? void 0 : details.age; 33372 this.log(`Loading level index ${currentLevelIndex}${(hlsUrlParameters == null ? void 0 : hlsUrlParameters.msn) !== undefined ? ' at sn ' + hlsUrlParameters.msn + ' part ' + hlsUrlParameters.part : ''}${pathwayId ? ' Pathway ' + pathwayId : ''}${age && details.live ? ' age ' + age.toFixed(1) + (details.type ? ' ' + details.type ||
333720
vendor: 12,252 bytes, lines 33372-33715
33372 : '') : ''} ${url}`); 33373 this.hls.trigger(Events.LEVEL_LOADING, { 33374 url, 33375 level: currentLevelIndex, 33376 levelInfo: currentLevel, 33377 pathwayId: currentLevel.attrs['PATHWAY-ID'], 33378 id: 0, 33379 // Deprecated Level urlId 33380 deliveryDirectives: hlsUrlParameters || null 33381 }); 33382 } 33383 get nextLoadLevel() { 33384 if (this.manualLevelIndex !== -1) { 33385 return this.manualLevelIndex; 33386 } else { 33387 return this.hls.nextAutoLevel; 33388 } 33389 } 33390 set nextLoadLevel(nextLevel) { 33391 this.level = nextLevel; 33392 if (this.manualLevelIndex === -1) { 33393 this.hls.nextAutoLevel = nextLevel; 33394 } 33395 } 33396 removeLevel(levelIndex) { 33397 var _this$currentLevel; 33398 if (this._levels.length === 1) { 33399 return; 33400 } 33401 const levels = this._levels.filter((level, index) => { 33402 if (index !== levelIndex) { 33403 return true; 33404 } 33405 if (this.steering) { 33406 this.steering.removeLevel(level); 33407 } 33408 if (level === this.currentLevel) { 33409 this.currentLevel = null; 33410 this.currentLevelIndex = -1; 33411 if (level.details) { 33412 level.details.fragments.forEach(f => f.level = -1); 33413 } 33414 } 33415 return false; 33416 }); 33417 reassignFragmentLevelIndexes(levels); 33418 this._levels = levels; 33419 if (this.currentLevelIndex > -1 && (_this$currentLevel = this.currentLevel) != null && _this$currentLevel.details) { 33420 this.currentLevelIndex = this.currentLevel.details.fragments[0].level; 33421 } 33422 if (this.manualLevelIndex > -1) { 33423 this.manualLevelIndex = this.currentLevelIndex; 33424 } 33425 const maxLevel = levels.length - 1; 33426 this._firstLevel = Math.min(this._firstLevel, maxLevel); 33427 if (this._startLevel) { 33428 this._startLevel = Math.min(this._startLevel, maxLevel); 33429 } 33430 this.hls.trigger(Events.LEVELS_UPDATED, { 33431 levels 33432 }); 33433 } 33434 onLevelsUpdated(event, { 33435 levels 33436 }) { 33437 this._levels = levels; 33438 } 33439 checkMaxAutoUpdated() { 33440 const { 33441 autoLevelCapping, 33442 maxAutoLevel, 33443 maxHdcpLevel 33444 } = this.hls; 33445 if (this._maxAutoLevel !== maxAutoLevel) { 33446 this._maxAutoLevel = maxAutoLevel; 33447 this.hls.trigger(Events.MAX_AUTO_LEVEL_UPDATED, { 33448 autoLevelCapping, 33449 levels: this.levels, 33450 maxAutoLevel, 33451 minAutoLevel: this.hls.minAutoLevel, 33452 maxHdcpLevel 33453 }); 33454 } 33455 } 33456} 33457function assignTrackIdsByGroup(tracks) { 33458 const groups = {}; 33459 tracks.forEach(track => { 33460 const groupId = track.groupId || ''; 33461 track.id = groups[groupId] = groups[groupId] || 0; 33462 groups[groupId]++; 33463 }); 33464} 33465 33466function getSourceBuffer() { 33467 return self.SourceBuffer || self.WebKitSourceBuffer; 33468} 33469function isMSESupported() { 33470 const mediaSource = getMediaSource(); 33471 if (!mediaSource) { 33472 return false; 33473 } 33474 33475 // if SourceBuffer is exposed ensure its API is valid 33476 // Older browsers do not expose SourceBuffer globally so checking SourceBuffer.prototype is impossible 33477 const sourceBuffer = getSourceBuffer(); 33478 return !sourceBuffer || sourceBuffer.prototype && typeof sourceBuffer.prototype.appendBuffer === 'function' && typeof sourceBuffer.prototype.remove === 'function'; 33479} 33480function isSupported() { 33481 if (!isMSESupported()) { 33482 return false; 33483 } 33484 const mediaSource = getMediaSource(); 33485 return typeof (mediaSource == null ? void 0 : mediaSource.isTypeSupported) === 'function' && (['avc1.42E01E,mp4a.40.2', 'av01.0.01M.08', 'vp09.00.50.08'].some(codecsForVideoContainer => mediaSource.isTypeSupported(mimeTypeForCodec(codecsForVideoContainer, 'video'))) || ['mp4a.40.2', 'fLaC'].some(codecForAudioContainer => mediaSource.isTypeSupported(mimeTypeForCodec(codecForAudioContainer, 'audio')))); 33486} 33487function changeTypeSupported() { 33488 var _sourceBuffer$prototy; 33489 const sourceBuffer = getSourceBuffer(); 33490 return typeof (sourceBuffer == null || (_sourceBuffer$prototy = sourceBuffer.prototype) == null ? void 0 : _sourceBuffer$prototy.changeType) === 'function'; 33491} 33492 33493const TICK_INTERVAL = 100; // how often to tick in ms 33494 33495class StreamController extends BaseStreamController { 33496 constructor(hls, fragmentTracker, keyLoader) { 33497 super(hls, fragmentTracker, keyLoader, 'stream-controller', PlaylistLevelType.MAIN); 33498 this.audioCodecSwap = false; 33499 this.level = -1; 33500 this._forceStartLoad = false; 33501 this._hasEnoughToStart = false; 33502 this.altAudio = 0; 33503 this.audioOnly = false; 33504 this.fragPlaying = null; 33505 this.fragLastKbps = 0; 33506 this.couldBacktrack = false; 33507 this.backtrackFragment = null; 33508 this.audioCodecSwitch = false; 33509 this.videoBuffer = null; 33510 this.onMediaPlaying = () => { 33511 // tick to speed up FRAG_CHANGED triggering 33512 this.tick(); 33513 }; 33514 this.onMediaSeeked = () => { 33515 const media = this.media; 33516 const currentTime = media ? media.currentTime : null; 33517 if (currentTime === null || !isFiniteNumber(currentTime)) { 33518 return; 33519 } 33520 this.log(`Media seeked to ${currentTime.toFixed(3)}`); 33521 33522 // If seeked was issued before buffer was appended do not tick immediately 33523 if (!this.getBufferedFrag(currentTime)) { 33524 return; 33525 } 33526 const bufferInfo = this.getFwdBufferInfoAtPos(media, currentTime, PlaylistLevelType.MAIN, 0); 33527 if (bufferInfo === null || bufferInfo.len === 0) { 33528 this.warn(`Main forward buffer length at ${currentTime} on "seeked" event ${bufferInfo ? bufferInfo.len : 'empty'})`); 33529 return; 33530 } 33531 33532 // tick to speed up FRAG_CHANGED triggering 33533 this.tick(); 33534 }; 33535 this.registerListeners(); 33536 } 33537 registerListeners() { 33538 super.registerListeners(); 33539 const { 33540 hls 33541 } = this; 33542 hls.on(Events.MANIFEST_PARSED, this.onManifestParsed, this); 33543 hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this); 33544 hls.on(Events.LEVEL_LOADED, this.onLevelLoaded, this); 33545 hls.on(Events.FRAG_LOAD_EMERGENCY_ABORTED, this.onFragLoadEmergencyAborted, this); 33546 hls.on(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); 33547 hls.on(Events.AUDIO_TRACK_SWITCHED, this.onAudioTrackSwitched, this); 33548 hls.on(Events.BUFFER_CREATED, this.onBufferCreated, this); 33549 hls.on(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); 33550 hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); 33551 hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this); 33552 } 33553 unregisterListeners() { 33554 super.unregisterListeners(); 33555 const { 33556 hls 33557 } = this; 33558 hls.off(Events.MANIFEST_PARSED, this.onManifestParsed, this); 33559 hls.off(Events.LEVEL_LOADED, this.onLevelLoaded, this); 33560 hls.off(Events.FRAG_LOAD_EMERGENCY_ABORTED, this.onFragLoadEmergencyAborted, this); 33561 hls.off(Events.AUDIO_TRACK_SWITCHING, this.onAudioTrackSwitching, this); 33562 hls.off(Events.AUDIO_TRACK_SWITCHED, this.onAudioTrackSwitched, this); 33563 hls.off(Events.BUFFER_CREATED, this.onBufferCreated, this); 33564 hls.off(Events.BUFFER_FLUSHED, this.onBufferFlushed, this); 33565 hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); 33566 hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this); 33567 } 33568 onHandlerDestroying() { 33569 // @ts-ignore 33570 this.onMediaPlaying = this.onMediaSeeked = null; 33571 this.unregisterListeners(); 33572 super.onHandlerDestroying(); 33573 } 33574 startLoad(startPosition, skipSeekToStartPosition) { 33575 if (this.levels) { 33576 const { 33577 lastCurrentTime, 33578 hls 33579 } = this; 33580 this.stopLoad(); 33581 this.setInterval(TICK_INTERVAL); 33582 this.level = -1; 33583 if (!this.startFragRequested) { 33584 // determine load level 33585 let startLevel = hls.startLevel; 33586 if (startLevel === -1) { 33587 if (hls.config.testBandwidth && this.levels.length > 1) { 33588 // -1 : guess start Level by doing a bitrate test by loading first fragment of lowest quality level 33589 startLevel = 0; 33590 this.bitrateTest = true; 33591 } else { 33592 startLevel = hls.firstAutoLevel; 33593 } 33594 } 33595 // set new level to playlist loader : this will trigger start level load 33596 // hls.nextLoadLevel remains until it is set to a new value or until a new frag is successfully loaded 33597 hls.nextLoadLevel = startLevel; 33598 this.level = hls.loadLevel; 33599 this._hasEnoughToStart = !!skipSeekToStartPosition; 33600 } 33601 // if startPosition undefined but lastCurrentTime set, set startPosition to last currentTime 33602 if (lastCurrentTime > 0 && startPosition === -1 && !skipSeekToStartPosition) { 33603 this.log(`Override startPosition with lastCurrentTime @${lastCurrentTime.toFixed(3)}`); 33604 startPosition = lastCurrentTime; 33605 } 33606 this.state = State.IDLE; 33607 this.nextLoadPosition = this.lastCurrentTime = startPosition + this.timelineOffset; 33608 this.startPosition = skipSeekToStartPosition ? -1 : startPosition; 33609 this.tick(); 33610 } else { 33611 this._forceStartLoad = true; 33612 this.state = State.STOPPED; 33613 } 33614 } 33615 stopLoad() { 33616 this._forceStartLoad = false; 33617 super.stopLoad(); 33618 } 33619 doTick() { 33620 switch (this.state) { 33621 case State.WAITING_LEVEL: 33622 { 33623 const { 33624 levels, 33625 level 33626 } = this; 33627 const currentLevel = levels == null ? void 0 : levels[level]; 33628 const details = currentLevel == null ? void 0 : currentLevel.details; 33629 if (details && (!details.live || this.levelLastLoaded === currentLevel && !this.waitForLive(currentLevel))) { 33630 if (this.waitForCdnTuneIn(details)) { 33631 break; 33632 } 33633 this.state = State.IDLE; 33634 break; 33635 } else if (this.hls.nextLoadLevel !== this.level) { 33636 this.state = State.IDLE; 33637 break; 33638 } 33639 break; 33640 } 33641 case State.FRAG_LOADING_WAITING_RETRY: 33642 this.checkRetryDate(); 33643 break; 33644 } 33645 if (this.state === State.IDLE) { 33646 this.doTickIdle(); 33647 } 33648 this.onTickEnd(); 33649 } 33650 onTickEnd() { 33651 var _this$media; 33652 super.onTickEnd(); 33653 if ((_this$media = this.media) != null && _this$media.readyState && this.media.seeking === false) { 33654 this.lastCurrentTime = this.media.currentTime; 33655 } 33656 this.checkFragmentChanged(); 33657 } 33658 doTickIdle() { 33659 const { 33660 hls, 33661 levelLastLoaded, 33662 levels, 33663 media 33664 } = this; 33665 33666 // if start level not parsed yet OR 33667 // if video not attached AND start fragment already requested OR start frag prefetch not enabled 33668 // exit loop, as we either need more info (level not parsed) or we need media to be attached to load new fragment 33669 if (levelLastLoaded === null || !media && !this.primaryPrefetch && (this.startFragRequested || !hls.config.startFragPrefetch)) { 33670 return; 33671 } 33672 33673 // If the "main" level is audio-only but we are loading an alternate track in the same group, do not load anything 33674 if (this.altAudio && this.audioOnly) { 33675 return; 33676 } 33677 const level = this.buffering ? hls.nextLoadLevel : hls.loadLevel; 33678 if (!(levels != null && levels[level])) { 33679 return; 33680 } 33681 const levelInfo = levels[level]; 33682 33683 // if buffer length is less than maxBufLen try to load a new fragment 33684 33685 const bufferInfo = this.getMainFwdBufferInfo(); 33686 if (bufferInfo === null) { 33687 return; 33688 } 33689 const lastDetails = this.getLevelDetails(); 33690 if (lastDetails && this._streamEnded(bufferInfo, lastDetails)) { 33691 const data = {}; 33692 if (this.altAudio === 2) { 33693 data.type = 'video'; 33694 } 33695 this.hls.trigger(Events.BUFFER_EOS, data); 33696 this.state = State.ENDED; 33697 return; 33698 } 33699 if (!this.buffering) { 33700 return; 33701 } 33702 33703 // set next load level : this will trigger a playlist load if needed 33704 if (hls.loadLevel !== level && hls.manualLevel === -1) { 33705 this.log(`Adapting to level ${level} from level ${this.level}`); 33706 } 33707 this.level = hls.nextLoadLevel = level; 33708 const levelDetails = levelInfo.details; 33709 // if level info not retrieved yet, switch state and wait for level retrieval 33710 // if live playlist, ensure that new playlist has been refreshed to avoid loading/try to load 33711 // a useless and outdated fragment (that might even introduce load error if it is already out of the live playlist) 33712 if (!levelDetails || this.state === State.WAITING_LEVEL || this.waitForLive(levelInfo)) { 33713 this.level = level; 33714 this.state = State.WAITING_LEVEL; 33715 this.startFragRequested = false;
vendor: 8,506 bytes, lines 33716-33917
33716 return; 33717 } 33718 const bufferLen = bufferInfo.len; 33719 33720 // compute max Buffer Length that we could get from this load level, based on level bitrate. don't buffer more than 60 MB and more than 30s 33721 const maxBufLen = this.getMaxBufferLength(levelInfo.maxBitrate); 33722 33723 // Stay idle if we are still with buffer margins 33724 if (bufferLen >= maxBufLen) { 33725 return; 33726 } 33727 if (this.backtrackFragment && this.backtrackFragment.start > bufferInfo.end) { 33728 this.backtrackFragment = null; 33729 } 33730 const targetBufferTime = this.backtrackFragment ? this.backtrackFragment.start : bufferInfo.end; 33731 let frag = this.getNextFragment(targetBufferTime, levelDetails); 33732 // Avoid backtracking by loading an earlier segment in streams with segments that do not start with a key frame (flagged by `couldBacktrack`) 33733 if (this.couldBacktrack && !this.fragPrevious && frag && isMediaFragment(frag) && this.fragmentTracker.getState(frag) !== FragmentState.OK) { 33734 var _this$backtrackFragme; 33735 const backtrackSn = ((_this$backtrackFragme = this.backtrackFragment) != null ? _this$backtrackFragme : frag).sn; 33736 const fragIdx = backtrackSn - levelDetails.startSN; 33737 const backtrackFrag = levelDetails.fragments[fragIdx - 1]; 33738 if (backtrackFrag && frag.cc === backtrackFrag.cc) { 33739 frag = backtrackFrag; 33740 this.fragmentTracker.removeFragment(backtrackFrag); 33741 } 33742 } else if (this.backtrackFragment && bufferInfo.len) { 33743 this.backtrackFragment = null; 33744 } 33745 // Avoid loop loading by using nextLoadPosition set for backtracking and skipping consecutive GAP tags 33746 if (frag && this.isLoopLoading(frag, targetBufferTime)) { 33747 const gapStart = frag.gap; 33748 if (!gapStart) { 33749 // Cleanup the fragment tracker before trying to find the next unbuffered fragment 33750 const type = this.audioOnly && !this.altAudio ? ElementaryStreamTypes.AUDIO : ElementaryStreamTypes.VIDEO; 33751 const mediaBuffer = (type === ElementaryStreamTypes.VIDEO ? this.videoBuffer : this.mediaBuffer) || this.media; 33752 if (mediaBuffer) { 33753 this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN); 33754 } 33755 } 33756 frag = this.getNextFragmentLoopLoading(frag, levelDetails, bufferInfo, PlaylistLevelType.MAIN, maxBufLen); 33757 } 33758 if (!frag) { 33759 return; 33760 } 33761 if (this.exceedsMaxBuffer(bufferInfo, maxBufLen, frag)) { 33762 return; 33763 } 33764 if (frag.initSegment && !frag.initSegment.data && !this.bitrateTest) { 33765 frag = frag.initSegment; 33766 } 33767 this.loadFragment(frag, levelInfo, targetBufferTime); 33768 } 33769 loadFragment(frag, level, targetBufferTime) { 33770 // Check if fragment is not loaded 33771 const fragState = this.fragmentTracker.getState(frag); 33772 if (fragState === FragmentState.NOT_LOADED || fragState === FragmentState.PARTIAL) { 33773 if (!isMediaFragment(frag)) { 33774 this._loadInitSegment(frag, level); 33775 } else if (this.bitrateTest) { 33776 this.log(`Fragment ${frag.sn} of level ${frag.level} is being downloaded to test bitrate and will not be buffered`); 33777 this._loadBitrateTestFrag(frag, level); 33778 } else { 33779 super.loadFragment(frag, level, targetBufferTime); 33780 } 33781 } else { 33782 this.clearTrackerIfNeeded(frag); 33783 } 33784 } 33785 getBufferedFrag(position) { 33786 return this.fragmentTracker.getBufferedFrag(position, PlaylistLevelType.MAIN); 33787 } 33788 followingBufferedFrag(frag) { 33789 if (frag) { 33790 // try to get range of next fragment (500ms after this range) 33791 return this.getBufferedFrag(frag.end + 0.5); 33792 } 33793 return null; 33794 } 33795 33796 /* 33797 on immediate level switch : 33798 - pause playback if playing 33799 - cancel any pending load request 33800 - and trigger a buffer flush 33801 */ 33802 immediateLevelSwitch() { 33803 this.abortCurrentFrag(); 33804 this.flushMainBuffer(0, Number.POSITIVE_INFINITY); 33805 if (this.altAudio !== 0) { 33806 var _this$getLevelDetails; 33807 // If behind the live window, remove audio too to keep buffered media aligned 33808 const liveWindowStart = ((_this$getLevelDetails = this.getLevelDetails()) == null ? void 0 : _this$getLevelDetails.fragmentStart) || 0; 33809 if (liveWindowStart > this.lastCurrentTime) { 33810 super.flushMainBuffer(0, Number.POSITIVE_INFINITY, 'audio'); 33811 } 33812 } 33813 } 33814 33815 /** 33816 * try to switch ASAP without breaking video playback: 33817 * in order to ensure smooth but quick level switching, 33818 * we need to find the next flushable buffer range 33819 * we should take into account new segment fetch time 33820 */ 33821 nextLevelSwitch() { 33822 const { 33823 levels, 33824 media 33825 } = this; 33826 // ensure that media is defined and that metadata are available (to retrieve currentTime) 33827 if (media != null && media.readyState) { 33828 let fetchdelay; 33829 const fragPlayingCurrent = this.getAppendedFrag(media.currentTime); 33830 if (fragPlayingCurrent && fragPlayingCurrent.start > 1) { 33831 // flush buffer preceding current fragment (flush until current fragment start offset) 33832 // minus 1s to avoid video freezing, that could happen if we flush keyframe of current video ... 33833 this.flushMainBuffer(0, fragPlayingCurrent.start - 1); 33834 } 33835 const levelDetails = this.getLevelDetails(); 33836 if (levelDetails != null && levelDetails.live) { 33837 const bufferInfo = this.getMainFwdBufferInfo(); 33838 // Do not flush in live stream with low buffer 33839 if (!bufferInfo || bufferInfo.len < levelDetails.targetduration * 2) { 33840 return; 33841 } 33842 } 33843 if (!media.paused && levels) { 33844 // add a safety delay of 1s 33845 const nextLevelId = this.hls.nextLoadLevel; 33846 const nextLevel = levels[nextLevelId]; 33847 const fragLastKbps = this.fragLastKbps; 33848 if (fragLastKbps && this.fragCurrent) { 33849 fetchdelay = this.fragCurrent.duration * nextLevel.maxBitrate / (1000 * fragLastKbps) + 1; 33850 } else { 33851 fetchdelay = 0; 33852 } 33853 } else { 33854 fetchdelay = 0; 33855 } 33856 // this.log('fetchdelay:'+fetchdelay); 33857 // find buffer range that will be reached once new fragment will be fetched 33858 const bufferedFrag = this.getBufferedFrag(media.currentTime + fetchdelay); 33859 if (bufferedFrag) { 33860 // we can flush buffer range following this one without stalling playback 33861 const nextBufferedFrag = this.followingBufferedFrag(bufferedFrag); 33862 if (nextBufferedFrag) { 33863 // if we are here, we can also cancel any loading/demuxing in progress, as they are useless 33864 this.abortCurrentFrag(); 33865 // start flush position is in next buffered frag. Leave some padding for non-independent segments and smoother playback. 33866 const maxStart = nextBufferedFrag.maxStartPTS ? nextBufferedFrag.maxStartPTS : nextBufferedFrag.start; 33867 const fragDuration = nextBufferedFrag.duration; 33868 const startPts = Math.max(bufferedFrag.end, maxStart + Math.min(Math.max(fragDuration - this.config.maxFragLookUpTolerance, fragDuration * (this.couldBacktrack ? 0.5 : 0.125)), fragDuration * (this.couldBacktrack ? 0.75 : 0.25))); 33869 this.flushMainBuffer(startPts, Number.POSITIVE_INFINITY); 33870 } 33871 } 33872 } 33873 } 33874 abortCurrentFrag() { 33875 const fragCurrent = this.fragCurrent; 33876 this.fragCurrent = null; 33877 this.backtrackFragment = null; 33878 if (fragCurrent) { 33879 fragCurrent.abortRequests(); 33880 this.fragmentTracker.removeFragment(fragCurrent); 33881 } 33882 switch (this.state) { 33883 case State.KEY_LOADING: 33884 case State.FRAG_LOADING: 33885 case State.FRAG_LOADING_WAITING_RETRY: 33886 case State.PARSING: 33887 case State.PARSED: 33888 this.state = State.IDLE; 33889 break; 33890 } 33891 this.nextLoadPosition = this.getLoadPosition(); 33892 } 33893 flushMainBuffer(startOffset, endOffset) { 33894 super.flushMainBuffer(startOffset, endOffset, this.altAudio === 2 ? 'video' : null); 33895 } 33896 onMediaAttached(event, data) { 33897 super.onMediaAttached(event, data); 33898 const media = data.media; 33899 addEventListener(media, 'playing', this.onMediaPlaying); 33900 addEventListener(media, 'seeked', this.onMediaSeeked); 33901 } 33902 onMediaDetaching(event, data) { 33903 const { 33904 media 33905 } = this; 33906 if (media) { 33907 removeEventListener(media, 'playing', this.onMediaPlaying); 33908 removeEventListener(media, 'seeked', this.onMediaSeeked); 33909 } 33910 this.videoBuffer = null; 33911 this.fragPlaying = null; 33912 super.onMediaDetaching(event, data); 33913 const transferringMedia = !!data.transferMedia; 33914 if (transferringMedia) { 33915 return; 33916 } 33917 this._hasEnoughToStart = false;
vendor: 96,412 bytes, lines 33918-36714
33918 } 33919 onManifestLoading() { 33920 super.onManifestLoading(); 33921 // reset buffer on manifest loading 33922 this.log('Trigger BUFFER_RESET'); 33923 this.hls.trigger(Events.BUFFER_RESET, undefined); 33924 this.couldBacktrack = false; 33925 this.fragLastKbps = 0; 33926 this.fragPlaying = this.backtrackFragment = null; 33927 this.altAudio = 0; 33928 this.audioOnly = false; 33929 } 33930 onManifestParsed(event, data) { 33931 // detect if we have different kind of audio codecs used amongst playlists 33932 let aac = false; 33933 let heaac = false; 33934 for (let i = 0; i < data.levels.length; i++) { 33935 const codec = data.levels[i].audioCodec; 33936 if (codec) { 33937 aac = aac || codec.indexOf('mp4a.40.2') !== -1; 33938 heaac = heaac || codec.indexOf('mp4a.40.5') !== -1; 33939 } 33940 } 33941 this.audioCodecSwitch = aac && heaac && !changeTypeSupported(); 33942 if (this.audioCodecSwitch) { 33943 this.log('Both AAC/HE-AAC audio found in levels; declaring level codec as HE-AAC'); 33944 } 33945 this.levels = data.levels; 33946 this.startFragRequested = false; 33947 } 33948 onLevelLoading(event, data) { 33949 const { 33950 levels 33951 } = this; 33952 if (!levels || this.state !== State.IDLE) { 33953 return; 33954 } 33955 const level = data.levelInfo; 33956 if (!level.details || level.details.live && (this.levelLastLoaded !== level || level.details.expired) || this.waitForCdnTuneIn(level.details)) { 33957 this.state = State.WAITING_LEVEL; 33958 } 33959 } 33960 onLevelLoaded(event, data) { 33961 var _curLevel$details; 33962 const { 33963 levels, 33964 startFragRequested 33965 } = this; 33966 const newLevelId = data.level; 33967 const newDetails = data.details; 33968 const duration = newDetails.totalduration; 33969 if (!levels) { 33970 this.warn(`Levels were reset while loading level ${newLevelId}`); 33971 return; 33972 } 33973 this.log(`Level ${newLevelId} loaded [${newDetails.startSN},${newDetails.endSN}]${newDetails.lastPartSn ? `[part-${newDetails.lastPartSn}-${newDetails.lastPartIndex}]` : ''}, cc [${newDetails.startCC}, ${newDetails.endCC}] duration:${duration}`); 33974 const curLevel = data.levelInfo; 33975 const fragCurrent = this.fragCurrent; 33976 if (fragCurrent && (this.state === State.FRAG_LOADING || this.state === State.FRAG_LOADING_WAITING_RETRY)) { 33977 if (fragCurrent.level !== data.level && fragCurrent.loader) { 33978 this.abortCurrentFrag(); 33979 } 33980 } 33981 let sliding = 0; 33982 if (newDetails.live || (_curLevel$details = curLevel.details) != null && _curLevel$details.live) { 33983 var _this$levelLastLoaded; 33984 this.checkLiveUpdate(newDetails); 33985 if (newDetails.deltaUpdateFailed) { 33986 return; 33987 } 33988 sliding = this.alignPlaylists(newDetails, curLevel.details, (_this$levelLastLoaded = this.levelLastLoaded) == null ? void 0 : _this$levelLastLoaded.details); 33989 } 33990 // override level info 33991 curLevel.details = newDetails; 33992 this.levelLastLoaded = curLevel; 33993 if (!startFragRequested) { 33994 this.setStartPosition(newDetails, sliding); 33995 } 33996 this.hls.trigger(Events.LEVEL_UPDATED, { 33997 details: newDetails, 33998 level: newLevelId 33999 }); 34000 34001 // only switch back to IDLE state if we were waiting for level to start downloading a new fragment 34002 if (this.state === State.WAITING_LEVEL) { 34003 if (this.waitForCdnTuneIn(newDetails)) { 34004 // Wait for Low-Latency CDN Tune-in 34005 return; 34006 } 34007 this.state = State.IDLE; 34008 } 34009 if (startFragRequested && newDetails.live) { 34010 this.synchronizeToLiveEdge(newDetails); 34011 } 34012 34013 // trigger handler right now 34014 this.tick(); 34015 } 34016 synchronizeToLiveEdge(levelDetails) { 34017 const { 34018 config, 34019 media 34020 } = this; 34021 if (!media) { 34022 return; 34023 } 34024 const liveSyncPosition = this.hls.liveSyncPosition; 34025 const currentTime = this.getLoadPosition(); 34026 const start = levelDetails.fragmentStart; 34027 const end = levelDetails.edge; 34028 const withinSlidingWindow = currentTime >= start - config.maxFragLookUpTolerance && currentTime <= end; 34029 // Continue if we can seek forward to sync position or if current time is outside of sliding window 34030 if (liveSyncPosition !== null && media.duration > liveSyncPosition && (currentTime < liveSyncPosition || !withinSlidingWindow)) { 34031 // Continue if buffer is starving or if current time is behind max latency 34032 const maxLatency = config.liveMaxLatencyDuration !== undefined ? config.liveMaxLatencyDuration : config.liveMaxLatencyDurationCount * levelDetails.targetduration; 34033 if (!withinSlidingWindow && media.readyState < 4 || currentTime < end - maxLatency) { 34034 if (!this._hasEnoughToStart) { 34035 this.nextLoadPosition = liveSyncPosition; 34036 } 34037 // Only seek if ready and there is not a significant forward buffer available for playback 34038 if (media.readyState) { 34039 this.warn(`Playback: ${currentTime.toFixed(3)} is located too far from the end of live sliding playlist: ${end}, reset currentTime to : ${liveSyncPosition.toFixed(3)}`); 34040 if (this.config.liveSyncMode === 'buffered') { 34041 var _bufferInfo$buffered; 34042 const bufferInfo = BufferHelper.bufferInfo(media, liveSyncPosition, 0); 34043 if (!((_bufferInfo$buffered = bufferInfo.buffered) != null && _bufferInfo$buffered.length)) { 34044 media.currentTime = liveSyncPosition; 34045 return; 34046 } 34047 const isLiveSyncInBuffer = bufferInfo.start <= currentTime; 34048 if (isLiveSyncInBuffer) { 34049 media.currentTime = liveSyncPosition; 34050 return; 34051 } 34052 const { 34053 nextStart 34054 } = BufferHelper.bufferedInfo(bufferInfo.buffered, currentTime, 0); 34055 if (nextStart) { 34056 media.currentTime = nextStart; 34057 } 34058 } else { 34059 media.currentTime = liveSyncPosition; 34060 } 34061 } 34062 } 34063 } 34064 } 34065 _handleFragmentLoadProgress(data) { 34066 var _frag$initSegment; 34067 const frag = data.frag; 34068 const { 34069 part, 34070 payload 34071 } = data; 34072 const { 34073 levels 34074 } = this; 34075 if (!levels) { 34076 this.warn(`Levels were reset while fragment load was in progress. Fragment ${frag.sn} of level ${frag.level} will not be buffered`); 34077 return; 34078 } 34079 const currentLevel = levels[frag.level]; 34080 if (!currentLevel) { 34081 this.warn(`Level ${frag.level} not found on progress`); 34082 return; 34083 } 34084 const details = currentLevel.details; 34085 if (!details) { 34086 this.warn(`Dropping fragment ${frag.sn} of level ${frag.level} after level details were reset`); 34087 this.fragmentTracker.removeFragment(frag); 34088 return; 34089 } 34090 const videoCodec = currentLevel.videoCodec; 34091 34092 // time Offset is accurate if level PTS is known, or if playlist is not sliding (not live) 34093 const accurateTimeOffset = details.PTSKnown || !details.live; 34094 const initSegmentData = (_frag$initSegment = frag.initSegment) == null ? void 0 : _frag$initSegment.data; 34095 const audioCodec = this._getAudioCodec(currentLevel); 34096 34097 // transmux the MPEG-TS data to ISO-BMFF segments 34098 // this.log(`Transmuxing ${frag.sn} of [${details.startSN} ,${details.endSN}],level ${frag.level}, cc ${frag.cc}`); 34099 const transmuxer = this.transmuxer = this.transmuxer || new TransmuxerInterface(this.hls, PlaylistLevelType.MAIN, this._handleTransmuxComplete.bind(this), this._handleTransmuxerFlush.bind(this)); 34100 const partIndex = part ? part.index : -1; 34101 const partial = partIndex !== -1; 34102 const chunkMeta = new ChunkMetadata(frag.level, frag.sn, frag.stats.chunkCount, payload.byteLength, partIndex, partial); 34103 const initPTS = this.initPTS[frag.cc]; 34104 transmuxer.push(payload, initSegmentData, audioCodec, videoCodec, frag, part, details.totalduration, accurateTimeOffset, chunkMeta, initPTS); 34105 } 34106 onAudioTrackSwitching(event, data) { 34107 const hls = this.hls; 34108 // if any URL found on new audio track, it is an alternate audio track 34109 const fromAltAudio = this.altAudio !== 0; 34110 const altAudio = useAlternateAudio(data.url, hls); 34111 // if we switch on main audio, ensure that main fragment scheduling is synced with media.buffered 34112 // don't do anything if we switch to alt audio: audio stream controller is handling it. 34113 // we will just have to change buffer scheduling on audioTrackSwitched 34114 if (!altAudio) { 34115 if (this.mediaBuffer !== this.media) { 34116 this.log('Switching on main audio, use media.buffered to schedule main fragment loading'); 34117 this.mediaBuffer = this.media; 34118 const fragCurrent = this.fragCurrent; 34119 // we need to refill audio buffer from main: cancel any frag loading to speed up audio switch 34120 if (fragCurrent) { 34121 this.log('Switching to main audio track, cancel main fragment load'); 34122 fragCurrent.abortRequests(); 34123 this.fragmentTracker.removeFragment(fragCurrent); 34124 } 34125 // destroy transmuxer to force init segment generation (following audio switch) 34126 this.resetTransmuxer(); 34127 // switch to IDLE state to load new fragment 34128 this.resetLoadingState(); 34129 } else if (this.audioOnly) { 34130 // Reset audio transmuxer so when switching back to main audio we're not still appending where we left off 34131 this.resetTransmuxer(); 34132 } 34133 // If switching from alt to main audio, flush all audio and trigger track switched 34134 if (fromAltAudio) { 34135 this.altAudio = 0; 34136 this.fragmentTracker.removeAllFragments(); 34137 hls.once(Events.BUFFER_FLUSHED, () => { 34138 if (!this.hls) { 34139 return; 34140 } 34141 this.hls.trigger(Events.AUDIO_TRACK_SWITCHED, data); 34142 }); 34143 hls.trigger(Events.BUFFER_FLUSHING, { 34144 startOffset: 0, 34145 endOffset: Number.POSITIVE_INFINITY, 34146 type: null 34147 }); 34148 return; 34149 } 34150 hls.trigger(Events.AUDIO_TRACK_SWITCHED, data); 34151 } else { 34152 this.altAudio = 1; 34153 } 34154 } 34155 onAudioTrackSwitched(event, data) { 34156 const altAudio = useAlternateAudio(data.url, this.hls); 34157 if (altAudio) { 34158 const videoBuffer = this.videoBuffer; 34159 // if we switched on alternate audio, ensure that main fragment scheduling is synced with video sourcebuffer buffered 34160 if (videoBuffer && this.mediaBuffer !== videoBuffer) { 34161 this.log('Switching on alternate audio, use video.buffered to schedule main fragment loading'); 34162 this.mediaBuffer = videoBuffer; 34163 } 34164 } 34165 this.altAudio = altAudio ? 2 : 0; 34166 this.tick(); 34167 } 34168 onBufferCreated(event, data) { 34169 const tracks = data.tracks; 34170 let mediaTrack; 34171 let name; 34172 let alternate = false; 34173 for (const type in tracks) { 34174 const track = tracks[type]; 34175 if (track.id === 'main') { 34176 name = type; 34177 mediaTrack = track; 34178 // keep video source buffer reference 34179 if (type === 'video') { 34180 const videoTrack = tracks[type]; 34181 if (videoTrack) { 34182 this.videoBuffer = videoTrack.buffer; 34183 } 34184 } 34185 } else { 34186 alternate = true; 34187 } 34188 } 34189 if (alternate && mediaTrack) { 34190 this.log(`Alternate track found, use ${name}.buffered to schedule main fragment loading`); 34191 this.mediaBuffer = mediaTrack.buffer; 34192 } else { 34193 this.mediaBuffer = this.media; 34194 } 34195 } 34196 onFragBuffered(event, data) { 34197 const { 34198 frag, 34199 part 34200 } = data; 34201 const bufferedMainFragment = frag.type === PlaylistLevelType.MAIN; 34202 if (bufferedMainFragment) { 34203 if (this.fragContextChanged(frag)) { 34204 // If a level switch was requested while a fragment was buffering, it will emit the FRAG_BUFFERED event upon completion 34205 // Avoid setting state back to IDLE, since that will interfere with a level switch 34206 this.warn(`Fragment ${frag.sn}${part ? ' p: ' + part.index : ''} of level ${frag.level} finished buffering, but was aborted. state: ${this.state}`); 34207 if (this.state === State.PARSED) { 34208 this.state = State.IDLE; 34209 } 34210 return; 34211 } 34212 const stats = part ? part.stats : frag.stats; 34213 this.fragLastKbps = Math.round(8 * stats.total / (stats.buffering.end - stats.loading.first)); 34214 if (isMediaFragment(frag)) { 34215 this.fragPrevious = frag; 34216 } 34217 this.fragBufferedComplete(frag, part); 34218 } 34219 const media = this.media; 34220 if (!media) { 34221 return; 34222 } 34223 if (!this._hasEnoughToStart && BufferHelper.getBuffered(media).length) { 34224 this._hasEnoughToStart = true; 34225 this.seekToStartPos(); 34226 } 34227 if (bufferedMainFragment) { 34228 this.tick(); 34229 } 34230 } 34231 get hasEnoughToStart() { 34232 return this._hasEnoughToStart; 34233 } 34234 onError(event, data) { 34235 var _data$context; 34236 if (data.fatal) { 34237 this.state = State.ERROR; 34238 return; 34239 } 34240 switch (data.details) { 34241 case ErrorDetails.FRAG_GAP: 34242 case ErrorDetails.FRAG_PARSING_ERROR: 34243 case ErrorDetails.FRAG_DECRYPT_ERROR: 34244 case ErrorDetails.FRAG_LOAD_ERROR: 34245 case ErrorDetails.FRAG_LOAD_TIMEOUT: 34246 case ErrorDetails.KEY_LOAD_ERROR: 34247 case ErrorDetails.KEY_LOAD_TIMEOUT: 34248 this.onFragmentOrKeyLoadError(PlaylistLevelType.MAIN, data); 34249 break; 34250 case ErrorDetails.LEVEL_LOAD_ERROR: 34251 case ErrorDetails.LEVEL_LOAD_TIMEOUT: 34252 case ErrorDetails.LEVEL_PARSING_ERROR: 34253 // in case of non fatal error while loading level, if level controller is not retrying to load level, switch back to IDLE 34254 if (!data.levelRetry && this.state === State.WAITING_LEVEL && ((_data$context = data.context) == null ? void 0 : _data$context.type) === PlaylistContextType.LEVEL) { 34255 this.state = State.IDLE; 34256 } 34257 break; 34258 case ErrorDetails.BUFFER_ADD_CODEC_ERROR: 34259 case ErrorDetails.BUFFER_APPEND_ERROR: 34260 if (data.parent !== 'main') { 34261 return; 34262 } 34263 if (this.reduceLengthAndFlushBuffer(data)) { 34264 this.resetLoadingState(); 34265 } 34266 break; 34267 case ErrorDetails.BUFFER_FULL_ERROR: 34268 if (data.parent !== 'main') { 34269 return; 34270 } 34271 if (this.reduceLengthAndFlushBuffer(data)) { 34272 const isAssetPlayer = !this.config.interstitialsController && this.config.assetPlayerId; 34273 if (isAssetPlayer) { 34274 // Use currentTime in buffer estimate to prevent loading more until playback advances 34275 this._hasEnoughToStart = true; 34276 } else { 34277 this.flushMainBuffer(0, Number.POSITIVE_INFINITY); 34278 } 34279 } 34280 break; 34281 case ErrorDetails.INTERNAL_EXCEPTION: 34282 this.recoverWorkerError(data); 34283 break; 34284 } 34285 } 34286 onFragLoadEmergencyAborted() { 34287 this.state = State.IDLE; 34288 // if loadedmetadata is not set, it means that we are emergency switch down on first frag 34289 // in that case, reset startFragRequested flag 34290 if (!this._hasEnoughToStart) { 34291 this.startFragRequested = false; 34292 this.nextLoadPosition = this.lastCurrentTime; 34293 } 34294 this.tickImmediate(); 34295 } 34296 onBufferFlushed(event, { 34297 type 34298 }) { 34299 if (type !== ElementaryStreamTypes.AUDIO || !this.altAudio) { 34300 const mediaBuffer = (type === ElementaryStreamTypes.VIDEO ? this.videoBuffer : this.mediaBuffer) || this.media; 34301 if (mediaBuffer) { 34302 this.afterBufferFlushed(mediaBuffer, type, PlaylistLevelType.MAIN); 34303 this.tick(); 34304 } 34305 } 34306 } 34307 onLevelsUpdated(event, data) { 34308 if (this.level > -1 && this.fragCurrent) { 34309 this.level = this.fragCurrent.level; 34310 if (this.level === -1) { 34311 this.resetWhenMissingContext(this.fragCurrent); 34312 } 34313 } 34314 this.levels = data.levels; 34315 } 34316 swapAudioCodec() { 34317 this.audioCodecSwap = !this.audioCodecSwap; 34318 } 34319 34320 /** 34321 * Seeks to the set startPosition if not equal to the mediaElement's current time. 34322 */ 34323 seekToStartPos() { 34324 const { 34325 media 34326 } = this; 34327 if (!media) { 34328 return; 34329 } 34330 const currentTime = media.currentTime; 34331 let startPosition = this.startPosition; 34332 // only adjust currentTime if different from startPosition or if startPosition not buffered 34333 // at that stage, there should be only one buffered range, as we reach that code after first fragment has been buffered 34334 if (startPosition >= 0 && currentTime < startPosition) { 34335 if (media.seeking) { 34336 this.log(`could not seek to ${startPosition}, already seeking at ${currentTime}`); 34337 return; 34338 } 34339 34340 // Offset start position by timeline offset 34341 const timelineOffset = this.timelineOffset; 34342 if (timelineOffset && startPosition) { 34343 startPosition += timelineOffset; 34344 } 34345 const details = this.getLevelDetails(); 34346 const buffered = BufferHelper.getBuffered(media); 34347 const bufferStart = buffered.length ? buffered.start(0) : 0; 34348 const delta = bufferStart - startPosition; 34349 const skipTolerance = Math.max(this.config.maxBufferHole, this.config.maxFragLookUpTolerance); 34350 if (this.config.startOnSegmentBoundary || delta > 0 && (delta < skipTolerance || this.loadingParts && delta < 2 * ((details == null ? void 0 : details.partTarget) || 0))) { 34351 this.log(`adjusting start position by ${delta} to match buffer start`); 34352 startPosition += delta; 34353 this.startPosition = startPosition; 34354 } 34355 if (currentTime < startPosition) { 34356 this.log(`seek to target start position ${startPosition} from current time ${currentTime} buffer start ${bufferStart}`); 34357 media.currentTime = startPosition; 34358 } 34359 } 34360 } 34361 _getAudioCodec(currentLevel) { 34362 let audioCodec = this.config.defaultAudioCodec || currentLevel.audioCodec; 34363 if (this.audioCodecSwap && audioCodec) { 34364 this.log('Swapping audio codec'); 34365 if (audioCodec.indexOf('mp4a.40.5') !== -1) { 34366 audioCodec = 'mp4a.40.2'; 34367 } else { 34368 audioCodec = 'mp4a.40.5'; 34369 } 34370 } 34371 return audioCodec; 34372 } 34373 _loadBitrateTestFrag(fragment, level) { 34374 fragment.bitrateTest = true; 34375 this._doFragLoad(fragment, level).then(data => { 34376 const { 34377 hls 34378 } = this; 34379 const frag = data == null ? void 0 : data.frag; 34380 if (!frag || this.fragContextChanged(frag)) { 34381 return; 34382 } 34383 level.fragmentError = 0; 34384 this.state = State.IDLE; 34385 this.startFragRequested = false; 34386 this.bitrateTest = false; 34387 const stats = frag.stats; 34388 // Bitrate tests fragments are neither parsed nor buffered 34389 stats.parsing.start = stats.parsing.end = stats.buffering.start = stats.buffering.end = self.performance.now(); 34390 hls.trigger(Events.FRAG_LOADED, data); 34391 frag.bitrateTest = false; 34392 }).catch(reason => { 34393 if (this.state === State.STOPPED || this.state === State.ERROR) { 34394 return; 34395 } 34396 this.warn(reason); 34397 this.resetFragmentLoading(fragment); 34398 }); 34399 } 34400 _handleTransmuxComplete(transmuxResult) { 34401 const id = this.playlistType; 34402 const { 34403 hls 34404 } = this; 34405 const { 34406 remuxResult, 34407 chunkMeta 34408 } = transmuxResult; 34409 const context = this.getCurrentContext(chunkMeta); 34410 if (!context) { 34411 this.resetWhenMissingContext(chunkMeta); 34412 return; 34413 } 34414 const { 34415 frag, 34416 part, 34417 level 34418 } = context; 34419 const { 34420 video, 34421 text, 34422 id3, 34423 initSegment 34424 } = remuxResult; 34425 const { 34426 details 34427 } = level; 34428 // The audio-stream-controller handles audio buffering if Hls.js is playing an alternate audio track 34429 const audio = this.altAudio ? undefined : remuxResult.audio; 34430 34431 // Check if the current fragment has been aborted. We check this by first seeing if we're still playing the current level. 34432 // If we are, subsequently check if the currently loading fragment (fragCurrent) has changed. 34433 if (this.fragContextChanged(frag)) { 34434 this.fragmentTracker.removeFragment(frag); 34435 return; 34436 } 34437 this.state = State.PARSING; 34438 if (initSegment) { 34439 const tracks = initSegment.tracks; 34440 if (tracks) { 34441 const mapFragment = frag.initSegment || frag; 34442 if (this.unhandledEncryptionError(initSegment, frag)) { 34443 return; 34444 } 34445 this._bufferInitSegment(level, tracks, mapFragment, chunkMeta); 34446 hls.trigger(Events.FRAG_PARSING_INIT_SEGMENT, { 34447 frag: mapFragment, 34448 id, 34449 tracks 34450 }); 34451 } 34452 const baseTime = initSegment.initPTS; 34453 const timescale = initSegment.timescale; 34454 const initPTS = this.initPTS[frag.cc]; 34455 if (isFiniteNumber(baseTime) && (!initPTS || initPTS.baseTime !== baseTime || initPTS.timescale !== timescale)) { 34456 const trackId = initSegment.trackId; 34457 this.initPTS[frag.cc] = { 34458 baseTime, 34459 timescale, 34460 trackId 34461 }; 34462 hls.trigger(Events.INIT_PTS_FOUND, { 34463 frag, 34464 id, 34465 initPTS: baseTime, 34466 timescale, 34467 trackId 34468 }); 34469 } 34470 } 34471 34472 // Avoid buffering if backtracking this fragment 34473 if (video && details) { 34474 if (audio && video.type === 'audiovideo') { 34475 this.logMuxedErr(frag); 34476 } 34477 const prevFrag = details.fragments[frag.sn - 1 - details.startSN]; 34478 const isFirstFragment = frag.sn === details.startSN; 34479 const isFirstInDiscontinuity = !prevFrag || frag.cc > prevFrag.cc; 34480 if (remuxResult.independent !== false) { 34481 const { 34482 startPTS, 34483 endPTS, 34484 startDTS, 34485 endDTS 34486 } = video; 34487 if (part) { 34488 part.elementaryStreams[video.type] = { 34489 startPTS, 34490 endPTS, 34491 startDTS, 34492 endDTS 34493 }; 34494 } else { 34495 if (video.firstKeyFrame && video.independent && chunkMeta.id === 1 && !isFirstInDiscontinuity) { 34496 this.couldBacktrack = true; 34497 } 34498 if (video.dropped && video.independent) { 34499 // Backtrack if dropped frames create a gap after currentTime 34500 34501 const bufferInfo = this.getMainFwdBufferInfo(); 34502 const targetBufferTime = (bufferInfo ? bufferInfo.end : this.getLoadPosition()) + this.config.maxBufferHole; 34503 const startTime = video.firstKeyFramePTS ? video.firstKeyFramePTS : startPTS; 34504 if (!isFirstFragment && targetBufferTime < startTime - this.config.maxBufferHole && !isFirstInDiscontinuity) { 34505 this.backtrack(frag); 34506 return; 34507 } else if (isFirstInDiscontinuity) { 34508 // Mark segment with a gap to avoid loop loading 34509 frag.gap = true; 34510 } 34511 // Set video stream start to fragment start so that truncated samples do not distort the timeline, and mark it partial 34512 frag.setElementaryStreamInfo(video.type, frag.start, endPTS, frag.start, endDTS, true); 34513 } else if (isFirstFragment && startPTS - (details.appliedTimelineOffset || 0) > MAX_START_GAP_JUMP) { 34514 // Mark segment with a gap to skip large start gap 34515 frag.gap = true; 34516 } 34517 } 34518 frag.setElementaryStreamInfo(video.type, startPTS, endPTS, startDTS, endDTS); 34519 if (this.backtrackFragment) { 34520 this.backtrackFragment = frag; 34521 } 34522 this.bufferFragmentData(video, frag, part, chunkMeta, isFirstFragment || isFirstInDiscontinuity); 34523 } else if (isFirstFragment || isFirstInDiscontinuity) { 34524 // Mark segment with a gap to avoid loop loading 34525 frag.gap = true; 34526 } else { 34527 this.backtrack(frag); 34528 return; 34529 } 34530 } 34531 if (audio) { 34532 const { 34533 startPTS, 34534 endPTS, 34535 startDTS, 34536 endDTS 34537 } = audio; 34538 if (part) { 34539 part.elementaryStreams[ElementaryStreamTypes.AUDIO] = { 34540 startPTS, 34541 endPTS, 34542 startDTS, 34543 endDTS 34544 }; 34545 } 34546 frag.setElementaryStreamInfo(ElementaryStreamTypes.AUDIO, startPTS, endPTS, startDTS, endDTS); 34547 this.bufferFragmentData(audio, frag, part, chunkMeta); 34548 } 34549 if (details && id3 != null && id3.samples.length) { 34550 const emittedID3 = { 34551 id, 34552 frag, 34553 details, 34554 samples: id3.samples 34555 }; 34556 hls.trigger(Events.FRAG_PARSING_METADATA, emittedID3); 34557 } 34558 if (details && text) { 34559 const emittedText = { 34560 id, 34561 frag, 34562 details, 34563 samples: text.samples 34564 }; 34565 hls.trigger(Events.FRAG_PARSING_USERDATA, emittedText); 34566 } 34567 } 34568 logMuxedErr(frag) { 34569 this.warn(`${isMediaFragment(frag) ? 'Media' : 'Init'} segment with muxed audiovideo where only video expected: ${frag.url}`); 34570 } 34571 _bufferInitSegment(currentLevel, tracks, frag, chunkMeta) { 34572 if (this.state !== State.PARSING) { 34573 return; 34574 } 34575 this.audioOnly = !!tracks.audio && !tracks.video; 34576 34577 // if audio track is expected to come from audio stream controller, discard any coming from main 34578 if (this.altAudio && !this.audioOnly) { 34579 delete tracks.audio; 34580 if (tracks.audiovideo) { 34581 this.logMuxedErr(frag); 34582 } 34583 } 34584 // include levelCodec in audio and video tracks 34585 const { 34586 audio, 34587 video, 34588 audiovideo 34589 } = tracks; 34590 if (audio) { 34591 const levelCodec = currentLevel.audioCodec; 34592 let audioCodec = pickMostCompleteCodecName(audio.codec, levelCodec); 34593 // Add level and profile to make up for remuxer not being able to parse full codec 34594 // (logger warning "Unhandled audio codec...") 34595 if (audioCodec === 'mp4a') { 34596 audioCodec = 'mp4a.40.5'; 34597 } 34598 // Handle `audioCodecSwitch` 34599 const ua = navigator.userAgent.toLowerCase(); 34600 if (this.audioCodecSwitch) { 34601 if (audioCodec) { 34602 if (audioCodec.indexOf('mp4a.40.5') !== -1) { 34603 audioCodec = 'mp4a.40.2'; 34604 } else { 34605 audioCodec = 'mp4a.40.5'; 34606 } 34607 } 34608 // In the case that AAC and HE-AAC audio codecs are signalled in manifest, 34609 // force HE-AAC, as it seems that most browsers prefers it. 34610 // don't force HE-AAC if mono stream, or in Firefox 34611 const audioMetadata = audio.metadata; 34612 if (audioMetadata && 'channelCount' in audioMetadata && (audioMetadata.channelCount || 1) !== 1 && ua.indexOf('firefox') === -1) { 34613 audioCodec = 'mp4a.40.5'; 34614 } 34615 } 34616 // HE-AAC is broken on Android, always signal audio codec as AAC even if variant manifest states otherwise 34617 if (audioCodec && audioCodec.indexOf('mp4a.40.5') !== -1 && ua.indexOf('android') !== -1 && audio.container !== 'audio/mpeg') { 34618 // Exclude mpeg audio 34619 audioCodec = 'mp4a.40.2'; 34620 this.log(`Android: force audio codec to ${audioCodec}`); 34621 } 34622 if (levelCodec && levelCodec !== audioCodec) { 34623 this.log(`Swapping manifest audio codec "${levelCodec}" for "${audioCodec}"`); 34624 } 34625 audio.levelCodec = audioCodec; 34626 audio.id = PlaylistLevelType.MAIN; 34627 this.log(`Init audio buffer, container:${audio.container}, codecs[selected/level/parsed]=[${audioCodec || ''}/${levelCodec || ''}/${audio.codec}]`); 34628 delete tracks.audiovideo; 34629 } 34630 if (video) { 34631 video.levelCodec = currentLevel.videoCodec; 34632 video.id = PlaylistLevelType.MAIN; 34633 const parsedVideoCodec = video.codec; 34634 if ((parsedVideoCodec == null ? void 0 : parsedVideoCodec.length) === 4) { 34635 // Make up for passthrough-remuxer not being able to parse full codec 34636 // (logger warning "Unhandled video codec...") 34637 switch (parsedVideoCodec) { 34638 case 'hvc1': 34639 case 'hev1': 34640 video.codec = 'hvc1.1.6.L120.90'; 34641 break; 34642 case 'av01': 34643 video.codec = 'av01.0.04M.08'; 34644 break; 34645 case 'avc1': 34646 video.codec = 'avc1.42e01e'; 34647 break; 34648 } 34649 } 34650 this.log(`Init video buffer, container:${video.container}, codecs[level/parsed]=[${currentLevel.videoCodec || ''}/${parsedVideoCodec}]${video.codec !== parsedVideoCodec ? ' parsed-corrected=' + video.codec : ''}${video.supplemental ? ' supplemental=' + video.supplemental : ''}`); 34651 delete tracks.audiovideo; 34652 } 34653 if (audiovideo) { 34654 this.log(`Init audiovideo buffer, container:${audiovideo.container}, codecs[level/parsed]=[${currentLevel.codecs}/${audiovideo.codec}]`); 34655 delete tracks.video; 34656 delete tracks.audio; 34657 } 34658 const trackTypes = Object.keys(tracks); 34659 if (trackTypes.length) { 34660 this.hls.trigger(Events.BUFFER_CODECS, tracks); 34661 if (!this.hls) { 34662 // Exit after fatal tracks error 34663 return; 34664 } 34665 // loop through tracks that are going to be provided to bufferController 34666 trackTypes.forEach(trackName => { 34667 const track = tracks[trackName]; 34668 const initSegment = track.initSegment; 34669 if (initSegment != null && initSegment.byteLength) { 34670 this.hls.trigger(Events.BUFFER_APPENDING, { 34671 type: trackName, 34672 data: initSegment, 34673 frag, 34674 part: null, 34675 chunkMeta, 34676 parent: frag.type 34677 }); 34678 } 34679 }); 34680 } 34681 // trigger handler right now 34682 this.tickImmediate(); 34683 } 34684 getMainFwdBufferInfo() { 34685 // Observe video SourceBuffer (this.mediaBuffer) only when alt-audio is used, otherwise observe combined media buffer 34686 const bufferOutput = this.mediaBuffer && this.altAudio === 2 ? this.mediaBuffer : this.media; 34687 return this.getFwdBufferInfo(bufferOutput, PlaylistLevelType.MAIN); 34688 } 34689 get maxBufferLength() { 34690 const { 34691 levels, 34692 level 34693 } = this; 34694 const levelInfo = levels == null ? void 0 : levels[level]; 34695 if (!levelInfo) { 34696 return this.config.maxBufferLength; 34697 } 34698 return this.getMaxBufferLength(levelInfo.maxBitrate); 34699 } 34700 backtrack(frag) { 34701 this.couldBacktrack = true; 34702 // Causes findFragments to backtrack through fragments to find the keyframe 34703 this.backtrackFragment = frag; 34704 this.resetTransmuxer(); 34705 this.flushBufferGap(frag); 34706 this.fragmentTracker.removeFragment(frag); 34707 this.fragPrevious = null; 34708 this.nextLoadPosition = frag.start; 34709 this.state = State.IDLE; 34710 } 34711 checkFragmentChanged() { 34712 const video = this.media; 34713 let fragPlayingCurrent = null; 34714 if (video && video.readyState > 1 && video.seeking === false) { 34715 const currentTime = video.currentTime; 34716 /* if video element is in seeked state, currentTime can only increase. 34717 (assuming that playback rate is positive ...) 34718 As sometimes currentTime jumps back to zero after a 34719 media decode error, check this, to avoid seeking back to 34720 wrong position after a media decode error 34721 */ 34722 34723 if (BufferHelper.isBuffered(video, currentTime)) { 34724 fragPlayingCurrent = this.getAppendedFrag(currentTime); 34725 } else if (BufferHelper.isBuffered(video, currentTime + 0.1)) { 34726 /* ensure that FRAG_CHANGED event is triggered at startup, 34727 when first video frame is displayed and playback is paused. 34728 add a tolerance of 100ms, in case current position is not buffered, 34729 check if current pos+100ms is buffered and use that buffer range 34730 for FRAG_CHANGED event reporting */ 34731 fragPlayingCurrent = this.getAppendedFrag(currentTime + 0.1); 34732 } 34733 if (fragPlayingCurrent) { 34734 this.backtrackFragment = null; 34735 const fragPlaying = this.fragPlaying; 34736 const fragCurrentLevel = fragPlayingCurrent.level; 34737 if (!fragPlaying || fragPlayingCurrent.sn !== fragPlaying.sn || fragPlaying.level !== fragCurrentLevel) { 34738 this.fragPlaying = fragPlayingCurrent; 34739 this.hls.trigger(Events.FRAG_CHANGED, { 34740 frag: fragPlayingCurrent 34741 }); 34742 if (!fragPlaying || fragPlaying.level !== fragCurrentLevel) { 34743 this.hls.trigger(Events.LEVEL_SWITCHED, { 34744 level: fragCurrentLevel 34745 }); 34746 } 34747 } 34748 } 34749 } 34750 } 34751 get nextLevel() { 34752 const frag = this.nextBufferedFrag; 34753 if (frag) { 34754 return frag.level; 34755 } 34756 return -1; 34757 } 34758 get currentFrag() { 34759 var _this$media2; 34760 if (this.fragPlaying) { 34761 return this.fragPlaying; 34762 } 34763 const currentTime = ((_this$media2 = this.media) == null ? void 0 : _this$media2.currentTime) || this.lastCurrentTime; 34764 if (isFiniteNumber(currentTime)) { 34765 return this.getAppendedFrag(currentTime); 34766 } 34767 return null; 34768 } 34769 get currentProgramDateTime() { 34770 var _this$media3; 34771 const currentTime = ((_this$media3 = this.media) == null ? void 0 : _this$media3.currentTime) || this.lastCurrentTime; 34772 if (isFiniteNumber(currentTime)) { 34773 const details = this.getLevelDetails(); 34774 const frag = this.currentFrag || (details ? findFragmentByPTS(null, details.fragments, currentTime) : null); 34775 if (frag) { 34776 const programDateTime = frag.programDateTime; 34777 if (programDateTime !== null) { 34778 const epocMs = programDateTime + (currentTime - frag.start) * 1000; 34779 return new Date(epocMs); 34780 } 34781 } 34782 } 34783 return null; 34784 } 34785 get currentLevel() { 34786 const frag = this.currentFrag; 34787 if (frag) { 34788 return frag.level; 34789 } 34790 return -1; 34791 } 34792 get nextBufferedFrag() { 34793 const frag = this.currentFrag; 34794 if (frag) { 34795 return this.followingBufferedFrag(frag); 34796 } 34797 return null; 34798 } 34799 get forceStartLoad() { 34800 return this._forceStartLoad; 34801 } 34802} 34803 34804class KeyLoader extends Logger { 34805 constructor(config, logger) { 34806 super('key-loader', logger); 34807 this.config = void 0; 34808 this.keyIdToKeyInfo = {}; 34809 this.emeController = null; 34810 this.config = config; 34811 } 34812 abort(type) { 34813 for (const id in this.keyIdToKeyInfo) { 34814 const loader = this.keyIdToKeyInfo[id].loader; 34815 if (loader) { 34816 var _loader$context; 34817 if (type && type !== ((_loader$context = loader.context) == null ? void 0 : _loader$context.frag.type)) { 34818 return; 34819 } 34820 loader.abort(); 34821 } 34822 } 34823 } 34824 detach() { 34825 for (const id in this.keyIdToKeyInfo) { 34826 const keyInfo = this.keyIdToKeyInfo[id]; 34827 // Remove cached EME keys on detach 34828 if (keyInfo.mediaKeySessionContext || keyInfo.decryptdata.isCommonEncryption) { 34829 delete this.keyIdToKeyInfo[id]; 34830 } 34831 } 34832 } 34833 destroy() { 34834 this.detach(); 34835 for (const id in this.keyIdToKeyInfo) { 34836 const loader = this.keyIdToKeyInfo[id].loader; 34837 if (loader) { 34838 loader.destroy(); 34839 } 34840 } 34841 this.keyIdToKeyInfo = {}; 34842 } 34843 createKeyLoadError(frag, details = ErrorDetails.KEY_LOAD_ERROR, error, networkDetails, response) { 34844 return new LoadError({ 34845 type: ErrorTypes.NETWORK_ERROR, 34846 details, 34847 fatal: false, 34848 frag, 34849 response, 34850 error, 34851 networkDetails 34852 }); 34853 } 34854 loadClear(loadingFrag, encryptedFragments, startFragRequested) { 34855 if (this.emeController && this.config.emeEnabled && !this.emeController.getSelectedKeySystemFormats().length) { 34856 // Access key-system with nearest key on start (loading frag is unencrypted) 34857 if (encryptedFragments.length) { 34858 for (let i = 0, l = encryptedFragments.length; i < l; i++) { 34859 const frag = encryptedFragments[i]; 34860 // Loading at or before segment with EXT-X-KEY, or first frag loading and last EXT-X-KEY 34861 if (loadingFrag.cc <= frag.cc && (!isMediaFragment(loadingFrag) || !isMediaFragment(frag) || loadingFrag.sn < frag.sn) || !startFragRequested && i == l - 1) { 34862 return this.emeController.selectKeySystemFormat(frag).then(keySystemFormat => { 34863 if (!this.emeController) { 34864 return; 34865 } 34866 frag.setKeyFormat(keySystemFormat); 34867 const keySystem = keySystemFormatToKeySystemDomain(keySystemFormat); 34868 if (keySystem) { 34869 return this.emeController.getKeySystemAccess([keySystem]); 34870 } 34871 }); 34872 } 34873 } 34874 } 34875 if (this.config.requireKeySystemAccessOnStart) { 34876 const keySystemsInConfig = getKeySystemsForConfig(this.config); 34877 if (keySystemsInConfig.length) { 34878 return this.emeController.getKeySystemAccess(keySystemsInConfig); 34879 } 34880 } 34881 } 34882 return null; 34883 } 34884 load(frag) { 34885 if (!frag.decryptdata && frag.encrypted && this.emeController && this.config.emeEnabled) { 34886 // Multiple keys, but none selected, resolve in eme-controller 34887 return this.emeController.selectKeySystemFormat(frag).then(keySystemFormat => { 34888 return this.loadInternal(frag, keySystemFormat); 34889 }); 34890 } 34891 return this.loadInternal(frag); 34892 } 34893 loadInternal(frag, keySystemFormat) { 34894 var _keyInfo, _keyInfo2; 34895 if (keySystemFormat) { 34896 frag.setKeyFormat(keySystemFormat); 34897 } 34898 const decryptdata = frag.decryptdata; 34899 if (!decryptdata) { 34900 const error = new Error(keySystemFormat ? `Expected frag.decryptdata to be defined after setting format ${keySystemFormat}` : `Missing decryption data on fragment in onKeyLoading (emeEnabled with controller: ${this.emeController && this.config.emeEnabled})`); 34901 return Promise.reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, error)); 34902 } 34903 const uri = decryptdata.uri; 34904 if (!uri) { 34905 return Promise.reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, new Error(`Invalid key URI: "${uri}"`))); 34906 } 34907 const id = getKeyId(decryptdata); 34908 let keyInfo = this.keyIdToKeyInfo[id]; 34909 if ((_keyInfo = keyInfo) != null && _keyInfo.decryptdata.key) { 34910 decryptdata.key = keyInfo.decryptdata.key; 34911 return Promise.resolve({ 34912 frag, 34913 keyInfo 34914 }); 34915 } 34916 // Return key load promise once it has a mediakey session with an usable key status 34917 if (this.emeController && (_keyInfo2 = keyInfo) != null && _keyInfo2.keyLoadPromise) { 34918 const keyStatus = this.emeController.getKeyStatus(keyInfo.decryptdata); 34919 switch (keyStatus) { 34920 case 'usable': 34921 case 'usable-in-future': 34922 return keyInfo.keyLoadPromise.then(keyLoadedData => { 34923 // Return the correct fragment with updated decryptdata key and loaded keyInfo 34924 const { 34925 keyInfo 34926 } = keyLoadedData; 34927 decryptdata.key = keyInfo.decryptdata.key; 34928 return { 34929 frag, 34930 keyInfo 34931 }; 34932 }); 34933 } 34934 // If we have a key session and status and it is not pending or usable, continue 34935 // This will go back to the eme-controller for expired keys to get a new keyLoadPromise 34936 } 34937 34938 // Load the key or return the loading promise 34939 this.log(`${this.keyIdToKeyInfo[id] ? 'Rel' : 'L'}oading${decryptdata.keyId ? ' keyId: ' + arrayToHex(decryptdata.keyId) : ''} URI: ${decryptdata.uri} from ${frag.type} ${frag.level}`); 34940 keyInfo = this.keyIdToKeyInfo[id] = { 34941 decryptdata, 34942 keyLoadPromise: null, 34943 loader: null, 34944 mediaKeySessionContext: null 34945 }; 34946 switch (decryptdata.method) { 34947 case 'SAMPLE-AES': 34948 case 'SAMPLE-AES-CENC': 34949 case 'SAMPLE-AES-CTR': 34950 if (decryptdata.keyFormat === 'identity') { 34951 // loadKeyHTTP handles http(s) and data URLs 34952 return this.loadKeyHTTP(keyInfo, frag); 34953 } 34954 return this.loadKeyEME(keyInfo, frag); 34955 case 'AES-128': 34956 case 'AES-256': 34957 case 'AES-256-CTR': 34958 return this.loadKeyHTTP(keyInfo, frag); 34959 default: 34960 return Promise.reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, new Error(`Key supplied with unsupported METHOD: "${decryptdata.method}"`))); 34961 } 34962 } 34963 loadKeyEME(keyInfo, frag) { 34964 const keyLoadedData = { 34965 frag, 34966 keyInfo 34967 }; 34968 if (this.emeController && this.config.emeEnabled) { 34969 var _frag$initSegment; 34970 if (!keyInfo.decryptdata.keyId && (_frag$initSegment = frag.initSegment) != null && _frag$initSegment.data) { 34971 const keyIds = parseKeyIdsFromTenc(frag.initSegment.data); 34972 if (keyIds.length) { 34973 let keyId = keyIds[0]; 34974 if (keyId.some(b => b !== 0)) { 34975 this.log(`Using keyId found in init segment ${arrayToHex(keyId)}`); 34976 LevelKey.setKeyIdForUri(keyInfo.decryptdata.uri, keyId); 34977 } else { 34978 keyId = LevelKey.addKeyIdForUri(keyInfo.decryptdata.uri); 34979 this.log(`Generating keyId to patch media ${arrayToHex(keyId)}`); 34980 } 34981 keyInfo.decryptdata.keyId = keyId; 34982 } 34983 } 34984 if (!keyInfo.decryptdata.keyId && !isMediaFragment(frag)) { 34985 // Resolve so that unencrypted init segment is loaded 34986 // key id is extracted from tenc box when processing key for next segment above 34987 return Promise.resolve(keyLoadedData); 34988 } 34989 const keySessionContextPromise = this.emeController.loadKey(keyLoadedData); 34990 return (keyInfo.keyLoadPromise = keySessionContextPromise.then(keySessionContext => { 34991 keyInfo.mediaKeySessionContext = keySessionContext; 34992 return keyLoadedData; 34993 })).catch(error => { 34994 // Remove promise for license renewal or retry 34995 keyInfo.keyLoadPromise = null; 34996 if ('data' in error) { 34997 error.data.frag = frag; 34998 } 34999 throw error; 35000 }); 35001 } 35002 return Promise.resolve(keyLoadedData); 35003 } 35004 loadKeyHTTP(keyInfo, frag) { 35005 const config = this.config; 35006 const Loader = config.loader; 35007 const keyLoader = new Loader(config); 35008 frag.keyLoader = keyInfo.loader = keyLoader; 35009 return keyInfo.keyLoadPromise = new Promise((resolve, reject) => { 35010 const loaderContext = { 35011 keyInfo, 35012 frag, 35013 responseType: 'arraybuffer', 35014 url: keyInfo.decryptdata.uri 35015 }; 35016 35017 // maxRetry is 0 so that instead of retrying the same key on the same variant multiple times, 35018 // key-loader will trigger an error and rely on stream-controller to handle retry logic. 35019 // this will also align retry logic with fragment-loader 35020 const loadPolicy = config.keyLoadPolicy.default; 35021 const loaderConfig = { 35022 loadPolicy, 35023 timeout: loadPolicy.maxLoadTimeMs, 35024 maxRetry: 0, 35025 retryDelay: 0, 35026 maxRetryDelay: 0 35027 }; 35028 const loaderCallbacks = { 35029 onSuccess: (response, stats, context, networkDetails) => { 35030 const { 35031 frag, 35032 keyInfo 35033 } = context; 35034 const id = getKeyId(keyInfo.decryptdata); 35035 if (!frag.decryptdata || keyInfo !== this.keyIdToKeyInfo[id]) { 35036 return reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, new Error('after key load, decryptdata unset or changed'), networkDetails)); 35037 } 35038 keyInfo.decryptdata.key = frag.decryptdata.key = new Uint8Array(response.data); 35039 35040 // detach fragment key loader on load success 35041 frag.keyLoader = null; 35042 keyInfo.loader = null; 35043 resolve({ 35044 frag, 35045 keyInfo 35046 }); 35047 }, 35048 onError: (response, context, networkDetails, stats) => { 35049 this.resetLoader(context); 35050 reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_ERROR, new Error(`HTTP Error ${response.code} loading key ${response.text}`), networkDetails, _objectSpread2({ 35051 url: loaderContext.url, 35052 data: undefined 35053 }, response))); 35054 }, 35055 onTimeout: (stats, context, networkDetails) => { 35056 this.resetLoader(context); 35057 reject(this.createKeyLoadError(frag, ErrorDetails.KEY_LOAD_TIMEOUT, new Error('key loading timed out'), networkDetails)); 35058 }, 35059 onAbort: (stats, context, networkDetails) => { 35060 this.resetLoader(context); 35061 reject(this.createKeyLoadError(frag, ErrorDetails.INTERNAL_ABORTED, new Error('key loading aborted'), networkDetails)); 35062 } 35063 }; 35064 keyLoader.load(loaderContext, loaderConfig, loaderCallbacks); 35065 }); 35066 } 35067 resetLoader(context) { 35068 const { 35069 frag, 35070 keyInfo, 35071 url: uri 35072 } = context; 35073 const loader = keyInfo.loader; 35074 if (frag.keyLoader === loader) { 35075 frag.keyLoader = null; 35076 keyInfo.loader = null; 35077 } 35078 const id = getKeyId(keyInfo.decryptdata) || uri; 35079 delete this.keyIdToKeyInfo[id]; 35080 if (loader) { 35081 loader.destroy(); 35082 } 35083 } 35084} 35085function getKeyId(decryptdata) { 35086 if (decryptdata.keyFormat !== KeySystemFormats.FAIRPLAY) { 35087 const keyId = decryptdata.keyId; 35088 if (keyId) { 35089 return arrayToHex(keyId); 35090 } 35091 } 35092 return decryptdata.uri; 35093} 35094 35095function mapContextToLevelType(context) { 35096 const { 35097 type 35098 } = context; 35099 switch (type) { 35100 case PlaylistContextType.AUDIO_TRACK: 35101 return PlaylistLevelType.AUDIO; 35102 case PlaylistContextType.SUBTITLE_TRACK: 35103 return PlaylistLevelType.SUBTITLE; 35104 default: 35105 return PlaylistLevelType.MAIN; 35106 } 35107} 35108function getResponseUrl(response, context) { 35109 let url = response.url; 35110 // responseURL not supported on some browsers (it is used to detect URL redirection) 35111 // data-uri mode also not supported (but no need to detect redirection) 35112 if (url === undefined || url.indexOf('data:') === 0) { 35113 // fallback to initial URL 35114 url = context.url; 35115 } 35116 return url; 35117} 35118class PlaylistLoader { 35119 constructor(hls) { 35120 this.hls = void 0; 35121 this.loaders = Object.create(null); 35122 this.variableList = null; 35123 this.onManifestLoaded = this.checkAutostartLoad; 35124 this.hls = hls; 35125 this.registerListeners(); 35126 } 35127 startLoad(startPosition) {} 35128 stopLoad() { 35129 this.destroyInternalLoaders(); 35130 } 35131 registerListeners() { 35132 const { 35133 hls 35134 } = this; 35135 hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this); 35136 hls.on(Events.LEVEL_LOADING, this.onLevelLoading, this); 35137 hls.on(Events.AUDIO_TRACK_LOADING, this.onAudioTrackLoading, this); 35138 hls.on(Events.SUBTITLE_TRACK_LOADING, this.onSubtitleTrackLoading, this); 35139 hls.on(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); 35140 } 35141 unregisterListeners() { 35142 const { 35143 hls 35144 } = this; 35145 hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this); 35146 hls.off(Events.LEVEL_LOADING, this.onLevelLoading, this); 35147 hls.off(Events.AUDIO_TRACK_LOADING, this.onAudioTrackLoading, this); 35148 hls.off(Events.SUBTITLE_TRACK_LOADING, this.onSubtitleTrackLoading, this); 35149 hls.off(Events.LEVELS_UPDATED, this.onLevelsUpdated, this); 35150 } 35151 35152 /** 35153 * Returns defaults or configured loader-type overloads (pLoader and loader config params) 35154 */ 35155 createInternalLoader(context) { 35156 const config = this.hls.config; 35157 const PLoader = config.pLoader; 35158 const Loader = config.loader; 35159 const InternalLoader = PLoader || Loader; 35160 const loader = new InternalLoader(config); 35161 this.loaders[context.type] = loader; 35162 return loader; 35163 } 35164 getInternalLoader(context) { 35165 return this.loaders[context.type]; 35166 } 35167 resetInternalLoader(contextType) { 35168 if (this.loaders[contextType]) { 35169 delete this.loaders[contextType]; 35170 } 35171 } 35172 35173 /** 35174 * Call `destroy` on all internal loader instances mapped (one per context type) 35175 */ 35176 destroyInternalLoaders() { 35177 for (const contextType in this.loaders) { 35178 const loader = this.loaders[contextType]; 35179 if (loader) { 35180 loader.destroy(); 35181 } 35182 this.resetInternalLoader(contextType); 35183 } 35184 } 35185 destroy() { 35186 this.variableList = null; 35187 this.unregisterListeners(); 35188 this.destroyInternalLoaders(); 35189 } 35190 onManifestLoading(event, data) { 35191 const { 35192 url 35193 } = data; 35194 this.variableList = null; 35195 this.load({ 35196 id: null, 35197 level: 0, 35198 responseType: 'text', 35199 type: PlaylistContextType.MANIFEST, 35200 url, 35201 deliveryDirectives: null, 35202 levelOrTrack: null 35203 }); 35204 } 35205 onLevelLoading(event, data) { 35206 const { 35207 id, 35208 level, 35209 pathwayId, 35210 url, 35211 deliveryDirectives, 35212 levelInfo 35213 } = data; 35214 this.load({ 35215 id, 35216 level, 35217 pathwayId, 35218 responseType: 'text', 35219 type: PlaylistContextType.LEVEL, 35220 url, 35221 deliveryDirectives, 35222 levelOrTrack: levelInfo 35223 }); 35224 } 35225 onAudioTrackLoading(event, data) { 35226 const { 35227 id, 35228 groupId, 35229 url, 35230 deliveryDirectives, 35231 track 35232 } = data; 35233 this.load({ 35234 id, 35235 groupId, 35236 level: null, 35237 responseType: 'text', 35238 type: PlaylistContextType.AUDIO_TRACK, 35239 url, 35240 deliveryDirectives, 35241 levelOrTrack: track 35242 }); 35243 } 35244 onSubtitleTrackLoading(event, data) { 35245 const { 35246 id, 35247 groupId, 35248 url, 35249 deliveryDirectives, 35250 track 35251 } = data; 35252 this.load({ 35253 id, 35254 groupId, 35255 level: null, 35256 responseType: 'text', 35257 type: PlaylistContextType.SUBTITLE_TRACK, 35258 url, 35259 deliveryDirectives, 35260 levelOrTrack: track 35261 }); 35262 } 35263 onLevelsUpdated(event, data) { 35264 // abort and delete loader of removed levels 35265 const loader = this.loaders[PlaylistContextType.LEVEL]; 35266 if (loader) { 35267 const context = loader.context; 35268 if (context && !data.levels.some(lvl => lvl === context.levelOrTrack)) { 35269 loader.abort(); 35270 delete this.loaders[PlaylistContextType.LEVEL]; 35271 } 35272 } 35273 } 35274 load(context) { 35275 var _context$deliveryDire; 35276 const config = this.hls.config; 35277 35278 // logger.debug(`[playlist-loader]: Loading playlist of type ${context.type}, level: ${context.level}, id: ${context.id}`); 35279 35280 // Check if a loader for this context already exists 35281 let loader = this.getInternalLoader(context); 35282 if (loader) { 35283 const logger = this.hls.logger; 35284 const loaderContext = loader.context; 35285 if (loaderContext && loaderContext.levelOrTrack === context.levelOrTrack && (loaderContext.url === context.url || loaderContext.deliveryDirectives && !context.deliveryDirectives)) { 35286 // same URL can't overlap, or wait for blocking request 35287 if (loaderContext.url === context.url) { 35288 logger.log(`[playlist-loader]: ignore ${context.url} ongoing request`); 35289 } else { 35290 logger.log(`[playlist-loader]: ignore ${context.url} in favor of ${loaderContext.url}`); 35291 } 35292 return; 35293 } 35294 logger.log(`[playlist-loader]: aborting previous loader for type: ${context.type}`); 35295 loader.abort(); 35296 } 35297 35298 // apply different configs for retries depending on 35299 // context (manifest, level, audio/subs playlist) 35300 let loadPolicy; 35301 if (context.type === PlaylistContextType.MANIFEST) { 35302 loadPolicy = config.manifestLoadPolicy.default; 35303 } else { 35304 loadPolicy = _extends({}, config.playlistLoadPolicy.default, { 35305 timeoutRetry: null, 35306 errorRetry: null 35307 }); 35308 } 35309 loader = this.createInternalLoader(context); 35310 35311 // Override level/track timeout for LL-HLS requests 35312 // (the default of 10000ms is counter productive to blocking playlist reload requests) 35313 if (isFiniteNumber((_context$deliveryDire = context.deliveryDirectives) == null ? void 0 : _context$deliveryDire.part)) { 35314 let levelDetails; 35315 if (context.type === PlaylistContextType.LEVEL && context.level !== null) { 35316 levelDetails = this.hls.levels[context.level].details; 35317 } else if (context.type === PlaylistContextType.AUDIO_TRACK && context.id !== null) { 35318 levelDetails = this.hls.audioTracks[context.id].details; 35319 } else if (context.type === PlaylistContextType.SUBTITLE_TRACK && context.id !== null) { 35320 levelDetails = this.hls.subtitleTracks[context.id].details; 35321 } 35322 if (levelDetails) { 35323 const partTarget = levelDetails.partTarget; 35324 const targetDuration = levelDetails.targetduration; 35325 if (partTarget && targetDuration) { 35326 const maxLowLatencyPlaylistRefresh = Math.max(partTarget * 3, targetDuration * 0.8) * 1000; 35327 loadPolicy = _extends({}, loadPolicy, { 35328 maxTimeToFirstByteMs: Math.min(maxLowLatencyPlaylistRefresh, loadPolicy.maxTimeToFirstByteMs), 35329 maxLoadTimeMs: Math.min(maxLowLatencyPlaylistRefresh, loadPolicy.maxTimeToFirstByteMs) 35330 }); 35331 } 35332 } 35333 } 35334 const legacyRetryCompatibility = loadPolicy.errorRetry || loadPolicy.timeoutRetry || {}; 35335 const loaderConfig = { 35336 loadPolicy, 35337 timeout: loadPolicy.maxLoadTimeMs, 35338 maxRetry: legacyRetryCompatibility.maxNumRetry || 0, 35339 retryDelay: legacyRetryCompatibility.retryDelayMs || 0, 35340 maxRetryDelay: legacyRetryCompatibility.maxRetryDelayMs || 0 35341 }; 35342 const loaderCallbacks = { 35343 onSuccess: (response, stats, context, networkDetails) => { 35344 const loader = this.getInternalLoader(context); 35345 this.resetInternalLoader(context.type); 35346 const string = response.data; 35347 stats.parsing.start = performance.now(); 35348 if (M3U8Parser.isMediaPlaylist(string) || context.type !== PlaylistContextType.MANIFEST) { 35349 this.handleTrackOrLevelPlaylist(response, stats, context, networkDetails || null, loader); 35350 } else { 35351 this.handleMasterPlaylist(response, stats, context, networkDetails); 35352 } 35353 }, 35354 onError: (response, context, networkDetails, stats) => { 35355 this.handleNetworkError(context, networkDetails, false, response, stats); 35356 }, 35357 onTimeout: (stats, context, networkDetails) => { 35358 this.handleNetworkError(context, networkDetails, true, undefined, stats); 35359 } 35360 }; 35361 35362 // logger.debug(`[playlist-loader]: Calling internal loader delegate for URL: ${context.url}`); 35363 35364 loader.load(context, loaderConfig, loaderCallbacks); 35365 } 35366 checkAutostartLoad() { 35367 if (!this.hls) { 35368 return; 35369 } 35370 const { 35371 config: { 35372 autoStartLoad, 35373 startPosition 35374 }, 35375 forceStartLoad 35376 } = this.hls; 35377 if (autoStartLoad || forceStartLoad) { 35378 this.hls.logger.log(`${autoStartLoad ? 'auto' : 'force'} startLoad with configured startPosition ${startPosition}`); 35379 this.hls.startLoad(startPosition); 35380 } 35381 } 35382 handleMasterPlaylist(response, stats, context, networkDetails) { 35383 const hls = this.hls; 35384 const string = response.data; 35385 const url = getResponseUrl(response, context); 35386 const parsedResult = M3U8Parser.parseMasterPlaylist(string, url); 35387 if (parsedResult.playlistParsingError) { 35388 stats.parsing.end = performance.now(); 35389 this.handleManifestParsingError(response, context, parsedResult.playlistParsingError, networkDetails, stats); 35390 return; 35391 } 35392 const { 35393 contentSteering, 35394 levels, 35395 sessionData, 35396 sessionKeys, 35397 startTimeOffset, 35398 variableList 35399 } = parsedResult; 35400 this.variableList = variableList; 35401 35402 // Treat unknown codec as audio or video codec based on passing `isTypeSupported` check 35403 // (allows for playback of any supported codec even if not indexed in utils/codecs) 35404 levels.forEach(levelParsed => { 35405 const { 35406 unknownCodecs 35407 } = levelParsed; 35408 if (unknownCodecs) { 35409 const { 35410 preferManagedMediaSource 35411 } = this.hls.config; 35412 let { 35413 audioCodec, 35414 videoCodec 35415 } = levelParsed; 35416 for (let i = unknownCodecs.length; i--;) { 35417 const unknownCodec = unknownCodecs[i]; 35418 if (areCodecsMediaSourceSupported(unknownCodec, 'audio', preferManagedMediaSource)) { 35419 levelParsed.audioCodec = audioCodec = audioCodec ? `${audioCodec},${unknownCodec}` : unknownCodec; 35420 sampleEntryCodesISO.audio[audioCodec.substring(0, 4)] = 2; 35421 unknownCodecs.splice(i, 1); 35422 } else if (areCodecsMediaSourceSupported(unknownCodec, 'video', preferManagedMediaSource)) { 35423 levelParsed.videoCodec = videoCodec = videoCodec ? `${videoCodec},${unknownCodec}` : unknownCodec; 35424 sampleEntryCodesISO.video[videoCodec.substring(0, 4)] = 2; 35425 unknownCodecs.splice(i, 1); 35426 } 35427 } 35428 } 35429 }); 35430 const { 35431 AUDIO: audioTracks = [], 35432 SUBTITLES: subtitles, 35433 'CLOSED-CAPTIONS': captions 35434 } = M3U8Parser.parseMasterPlaylistMedia(string, url, parsedResult); 35435 if (audioTracks.length) { 35436 // check if we have found an audio track embedded in main playlist (audio track without URI attribute) 35437 const embeddedAudioFound = audioTracks.some(audioTrack => !audioTrack.url); 35438 35439 // if no embedded audio track defined, but audio codec signaled in quality level, 35440 // we need to signal this main audio track this could happen with playlists with 35441 // alt audio rendition in which quality levels (main) 35442 // contains both audio+video. but with mixed audio track not signaled 35443 if (!embeddedAudioFound && levels[0].audioCodec && !levels[0].attrs.AUDIO) { 35444 this.hls.logger.log('[playlist-loader]: audio codec signaled in quality level, but no embedded audio track signaled, create one'); 35445 audioTracks.unshift({ 35446 type: 'main', 35447 name: 'main', 35448 groupId: 'main', 35449 default: false, 35450 autoselect: false, 35451 forced: false, 35452 id: -1, 35453 attrs: new AttrList({}), 35454 bitrate: 0, 35455 url: '' 35456 }); 35457 } 35458 } 35459 hls.trigger(Events.MANIFEST_LOADED, { 35460 levels, 35461 audioTracks, 35462 subtitles, 35463 captions, 35464 contentSteering, 35465 url, 35466 stats, 35467 networkDetails, 35468 sessionData, 35469 sessionKeys, 35470 startTimeOffset, 35471 variableList 35472 }); 35473 } 35474 handleTrackOrLevelPlaylist(response, stats, context, networkDetails, loader) { 35475 const hls = this.hls; 35476 const { 35477 id, 35478 level, 35479 type 35480 } = context; 35481 const url = getResponseUrl(response, context); 35482 const levelId = isFiniteNumber(level) ? level : isFiniteNumber(id) ? id : 0; 35483 const levelType = mapContextToLevelType(context); 35484 const levelDetails = M3U8Parser.parseLevelPlaylist(response.data, url, levelId, levelType, 0, this.variableList); 35485 35486 // We have done our first request (Manifest-type) and receive 35487 // not a master playlist but a chunk-list (track/level) 35488 // We fire the manifest-loaded event anyway with the parsed level-details 35489 // by creating a single-level structure for it. 35490 if (type === PlaylistContextType.MANIFEST) { 35491 const singleLevel = { 35492 attrs: new AttrList({}), 35493 bitrate: 0, 35494 details: levelDetails, 35495 name: '', 35496 url 35497 }; 35498 levelDetails.requestScheduled = stats.loading.start + computeReloadInterval(levelDetails, 0); 35499 hls.trigger(Events.MANIFEST_LOADED, { 35500 levels: [singleLevel], 35501 audioTracks: [], 35502 url, 35503 stats, 35504 networkDetails, 35505 sessionData: null, 35506 sessionKeys: null, 35507 contentSteering: null, 35508 startTimeOffset: null, 35509 variableList: null 35510 }); 35511 } 35512 35513 // save parsing time 35514 stats.parsing.end = performance.now(); 35515 35516 // extend the context with the new levelDetails property 35517 context.levelDetails = levelDetails; 35518 this.handlePlaylistLoaded(levelDetails, response, stats, context, networkDetails, loader); 35519 } 35520 handleManifestParsingError(response, context, error, networkDetails, stats) { 35521 this.hls.trigger(Events.ERROR, { 35522 type: ErrorTypes.NETWORK_ERROR, 35523 details: ErrorDetails.MANIFEST_PARSING_ERROR, 35524 fatal: context.type === PlaylistContextType.MANIFEST, 35525 url: response.url, 35526 err: error, 35527 error, 35528 reason: error.message, 35529 response, 35530 context, 35531 networkDetails, 35532 stats 35533 }); 35534 } 35535 handleNetworkError(context, networkDetails, timeout = false, response, stats) { 35536 let message = `A network ${timeout ? 'timeout' : 'error' + (response ? ' (status ' + response.code + ')' : '')} occurred while loading ${context.type}`; 35537 if (context.type === PlaylistContextType.LEVEL) { 35538 message += `: ${context.level} id: ${context.id}`; 35539 } else if (context.type === PlaylistContextType.AUDIO_TRACK || context.type === PlaylistContextType.SUBTITLE_TRACK) { 35540 message += ` id: ${context.id} group-id: "${context.groupId}"`; 35541 } 35542 const error = new Error(message); 35543 this.hls.logger.warn(`[playlist-loader]: ${message}`); 35544 let details = ErrorDetails.UNKNOWN; 35545 let fatal = false; 35546 const loader = this.getInternalLoader(context); 35547 switch (context.type) { 35548 case PlaylistContextType.MANIFEST: 35549 details = timeout ? ErrorDetails.MANIFEST_LOAD_TIMEOUT : ErrorDetails.MANIFEST_LOAD_ERROR; 35550 fatal = true; 35551 break; 35552 case PlaylistContextType.LEVEL: 35553 details = timeout ? ErrorDetails.LEVEL_LOAD_TIMEOUT : ErrorDetails.LEVEL_LOAD_ERROR; 35554 fatal = false; 35555 break; 35556 case PlaylistContextType.AUDIO_TRACK: 35557 details = timeout ? ErrorDetails.AUDIO_TRACK_LOAD_TIMEOUT : ErrorDetails.AUDIO_TRACK_LOAD_ERROR; 35558 fatal = false; 35559 break; 35560 case PlaylistContextType.SUBTITLE_TRACK: 35561 details = timeout ? ErrorDetails.SUBTITLE_TRACK_LOAD_TIMEOUT : ErrorDetails.SUBTITLE_LOAD_ERROR; 35562 fatal = false; 35563 break; 35564 } 35565 if (loader) { 35566 this.resetInternalLoader(context.type); 35567 } 35568 const errorData = { 35569 type: ErrorTypes.NETWORK_ERROR, 35570 details, 35571 fatal, 35572 url: context.url, 35573 loader, 35574 context, 35575 error, 35576 networkDetails, 35577 stats 35578 }; 35579 if (response) { 35580 const url = (networkDetails == null ? void 0 : networkDetails.url) || context.url; 35581 errorData.response = _objectSpread2({ 35582 url, 35583 data: undefined 35584 }, response); 35585 } 35586 this.hls.trigger(Events.ERROR, errorData); 35587 } 35588 handlePlaylistLoaded(levelDetails, response, stats, context, networkDetails, loader) { 35589 const hls = this.hls; 35590 const { 35591 type, 35592 level, 35593 levelOrTrack, 35594 id, 35595 groupId, 35596 deliveryDirectives 35597 } = context; 35598 const url = getResponseUrl(response, context); 35599 const parent = mapContextToLevelType(context); 35600 let levelIndex = typeof context.level === 'number' && parent === PlaylistLevelType.MAIN ? level : undefined; 35601 const error = levelDetails.playlistParsingError; 35602 if (error) { 35603 this.hls.logger.warn(`${error} ${levelDetails.url}`); 35604 if (!hls.config.ignorePlaylistParsingErrors) { 35605 hls.trigger(Events.ERROR, { 35606 type: ErrorTypes.NETWORK_ERROR, 35607 details: ErrorDetails.LEVEL_PARSING_ERROR, 35608 fatal: false, 35609 url, 35610 error, 35611 reason: error.message, 35612 response, 35613 context, 35614 level: levelIndex, 35615 parent, 35616 networkDetails, 35617 stats 35618 }); 35619 return; 35620 } 35621 levelDetails.playlistParsingError = null; 35622 } 35623 if (!levelDetails.fragments.length) { 35624 const _error = levelDetails.playlistParsingError = new Error('No Segments found in Playlist'); 35625 hls.trigger(Events.ERROR, { 35626 type: ErrorTypes.NETWORK_ERROR, 35627 details: ErrorDetails.LEVEL_EMPTY_ERROR, 35628 fatal: false, 35629 url, 35630 error: _error, 35631 reason: _error.message, 35632 response, 35633 context, 35634 level: levelIndex, 35635 parent, 35636 networkDetails, 35637 stats 35638 }); 35639 return; 35640 } 35641 if (levelDetails.live && loader) { 35642 if (loader.getCacheAge) { 35643 levelDetails.ageHeader = loader.getCacheAge() || 0; 35644 } 35645 if (!loader.getCacheAge || isNaN(levelDetails.ageHeader)) { 35646 levelDetails.ageHeader = 0; 35647 } 35648 } 35649 switch (type) { 35650 case PlaylistContextType.MANIFEST: 35651 case PlaylistContextType.LEVEL: 35652 if (levelIndex) { 35653 if (!levelOrTrack) { 35654 // fall-through to hls.levels[0] 35655 levelIndex = 0; 35656 } else { 35657 if (levelOrTrack !== hls.levels[levelIndex]) { 35658 // correct levelIndex when lower levels were removed from hls.levels 35659 const updatedIndex = hls.levels.indexOf(levelOrTrack); 35660 if (updatedIndex > -1) { 35661 levelIndex = updatedIndex; 35662 } 35663 } 35664 } 35665 } 35666 hls.trigger(Events.LEVEL_LOADED, { 35667 details: levelDetails, 35668 levelInfo: levelOrTrack || hls.levels[0], 35669 level: levelIndex || 0, 35670 id: id || 0, 35671 stats, 35672 networkDetails, 35673 deliveryDirectives, 35674 withoutMultiVariant: type === PlaylistContextType.MANIFEST 35675 }); 35676 break; 35677 case PlaylistContextType.AUDIO_TRACK: 35678 hls.trigger(Events.AUDIO_TRACK_LOADED, { 35679 details: levelDetails, 35680 track: levelOrTrack, 35681 id: id || 0, 35682 groupId: groupId || '', 35683 stats, 35684 networkDetails, 35685 deliveryDirectives 35686 }); 35687 break; 35688 case PlaylistContextType.SUBTITLE_TRACK: 35689 hls.trigger(Events.SUBTITLE_TRACK_LOADED, { 35690 details: levelDetails, 35691 track: levelOrTrack, 35692 id: id || 0, 35693 groupId: groupId || '', 35694 stats, 35695 networkDetails, 35696 deliveryDirectives 35697 }); 35698 break; 35699 } 35700 } 35701} 35702 35703/** 35704 * The `Hls` class is the core of the HLS.js library used to instantiate player instances. 35705 * @public 35706 */ 35707class Hls { 35708 /** 35709 * Get the video-dev/hls.js package version. 35710 */ 35711 static get version() { 35712 return version; 35713 } 35714 35715 /** 35716 * Check if the required MediaSource Extensions are available. 35717 */ 35718 static isMSESupported() { 35719 return isMSESupported(); 35720 } 35721 35722 /** 35723 * Check if MediaSource Extensions are available and isTypeSupported checks pass for any baseline codecs. 35724 */ 35725 static isSupported() { 35726 return isSupported(); 35727 } 35728 35729 /** 35730 * Get the MediaSource global used for MSE playback (ManagedMediaSource, MediaSource, or WebKitMediaSource). 35731 */ 35732 static getMediaSource() { 35733 return getMediaSource(); 35734 } 35735 static get Events() { 35736 return Events; 35737 } 35738 static get MetadataSchema() { 35739 return MetadataSchema; 35740 } 35741 static get ErrorTypes() { 35742 return ErrorTypes; 35743 } 35744 static get ErrorDetails() { 35745 return ErrorDetails; 35746 } 35747 35748 /** 35749 * Get the default configuration applied to new instances. 35750 */ 35751 static get DefaultConfig() { 35752 if (!Hls.defaultConfig) { 35753 return hlsDefaultConfig; 35754 } 35755 return Hls.defaultConfig; 35756 } 35757 35758 /** 35759 * Replace the default configuration applied to new instances. 35760 */ 35761 static set DefaultConfig(defaultConfig) { 35762 Hls.defaultConfig = defaultConfig; 35763 } 35764 35765 /** 35766 * Creates an instance of an HLS client that can attach to exactly one `HTMLMediaElement`. 35767 * @param userConfig - Configuration options applied over `Hls.DefaultConfig` 35768 */ 35769 constructor(userConfig = {}) { 35770 /** 35771 * The runtime configuration used by the player. At instantiation this is combination of `hls.userConfig` merged over `Hls.DefaultConfig`. 35772 */ 35773 this.config = void 0; 35774 /** 35775 * The configuration object provided on player instantiation. 35776 */ 35777 this.userConfig = void 0; 35778 /** 35779 * The logger functions used by this player instance, configured on player instantiation. 35780 */ 35781 this.logger = void 0; 35782 this.coreComponents = void 0; 35783 this.networkControllers = void 0; 35784 this._emitter = new EventEmitter(); 35785 this._autoLevelCapping = -1; 35786 this._maxHdcpLevel = null; 35787 this.abrController = void 0; 35788 this.bufferController = void 0; 35789 this.capLevelController = void 0; 35790 this.latencyController = void 0; 35791 this.levelController = void 0; 35792 this.streamController = void 0; 35793 this.audioStreamController = void 0; 35794 this.subtititleStreamController = void 0; 35795 this.audioTrackController = void 0; 35796 this.subtitleTrackController = void 0; 35797 this.interstitialsController = void 0; 35798 this.gapController = void 0; 35799 this.emeController = void 0; 35800 this.cmcdController = void 0; 35801 this._media = null; 35802 this._url = null; 35803 this._sessionId = void 0; 35804 this.triggeringException = void 0; 35805 this.started = false; 35806 const logger = this.logger = enableLogs(userConfig.debug || false, 'Hls instance', userConfig.assetPlayerId); 35807 const config = this.config = mergeConfig(Hls.DefaultConfig, userConfig, logger); 35808 this.userConfig = userConfig; 35809 if (config.progressive) { 35810 enableStreamingMode(config, logger); 35811 } 35812 35813 // core controllers and network loaders 35814 const { 35815 abrController: _AbrController, 35816 bufferController: _BufferController, 35817 capLevelController: _CapLevelController, 35818 errorController: _ErrorController, 35819 fpsController: _FpsController 35820 } = config; 35821 const errorController = new _ErrorController(this); 35822 const abrController = this.abrController = new _AbrController(this); 35823 // FragmentTracker must be defined before StreamController because the order of event handling is important 35824 const fragmentTracker = new FragmentTracker(this); 35825 const _InterstitialsController = config.interstitialsController; 35826 const interstitialsController = _InterstitialsController ? this.interstitialsController = new _InterstitialsController(this, Hls) : null; 35827 const bufferController = this.bufferController = new _BufferController(this, fragmentTracker); 35828 const capLevelController = this.capLevelController = new _CapLevelController(this); 35829 const fpsController = new _FpsController(this); 35830 const playListLoader = new PlaylistLoader(this); 35831 const _ContentSteeringController = config.contentSteeringController; 35832 // Instantiate ConentSteeringController before LevelController to receive Multivariant Playlist events first 35833 const contentSteering = _ContentSteeringController ? new _ContentSteeringController(this) : null; 35834 const levelController = this.levelController = new LevelController(this, contentSteering); 35835 const id3TrackController = new ID3TrackController(this); 35836 const keyLoader = new KeyLoader(this.config, this.logger); 35837 const streamController = this.streamController = new StreamController(this, fragmentTracker, keyLoader); 35838 const gapController = this.gapController = new GapController(this, fragmentTracker); 35839 35840 // Cap level controller uses streamController to flush the buffer 35841 capLevelController.setStreamController(streamController); 35842 // fpsController uses streamController to switch when frames are being dropped 35843 fpsController.setStreamController(streamController); 35844 const networkControllers = [playListLoader, levelController, streamController]; 35845 if (interstitialsController) { 35846 networkControllers.splice(1, 0, interstitialsController); 35847 } 35848 if (contentSteering) { 35849 networkControllers.splice(1, 0, contentSteering); 35850 } 35851 this.networkControllers = networkControllers; 35852 const coreComponents = [abrController, bufferController, gapController, capLevelController, fpsController, id3TrackController, fragmentTracker]; 35853 this.audioTrackController = this.createController(config.audioTrackController, networkControllers); 35854 const AudioStreamControllerClass = config.audioStreamController; 35855 if (AudioStreamControllerClass) { 35856 networkControllers.push(this.audioStreamController = new AudioStreamControllerClass(this, fragmentTracker, keyLoader)); 35857 } 35858 // Instantiate subtitleTrackController before SubtitleStreamController to receive level events first 35859 this.subtitleTrackController = this.createController(config.subtitleTrackController, networkControllers); 35860 const SubtitleStreamControllerClass = config.subtitleStreamController; 35861 if (SubtitleStreamControllerClass) { 35862 networkControllers.push(this.subtititleStreamController = new SubtitleStreamControllerClass(this, fragmentTracker, keyLoader)); 35863 } 35864 this.createController(config.timelineController, coreComponents); 35865 keyLoader.emeController = this.emeController = this.createController(config.emeController, coreComponents); 35866 this.cmcdController = this.createController(config.cmcdController, coreComponents); 35867 this.latencyController = this.createController(LatencyController, coreComponents); 35868 this.coreComponents = coreComponents; 35869 35870 // Error controller handles errors before and after all other controllers 35871 // This listener will be invoked after all other controllers error listeners 35872 networkControllers.push(errorController); 35873 const onErrorOut = errorController.onErrorOut; 35874 if (typeof onErrorOut === 'function') { 35875 this.on(Events.ERROR, onErrorOut, errorController); 35876 } 35877 // Autostart load handler 35878 this.on(Events.MANIFEST_LOADED, playListLoader.onManifestLoaded, playListLoader); 35879 } 35880 createController(ControllerClass, components) { 35881 if (ControllerClass) { 35882 const controllerInstance = new ControllerClass(this); 35883 if (components) { 35884 components.push(controllerInstance); 35885 } 35886 return controllerInstance; 35887 } 35888 return null; 35889 } 35890 35891 // Delegate the EventEmitter through the public API of Hls.js 35892 on(event, listener, context = this) { 35893 this._emitter.on(event, listener, context); 35894 } 35895 once(event, listener, context = this) { 35896 this._emitter.once(event, listener, context); 35897 } 35898 removeAllListeners(event) { 35899 this._emitter.removeAllListeners(event); 35900 } 35901 off(event, listener, context = this, once) { 35902 this._emitter.off(event, listener, context, once); 35903 } 35904 listeners(event) { 35905 return this._emitter.listeners(event); 35906 } 35907 emit(event, name, eventObject) { 35908 return this._emitter.emit(event, name, eventObject); 35909 } 35910 trigger(event, eventObject) { 35911 if (this.config.debug) { 35912 return this.emit(event, event, eventObject); 35913 } else { 35914 try { 35915 return this.emit(event, event, eventObject); 35916 } catch (error) { 35917 this.logger.error('An internal error happened while handling event ' + event + '. Error message: "' + error.message + '". Here is a stacktrace:', error); 35918 // Prevent recursion in error event handlers that throw #5497 35919 if (!this.triggeringException) { 35920 this.triggeringException = true; 35921 const fatal = event === Events.ERROR; 35922 this.trigger(Events.ERROR, { 35923 type: ErrorTypes.OTHER_ERROR, 35924 details: ErrorDetails.INTERNAL_EXCEPTION, 35925 fatal, 35926 event, 35927 error 35928 }); 35929 this.triggeringException = false; 35930 } 35931 } 35932 } 35933 return false; 35934 } 35935 listenerCount(event) { 35936 return this._emitter.listenerCount(event); 35937 } 35938 35939 /** 35940 * Dispose of the instance 35941 */ 35942 destroy() { 35943 this.logger.log('destroy'); 35944 this.trigger(Events.DESTROYING, undefined); 35945 this.detachMedia(); 35946 this.removeAllListeners(); 35947 this._autoLevelCapping = -1; 35948 this._url = null; 35949 this.networkControllers.forEach(component => component.destroy()); 35950 this.networkControllers.length = 0; 35951 this.coreComponents.forEach(component => component.destroy()); 35952 this.coreComponents.length = 0; 35953 // Remove any references that could be held in config options or callbacks 35954 const config = this.config; 35955 config.xhrSetup = config.fetchSetup = undefined; 35956 // @ts-ignore 35957 this.userConfig = null; 35958 } 35959 35960 /** 35961 * Attaches Hls.js to a media element 35962 */ 35963 attachMedia(data) { 35964 if (!data || 'media' in data && !data.media) { 35965 const error = new Error(`attachMedia failed: invalid argument (${data})`); 35966 this.trigger(Events.ERROR, { 35967 type: ErrorTypes.OTHER_ERROR, 35968 details: ErrorDetails.ATTACH_MEDIA_ERROR, 35969 fatal: true, 35970 error 35971 }); 35972 return; 35973 } 35974 this.logger.log(`attachMedia`); 35975 if (this._media) { 35976 this.logger.warn(`media must be detached before attaching`); 35977 this.detachMedia(); 35978 } 35979 const attachMediaSource = 'media' in data; 35980 const media = attachMediaSource ? data.media : data; 35981 const attachingData = attachMediaSource ? data : { 35982 media 35983 }; 35984 this._media = media; 35985 this.trigger(Events.MEDIA_ATTACHING, attachingData); 35986 } 35987 35988 /** 35989 * Detach Hls.js from the media 35990 */ 35991 detachMedia() { 35992 this.logger.log('detachMedia'); 35993 this.trigger(Events.MEDIA_DETACHING, {}); 35994 this._media = null; 35995 } 35996 35997 /** 35998 * Detach HTMLMediaElement, MediaSource, and SourceBuffers without reset, for attaching to another instance 35999 */ 36000 transferMedia() { 36001 this._media = null; 36002 const transferMedia = this.bufferController.transferMedia(); 36003 this.trigger(Events.MEDIA_DETACHING, { 36004 transferMedia 36005 }); 36006 return transferMedia; 36007 } 36008 36009 /** 36010 * Set the source URL. Can be relative or absolute. 36011 */ 36012 loadSource(url) { 36013 this.stopLoad(); 36014 const media = this.media; 36015 const loadedSource = this._url; 36016 const loadingSource = this._url = urlToolkitExports.buildAbsoluteURL(self.location.href, url, { 36017 alwaysNormalize: true 36018 }); 36019 this._autoLevelCapping = -1; 36020 this._maxHdcpLevel = null; 36021 this.logger.log(`loadSource:${loadingSource}`); 36022 if (media && loadedSource && (loadedSource !== loadingSource || this.bufferController.hasSourceTypes())) { 36023 // Remove and re-create MediaSource 36024 this.detachMedia(); 36025 this.attachMedia(media); 36026 } 36027 // when attaching to a source URL, trigger a playlist load 36028 this.trigger(Events.MANIFEST_LOADING, { 36029 url: url 36030 }); 36031 } 36032 36033 /** 36034 * Gets the currently loaded URL 36035 */ 36036 get url() { 36037 return this._url; 36038 } 36039 36040 /** 36041 * Whether or not enough has been buffered to seek to start position or use `media.currentTime` to determine next load position 36042 */ 36043 get hasEnoughToStart() { 36044 return this.streamController.hasEnoughToStart; 36045 } 36046 36047 /** 36048 * Get the startPosition set on startLoad(position) or on autostart with config.startPosition 36049 */ 36050 get startPosition() { 36051 return this.streamController.startPositionValue; 36052 } 36053 36054 /** 36055 * Start loading data from the stream source. 36056 * Depending on default config, client starts loading automatically when a source is set. 36057 * 36058 * @param startPosition - Set the start position to stream from. 36059 * Defaults to -1 (None: starts from earliest point) 36060 */ 36061 startLoad(startPosition = -1, skipSeekToStartPosition) { 36062 this.logger.log(`startLoad(${startPosition + (skipSeekToStartPosition ? ', <skip seek to start>' : '')})`); 36063 this.started = true; 36064 this.resumeBuffering(); 36065 for (let i = 0; i < this.networkControllers.length; i++) { 36066 this.networkControllers[i].startLoad(startPosition, skipSeekToStartPosition); 36067 if (!this.started || !this.networkControllers) { 36068 break; 36069 } 36070 } 36071 } 36072 36073 /** 36074 * Stop loading of any stream data. 36075 */ 36076 stopLoad() { 36077 this.logger.log('stopLoad'); 36078 this.started = false; 36079 for (let i = 0; i < this.networkControllers.length; i++) { 36080 this.networkControllers[i].stopLoad(); 36081 if (this.started || !this.networkControllers) { 36082 break; 36083 } 36084 } 36085 } 36086 36087 /** 36088 * Returns whether loading, toggled with `startLoad()` and `stopLoad()`, is active or not`. 36089 */ 36090 get loadingEnabled() { 36091 return this.started; 36092 } 36093 36094 /** 36095 * Returns state of fragment loading toggled by calling `pauseBuffering()` and `resumeBuffering()`. 36096 */ 36097 get bufferingEnabled() { 36098 return this.streamController.bufferingEnabled; 36099 } 36100 36101 /** 36102 * Resumes stream controller segment loading after `pauseBuffering` has been called. 36103 */ 36104 resumeBuffering() { 36105 if (!this.bufferingEnabled) { 36106 this.logger.log(`resume buffering`); 36107 this.networkControllers.forEach(controller => { 36108 if (controller.resumeBuffering) { 36109 controller.resumeBuffering(); 36110 } 36111 }); 36112 } 36113 } 36114 36115 /** 36116 * Prevents stream controller from loading new segments until `resumeBuffering` is called. 36117 * This allows for media buffering to be paused without interupting playlist loading. 36118 */ 36119 pauseBuffering() { 36120 if (this.bufferingEnabled) { 36121 this.logger.log(`pause buffering`); 36122 this.networkControllers.forEach(controller => { 36123 if (controller.pauseBuffering) { 36124 controller.pauseBuffering(); 36125 } 36126 }); 36127 } 36128 } 36129 get inFlightFragments() { 36130 const inFlightData = { 36131 [PlaylistLevelType.MAIN]: this.streamController.inFlightFrag 36132 }; 36133 if (this.audioStreamController) { 36134 inFlightData[PlaylistLevelType.AUDIO] = this.audioStreamController.inFlightFrag; 36135 } 36136 if (this.subtititleStreamController) { 36137 inFlightData[PlaylistLevelType.SUBTITLE] = this.subtititleStreamController.inFlightFrag; 36138 } 36139 return inFlightData; 36140 } 36141 36142 /** 36143 * Swap through possible audio codecs in the stream (for example to switch from stereo to 5.1) 36144 */ 36145 swapAudioCodec() { 36146 this.logger.log('swapAudioCodec'); 36147 this.streamController.swapAudioCodec(); 36148 } 36149 36150 /** 36151 * When the media-element fails, this allows to detach and then re-attach it 36152 * as one call (convenience method). 36153 * 36154 * Automatic recovery of media-errors by this process is configurable. 36155 */ 36156 recoverMediaError() { 36157 this.logger.log('recoverMediaError'); 36158 const media = this._media; 36159 const time = media == null ? void 0 : media.currentTime; 36160 this.detachMedia(); 36161 if (media) { 36162 this.attachMedia(media); 36163 if (time) { 36164 this.startLoad(time); 36165 } 36166 } 36167 } 36168 removeLevel(levelIndex) { 36169 this.levelController.removeLevel(levelIndex); 36170 } 36171 36172 /** 36173 * @returns a UUID for this player instance 36174 */ 36175 get sessionId() { 36176 let _sessionId = this._sessionId; 36177 if (!_sessionId) { 36178 _sessionId = this._sessionId = uuid(); 36179 } 36180 return _sessionId; 36181 } 36182 36183 /** 36184 * @returns an array of levels (variants) sorted by HDCP-LEVEL, RESOLUTION (height), FRAME-RATE, CODECS, VIDEO-RANGE, and BANDWIDTH 36185 */ 36186 get levels() { 36187 const levels = this.levelController.levels; 36188 return levels ? levels : []; 36189 } 36190 36191 /** 36192 * @returns LevelDetails of last loaded level (variant) or `null` prior to loading a media playlist. 36193 */ 36194 get latestLevelDetails() { 36195 return this.streamController.getLevelDetails() || null; 36196 } 36197 36198 /** 36199 * @returns Level object of selected level (variant) or `null` prior to selecting a level or once the level is removed. 36200 */ 36201 get loadLevelObj() { 36202 return this.levelController.loadLevelObj; 36203 } 36204 36205 /** 36206 * Index of quality level (variant) currently played 36207 */ 36208 get currentLevel() { 36209 return this.streamController.currentLevel; 36210 } 36211 36212 /** 36213 * Set quality level index immediately. This will flush the current buffer to replace the quality asap. That means playback will interrupt at least shortly to re-buffer and re-sync eventually. Set to -1 for automatic level selection. 36214 */ 36215 set currentLevel(newLevel) { 36216 this.logger.log(`set currentLevel:${newLevel}`); 36217 this.levelController.manualLevel = newLevel; 36218 this.streamController.immediateLevelSwitch(); 36219 } 36220 36221 /** 36222 * Index of next quality level loaded as scheduled by stream controller. 36223 */ 36224 get nextLevel() { 36225 return this.streamController.nextLevel; 36226 } 36227 36228 /** 36229 * Set quality level index for next loaded data. 36230 * This will switch the video quality asap, without interrupting playback. 36231 * May abort current loading of data, and flush parts of buffer (outside currently played fragment region). 36232 * @param newLevel - Pass -1 for automatic level selection 36233 */ 36234 set nextLevel(newLevel) { 36235 this.logger.log(`set nextLevel:${newLevel}`); 36236 this.levelController.manualLevel = newLevel; 36237 this.streamController.nextLevelSwitch(); 36238 } 36239 36240 /** 36241 * Return the quality level of the currently or last (of none is loaded currently) segment 36242 */ 36243 get loadLevel() { 36244 return this.levelController.level; 36245 } 36246 36247 /** 36248 * Set quality level index for next loaded data in a conservative way. 36249 * This will switch the quality without flushing, but interrupt current loading. 36250 * Thus the moment when the quality switch will appear in effect will only be after the already existing buffer. 36251 * @param newLevel - Pass -1 for automatic level selection 36252 */ 36253 set loadLevel(newLevel) { 36254 this.logger.log(`set loadLevel:${newLevel}`); 36255 this.levelController.manualLevel = newLevel; 36256 } 36257 36258 /** 36259 * get next quality level loaded 36260 */ 36261 get nextLoadLevel() { 36262 return this.levelController.nextLoadLevel; 36263 } 36264 36265 /** 36266 * Set quality level of next loaded segment in a fully "non-destructive" way. 36267 * Same as `loadLevel` but will wait for next switch (until current loading is done). 36268 */ 36269 set nextLoadLevel(level) { 36270 this.levelController.nextLoadLevel = level; 36271 } 36272 36273 /** 36274 * Return "first level": like a default level, if not set, 36275 * falls back to index of first level referenced in manifest 36276 */ 36277 get firstLevel() { 36278 return Math.max(this.levelController.firstLevel, this.minAutoLevel); 36279 } 36280 36281 /** 36282 * Sets "first-level", see getter. 36283 */ 36284 set firstLevel(newLevel) { 36285 this.logger.log(`set firstLevel:${newLevel}`); 36286 this.levelController.firstLevel = newLevel; 36287 } 36288 36289 /** 36290 * Return the desired start level for the first fragment that will be loaded. 36291 * The default value of -1 indicates automatic start level selection. 36292 * Setting hls.nextAutoLevel without setting a startLevel will result in 36293 * the nextAutoLevel value being used for one fragment load. 36294 */ 36295 get startLevel() { 36296 const startLevel = this.levelController.startLevel; 36297 if (startLevel === -1 && this.abrController.forcedAutoLevel > -1) { 36298 return this.abrController.forcedAutoLevel; 36299 } 36300 return startLevel; 36301 } 36302 36303 /** 36304 * set start level (level of first fragment that will be played back) 36305 * if not overrided by user, first level appearing in manifest will be used as start level 36306 * if -1 : automatic start level selection, playback will start from level matching download bandwidth 36307 * (determined from download of first segment) 36308 */ 36309 set startLevel(newLevel) { 36310 this.logger.log(`set startLevel:${newLevel}`); 36311 // if not in automatic start level detection, ensure startLevel is greater than minAutoLevel 36312 if (newLevel !== -1) { 36313 newLevel = Math.max(newLevel, this.minAutoLevel); 36314 } 36315 this.levelController.startLevel = newLevel; 36316 } 36317 36318 /** 36319 * Whether level capping is enabled. 36320 * Default value is set via `config.capLevelToPlayerSize`. 36321 */ 36322 get capLevelToPlayerSize() { 36323 return this.config.capLevelToPlayerSize; 36324 } 36325 36326 /** 36327 * Enables or disables level capping. If disabled after previously enabled, `nextLevelSwitch` will be immediately called. 36328 */ 36329 set capLevelToPlayerSize(shouldStartCapping) { 36330 const newCapLevelToPlayerSize = !!shouldStartCapping; 36331 if (newCapLevelToPlayerSize !== this.config.capLevelToPlayerSize) { 36332 if (newCapLevelToPlayerSize) { 36333 this.capLevelController.startCapping(); // If capping occurs, nextLevelSwitch will happen based on size. 36334 } else { 36335 this.capLevelController.stopCapping(); 36336 this.autoLevelCapping = -1; 36337 this.streamController.nextLevelSwitch(); // Now we're uncapped, get the next level asap. 36338 } 36339 this.config.capLevelToPlayerSize = newCapLevelToPlayerSize; 36340 } 36341 } 36342 36343 /** 36344 * Capping/max level value that should be used by automatic level selection algorithm (`ABRController`) 36345 */ 36346 get autoLevelCapping() { 36347 return this._autoLevelCapping; 36348 } 36349 36350 /** 36351 * Returns the current bandwidth estimate in bits per second, when available. Otherwise, `NaN` is returned. 36352 */ 36353 get bandwidthEstimate() { 36354 const { 36355 bwEstimator 36356 } = this.abrController; 36357 if (!bwEstimator) { 36358 return NaN; 36359 } 36360 return bwEstimator.getEstimate(); 36361 } 36362 set bandwidthEstimate(abrEwmaDefaultEstimate) { 36363 this.abrController.resetEstimator(abrEwmaDefaultEstimate); 36364 } 36365 get abrEwmaDefaultEstimate() { 36366 const { 36367 bwEstimator 36368 } = this.abrController; 36369 if (!bwEstimator) { 36370 return NaN; 36371 } 36372 return bwEstimator.defaultEstimate; 36373 } 36374 36375 /** 36376 * get time to first byte estimate 36377 * @type {number} 36378 */ 36379 get ttfbEstimate() { 36380 const { 36381 bwEstimator 36382 } = this.abrController; 36383 if (!bwEstimator) { 36384 return NaN; 36385 } 36386 return bwEstimator.getEstimateTTFB(); 36387 } 36388 36389 /** 36390 * Capping/max level value that should be used by automatic level selection algorithm (`ABRController`) 36391 */ 36392 set autoLevelCapping(newLevel) { 36393 if (this._autoLevelCapping !== newLevel) { 36394 this.logger.log(`set autoLevelCapping:${newLevel}`); 36395 this._autoLevelCapping = newLevel; 36396 this.levelController.checkMaxAutoUpdated(); 36397 } 36398 } 36399 get maxHdcpLevel() { 36400 return this._maxHdcpLevel; 36401 } 36402 set maxHdcpLevel(value) { 36403 if (isHdcpLevel(value) && this._maxHdcpLevel !== value) { 36404 this._maxHdcpLevel = value; 36405 this.levelController.checkMaxAutoUpdated(); 36406 } 36407 } 36408 36409 /** 36410 * True when automatic level selection enabled 36411 */ 36412 get autoLevelEnabled() { 36413 return this.levelController.manualLevel === -1; 36414 } 36415 36416 /** 36417 * Level set manually (if any) 36418 */ 36419 get manualLevel() { 36420 return this.levelController.manualLevel; 36421 } 36422 36423 /** 36424 * min level selectable in auto mode according to config.minAutoBitrate 36425 */ 36426 get minAutoLevel() { 36427 const { 36428 levels, 36429 config: { 36430 minAutoBitrate 36431 } 36432 } = this; 36433 if (!levels) return 0; 36434 const len = levels.length; 36435 for (let i = 0; i < len; i++) { 36436 if (levels[i].maxBitrate >= minAutoBitrate) { 36437 return i; 36438 } 36439 } 36440 return 0; 36441 } 36442 36443 /** 36444 * max level selectable in auto mode according to autoLevelCapping 36445 */ 36446 get maxAutoLevel() { 36447 const { 36448 levels, 36449 autoLevelCapping, 36450 maxHdcpLevel 36451 } = this; 36452 let maxAutoLevel; 36453 if (autoLevelCapping === -1 && levels != null && levels.length) { 36454 maxAutoLevel = levels.length - 1; 36455 } else { 36456 maxAutoLevel = autoLevelCapping; 36457 } 36458 if (maxHdcpLevel) { 36459 for (let i = maxAutoLevel; i--;) { 36460 const hdcpLevel = levels[i].attrs['HDCP-LEVEL']; 36461 if (hdcpLevel && hdcpLevel <= maxHdcpLevel) { 36462 return i; 36463 } 36464 } 36465 } 36466 return maxAutoLevel; 36467 } 36468 get firstAutoLevel() { 36469 return this.abrController.firstAutoLevel; 36470 } 36471 36472 /** 36473 * next automatically selected quality level 36474 */ 36475 get nextAutoLevel() { 36476 return this.abrController.nextAutoLevel; 36477 } 36478 36479 /** 36480 * this setter is used to force next auto level. 36481 * this is useful to force a switch down in auto mode: 36482 * in case of load error on level N, hls.js can set nextAutoLevel to N-1 for example) 36483 * forced value is valid for one fragment. upon successful frag loading at forced level, 36484 * this value will be resetted to -1 by ABR controller. 36485 */ 36486 set nextAutoLevel(nextLevel) { 36487 this.abrController.nextAutoLevel = nextLevel; 36488 } 36489 36490 /** 36491 * get the datetime value relative to media.currentTime for the active level Program Date Time if present 36492 */ 36493 get playingDate() { 36494 return this.streamController.currentProgramDateTime; 36495 } 36496 get mainForwardBufferInfo() { 36497 return this.streamController.getMainFwdBufferInfo(); 36498 } 36499 get maxBufferLength() { 36500 return this.streamController.maxBufferLength; 36501 } 36502 36503 /** 36504 * Find and select the best matching audio track, making a level switch when a Group change is necessary. 36505 * Updates `hls.config.audioPreference`. Returns the selected track, or null when no matching track is found. 36506 */ 36507 setAudioOption(audioOption) { 36508 var _this$audioTrackContr; 36509 return ((_this$audioTrackContr = this.audioTrackController) == null ? void 0 : _this$audioTrackContr.setAudioOption(audioOption)) || null; 36510 } 36511 /** 36512 * Find and select the best matching subtitle track, making a level switch when a Group change is necessary. 36513 * Updates `hls.config.subtitlePreference`. Returns the selected track, or null when no matching track is found. 36514 */ 36515 setSubtitleOption(subtitleOption) { 36516 var _this$subtitleTrackCo; 36517 return ((_this$subtitleTrackCo = this.subtitleTrackController) == null ? void 0 : _this$subtitleTrackCo.setSubtitleOption(subtitleOption)) || null; 36518 } 36519 36520 /** 36521 * Get the complete list of audio tracks across all media groups 36522 */ 36523 get allAudioTracks() { 36524 const audioTrackController = this.audioTrackController; 36525 return audioTrackController ? audioTrackController.allAudioTracks : []; 36526 } 36527 36528 /** 36529 * Get the list of selectable audio tracks 36530 */ 36531 get audioTracks() { 36532 const audioTrackController = this.audioTrackController; 36533 return audioTrackController ? audioTrackController.audioTracks : []; 36534 } 36535 36536 /** 36537 * index of the selected audio track (index in audio track lists) 36538 */ 36539 get audioTrack() { 36540 const audioTrackController = this.audioTrackController; 36541 return audioTrackController ? audioTrackController.audioTrack : -1; 36542 } 36543 36544 /** 36545 * selects an audio track, based on its index in audio track lists 36546 */ 36547 set audioTrack(audioTrackId) { 36548 const audioTrackController = this.audioTrackController; 36549 if (audioTrackController) { 36550 audioTrackController.audioTrack = audioTrackId; 36551 } 36552 } 36553 36554 /** 36555 * get the complete list of subtitle tracks across all media groups 36556 */ 36557 get allSubtitleTracks() { 36558 const subtitleTrackController = this.subtitleTrackController; 36559 return subtitleTrackController ? subtitleTrackController.allSubtitleTracks : []; 36560 } 36561 36562 /** 36563 * get alternate subtitle tracks list from playlist 36564 */ 36565 get subtitleTracks() { 36566 const subtitleTrackController = this.subtitleTrackController; 36567 return subtitleTrackController ? subtitleTrackController.subtitleTracks : []; 36568 } 36569 36570 /** 36571 * index of the selected subtitle track (index in subtitle track lists) 36572 */ 36573 get subtitleTrack() { 36574 const subtitleTrackController = this.subtitleTrackController; 36575 return subtitleTrackController ? subtitleTrackController.subtitleTrack : -1; 36576 } 36577 get media() { 36578 return this._media; 36579 } 36580 36581 /** 36582 * select an subtitle track, based on its index in subtitle track lists 36583 */ 36584 set subtitleTrack(subtitleTrackId) { 36585 const subtitleTrackController = this.subtitleTrackController; 36586 if (subtitleTrackController) { 36587 subtitleTrackController.subtitleTrack = subtitleTrackId; 36588 } 36589 } 36590 36591 /** 36592 * Whether subtitle display is enabled or not 36593 */ 36594 get subtitleDisplay() { 36595 const subtitleTrackController = this.subtitleTrackController; 36596 return subtitleTrackController ? subtitleTrackController.subtitleDisplay : false; 36597 } 36598 36599 /** 36600 * Enable/disable subtitle display rendering 36601 */ 36602 set subtitleDisplay(value) { 36603 const subtitleTrackController = this.subtitleTrackController; 36604 if (subtitleTrackController) { 36605 subtitleTrackController.subtitleDisplay = value; 36606 } 36607 } 36608 36609 /** 36610 * get mode for Low-Latency HLS loading 36611 */ 36612 get lowLatencyMode() { 36613 return this.config.lowLatencyMode; 36614 } 36615 36616 /** 36617 * Enable/disable Low-Latency HLS part playlist and segment loading, and start live streams at playlist PART-HOLD-BACK rather than HOLD-BACK. 36618 */ 36619 set lowLatencyMode(mode) { 36620 this.config.lowLatencyMode = mode; 36621 } 36622 36623 /** 36624 * Position (in seconds) of live sync point (ie edge of live position minus safety delay defined by ```hls.config.liveSyncDuration```) 36625 * @returns null prior to loading live Playlist 36626 */ 36627 get liveSyncPosition() { 36628 return this.latencyController.liveSyncPosition; 36629 } 36630 36631 /** 36632 * Estimated position (in seconds) of live edge (ie edge of live playlist plus time sync playlist advanced) 36633 * @returns 0 before first playlist is loaded 36634 */ 36635 get latency() { 36636 return this.latencyController.latency; 36637 } 36638 36639 /** 36640 * maximum distance from the edge before the player seeks forward to ```hls.liveSyncPosition``` 36641 * configured using ```liveMaxLatencyDurationCount``` (multiple of target duration) or ```liveMaxLatencyDuration``` 36642 * @returns 0 before first playlist is loaded 36643 */ 36644 get maxLatency() { 36645 return this.latencyController.maxLatency; 36646 } 36647 36648 /** 36649 * target distance from the edge as calculated by the latency controller 36650 */ 36651 get targetLatency() { 36652 return this.latencyController.targetLatency; 36653 } 36654 set targetLatency(latency) { 36655 this.latencyController.targetLatency = latency; 36656 } 36657 36658 /** 36659 * the rate at which the edge of the current live playlist is advancing or 1 if there is none 36660 */ 36661 get drift() { 36662 return this.latencyController.drift; 36663 } 36664 36665 /** 36666 * set to true when startLoad is called before MANIFEST_PARSED event 36667 */ 36668 get forceStartLoad() { 36669 return this.streamController.forceStartLoad; 36670 } 36671 36672 /** 36673 * ContentSteering pathways getter 36674 */ 36675 get pathways() { 36676 return this.levelController.pathways; 36677 } 36678 36679 /** 36680 * ContentSteering pathwayPriority getter/setter 36681 */ 36682 get pathwayPriority() { 36683 return this.levelController.pathwayPriority; 36684 } 36685 set pathwayPriority(pathwayPriority) { 36686 this.levelController.pathwayPriority = pathwayPriority; 36687 } 36688 36689 /** 36690 * returns true when all SourceBuffers are buffered to the end 36691 */ 36692 get bufferedToEnd() { 36693 var _this$bufferControlle; 36694 return !!((_this$bufferControlle = this.bufferController) != null && _this$bufferControlle.bufferedToEnd); 36695 } 36696 36697 /** 36698 * returns Interstitials Program Manager 36699 */ 36700 get interstitialsManager() { 36701 var _this$interstitialsCo; 36702 return ((_this$interstitialsCo = this.interstitialsController) == null ? void 0 : _this$interstitialsCo.interstitialsManager) || null; 36703 } 36704 36705 /** 36706 * returns mediaCapabilities.decodingInfo for a variant/rendition 36707 */ 36708 getMediaDecodingInfo(level, audioTracks = this.allAudioTracks) { 36709 const audioTracksByGroup = getAudioTracksByGroup(audioTracks); 36710 return getMediaDecodingInfoPromise(level, audioTracksByGroup, navigator.mediaCapabilities); 36711 } 36712} 36713Hls.defaultConfig = void 0; 36714
36715 36716//# sourceMappingURL=hls.mjs.map 36717 36718 36719/***/ }) 36720 36721}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.