diff --git a/.gitignore b/.gitignore
index 651702e49b..00185f3ecf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,4 +36,6 @@
/src/parsers/manifest/dash/wasm-parser/target
+# IDE files
/.idea
+/.vscode
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 40acf1b351..8df6b0d9a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,28 @@
# Changelog
+## Unreleased
+
+### Features
+
+ - Add `getLivePosition` RxPlayer method [#1300]
+ - Add `startAt.fromLivePosition` `loadVideo` option [#1300]
+ - Add the possibility to set a new `keySystems` option on the `reload` API [#1308]
+
+### Bug Fixes
+
+ - Fix subtitles "blinking" in some specific conditions, especially with some DASH low-latency contents [#1314]
+ - DASH: Fix Period overlap resolution logic for when the first Period is removed [#1311]
+ - Fix export of the `LOCAL_MANIFEST` experimental feature [#1305]
+
+### Other improvements
+
+ - DASH: rely on SCTE214 `supplementalCodecs` instead of `codecs` if it's supported to better support backward compatible Dolby Vision contents [#1307]
+ - DASH: Provide better support of the `availabilityTimeOffset` attribute [#1300]
+ - DEBUG_ELEMENT: Add unsupported and undecipherable bitrates to the debug element [#1321]
+ - DEBUG_ELEMENT: update buffer graph maximum size so it becomes more readable for lengthy contents [#1316]
+ - DEBUG_ELEMENT: always synchronize inventory of segments before rendering it [#1317]
+
+
## v3.32.1 (2023-10-19)
### Features
diff --git a/FILES.md b/FILES.md
index b383a6dc2b..4529509f3e 100644
--- a/FILES.md
+++ b/FILES.md
@@ -20,7 +20,7 @@ At the time of writing, there are two distinct demos:
## `dist/`: Builds
-The `demo/` directory stores the player builds of the last version released.
+The `dist/` directory stores the player builds of the last version released.
Contains the minified (``rx-player.min.js``) and the non-minified files
(``rx-player.js``). Both are automatically generated with scripts at every new
diff --git a/README.md b/README.md
index 3fd88bd9ff..527da2e76a 100644
--- a/README.md
+++ b/README.md
@@ -179,21 +179,11 @@ Demo pages for our previous versions are also available
-## Your questions ##############################################################
-
-You can ask directly your questions about the project on [our
-gitter](https://gitter.im/canalplus/rx-player).
-We will try our best to answer them as quickly as possible.
-
-
-
## Contribute ##################################################################
Details on how to contribute is written in the [CONTRIBUTING.md
file](./CONTRIBUTING.md) at the root of this repository.
-If you need more information, you can contact us via our [gitter
-room](https://gitter.im/canalplus/rx-player).
### Dependencies ###############################################################
@@ -238,7 +228,7 @@ Canal+ Group is a media company with many advanced needs when it comes to media
playback: it provides both live and VoD stream with multiple encryption
requirements, supports a very large panel of devices and has many other
specificities (like adult content restrictions, ad-insertion, Peer-To-Peer
-integration...).
+integration, low-latency live streaming...).
When the time came to switch from a plugin-based web player approach to an HTML5
one back in 2015, no media player had the key features we wanted, and including
@@ -249,7 +239,7 @@ The R&D department of Canal+ Group thus started to work on a new featureful
media-player: the RxPlayer. To both help and profit from the community, it also
decided to share it to everyone under a permissive open-source licence.
-Now, more than 6 years later, the RxPlayer continues to evolve at the same fast
+Now, more than 8 years later, the RxPlayer continues to evolve at the same fast
pace to include a lot of features and improvements you may not find in other
media players.
You can look at our
@@ -296,24 +286,3 @@ them. Amongst those:
risks always low.
\* In "directfile" mode, on compatible browsers
-
-
-## Target support ##############################################################
-
-Here is a basic list of supported platforms:
-
-| | Chrome | IE [1] | Edge | Firefox | Safari | Opera |
-|-------------|:-------:|:-------:|:------:|:---------:|:--------:|:-------:|
-| Windows | >= 30 | >= 11 | >= 12 | >= 42 | >= 8 | >= 25 |
-| OSX | >= 30 | - | - | >= 42 | >= 8 | >= 25 |
-| Linux | >= 37 | - | - | >= 42 | - | >= 25 |
-| Android [2] | >= 30 | - | - | >= 42 | - | >= 15 |
-| iOS | No | - | - | No | No | No |
-
-[1] Only on Windows >= 8.
-
-[2] Android version >= 4.2
-
-And more. A good way to know if the browser should be supported by our player is
-to go on the page https://www.youtube.com/html5 and check for "Media Source
-Extensions" support.
diff --git a/demo/full/scripts/contents.ts b/demo/full/scripts/contents.ts
index 42ec62dff7..27f01bd121 100644
--- a/demo/full/scripts/contents.ts
+++ b/demo/full/scripts/contents.ts
@@ -114,7 +114,7 @@ const DEFAULT_CONTENTS: IDefaultContent[] = [
},
{
"name": "Multi Video Tracks",
- "url": "https://utils.ssl.cdn.cra.cz/dash/1/manifest.mpd",
+ "url": "https://storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd",
"transport": "dash",
"live": false,
},
diff --git a/demo/full/scripts/controllers/ControlBar.tsx b/demo/full/scripts/controllers/ControlBar.tsx
index 02e002e6db..081f0542e1 100644
--- a/demo/full/scripts/controllers/ControlBar.tsx
+++ b/demo/full/scripts/controllers/ControlBar.tsx
@@ -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");
@@ -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 (
diff --git a/demo/full/scripts/controllers/ProgressBar.tsx b/demo/full/scripts/controllers/ProgressBar.tsx
index 43d81c0486..a1e5f53c09 100644
--- a/demo/full/scripts/controllers/ProgressBar.tsx
+++ b/demo/full/scripts/controllers/ProgressBar.tsx
@@ -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);
@@ -189,7 +190,7 @@ function ProgressBar({
onMouseMove={onMouseMove}
position={currentTime}
minimumPosition={minimumPosition}
- maximumPosition={maximumPosition}
+ maximumPosition={livePosition ?? maximumPosition}
bufferGap={bufferGap}
/>
}
diff --git a/demo/full/scripts/modules/player/catchUp.ts b/demo/full/scripts/modules/player/catchUp.ts
index b8d7fb3fa1..3b66b99192 100644
--- a/demo/full/scripts/modules/player/catchUp.ts
+++ b/demo/full/scripts/modules/player/catchUp.ts
@@ -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;
diff --git a/demo/full/scripts/modules/player/events.ts b/demo/full/scripts/modules/player/events.ts
index d7726bb66b..a50a12d250 100644
--- a/demo/full/scripts/modules/player/events.ts
+++ b/demo/full/scripts/modules/player/events.ts
@@ -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 &&
@@ -210,6 +214,7 @@ function linkPlayerEventsToState(
stateUpdates.duration = undefined;
stateUpdates.minimumPosition = undefined;
stateUpdates.maximumPosition = undefined;
+ stateUpdates.livePosition = undefined;
break;
}
diff --git a/demo/full/scripts/modules/player/index.ts b/demo/full/scripts/modules/player/index.ts
index da4e6e09e1..3b53894589 100644
--- a/demo/full/scripts/modules/player/index.ts
+++ b/demo/full/scripts/modules/player/index.ts
@@ -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;
@@ -185,6 +186,7 @@ const PlayerModule = declareModule(
liveGap: undefined,
loadedVideo: null,
lowLatencyMode: false,
+ livePosition: undefined,
maximumPosition: undefined,
minimumPosition: undefined,
playbackRate: 1,
diff --git a/doc/api/Basic_Methods/reload.md b/doc/api/Basic_Methods/reload.md
index cc3534ba03..d64547a486 100644
--- a/doc/api/Basic_Methods/reload.md
+++ b/doc/api/Basic_Methods/reload.md
@@ -35,6 +35,17 @@ The options argument is an object containing :
content was playing the last time it was played and stay in the `"LOADED"`
state (and paused) if it was paused last time it was played.
+- _keySystems_ (`Array. | undefined`): If set, a new configuration will
+ be set on this reloaded content regarding its decryption.
+
+ The value of this property follows the exact same structure than for the
+ original `loadVideo` call, it is described in the [decryption options
+ documentation page](../Decryption_Options.md).
+
+ You might for example want to update that way the `keySystems` option compared
+ to the one of the original `loadVideo` call when you suspect that there is a
+ decryption-related issue with the original `keySystems` given.
+
Note that despite this method's name, the player will not go through the
`RELOADING` state while reloading the content but through the regular `LOADING`
state - as if `loadVideo` was called on that same content again.
diff --git a/doc/api/Loading_a_Content.md b/doc/api/Loading_a_Content.md
index b4c890f77e..0cae7663bc 100644
--- a/doc/api/Loading_a_Content.md
+++ b/doc/api/Loading_a_Content.md
@@ -202,6 +202,16 @@ can be either:
- for VoD contents, it is the difference between the starting position and
the end position of the content.
+
+- **fromLivePosition** relative position relative to the content's live edge
+ (for live contents, it is the position that is intended to be broadcasted
+ at the current time) if it makes sense, in seconds. Should be a negative
+ number.
+
+ If the live edge is unknown or if it does not make sense for the current
+ content (for example, it won't make sense for a VoD content), that setting
+ repeats the same behavior than **fromLastPosition**.
+
- **percentage** (`Number`): percentage of the wanted position. `0` being
the minimum position possible (0 for static content, buffer depth for
dynamic contents) and `100` being the maximum position possible
diff --git a/doc/api/Miscellaneous/Debug_Element.md b/doc/api/Miscellaneous/Debug_Element.md
index 34a89d5a15..c77c2aa267 100644
--- a/doc/api/Miscellaneous/Debug_Element.md
+++ b/doc/api/Miscellaneous/Debug_Element.md
@@ -104,8 +104,20 @@ reflect exactly what's going on at a particular point in time.
- **vt**: _Video tracks_. List of the video tracks' `id` property. The line begins with a number indicating the number of available video tracks, followed by `:`, followed by each video track's id separated by a space. The current video track is prepended by a `*` character.
- **at**: _Audio tracks_. List of the audio tracks' `id` property. The line begins with a number indicating the number of available audio tracks, followed by `:`, followed by each audio track's id separated by a space. The current audio track is prepended by a `*` character.
- **tt**: _Text tracks_. List of the text tracks' `id` property. The line begins with a number indicating the number of available text tracks, followed by `:`, followed by each text track's id separated by a space. The current text track is prepended by a `*` character.
- - **vb**: _Video Bitrates_. The available video bitrates in the current video track, separated by a space.
- - **ab**: _Audio Bitrates_. The available audio bitrates in the current audio track, separated by a space.
+ - **vb**: _Video Bitrates_. The available video bitrates in the current
+ video track, separated by a space.
+ Each bitrate value can optionally be followed by an "`U!`", in which case
+ the codec of the corresponding Representation is unsupported, and/or be
+ followed by an "`E!`", in which case it is undecipherable currently.
+ In both of those cases the corresponding video Representation won't be
+ played by the RxPlayer.
+ - **ab**: _Audio Bitrates_. The available audio bitrates in the current
+ audio track, separated by a space.
+ Each bitrate value can optionally be followed by an "`U!`", in which case
+ the codec of the corresponding Representation is unsupported, and/or be
+ followed by an "`E!`", in which case it is undecipherable currently.
+ In both of those cases the corresponding audio Representation won't be
+ played by the RxPlayer.
- Buffer information
- **vbuf**: _Graphical representation of the video buffer_. The red rectangle indicates the current position, the different colors indicate different video qualities in the buffer.
diff --git a/doc/api/Miscellaneous/Low_Latency.md b/doc/api/Miscellaneous/Low_Latency.md
index fcc010dfae..d9f0e5bf17 100644
--- a/doc/api/Miscellaneous/Low_Latency.md
+++ b/doc/api/Miscellaneous/Low_Latency.md
@@ -70,7 +70,7 @@ rxPlayer.loadVideo({
url: "https://www.example.com/content.mpd",
transport: "dash",
lowLatencyMode: true,
- startAt: { fromLastPosition: 2 }, // Play 2 seconds from the live edge instead
+ startAt: { fromLivePosition: 2 }, // Play 2 seconds from the live edge instead
// (beware of much more frequent rebuffering
// risks)
});
diff --git a/doc/reference/API_Reference.md b/doc/reference/API_Reference.md
index b42b2409e8..115a558d64 100644
--- a/doc/reference/API_Reference.md
+++ b/doc/reference/API_Reference.md
@@ -104,6 +104,9 @@ properties, methods, events and so on.
- [`keySystems[].licenseStorage`](../api/Decryption_Options.md#licensestorage):
Allows to ask for the DRM session to persist the license.
+ - [`keySystems[].onKeyExpiration`](../api/Decryption_Options.md#onkeyexpiration):
+ Behavior when a key has an `"expired"` status.
+
- [`keySystems[].fallbackOn`](../api/Decryption_Options.md#fallbackon):
Allows to fallback to another quality when a key is refused.
@@ -235,6 +238,11 @@ properties, methods, events and so on.
- [`defaultTextTrack`](../api/Loading_a_Content.md#defaulttexttrack):
[Deprecated] Default characteristics wanted for the text track.
+## Static methods
+
+ - [`addFeatures`](../api/RxPlayer_Features.md):
+ Add features to the RxPlayer (e.g.: multithreading, offline playback etc.).
+
## Methods
- [`loadVideo`](../api/Loading_a_Content.md): Load a content.
@@ -454,15 +462,18 @@ properties, methods, events and so on.
- [`isContentLoaded`](../api/Playback_Information/isContentLoaded.md):
Returns `true` if a content is loaded.
-
+
- [`isBuffering`](../api/Playback_Information/isBuffering.md):
Returns `true` if the player is buffering.
-
+
- [`isPaused`](../api/Playback_Information/isPaused.md):
Returns `true` if the `` element is paused.
+ - [`isContentLoaded`](../api/Playback_Information/isContentLoaded.md):
+ Returns `true` if a content is loaded.
+
- [`getLastStoredContentPosition`](../api/Playback_Information/getLastStoredContentPosition.md):
- Returns the last stored content position, in seconds.
+ Returns the last stored content position, in seconds.
- [`getVideoLoadedTime`](../api/Deprecated/getVideoLoadedTime.md):
[Deprecated] Returns in seconds the difference between the start and the end
@@ -501,6 +512,9 @@ properties, methods, events and so on.
[Deprecated] Returns the first `` element attached to the media
element.
+ - [`createDebugElement`](../api/Miscellaneous/Debug_Element.md):
+ Display a RxPlayer-specialized debugging element.
+
## Static Properties
- [`version`](../api/Static_Properties.md#version):
@@ -568,12 +582,21 @@ properties, methods, events and so on.
- [`bitrateEstimationChange`](../api/Player_Events.md#bitrateestimationchange):
A new bitrate estimate is available.
+ - [`volumeChange`](../api/Player_Events.md#volumechange):
+ Characteristics of the currently set volume changed.
+
- [`periodChange`](../api/Player_Events.md#periodchange):
A new Period begins.
- [`decipherabilityUpdate`](../api/Player_Events.md#decipherabilityupdate):
A Representation's decipherability status has been updated.
+ - [`play`](../api/Player_Events.md#play):
+ Emitted when playback is no longer consider paused.
+
+ - [`pause`](../api/Player_Events.md#pause):
+ Emitted when playback is now consider paused.
+
- [`inbandEvents`](../api/Player_Events.md#inbandevents):
Events in the media have been encountered.
diff --git a/package-lock.json b/package-lock.json
index bce1ea0190..1a6f6eb72d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"@babel/plugin-transform-runtime": "7.22.15",
"@babel/preset-env": "7.22.20",
"@babel/preset-react": "7.22.15",
+ "@canalplus/readme.doc": "^0.3.0",
"@types/chai": "4.3.6",
"@types/jest": "29.5.5",
"@types/mocha": "10.0.1",
@@ -29,7 +30,6 @@
"babel-loader": "9.1.3",
"chai": "4.3.8",
"core-js": "3.32.2",
- "docgen.ico": "^0.2.3",
"esbuild": "0.19.3",
"eslint": "8.50.0",
"eslint-plugin-ban": "1.6.0",
@@ -59,7 +59,6 @@
"terser-webpack-plugin": "5.3.9",
"ts-jest": "29.1.1",
"ts-loader": "9.4.4",
- "tslint": "6.1.3",
"typescript": "5.2.2",
"webpack": "5.88.2",
"webpack-bundle-analyzer": "4.9.1",
@@ -1937,6 +1936,27 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
+ "node_modules/@canalplus/readme.doc": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@canalplus/readme.doc/-/readme.doc-0.3.0.tgz",
+ "integrity": "sha512-MVTz6iJs8a0KucpheT37gLADqyFTTk3PRaf95g5MRduRSwangwE4zq3306QtUId8dPuTLPXxWGUyULY00jmeRw==",
+ "dev": true,
+ "dependencies": {
+ "cheerio": "1.0.0-rc.12",
+ "highlight.js": "11.7.0",
+ "html-entities": "2.3.3",
+ "markdown-it": "13.0.1"
+ },
+ "bin": {
+ "readme.doc": "build/index.js"
+ }
+ },
+ "node_modules/@canalplus/readme.doc/node_modules/html-entities": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz",
+ "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==",
+ "dev": true
+ },
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@@ -4904,6 +4924,7 @@
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
"integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -5666,6 +5687,7 @@
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true,
+ "peer": true,
"engines": {
"node": ">=0.3.1"
}
@@ -5691,27 +5713,6 @@
"node": ">=8"
}
},
- "node_modules/docgen.ico": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/docgen.ico/-/docgen.ico-0.2.3.tgz",
- "integrity": "sha512-dfXbeTpcQwZbpXXTFOR4MCGm1aQr171c3kA0KZqvzawGm5WJe8IqojO+JHdu2xXDep/rDxC8BPfELzRKAZeK3w==",
- "dev": true,
- "dependencies": {
- "cheerio": "1.0.0-rc.12",
- "highlight.js": "11.7.0",
- "html-entities": "2.3.3",
- "markdown-it": "13.0.1"
- },
- "bin": {
- "docgen.ico": "build/index.js"
- }
- },
- "node_modules/docgen.ico/node_modules/html-entities": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz",
- "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==",
- "dev": true
- },
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -13841,6 +13842,7 @@
"resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz",
"integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==",
"dev": true,
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.0.0",
"builtin-modules": "^1.1.1",
@@ -13868,6 +13870,7 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
+ "peer": true,
"bin": {
"semver": "bin/semver"
}
@@ -13877,6 +13880,7 @@
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
"integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
"dev": true,
+ "peer": true,
"dependencies": {
"tslib": "^1.8.1"
}
@@ -16174,6 +16178,26 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
+ "@canalplus/readme.doc": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@canalplus/readme.doc/-/readme.doc-0.3.0.tgz",
+ "integrity": "sha512-MVTz6iJs8a0KucpheT37gLADqyFTTk3PRaf95g5MRduRSwangwE4zq3306QtUId8dPuTLPXxWGUyULY00jmeRw==",
+ "dev": true,
+ "requires": {
+ "cheerio": "1.0.0-rc.12",
+ "highlight.js": "11.7.0",
+ "html-entities": "2.3.3",
+ "markdown-it": "13.0.1"
+ },
+ "dependencies": {
+ "html-entities": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz",
+ "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==",
+ "dev": true
+ }
+ }
+ },
"@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@@ -18347,7 +18371,8 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
"integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
- "dev": true
+ "dev": true,
+ "peer": true
},
"bytes": {
"version": "3.1.2",
@@ -18911,7 +18936,8 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"diff-sequences": {
"version": "29.6.3",
@@ -18928,26 +18954,6 @@
"path-type": "^4.0.0"
}
},
- "docgen.ico": {
- "version": "0.2.3",
- "resolved": "https://registry.npmjs.org/docgen.ico/-/docgen.ico-0.2.3.tgz",
- "integrity": "sha512-dfXbeTpcQwZbpXXTFOR4MCGm1aQr171c3kA0KZqvzawGm5WJe8IqojO+JHdu2xXDep/rDxC8BPfELzRKAZeK3w==",
- "dev": true,
- "requires": {
- "cheerio": "1.0.0-rc.12",
- "highlight.js": "11.7.0",
- "html-entities": "2.3.3",
- "markdown-it": "13.0.1"
- },
- "dependencies": {
- "html-entities": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz",
- "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==",
- "dev": true
- }
- }
- },
"doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -24983,6 +24989,7 @@
"resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz",
"integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==",
"dev": true,
+ "peer": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"builtin-modules": "^1.1.1",
@@ -25003,13 +25010,15 @@
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
- "dev": true
+ "dev": true,
+ "peer": true
},
"tsutils": {
"version": "2.29.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
"integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
"dev": true,
+ "peer": true,
"requires": {
"tslib": "^1.8.1"
}
diff --git a/package.json b/package.json
index 562ba965ec..bf3aa225d9 100644
--- a/package.json
+++ b/package.json
@@ -45,12 +45,13 @@
"demo": "node ./scripts/generate_full_demo.js --production-mode",
"demo:min": "node ./scripts/generate_full_demo.js --production-mode --minify",
"demo:watch": "node ./scripts/generate_full_demo.js --watch --production-mode",
- "doc": "docgen.ico doc/ doc/generated \"$(cat VERSION)\"",
+ "doc": "readme.doc doc/ doc/generated \"$(cat VERSION)\"",
"lint": "eslint src -c .eslintrc.js",
"lint:demo": "eslint -c demo/full/.eslintrc.js demo/full/scripts",
"lint:tests": "eslint tests/**/*.js --ignore-pattern '/tests/performance/bundle*'",
"list": "node scripts/list-npm-scripts.js",
"prepublishOnly": "npm run build:modular",
+ "releases:dev": "./scripts/make-dev-releases",
"standalone": "node ./scripts/run_standalone_demo.js",
"start": "node ./scripts/start_demo_web_server.js",
"start:wasm": "node ./scripts/start_demo_web_server.js --include-wasm",
@@ -84,6 +85,7 @@
"@babel/plugin-transform-runtime": "7.22.15",
"@babel/preset-env": "7.22.20",
"@babel/preset-react": "7.22.15",
+ "@canalplus/readme.doc": "^0.3.0",
"@types/chai": "4.3.6",
"@types/jest": "29.5.5",
"@types/mocha": "10.0.1",
@@ -97,7 +99,6 @@
"babel-loader": "9.1.3",
"chai": "4.3.8",
"core-js": "3.32.2",
- "docgen.ico": "^0.2.3",
"esbuild": "0.19.3",
"eslint": "8.50.0",
"eslint-plugin-ban": "1.6.0",
@@ -127,7 +128,6 @@
"terser-webpack-plugin": "5.3.9",
"ts-jest": "29.1.1",
"ts-loader": "9.4.4",
- "tslint": "6.1.3",
"typescript": "5.2.2",
"webpack": "5.88.2",
"webpack-bundle-analyzer": "4.9.1",
@@ -200,6 +200,9 @@
"doc": "Generate the HTML documentation in doc/generated/pages"
},
"Update the RxPlayer's version": {
+ },
+ "Make a release": {
+ "releases:dev": "Produce dev npm releases (which are tagged pre-releases on npm) with the code in the current branch",
"update-version": "Update the version to the string given in argument (example: `npm run update-version 3.8.0`). Will update the codebase and perform every builds."
}
}
diff --git a/scripts/canal-release.patch b/scripts/canal-release.patch
new file mode 100644
index 0000000000..e170100e7f
--- /dev/null
+++ b/scripts/canal-release.patch
@@ -0,0 +1,316 @@
+diff --git a/src/core/init/directfile_content_initializer.ts b/src/core/init/directfile_content_initializer.ts
+index 68a24d897..80f05baf7 100644
+--- a/src/core/init/directfile_content_initializer.ts
++++ b/src/core/init/directfile_content_initializer.ts
+@@ -122,6 +122,7 @@ export default class DirectFileContentInitializer extends ContentInitializer {
+ * events when it cannot, as well as "unstalled" events when it get out of one.
+ */
+ const rebufferingController = new RebufferingController(playbackObserver,
++ null,
+ null,
+ speed);
+ rebufferingController.addEventListener("stalled", (evt) =>
+diff --git a/src/core/init/media_source_content_initializer.ts b/src/core/init/media_source_content_initializer.ts
+index 1cc84a4d5..ce3522ef0 100644
+--- a/src/core/init/media_source_content_initializer.ts
++++ b/src/core/init/media_source_content_initializer.ts
+@@ -462,9 +462,20 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
+
+ const rebufferingController = this._createRebufferingController(playbackObserver,
+ manifest,
++ segmentBuffersStore,
+ speed,
+ cancelSignal);
+-
++ rebufferingController.addEventListener("needsReload", () => {
++ // NOTE couldn't both be always calculated at event destination?
++ // Maybe there are exceptions?
++ const position = initialSeekPerformed.getValue() ?
++ playbackObserver.getCurrentTime() :
++ initialTime;
++ const autoplay = initialPlayPerformed.getValue() ?
++ !playbackObserver.getIsPaused() :
++ autoPlay;
++ onReloadOrder({ position, autoPlay: autoplay });
++ }, cancelSignal);
+ const contentTimeBoundariesObserver = this
+ ._createContentTimeBoundariesObserver(manifest,
+ mediaSource,
+@@ -768,11 +779,13 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
+ private _createRebufferingController(
+ playbackObserver : PlaybackObserver,
+ manifest : Manifest,
++ segmentBuffersStore : SegmentBuffersStore,
+ speed : IReadOnlySharedReference,
+ cancelSignal : CancellationSignal
+ ) : RebufferingController {
+ const rebufferingController = new RebufferingController(playbackObserver,
+ manifest,
++ segmentBuffersStore,
+ speed);
+ // Bubble-up events
+ rebufferingController.addEventListener("stalled",
+diff --git a/src/core/init/utils/rebuffering_controller.ts b/src/core/init/utils/rebuffering_controller.ts
+index f1753b8c2..93471ccaf 100644
+--- a/src/core/init/utils/rebuffering_controller.ts
++++ b/src/core/init/utils/rebuffering_controller.ts
+@@ -30,7 +30,7 @@ import {
+ IPlaybackObservation,
+ PlaybackObserver,
+ } from "../../api";
+-import { IBufferType } from "../../segment_buffers";
++import SegmentBuffersStore, { IBufferType } from "../../segment_buffers";
+ import { IStallingSituation } from "../types";
+
+
+@@ -54,6 +54,7 @@ export default class RebufferingController
+ /** Emit the current playback conditions */
+ private _playbackObserver : PlaybackObserver;
+ private _manifest : Manifest | null;
++ private _segmentBuffersStore : SegmentBuffersStore | null;
+ private _speed : IReadOnlySharedReference;
+ private _isStarted : boolean;
+
+@@ -65,6 +66,18 @@ export default class RebufferingController
+
+ private _canceller : TaskCanceller;
+
++ /**
++ * If set to something else than `null`, this is the DOMHighResTimestamp as
++ * outputed by `performance.now()` when playback begin to seem to not start
++ * despite having decipherable data in the buffer(s).
++ *
++ * If enough time in that condition is spent, special considerations are
++ * taken at which point `_currentFreezeTimestamp` is reset to `null`.
++ *
++ * It is also reset to `null` when and if there is no such issue anymore.
++ */
++ private _currentFreezeTimestamp : number | null;
++
+ /**
+ * @param {object} playbackObserver - emit the current playback conditions.
+ * @param {Object} manifest - The Manifest of the currently-played content.
+@@ -72,16 +85,19 @@ export default class RebufferingController
+ */
+ constructor(
+ playbackObserver : PlaybackObserver,
+- manifest: Manifest | null,
++ manifest : Manifest | null,
++ segmentBuffersStore : SegmentBuffersStore | null,
+ speed : IReadOnlySharedReference
+ ) {
+ super();
+ this._playbackObserver = playbackObserver;
+ this._manifest = manifest;
++ this._segmentBuffersStore = segmentBuffersStore;
+ this._speed = speed;
+ this._discontinuitiesStore = [];
+ this._isStarted = false;
+ this._canceller = new TaskCanceller();
++ this._currentFreezeTimestamp = null;
+ }
+
+ public start() : void {
+@@ -154,6 +170,10 @@ export default class RebufferingController
+ Math.max(observation.pendingInternalSeek ?? 0, observation.position) :
+ null;
+
++ if (this._checkDecipherabilityFreeze(observation)) {
++ return ;
++ }
++
+ if (freezing !== null) {
+ const now = performance.now();
+
+@@ -215,7 +235,7 @@ export default class RebufferingController
+ this.trigger("stalled", stalledReason);
+ return ;
+ } else {
+- log.warn("Init: ignored stall for too long, checking discontinuity",
++ log.warn("Init: ignored stall for too long, considering it",
+ now - ignoredStallTimeStamp);
+ }
+ }
+@@ -358,6 +378,96 @@ export default class RebufferingController
+ public destroy() : void {
+ this._canceller.cancel();
+ }
++
++ /**
++ * Support of contents with DRM on all the platforms out there is a pain in
++ * the *ss considering all the DRM-related bugs there are.
++ *
++ * We found out a frequent issue which is to be unable to play despite having
++ * all the decryption keys to play what is currently buffered.
++ * When this happens, re-creating the buffers from scratch, with a reload, is
++ * usually sufficient to unlock the situation.
++ *
++ * Although we prefer providing more targeted fixes or telling to platform
++ * developpers to fix their implementation, it's not always possible.
++ * We thus resorted to developping an heuristic which detects such situation
++ * and reload in that case.
++ *
++ * @param {Object} observation - The last playback observation produced, it
++ * has to be recent (just triggered for example).
++ * @returns {boolean} - Returns `true` if it seems to be such kind of
++ * decipherability freeze, in which case this method already performed the
++ * right handling steps.
++ */
++ private _checkDecipherabilityFreeze(
++ observation : IPlaybackObservation
++ ): boolean {
++ const { readyState,
++ rebuffering,
++ freezing } = observation;
++ const bufferGap = observation.bufferGap !== undefined &&
++ isFinite(observation.bufferGap) ? observation.bufferGap :
++ 0;
++ if (
++ this._segmentBuffersStore === null ||
++ bufferGap < 6 ||
++ (rebuffering === null && freezing === null) ||
++ readyState > 1
++ ) {
++ this._currentFreezeTimestamp = null;
++ return false;
++ }
++
++ const now = performance.now();
++ if (this._currentFreezeTimestamp === null) {
++ this._currentFreezeTimestamp = now;
++ }
++ const rebufferingForTooLong =
++ rebuffering !== null && now - rebuffering.timestamp > 4000;
++ const frozenForTooLong =
++ freezing !== null && now - freezing.timestamp > 4000;
++
++ if (
++ (rebufferingForTooLong || frozenForTooLong) &&
++ this._currentFreezeTimestamp > 4000
++ ) {
++ const statusAudio = this._segmentBuffersStore.getStatus("audio");
++ const statusVideo = this._segmentBuffersStore.getStatus("video");
++ let hasOnlyDecipherableSegments = true;
++ let isClear = true;
++ for (const status of [statusAudio, statusVideo]) {
++ if (status.type === "initialized") {
++ for (const segment of status.value.getInventory()) {
++ const { representation } = segment.infos;
++ if (representation.decipherable === false) {
++ log.warn(
++ "Init: we have undecipherable segments left in the buffer, reloading"
++ );
++ this._currentFreezeTimestamp = null;
++ this.trigger("needsReload", null);
++ return true;
++ } else if (representation.contentProtections !== undefined) {
++ isClear = false;
++ if (representation.decipherable !== true) {
++ hasOnlyDecipherableSegments = false;
++ }
++ }
++ }
++ }
++ }
++
++ if (!isClear && hasOnlyDecipherableSegments) {
++ log.warn(
++ "Init: we are frozen despite only having decipherable " +
++ "segments left in the buffer, reloading"
++ );
++ this._currentFreezeTimestamp = null;
++ this.trigger("needsReload", null);
++ return true;
++ }
++ }
++ return false;
++ }
+ }
+
+ /**
+@@ -581,6 +691,7 @@ class PlaybackRateUpdater {
+ export interface IRebufferingControllerEvent {
+ stalled : IStallingSituation;
+ unstalled : null;
++ needsReload : null;
+ warning : IPlayerError;
+ }
+
+diff --git a/src/parsers/texttracks/ttml/html/apply_extent.ts b/src/parsers/texttracks/ttml/html/apply_extent.ts
+index 5772fa8fb..eb3a051e4 100644
+--- a/src/parsers/texttracks/ttml/html/apply_extent.ts
++++ b/src/parsers/texttracks/ttml/html/apply_extent.ts
+@@ -54,7 +54,14 @@ export default function applyExtent(
+ secondExtent[2] === "%" ||
+ secondExtent[2] === "em")
+ {
+- element.style.height = secondExtent[1] + secondExtent[2];
++ const toNum = Number(secondExtent[1]);
++ if (secondExtent[2] === "%" && !isNaN(toNum) &&
++ (toNum < 0 || toNum > 100))
++ {
++ element.style.width = "80%";
++ } else {
++ element.style.height = secondExtent[1] + secondExtent[2];
++ }
+ } else if (secondExtent[2] === "c") {
+ addClassName(element, "proportional-style");
+ element.setAttribute("data-proportional-height", secondExtent[1]);
+diff --git a/src/parsers/texttracks/ttml/html/apply_line_height.ts b/src/parsers/texttracks/ttml/html/apply_line_height.ts
+index 4f727229a..253aa1a72 100644
+--- a/src/parsers/texttracks/ttml/html/apply_line_height.ts
++++ b/src/parsers/texttracks/ttml/html/apply_line_height.ts
+@@ -14,16 +14,15 @@
+ * limitations under the License.
+ */
+
+-import { addClassName } from "../../../../compat";
+ import log from "../../../../log";
+ import { REGXP_LENGTH } from "../regexps";
+
+ /**
+- * @param {HTMLElement} element
++ * @param {HTMLElement} _element
+ * @param {string} lineHeight
+ */
+ export default function applyLineHeight(
+- element : HTMLElement,
++ _element : HTMLElement,
+ lineHeight : string
+ ) : void {
+ const trimmedLineHeight = lineHeight.trim();
+@@ -40,10 +39,10 @@ export default function applyLineHeight(
+ firstLineHeight[2] === "%" ||
+ firstLineHeight[2] === "em")
+ {
+- element.style.lineHeight = firstLineHeight[1] + firstLineHeight[2];
++ // element.style.lineHeight = firstLineHeight[1] + firstLineHeight[2];
+ } else if (firstLineHeight[2] === "c") {
+- addClassName(element, "proportional-style");
+- element.setAttribute("data-proportional-line-height", firstLineHeight[1]);
++ // addClassName(element, "proportional-style");
++ // element.setAttribute("data-proportional-line-height", firstLineHeight[1]);
+ } else {
+ log.warn("TTML Parser: unhandled lineHeight unit:", firstLineHeight[2]);
+ }
+diff --git a/src/parsers/texttracks/ttml/html/apply_origin.ts b/src/parsers/texttracks/ttml/html/apply_origin.ts
+index 01a205aad..91d69fa3c 100644
+--- a/src/parsers/texttracks/ttml/html/apply_origin.ts
++++ b/src/parsers/texttracks/ttml/html/apply_origin.ts
+@@ -53,7 +53,15 @@ export default function applyOrigin(
+ secondOrigin[2] === "%" ||
+ secondOrigin[2] === "em")
+ {
+- element.style.top = secondOrigin[1] + secondOrigin[2];
++ const toNum = Number(secondOrigin[1]);
++ if (secondOrigin[2] === "%" && !isNaN(toNum) &&
++ (toNum < 0 || toNum > 100))
++ {
++ element.style.bottom = "5%";
++ element.style.left = "10%";
++ } else {
++ element.style.top = secondOrigin[1] + secondOrigin[2];
++ }
+ } else if (secondOrigin[2] === "c") {
+ addClassName(element, "proportional-style");
+ element.setAttribute("data-proportional-top", secondOrigin[1]);
diff --git a/scripts/make-dev-releases b/scripts/make-dev-releases
new file mode 100755
index 0000000000..601244d292
--- /dev/null
+++ b/scripts/make-dev-releases
@@ -0,0 +1,79 @@
+#!/bin/bash
+
+# make-dev-releases
+# =================
+#
+# This script produces pre-releases on top of the current branch for the
+# `dev` and `canal` versions (as per their npm tags).
+#
+# To use it:
+#
+# 1. Be sure that you're on the branch corresponding to the pre-release you
+# want to publish, at the repository's root directory.
+#
+# 2. Call this script followed with the corresponding version number it would
+# have as an official release in the `MAJOR.MINOR.PATCH` format (e.g.
+# `./update-version 4.1.3`). Special suffix corresponding to the date and
+# tag will be added automatically by this script.
+#
+# 3. When the script ask you to confirm, check that the preceding commands did
+# not output any issue and if it didn't you can confirm.
+#
+# 4. That's it!
+
+set -e
+
+if [ $# -eq 0 ]; then
+ read -r -p "Please enter the wanted version number (example: 4.12.1): " version
+ if [ -z "${version}" ]; then
+ echo "Please enter a valid version number"
+ exit 1
+ fi
+fi
+
+if [ $# -lt 2 ]; then
+ read -r -p "Please enter the increment number [by default: 00]: " incr
+ if [ -z "${incr}" ]; then
+ incr="00"
+ fi
+fi
+
+current_branch=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
+date=$(date "+%Y%m%d")
+dev_branch="release/v${version}-dev.${date}00"
+canal_branch="release/v${version}-canal.${date}00"
+
+git checkout -b ${dev_branch}
+./scripts/update-version $1-dev.${date}00
+git add --all
+git commit -m "update version"
+while true; do
+ read -n1 -p "Do you wish to push and publish the dev build? [y/n] " yn
+ echo ""
+ case $yn in
+ [Yy]* ) break;;
+ [Nn]* ) exit;;
+ * ) echo "Please answer y or n.";;
+ esac
+done
+git push origin ${dev_branch}
+npm publish --tag dev
+
+git checkout $current_branch
+
+git checkout -b ${canal_branch}
+git apply ./scripts/canal-release.patch
+./scripts/update-version $1-canal.${date}00
+git add --all
+git commit -m "update version"
+git push origin ${canal_branch}
+while true; do
+ read -n1 -p "Do you wish to push and publish the canal build? [y/n] " yn
+ echo ""
+ case $yn in
+ [Yy]* ) break;;
+ [Nn]* ) exit;;
+ * ) echo "Please answer y or n.";;
+ esac
+done
+npm publish --tag canal
diff --git a/scripts/update-version b/scripts/update-version
index 0a8e02b029..288d13048e 100755
--- a/scripts/update-version
+++ b/scripts/update-version
@@ -21,8 +21,15 @@
set -e
+if [ $# -eq 0 ]; then
+ echo "no version in argument"
+ exit 1
+fi
+
version=$1
+date_iso=$(date "+%Y-%m-%d")
+sed -i.bak -e "s/^\#\# Unreleased/\#\# v${version} \(${date_iso}\)/gi" CHANGELOG.md && rm CHANGELOG.md.bak
sed -i.bak -e "s/\/\\* PLAYER_VERSION \\*\/\"\(.*\)\";/\/* PLAYER_VERSION *\/\"${version}\";/g" src/core/api/public_api.ts && rm src/core/api/public_api.ts.bak
sed -i.bak -e "s/\"version\":\s*\"[0-9]\+\.[0-9]\+\.[0-9]\+[^\"]*\"/\"version\": \"${version}\"/g" package.json && rm package.json.bak
sed -i.bak -e "s/sonar\.projectVersion= *.*/sonar.projectVersion=${version}/g" sonar-project.properties && rm sonar-project.properties.bak
diff --git a/src/compat/browser_detection.ts b/src/compat/browser_detection.ts
index 4215b27d2f..83aeeb1e7a 100644
--- a/src/compat/browser_detection.ts
+++ b/src/compat/browser_detection.ts
@@ -63,6 +63,9 @@ let isPanasonic = false;
/** `true` for the PlayStation 5 game console. */
let isPlayStation5 = false;
+/** `true` for the Xbox game consoles. */
+let isXbox = false;
+
((function findCurrentBrowser() : void {
if (isNode) {
return ;
@@ -90,9 +93,23 @@ let isPlayStation5 = false;
{
isSafariMobile = true;
} else if (
+ // the following statement check if the window.safari contains the method
+ // "pushNotification", this condition is not met when using web app from the dock
+ // on macOS, this is why we also check userAgent.
Object.prototype.toString.call(window.HTMLElement).indexOf("Constructor") >= 0 ||
(window as ISafariWindowObject).safari?.pushNotification?.toString() ===
- "[object SafariRemoteNotification]"
+ "[object SafariRemoteNotification]" ||
+ // browsers are lying: Chrome reports both as Chrome and Safari in user
+ // agent string, So to detect Safari we have to check for the Safari string
+ // and the absence of the Chrome string
+ // eslint-disable-next-line max-len
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent#which_part_of_the_user_agent_contains_the_information_you_are_looking_for
+ ((/Safari\/(\d+)/).test(navigator.userAgent) &&
+ // Safari should contain Version/ in userAgent
+ (/Version\/(\d+)/).test(navigator.userAgent) &&
+ (navigator.vendor?.indexOf("Apple") !== -1) &&
+ !(/Chrome\/(\d+)/).test(navigator.userAgent) &&
+ !(/Chromium\/(\d+)/).test(navigator.userAgent))
) {
isSafariDesktop = true;
}
@@ -128,6 +145,8 @@ let isPlayStation5 = false;
}
} else if (/[Pp]anasonic/.test(navigator.userAgent)) {
isPanasonic = true;
+ } else if (navigator.userAgent.indexOf("Xbox") !== -1) {
+ isXbox = true;
}
})());
@@ -142,6 +161,7 @@ export {
isFirefox,
isPanasonic,
isPlayStation5,
+ isXbox,
isSafariDesktop,
isSafariMobile,
isSamsungBrowser,
diff --git a/src/compat/should_prevent_seeking_at_0_initially.ts b/src/compat/should_prevent_seeking_at_0_initially.ts
new file mode 100644
index 0000000000..c27eb9aa9d
--- /dev/null
+++ b/src/compat/should_prevent_seeking_at_0_initially.ts
@@ -0,0 +1,19 @@
+import { isIEOrEdge, isXbox } from "./browser_detection";
+
+/**
+ * We noticed that on Xbox game consoles and Universal windows platforms
+ * (presumably an Edge version is in cause here), the browser didn't send
+ * a "seeking" event if we were seeking at a 0 position initially.
+ *
+ * We could theoretically never seek at 0 initially as the initial position of
+ * an HTMLMediaElement should be at 0 anyway, but we still do it as a safe
+ * solution, as many devices have a buggy integration of HTML5 media API.
+ *
+ * This function returns `true` when we should avoid doing so, for now only for
+ * the non-standard behavior of those Edge platforms.
+ * @returns {number}
+ */
+export default function shouldPreventSeekingAt0Initially(
+): boolean {
+ return isXbox || isIEOrEdge;
+}
diff --git a/src/core/api/debug/buffer_graph.ts b/src/core/api/debug/buffer_graph.ts
index 44e5da72e4..e587b4c1a4 100644
--- a/src/core/api/debug/buffer_graph.ts
+++ b/src/core/api/debug/buffer_graph.ts
@@ -1,7 +1,7 @@
import { Representation } from "../../../manifest";
import { IBufferedChunk } from "../../segment_buffers";
-const BUFFER_WIDTH_IN_SECONDS = 10000;
+const BUFFER_WIDTH_IN_SECONDS = 30 * 60;
const COLORS = [
"#2ab7ca",
@@ -95,10 +95,7 @@ export default class SegmentBufferGraph {
let minimumPosition;
let maximumPosition;
if (maximumPoint - minimumPoint > BUFFER_WIDTH_IN_SECONDS) {
- if (currentTime === undefined) {
- minimumPosition = minimumPoint;
- maximumPosition = maximumPoint;
- } else if (maximumPoint - currentTime < BUFFER_WIDTH_IN_SECONDS / 2) {
+ if (maximumPoint - currentTime < BUFFER_WIDTH_IN_SECONDS / 2) {
maximumPosition = maximumPoint;
minimumPosition = maximumPoint - BUFFER_WIDTH_IN_SECONDS;
} else if (currentTime - minimumPoint < BUFFER_WIDTH_IN_SECONDS / 2) {
diff --git a/src/core/api/debug/modules/general_info.ts b/src/core/api/debug/modules/general_info.ts
index cd8491fa1d..b4429c42cd 100644
--- a/src/core/api/debug/modules/general_info.ts
+++ b/src/core/api/debug/modules/general_info.ts
@@ -181,19 +181,28 @@ export default function constructDebugGeneralInfo(
]);
adaptationsElt.appendChild(textAdaps);
}
- const videoBitrates = instance.getAvailableVideoBitrates();
- const audioBitrates = instance.getAvailableAudioBitrates();
+ const adaptations = instance.getCurrentAdaptations();
+ const videoBitratesStr = adaptations?.video?.representations.map((r) => {
+ return String(r.bitrate) +
+ (r.isSupported ? "" : " U!") +
+ (r.decipherable !== false ? "" : " E!");
+ }) ?? [];
+ const audioBitratesStr = adaptations?.video?.representations.map((r) => {
+ return String(r.bitrate) +
+ (r.isSupported ? "" : " U!") +
+ (r.decipherable !== false ? "" : " E!");
+ }) ?? [];
representationsElt.innerHTML = "";
- if (videoBitrates.length > 0) {
+ if (videoBitratesStr.length > 0) {
representationsElt.appendChild(createMetricTitle("vb"));
representationsElt.appendChild(createElement("span", {
- textContent: videoBitrates.join(" ") + " ",
+ textContent: videoBitratesStr.join(" ") + " ",
}));
}
- if (audioBitrates.length > 0) {
+ if (audioBitratesStr.length > 0) {
representationsElt.appendChild(createMetricTitle("ab"));
representationsElt.appendChild(createElement("span", {
- textContent: audioBitrates.join(" ") + " ",
+ textContent: audioBitratesStr.join(" ") + " ",
}));
}
} else {
diff --git a/src/core/api/option_utils.ts b/src/core/api/option_utils.ts
index 1b00fd1778..88193bdbec 100644
--- a/src/core/api/option_utils.ts
+++ b/src/core/api/option_utils.ts
@@ -56,6 +56,7 @@ export type IParsedStartAtOption = { position : number } |
{ wallClockTime : number } |
{ percentage : number } |
{ fromLastPosition : number } |
+ { fromLivePosition : number } |
{ fromFirstPosition : number };
export interface IParsedTransportOptions {
@@ -397,6 +398,8 @@ function parseConstructorOptions(
*/
function checkReloadOptions(options?: {
reloadAt?: { position?: number; relative?: number };
+ keySystems?: IKeySystemOption[];
+ autoPlay?: boolean;
}): void {
if (options === null ||
(typeof options !== "object" && options !== undefined)) {
@@ -414,6 +417,12 @@ function checkReloadOptions(options?: {
options?.reloadAt?.relative !== undefined) {
throw new Error("API: reload - Invalid 'reloadAt.relative' option format.");
}
+ if (!Array.isArray(options?.keySystems) && options?.keySystems !== undefined) {
+ throw new Error("API: reload - Invalid 'keySystems' option format.");
+ }
+ if (options?.autoPlay !== undefined && typeof options.autoPlay !== "boolean") {
+ throw new Error("API: reload - Invalid 'autoPlay' option format.");
+ }
}
/**
@@ -645,9 +654,8 @@ function parseLoadVideoOptions(
}
if (!isNullOrUndefined(options.startAt)) {
- // TODO Better way to express that in TypeScript?
- if ((options.startAt as { wallClockTime? : Date|number }).wallClockTime
- instanceof Date
+ if ("wallClockTime" in options.startAt
+ && options.startAt.wallClockTime instanceof Date
) {
const wallClockTime = (options.startAt as { wallClockTime : Date })
.wallClockTime.getTime() / 1000;
diff --git a/src/core/api/public_api.ts b/src/core/api/public_api.ts
index 8d1ff7682b..1f8e07bdfc 100644
--- a/src/core/api/public_api.ts
+++ b/src/core/api/public_api.ts
@@ -59,6 +59,7 @@ import {
IConstructorOptions,
IDecipherabilityUpdateContent,
IKeySystemConfigurationOutput,
+ IKeySystemOption,
ILoadVideoOptions,
IPeriod,
IPlayerError,
@@ -577,6 +578,7 @@ class Player extends EventEmitter {
*/
reload(reloadOpts?: {
reloadAt?: { position?: number; relative?: number };
+ keySystems?: IKeySystemOption[];
autoPlay?: boolean;
}): void {
const { options,
@@ -609,6 +611,13 @@ class Player extends EventEmitter {
autoPlay = !reloadInPause;
}
+ let keySystems : IKeySystemOption[] | undefined;
+ if (reloadOpts?.keySystems !== undefined) {
+ keySystems = reloadOpts.keySystems;
+ } else if (this._priv_reloadingMetadata.options?.keySystems !== undefined) {
+ keySystems = this._priv_reloadingMetadata.options.keySystems;
+ }
+
const newOptions = { ...options,
initialManifest: manifest };
if (startAt !== undefined) {
@@ -617,6 +626,9 @@ class Player extends EventEmitter {
if (autoPlay !== undefined) {
newOptions.autoPlay = autoPlay;
}
+ if (keySystems !== undefined) {
+ newOptions.keySystems = keySystems;
+ }
this._priv_initializeContentPlayback(newOptions);
}
@@ -626,7 +638,7 @@ class Player extends EventEmitter {
if (features.createDebugElement === null) {
throw new Error("Feature `DEBUG_ELEMENT` not added to the RxPlayer");
}
- const canceller = new TaskCanceller() ;
+ const canceller = new TaskCanceller();
features.createDebugElement(element, this, canceller.signal);
return {
dispose() {
@@ -2386,6 +2398,29 @@ class Player extends EventEmitter {
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}
@@ -2429,9 +2464,11 @@ class Player extends EventEmitter {
}
const segmentBufferStatus = this._priv_contentInfos
.segmentBuffersStore.getStatus(bufferType);
- return segmentBufferStatus.type === "initialized" ?
- segmentBufferStatus.value.getInventory() :
- null;
+ if (segmentBufferStatus.type === "initialized") {
+ segmentBufferStatus.value.synchronizeInventory(true);
+ return segmentBufferStatus.value.getInventory();
+ }
+ return null;
}
/**
diff --git a/src/core/decrypt/attach_media_keys.ts b/src/core/decrypt/attach_media_keys.ts
index 0b0d0a9b60..4aa4dc4497 100644
--- a/src/core/decrypt/attach_media_keys.ts
+++ b/src/core/decrypt/attach_media_keys.ts
@@ -29,10 +29,13 @@ import MediaKeysInfosStore from "./utils/media_keys_infos_store";
* Dispose of the MediaKeys instance attached to the given media element, if
* one.
* @param {Object} mediaElement
+ * @returns {Promise}
*/
-export function disableMediaKeys(mediaElement : HTMLMediaElement): void {
+export function disableMediaKeys(
+ mediaElement : HTMLMediaElement
+): Promise {
MediaKeysInfosStore.setState(mediaElement, null);
- eme.setMediaKeys(mediaElement, null)
+ return eme.setMediaKeys(mediaElement, null)
.then(() => {
log.info("DRM: MediaKeys disabled with success");
})
diff --git a/src/core/decrypt/init_media_keys.ts b/src/core/decrypt/init_media_keys.ts
index ecd79efe74..2fd38ce6c7 100644
--- a/src/core/decrypt/init_media_keys.ts
+++ b/src/core/decrypt/init_media_keys.ts
@@ -14,8 +14,10 @@
* limitations under the License.
*/
+import { isWebOs } from "../../compat/browser_detection";
import log from "../../log";
import { IKeySystemOption } from "../../public_types";
+import noop from "../../utils/noop";
import { CancellationSignal } from "../../utils/task_canceller";
import { disableMediaKeys } from "./attach_media_keys";
import getMediaKeysInfos, {
@@ -45,7 +47,14 @@ export default async function initMediaKeys(
if (shouldDisableOldMediaKeys) {
log.debug("DRM: Disabling old MediaKeys");
- disableMediaKeys(mediaElement);
+ // TODO should we be awaiting always?
+ // Should be tested on all devices, we may want to wait for another
+ // version to make this important change.
+ if (isWebOs) {
+ await disableMediaKeys(mediaElement);
+ } else {
+ disableMediaKeys(mediaElement).catch(noop);
+ }
}
return mediaKeysInfo;
}
diff --git a/src/core/init/directfile_content_initializer.ts b/src/core/init/directfile_content_initializer.ts
index c9aa31d7a3..f92ccf33b8 100644
--- a/src/core/init/directfile_content_initializer.ts
+++ b/src/core/init/directfile_content_initializer.ts
@@ -27,6 +27,7 @@ import {
IPlayerError,
} from "../../public_types";
import assert from "../../utils/assert";
+import isNullOrUndefined from "../../utils/is_null_or_undefined";
import SharedReference, {
IReadOnlySharedReference,
} from "../../utils/reference";
@@ -212,6 +213,7 @@ export default class DirectFileContentInitializer extends ContentInitializer {
initialTime,
autoPlay,
(err) => this.trigger("warning", err),
+ true,
cancelSignal
).autoPlayResult
.then(() =>
@@ -240,11 +242,11 @@ function getDirectFileInitialTime(
mediaElement : HTMLMediaElement,
startAt? : IInitialTimeOptions
) : number {
- if (startAt == null) {
+ if (isNullOrUndefined(startAt)) {
return 0;
}
- if (startAt.position != null) {
+ if (!isNullOrUndefined(startAt.position)) {
return startAt.position;
} else if (startAt.wallClockTime != null) {
return startAt.wallClockTime;
@@ -253,15 +255,30 @@ function getDirectFileInitialTime(
}
const duration = mediaElement.duration;
- if (duration == null || !isFinite(duration)) {
- log.warn("startAt.fromLastPosition set but no known duration, " +
- "beginning at 0.");
- return 0;
- }
if (typeof startAt.fromLastPosition === "number") {
+ if (isNullOrUndefined(duration) || !isFinite(duration)) {
+ log.warn("startAt.fromLastPosition set but no known duration, " +
+ "beginning at 0.");
+ return 0;
+ }
return Math.max(0, duration + startAt.fromLastPosition);
+ } else if (typeof startAt.fromLivePosition === "number") {
+ const livePosition = mediaElement.seekable.length > 0 ?
+ mediaElement.seekable.end(0) :
+ duration;
+ if (isNullOrUndefined(livePosition)) {
+ log.warn("startAt.fromLivePosition set but no known live position, " +
+ "beginning at 0.");
+ return 0;
+ }
+ return Math.max(0, livePosition + startAt.fromLivePosition);
} else if (startAt.percentage != null) {
+ if (isNullOrUndefined(duration) || !isFinite(duration)) {
+ log.warn("startAt.percentage set but no known duration, " +
+ "beginning at 0.");
+ return 0;
+ }
const { percentage } = startAt;
if (percentage >= 100) {
return duration;
diff --git a/src/core/init/media_source_content_initializer.ts b/src/core/init/media_source_content_initializer.ts
index d89a39828e..3efe571644 100644
--- a/src/core/init/media_source_content_initializer.ts
+++ b/src/core/init/media_source_content_initializer.ts
@@ -426,6 +426,7 @@ export default class MediaSourceContentInitializer extends ContentInitializer {
initialTime,
autoPlay,
(err) => this.trigger("warning", err),
+ true,
cancelSignal);
if (cancelSignal.isCancelled()) {
@@ -723,9 +724,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();
@@ -741,9 +743,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;
}
diff --git a/src/core/init/utils/content_time_boundaries_observer.ts b/src/core/init/utils/content_time_boundaries_observer.ts
index 922e990d59..219ca86d7f 100644
--- a/src/core/init/utils/content_time_boundaries_observer.ts
+++ b/src/core/init/utils/content_time_boundaries_observer.ts
@@ -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
@@ -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;
}
@@ -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();
}
/**
@@ -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);
}
}
}
@@ -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 {
@@ -348,16 +350,16 @@ 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.
@@ -365,7 +367,7 @@ export interface IDurationItem {
* 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;
}
@@ -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
@@ -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)
diff --git a/src/core/init/utils/get_initial_time.ts b/src/core/init/utils/get_initial_time.ts
index 1c682937c6..781c63f4df 100644
--- a/src/core/init/utils/get_initial_time.ts
+++ b/src/core/init/utils/get_initial_time.ts
@@ -42,11 +42,25 @@ export interface IInitialTimeOptions {
*/
fromFirstPosition? : number | null | undefined;
/**
- * If set, we should begin at this position relative to the content's end,
- * in seconds.
+ * If set, we should begin at this position relative to the content's maximum
+ * seekable position, in seconds.
+ *
+ * It should consequently in most cases be a negative value.
*/
fromLastPosition? : number | null | undefined;
- /** If set, we should begin at this position relative to the whole duration of
+ /**
+ * If set, we should begin at this position relative to the content's live
+ * edge if it makes sense, in seconds.
+ *
+ * It should consequently in most cases be a negative value.
+ *
+ * If the live edge is unknown or if it does not make sense for the current
+ * content, that position is relative to the content's maximum position
+ * instead.
+ */
+ fromLivePosition? : number | null | undefined;
+ /**
+ * If set, we should begin at this position relative to the whole duration of
* the content, in percentage.
*/
percentage? : number | null | undefined;
@@ -72,13 +86,7 @@ export default function getInitialTime(
) : number {
if (!isNullOrUndefined(startAt)) {
const min = manifest.getMinimumSafePosition();
- let max;
- if (manifest.isLive) {
- max = manifest.getLivePosition();
- }
- if (max === undefined) {
- max = manifest.getMaximumSafePosition();
- }
+ const max = manifest.getMaximumSafePosition();
if (!isNullOrUndefined(startAt.position)) {
log.debug("Init: using startAt.minimumPosition");
return Math.max(Math.min(startAt.position, max), min);
@@ -96,12 +104,17 @@ export default function getInitialTime(
const { fromFirstPosition } = startAt;
return fromFirstPosition <= 0 ? min :
Math.min(max, min + fromFirstPosition);
- }
- else if (!isNullOrUndefined(startAt.fromLastPosition)) {
+ } else if (!isNullOrUndefined(startAt.fromLastPosition)) {
log.debug("Init: using startAt.fromLastPosition");
const { fromLastPosition } = startAt;
return fromLastPosition >= 0 ? max :
Math.max(min, max + fromLastPosition);
+ } else if (!isNullOrUndefined(startAt.fromLivePosition)) {
+ log.debug("Init: using startAt.fromLivePosition");
+ const livePosition = manifest.getLivePosition() ?? max;
+ const { fromLivePosition } = startAt;
+ return fromLivePosition >= 0 ? livePosition :
+ Math.max(min, livePosition + fromLivePosition);
} else if (!isNullOrUndefined(startAt.percentage)) {
log.debug("Init: using startAt.percentage");
const { percentage } = startAt;
diff --git a/src/core/init/utils/get_loaded_reference.ts b/src/core/init/utils/get_loaded_reference.ts
index 1fd1e1d90b..2e6091d214 100644
--- a/src/core/init/utils/get_loaded_reference.ts
+++ b/src/core/init/utils/get_loaded_reference.ts
@@ -67,11 +67,13 @@ export default function getLoadedReference(
const minReadyState = shouldWaitForHaveEnoughData() ? 4 :
3;
- if (observation.readyState >= minReadyState && observation.currentRange !== null) {
- if (!shouldValidateMetadata() || mediaElement.duration > 0) {
- isLoaded.setValue(true);
- listenCanceller.cancel();
- return;
+ if (observation.readyState >= minReadyState) {
+ if (observation.currentRange !== null || observation.ended) {
+ if (!shouldValidateMetadata() || mediaElement.duration > 0) {
+ isLoaded.setValue(true);
+ listenCanceller.cancel();
+ return;
+ }
}
}
}, { includeLastObservation: true, clearSignal: listenCanceller.signal });
diff --git a/src/core/init/utils/initial_seek_and_play.ts b/src/core/init/utils/initial_seek_and_play.ts
index d13aadd707..16cf8fe6b8 100644
--- a/src/core/init/utils/initial_seek_and_play.ts
+++ b/src/core/init/utils/initial_seek_and_play.ts
@@ -16,6 +16,9 @@
import { shouldValidateMetadata } from "../../../compat";
import { READY_STATES } from "../../../compat/browser_compatibility_types";
+import { isSafariMobile } from "../../../compat/browser_detection";
+/* eslint-disable-next-line max-len */
+import shouldPreventSeekingAt0Initially from "../../../compat/should_prevent_seeking_at_0_initially";
import { MediaError } from "../../../errors";
import log from "../../../log";
import { IPlayerError } from "../../../public_types";
@@ -63,6 +66,7 @@ export interface IInitialSeekAndPlayObject {
* @param {number|Function} startTime
* @param {boolean} mustAutoPlay
* @param {Function} onWarning
+ * @param {boolean} isDirectfile
* @param {Object} cancelSignal
* @returns {Object}
*/
@@ -72,6 +76,7 @@ export default function performInitialSeekAndPlay(
startTime : number|(() => number),
mustAutoPlay : boolean,
onWarning : (err : IPlayerError) => void,
+ isDirectfile : boolean,
cancelSignal : CancellationSignal
) : IInitialSeekAndPlayObject {
let resolveAutoPlay : (x : IInitialPlayEvent) => void;
@@ -99,12 +104,40 @@ export default function performInitialSeekAndPlay(
function onLoadedMetadata() {
mediaElement.removeEventListener("loadedmetadata", onLoadedMetadata);
+
+ /** `true` if we asked the `PlaybackObserver` to perform an initial seek. */
+ let hasAskedForInitialSeek = false;
+
+ const performInitialSeek = (initialSeekTime: number) => {
+ log.info("Init: Set initial time", initialSeekTime);
+ playbackObserver.setCurrentTime(initialSeekTime);
+ hasAskedForInitialSeek = true;
+ initialSeekPerformed.setValue(true);
+ initialSeekPerformed.finish();
+ };
+
+ // `startTime` defined as a function might depend on metadata to make its
+ // choice, such as the content duration, minimum and/or maximum position.
+ //
+ // The RxPlayer might already know those through the Manifest file for
+ // non-Directfile contents, yet only through the `HTMLMediaElement` once a
+ // a sufficient `readyState` has been reached for directfile contents.
+ // So let's divide the two possibilities here.
const initialTime = typeof startTime === "function" ? startTime() :
startTime;
- log.info("Init: Set initial time", initialTime);
- playbackObserver.setCurrentTime(initialTime);
- initialSeekPerformed.setValue(true);
- initialSeekPerformed.finish();
+ if (shouldPreventSeekingAt0Initially() && initialTime === 0) {
+ initialSeekPerformed.setValue(true);
+ initialSeekPerformed.finish();
+ } else if (isDirectfile && isSafariMobile) {
+ // On safari mobile (version 17.1.2) seeking too early cause the video
+ // to never buffer media data. Using setTimeout 0 defers the seek
+ // to a moment at which safari should be more able to handle a seek.
+ setTimeout(() => {
+ performInitialSeek(initialTime);
+ }, 0);
+ } else {
+ performInitialSeek(initialTime);
+ }
if (shouldValidateMetadata() && mediaElement.duration === 0) {
const error = new MediaError("MEDIA_ERR_NOT_LOADED_METADATA",
@@ -116,8 +149,19 @@ export default function performInitialSeekAndPlay(
return ;
}
+ /**
+ * We only want to continue to `play` when a `seek` has actually been
+ * performed (if it has been asked). This boolean keep track of if the
+ * seek arised.
+ */
+ let isAwaitingSeek = hasAskedForInitialSeek;
playbackObserver.listen((observation, stopListening) => {
- if (!observation.seeking &&
+ if (hasAskedForInitialSeek && observation.seeking) {
+ isAwaitingSeek = false;
+ return;
+ }
+ if (!isAwaitingSeek &&
+ !observation.seeking &&
observation.rebuffering === null &&
observation.readyState >= 1)
{
@@ -138,6 +182,16 @@ export default function performInitialSeekAndPlay(
initialPlayPerformed.finish();
deregisterCancellation();
return resolveAutoPlay({ type: "skipped" as const });
+ } else if (mediaElement.ended) {
+ // the video has ended state to true, executing VideoElement.play() will
+ // restart the video from the start, which is not wanted in most cases.
+ // returning "skipped" prevents the call to play() and fix the issue
+ log.warn("Init: autoplay is enabled but the video is ended. " +
+ "Skipping autoplay to prevent video to start again");
+ initialPlayPerformed.setValue(true);
+ initialPlayPerformed.finish();
+ deregisterCancellation();
+ return resolveAutoPlay({ type: "skipped" as const });
}
let playResult : Promise;
diff --git a/src/core/segment_buffers/implementations/audio_video/audio_video_segment_buffer.ts b/src/core/segment_buffers/implementations/audio_video/audio_video_segment_buffer.ts
index 70f4de37fc..4ce658a7db 100644
--- a/src/core/segment_buffers/implementations/audio_video/audio_video_segment_buffer.ts
+++ b/src/core/segment_buffers/implementations/audio_video/audio_video_segment_buffer.ts
@@ -376,7 +376,15 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer {
err :
new Error("An unknown error occured when doing operations " +
"on the SourceBuffer");
- this._pendingTask.reject(error);
+ const task = this._pendingTask;
+ if (task.type === SegmentBufferOperation.Push &&
+ task.data.length === 0 &&
+ task.inventoryData !== null)
+ {
+ this._segmentInventory.insertChunk(task.inventoryData, false);
+ }
+ this._pendingTask = null;
+ task.reject(error);
}
}
@@ -429,7 +437,7 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer {
switch (task.type) {
case SegmentBufferOperation.Push:
if (task.inventoryData !== null) {
- this._segmentInventory.insertChunk(task.inventoryData);
+ this._segmentInventory.insertChunk(task.inventoryData, true);
}
break;
case SegmentBufferOperation.EndOfSegment:
diff --git a/src/core/segment_buffers/implementations/image/image_segment_buffer.ts b/src/core/segment_buffers/implementations/image/image_segment_buffer.ts
index d89d834b07..176d35fb96 100644
--- a/src/core/segment_buffers/implementations/image/image_segment_buffer.ts
+++ b/src/core/segment_buffers/implementations/image/image_segment_buffer.ts
@@ -83,7 +83,7 @@ export default class ImageSegmentBuffer extends SegmentBuffer {
try {
this._buffered.insert(startTime, endTime);
if (infos.inventoryInfos !== null) {
- this._segmentInventory.insertChunk(infos.inventoryInfos);
+ this._segmentInventory.insertChunk(infos.inventoryInfos, true);
}
} catch (err) {
return Promise.reject(err);
diff --git a/src/core/segment_buffers/implementations/text/html/__tests__/utils.test.ts b/src/core/segment_buffers/implementations/text/html/__tests__/utils.test.ts
index 697f38a8e5..2e9c2b8299 100644
--- a/src/core/segment_buffers/implementations/text/html/__tests__/utils.test.ts
+++ b/src/core/segment_buffers/implementations/text/html/__tests__/utils.test.ts
@@ -261,6 +261,21 @@ describe("HTML Text buffer utils - areNearlyEqual", () => {
it("should return true if input number are equals", () => {
expect(areNearlyEqual(5, 5)).toBe(true);
});
+ it(
+ "should return false if input number are not nearly equals with delta parameter",
+ () => {
+ expect(areNearlyEqual(5, 5.1, 0.02)).toBe(false);
+ });
+ it(
+ "should return true if input number are nearly equals with delta parameter",
+ () => {
+ expect(areNearlyEqual(5, 5.01, 0.02)).toBe(true);
+ });
+ it(
+ "should return true if input number are equals with delta parameter",
+ () => {
+ expect(areNearlyEqual(5, 5, 0.02)).toBe(true);
+ });
});
describe("HTML Text buffer utils - removeCuesInfosBetween", () => {
diff --git a/src/core/segment_buffers/implementations/text/html/html_text_segment_buffer.ts b/src/core/segment_buffers/implementations/text/html/html_text_segment_buffer.ts
index 069a021f10..cb1b3985c6 100644
--- a/src/core/segment_buffers/implementations/text/html/html_text_segment_buffer.ts
+++ b/src/core/segment_buffers/implementations/text/html/html_text_segment_buffer.ts
@@ -302,7 +302,7 @@ export default class HTMLTextSegmentBuffer extends SegmentBuffer {
}
if (infos.inventoryInfos !== null) {
- this._segmentInventory.insertChunk(infos.inventoryInfos);
+ this._segmentInventory.insertChunk(infos.inventoryInfos, true);
}
this._buffer.insert(cues, start, end);
this._buffered.insert(start, end);
diff --git a/src/core/segment_buffers/implementations/text/html/text_track_cues_store.ts b/src/core/segment_buffers/implementations/text/html/text_track_cues_store.ts
index c73670fb4f..16d2942039 100644
--- a/src/core/segment_buffers/implementations/text/html/text_track_cues_store.ts
+++ b/src/core/segment_buffers/implementations/text/html/text_track_cues_store.ts
@@ -26,6 +26,23 @@ import {
removeCuesInfosBetween,
} from "./utils";
+/**
+ * first or last IHTMLCue in a group can have a slighlty different start
+ * or end time than the start or end time of the ICuesGroup due to parsing
+ * approximation.
+ * DELTA_CUES_GROUP defines the tolerance level when comparing the start/end
+ * of a IHTMLCue to the start/end of a ICuesGroup.
+ * Having this value too high may lead to have unwanted subtitle displayed
+ * Having this value too low may lead to have subtitles not displayed
+ */
+const DELTA_CUES_GROUP = 1e-3;
+
+/**
+ * segment_duration / RELATIVE_DELTA_RATIO = relative_delta
+ *
+ * relative_delta is the tolerance to determine if two segements are the same
+ */
+const RELATIVE_DELTA_RATIO = 5;
/**
* Manage the buffer of the HTMLTextSegmentBuffer.
* Allows to add, remove and recuperate cues at given times.
@@ -72,6 +89,19 @@ export default class TextTrackCuesStore {
ret.push(cues[j].element);
}
}
+ // first or last IHTMLCue in a group can have a slighlty different start
+ // or end time than the start or end time of the ICuesGroup due to parsing
+ // approximation.
+ // Add a tolerance of 1ms to fix this issue
+ if (ret.length === 0 && cues.length > 0) {
+ for (let j = 0; j < cues.length; j++) {
+ if (areNearlyEqual(time, cues[j].start, DELTA_CUES_GROUP)
+ || areNearlyEqual(time, cues[j].end, DELTA_CUES_GROUP)
+ ) {
+ ret.push(cues[j].element);
+ }
+ }
+ }
return ret;
}
}
@@ -163,6 +193,11 @@ export default class TextTrackCuesStore {
insert(cues : IHTMLCue[], start : number, end : number) : void {
const cuesBuffer = this._cuesBuffer;
const cuesInfosToInsert = { start, end, cues };
+ // it's preferable to have a delta depending on the duration of the segment
+ // if the delta is one fifth of the length of the segment:
+ // a segment of [0, 2] is the "same" segment as [0, 2.1]
+ // but [0, 0.04] is not the "same" segement as [0,04, 0.08]
+ const relativeDelta = Math.abs(start - end) / RELATIVE_DELTA_RATIO;
/**
* Called when we found the index of the next cue relative to the cue we
@@ -175,7 +210,7 @@ export default class TextTrackCuesStore {
function onIndexOfNextCueFound(indexOfNextCue : number) : void {
const nextCue = cuesBuffer[indexOfNextCue];
if (nextCue === undefined || // no cue
- areNearlyEqual(cuesInfosToInsert.end, nextCue.end)) // samey end
+ areNearlyEqual(cuesInfosToInsert.end, nextCue.end, relativeDelta)) // samey end
{
// ours: |AAAAA|
// the current one: |BBBBB|
@@ -210,8 +245,8 @@ export default class TextTrackCuesStore {
for (let cueIdx = 0; cueIdx < cuesBuffer.length; cueIdx++) {
let cuesInfos = cuesBuffer[cueIdx];
if (start < cuesInfos.end) {
- if (areNearlyEqual(start, cuesInfos.start)) {
- if (areNearlyEqual(end, cuesInfos.end)) {
+ if (areNearlyEqual(start, cuesInfos.start, relativeDelta)) {
+ if (areNearlyEqual(end, cuesInfos.end, relativeDelta)) {
// exact same segment
// ours: |AAAAA|
// the current one: |BBBBB|
@@ -257,7 +292,7 @@ export default class TextTrackCuesStore {
// - add ours before the current one
cuesBuffer.splice(cueIdx, 0, cuesInfosToInsert);
return;
- } else if (areNearlyEqual(end, cuesInfos.start)) {
+ } else if (areNearlyEqual(end, cuesInfos.start, relativeDelta)) {
// our cue goes just before the current one:
// ours: |AAAAAAA|
// the current one: |BBBB|
@@ -268,7 +303,7 @@ export default class TextTrackCuesStore {
cuesInfos.start = end;
cuesBuffer.splice(cueIdx, 0, cuesInfosToInsert);
return;
- } else if (areNearlyEqual(end, cuesInfos.end)) {
+ } else if (areNearlyEqual(end, cuesInfos.end, relativeDelta)) {
// ours: |AAAAAAA|
// the current one: |BBBB|
// Result: |AAAAAAA|
@@ -297,7 +332,7 @@ export default class TextTrackCuesStore {
}
// else -> start > cuesInfos.start
- if (areNearlyEqual(cuesInfos.end, end)) {
+ if (areNearlyEqual(cuesInfos.end, end, relativeDelta)) {
// ours: |AAAAAA|
// the current one: |BBBBBBBB|
// Result: |BBAAAAAA|
@@ -333,6 +368,22 @@ export default class TextTrackCuesStore {
}
}
}
+
+ if (cuesBuffer.length) {
+ const lastCue = cuesBuffer[cuesBuffer.length - 1];
+ if (areNearlyEqual(lastCue.end, start, relativeDelta)) {
+ // Match the end of the previous cue to the start of the following one
+ // if they are close enough. If there is a small gap between two segments
+ // it can lead to having no subtitles for a short time, this is noticeable when
+ // two successive segments displays the same text, making it diseappear
+ // and reappear quickly, which gives the impression of blinking
+ //
+ // ours: |AAAAA|
+ // the current one: |BBBBB|...
+ // Result: |BBBBBBBAAAAA|
+ lastCue.end = start;
+ }
+ }
// no cues group has the end after our current start.
// These cues should be the last one
cuesBuffer.push(cuesInfosToInsert);
diff --git a/src/core/segment_buffers/implementations/text/html/utils.ts b/src/core/segment_buffers/implementations/text/html/utils.ts
index 82e4ee280e..285e61c6a5 100644
--- a/src/core/segment_buffers/implementations/text/html/utils.ts
+++ b/src/core/segment_buffers/implementations/text/html/utils.ts
@@ -50,6 +50,21 @@ import {
* Setting a value too high might lead to two segments targeting different times
* to be wrongly believed to target the same time. In worst case scenarios, this
* could lead to wanted text tracks being removed.
+ *
+ * When comparing 2 segments s1 and s2, you may want to take into account the duration
+ * of the segments:
+ * - if s1 is [0, 2] and s2 is [0, 2.1] s1 and s2 can be considered as nearly equal as
+ * there is a relative difference of: (2.1-2) / 2 = 5%;
+ * Formula: (end_s1 - end_s2) / duration_s2 = relative_difference
+ * - if s1 is [0, 0.04] and s2 is [0.04, 0.08] s1 and s2 may not considered as nearly
+ * equal as there is a relative difference of: (0.04-0.08) / 0.04 = 100%
+ *
+ * To compare relatively to the duration of a segment you can provide and additional
+ * parameter "delta" that remplace MAX_DELTA_BUFFER_TIME.
+ * If parameter "delta" is higher than MAX_DELTA_BUFFER_TIME, MAX_DELTA_BUFFER_TIME
+ * is used instead of delta. This ensure that segments are nearly equal when comparing
+ * relatively AND absolutely.
+ *
* @type Number
*/
const MAX_DELTA_BUFFER_TIME = 0.2;
@@ -58,10 +73,12 @@ const MAX_DELTA_BUFFER_TIME = 0.2;
* @see MAX_DELTA_BUFFER_TIME
* @param {Number} a
* @param {Number} b
+ * @param {Number} delta
* @returns {Boolean}
*/
-export function areNearlyEqual(a : number, b : number) : boolean {
- return Math.abs(a - b) <= MAX_DELTA_BUFFER_TIME;
+export function areNearlyEqual(
+ a : number, b : number, delta: number = MAX_DELTA_BUFFER_TIME) : boolean {
+ return Math.abs(a - b) <= Math.min(delta, MAX_DELTA_BUFFER_TIME);
}
/**
diff --git a/src/core/segment_buffers/implementations/text/native/native_text_segment_buffer.ts b/src/core/segment_buffers/implementations/text/native/native_text_segment_buffer.ts
index a67d3caac5..ba8d26a59d 100644
--- a/src/core/segment_buffers/implementations/text/native/native_text_segment_buffer.ts
+++ b/src/core/segment_buffers/implementations/text/native/native_text_segment_buffer.ts
@@ -187,7 +187,7 @@ export default class NativeTextSegmentBuffer extends SegmentBuffer {
}
this._buffered.insert(start, end);
if (infos.inventoryInfos !== null) {
- this._segmentInventory.insertChunk(infos.inventoryInfos);
+ this._segmentInventory.insertChunk(infos.inventoryInfos, true);
}
} catch (err) {
return Promise.reject(err);
diff --git a/src/core/segment_buffers/implementations/types.ts b/src/core/segment_buffers/implementations/types.ts
index 39cc1abaa4..ebb612e58e 100644
--- a/src/core/segment_buffers/implementations/types.ts
+++ b/src/core/segment_buffers/implementations/types.ts
@@ -169,10 +169,15 @@ export abstract class SegmentBuffer {
* This methods allow to manually trigger a synchronization. It should be
* called before retrieving Segment information from it (e.g. with
* `getInventory`).
+ * @param {boolean} [skipLog] - This method may trigger a voluminous debug
+ * log once synchronization is finished if debug logs are enabled.
+ * As this method might be called very often in some specific debugging
+ * situations, setting this value to `true` allows to prevent the call from
+ * triggering a log.
*/
- public synchronizeInventory() : void {
+ public synchronizeInventory(skipLog? : boolean) : void {
// The default implementation just use the SegmentInventory
- this._segmentInventory.synchronizeBuffered(this.getBufferedRanges());
+ this._segmentInventory.synchronizeBuffered(this.getBufferedRanges(), skipLog);
}
/**
diff --git a/src/core/segment_buffers/index.ts b/src/core/segment_buffers/index.ts
index 462a852b8c..5458352498 100644
--- a/src/core/segment_buffers/index.ts
+++ b/src/core/segment_buffers/index.ts
@@ -28,6 +28,7 @@ import {
SegmentBufferOperation,
} from "./implementations";
import {
+ ChunkStatus,
IBufferedChunk,
IChunkContext,
IInsertedChunkInfos,
@@ -40,6 +41,7 @@ import SegmentBuffersStore, {
export default SegmentBuffersStore;
export {
BufferGarbageCollector,
+ ChunkStatus,
ISegmentBufferOptions,
ITextTrackSegmentBufferOptions,
diff --git a/src/core/segment_buffers/inventory/index.ts b/src/core/segment_buffers/inventory/index.ts
index ebfe99a227..93bf5ef395 100644
--- a/src/core/segment_buffers/inventory/index.ts
+++ b/src/core/segment_buffers/inventory/index.ts
@@ -15,12 +15,14 @@
*/
import SegmentInventory, {
+ ChunkStatus,
IBufferedChunk,
IInsertedChunkInfos,
} from "./segment_inventory";
export default SegmentInventory;
export {
+ ChunkStatus,
IBufferedChunk,
IInsertedChunkInfos,
};
diff --git a/src/core/segment_buffers/inventory/segment_inventory.ts b/src/core/segment_buffers/inventory/segment_inventory.ts
index 5dfda903e4..72e9c48788 100644
--- a/src/core/segment_buffers/inventory/segment_inventory.ts
+++ b/src/core/segment_buffers/inventory/segment_inventory.ts
@@ -28,6 +28,27 @@ import BufferedHistory, {
} from "./buffered_history";
import { 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 `Complete`.
+ */
+ PartiallyPushed = 0,
+ /** This chunk corresponds to a fully-loaded segment. */
+ Complete = 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 {
@@ -82,14 +103,10 @@ export interface IBufferedChunk {
/** Information on what that chunk actually contains. */
infos : IChunkContext;
/**
- * If `true`, this chunk is only a partial chunk of a whole segment.
- *
- * Inversely, if `false`, this chunk is a whole segment whose inner chunks
- * have all been fully pushed.
- * In that condition, the `start` and `end` properties refer to that fully
- * pushed segment.
+ * Status of this chunk.
+ * @see ChunkStatus
*/
- partiallyPushed : boolean;
+ status : ChunkStatus;
/**
* If `true`, the segment as a whole is divided into multiple parts in the
* buffer, with other segment(s) between them.
@@ -187,8 +204,13 @@ export default class SegmentInventory {
* at a time, so each `synchronizeBuffered` call should be given a TimeRanges
* coming from the same buffer.
* @param {TimeRanges} buffered
+ * @param {boolean|undefined} [skipLog=false] - This method normally may
+ * trigger a voluminous debug log if debug logs are enabled.
+ * As this method might be called very often in some specific debugging
+ * situations, setting this value to `true` allows to prevent the call from
+ * triggering a log.
*/
- public synchronizeBuffered(buffered : TimeRanges) : void {
+ public synchronizeBuffered(buffered : TimeRanges, skipLog : boolean = false) : void {
const inventory = this._inventory;
let inventoryIndex = 0; // Current index considered.
let thisSegment = inventory[0]; // Current segmentInfos considered
@@ -244,7 +266,11 @@ export default class SegmentInventory {
log.debug(`SI: ${numberOfSegmentToDelete} segments GCed.`, bufferType);
const removed = inventory.splice(indexBefore, numberOfSegmentToDelete);
for (const seg of removed) {
- if (seg.bufferedStart === undefined && seg.bufferedEnd === undefined) {
+ if (
+ seg.bufferedStart === undefined &&
+ seg.bufferedEnd === undefined &&
+ seg.status !== ChunkStatus.Failed
+ ) {
this._bufferedHistory.addBufferedSegment(seg.infos, null);
}
}
@@ -318,12 +344,16 @@ export default class SegmentInventory {
bufferType, inventoryIndex, inventory.length);
const removed = inventory.splice(inventoryIndex, inventory.length - inventoryIndex);
for (const seg of removed) {
- if (seg.bufferedStart === undefined && seg.bufferedEnd === undefined) {
+ if (
+ seg.bufferedStart === undefined &&
+ seg.bufferedEnd === undefined &&
+ seg.status !== ChunkStatus.Failed
+ ) {
this._bufferedHistory.addBufferedSegment(seg.infos, null);
}
}
}
- if (bufferType !== undefined && log.hasLevel("DEBUG")) {
+ if (!skipLog && bufferType !== undefined && log.hasLevel("DEBUG")) {
log.debug(`SI: current ${bufferType} inventory timeline:\n` +
prettyPrintInventory(this._inventory));
}
@@ -343,7 +373,8 @@ export default class SegmentInventory {
segment,
chunkSize,
start,
- end } : IInsertedChunkInfos
+ end } : IInsertedChunkInfos,
+ succeed: boolean
) : void {
if (segment.isInit) {
return;
@@ -357,7 +388,8 @@ export default class SegmentInventory {
}
const inventory = this._inventory;
- const newSegment = { partiallyPushed: true,
+ const newSegment = { status: succeed ? ChunkStatus.PartiallyPushed :
+ ChunkStatus.Failed,
chunkSize,
splitted: false,
start,
@@ -565,7 +597,7 @@ export default class SegmentInventory {
// ===> : |--|====|-|
log.warn("SI: Segment pushed is contained in a previous one",
bufferType, start, end, segmentI.start, segmentI.end);
- const nextSegment = { partiallyPushed: segmentI.partiallyPushed,
+ const nextSegment = { status: segmentI.status,
/**
* Note: this sadly means we're doing as if
* that chunk is present two times.
@@ -743,7 +775,9 @@ export default class SegmentInventory {
this._inventory.splice(firstI + 1, length);
i -= length;
}
- this._inventory[firstI].partiallyPushed = false;
+ if (this._inventory[firstI].status === ChunkStatus.PartiallyPushed) {
+ this._inventory[firstI].status = ChunkStatus.Complete;
+ }
this._inventory[firstI].chunkSize = segmentSize;
this._inventory[firstI].end = lastEnd;
this._inventory[firstI].bufferedEnd = lastBufferedEnd;
@@ -760,8 +794,11 @@ export default class SegmentInventory {
this.synchronizeBuffered(newBuffered);
for (const seg of resSegments) {
if (seg.bufferedStart !== undefined && seg.bufferedEnd !== undefined) {
- this._bufferedHistory.addBufferedSegment(seg.infos, { start: seg.bufferedStart,
- end: seg.bufferedEnd });
+ if (seg.status !== ChunkStatus.Failed) {
+ this._bufferedHistory.addBufferedSegment(seg.infos,
+ { start: seg.bufferedStart,
+ end: seg.bufferedEnd });
+ }
} else {
log.debug("SI: buffered range not known after sync. Skipping history.",
seg.start,
@@ -810,7 +847,7 @@ function bufferedStartLooksCoherent(
thisSegment : IBufferedChunk
) : boolean {
if (thisSegment.bufferedStart === undefined ||
- thisSegment.partiallyPushed)
+ thisSegment.status !== ChunkStatus.Complete)
{
return false;
}
@@ -837,7 +874,7 @@ function bufferedEndLooksCoherent(
thisSegment : IBufferedChunk
) : boolean {
if (thisSegment.bufferedEnd === undefined ||
- thisSegment.partiallyPushed)
+ thisSegment.status !== ChunkStatus.Complete)
{
return false;
}
diff --git a/src/core/stream/orchestrator/stream_orchestrator.ts b/src/core/stream/orchestrator/stream_orchestrator.ts
index b2ffa609c0..3d4fe90720 100644
--- a/src/core/stream/orchestrator/stream_orchestrator.ts
+++ b/src/core/stream/orchestrator/stream_orchestrator.ts
@@ -108,7 +108,8 @@ export default function StreamOrchestrator(
wantedBufferAhead,
maxVideoBufferSize } = options;
- const { MAXIMUM_MAX_BUFFER_AHEAD,
+ const { MINIMUM_MAX_BUFFER_AHEAD,
+ MAXIMUM_MAX_BUFFER_AHEAD,
MAXIMUM_MAX_BUFFER_BEHIND } = config.getCurrent();
// Keep track of a unique BufferGarbageCollector created per
@@ -116,12 +117,8 @@ export default function StreamOrchestrator(
const garbageCollectors =
new WeakMapMemory((segmentBuffer : SegmentBuffer) => {
const { bufferType } = segmentBuffer;
- const defaultMaxBehind = MAXIMUM_MAX_BUFFER_BEHIND[bufferType] != null ?
- MAXIMUM_MAX_BUFFER_BEHIND[bufferType] as number :
- Infinity;
- const defaultMaxAhead = MAXIMUM_MAX_BUFFER_AHEAD[bufferType] != null ?
- MAXIMUM_MAX_BUFFER_AHEAD[bufferType] as number :
- Infinity;
+ const defaultMaxBehind = MAXIMUM_MAX_BUFFER_BEHIND[bufferType] ?? Infinity;
+ const maxAheadHigherBound = MAXIMUM_MAX_BUFFER_AHEAD[bufferType] ?? Infinity;
return (gcCancelSignal : CancellationSignal) => {
BufferGarbageCollector(
{ segmentBuffer,
@@ -130,10 +127,11 @@ export default function StreamOrchestrator(
(val) =>
Math.min(val, defaultMaxBehind),
gcCancelSignal),
- maxBufferAhead: createMappedReference(maxBufferAhead,
- (val) =>
- Math.min(val, defaultMaxAhead),
- gcCancelSignal) },
+ maxBufferAhead: createMappedReference(maxBufferAhead, (val) => {
+ const lowerBound = Math.max(val,
+ MINIMUM_MAX_BUFFER_AHEAD[bufferType] ?? 0);
+ return Math.min(lowerBound, maxAheadHigherBound);
+ }, gcCancelSignal) },
gcCancelSignal
);
};
@@ -194,6 +192,7 @@ export default function StreamOrchestrator(
manifest.getNextPeriod(time);
if (nextPeriod === undefined) {
log.warn("Stream: The wanted position is not found in the Manifest.");
+ enableOutOfBoundsCheck = true;
return;
}
launchConsecutiveStreamsForPeriod(nextPeriod);
@@ -271,7 +270,7 @@ export default function StreamOrchestrator(
/**
* React to a Manifest's decipherability updates.
- * @param {Array.}
+ * @param {Array.} updates
* @returns {Promise}
*/
async function onDecipherabilityUpdates(
@@ -438,9 +437,14 @@ export default function StreamOrchestrator(
// Stop current PeriodStream when the current position goes over the end of
// that Period.
playbackObserver.listen(({ position }, stopListeningObservations) => {
- if (basePeriod.end !== undefined &&
- (position.pending ?? position.last) >= basePeriod.end)
- {
+ const wantedPosition = position.pending ?? position.last;
+ if (basePeriod.end !== undefined && wantedPosition >= basePeriod.end) {
+ const nextPeriod = manifest.getPeriodAfter(basePeriod);
+
+ // Handle special wantedPosition === basePeriod.end cases
+ if (basePeriod.containsTime(wantedPosition, nextPeriod)) {
+ return;
+ }
log.info("Stream: Destroying PeriodStream as the current playhead moved above it",
bufferType,
basePeriod.start,
diff --git a/src/core/stream/representation/utils/get_buffer_status.ts b/src/core/stream/representation/utils/get_buffer_status.ts
index 87ee3a6e63..1565eca59a 100644
--- a/src/core/stream/representation/utils/get_buffer_status.ts
+++ b/src/core/stream/representation/utils/get_buffer_status.ts
@@ -23,6 +23,7 @@ import Manifest, {
import isNullOrUndefined from "../../../../utils/is_null_or_undefined";
import { IReadOnlyPlaybackObserver } from "../../../api";
import SegmentBuffersStore, {
+ ChunkStatus,
IBufferedChunk,
IEndOfSegmentOperation,
SegmentBuffer,
@@ -153,7 +154,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;
@@ -221,7 +222,7 @@ function getRangeOfNeededSegments(
SegmentBuffersStore.isNative(content.adaptation.type) &&
initialWantedTime >= lastIndexPosition &&
representationIndex.isInitialized() &&
- representationIndex.isFinished() &&
+ !representationIndex.isStillAwaitingFutureSegments() &&
isPeriodTheCurrentAndLastOne(manifest, period, initialWantedTime))
{
wantedStartPosition = lastIndexPosition - 1;
@@ -233,7 +234,7 @@ function getRangeOfNeededSegments(
let hasReachedPeriodEnd;
if (!representation.index.isInitialized() ||
- !representation.index.isFinished() ||
+ representation.index.isStillAwaitingFutureSegments() ||
period.end === undefined)
{
hasReachedPeriodEnd = false;
@@ -272,7 +273,8 @@ function isPeriodTheCurrentAndLastOne(
period : Period,
time : number
) : boolean {
- return period.containsTime(time) &&
+ const nextPeriod = manifest.getPeriodAfter(period);
+ return period.containsTime(time, nextPeriod) &&
manifest.isLastPeriodKnown &&
period.id === manifest.periods[manifest.periods.length - 1]?.id;
}
@@ -299,7 +301,7 @@ function getPlayableBufferedSegments(
const eltInventory = segmentInventory[i];
const { representation } = eltInventory.infos;
- if (!eltInventory.partiallyPushed &&
+ if (eltInventory.status === ChunkStatus.Complete &&
representation.decipherable !== false &&
representation.isSupported)
{
diff --git a/src/core/stream/representation/utils/get_needed_segments.ts b/src/core/stream/representation/utils/get_needed_segments.ts
index c139532f14..a2681a8cae 100644
--- a/src/core/stream/representation/utils/get_needed_segments.ts
+++ b/src/core/stream/representation/utils/get_needed_segments.ts
@@ -493,8 +493,8 @@ function doesEndSeemGarbageCollected(
currentSeg.end - currentSeg.bufferedEnd > MAX_TIME_MISSING_FROM_COMPLETE_SEGMENT)
{
log.info("Stream: The end of the wanted segment has been garbage collected",
- currentSeg.start,
- currentSeg.bufferedStart);
+ currentSeg.end,
+ currentSeg.bufferedEnd);
return true;
}
diff --git a/src/default_config.ts b/src/default_config.ts
index 2693d4b2f4..448e415159 100644
--- a/src/default_config.ts
+++ b/src/default_config.ts
@@ -201,6 +201,23 @@ const DEFAULT_CONFIG = {
} as Partial>,
/* eslint-enable @typescript-eslint/consistent-type-assertions */
+ /* eslint-disable @typescript-eslint/consistent-type-assertions */
+ /**
+ * Minimum possible buffer ahead for each type of buffer, to avoid Garbage
+ * Collecting too much data when it would have adverse effects.
+ * Equal to `0` if not defined here.
+ * @type {Object}
+ */
+ MINIMUM_MAX_BUFFER_AHEAD: {
+ // Text segments are both much lighter on resources and might
+ // actually be much larger than other types of segments in terms
+ // of duration. Let's make an exception here by authorizing a
+ // larger text buffer ahead, to avoid unnecesarily reloading the
+ // same text track.
+ text: 2 * 60,
+ } as Partial>,
+ /* eslint-enable @typescript-eslint/consistent-type-assertions */
+
/* eslint-disable @typescript-eslint/consistent-type-assertions */
/**
* Maximum possible buffer behind for each type of buffer, to avoid too much
diff --git a/src/manifest/manifest.ts b/src/manifest/manifest.ts
index 11cd1344e7..c18e078543 100644
--- a/src/manifest/manifest.ts
+++ b/src/manifest/manifest.ts
@@ -410,10 +410,14 @@ export default class Manifest extends EventEmitter {
* @returns {Object|undefined}
*/
public getPeriodForTime(time : number) : Period | undefined {
- return arrayFind(this.periods, (period) => {
- return time >= period.start &&
- (period.end === undefined || period.end > time);
- });
+ let nextPeriod = null;
+ for (let i = this.periods.length - 1; i >= 0; i--) {
+ const period = this.periods[i];
+ if (period.containsTime(time, nextPeriod)) {
+ return period;
+ }
+ nextPeriod = period;
+ }
}
/**
diff --git a/src/manifest/period.ts b/src/manifest/period.ts
index ff11856760..4bf791aa23 100644
--- a/src/manifest/period.ts
+++ b/src/manifest/period.ts
@@ -189,10 +189,23 @@ export default class Period {
/**
* Returns true if the give time is in the time boundaries of this `Period`.
* @param {number} time
+ * @param {object|null} nextPeriod - Period coming chronologically just
+ * after in the same Manifest. `null` if this instance is the last `Period`.
* @returns {boolean}
*/
- containsTime(time : number) : boolean {
- return time >= this.start && (this.end === undefined ||
- time < this.end);
+ containsTime(time : number, nextPeriod : Period | null) : boolean {
+ if (time >= this.start && (this.end === undefined || time < this.end)) {
+ return true;
+ } else if (time === this.end && (nextPeriod === null ||
+ nextPeriod.start > this.end))
+ {
+ // The last possible timed position of a Period is ambiguous as it is
+ // frequently in common with the start of the next one: is it part of
+ // the current or of the next Period?
+ // Here we only consider it part of the current Period if it is the
+ // only one with that position.
+ return true;
+ }
+ return false;
}
}
diff --git a/src/manifest/representation_index/static.ts b/src/manifest/representation_index/static.ts
index 4b47ccc4b5..0835e7cb99 100644
--- a/src/manifest/representation_index/static.ts
+++ b/src/manifest/representation_index/static.ts
@@ -137,8 +137,8 @@ export default class StaticRepresentationIndex implements IRepresentationIndex {
/**
* @returns {Boolean}
*/
- isFinished() : true {
- return true;
+ isStillAwaitingFutureSegments() : false {
+ return false;
}
/**
diff --git a/src/manifest/representation_index/types.ts b/src/manifest/representation_index/types.ts
index ca9ad29fa1..46611d75a5 100644
--- a/src/manifest/representation_index/types.ts
+++ b/src/manifest/representation_index/types.ts
@@ -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
@@ -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
diff --git a/src/parsers/manifest/dash/common/__tests__/manifest_bounds_calculator.test.ts b/src/parsers/manifest/dash/common/__tests__/manifest_bounds_calculator.test.ts
index 724d935dff..cd5a56722a 100644
--- a/src/parsers/manifest/dash/common/__tests__/manifest_bounds_calculator.test.ts
+++ b/src/parsers/manifest/dash/common/__tests__/manifest_bounds_calculator.test.ts
@@ -18,39 +18,46 @@ import ManifestBoundsCalculator from "../manifest_bounds_calculator";
describe("DASH parsers - ManifestBoundsCalculator", () => {
/* eslint-disable max-len */
- it("should return undefined through `estimateMinimumBound` if the live edge was never set for a dynamic content with a timeShiftBufferDepth", () => {
+ it("should return undefined through `getEstimatedMinimumSegmentTime` if the live edge was never set for a dynamic content with a timeShiftBufferDepth", () => {
/* eslint-enable max-len */
const manifestBoundsCalculator = new ManifestBoundsCalculator({
isDynamic: true,
timeShiftBufferDepth: 5,
+ availabilityStartTime: 0,
+ serverTimestampOffset: undefined,
});
- expect(manifestBoundsCalculator.estimateMinimumBound()).toEqual(undefined);
- expect(manifestBoundsCalculator.estimateMinimumBound()).toEqual(undefined);
- expect(manifestBoundsCalculator.estimateMinimumBound()).toEqual(undefined);
+ expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(undefined);
+ expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(undefined);
+ expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(undefined);
});
/* eslint-disable max-len */
- it("should return 0 through `estimateMinimumBound` if the live edge was never set for a static content", () => {
+ it("should return 0 through `getEstimatedMinimumSegmentTime` for a static content", () => {
/* eslint-enable max-len */
const manifestBoundsCalculator = new ManifestBoundsCalculator({
isDynamic: false,
timeShiftBufferDepth: 5,
+ availabilityStartTime: 0,
+ serverTimestampOffset: 555555,
});
- expect(manifestBoundsCalculator.estimateMinimumBound()).toEqual(0);
- expect(manifestBoundsCalculator.estimateMinimumBound()).toEqual(0);
- expect(manifestBoundsCalculator.estimateMinimumBound()).toEqual(0);
+ expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(0);
+ manifestBoundsCalculator.setLastPosition(5555, 2135);
+ expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(0);
+ expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(0);
});
/* eslint-disable max-len */
- it("should return 0 through `estimateMinimumBound` if the live edge was never set for a dynamic content with no timeShiftBufferDepth", () => {
+ it("should return 0 through `getEstimatedMinimumSegmentTime` if the `serverTimestampOffset` was never set nor the last position for a dynamic content with no timeShiftBufferDepth", () => {
/* eslint-enable max-len */
const manifestBoundsCalculator = new ManifestBoundsCalculator({
isDynamic: false,
timeShiftBufferDepth: undefined,
+ availabilityStartTime: 0,
+ serverTimestampOffset: undefined,
});
- expect(manifestBoundsCalculator.estimateMinimumBound()).toEqual(0);
- expect(manifestBoundsCalculator.estimateMinimumBound()).toEqual(0);
- expect(manifestBoundsCalculator.estimateMinimumBound()).toEqual(0);
+ expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(0);
+ expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(0);
+ expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(0);
});
/* eslint-disable max-len */
@@ -59,9 +66,11 @@ describe("DASH parsers - ManifestBoundsCalculator", () => {
const manifestBoundsCalculator = new ManifestBoundsCalculator({
isDynamic: true,
timeShiftBufferDepth: 5,
+ availabilityStartTime: 0,
+ serverTimestampOffset: undefined,
});
expect(manifestBoundsCalculator.lastPositionIsKnown()).toEqual(false);
- expect(manifestBoundsCalculator.estimateMinimumBound()).toEqual(undefined);
+ expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(undefined);
expect(manifestBoundsCalculator.lastPositionIsKnown()).toEqual(false);
});
@@ -71,6 +80,8 @@ describe("DASH parsers - ManifestBoundsCalculator", () => {
const manifestBoundsCalculator = new ManifestBoundsCalculator({
isDynamic: true,
timeShiftBufferDepth: 5,
+ availabilityStartTime: 0,
+ serverTimestampOffset: undefined,
});
manifestBoundsCalculator.setLastPosition(1000, 0);
expect(manifestBoundsCalculator.lastPositionIsKnown()).toEqual(true);
@@ -82,63 +93,198 @@ describe("DASH parsers - ManifestBoundsCalculator", () => {
const manifestBoundsCalculator = new ManifestBoundsCalculator({
isDynamic: false,
timeShiftBufferDepth: 5,
+ availabilityStartTime: 0,
+ serverTimestampOffset: undefined,
});
manifestBoundsCalculator.setLastPosition(1000, 0);
expect(manifestBoundsCalculator.lastPositionIsKnown()).toEqual(true);
});
/* eslint-disable max-len */
- it("should return how much time has elapsed through `estimateMinimumBound` since the live edge was set for a dynamic content", () => {
+ it("should return how much time has elapsed through `getEstimatedMinimumSegmentTime` since the last position was set for a dynamic content", () => {
/* eslint-enable max-len */
- let date = 5000;
+ let performanceNow = 5000;
const mockPerformanceNow = jest.spyOn(performance, "now")
- .mockImplementation(jest.fn(() => date));
+ .mockImplementation(jest.fn(() => performanceNow));
const manifestBoundsCalculator = new ManifestBoundsCalculator({
isDynamic: true,
timeShiftBufferDepth: 5,
+ availabilityStartTime: 0,
+ serverTimestampOffset: undefined,
});
manifestBoundsCalculator.setLastPosition(1000, 10);
- date = 25000;
- expect(manifestBoundsCalculator.estimateMinimumBound()).toEqual(1010);
- date = 35000;
- expect(manifestBoundsCalculator.estimateMinimumBound()).toEqual(1020);
+ performanceNow = 25000;
+ expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(1010);
+ performanceNow = 35000;
+ expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(1020);
mockPerformanceNow.mockRestore();
});
/* eslint-disable max-len */
- it("should return 0 even when a last position has been set for a static content", () => {
+ it("should prefer relying on the live edge for `getEstimatedMinimumSegmentTime` if it was set", () => {
/* eslint-enable max-len */
- let date = 5000;
+ let performanceNow = 5000;
const mockPerformanceNow = jest.spyOn(performance, "now")
- .mockImplementation(jest.fn(() => date));
+ .mockImplementation(jest.fn(() => performanceNow));
const manifestBoundsCalculator = new ManifestBoundsCalculator({
- isDynamic: false,
- timeShiftBufferDepth: 5,
+ isDynamic: true,
+ timeShiftBufferDepth: 3,
+ availabilityStartTime: 4,
+ serverTimestampOffset: 7000,
});
- manifestBoundsCalculator.setLastPosition(1000, 0);
- date = 25000;
- expect(manifestBoundsCalculator.estimateMinimumBound()).toEqual(0);
- date = 35000;
- expect(manifestBoundsCalculator.estimateMinimumBound()).toEqual(0);
+ manifestBoundsCalculator.setLastPosition(3000, 10);
+ expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime())
+ .toEqual(7 + 5 - 4 - 3);
+ performanceNow = 25000;
+ expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime())
+ .toEqual(7 + 25 - 4 - 3);
+ performanceNow = 35000;
+ manifestBoundsCalculator.setLastPosition(84546464, 5642);
+ expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime())
+ .toEqual(7 + 35 - 4 - 3);
mockPerformanceNow.mockRestore();
});
/* eslint-disable max-len */
it("should authorize and handle multiple `setLastPositionOffset` calls for dynamic contents", () => {
/* eslint-enable max-len */
- let date = 5000;
+ let performanceNow = 5000;
const mockPerformanceNow = jest.spyOn(performance, "now")
- .mockImplementation(jest.fn(() => date));
+ .mockImplementation(jest.fn(() => performanceNow));
const manifestBoundsCalculator = new ManifestBoundsCalculator({
isDynamic: true,
timeShiftBufferDepth: 5,
+ availabilityStartTime: 0,
+ serverTimestampOffset: undefined,
});
manifestBoundsCalculator.setLastPosition(1000, 0);
- date = 50000;
- expect(manifestBoundsCalculator.estimateMinimumBound()).toEqual(1045);
+ performanceNow = 50000;
+ expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(1045);
manifestBoundsCalculator.setLastPosition(0, 0);
- date = 55000;
- expect(manifestBoundsCalculator.estimateMinimumBound()).toEqual(50);
+ performanceNow = 55000;
+ expect(manifestBoundsCalculator.getEstimatedMinimumSegmentTime()).toEqual(50);
+ mockPerformanceNow.mockRestore();
+ });
+
+ /* eslint-disable max-len */
+ it("`getEstimatedMaximumPosition` should be based on the last position on on-dynamic manifest", () => {
+ /* eslint-enable max-len */
+ let performanceNow = 5000;
+ const mockPerformanceNow = jest.spyOn(performance, "now")
+ .mockImplementation(jest.fn(() => performanceNow));
+ const manifestBoundsCalculator1 = new ManifestBoundsCalculator({
+ isDynamic: false,
+ timeShiftBufferDepth: 5,
+ availabilityStartTime: 0,
+ serverTimestampOffset: undefined,
+ });
+ const manifestBoundsCalculator2 = new ManifestBoundsCalculator({
+ isDynamic: false,
+ timeShiftBufferDepth: 5,
+ availabilityStartTime: 0,
+ serverTimestampOffset: 10,
+ });
+ manifestBoundsCalculator1.setLastPosition(1000, 0);
+ manifestBoundsCalculator2.setLastPosition(1000, 0);
+ performanceNow = 50000;
+ expect(manifestBoundsCalculator1.getEstimatedMaximumPosition(10)).toEqual(1000);
+ expect(manifestBoundsCalculator2.getEstimatedMaximumPosition(19)).toEqual(1000);
+ performanceNow = 55000;
+ expect(manifestBoundsCalculator1.getEstimatedMaximumPosition(98)).toEqual(1000);
+ expect(manifestBoundsCalculator2.getEstimatedMaximumPosition(93)).toEqual(1000);
+ manifestBoundsCalculator1.setLastPosition(0, 0);
+ manifestBoundsCalculator2.setLastPosition(0, 0);
+ expect(manifestBoundsCalculator1.getEstimatedMaximumPosition(43)).toEqual(0);
+ expect(manifestBoundsCalculator2.getEstimatedMaximumPosition(421)).toEqual(0);
+ mockPerformanceNow.mockRestore();
+ });
+
+ /* eslint-disable max-len */
+ it("`getEstimatedMaximumPosition` should evolve based on the last position on dynamic manifest without `serverTimestampOffset`", () => {
+ /* eslint-enable max-len */
+ let performanceNow = 5000;
+ const mockPerformanceNow = jest.spyOn(performance, "now")
+ .mockImplementation(jest.fn(() => performanceNow));
+ const manifestBoundsCalculator = new ManifestBoundsCalculator({
+ isDynamic: true,
+ timeShiftBufferDepth: 5,
+ availabilityStartTime: 7,
+ serverTimestampOffset: undefined,
+ });
+ manifestBoundsCalculator.setLastPosition(1050, 0);
+ performanceNow = 50000;
+ expect(manifestBoundsCalculator.getEstimatedMaximumPosition(10)).toEqual(1050 + 50);
+ performanceNow = 55000;
+ expect(manifestBoundsCalculator.getEstimatedMaximumPosition(98)).toEqual(1050 + 55);
+ manifestBoundsCalculator.setLastPosition(0, 10);
+ expect(manifestBoundsCalculator.getEstimatedMaximumPosition(43)).toEqual(0 + 55 - 10);
+ mockPerformanceNow.mockRestore();
+ });
+
+ it("should not return a live edge if `serverTimestampOffset` isn't set", () => {
+ const manifestBoundsCalculator = new ManifestBoundsCalculator({
+ isDynamic: true,
+ timeShiftBufferDepth: 5,
+ availabilityStartTime: 0,
+ serverTimestampOffset: undefined,
+ });
+ manifestBoundsCalculator.setLastPosition(1000, 0);
+ expect(manifestBoundsCalculator.getEstimatedLiveEdge()).toEqual(undefined);
+ });
+
+ it("should not return a live edge if the manifest is not dynamic", () => {
+ const manifestBoundsCalculator = new ManifestBoundsCalculator({
+ isDynamic: false,
+ timeShiftBufferDepth: 5,
+ availabilityStartTime: 0,
+ serverTimestampOffset: 100,
+ });
+ manifestBoundsCalculator.setLastPosition(1000, 0);
+ expect(manifestBoundsCalculator.getEstimatedLiveEdge()).toEqual(undefined);
+ });
+
+ it("should rely on `serverTimestampOffset` to produce live edge if set", () => {
+ let performanceNow = 3000;
+ const mockPerformanceNow = jest.spyOn(performance, "now")
+ .mockImplementation(jest.fn(() => performanceNow));
+ const manifestBoundsCalculator = new ManifestBoundsCalculator({
+ isDynamic: true,
+ timeShiftBufferDepth: 5,
+ availabilityStartTime: 2,
+ serverTimestampOffset: 5000,
+ });
+ expect(manifestBoundsCalculator.getEstimatedLiveEdge()).toEqual(3 + 5 - 2);
+ manifestBoundsCalculator.setLastPosition(1000, 0);
+ expect(manifestBoundsCalculator.getEstimatedLiveEdge()).toEqual(3 + 5 - 2);
+ performanceNow = 9000;
+ expect(manifestBoundsCalculator.getEstimatedLiveEdge()).toEqual(9 + 5 - 2);
+ mockPerformanceNow.mockRestore();
+ });
+
+ /* eslint-disable max-len */
+ it("`getEstimatedMaximumPosition` should evolve based on the live edge position on dynamic manifest with `serverTimestampOffset`", () => {
+ /* eslint-enable max-len */
+ let performanceNow = 5000;
+ const mockPerformanceNow = jest.spyOn(performance, "now")
+ .mockImplementation(jest.fn(() => performanceNow));
+ const manifestBoundsCalculator = new ManifestBoundsCalculator({
+ isDynamic: true,
+ timeShiftBufferDepth: 3,
+ availabilityStartTime: 7,
+ serverTimestampOffset: 1000,
+ });
+ expect(manifestBoundsCalculator.getEstimatedMaximumPosition(4))
+ .toEqual(5 + 1 - 7 + 4);
+ manifestBoundsCalculator.setLastPosition(1050, 0);
+ performanceNow = 70000;
+ expect(manifestBoundsCalculator.getEstimatedMaximumPosition(11))
+ .toEqual(5 + 1 - 7 + 11 + 70 - 5);
+ performanceNow = 85000;
+ expect(manifestBoundsCalculator.getEstimatedMaximumPosition(98))
+ .toEqual(5 + 1 - 7 + 98 + 85 - 5);
+ manifestBoundsCalculator.setLastPosition(0, 10);
+ expect(manifestBoundsCalculator.getEstimatedMaximumPosition(43))
+ .toEqual(5 + 1 - 7 + 43 + 85 - 5);
mockPerformanceNow.mockRestore();
});
});
diff --git a/src/parsers/manifest/dash/common/indexes/base.ts b/src/parsers/manifest/dash/common/indexes/base.ts
index 405bd533cc..e20350fae8 100644
--- a/src/parsers/manifest/dash/common/indexes/base.ts
+++ b/src/parsers/manifest/dash/common/indexes/base.ts
@@ -26,6 +26,7 @@ import {
IIndexSegment,
toIndexTime,
} from "../../../utils/index_helpers";
+import ManifestBoundsCalculator from "../manifest_bounds_calculator";
import getInitSegment from "./get_init_segment";
import getSegmentsFromTimeline from "./get_segments_from_timeline";
import { constructRepresentationUrl } from "./tokens";
@@ -120,6 +121,8 @@ export interface IBaseIndexContextArgument {
representationId? : string | undefined;
/** Bitrate of the Representation concerned. */
representationBitrate? : number | undefined;
+ /** Allows to obtain the minimum and maximum positions of a content. */
+ manifestBoundsCalculator : ManifestBoundsCalculator;
/* Function that tells if an EMSG is whitelisted by the manifest */
isEMSGWhitelisted: (inbandEvent: IEMSG) => boolean;
}
@@ -179,6 +182,9 @@ export default class BaseRepresentationIndex implements IRepresentationIndex {
/** Absolute end of the period, timescaled and converted to index time. */
private _scaledPeriodEnd : number | undefined;
+ /** Allows to obtain the minimum and maximum positions of a content. */
+ private _manifestBoundsCalculator : ManifestBoundsCalculator;
+
/* Function that tells if an EMSG is whitelisted by the manifest */
private _isEMSGWhitelisted: (inbandEvent: IEMSG) => boolean;
@@ -228,6 +234,7 @@ export default class BaseRepresentationIndex implements IRepresentationIndex {
endNumber: index.endNumber,
timeline: index.timeline ?? [],
timescale };
+ this._manifestBoundsCalculator = context.manifestBoundsCalculator;
this._scaledPeriodStart = toIndexTime(periodStart, this._index);
this._scaledPeriodEnd = periodEnd == null ? undefined :
toIndexTime(periodEnd, this._index);
@@ -261,8 +268,9 @@ export default class BaseRepresentationIndex implements IRepresentationIndex {
return getSegmentsFromTimeline(this._index,
from,
dur,
- this._isEMSGWhitelisted,
- this._scaledPeriodEnd);
+ this._manifestBoundsCalculator,
+ this._scaledPeriodEnd,
+ this._isEMSGWhitelisted);
}
/**
@@ -379,8 +387,8 @@ export default class BaseRepresentationIndex implements IRepresentationIndex {
* should become available in the future.
* @returns {Boolean}
*/
- isFinished() : true {
- return true;
+ isStillAwaitingFutureSegments() : false {
+ return false;
}
/**
diff --git a/src/parsers/manifest/dash/common/indexes/get_segments_from_timeline.ts b/src/parsers/manifest/dash/common/indexes/get_segments_from_timeline.ts
index f0839e7ac4..940333bc19 100644
--- a/src/parsers/manifest/dash/common/indexes/get_segments_from_timeline.ts
+++ b/src/parsers/manifest/dash/common/indexes/get_segments_from_timeline.ts
@@ -21,6 +21,7 @@ import {
IIndexSegment,
toIndexTime,
} from "../../../utils/index_helpers";
+import ManifestBoundsCalculator from "../manifest_bounds_calculator";
import { createDashUrlDetokenizer } from "./tokens";
/**
@@ -47,12 +48,14 @@ function getWantedRepeatIndex(
* @param {Object} index - index object, constructed by parsing the manifest.
* @param {number} from - starting timestamp wanted, in seconds
* @param {number} durationWanted - duration wanted, in seconds
+ * @param {Object} manifestBoundsCalculator
+ * @param {number|undefined} scaledPeriodEnd
* @param {function} isEMSGWhitelisted
- * @param {number|undefined} maximumTime
* @returns {Array.}
*/
export default function getSegmentsFromTimeline(
index : { availabilityTimeComplete? : boolean | undefined;
+ availabilityTimeOffset? : number | undefined;
segmentUrlTemplate : string | null;
startNumber? : number | undefined;
endNumber? : number | undefined;
@@ -61,11 +64,16 @@ export default function getSegmentsFromTimeline(
indexTimeOffset : number; },
from : number,
durationWanted : number,
- isEMSGWhitelisted: (inbandEvent: IEMSG) => boolean,
- maximumTime? : number
+ manifestBoundsCalculator : ManifestBoundsCalculator,
+ scaledPeriodEnd : number | undefined,
+ isEMSGWhitelisted: (inbandEvent: IEMSG) => boolean
) : ISegment[] {
+ const maximumTime = manifestBoundsCalculator.getEstimatedMaximumPosition(
+ index.availabilityTimeOffset ?? 0
+ );
+ const wantedMaximum = Math.min(from + durationWanted, maximumTime ?? Infinity);
const scaledUp = toIndexTime(from, index);
- const scaledTo = toIndexTime(from + durationWanted, index);
+ const scaledTo = toIndexTime(wantedMaximum, index);
const { timeline, timescale, segmentUrlTemplate, startNumber, endNumber } = index;
let currentNumber = startNumber ?? 1;
@@ -77,7 +85,13 @@ export default function getSegmentsFromTimeline(
const timelineItem = timeline[i];
const { duration, start, range } = timelineItem;
- const repeat = calculateRepeat(timelineItem, timeline[i + 1], maximumTime);
+ let maxRepeatTime;
+ if (maximumTime === undefined) {
+ maxRepeatTime = scaledPeriodEnd;
+ } else {
+ maxRepeatTime = Math.min(maximumTime * timescale, scaledPeriodEnd ?? Infinity);
+ }
+ const repeat = calculateRepeat(timelineItem, timeline[i + 1], maxRepeatTime);
const complete = index.availabilityTimeComplete !== false ||
i !== timelineLength - 1 &&
repeat !== 0;
diff --git a/src/parsers/manifest/dash/common/indexes/index.ts b/src/parsers/manifest/dash/common/indexes/index.ts
index 97c92ee934..f44be837c7 100644
--- a/src/parsers/manifest/dash/common/indexes/index.ts
+++ b/src/parsers/manifest/dash/common/indexes/index.ts
@@ -14,14 +14,26 @@
* limitations under the License.
*/
-import BaseRepresentationIndex from "./base";
-import ListRepresentationIndex from "./list";
-import TemplateRepresentationIndex from "./template";
-import TimelineRepresentationIndex from "./timeline";
+import BaseRepresentationIndex, {
+ IBaseIndexContextArgument,
+} from "./base";
+import ListRepresentationIndex, {
+ IListIndexContextArgument,
+} from "./list";
+import TemplateRepresentationIndex, {
+ ITemplateIndexContextArgument,
+} from "./template";
+import TimelineRepresentationIndex, {
+ ITimelineIndexContextArgument,
+} from "./timeline";
export {
BaseRepresentationIndex,
ListRepresentationIndex,
TemplateRepresentationIndex,
TimelineRepresentationIndex,
+ IBaseIndexContextArgument,
+ IListIndexContextArgument,
+ ITemplateIndexContextArgument,
+ ITimelineIndexContextArgument,
};
diff --git a/src/parsers/manifest/dash/common/indexes/list.ts b/src/parsers/manifest/dash/common/indexes/list.ts
index a73ce5dbce..3eb6f6ef7f 100644
--- a/src/parsers/manifest/dash/common/indexes/list.ts
+++ b/src/parsers/manifest/dash/common/indexes/list.ts
@@ -314,8 +314,8 @@ export default class ListRepresentationIndex implements IRepresentationIndex {
/**
* @returns {Boolean}
*/
- isFinished() : true {
- return true;
+ isStillAwaitingFutureSegments() : false {
+ return false;
}
/**
diff --git a/src/parsers/manifest/dash/common/indexes/template.ts b/src/parsers/manifest/dash/common/indexes/template.ts
index 191935e22f..dae6842c79 100644
--- a/src/parsers/manifest/dash/common/indexes/template.ts
+++ b/src/parsers/manifest/dash/common/indexes/template.ts
@@ -118,9 +118,15 @@ export interface ITemplateIndexIndexArgument {
/** Aditional context needed by a SegmentTemplate RepresentationIndex. */
export interface ITemplateIndexContextArgument {
- aggressiveMode : boolean;
- /** Minimum availabilityTimeOffset concerning the segments of this Representation. */
- availabilityTimeOffset : number;
+ /**
+ * availability time offset of the concerned Adaptation.
+ *
+ * If `undefined`, the corresponding property was not set in the MPD and it is
+ * thus assumed to be equal to `0`.
+ * It might however be semantically different than `0` in the RxPlayer as it
+ * means that the packager didn't include that information in the MPD.
+ */
+ availabilityTimeOffset : number | undefined;
/** Allows to obtain the minimum and maximum positions of a content. */
manifestBoundsCalculator : ManifestBoundsCalculator;
/** Start of the period concerned by this RepresentationIndex, in seconds. */
@@ -145,11 +151,6 @@ export interface ITemplateIndexContextArgument {
export default class TemplateRepresentationIndex implements IRepresentationIndex {
/** Underlying structure to retrieve segment information. */
private _index : ITemplateIndex;
- /**
- * Whether the "aggressiveMode" is enabled. If enabled, segments can be
- * requested in advance.
- */
- private _aggressiveMode : boolean;
/** Retrieve the maximum and minimum position of the whole content. */
private _manifestBoundsCalculator : ManifestBoundsCalculator;
/** Absolute start of the Period, in seconds. */
@@ -171,8 +172,7 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex
index : ITemplateIndexIndexArgument,
context : ITemplateIndexContextArgument
) {
- const { aggressiveMode,
- availabilityTimeOffset,
+ const { availabilityTimeOffset,
manifestBoundsCalculator,
isDynamic,
periodEnd,
@@ -185,7 +185,6 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex
this._availabilityTimeOffset = availabilityTimeOffset;
this._manifestBoundsCalculator = manifestBoundsCalculator;
- this._aggressiveMode = aggressiveMode;
const presentationTimeOffset = index.presentationTimeOffset != null ?
index.presentationTimeOffset :
0;
@@ -389,14 +388,26 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex
const { timescale } = this._index;
const segmentTimeRounding = getSegmentTimeRoundingError(timescale);
const scaledPeriodStart = this._periodStart * timescale;
- const scaledRelativeEnd = end * timescale - scaledPeriodStart;
+ const scaledRelativeStart = (start * timescale) - scaledPeriodStart;
+ const scaledRelativeEnd = (end * timescale) - scaledPeriodStart;
+ const lastSegmentStart = this._getLastSegmentStart();
+ if (isNullOrUndefined(lastSegmentStart)) {
+ const relativeScaledIndexEnd = this._estimateRelativeScaledEnd();
+ if (relativeScaledIndexEnd === undefined) {
+ return scaledRelativeEnd + segmentTimeRounding >= 0;
+ }
+ return scaledRelativeEnd + segmentTimeRounding >= 0 &&
+ scaledRelativeStart < relativeScaledIndexEnd - segmentTimeRounding;
+
+ }
+ const lastSegmentEnd = lastSegmentStart + this._index.duration;
const relativeScaledIndexEnd = this._estimateRelativeScaledEnd();
if (relativeScaledIndexEnd === undefined) {
- return (scaledRelativeEnd + segmentTimeRounding) >= 0;
+ return scaledRelativeEnd > lastSegmentEnd - segmentTimeRounding;
}
- const scaledRelativeStart = start * timescale - scaledPeriodStart;
- return (scaledRelativeStart - segmentTimeRounding) < relativeScaledIndexEnd;
+ return scaledRelativeEnd > lastSegmentEnd - segmentTimeRounding &&
+ scaledRelativeStart < relativeScaledIndexEnd - segmentTimeRounding;
}
/**
@@ -446,20 +457,20 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex
}
/**
- * 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 {
if (!this._isDynamic) {
- return true;
+ return false;
}
const scaledRelativeIndexEnd = this._estimateRelativeScaledEnd();
if (scaledRelativeIndexEnd === undefined) {
- return false;
+ return true;
}
const { timescale } = this._index;
@@ -468,11 +479,11 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex
// As last segment start is null if live time is before
// current period, consider the index not to be finished.
if (isNullOrUndefined(lastSegmentStart)) {
- return false;
+ return true;
}
const lastSegmentEnd = lastSegmentStart + this._index.duration;
const segmentTimeRounding = getSegmentTimeRoundingError(timescale);
- return (lastSegmentEnd + segmentTimeRounding) >= scaledRelativeIndexEnd;
+ return (lastSegmentEnd + segmentTimeRounding) < scaledRelativeIndexEnd;
}
/**
@@ -487,7 +498,6 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex
*/
_replace(newIndex : TemplateRepresentationIndex) : void {
this._index = newIndex._index;
- this._aggressiveMode = newIndex._aggressiveMode;
this._isDynamic = newIndex._isDynamic;
this._periodStart = newIndex._periodStart;
this._scaledRelativePeriodEnd = newIndex._scaledRelativePeriodEnd;
@@ -520,8 +530,11 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex
// /!\ The scaled max position augments continuously and might not
// reflect exactly the real server-side value. As segments are
// generated discretely.
- const maximumBound = this._manifestBoundsCalculator.estimateMaximumBound();
- if (maximumBound !== undefined && maximumBound < this._periodStart) {
+ const maximumSegmentTime =
+ this._manifestBoundsCalculator.getEstimatedMaximumPosition(
+ this._availabilityTimeOffset ?? 0
+ );
+ if (maximumSegmentTime !== undefined && maximumSegmentTime < this._periodStart) {
// Maximum position is before this period.
// No segment is yet available here
return null;
@@ -529,7 +542,7 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex
}
const { duration, timescale } = this._index;
- const firstPosition = this._manifestBoundsCalculator.estimateMinimumBound();
+ const firstPosition = this._manifestBoundsCalculator.getEstimatedMinimumSegmentTime();
if (firstPosition === undefined) {
return undefined;
}
@@ -551,15 +564,12 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex
const { duration, timescale, endNumber, startNumber = 1 } = this._index;
if (this._isDynamic) {
- const lastPos = this._manifestBoundsCalculator.estimateMaximumBound();
- if (lastPos === undefined) {
- return undefined;
- }
- const agressiveModeOffset = this._aggressiveMode ? (duration / timescale) :
- 0;
- if (this._scaledRelativePeriodEnd !== undefined &&
- this._scaledRelativePeriodEnd <
- (lastPos + agressiveModeOffset - this._periodStart) * this._index.timescale) {
+ const liveEdge = this._manifestBoundsCalculator.getEstimatedLiveEdge();
+ if (liveEdge !== undefined &&
+ this._scaledRelativePeriodEnd !== undefined &&
+ this._scaledRelativePeriodEnd < liveEdge - (this._periodStart *
+ this._index.timescale))
+ {
let numberOfSegments = Math.ceil(this._scaledRelativePeriodEnd / duration);
if (endNumber !== undefined && (endNumber - startNumber + 1) < numberOfSegments) {
@@ -567,10 +577,16 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex
}
return (numberOfSegments - 1) * duration;
}
+ const lastPosition = this._manifestBoundsCalculator
+ .getEstimatedMaximumPosition(this._availabilityTimeOffset ?? 0);
+ if (lastPosition === undefined) {
+ return undefined;
+ }
+
// /!\ The scaled last position augments continuously and might not
// reflect exactly the real server-side value. As segments are
// generated discretely.
- const scaledLastPosition = (lastPos - this._periodStart) * timescale;
+ const scaledLastPosition = (lastPosition - this._periodStart) * timescale;
// Maximum position is before this period.
// No segment is yet available here
@@ -578,13 +594,7 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex
return null;
}
- const availabilityTimeOffset =
- ((this._availabilityTimeOffset !== undefined ? this._availabilityTimeOffset : 0) +
- agressiveModeOffset) * timescale;
-
- let numberOfSegmentsAvailable =
- Math.floor((scaledLastPosition + availabilityTimeOffset) / duration);
-
+ let numberOfSegmentsAvailable = Math.floor(scaledLastPosition / duration);
if (endNumber !== undefined &&
(endNumber - startNumber + 1) < numberOfSegmentsAvailable) {
numberOfSegmentsAvailable = endNumber - startNumber + 1;
diff --git a/src/parsers/manifest/dash/common/indexes/timeline/index.ts b/src/parsers/manifest/dash/common/indexes/timeline/index.ts
index d059c0bfef..05367c2fc3 100644
--- a/src/parsers/manifest/dash/common/indexes/timeline/index.ts
+++ b/src/parsers/manifest/dash/common/indexes/timeline/index.ts
@@ -14,5 +14,8 @@
* limitations under the License.
*/
-import TimelineRepresentationIndex from "./timeline_representation_index";
+import TimelineRepresentationIndex, {
+ ITimelineIndexContextArgument,
+} from "./timeline_representation_index";
export default TimelineRepresentationIndex;
+export { ITimelineIndexContextArgument };
diff --git a/src/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.ts b/src/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.ts
index 0d105ee865..e176e2535b 100644
--- a/src/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.ts
+++ b/src/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.ts
@@ -33,7 +33,6 @@ import {
IIndexSegment,
toIndexTime,
} from "../../../../utils/index_helpers";
-import isSegmentStillAvailable from "../../../../utils/is_segment_still_available";
import updateSegmentTimeline from "../../../../utils/update_segment_timeline";
import { ISegmentTimelineElement } from "../../../node_parser_types";
import ManifestBoundsCalculator from "../../manifest_bounds_calculator";
@@ -53,6 +52,8 @@ import constructTimelineFromPreviousTimeline from "./construct_timeline_from_pre
export interface ITimelineIndex {
/** If `false`, the last segment anounced might be still incomplete. */
availabilityTimeComplete : boolean;
+ /** Minimum availabilityTimeOffset concerning the segments of this Representation. */
+ availabilityTimeOffset : number;
/** Byte range for a possible index of segments in the server. */
indexRange?: [number, number] | undefined;
/**
@@ -151,8 +152,25 @@ export interface ITimelineIndexIndexArgument {
/** Aditional context needed by a SegmentTimeline RepresentationIndex. */
export interface ITimelineIndexContextArgument {
- /** If `false`, the last segment anounced might be still incomplete. */
- availabilityTimeComplete : boolean;
+ /**
+ * If `false`, declared segments in the MPD might still be not completely generated.
+ * If `true`, they are completely generated.
+ *
+ * If `undefined`, the corresponding property was not set in the MPD and it is
+ * thus assumed that they are all generated.
+ * It might however be semantically different than `true` in the RxPlayer as it
+ * means that the packager didn't include that information in the MPD.
+ */
+ availabilityTimeComplete : boolean | undefined;
+ /**
+ * availability time offset of the concerned Adaptation.
+ *
+ * If `undefined`, the corresponding property was not set in the MPD and it is
+ * thus assumed to be equal to `0`.
+ * It might however be semantically different than `0` in the RxPlayer as it
+ * means that the packager didn't include that information in the MPD.
+ */
+ availabilityTimeOffset : number | undefined;
/** Allows to obtain the minimum and maximum positions of a content. */
manifestBoundsCalculator : ManifestBoundsCalculator;
/** Start of the period linked to this RepresentationIndex, in seconds. */
@@ -196,6 +214,11 @@ export interface ILastSegmentInformation {
time : number;
}
+/**
+ * `IRepresentationIndex` implementation for a DASH `SegmentTimeline` segment
+ * indexing scheme.
+ * @class TimelineRepresentationIndex
+ */
export default class TimelineRepresentationIndex implements IRepresentationIndex {
/** Underlying structure to retrieve segment information. */
protected _index : ITimelineIndex;
@@ -250,6 +273,7 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex
"TimelineRepresentationIndex.");
}
const { availabilityTimeComplete,
+ availabilityTimeOffset,
manifestBoundsCalculator,
isDynamic,
isLastPeriod,
@@ -298,7 +322,29 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex
const segmentUrlTemplate = index.media === undefined ?
null :
constructRepresentationUrl(index.media, representationId, representationBitrate);
- this._index = { availabilityTimeComplete,
+
+ let actualAvailabilityTimeOffset;
+ // Technically, it seems (although it is not clear) that an MPD may contain
+ // future segments and it's the job of a player to not request segments later
+ // than the time at which they should be available.
+ // In practice, we don't do that for various reasons: precision issues,
+ // various DASH spec interpretations by packagers and players...
+ //
+ // So as a compromise, if nothing in the MPD indicates that future segments
+ // may be announced (see code below), we will act as if ALL segments in this
+ // TimelineRepresentationIndex are requestable
+ if (
+ availabilityTimeOffset === undefined &&
+ availabilityTimeComplete === undefined
+ ) {
+ actualAvailabilityTimeOffset = Infinity; // Meaning: we can request
+ // everything in the index
+ } else {
+ actualAvailabilityTimeOffset = availabilityTimeOffset ?? 0;
+ }
+
+ this._index = { availabilityTimeComplete: availabilityTimeComplete ?? true,
+ availabilityTimeOffset: actualAvailabilityTimeOffset,
indexRange: index.indexRange,
indexTimeOffset,
initialization: index.initialization == null ?
@@ -316,6 +362,7 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex
index.startNumber,
index.endNumber),
timescale };
+
this._scaledPeriodStart = toIndexTime(periodStart, this._index);
this._scaledPeriodEnd = periodEnd === undefined ? undefined :
toIndexTime(periodEnd, this._index);
@@ -356,8 +403,9 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex
indexTimeOffset },
from,
duration,
- this._isEMSGWhitelisted,
- this._scaledPeriodEnd);
+ this._manifestBoundsCalculator,
+ this._scaledPeriodEnd,
+ this._isEMSGWhitelisted);
}
/**
@@ -399,10 +447,19 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex
if (this._index.timeline === null) {
this._index.timeline = this._getTimeline();
}
- const lastTime = TimelineRepresentationIndex.getIndexEnd(this._index.timeline,
- this._scaledPeriodEnd);
- return lastTime === null ? null :
- fromIndexTime(lastTime, this._index);
+
+ const lastReqSegInfo = getLastRequestableSegmentInfo(
+ // Needed typecast for TypeScript
+ this._index as typeof this._index & { timeline: IIndexSegment[] },
+ this._manifestBoundsCalculator,
+ this._scaledPeriodEnd
+ );
+ if (lastReqSegInfo === null) {
+ return null;
+ }
+ const lastScaledPosition = Math.min(lastReqSegInfo.end,
+ this._scaledPeriodEnd ?? Infinity);
+ return fromIndexTime(lastScaledPosition, this._index);
}
/**
@@ -411,10 +468,23 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex
* @returns {number|null|undefined}
*/
getEnd(): number | undefined | null {
- if (!this._isDynamic || !this._isLastPeriod) { // @see isFinished
- return this.getLastAvailablePosition();
+ if (this._isDynamic && !this._isLastPeriod) {
+ return undefined;
+ }
+
+ this._refreshTimeline();
+ if (this._index.timeline === null) {
+ this._index.timeline = this._getTimeline();
+ }
+ if (this._index.timeline.length <= 0) {
+ return null;
}
- return undefined;
+ const lastSegment = this._index.timeline[this._index.timeline.length - 1];
+ const lastTime = Math.min(getIndexSegmentEnd(lastSegment,
+ null,
+ this._scaledPeriodEnd),
+ this._scaledPeriodEnd ?? Infinity);
+ return fromIndexTime(lastTime, this._index);
}
/**
@@ -430,33 +500,67 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex
*/
awaitSegmentBetween(start: number, end: number): boolean | undefined {
assert(start <= end);
- if (!this._isDynamic || !this._isLastPeriod) {
- return false;
+ if (!this._isDynamic) {
+ return false; // No segment will be newly available in the future
}
+
this._refreshTimeline();
if (this._index.timeline === null) {
this._index.timeline = this._getTimeline();
}
- const { timeline, timescale } = this._index;
+ const { timescale, timeline } = this._index;
const segmentTimeRounding = getSegmentTimeRoundingError(timescale);
- const scaledEnd = toIndexTime(end, this._index);
- if (timeline.length > 0) {
- const lastTimelineElement = timeline[timeline.length - 1];
- const lastSegmentEnd = getIndexSegmentEnd(lastTimelineElement,
+ const scaledWantedEnd = toIndexTime(end, this._index);
+ const lastReqSegInfo = getLastRequestableSegmentInfo(
+ // Needed typecast for TypeScript
+ this._index as typeof this._index & { timeline: IIndexSegment[] },
+ this._manifestBoundsCalculator,
+ this._scaledPeriodEnd
+ );
+ if (lastReqSegInfo !== null) {
+ const lastReqSegmentEnd = Math.min(lastReqSegInfo.end,
+ this._scaledPeriodEnd ?? Infinity);
+ const roundedReqSegmentEnd = lastReqSegmentEnd + segmentTimeRounding;
+ if (roundedReqSegmentEnd >= Math.min(scaledWantedEnd,
+ this._scaledPeriodEnd ?? Infinity))
+ {
+ return false; // everything up to that point is already requestable
+ }
+ }
+
+ const scaledWantedStart = toIndexTime(start, this._index);
+ if (timeline.length > 0 &&
+ lastReqSegInfo !== null &&
+ !lastReqSegInfo.isLastOfTimeline)
+ {
+ // There are some future segments already anounced in the MPD
+
+ const lastSegment = timeline[timeline.length - 1];
+ const lastSegmentEnd = getIndexSegmentEnd(lastSegment,
null,
this._scaledPeriodEnd);
- const roundedEnd = lastSegmentEnd + segmentTimeRounding;
- if (roundedEnd >= Math.min(scaledEnd, this._scaledPeriodEnd ?? Infinity)) {
- return false; // already loaded
+ const roundedLastSegEnd = lastSegmentEnd + segmentTimeRounding;
+ if (scaledWantedStart < roundedLastSegEnd + segmentTimeRounding) {
+ return true; // The MPD's timeline already contains one such element,
+ // It is just not requestable yet
}
}
+
+ if (!this._isLastPeriod) {
+ // Let's consider - perhaps wrongly, that Periods which aren't the last
+ // one have all of their segments announced.
+ return false;
+ }
+
if (this._scaledPeriodEnd === undefined) {
- return (scaledEnd + segmentTimeRounding) > this._scaledPeriodStart ? undefined :
- false;
+ return (scaledWantedEnd + segmentTimeRounding) > this._scaledPeriodStart ?
+ undefined : // There may be future segments at this point
+ false; // Before the current Period
}
- const scaledStart = toIndexTime(start, this._index);
- return (scaledStart - segmentTimeRounding) < this._scaledPeriodEnd &&
- (scaledEnd + segmentTimeRounding) > this._scaledPeriodStart;
+
+ // `true` if within the boundaries of this Period. `false` otherwise.
+ return (scaledWantedStart - segmentTimeRounding) < this._scaledPeriodEnd &&
+ (scaledWantedEnd + segmentTimeRounding) > this._scaledPeriodStart;
}
/**
@@ -475,8 +579,13 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex
if (this._index.timeline === null) {
this._index.timeline = this._getTimeline();
}
- const { timeline, timescale, indexTimeOffset } = this._index;
- return isSegmentStillAvailable(segment, timeline, timescale, indexTimeOffset);
+ return isSegmentStillAvailable(segment,
+ // Needed typecast for TypeScript
+ this._index as typeof this._index & {
+ timeline: IIndexSegment[];
+ },
+ this._manifestBoundsCalculator,
+ this._scaledPeriodEnd);
}
/**
@@ -546,6 +655,8 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex
if (hasReplaced) {
this._index.startNumber = newIndex._index.startNumber;
}
+ this._index.availabilityTimeOffset = newIndex._index.availabilityTimeOffset;
+ this._index.availabilityTimeComplete = newIndex._index.availabilityTimeComplete;
this._index.endNumber = newIndex._index.endNumber;
this._isDynamic = newIndex._isDynamic;
this._scaledPeriodStart = newIndex._scaledPeriodStart;
@@ -555,35 +666,80 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex
}
/**
- * Returns `true` if this RepresentationIndex currently contains its last
+ * Returns `false` if this RepresentationIndex currently contains its last
* segment.
- * Returns `false` if it's still pending.
+ * Returns `true` if it's still pending.
* @returns {Boolean}
*/
- isFinished() : boolean {
- if (!this._isDynamic || !this._isLastPeriod) {
- // Either the content is not dynamic, in which case no new segment will
- // be generated, either it is but this index is not linked to the current
- // last Period in the MPD, in which case it is inferred that it has been
- // completely generated. Note that this second condition might break very
- // very rare use cases where old Periods are still being generated, yet it
- // should fix more cases than it breaks.
- return true;
+ isStillAwaitingFutureSegments() : boolean {
+ if (!this._isDynamic) {
+ return false;
}
+ this._refreshTimeline();
if (this._index.timeline === null) {
this._index.timeline = this._getTimeline();
}
+
const { timeline } = this._index;
- if (this._scaledPeriodEnd === undefined || timeline.length === 0) {
- return false;
+ if (timeline.length === 0) {
+ // No segment announced in this Period
+ if (this._scaledPeriodEnd !== undefined) {
+ const liveEdge = this._manifestBoundsCalculator.getEstimatedLiveEdge();
+ if (liveEdge !== undefined &&
+ toIndexTime(liveEdge, this._index) > this._scaledPeriodEnd)
+ {
+ // This Period is over, we're not awaiting anything
+ return false;
+ }
+ }
+ // Let's just consider that we're awaiting only for when this is the last Period.
+ return this._isLastPeriod;
}
- const lastTimelineElement = timeline[timeline.length - 1];
- const lastTime = getIndexSegmentEnd(lastTimelineElement,
- null,
- this._scaledPeriodEnd);
+
const segmentTimeRounding = getSegmentTimeRoundingError(this._index.timescale);
- return (lastTime + segmentTimeRounding) >= this._scaledPeriodEnd;
+ const lastReqSegInfo = getLastRequestableSegmentInfo(
+ // Needed typecast for TypeScript
+ this._index as typeof this._index & { timeline: IIndexSegment[] },
+ this._manifestBoundsCalculator,
+ this._scaledPeriodEnd
+ );
+
+ if (lastReqSegInfo !== null && !lastReqSegInfo.isLastOfTimeline) {
+ // There might be non-yet requestable segments in the manifest
+ const lastReqSegmentEnd = Math.min(lastReqSegInfo.end,
+ this._scaledPeriodEnd ?? Infinity);
+ if (this._scaledPeriodEnd !== undefined &&
+ lastReqSegmentEnd + segmentTimeRounding >= this._scaledPeriodEnd)
+ {
+ // The last requestable segment ends after the end of the Period anyway
+ return false;
+ }
+ return true; // There are not-yet requestable segments
+ }
+
+ if (!this._isLastPeriod) {
+ // This index is not linked to the current last Period in the MPD, in
+ // which case it is inferred that all segments have been announced.
+ //
+ // Note that this condition might break very very rare use cases where old
+ // Periods are still being generated, yet it should fix more cases than it
+ // breaks.
+ return false;
+ }
+
+ if (this._scaledPeriodEnd === undefined) {
+ // This is the last Period of a dynamic content whose end is unknown.
+ // Just return true.
+ return true;
+ }
+ const lastSegment = timeline[timeline.length - 1];
+ const lastSegmentEnd = getIndexSegmentEnd(lastSegment,
+ null,
+ this._scaledPeriodEnd);
+ // We're awaiting future segments only if the current end is before the end
+ // of the Period
+ return (lastSegmentEnd + segmentTimeRounding) < this._scaledPeriodEnd;
}
/**
@@ -615,7 +771,8 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex
if (!this._isDynamic) {
return;
}
- const firstPosition = this._manifestBoundsCalculator.estimateMinimumBound();
+ const firstPosition = this._manifestBoundsCalculator
+ .getEstimatedMinimumSegmentTime();
if (firstPosition == null) {
return; // we don't know yet
}
@@ -629,17 +786,6 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex
}
}
- static getIndexEnd(timeline : IIndexSegment[],
- scaledPeriodEnd : number | undefined) : number | null {
- if (timeline.length <= 0) {
- return null;
- }
- return Math.min(getIndexSegmentEnd(timeline[timeline.length - 1],
- null,
- scaledPeriodEnd),
- scaledPeriodEnd ?? Infinity);
- }
-
/**
* Allows to generate the "timeline" for this RepresentationIndex.
* Call this function when the timeline is unknown.
@@ -741,3 +887,163 @@ function updateTimelineFromEndNumber(
}
return timeline;
}
+
+/**
+ * Returns true if a Segment returned by the corresponding index is still
+ * considered available.
+ * Returns false if it is not available anymore.
+ * Returns undefined if we cannot know whether it is still available or not.
+ * /!\ We do not check the mediaURLs of the segment.
+ * @param {Object} segment
+ * @param {Object} index
+ * @param {Object} manifestBoundsCalculator
+ * @param {number|undefined} scaledPeriodEnd
+ * @returns {Boolean|undefined}
+ */
+export function isSegmentStillAvailable(
+ segment : ISegment,
+ index: { availabilityTimeOffset : number;
+ timeline : IIndexSegment[];
+ indexTimeOffset: number;
+ timescale : number; },
+ manifestBoundsCalculator : ManifestBoundsCalculator,
+ scaledPeriodEnd : number | undefined
+) : boolean | undefined {
+ const lastReqSegInfo = getLastRequestableSegmentInfo(index,
+ manifestBoundsCalculator,
+ scaledPeriodEnd);
+ if (lastReqSegInfo === null) {
+ return false;
+ }
+
+ for (let i = 0; i < index.timeline.length; i++) {
+ if (lastReqSegInfo.timelineIdx < i) {
+ return false;
+ }
+ const tSegment = index.timeline[i];
+ const tSegmentTime = (tSegment.start - index.indexTimeOffset) / index.timescale;
+ if (tSegmentTime > segment.time) {
+ return false; // We went over it without finding it
+ } else if (tSegmentTime === segment.time) {
+ if (tSegment.range === undefined) {
+ return segment.range === undefined;
+ }
+ return segment.range != null &&
+ tSegment.range[0] === segment.range[0] &&
+ tSegment.range[1] === segment.range[1];
+ } else { // tSegment.start < segment.time
+ if (tSegment.repeatCount >= 0 && tSegment.duration !== undefined) {
+ const timeDiff = tSegmentTime - tSegment.start;
+ const repeat = (timeDiff / tSegment.duration) - 1;
+ return repeat % 1 === 0 && repeat <= lastReqSegInfo.newRepeatCount;
+ }
+ }
+ }
+ return false;
+}
+
+/**
+ * Returns from the given RepresentationIndex information on the last segment
+ * that may be requested currently.
+ *
+ * Returns `null` if there's no such segment.
+ * @param {Object} index
+ * @param {Object} manifestBoundsCalculator
+ * @param {number|undefined} scaledPeriodEnd
+ * @returns {number|null}
+ */
+export function getLastRequestableSegmentInfo(
+ index: { availabilityTimeOffset : number;
+ timeline : IIndexSegment[];
+ timescale : number; },
+ manifestBoundsCalculator : ManifestBoundsCalculator,
+ scaledPeriodEnd : number | undefined
+) : ILastRequestableSegmentInfo | null {
+ if (index.timeline.length <= 0) {
+ return null;
+ }
+
+ if (index.availabilityTimeOffset === Infinity) {
+ // availabilityTimeOffset to Infinity == Everything is requestable in the timeline.
+ const lastIndex = index.timeline.length - 1;
+ const lastElem = index.timeline[lastIndex];
+ return { isLastOfTimeline: true,
+ timelineIdx: lastIndex,
+ newRepeatCount: lastElem.repeatCount,
+ end: getIndexSegmentEnd(lastElem, null, scaledPeriodEnd) };
+ }
+
+ const adjustedMaxSeconds = manifestBoundsCalculator.getEstimatedMaximumPosition(
+ index.availabilityTimeOffset
+ );
+ if (adjustedMaxSeconds === undefined) {
+ const lastIndex = index.timeline.length - 1;
+ const lastElem = index.timeline[lastIndex];
+ return { isLastOfTimeline: true,
+ timelineIdx: lastIndex,
+ newRepeatCount: lastElem.repeatCount,
+ end: getIndexSegmentEnd(lastElem, null, scaledPeriodEnd) };
+ }
+ for (let i = index.timeline.length - 1; i >= index.timeline.length; i--) {
+ const element = index.timeline[i];
+ const endOfFirstOccurence = element.start + element.duration;
+ if (fromIndexTime(endOfFirstOccurence, index) <= adjustedMaxSeconds) {
+ const endTime = getIndexSegmentEnd(element, index.timeline[i + 1], scaledPeriodEnd);
+ if (fromIndexTime(endTime, index) <= adjustedMaxSeconds) {
+ return { isLastOfTimeline: i === index.timeline.length - 1,
+ timelineIdx: i,
+ newRepeatCount: element.repeatCount,
+ end: endOfFirstOccurence };
+ } else {
+ // We have to find the right repeatCount
+ const maxIndexTime = toIndexTime(adjustedMaxSeconds, index);
+ const diffToSegStart = maxIndexTime - element.start;
+ const nbOfSegs = Math.floor(diffToSegStart / element.duration);
+ assert(nbOfSegs >= 1);
+ return { isLastOfTimeline: false,
+ timelineIdx: i,
+ newRepeatCount: nbOfSegs - 1,
+ end: element.start + nbOfSegs * element.duration };
+ }
+ }
+ }
+ return null;
+}
+
+/**
+ * Information on the last requestable segment deduced from a timeline array of
+ * segment information.
+ */
+export interface ILastRequestableSegmentInfo {
+ /**
+ * If `true`, we know that the last requestable segment is equal to the last
+ * segment that can be deduced from the corresponding given timeline.
+ * Written another way, there seem to be no segment announced in the timeline
+ * that are not yet requestable.
+ *
+ * If `false`, we know that the last requestable segment is not the last
+ * segment that can be deduced from the corresponding timeline.
+ * Written another way, there are supplementary segments in the timeline which
+ * are not yet requestable.
+ *
+ * Note that if the last requestable segment has its information from the last
+ * element from the timeline but it's not the last segment that would be
+ * deduced from the `repeatCount` property, then this value is set to `false`.
+ */
+ isLastOfTimeline: boolean;
+ /**
+ * End time at which the last requestable segment ends, in the corresponding
+ * index timescale (__NOT__ in seconds).
+ */
+ end: number;
+ /**
+ * The index in `timeline` of the last requestable segment.
+ * Note that its `repeatCount` may be updated and put as `newRepeatCount`.
+ */
+ timelineIdx: number;
+ /**
+ * The new `repeatCount` value for that last segment. May be equal or
+ * different from the timeline element found at `timelineIdx`.
+ */
+ newRepeatCount: number;
+}
diff --git a/src/parsers/manifest/dash/common/manifest_bounds_calculator.ts b/src/parsers/manifest/dash/common/manifest_bounds_calculator.ts
index cf7a423d7e..2be89900e4 100644
--- a/src/parsers/manifest/dash/common/manifest_bounds_calculator.ts
+++ b/src/parsers/manifest/dash/common/manifest_bounds_calculator.ts
@@ -18,16 +18,6 @@
* This class allows to easily calculate the first and last available positions
* in a content at any time.
*
- * That task can be an hard for dynamic DASH contents: it depends on a
- * `timeShiftBufferDepth` defined in the MPD and on the maximum possible
- * position.
- *
- * The latter can come from either a clock synchronization mechanism or the
- * indexing schemes (e.g. SegmentTemplate, SegmentTimeline etc.) of the last
- * Periods.
- * As such, it might only be known once a large chunk of the MPD has already
- * been parsed.
- *
* By centralizing the manifest bounds calculation in this class and by giving
* an instance of it to each parsed elements which might depend on it, we
* ensure that we can provide it once it is known to every one of those
@@ -35,26 +25,46 @@
* @class ManifestBoundsCalculator
*/
export default class ManifestBoundsCalculator {
- /** Value of MPD@timeShiftBufferDepth. */
+ /**
+ * Value of MPD@timeShiftBufferDepth.
+ * `null` if not defined.
+ */
private _timeShiftBufferDepth : number | null;
+ /**
+ * Value of MPD@availabilityStartTime as an unix timestamp in seconds.
+ * `0` if it wasn't defined.
+ */
+ private _availabilityStartTime : number;
+ /** `true` if MPD@type is equal to "dynamic". */
+ private _isDynamic : boolean;
/** Value of `performance.now` at the time `lastPosition` was calculated. */
private _positionTime : number | undefined;
/** Last position calculated at a given moment (itself indicated by `_positionTime`. */
private _lastPosition : number | undefined;
- /** `true` if MPD@type is equal to "dynamic". */
- private _isDynamic : boolean;
+ /**
+ * Offset to add to `performance.now` to obtain a good estimation of the
+ * server-side unix timestamp.
+ *
+ * `undefined` if unknown.
+ */
+ private _serverTimestampOffset : number | undefined;
/**
* @param {Object} args
*/
- constructor(args : { timeShiftBufferDepth : number | undefined;
- isDynamic : boolean; }
- ) {
+ constructor(args : {
+ availabilityStartTime : number;
+ timeShiftBufferDepth : number | undefined;
+ isDynamic : boolean;
+ serverTimestampOffset: number | undefined;
+ }) {
this._isDynamic = args.isDynamic;
this._timeShiftBufferDepth = !args.isDynamic ||
args.timeShiftBufferDepth === undefined ?
null :
args.timeShiftBufferDepth;
+ this._serverTimestampOffset = args.serverTimestampOffset;
+ this._availabilityStartTime = args.availabilityStartTime;
}
/**
@@ -96,11 +106,12 @@ export default class ManifestBoundsCalculator {
* Consider that it is only an estimation, not the real value.
* @return {number|undefined}
*/
- estimateMinimumBound(): number | undefined {
+ getEstimatedMinimumSegmentTime(): number | undefined {
if (!this._isDynamic || this._timeShiftBufferDepth === null) {
return 0;
}
- const maximumBound = this.estimateMaximumBound();
+ const maximumBound = this.getEstimatedLiveEdge() ??
+ this.getEstimatedMaximumPosition(0);
if (maximumBound === undefined) {
return undefined;
}
@@ -109,15 +120,42 @@ export default class ManifestBoundsCalculator {
}
/**
- * Estimate a maximum bound for the content from the last set segment time.
- * Consider that it is only an estimation, not the real value.
+ * Estimate the segment time in seconds that corresponds to what could be
+ * considered the live edge (or `undefined` for non-live contents).
+ *
+ * Note that for some contents which just anounce segments in advance, this
+ * value might be very different than the maximum position that is
+ * requestable.
* @return {number|undefined}
*/
- estimateMaximumBound() : number | undefined {
- if (this._isDynamic &&
- this._positionTime != null &&
- this._lastPosition != null)
- {
+ getEstimatedLiveEdge() : number | undefined {
+ if (!this._isDynamic || this._serverTimestampOffset === undefined) {
+ return undefined;
+ }
+ return (performance.now() + this._serverTimestampOffset) / 1000 -
+ this._availabilityStartTime;
+ }
+
+ /**
+ * Produce a rough estimate of the ending time of the last requestable segment
+ * in that content.
+ *
+ * This value is only an estimate and may be far from reality.
+ *
+ * The `availabilityTimeOffset` in argument is the corresponding
+ * `availabilityTimeOffset` that applies to the current wanted segment, or `0`
+ * if none exist. It will be applied on live content to deduce the maximum
+ * segment time available.
+ */
+ getEstimatedMaximumPosition(availabilityTimeOffset: number) : number | undefined {
+ if (!this._isDynamic) {
+ return this._lastPosition;
+ }
+
+ const liveEdge = this.getEstimatedLiveEdge();
+ if (liveEdge !== undefined && availabilityTimeOffset !== Infinity) {
+ return liveEdge + availabilityTimeOffset;
+ } else if (this._positionTime !== undefined && this._lastPosition !== undefined) {
return Math.max((this._lastPosition - this._positionTime) +
(performance.now() / 1000),
0);
diff --git a/src/parsers/manifest/dash/common/parse_adaptation_sets.ts b/src/parsers/manifest/dash/common/parse_adaptation_sets.ts
index 2fc070d1cc..5c14eea8de 100644
--- a/src/parsers/manifest/dash/common/parse_adaptation_sets.ts
+++ b/src/parsers/manifest/dash/common/parse_adaptation_sets.ts
@@ -282,12 +282,19 @@ export default function parseAdaptationSets(
roles.some((role) => role.schemeIdUri === "urn:mpeg:dash:role:2011");
const representationsIR = adaptation.children.representations;
+
const availabilityTimeComplete =
adaptation.attributes.availabilityTimeComplete ??
context.availabilityTimeComplete;
- const availabilityTimeOffset =
- (adaptation.attributes.availabilityTimeOffset ?? 0) +
- context.availabilityTimeOffset;
+
+ let availabilityTimeOffset;
+ if (
+ adaptation.attributes.availabilityTimeOffset !== undefined ||
+ context.availabilityTimeOffset !== undefined
+ ) {
+ availabilityTimeOffset = (adaptation.attributes.availabilityTimeOffset ?? 0) +
+ (context.availabilityTimeOffset ?? 0);
+ }
const adaptationMimeType = adaptation.attributes.mimeType;
const adaptationCodecs = adaptation.attributes.codecs;
@@ -317,7 +324,6 @@ export default function parseAdaptationSets(
}
const reprCtxt : IRepresentationContext = {
- aggressiveMode: context.aggressiveMode,
availabilityTimeComplete,
availabilityTimeOffset,
baseURLs: resolveBaseURLs(context.baseURLs, adaptationChildren.baseURLs),
@@ -329,7 +335,6 @@ export default function parseAdaptationSets(
parentSegmentTemplates,
receivedTime: context.receivedTime,
start: context.start,
- timeShiftBufferDepth: context.timeShiftBufferDepth,
unsafelyBaseOnPreviousAdaptation: null,
};
diff --git a/src/parsers/manifest/dash/common/parse_mpd.ts b/src/parsers/manifest/dash/common/parse_mpd.ts
index 03e472826e..4236410cf2 100644
--- a/src/parsers/manifest/dash/common/parse_mpd.ts
+++ b/src/parsers/manifest/dash/common/parse_mpd.ts
@@ -29,6 +29,7 @@ import { IResponseData } from "../parsers_types";
import getClockOffset from "./get_clock_offset";
import getHTTPUTCTimingURL from "./get_http_utc-timing_url";
import getMinimumAndMaximumPositions from "./get_minimum_and_maximum_positions";
+import ManifestBoundsCalculator from "./manifest_bounds_calculator";
import parseAvailabilityStartTime from "./parse_availability_start_time";
import parsePeriods, {
IXLinkInfos,
@@ -250,12 +251,20 @@ function parseCompleteIntermediateRepresentation(
const { externalClockOffset: clockOffset,
unsafelyBaseOnPreviousManifest } = args;
+ const { externalClockOffset } = args;
+ const manifestBoundsCalculator = new ManifestBoundsCalculator({
+ availabilityStartTime,
+ isDynamic,
+ timeShiftBufferDepth,
+ serverTimestampOffset: externalClockOffset,
+ });
const manifestInfos = { aggressiveMode: args.aggressiveMode,
availabilityStartTime,
baseURLs: mpdBaseUrls,
clockOffset,
duration: rootAttributes.duration,
isDynamic,
+ manifestBoundsCalculator,
manifestProfiles: mpdIR.attributes.profiles,
receivedTime: args.manifestReceivedTime,
timeShiftBufferDepth,
@@ -312,31 +321,33 @@ function parseCompleteIntermediateRepresentation(
livePosition: undefined,
time: now };
} else {
- minimumTime = minimumSafePosition;
- timeshiftDepth = timeShiftBufferDepth ?? null;
+ // Determine the maximum seekable position
let finalMaximumSafePosition : number;
- let livePosition;
-
- if (maximumUnsafePosition !== undefined) {
- livePosition = maximumUnsafePosition;
- }
-
if (maximumSafePosition !== undefined) {
finalMaximumSafePosition = maximumSafePosition;
} else {
- const ast = availabilityStartTime ?? 0;
- const { externalClockOffset } = args;
if (externalClockOffset === undefined) {
log.warn("DASH Parser: use system clock to define maximum position");
- finalMaximumSafePosition = (Date.now() / 1000) - ast;
+ finalMaximumSafePosition = (Date.now() / 1000) - availabilityStartTime;
} else {
const serverTime = performance.now() + externalClockOffset;
- finalMaximumSafePosition = (serverTime / 1000) - ast;
+ finalMaximumSafePosition = (serverTime / 1000) - availabilityStartTime;
}
}
+
+ // Determine live edge (what position corresponds to live content, can be
+ // inferior or superior to the maximum anounced position in some specific
+ // scenarios). However, the `timeShiftBufferDepth` should be based on it.
+ let livePosition = manifestBoundsCalculator.getEstimatedLiveEdge();
if (livePosition === undefined) {
- livePosition = finalMaximumSafePosition;
+ if (maximumUnsafePosition !== undefined) {
+ livePosition = maximumUnsafePosition;
+ } else {
+ livePosition = finalMaximumSafePosition;
+ }
+ // manifestBoundsCalculator.forceLiveEdge(livePosition);
}
+
maximumTimeData = { isLinear: true,
maximumSafePosition: finalMaximumSafePosition,
livePosition,
@@ -344,10 +355,12 @@ function parseCompleteIntermediateRepresentation(
// if the minimum calculated time is even below the buffer depth, perhaps we
// can go even lower in terms of depth
+ minimumTime = minimumSafePosition;
+ timeshiftDepth = timeShiftBufferDepth ?? null;
if (timeshiftDepth !== null && minimumTime !== undefined &&
- finalMaximumSafePosition - minimumTime > timeshiftDepth)
+ livePosition - minimumTime > timeshiftDepth)
{
- timeshiftDepth = finalMaximumSafePosition - minimumTime;
+ timeshiftDepth = livePosition - minimumTime;
}
}
diff --git a/src/parsers/manifest/dash/common/parse_periods.ts b/src/parsers/manifest/dash/common/parse_periods.ts
index 8c55e67a3d..3f0c29d6e1 100644
--- a/src/parsers/manifest/dash/common/parse_periods.ts
+++ b/src/parsers/manifest/dash/common/parse_periods.ts
@@ -18,6 +18,7 @@ import log from "../../../../log";
import Manifest from "../../../../manifest";
import flatMap from "../../../../utils/flat_map";
import idGenerator from "../../../../utils/id_generator";
+import isNullOrUndefined from "../../../../utils/is_null_or_undefined";
import objectValues from "../../../../utils/object_values";
import { utf8ToStr } from "../../../../utils/string_parsing";
import {
@@ -33,7 +34,6 @@ import {
// eslint-disable-next-line max-len
import flattenOverlappingPeriods from "./flatten_overlapping_periods";
import getPeriodsTimeInformation from "./get_periods_time_infos";
-import ManifestBoundsCalculator from "./manifest_bounds_calculator";
import parseAdaptationSets, {
IAdaptationSetContext,
} from "./parse_adaptation_sets";
@@ -67,12 +67,9 @@ export default function parsePeriods(
throw new Error("MPD parsing error: the time information are incoherent.");
}
- const { isDynamic,
- timeShiftBufferDepth } = context;
- const manifestBoundsCalculator = new ManifestBoundsCalculator({ isDynamic,
- timeShiftBufferDepth });
+ const { isDynamic, manifestBoundsCalculator } = context;
- if (!isDynamic && context.duration != null) {
+ if (!isDynamic && !isNullOrUndefined(context.duration)) {
manifestBoundsCalculator.setLastPosition(context.duration);
}
@@ -90,7 +87,7 @@ export default function parsePeriods(
periodEnd } = periodsTimeInformation[i];
let periodID : string;
- if (periodIR.attributes.id == null) {
+ if (isNullOrUndefined(periodIR.attributes.id)) {
log.warn("DASH: No usable id found in the Period. Generating one.");
periodID = "gen-dash-period-" + generatePeriodID();
} else {
@@ -108,12 +105,11 @@ export default function parsePeriods(
const unsafelyBaseOnPreviousPeriod = context
.unsafelyBaseOnPreviousManifest?.getPeriod(periodID) ?? null;
- const availabilityTimeComplete = periodIR.attributes.availabilityTimeComplete ?? true;
- const availabilityTimeOffset = periodIR.attributes.availabilityTimeOffset ?? 0;
- const { aggressiveMode, manifestProfiles } = context;
+ const availabilityTimeComplete = periodIR.attributes.availabilityTimeComplete;
+ const availabilityTimeOffset = periodIR.attributes.availabilityTimeOffset;
+ const { manifestProfiles } = context;
const { segmentTemplate } = periodIR.children;
- const adapCtxt : IAdaptationSetContext = { aggressiveMode,
- availabilityTimeComplete,
+ const adapCtxt : IAdaptationSetContext = { availabilityTimeComplete,
availabilityTimeOffset,
baseURLs: periodBaseURLs,
manifestBoundsCalculator,
@@ -124,7 +120,6 @@ export default function parsePeriods(
receivedTime,
segmentTemplate,
start: periodStart,
- timeShiftBufferDepth,
unsafelyBaseOnPreviousPeriod };
const adaptations = parseAdaptationSets(periodIR.children.adaptations, adapCtxt);
@@ -207,7 +202,7 @@ function guessLastPositionFromClock(
context : IPeriodContext,
minimumTime : number
) : [number, number] | undefined {
- if (context.clockOffset != null) {
+ if (!isNullOrUndefined(context.clockOffset)) {
const lastPosition = context.clockOffset / 1000 -
context.availabilityStartTime;
const positionTime = performance.now() / 1000;
@@ -244,7 +239,7 @@ function getMaximumLastPosition(
let maxEncounteredPosition : number | null = null;
let allIndexAreEmpty = true;
const adaptationsVal = objectValues(adaptationsPerType)
- .filter((ada) : ada is IParsedAdaptation[] => ada != null);
+ .filter((ada) : ada is IParsedAdaptation[] => !isNullOrUndefined(ada));
const allAdaptations = flatMap(adaptationsVal,
(adaptationsForType) => adaptationsForType);
for (const adaptation of allAdaptations) {
@@ -255,15 +250,15 @@ function getMaximumLastPosition(
allIndexAreEmpty = false;
if (typeof position === "number") {
maxEncounteredPosition =
- maxEncounteredPosition == null ? position :
- Math.max(maxEncounteredPosition,
- position);
+ isNullOrUndefined(maxEncounteredPosition) ? position :
+ Math.max(maxEncounteredPosition,
+ position);
}
}
}
}
- if (maxEncounteredPosition != null) {
+ if (!isNullOrUndefined(maxEncounteredPosition)) {
return maxEncounteredPosition;
} else if (allIndexAreEmpty) {
return null;
@@ -364,6 +359,5 @@ type IInheritedAdaptationContext = Omit;
diff --git a/src/parsers/manifest/dash/common/parse_representation_index.ts b/src/parsers/manifest/dash/common/parse_representation_index.ts
index b1a107409a..606ffa10b4 100644
--- a/src/parsers/manifest/dash/common/parse_representation_index.ts
+++ b/src/parsers/manifest/dash/common/parse_representation_index.ts
@@ -31,6 +31,10 @@ import {
ListRepresentationIndex,
TemplateRepresentationIndex,
TimelineRepresentationIndex,
+ IBaseIndexContextArgument,
+ IListIndexContextArgument,
+ ITemplateIndexContextArgument,
+ ITimelineIndexContextArgument,
} from "./indexes";
import ManifestBoundsCalculator from "./manifest_bounds_calculator";
import { IResolvedBaseUrl } from "./resolve_base_urls";
@@ -46,14 +50,12 @@ export default function parseRepresentationIndex(
representation : IRepresentationIntermediateRepresentation,
context : IRepresentationIndexContext
) : IRepresentationIndex {
- const { aggressiveMode,
- availabilityTimeOffset,
+ const { availabilityTimeOffset,
manifestBoundsCalculator,
isDynamic,
end: periodEnd,
start: periodStart,
receivedTime,
- timeShiftBufferDepth,
unsafelyBaseOnPreviousRepresentation,
inbandEventStreams,
isLastPeriod } = context;
@@ -65,20 +67,24 @@ export default function parseRepresentationIndex(
return inbandEventStreams
.some(({ schemeIdUri }) => schemeIdUri === inbandEvent.schemeIdUri);
};
- const reprIndexCtxt = { aggressiveMode,
- availabilityTimeComplete: true,
- availabilityTimeOffset,
- unsafelyBaseOnPreviousRepresentation,
- isEMSGWhitelisted,
- isLastPeriod,
- manifestBoundsCalculator,
- isDynamic,
- periodEnd,
- periodStart,
- receivedTime,
- representationBitrate: representation.attributes.bitrate,
- representationId: representation.attributes.id,
- timeShiftBufferDepth };
+ const reprIndexCtxt: ITimelineIndexContextArgument |
+ ITemplateIndexContextArgument |
+ IListIndexContextArgument |
+ IBaseIndexContextArgument =
+ {
+ availabilityTimeComplete: undefined,
+ availabilityTimeOffset,
+ unsafelyBaseOnPreviousRepresentation,
+ isEMSGWhitelisted,
+ isLastPeriod,
+ manifestBoundsCalculator,
+ isDynamic,
+ periodEnd,
+ periodStart,
+ receivedTime,
+ representationBitrate: representation.attributes.bitrate,
+ representationId: representation.attributes.id,
+ };
let representationIndex : IRepresentationIndex;
if (representation.children.segmentBase !== undefined) {
const { segmentBase } = representation.children;
@@ -99,12 +105,15 @@ export default function parseRepresentationIndex(
...segmentTemplates as [
ISegmentTemplateIntermediateRepresentation
] /* Ugly TS Hack */);
- reprIndexCtxt.availabilityTimeComplete =
- segmentTemplate.availabilityTimeComplete ??
- context.availabilityTimeComplete;
- reprIndexCtxt.availabilityTimeOffset =
- (segmentTemplate.availabilityTimeOffset ?? 0) +
- context.availabilityTimeOffset;
+ if (
+ segmentTemplate.availabilityTimeOffset !== undefined ||
+ context.availabilityTimeOffset !== undefined
+ ) {
+ reprIndexCtxt.availabilityTimeOffset =
+ (segmentTemplate.availabilityTimeOffset ?? 0) +
+ (context.availabilityTimeOffset ?? 0);
+ }
+
representationIndex = TimelineRepresentationIndex
.isTimelineIndexArgument(segmentTemplate) ?
new TimelineRepresentationIndex(segmentTemplate, reprIndexCtxt) :
@@ -133,12 +142,25 @@ export default function parseRepresentationIndex(
export interface IRepresentationIndexContext {
/** Parsed AdaptationSet which contains the Representation. */
adaptation : IAdaptationSetIntermediateRepresentation;
- /** Whether we should request new segments even if they are not yet finished. */
- aggressiveMode : boolean;
- /** If false, declared segments in the MPD might still be not completely generated. */
- availabilityTimeComplete : boolean;
- /** availability time offset of the concerned Adaptation. */
- availabilityTimeOffset : number;
+ /**
+ * If `false`, declared segments in the MPD might still be not completely generated.
+ * If `true`, they are completely generated.
+ *
+ * If `undefined`, the corresponding property was not set in the MPD and it is
+ * thus assumed that they are all generated.
+ * It might however be semantically different than `true` in the RxPlayer as it
+ * means that the packager didn't include that information in the MPD.
+ */
+ availabilityTimeComplete : boolean | undefined;
+ /**
+ * availability time offset of the concerned Adaptation.
+ *
+ * If `undefined`, the corresponding property was not set in the MPD and it is
+ * thus assumed to be equal to `0`.
+ * It might however be semantically different than `0` in the RxPlayer as it
+ * means that the packager didn't include that information in the MPD.
+ */
+ availabilityTimeOffset : number | undefined;
/** Eventual URLs from which every relative URL will be based on. */
baseURLs : IResolvedBaseUrl[];
/** End time of the current Period, in seconds. */
@@ -167,8 +189,6 @@ export interface IRepresentationIndexContext {
receivedTime? : number | undefined;
/** Start time of the current period, in seconds. */
start : number;
- /** Depth of the buffer for the whole content, in seconds. */
- timeShiftBufferDepth? : number | undefined;
/**
* The parser should take this Representation - which is the same as this one
* parsed at an earlier time - as a base to speed-up the parsing process.
diff --git a/src/parsers/manifest/dash/common/parse_representations.ts b/src/parsers/manifest/dash/common/parse_representations.ts
index 0323d6d0e6..2b7daa8aad 100644
--- a/src/parsers/manifest/dash/common/parse_representations.ts
+++ b/src/parsers/manifest/dash/common/parse_representations.ts
@@ -153,9 +153,15 @@ export default function parseRepresentations(
const availabilityTimeComplete =
representation.attributes.availabilityTimeComplete ??
context.availabilityTimeComplete;
- const availabilityTimeOffset =
- (representation.attributes.availabilityTimeOffset ?? 0) +
- context.availabilityTimeOffset;
+
+ let availabilityTimeOffset: number | undefined;
+ if (
+ representation.attributes.availabilityTimeOffset !== undefined ||
+ context.availabilityTimeOffset !== undefined
+ ) {
+ availabilityTimeOffset = (representation.attributes.availabilityTimeOffset ?? 0) +
+ (context.availabilityTimeOffset ?? 0);
+ }
const reprIndexCtxt = objectAssign({},
context,
{ availabilityTimeOffset,
diff --git a/src/parsers/manifest/local/representation_index.ts b/src/parsers/manifest/local/representation_index.ts
index 49ba6d36e8..8a71ad6233 100644
--- a/src/parsers/manifest/local/representation_index.ts
+++ b/src/parsers/manifest/local/representation_index.ts
@@ -149,7 +149,7 @@ export default class LocalRepresentationIndex implements IRepresentationIndex {
* @returns {boolean|undefined}
*/
awaitSegmentBetween(start: number, end: number): boolean | undefined {
- if (this.isFinished()) {
+ if (this.isStillAwaitingFutureSegments()) {
return false;
}
if (this._index.incomingRanges === undefined) {
@@ -173,8 +173,8 @@ export default class LocalRepresentationIndex implements IRepresentationIndex {
return true;
}
- isFinished() : boolean {
- return this._index.isFinished;
+ isStillAwaitingFutureSegments() : boolean {
+ return !this._index.isFinished;
}
/**
diff --git a/src/parsers/manifest/metaplaylist/representation_index.ts b/src/parsers/manifest/metaplaylist/representation_index.ts
index 4bf4f0bb27..36230ab653 100644
--- a/src/parsers/manifest/metaplaylist/representation_index.ts
+++ b/src/parsers/manifest/metaplaylist/representation_index.ts
@@ -196,8 +196,8 @@ export default class MetaRepresentationIndex implements IRepresentationIndex {
/**
* @returns {Boolean}
*/
- public isFinished() : boolean {
- return this._wrappedIndex.isFinished();
+ public isStillAwaitingFutureSegments() : boolean {
+ return this._wrappedIndex.isStillAwaitingFutureSegments();
}
/**
diff --git a/src/parsers/manifest/smooth/representation_index.ts b/src/parsers/manifest/smooth/representation_index.ts
index 91537aa91b..65b28743c6 100644
--- a/src/parsers/manifest/smooth/representation_index.ts
+++ b/src/parsers/manifest/smooth/representation_index.ts
@@ -27,7 +27,6 @@ import {
checkDiscontinuity,
getIndexSegmentEnd,
} from "../utils/index_helpers";
-import isSegmentStillAvailable from "../utils/is_segment_still_available";
import updateSegmentTimeline from "../utils/update_segment_timeline";
import addSegmentInfos from "./utils/add_segment_infos";
import { replaceSegmentSmoothTokens } from "./utils/tokens";
@@ -493,7 +492,7 @@ export default class SmoothRepresentationIndex implements IRepresentationIndex {
*/
awaitSegmentBetween(start: number, end: number): boolean | undefined {
assert(start <= end);
- if (this.isFinished()) {
+ if (this.isStillAwaitingFutureSegments()) {
return false;
}
const lastAvailablePosition = this.getLastAvailablePosition();
@@ -532,7 +531,22 @@ export default class SmoothRepresentationIndex implements IRepresentationIndex {
}
this._refreshTimeline();
const { timeline, timescale } = this._index;
- return isSegmentStillAvailable(segment, timeline, timescale, 0);
+ for (let i = 0; i < timeline.length; i++) {
+ const tSegment = timeline[i];
+ const tSegmentTime = tSegment.start / timescale;
+ if (tSegmentTime > segment.time) {
+ return false; // We went over it without finding it
+ } else if (tSegmentTime === segment.time) {
+ return true;
+ } else { // tSegment.start < segment.time
+ if (tSegment.repeatCount >= 0 && tSegment.duration !== undefined) {
+ const timeDiff = tSegmentTime - tSegment.start;
+ const repeat = (timeDiff / tSegment.duration) - 1;
+ return repeat % 1 === 0 && repeat <= tSegment.repeatCount;
+ }
+ }
+ }
+ return false;
}
/**
@@ -628,9 +642,9 @@ export default class SmoothRepresentationIndex implements IRepresentationIndex {
}
/**
- * 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.
- * 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.
*
* For Smooth, it should only depend on whether the content is a live content
@@ -638,8 +652,8 @@ export default class SmoothRepresentationIndex implements IRepresentationIndex {
* TODO What about Smooth live content that finishes at some point?
* @returns {boolean}
*/
- isFinished() : boolean {
- return !this._isLive;
+ isStillAwaitingFutureSegments() : boolean {
+ return this._isLive;
}
/**
@@ -652,8 +666,8 @@ export default class SmoothRepresentationIndex implements IRepresentationIndex {
/**
* Add new segments to a `SmoothRepresentationIndex`.
* @param {Array.} nextSegments - The segment information parsed.
- * @param {Object} segment - Information on the segment which contained that
- * new segment information.
+ * @param {Object} currentSegment - Information on the segment which contained
+ * that new segment information.
*/
addNewSegments(
nextSegments : Array<{ duration : number; time : number; timescale : number }>,
diff --git a/src/parsers/manifest/utils/__tests__/get_first_time_from_adaptations.test.ts b/src/parsers/manifest/utils/__tests__/get_first_time_from_adaptations.test.ts
index 99e184f8e1..ce39772e64 100644
--- a/src/parsers/manifest/utils/__tests__/get_first_time_from_adaptations.test.ts
+++ b/src/parsers/manifest/utils/__tests__/get_first_time_from_adaptations.test.ts
@@ -30,7 +30,7 @@ function generateRepresentationIndex(
awaitSegmentBetween() : undefined { return ; },
checkDiscontinuity() : number | null { return null; },
isSegmentStillAvailable() : undefined { return ; },
- isFinished() { return false; },
+ isStillAwaitingFutureSegments() { return true; },
canBeOutOfSyncError() : true { return true; },
isInitialized() : true { return true; },
_replace() { /* noop */ },
diff --git a/src/parsers/manifest/utils/__tests__/get_last_time_from_adaptation.test.ts b/src/parsers/manifest/utils/__tests__/get_last_time_from_adaptation.test.ts
index ea401d4821..4e88ae8168 100644
--- a/src/parsers/manifest/utils/__tests__/get_last_time_from_adaptation.test.ts
+++ b/src/parsers/manifest/utils/__tests__/get_last_time_from_adaptation.test.ts
@@ -30,7 +30,7 @@ function generateRepresentationIndex(
awaitSegmentBetween() : undefined { return ; },
checkDiscontinuity() : number | null { return null; },
isSegmentStillAvailable() : undefined { return ; },
- isFinished() { return false; },
+ isStillAwaitingFutureSegments() { return true; },
isInitialized() : true { return true; },
canBeOutOfSyncError() : true { return true; },
_replace() { /* noop */ },
diff --git a/src/parsers/manifest/utils/index_helpers.ts b/src/parsers/manifest/utils/index_helpers.ts
index a79f432c67..78ceb6ae8b 100644
--- a/src/parsers/manifest/utils/index_helpers.ts
+++ b/src/parsers/manifest/utils/index_helpers.ts
@@ -136,7 +136,7 @@ export function getTimescaledRange(
* timescaled time.
* Returns -1 if the given time is lower than the start of the first available
* segment.
- * @param {Object} index
+ * @param {Object} timeline
* @param {Number} timeTScaled
* @returns {Number}
*/
diff --git a/src/parsers/manifest/utils/is_segment_still_available.ts b/src/parsers/manifest/utils/is_segment_still_available.ts
deleted file mode 100644
index ab8601db26..0000000000
--- a/src/parsers/manifest/utils/is_segment_still_available.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * 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 { ISegment } from "../../../manifest";
-import { IIndexSegment } from "./index_helpers";
-
-/**
- * Returns true if a Segment returned by the corresponding index is still
- * considered available.
- * Returns false if it is not available anymore.
- * Returns undefined if we cannot know whether it is still available or not.
- * /!\ We do not check the mediaURLs of the segment.
- * @param {Object} segment
- * @param {Array.} timescale
- * @param {number} timeline
- * @returns {Boolean|undefined}
- */
-export default function isSegmentStillAvailable(
- segment : ISegment,
- timeline : IIndexSegment[],
- timescale : number,
- indexTimeOffset : number
-) : boolean | undefined {
- for (let i = 0; i < timeline.length; i++) {
- const tSegment = timeline[i];
- const tSegmentTime = (tSegment.start - indexTimeOffset) / timescale;
- if (tSegmentTime > segment.time) {
- return false;
- } else if (tSegmentTime === segment.time) {
- if (tSegment.range === undefined) {
- return segment.range === undefined;
- }
- return segment.range != null &&
- tSegment.range[0] === segment.range[0] &&
- tSegment.range[1] === segment.range[1];
- } else { // tSegment.start < segment.time
- if (tSegment.repeatCount >= 0 && tSegment.duration !== undefined) {
- const timeDiff = tSegmentTime - tSegment.start;
- const repeat = (timeDiff / tSegment.duration) - 1;
- return repeat % 1 === 0 && repeat <= tSegment.repeatCount;
- }
- }
- }
- return false;
-}
diff --git a/src/parsers/texttracks/ttml/html/apply_line_height.ts b/src/parsers/texttracks/ttml/html/apply_line_height.ts
index 1897e9ab44..4f727229a1 100644
--- a/src/parsers/texttracks/ttml/html/apply_line_height.ts
+++ b/src/parsers/texttracks/ttml/html/apply_line_height.ts
@@ -27,10 +27,12 @@ export default function applyLineHeight(
lineHeight : string
) : void {
const trimmedLineHeight = lineHeight.trim();
+ const splittedLineHeight = trimmedLineHeight.split(" ");
+
if (trimmedLineHeight === "auto") {
return;
}
- const firstLineHeight = REGXP_LENGTH.exec(trimmedLineHeight[0]);
+ const firstLineHeight = REGXP_LENGTH.exec(splittedLineHeight[0]);
if (firstLineHeight === null) {
return;
}
diff --git a/src/public_types.ts b/src/public_types.ts
index 60359211d4..7d71d86841 100644
--- a/src/public_types.ts
+++ b/src/public_types.ts
@@ -313,6 +313,9 @@ export interface IRepresentation {
/** If the track is HDR, gives the HDR characteristics of the content */
hdrInfo? : IHDRInformation;
index : IRepresentationIndex;
+
+ /** NOTE: not part of the API. */
+ isSupported: boolean;
}
export interface IHDRInformation {
@@ -356,10 +359,20 @@ export type IStartAtOption =
percentage : number;
} | {
/**
- * If set, we should begin at this position relative to the content's end,
- * in seconds.
+ * If set, we should begin at this position relative to the content's maximum
+ * seekable position, in seconds.
*/
fromLastPosition : number;
+ } | {
+ /**
+ * If set, we should begin at this position relative to the content's live
+ * edge if it makes sense, in seconds.
+ *
+ * If the live edge is unknown or if it does not make sense for the current
+ * content, that position is relative to the content's maximum position
+ * instead.
+ */
+ fromLivePosition : number;
} | {
/**
* If set, we should begin at this position relative to the content's start,
diff --git a/src/typings/object-assign.d.ts b/src/typings/object-assign.d.ts
deleted file mode 100644
index 08544f31a3..0000000000
--- a/src/typings/object-assign.d.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-/**
- * 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.
- */
-
-declare module "object-assign" {
- function objectAssign(target : T, source : U) : T & U;
- function objectAssign(
- target : T,
- source1 : U,
- source2 : V
- ) : T & U & V;
- function objectAssign(
- target : T,
- source1 : U,
- source2 : V,
- source3 : W
- ) : T & U & V & W;
- function objectAssign(
- target : T,
- source1 : U,
- source2 : V,
- source3 : W,
- source4 : X
- ) : T & U & V & W & X;
- function objectAssign(
- target : T,
- source1 : U,
- source2 : V,
- source3 : W,
- source4 : X,
- source5 : Y
- ) : T & U & V & W & Y;
- // eslint-disable-next-line @typescript-eslint/ban-types
- function objectAssign(target : object, ...sources : T[]) : T;
- export default objectAssign;
-}
diff --git a/tests/integration/scenarios/loadVideo_options.js b/tests/integration/scenarios/loadVideo_options.js
index fce29d62a4..49f2de9c6d 100644
--- a/tests/integration/scenarios/loadVideo_options.js
+++ b/tests/integration/scenarios/loadVideo_options.js
@@ -186,6 +186,23 @@ describe("loadVideo Options", () => {
expect(player.getPosition()).to.equal(initialPosition);
});
+ it("should seek at the right position if startAt.fromLivePosition is set", async function () {
+ const startAt = 10;
+ player.loadVideo({
+ transport: manifestInfos.transport,
+ url: manifestInfos.url,
+ autoPlay: false,
+ startAt: { fromLivePosition: - startAt },
+ });
+ await waitForLoadedStateAfterLoadVideo(player);
+ expect(player.getPlayerState()).to.equal("LOADED");
+ const initialPosition = player.getPosition();
+ expect(initialPosition).to.be
+ .closeTo(player.getMaximumPosition() - startAt, 0.5);
+ await sleep(500);
+ expect(player.getPosition()).to.equal(initialPosition);
+ });
+
it("should seek at the right position if startAt.percentage is set", async function () {
player.loadVideo({
transport: manifestInfos.transport,
@@ -268,6 +285,23 @@ describe("loadVideo Options", () => {
expect(player.getPosition()).to.be.above(initialPosition);
});
+ it("should seek at the right position then play if startAt.fromLivePosition and autoPlay is set", async function () {
+ const startAt = 10;
+ player.loadVideo({
+ transport: manifestInfos.transport,
+ url: manifestInfos.url,
+ autoPlay: true,
+ startAt: { fromLivePosition: - startAt },
+ });
+ await waitForLoadedStateAfterLoadVideo(player);
+ expect(player.getPlayerState()).to.equal("PLAYING");
+ const initialPosition = player.getPosition();
+ expect(initialPosition).to.be
+ .closeTo(player.getMaximumPosition() - startAt, 0.5);
+ await sleep(500);
+ expect(player.getPosition()).to.be.above(initialPosition);
+ });
+
it("should seek at the right position then play if startAt.percentage and autoPlay is set", async function () {
player.loadVideo({
transport: manifestInfos.transport,