Skip to content

Commit

Permalink
Add getLivePosition API and better handle availabilityTimeOffset for …
Browse files Browse the repository at this point in the history
…DASH
  • Loading branch information
peaBerberian committed Oct 13, 2023
1 parent 75cf810 commit d3f0772
Show file tree
Hide file tree
Showing 32 changed files with 947 additions and 376 deletions.
8 changes: 5 additions & 3 deletions demo/full/scripts/controllers/ControlBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function ControlBar({
const isStopped = useModuleState(player, "isStopped");
const liveGap = useModuleState(player, "liveGap");
const lowLatencyMode = useModuleState(player, "lowLatencyMode");
const livePosition = useModuleState(player, "livePosition");
const maximumPosition = useModuleState(player, "maximumPosition");
const playbackRate = useModuleState(player, "playbackRate");

Expand Down Expand Up @@ -79,15 +80,16 @@ function ControlBar({
const isAtLiveEdge = isLive && isCloseToLive && !isCatchingUp;

const onLiveDotClick = React.useCallback(() => {
if (maximumPosition == null) {
const livePos = livePosition ?? maximumPosition;
if (livePos == null) {
/* eslint-disable-next-line no-console */
console.error("Cannot go back to live: live position not found");
return;
}
if (!isAtLiveEdge) {
player.actions.seek(maximumPosition - (lowLatencyMode ? 4 : 10));
player.actions.seek(livePos - (lowLatencyMode ? 4 : 10));
}
}, [isAtLiveEdge, player, maximumPosition, lowLatencyMode]);
}, [isAtLiveEdge, player, livePosition, maximumPosition, lowLatencyMode]);

return (
<div className="controls-bar-container">
Expand Down
3 changes: 2 additions & 1 deletion demo/full/scripts/controllers/ProgressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function ProgressBar({
const isContentLoaded = useModuleState(player, "isContentLoaded");
const isLive = useModuleState(player, "isLive");
const minimumPosition = useModuleState(player, "minimumPosition");
const livePosition = useModuleState(player, "livePosition");
const maximumPosition = useModuleState(player, "maximumPosition");

const [timeIndicatorVisible, setTimeIndicatorVisible] = React.useState(false);
Expand Down Expand Up @@ -189,7 +190,7 @@ function ProgressBar({
onMouseMove={onMouseMove}
position={currentTime}
minimumPosition={minimumPosition}
maximumPosition={maximumPosition}
maximumPosition={livePosition ?? maximumPosition}
bufferGap={bufferGap}
/>
}
Expand Down
10 changes: 5 additions & 5 deletions demo/full/scripts/modules/player/catchUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,19 +69,19 @@ export default class CatchUpModeController {
this._state.updateBulk({ isCatchingUp: false, playbackRate: 1 });
} else {
const checkCatchUp = () => {
const maximumPosition = this._rxPlayer.getMaximumPosition();
if (maximumPosition === null) {
const livePos = this._rxPlayer.getLivePosition() ??
this._rxPlayer.getMaximumPosition();
if (livePos === null) {
this._rxPlayer.setPlaybackRate(1);
this._state.updateBulk({ isCatchingUp: false, playbackRate: 1 });
return;
}
const position = this._rxPlayer.getPosition();
const liveGap = maximumPosition - position;
const liveGap = livePos - position;
if (liveGap >= CATCH_UP_SEEKING_STEP) {
// If we're too far from the live to just change the playback rate,
// seek directly close to live
this._rxPlayer
.seekTo(maximumPosition - LIVE_GAP_GOAL_WHEN_CATCHING_UP);
this._rxPlayer.seekTo(livePos - LIVE_GAP_GOAL_WHEN_CATCHING_UP);
this._rxPlayer.setPlaybackRate(1);
this._state.updateBulk({ isCatchingUp: false, playbackRate: 1 });
return;
Expand Down
11 changes: 8 additions & 3 deletions demo/full/scripts/modules/player/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,24 @@ function linkPlayerEventsToState(
const position = player.getPosition();
const duration = player.getVideoDuration();
const videoTrack = player.getVideoTrack();
const livePosition = player.getLivePosition();
const maximumPosition = player.getMaximumPosition();
let bufferGap = player.getVideoBufferGap();
bufferGap = !isFinite(bufferGap) || isNaN(bufferGap) ?
0 :
bufferGap;

const livePos = livePosition ?? maximumPosition;
state.updateBulk({
currentTime: player.getPosition(),
wallClockDiff: player.getWallClockTime() - position,
bufferGap,
duration: Number.isNaN(duration) ? undefined : duration,
livePosition,
minimumPosition: player.getMinimumPosition(),
maximumPosition: player.getMaximumPosition(),
liveGap: typeof maximumPosition === "number" ?
maximumPosition - player.getPosition() :
maximumPosition,
liveGap: typeof livePos === "number" ?
livePos - player.getPosition() :
undefined,
playbackRate: player.getPlaybackRate(),
videoTrackHasTrickMode: videoTrack !== null &&
Expand Down Expand Up @@ -210,6 +214,7 @@ function linkPlayerEventsToState(
stateUpdates.duration = undefined;
stateUpdates.minimumPosition = undefined;
stateUpdates.maximumPosition = undefined;
stateUpdates.livePosition = undefined;
break;
}

Expand Down
2 changes: 2 additions & 0 deletions demo/full/scripts/modules/player/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export interface IPlayerModuleState {
liveGap: number | undefined;
loadedVideo: ILoadVideoOptions | null;
lowLatencyMode: boolean;
livePosition: null | undefined | number;
maximumPosition: null | undefined | number;
minimumPosition: null | undefined | number;
playbackRate: number;
Expand Down Expand Up @@ -185,6 +186,7 @@ const PlayerModule = declareModule(
liveGap: undefined,
loadedVideo: null,
lowLatencyMode: false,
livePosition: undefined,
maximumPosition: undefined,
minimumPosition: undefined,
playbackRate: 1,
Expand Down
23 changes: 23 additions & 0 deletions src/core/api/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2372,6 +2372,29 @@ class Player extends EventEmitter<IPublicAPIEvent> {
return null;
}

/**
* Returns the current position for live contents.
*
* Returns `null` if no content is loaded or if the current loaded content is
* not considered as a live content.
* Returns `undefined` if that live position is currently unknown.
* @returns {number}
*/
getLivePosition() : number | undefined |null {
if (this._priv_contentInfos === null) {
return null;
}

const { isDirectFile, manifest } = this._priv_contentInfos;
if (isDirectFile) {
return undefined;
}
if (manifest?.isLive !== true) {
return null;
}
return manifest.getLivePosition();
}

/**
* Get maximum seek-able position.
* @returns {number}
Expand Down
12 changes: 6 additions & 6 deletions src/core/init/media_source_content_initializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -694,9 +694,10 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
contentTimeBoundariesObserver.addEventListener("periodChange", (period) => {
this.trigger("activePeriodChanged", { period });
});
contentTimeBoundariesObserver.addEventListener("durationUpdate", (newDuration) => {
mediaSourceDurationUpdater.updateDuration(newDuration.duration, newDuration.isEnd);
});
contentTimeBoundariesObserver.addEventListener(
"endingPositionChange",
(x) => mediaSourceDurationUpdater.updateDuration(x.endingPosition, x.isEnd)
);
contentTimeBoundariesObserver.addEventListener("endOfStream", () => {
if (endOfStreamCanceller === null) {
endOfStreamCanceller = new TaskCanceller();
Expand All @@ -712,9 +713,8 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
endOfStreamCanceller = null;
}
});
const currentDuration = contentTimeBoundariesObserver.getCurrentDuration();
mediaSourceDurationUpdater.updateDuration(currentDuration.duration,
currentDuration.isEnd);
const endInfo = contentTimeBoundariesObserver.getCurrentEndingTime();
mediaSourceDurationUpdater.updateDuration(endInfo.endingPosition, endInfo.isEnd);
return contentTimeBoundariesObserver;
}

Expand Down
45 changes: 23 additions & 22 deletions src/core/init/utils/content_time_boundaries_observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ import { IStreamOrchestratorPlaybackObservation } from "../../stream";
/**
* Observes what's being played and take care of media events relating to time
* boundaries:
* - Emits a `durationUpdate` when the duration of the current content is
* known and every time it changes.
* - Emits a `endingPositionChange` when the known maximum playable position
* of the current content is known and every time it changes.
* - Emits `endOfStream` API once segments have been pushed until the end and
* `resumeStream` if downloads starts back.
* - Emits a `periodChange` event when the currently-playing Period seemed to
Expand Down Expand Up @@ -111,7 +111,7 @@ export default class ContentTimeBoundariesObserver
}, { includeLastObservation: true, clearSignal: cancelSignal });

manifest.addEventListener("manifestUpdate", () => {
this.trigger("durationUpdate", this._getManifestDuration());
this.trigger("endingPositionChange", this._getManifestEndTime());
if (cancelSignal.isCancelled()) {
return;
}
Expand All @@ -120,11 +120,12 @@ export default class ContentTimeBoundariesObserver
}

/**
* Returns an estimate of the current duration of the content.
* Returns an estimate of the current last position which may be played in
* the content at the moment.
* @returns {Object}
*/
public getCurrentDuration() : IDurationItem {
return this._getManifestDuration();
public getCurrentEndingTime() : IEndingPositionInformation {
return this._getManifestEndTime();
}

/**
Expand Down Expand Up @@ -157,12 +158,13 @@ export default class ContentTimeBoundariesObserver
.updateLastVideoAdaptation(adaptation);
}
const endingPosition = this._maximumPositionCalculator.getEndingPosition();
const newDuration = endingPosition !== undefined ?
const newEndingPosition = endingPosition !== undefined ?
{ isEnd: true,
duration: endingPosition } :
endingPosition } :
{ isEnd: false,
duration: this._maximumPositionCalculator.getMaximumAvailablePosition() };
this.trigger("durationUpdate", newDuration);
endingPosition: this._maximumPositionCalculator
.getMaximumAvailablePosition() };
this.trigger("endingPositionChange", newEndingPosition);
}
}
}
Expand Down Expand Up @@ -311,13 +313,13 @@ export default class ContentTimeBoundariesObserver
}
}

private _getManifestDuration() : IDurationItem {
private _getManifestEndTime() : IEndingPositionInformation {
const endingPosition = this._maximumPositionCalculator.getEndingPosition();
return endingPosition !== undefined ?
{ isEnd: true,
duration: endingPosition } :
endingPosition } :
{ isEnd: false,
duration: this._maximumPositionCalculator.getMaximumAvailablePosition() };
endingPosition: this._maximumPositionCalculator.getMaximumAvailablePosition() };
}

private _lazilyCreateActiveStreamInfo(bufferType : IBufferType) : IActiveStreamsInfo {
Expand Down Expand Up @@ -348,24 +350,24 @@ export default class ContentTimeBoundariesObserver
}
}

export interface IDurationItem {
export interface IEndingPositionInformation {
/**
* The new maximum known position (note that this is the ending position
* currently known of the current content, it might be superior to the last
* position at which segments are available and it might also evolve over
* time), in seconds.
*/
duration : number;
endingPosition : number;
/**
* If `true`, the communicated `duration` is the actual end of the content.
* If `true`, the communicated `endingPosition` is the actual end of the content.
* It may still be updated due to a track change or to add precision, but it
* is still a (rough) estimate of the maximum position that content should
* have.
*
* If `false`, this is the currently known maximum position associated to
* the content, but the content is still evolving (typically, new media
* segments are still being generated) and as such it can still have a
* longer duration in the future.
* longer `endingPosition` in the future.
*/
isEnd : boolean;
}
Expand All @@ -380,10 +382,10 @@ export interface IContentTimeBoundariesObserverEvent {
/** Triggered when a new `Period` is currently playing. */
periodChange : Period;
/**
* Triggered when the duration of the currently-playing content became known
* or changed.
* Triggered when the ending position of the currently-playing content became
* known or changed.
*/
durationUpdate : IDurationItem;
endingPositionChange : IEndingPositionInformation;
/**
* Triggered when the last possible chronological segment for all types of
* buffers has either been pushed or is being pushed to the corresponding
Expand Down Expand Up @@ -460,8 +462,7 @@ class MaximumPositionCalculator {
*/
public getMaximumAvailablePosition() : number {
if (this._manifest.isDynamic) {
return this._manifest.getLivePosition() ??
this._manifest.getMaximumSafePosition();
return this._manifest.getMaximumSafePosition();
}
if (this._lastVideoAdaptation === undefined ||
this._lastAudioAdaptation === undefined)
Expand Down
6 changes: 3 additions & 3 deletions src/core/stream/representation/utils/get_buffer_status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export default function getBufferStatus(
* needed segments for this Representation until the end of the Period.
*/
const hasFinishedLoading = representation.index.isInitialized() &&
representation.index.isFinished() &&
!representation.index.isStillAwaitingFutureSegments() &&
neededRange.hasReachedPeriodEnd &&
prioritizedNeededSegments.length === 0 &&
segmentsOnHold.length === 0;
Expand Down Expand Up @@ -221,7 +221,7 @@ function getRangeOfNeededSegments(
SegmentBuffersStore.isNative(content.adaptation.type) &&
initialWantedTime >= lastIndexPosition &&
representationIndex.isInitialized() &&
representationIndex.isFinished() &&
!representationIndex.isStillAwaitingFutureSegments() &&
isPeriodTheCurrentAndLastOne(manifest, period, initialWantedTime))
{
wantedStartPosition = lastIndexPosition - 1;
Expand All @@ -233,7 +233,7 @@ function getRangeOfNeededSegments(

let hasReachedPeriodEnd;
if (!representation.index.isInitialized() ||
!representation.index.isFinished() ||
representation.index.isStillAwaitingFutureSegments() ||
period.end === undefined)
{
hasReachedPeriodEnd = false;
Expand Down
4 changes: 2 additions & 2 deletions src/manifest/representation_index/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ export default class StaticRepresentationIndex implements IRepresentationIndex {
/**
* @returns {Boolean}
*/
isFinished() : true {
return true;
isStillAwaitingFutureSegments() : false {
return false;
}

/**
Expand Down
8 changes: 4 additions & 4 deletions src/manifest/representation_index/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ export interface IRepresentationIndex {

/**
* Returns the ending time, in seconds, of the Representation once it is
* "finished" (@see isFinished).
* "finished" (@see isStillAwaitingFutureSegments).
* Should thus be equivalent to `getLastAvailablePosition` once finished.
*
* Returns `null` if nothing is in the index
Expand Down Expand Up @@ -384,13 +384,13 @@ export interface IRepresentationIndex {
checkDiscontinuity(time : number) : number | null;

/**
* Returns `true` if the last segments in this index have already been
* Returns `false` if the last segments in this index have already been
* generated so that we can freely go to the next period.
* Returns `false` if the index is still waiting on future segments to be
* Returns `true` if the index is still waiting on future segments to be
* generated.
* @returns {boolean}
*/
isFinished() : boolean;
isStillAwaitingFutureSegments() : boolean;

/**
* Returns `true` if this index has all the data it needs to give the list
Expand Down
Loading

0 comments on commit d3f0772

Please sign in to comment.