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/package-lock.json b/package-lock.json index bce1ea0190..d1123e1f11 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", @@ -1937,6 +1937,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", @@ -5691,27 +5712,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", @@ -16174,6 +16174,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", @@ -18928,26 +18948,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", diff --git a/package.json b/package.json index 562ba965ec..730fbc2de7 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "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*'", @@ -84,6 +84,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 +98,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", 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/option_utils.ts b/src/core/api/option_utils.ts index 1b00fd1778..ed7c230437 100644 --- a/src/core/api/option_utils.ts +++ b/src/core/api/option_utils.ts @@ -397,6 +397,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 +416,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."); + } } /** diff --git a/src/core/api/public_api.ts b/src/core/api/public_api.ts index 8d1ff7682b..de397d8778 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() { @@ -2429,9 +2441,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(); + return segmentBufferStatus.value.getInventory(); + } + return null; } /** 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/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); } /**