rx-player
Version:
Canal+ HTML5 Video Player
247 lines (224 loc) • 9.12 kB
text/typescript
/**
* 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 log from "../../../log";
import type { ISegment } from "../../../manifest";
import getMonotonicTimeStamp from "../../../utils/monotonic_timestamp";
import clearTimelineFromPosition from "../utils/clear_timeline_from_position";
import { getIndexSegmentEnd } from "../utils/index_helpers";
import updateSegmentTimeline from "../utils/update_segment_timeline";
import addSegmentInfos from "./utils/add_segment_infos";
/**
* Smooth contents provide the index of segments under a "StreamIndex", the
* smooth equivalent of an AdaptationSet.
*
* This means that multiple "QualityLevel" (smooth's Representation) are going
* to rely on the exact same list of segments. This also means that all
* mutations on that timeline (whether it is to evict old segments or to add
* new ones) should presumably happen for all of them at the same time.
*
* The `SharedSmoothSegmentTimeline` is an abstraction over that index of
* segments whose goal is to explicitely provide a data structure that can be
* shared to every `RepresentationIndex` linked to Representations being part
* of the same smooth Adaptation, thus allowing to mutualize any side-effect
* done to it automatically.
*
* @class SharedSmoothSegmentTimeline
*/
export default class SharedSmoothSegmentTimeline {
/**
* Array describing the list of segments available.
* Note that this same list might be shared by multiple `RepresentationIndex`
* (as they link to the same timeline in the Manifest).
*/
public timeline: IIndexSegment[];
/** Timescale allowing to convert time values in `timeline` into seconds. */
public timescale: number;
/**
* Defines the earliest time - expressed in the RxPlayer's
* monotonically-raising timestamp unit - when the timeline was known to be
* valid (that is, when all segments declared in it are available).
*
* This is either:
* - the Manifest downloading time, if known
* - else, the time of creation of this SharedSmoothSegmentTimeline, as a
* guess
*/
public validityTime: number;
private _timeShiftBufferDepth: number | undefined;
private _initialScaledLastPosition: number | undefined;
constructor(args: ISharedSmoothSegmentTimelineArguments) {
const { timeline, timescale, timeShiftBufferDepth, manifestReceivedTime } = args;
this.timeline = timeline;
this.timescale = timescale;
const estimatedReceivedTime = manifestReceivedTime ?? getMonotonicTimeStamp();
this.validityTime = estimatedReceivedTime;
this._timeShiftBufferDepth = timeShiftBufferDepth;
if (timeline.length !== 0) {
const lastItem = timeline[timeline.length - 1];
const scaledEnd = getIndexSegmentEnd(lastItem, null);
this._initialScaledLastPosition = scaledEnd;
}
}
/**
* Clean-up timeline to remove segment information which should not be
* available due to the timeshift window
*/
public refresh(): void {
// clean segments before time shift buffer depth
if (this._initialScaledLastPosition === undefined) {
return;
}
const timeShiftBufferDepth = this._timeShiftBufferDepth;
const timeSinceLastRealUpdate = (getMonotonicTimeStamp() - this.validityTime) / 1000;
const lastPositionEstimate =
timeSinceLastRealUpdate + this._initialScaledLastPosition / this.timescale;
if (timeShiftBufferDepth !== undefined) {
const minimumPosition =
(lastPositionEstimate - timeShiftBufferDepth) * this.timescale;
clearTimelineFromPosition(this.timeline, minimumPosition);
}
}
/**
* Replace this SharedSmoothSegmentTimeline by a newly downloaded one.
* Check if the old timeline had more information about new segments and re-add
* them if that's the case.
* @param {Object} newSmoothTimeline
*/
public replace(newSmoothTimeline: SharedSmoothSegmentTimeline): void {
const oldTimeline = this.timeline;
const newTimeline = newSmoothTimeline.timeline;
const oldTimescale = this.timescale;
const newTimescale = newSmoothTimeline.timescale;
this._initialScaledLastPosition = newSmoothTimeline._initialScaledLastPosition;
this.validityTime = newSmoothTimeline.validityTime;
if (
oldTimeline.length === 0 ||
newTimeline.length === 0 ||
oldTimescale !== newTimescale
) {
return; // don't take risk, if something is off, take the new one
}
const lastOldTimelineElement = oldTimeline[oldTimeline.length - 1];
const lastNewTimelineElement = newTimeline[newTimeline.length - 1];
const newEnd = getIndexSegmentEnd(lastNewTimelineElement, null);
if (getIndexSegmentEnd(lastOldTimelineElement, null) <= newEnd) {
return;
}
for (let i = 0; i < oldTimeline.length; i++) {
const oldTimelineRange = oldTimeline[i];
const oldEnd = getIndexSegmentEnd(oldTimelineRange, null);
if (oldEnd === newEnd) {
// just add the supplementary segments
this.timeline = this.timeline.concat(oldTimeline.slice(i + 1));
return;
}
if (oldEnd > newEnd) {
// adjust repeatCount + add supplementary segments
if (oldTimelineRange.duration !== lastNewTimelineElement.duration) {
return;
}
const rangeDuration = newEnd - oldTimelineRange.start;
if (rangeDuration === 0) {
log.warn(
"smooth",
"a discontinuity detected in the previous manifest" + " has been resolved.",
);
this.timeline = this.timeline.concat(oldTimeline.slice(i));
return;
}
if (rangeDuration < 0 || rangeDuration % oldTimelineRange.duration !== 0) {
return;
}
const repeatWithOld = rangeDuration / oldTimelineRange.duration - 1;
const relativeRepeat = oldTimelineRange.repeatCount - repeatWithOld;
if (relativeRepeat < 0) {
return;
}
lastNewTimelineElement.repeatCount += relativeRepeat;
const supplementarySegments = oldTimeline.slice(i + 1);
this.timeline = this.timeline.concat(supplementarySegments);
return;
}
}
}
/**
* Update the current SharedSmoothSegmentTimeline with a new, partial, version.
* This method might be use to only add information about new segments.
* @param {Object} newSmoothTimeline
*/
public update(newSmoothTimeline: SharedSmoothSegmentTimeline): void {
updateSegmentTimeline(this.timeline, newSmoothTimeline.timeline);
this._initialScaledLastPosition = newSmoothTimeline._initialScaledLastPosition;
this.validityTime = newSmoothTimeline.validityTime;
}
/**
* Add segments to a `SharedSmoothSegmentTimeline` that were predicted to come
* after `currentSegment`.
* @param {Array.<Object>} nextSegments - The segment information parsed.
* @param {Object} currentSegment - Information on the segment which contained
* that new segment information.
*/
public addPredictedSegments(
nextSegments: Array<{ duration: number; time: number; timescale: number }>,
currentSegment: ISegment,
): void {
if (currentSegment.privateInfos?.smoothMediaSegment === undefined) {
log.warn("smooth", "should only encounter SmoothRepresentationIndex");
return;
}
this.refresh();
for (const nextSeg of nextSegments) {
addSegmentInfos(
this.timeline,
this.timescale,
nextSeg,
currentSegment.privateInfos.smoothMediaSegment,
);
}
}
}
export interface ISharedSmoothSegmentTimelineArguments {
timeline: IIndexSegment[];
timescale: number;
timeShiftBufferDepth: number | undefined;
manifestReceivedTime: number | undefined;
}
/**
* Object describing information about one segment or several consecutive
* segments.
*/
export interface IIndexSegment {
/** Time (timescaled) at which the segment starts. */
start: number;
/** Duration (timescaled) of the segment. */
duration: number;
/**
* Amount of consecutive segments with that duration.
*
* For example let's consider the following IIndexSegment:
* ```
* { start: 10, duration: 2, repeatCount: 2 }
* ```
* Here, because `repeatCount` is set to `2`, this object actually defines 3
* segments:
* 1. one starting at `10` and ending at `12` (10 + 2)
* 2. another one starting at `12` (the previous one's end) and ending at
* `14` (12 + 2)
* 3. another one starting at `14` (the previous one's end) and ending at
* `16` (14 +2)
*/
repeatCount: number;
}