UNPKG

rx-player

Version:
1,368 lines (1,315 loc) 49.1 kB
/** * Copyright 2015 CANAL+ Group * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import config from "../../../config"; import log from "../../../log"; import type { IAdaptation, ISegment, IPeriod, IRepresentation } from "../../../manifest"; import { areSameContent, getLoggableSegmentId } from "../../../manifest"; import isNullOrUndefined from "../../../utils/is_null_or_undefined"; import getMonotonicTimeStamp from "../../../utils/monotonic_timestamp"; import type { IRange } from "../../../utils/ranges"; import type { IBufferedHistoryEntry } from "./buffered_history"; import BufferedHistory from "./buffered_history"; import type { IChunkContext } from "./types"; /** Categorization of a given chunk in the `SegmentInventory`. */ export const enum ChunkStatus { /** * This chunk is only a part of a partially-pushed segment for now, meaning * that it is only a sub-part of a requested segment that was not yet * fully-loaded and pushed. * * Once and if the corresponding segment is fully-pushed, its `ChunkStatus` * switches to `FullyLoaded`. */ PartiallyPushed = 0, /** This chunk corresponds to a fully-loaded segment. */ FullyLoaded = 1, /** * This chunk's push operation failed, in this scenario there is no certitude * about the presence of that chunk in the buffer: it may not be present, * partially-present, or fully-present depending on why that push operation * failed, which is generally only known by the lower-level code. */ Failed = 2, } /** Information stored on a single chunk by the SegmentInventory. */ export interface IBufferedChunk { /** * Value of the monotonically-increasing timestamp used by the RxPlayer at * the time the segment was succesfully pushed to the buffer. * * We add this value here as we may want to wait for some time before * synchronizing the buffer to ensure the browser has properly considered the * full segment in its `buffered` value. */ insertionTs: number; /** * Complete size of the pushed chunk, in bytes. * Note that this does not always reflect the memory imprint of the segment in * memory: * * 1. It's the size of the original container file. A browser receiving that * segment might then transform it under another form that may be more or * less voluminous. * * 2. It's the size of the full chunk. In some scenarios only a sub-part of * that chunk is actually considered (examples: when using append * windows, when another chunk overlap that one etc.). */ chunkSize: number | undefined; /** * Last inferred end in the media buffer this chunk ends at, in seconds. * * Depending on if contiguous chunks were around it during the first * synchronization for that chunk this value could be more or less precize. */ bufferedEnd: number | undefined; /** * Last inferred start in the media buffer this chunk starts at, in seconds. * * Depending on if contiguous chunks were around it during the first * synchronization for that chunk this value could be more or less precize. */ bufferedStart: number | undefined; /** Supposed end, in seconds, the chunk is expected to end at. */ end: number; /** * If `true` the `end` property is an estimate the `SegmentInventory` has * a high confidence in. * In that situation, `bufferedEnd` can easily be compared to it to check if * that segment has been partially, or fully, garbage collected. * * If `false`, it is just a guess based on segment information. */ precizeEnd: boolean; /** * If `true` the `start` property is an estimate the `SegmentInventory` has * a high confidence in. * In that situation, `bufferedStart` can easily be compared to it to check if * that segment has been partially, or fully, garbage collected. * * If `false`, it is just a guess based on segment information. */ precizeStart: boolean; /** Information on what that chunk actually contains. */ infos: IChunkContext; /** * Status of this chunk. * @see ChunkStatus */ status: ChunkStatus; /** * If `true`, the segment as a whole is divided into multiple parts in the * buffer, with other segment(s) between them. * If `false`, it is contiguous. * * Splitted segments are a rare occurence that is more complicated to handle * than contiguous ones. */ splitted: boolean; /** * Supposed start, in seconds, the chunk is expected to start at. * * If the current `chunk` is part of a "partially pushed" segment (see * `partiallyPushed`), the definition of this property is flexible in the way * that it can correspond either to the start of the chunk or to the start of * the whole segment the chunk is linked to. * As such, this property should not be relied on until the segment has been * fully-pushed. */ start: number; // Supposed start the segment should start from, in seconds } /** information to provide when "inserting" a new chunk into the SegmentInventory. */ export interface IInsertedChunkInfos { /** The Adaptation that chunk is linked to */ adaptation: IAdaptation; /** The Period that chunk is linked to */ period: IPeriod; /** The Representation that chunk is linked to. */ representation: IRepresentation; /** The Segment that chunk is linked to. */ segment: ISegment; /** Estimated size of the full pushed chunk, in bytes. */ chunkSize: number | undefined; /** * Start time, in seconds, this chunk most probably begins from after being * pushed. * In doubt, you can set it at the start of the whole segment (after * considering the possible offsets and append windows). */ start: number; /** * End time, in seconds, this chunk most probably ends at after being * pushed. * * In doubt, you can set it at the end of the whole segment (after * considering the possible offsets and append windows). */ end: number; } /** * Keep track of every chunk downloaded and currently in the linked media * buffer. * * The main point of this class is to know which chunks are already pushed to * the corresponding media buffer, at which bitrate, and which have been garbage-collected * since by the browser (and thus may need to be re-loaded). * @class SegmentInventory */ export default class SegmentInventory { /** * Keeps track of all the segments which should be currently in the browser's * memory. * This array contains objects, each being related to a single downloaded * chunk or segment which is at least partially added in the media buffer. */ private _inventory: IBufferedChunk[]; private _bufferedHistory: BufferedHistory; constructor() { const { BUFFERED_HISTORY_RETENTION_TIME, BUFFERED_HISTORY_MAXIMUM_ENTRIES } = config.getCurrent(); this._inventory = []; this._bufferedHistory = new BufferedHistory( BUFFERED_HISTORY_RETENTION_TIME, BUFFERED_HISTORY_MAXIMUM_ENTRIES, ); } /** * Reset the whole inventory. */ public reset(): void { this._inventory.length = 0; } /** * Infer each segment's `bufferedStart` and `bufferedEnd` properties from the * ranges given. * * The ranges object given should come from the media buffer linked to that * SegmentInventory. * * /!\ A SegmentInventory should not be associated to multiple media buffers * at a time, so each `synchronizeBuffered` call should be given ranges coming * from the same buffer. * @param {Array.<Object>} ranges */ public synchronizeBuffered(ranges: IRange[]): void { const inventory = this._inventory; let inventoryIndex = 0; // Current index considered. let thisSegment = inventory[0]; // Current segmentInfos considered const { MINIMUM_SEGMENT_SIZE } = config.getCurrent(); /** Type of buffer considered, used for logs */ const bufferType: string | undefined = thisSegment?.infos.adaptation.type; if (log.hasLevel("DEBUG")) { const prettyPrintedRanges = ranges.map((r) => `${r.start}-${r.end}`).join(","); log.debug( "SI", `synchronizing ${bufferType ?? "unknown"} buffered ranges:`, prettyPrintedRanges, ); } const rangesLength = ranges.length; for (let i = 0; i < rangesLength; i++) { if (thisSegment === undefined) { // we arrived at the end of our inventory return; } // take the i'nth contiguous buffered range const rangeStart = ranges[i].start; const rangeEnd = ranges[i].end; if (rangeEnd - rangeStart < MINIMUM_SEGMENT_SIZE) { log.warn("SI", "skipped range when synchronizing because it was too small", { t: bufferType, rangeStart, rangeEnd, }); continue; } const indexBefore = inventoryIndex; // keep track of that number // Find the first segment either within this range or completely past // it: // skip until first segment with at least `MINIMUM_SEGMENT_SIZE` past the // start of that range. while ( thisSegment !== undefined && (thisSegment.bufferedEnd ?? thisSegment.end) - rangeStart < MINIMUM_SEGMENT_SIZE ) { thisSegment = inventory[++inventoryIndex]; } // Contains infos about the last garbage-collected segment before // `thisSegment`. let lastDeletedSegmentInfos: { end: number; precizeEnd: boolean } | null = null; // remove garbage-collected segments // (Those not in that range nor in the previous one) const numberOfSegmentToDelete = inventoryIndex - indexBefore; if (numberOfSegmentToDelete > 0) { const lastDeletedSegment = inventory[indexBefore + numberOfSegmentToDelete - 1]; // last garbage-collected segment lastDeletedSegmentInfos = { end: lastDeletedSegment.bufferedEnd ?? lastDeletedSegment.end, precizeEnd: lastDeletedSegment.precizeEnd, }; log.debug("SI", `${numberOfSegmentToDelete} segments GCed.`, { t: bufferType, }); const removed = inventory.splice(indexBefore, numberOfSegmentToDelete); for (const seg of removed) { if ( seg.bufferedStart === undefined && seg.bufferedEnd === undefined && seg.status !== ChunkStatus.Failed ) { this._bufferedHistory.addBufferedSegment(seg.infos, null); } } inventoryIndex = indexBefore; } if (thisSegment === undefined) { return; } // If the current segment is actually completely outside that range (it // is contained in one of the next one), skip that part. if ( rangeEnd - (thisSegment.bufferedStart ?? thisSegment.start) >= MINIMUM_SEGMENT_SIZE ) { guessBufferedStartFromRangeStart( thisSegment, rangeStart, lastDeletedSegmentInfos, bufferType, ); if (inventoryIndex === inventory.length - 1) { // This is the last segment in the inventory. // We can directly update the end as the end of the current range. guessBufferedEndFromRangeEnd(thisSegment, rangeEnd, bufferType); return; } thisSegment = inventory[++inventoryIndex]; // Make contiguous until first segment outside that range let thisSegmentStart = thisSegment.bufferedStart ?? thisSegment.start; let thisSegmentEnd = thisSegment.bufferedEnd ?? thisSegment.end; const nextRangeStart = i < rangesLength - 1 ? ranges[i + 1].start : undefined; while (thisSegment !== undefined) { if (rangeEnd < thisSegmentStart) { // `thisSegment` is part of the next range break; } if ( rangeEnd - thisSegmentStart < MINIMUM_SEGMENT_SIZE && thisSegmentEnd - rangeEnd >= MINIMUM_SEGMENT_SIZE ) { // Ambiguous, but `thisSegment` seems more to come after the current // range than during it. break; } if ( nextRangeStart !== undefined && rangeEnd - thisSegmentStart < thisSegmentEnd - nextRangeStart ) { // Ambiguous, but `thisSegment` has more chance to be part of the // next range than the current one break; } const prevSegment = inventory[inventoryIndex - 1]; // those segments are contiguous, we have no way to infer their real // end if (prevSegment.bufferedEnd === undefined) { if (thisSegment.precizeStart) { prevSegment.bufferedEnd = thisSegment.start; } else if (prevSegment.infos.segment.complete) { prevSegment.bufferedEnd = prevSegment.end; } else { // We cannot truly trust the anounced end here as the segment was // potentially not complete at its time of announce. // Just assume the next's segment announced start is right - as // `start` is in that scenario more "trustable" than `end`. prevSegment.bufferedEnd = thisSegment.start; } log.debug("SI", "calculating buffered end of contiguous segment", { t: bufferType, prevSegmentBufferedEnd: prevSegment.bufferedEnd, pse: prevSegment.end, }); } thisSegment.bufferedStart = prevSegment.bufferedEnd; thisSegment = inventory[++inventoryIndex]; if (thisSegment !== undefined) { thisSegmentStart = thisSegment.bufferedStart ?? thisSegment.start; thisSegmentEnd = thisSegment.bufferedEnd ?? thisSegment.end; } } } // update the bufferedEnd of the last segment in that range const lastSegmentInRange = inventory[inventoryIndex - 1]; if (lastSegmentInRange !== undefined) { guessBufferedEndFromRangeEnd(lastSegmentInRange, rangeEnd, bufferType); } } // if we still have segments left, they are not affiliated to any range. // They might have been garbage collected, delete them from here. if (!isNullOrUndefined(thisSegment)) { const { SEGMENT_SYNCHRONIZATION_DELAY } = config.getCurrent(); const now = getMonotonicTimeStamp(); for (let i = inventoryIndex; i < inventory.length; i++) { const segmentInfo = inventory[i]; if (now - segmentInfo.insertionTs >= SEGMENT_SYNCHRONIZATION_DELAY) { log.debug( "SI", "A segment at the end has been completely GCed", getLoggableSegmentId(segmentInfo.infos), ); if ( segmentInfo.bufferedStart === undefined && segmentInfo.bufferedEnd === undefined && segmentInfo.status !== ChunkStatus.Failed ) { this._bufferedHistory.addBufferedSegment(segmentInfo.infos, null); } inventory.splice(i, 1); i--; } } } if (bufferType !== undefined && log.hasLevel("DEBUG")) { // TODO: Do not log if it did not change? log.debug( "SI", `current ${bufferType} inventory timeline:\n` + prettyPrintInventory(this._inventory), ); } } /** * Add a new chunk in the inventory. * * Chunks are decodable sub-parts of a whole segment. Once all chunks in a * segment have been inserted, you should call the `completeSegment` method. * @param {Object} chunkInformation * @param {boolean} succeed - If `true` the insertion operation finished with * success, if `false` an error arised while doing it. * @param {number} insertionTs - The monotonically-increasing timestamp at the * time the segment has been confirmed to be inserted by the buffer. */ public insertChunk( { period, adaptation, representation, segment, chunkSize, start, end, }: IInsertedChunkInfos, succeed: boolean, insertionTs: number, ): void { if (segment.isInit) { return; } const bufferType = adaptation.type; if (start >= end) { log.warn("SI", "Invalid chunked inserted: starts before it ends", { t: bufferType, start, end, }); return; } const inventory = this._inventory; const newSegment = { status: succeed ? ChunkStatus.PartiallyPushed : ChunkStatus.Failed, insertionTs, chunkSize, splitted: false, start, end, precizeStart: false, precizeEnd: false, bufferedStart: undefined, bufferedEnd: undefined, infos: { segment, period, adaptation, representation }, }; // begin by the end as in most use cases this will be faster for (let i = inventory.length - 1; i >= 0; i--) { const segmentI = inventory[i]; if (segmentI.start <= start) { if (segmentI.end <= start) { // our segment is after, push it after this one // // Case 1: // prevSegment : |------| // newSegment : |======| // ===> : |------|======| // // Case 2: // prevSegment : |------| // newSegment : |======| // ===> : |------| |======| log.debug("SI", "Pushing segment strictly after previous one.", { t: bufferType, pse: segmentI.end, ss: start, }); this._inventory.splice(i + 1, 0, newSegment); i += 2; // Go to segment immediately after newSegment while (i < inventory.length && inventory[i].start < newSegment.end) { if (inventory[i].end > newSegment.end) { // The next segment ends after newSegment. // Mutate the next segment. // // Case 1: // prevSegment : |------| // newSegment : |======| // nextSegment : |----| // ===> : |------|======|-| log.debug("SI", "Segment pushed updates the start of the next one", { t: bufferType, pss: inventory[i].start, ss: start, se: end, }); inventory[i].start = newSegment.end; inventory[i].bufferedStart = undefined; inventory[i].precizeStart = inventory[i].precizeStart && newSegment.precizeEnd; return; } // The next segment was completely contained in newSegment. // Remove it. // // Case 1: // prevSegment : |------| // newSegment : |======| // nextSegment : |---| // ===> : |------|======| // // Case 2: // prevSegment : |------| // newSegment : |======| // nextSegment : |----| // ===> : |------|======| log.debug("SI", "Segment pushed removes the next one", { t: bufferType, ss: start, se: end, pss: inventory[i].start, pse: inventory[i].end, }); inventory.splice(i, 1); } return; } else { if (segmentI.start === start) { if (segmentI.end <= end) { // In those cases, replace // // Case 1: // prevSegment : |-------| // newSegment : |=======| // ===> : |=======| // // Case 2: // prevSegment : |-------| // newSegment : |==========| // ===> : |==========| log.debug("SI", "Segment pushed replace another one", { t: bufferType, ss: start, se: end, pss: segmentI.start, pse: segmentI.end, }); this._inventory.splice(i, 1, newSegment); i += 1; // Go to segment immediately after newSegment while (i < inventory.length && inventory[i].start < newSegment.end) { if (inventory[i].end > newSegment.end) { // The next segment ends after newSegment. // Mutate the next segment. // // Case 1: // newSegment : |======| // nextSegment : |----| // ===> : |======|--| log.debug("SI", "Segment pushed updates the start of the next one", { t: bufferType, ss: start, se: end, pss: inventory[i].start, pse: inventory[i].end, }); inventory[i].start = newSegment.end; inventory[i].bufferedStart = undefined; inventory[i].precizeStart = inventory[i].precizeStart && newSegment.precizeEnd; return; } // The next segment was completely contained in newSegment. // Remove it. // // Case 1: // newSegment : |======| // nextSegment : |---| // ===> : |======| // // Case 2: // newSegment : |======| // nextSegment : |----| // ===> : |======| log.debug("SI", "Segment pushed removes the next one", { t: bufferType, ss: start, se: end, pss: inventory[i].start, pse: inventory[i].end, }); inventory.splice(i, 1); } return; } else { // The previous segment starts at the same time and finishes // after the new segment. // Update the start of the previous segment and put the new // segment before. // // Case 1: // prevSegment : |------------| // newSegment : |==========| // ===> : |==========|-| log.debug("SI", "Segment pushed ends before another with the same start", { t: bufferType, ss: start, se: end, pse: segmentI.end, }); inventory.splice(i, 0, newSegment); segmentI.start = newSegment.end; segmentI.bufferedStart = undefined; segmentI.precizeStart = segmentI.precizeStart && newSegment.precizeEnd; return; } } else { if (segmentI.end <= newSegment.end) { // our segment has a "complex" relation with this one, // update the old one end and add this one after it. // // Case 1: // prevSegment : |-------| // newSegment : |======| // ===> : |--|======| // // Case 2: // prevSegment : |-------| // newSegment : |====| // ===> : |--|====| log.debug("SI", "Segment pushed updates end of previous one", { t: bufferType, ss: start, se: end, pss: segmentI.start, pse: segmentI.end, }); this._inventory.splice(i + 1, 0, newSegment); segmentI.end = newSegment.start; segmentI.bufferedEnd = undefined; segmentI.precizeEnd = segmentI.precizeEnd && newSegment.precizeStart; i += 2; // Go to segment immediately after newSegment while (i < inventory.length && inventory[i].start < newSegment.end) { if (inventory[i].end > newSegment.end) { // The next segment ends after newSegment. // Mutate the next segment. // // Case 1: // newSegment : |======| // nextSegment : |----| // ===> : |======|--| log.debug("SI", "Segment pushed updates the start of the next one", { t: bufferType, ss: start, se: end, pss: inventory[i].start, }); inventory[i].start = newSegment.end; inventory[i].bufferedStart = undefined; inventory[i].precizeStart = inventory[i].precizeStart && newSegment.precizeEnd; return; } // The next segment was completely contained in newSegment. // Remove it. // // Case 1: // newSegment : |======| // nextSegment : |---| // ===> : |======| // // Case 2: // newSegment : |======| // nextSegment : |----| // ===> : |======| log.debug("SI", "Segment pushed removes the next one", { t: bufferType, ss: start, se: end, pss: inventory[i].start, pse: inventory[i].end, }); inventory.splice(i, 1); } return; } else { // The previous segment completely recovers the new segment. // Split the previous segment into two segments, before and after // the new segment. // // Case 1: // prevSegment : |---------| // newSegment : |====| // ===> : |--|====|-| log.warn("SI", "Segment pushed is contained in a previous one", { t: bufferType, ss: start, se: end, pss: segmentI.start, pse: segmentI.end, }); const nextSegment = { status: segmentI.status, insertionTs: segmentI.insertionTs, /** * Note: this sadly means we're doing as if * that chunk is present two times. * Thankfully, this scenario should be * fairly rare. */ chunkSize: segmentI.chunkSize, splitted: true, start: newSegment.end, end: segmentI.end, precizeStart: segmentI.precizeStart && segmentI.precizeEnd && newSegment.precizeEnd, precizeEnd: segmentI.precizeEnd, bufferedStart: undefined, bufferedEnd: segmentI.end, infos: segmentI.infos, }; segmentI.end = newSegment.start; segmentI.splitted = true; segmentI.bufferedEnd = undefined; segmentI.precizeEnd = segmentI.precizeEnd && newSegment.precizeStart; inventory.splice(i + 1, 0, newSegment); inventory.splice(i + 2, 0, nextSegment); return; } } } } } // if we got here, we are at the first segment // check bounds of the previous first segment const firstSegment = this._inventory[0]; if (firstSegment === undefined) { // we do not have any segment yet log.debug("SI", "first segment pushed", { t: bufferType, ss: start, se: end }); this._inventory.push(newSegment); return; } if (firstSegment.start >= end) { // our segment is before, put it before // // Case 1: // firstSegment : |----| // newSegment : |====| // ===> : |====|----| // // Case 2: // firstSegment : |----| // newSegment : |====| // ===> : |====| |----| log.debug("SI", "Segment pushed comes before all previous ones", { t: bufferType, ss: start, se: end, pss: firstSegment.start, }); this._inventory.splice(0, 0, newSegment); } else if (firstSegment.end <= end) { // Our segment is bigger, replace the first // // Case 1: // firstSegment : |---| // newSegment : |=======| // ===> : |=======| // // Case 2: // firstSegment : |-----| // newSegment : |=======| // ===> : |=======| log.debug( "SI", "Segment pushed starts before and completely " + "recovers the previous first one", { t: bufferType, ss: start, se: end, pss: firstSegment.start, pse: firstSegment.end, }, ); this._inventory.splice(0, 1, newSegment); while (inventory.length > 1 && inventory[1].start < newSegment.end) { if (inventory[1].end > newSegment.end) { // The next segment ends after newSegment. // Mutate the next segment. // // Case 1: // newSegment : |======| // nextSegment : |----| // ===> : |======|--| log.debug("SI", "Segment pushed updates the start of the next one", { t: bufferType, ss: start, se: end, pss: inventory[1].start, pse: inventory[1].end, }); inventory[1].start = newSegment.end; inventory[1].bufferedStart = undefined; inventory[1].precizeStart = newSegment.precizeEnd; return; } // The next segment was completely contained in newSegment. // Remove it. // // Case 1: // newSegment : |======| // nextSegment : |---| // ===> : |======| // // Case 2: // newSegment : |======| // nextSegment : |----| // ===> : |======| log.debug("SI", "Segment pushed removes the next one", { t: bufferType, ss: start, se: end, pss: inventory[1].start, pse: inventory[1].end, }); inventory.splice(1, 1); } return; } else { // our segment has a "complex" relation with the first one, // update the old one start and add this one before it. // // Case 1: // firstSegment : |------| // newSegment : |======| // ===> : |======|--| log.debug("SI", "Segment pushed start of the next one", bufferType, { ss: start, se: end, pss: firstSegment.start, pse: firstSegment.end, }); firstSegment.start = end; firstSegment.bufferedStart = undefined; firstSegment.precizeStart = newSegment.precizeEnd; this._inventory.splice(0, 0, newSegment); return; } } /** * Indicate that inserted chunks can now be considered as a fully-loaded * segment. * Take in argument the same content than what was given to `insertChunk` for * the corresponding chunks. * @param {Object} content */ public completeSegment(content: { period: IPeriod; adaptation: IAdaptation; representation: IRepresentation; segment: ISegment; }): void { if (content.segment.isInit) { return; } const inventory = this._inventory; const resSegments: IBufferedChunk[] = []; for (let i = 0; i < inventory.length; i++) { if (areSameContent(inventory[i].infos, content)) { let splitted = false; if (resSegments.length > 0) { splitted = true; if (resSegments.length === 1) { log.warn( "SI", "Completed Segment is splitted.", getLoggableSegmentId(content), ); resSegments[0].splitted = true; } } const firstI = i; let segmentSize = inventory[i].chunkSize; i += 1; while (i < inventory.length && areSameContent(inventory[i].infos, content)) { const chunkSize = inventory[i].chunkSize; if (segmentSize !== undefined && chunkSize !== undefined) { segmentSize += chunkSize; } i++; } const lastI = i - 1; const length = lastI - firstI; const lastEnd = inventory[lastI].end; const lastBufferedEnd = inventory[lastI].bufferedEnd; if (length > 0) { this._inventory.splice(firstI + 1, length); i -= length; } if (this._inventory[firstI].status === ChunkStatus.PartiallyPushed) { this._inventory[firstI].status = ChunkStatus.FullyLoaded; } this._inventory[firstI].chunkSize = segmentSize; this._inventory[firstI].end = lastEnd; this._inventory[firstI].bufferedEnd = lastBufferedEnd; this._inventory[firstI].splitted = splitted; resSegments.push(this._inventory[firstI]); } } if (resSegments.length === 0) { log.warn("SI", "Completed Segment not found", getLoggableSegmentId(content)); } else { for (const seg of resSegments) { if (seg.bufferedStart !== undefined && seg.bufferedEnd !== undefined) { if (seg.status !== ChunkStatus.Failed) { this._bufferedHistory.addBufferedSegment(seg.infos, { start: seg.bufferedStart, end: seg.bufferedEnd, }); } } else { // TODO FIXME There might be a false positive here when the // `SEGMENT_SYNCHRONIZATION_DELAY` config value is at play log.debug("SI", "buffered range not known after sync. Skipping history.", { ss: seg.start, se: seg.end, }); } } } } /** * Returns the whole inventory. * * To get a list synchronized with what a media buffer actually has buffered * you might want to call `synchronizeBuffered` before calling this method. * @returns {Array.<Object>} */ public getInventory(): IBufferedChunk[] { return this._inventory; } /** * Returns a recent history of registered operations performed and event * received linked to the segment given in argument. * * Not all operations and events are registered in the returned history. * Please check the return type for more information on what is available. * * Note that history is short-lived for memory usage and performance reasons. * You may not receive any information on operations that happened too long * ago. * @param {Object} context * @returns {Array.<Object>} */ public getHistoryFor(context: IChunkContext): IBufferedHistoryEntry[] { return this._bufferedHistory.getHistoryFor(context); } } /** * Returns `true` if the buffered start of the given chunk looks coherent enough * relatively to what is announced in the Manifest. * @param {Object} thisSegment * @returns {Boolean} */ function bufferedStartLooksCoherent(thisSegment: IBufferedChunk): boolean { if ( thisSegment.bufferedStart === undefined || thisSegment.status !== ChunkStatus.FullyLoaded ) { return false; } const { start, end } = thisSegment; const duration = end - start; const { MAX_MANIFEST_BUFFERED_START_END_DIFFERENCE, MAX_MANIFEST_BUFFERED_DURATION_DIFFERENCE, } = config.getCurrent(); return ( Math.abs(start - thisSegment.bufferedStart) <= MAX_MANIFEST_BUFFERED_START_END_DIFFERENCE && (thisSegment.bufferedEnd === undefined || (thisSegment.bufferedEnd > thisSegment.bufferedStart && Math.abs(thisSegment.bufferedEnd - thisSegment.bufferedStart - duration) <= Math.min(MAX_MANIFEST_BUFFERED_DURATION_DIFFERENCE, duration / 3))) ); } /** * Returns `true` if the buffered end of the given chunk looks coherent enough * relatively to what is announced in the Manifest. * @param {Object} thisSegment * @returns {Boolean} */ function bufferedEndLooksCoherent(thisSegment: IBufferedChunk): boolean { if ( thisSegment.bufferedEnd === undefined || !thisSegment.infos.segment.complete || thisSegment.status !== ChunkStatus.FullyLoaded ) { return false; } const { start, end } = thisSegment; const duration = end - start; const { MAX_MANIFEST_BUFFERED_START_END_DIFFERENCE, MAX_MANIFEST_BUFFERED_DURATION_DIFFERENCE, } = config.getCurrent(); return ( Math.abs(end - thisSegment.bufferedEnd) <= MAX_MANIFEST_BUFFERED_START_END_DIFFERENCE && thisSegment.bufferedStart !== undefined && thisSegment.bufferedEnd > thisSegment.bufferedStart && Math.abs(thisSegment.bufferedEnd - thisSegment.bufferedStart - duration) <= Math.min(MAX_MANIFEST_BUFFERED_DURATION_DIFFERENCE, duration / 3) ); } /** * Evaluate the given buffered Chunk's buffered start from its range's start, * considering that this chunk is the first one in it. * @param {Object} firstSegmentInRange * @param {number} rangeStart * @param {Object} lastDeletedSegmentInfos */ function guessBufferedStartFromRangeStart( firstSegmentInRange: IBufferedChunk, rangeStart: number, lastDeletedSegmentInfos: { end: number; precizeEnd: boolean } | null, bufferType: string, ): void { const { MAX_MANIFEST_BUFFERED_START_END_DIFFERENCE, MISSING_DATA_TRIGGER_SYNC_DELAY, SEGMENT_SYNCHRONIZATION_DELAY, } = config.getCurrent(); if (firstSegmentInRange.bufferedStart !== undefined) { if (firstSegmentInRange.bufferedStart < rangeStart) { log.debug("SI", "Segment partially GCed at the start", { t: bufferType, firstsbs: firstSegmentInRange.bufferedStart, rs: rangeStart, }); firstSegmentInRange.bufferedStart = rangeStart; } if ( !firstSegmentInRange.precizeStart && bufferedStartLooksCoherent(firstSegmentInRange) ) { firstSegmentInRange.start = firstSegmentInRange.bufferedStart; firstSegmentInRange.precizeStart = true; } } else if (firstSegmentInRange.precizeStart) { log.debug("SI", "buffered start is precize start", { t: bufferType, firstss: firstSegmentInRange.start, }); firstSegmentInRange.bufferedStart = firstSegmentInRange.start; } else if ( lastDeletedSegmentInfos !== null && lastDeletedSegmentInfos.end > rangeStart && (lastDeletedSegmentInfos.precizeEnd || firstSegmentInRange.start - lastDeletedSegmentInfos.end <= MAX_MANIFEST_BUFFERED_START_END_DIFFERENCE) ) { log.debug("SI", "buffered start is end of previous segment", { t: bufferType, rs: rangeStart, firstss: firstSegmentInRange.start, lastdelse: lastDeletedSegmentInfos.end, }); firstSegmentInRange.bufferedStart = lastDeletedSegmentInfos.end; if (bufferedStartLooksCoherent(firstSegmentInRange)) { firstSegmentInRange.start = lastDeletedSegmentInfos.end; firstSegmentInRange.precizeStart = true; } } else if ( firstSegmentInRange.start - rangeStart <= MAX_MANIFEST_BUFFERED_START_END_DIFFERENCE ) { const now = getMonotonicTimeStamp(); if ( firstSegmentInRange.start - rangeStart >= MISSING_DATA_TRIGGER_SYNC_DELAY && now - firstSegmentInRange.insertionTs < SEGMENT_SYNCHRONIZATION_DELAY ) { log.debug("SI", "Ignored bufferedStart synchronization", { t: bufferType, rs: rangeStart, firstss: firstSegmentInRange.start, delta: now - firstSegmentInRange.insertionTs, }); return; } log.debug("SI", "found true buffered start", { t: bufferType, rs: rangeStart, firstss: firstSegmentInRange.start, }); firstSegmentInRange.bufferedStart = rangeStart; if (bufferedStartLooksCoherent(firstSegmentInRange)) { firstSegmentInRange.start = rangeStart; firstSegmentInRange.precizeStart = true; } } else if (rangeStart < firstSegmentInRange.start) { log.debug("SI", "range start too far from expected start", { t: bufferType, rs: rangeStart, firstss: firstSegmentInRange.start, }); firstSegmentInRange.bufferedStart = firstSegmentInRange.start; } else { const now = getMonotonicTimeStamp(); if ( firstSegmentInRange.start - rangeStart >= MISSING_DATA_TRIGGER_SYNC_DELAY && now - firstSegmentInRange.insertionTs < SEGMENT_SYNCHRONIZATION_DELAY ) { log.debug("SI", "Ignored bufferedStart synchronization", { t: bufferType, rs: rangeStart, firstss: firstSegmentInRange.start, delta: now - firstSegmentInRange.insertionTs, }); return; } log.debug("SI", "Segment appears immediately garbage collected at the start", { t: bufferType, rs: rangeStart, firstss: firstSegmentInRange.start, }); firstSegmentInRange.bufferedStart = rangeStart; } } /** * Evaluate the given buffered Chunk's buffered end from its range's end, * considering that this chunk is the last one in it. * @param {Object} lastSegmentInRange * @param {number} rangeEnd * @param {string} bufferType */ function guessBufferedEndFromRangeEnd( lastSegmentInRange: IBufferedChunk, rangeEnd: number, bufferType?: string, ): void { const { MAX_MANIFEST_BUFFERED_START_END_DIFFERENCE, MISSING_DATA_TRIGGER_SYNC_DELAY, SEGMENT_SYNCHRONIZATION_DELAY, } = config.getCurrent(); if (lastSegmentInRange.bufferedEnd !== undefined) { if (lastSegmentInRange.bufferedEnd > rangeEnd) { log.debug("SI", "Segment partially GCed at the end", { t: bufferType, lastsbe: lastSegmentInRange.bufferedEnd, re: rangeEnd, }); lastSegmentInRange.bufferedEnd = rangeEnd; } if ( !lastSegmentInRange.precizeEnd && rangeEnd - lastSegmentInRange.end <= MAX_MANIFEST_BUFFERED_START_END_DIFFERENCE && bufferedEndLooksCoherent(lastSegmentInRange) ) { lastSegmentInRange.precizeEnd = true; lastSegmentInRange.end = rangeEnd; } } else if (lastSegmentInRange.precizeEnd) { log.debug("SI", "buffered end is precize end", { t: bufferType, lastse: lastSegmentInRange.end, }); lastSegmentInRange.bufferedEnd = lastSegmentInRange.end; } else if ( rangeEnd - lastSegmentInRange.end <= MAX_MANIFEST_BUFFERED_START_END_DIFFERENCE || !lastSegmentInRange.infos.segment.complete ) { const now = getMonotonicTimeStamp(); if ( rangeEnd - lastSegmentInRange.end >= MISSING_DATA_TRIGGER_SYNC_DELAY && now - lastSegmentInRange.insertionTs < SEGMENT_SYNCHRONIZATION_DELAY ) { log.debug("SI", "Ignored bufferedEnd synchronization", { t: bufferType, re: rangeEnd, lastse: lastSegmentInRange.end, delta: now - lastSegmentInRange.insertionTs, }); return; } log.debug("SI", "found true buffered end", { t: bufferType, re: rangeEnd, lastse: lastSegmentInRange.end, }); lastSegmentInRange.bufferedEnd = rangeEnd; if (bufferedEndLooksCoherent(lastSegmentInRange)) { lastSegmentInRange.end = rangeEnd; lastSegmentInRange.precizeEnd = true; } } else if (rangeEnd > lastSegmentInRange.end) { log.debug("SI", "range end too far from expected end", { t: bufferType, re: rangeEnd, lastse: lastSegmentInRange.end, }); lastSegmentInRange.bufferedEnd = lastSegmentInRange.end; } else { const now = getMonotonicTimeStamp(); if ( rangeEnd - lastSegmentInRange.end >= MISSING_DATA_TRIGGER_SYNC_DELAY && now - lastSegmentInRange.insertionTs < SEGMENT_SYNCHRONIZATION_DELAY ) { log.debug("SI", "Ignored bufferedEnd synchronization", { t: bufferType, re: rangeEnd, lastse: lastSegmentInRange.end, delta: now - lastSegmentInRange.insertionTs, }); return; } log.debug("SI", "Segment appears immediately garbage collected at the end", { t: bufferType, lastsbe: lastSegmentInRange.bufferedEnd, re: rangeEnd, }); lastSegmentInRange.bufferedEnd = rangeEnd; } } /** * Pretty print the inventory, to easily note which segments are where in the * current buffer. * * This is mostly useful when logging. * * @example * This function is called by giving it the inventory, such as: * ```js * prettyPrintInventory(inventory); * ``` * * Let's consider this possible return: * ``` * 0.00|A|9.00 ~ 9.00|B|45.08 ~ 282.08|B|318.08 * [A] P: gen-dash-period-0 || R: video/5(2362822) * [B] P: gen-dash-period-0 || R: video/6(2470094) * ``` * We have a first part, from 0 to 9 seconds, which contains segments for * the Representation with the id "video/5" and an associated bitrate of * 2362822 bits per seconds (in the Period with the id "gen-dash-period-0"). * * Then from 9.00 seconds to 45.08 seconds, we have segments from another * Representation from the same Period (with the id "video/6" and a bitrate * of 2470094 bits per seconds). * * At last we have a long time between 45.08 and 282.08 with no segment followed * by a segment from that same Representation between 282.08 seconds and 318.08 * seconds. * @param {Array.<Object>} inventory * @returns {string} */ function prettyPrintInventory(inventory: IBufferedChunk[]): string { const roundingError = 1 / 60; const encounteredReps: Partial< Record< string /* Period `id` */, Partial<Record<string /* Representation `id` */, string /* associated letter */>> > > = {}; const letters: Array<{ letter: string; periodId: string; representationId: string | number; bitrate?: number; }> = []; let lastChunk: IBufferedChunk | null = null; let lastLetter: string | null = null; function generateNewLetter(infos: IChunkContext): string { const currentLetter = String.fromCharCode(letters.length + 65); letters.push({ letter: currentLetter, periodId: infos.period.id, representationId: infos.representation.id, bitrate: infos.representation.bitrate, }); return currentLetter; } let str = ""; for (const chunk of inventory) { if (chunk.bufferedStart !== undefined && chunk.bufferedEnd !== undefined) { const periodId = chunk.infos.period.id; const representationId = chunk.infos.representation.id; const encounteredPeriod = encounteredReps[periodId]; let currentLetter: string; if (encounteredPeriod === undefined) { currentLetter = generateNewLetter(chunk.infos); encounteredReps[periodId] = { [representationId]: currentLetter }; } else { const previousLetter = encounteredPeriod[representationId]; if (previousLetter === undefined) { currentLetter = generateNewLetter(chunk.infos); encounteredPeriod[representationId] = currentLetter; } else { currentLetter = previousLetter; } } if (lastChunk === null) { str += `${chunk.bufferedStart.toFixed(2)}|${currentLetter}|`; } else if (lastLetter === currentLetter) { if ((lastChunk.bufferedEnd as number) + roundingError < chunk.bufferedStart) { str += `${(lastChunk.bufferedEnd as number).toFixed(2)} ~ ` + `${chunk.bufferedStart.toFixed(2)}|${currentLetter}|`; } } else { str += `${(lastChunk.bufferedEnd as number).toFixed(2)} ~ ` + `${chunk.bufferedStart.toFixed(2)}|${currentLetter}|`; } lastChunk = chunk; lastLetter = currentLetter; } } if (lastChunk !== null) { str += String(lastChunk.end.toFixed(2)); } letters.forEach((letterInfo) => { str += `\n[${letterInfo.letter}] ` + `P: ${letterInfo.periodId} || R: ${letterInfo.representationId}` + `(${letterInfo.bitrate ?? "unknown bitrate"})`; }); return str; }