From 735512ce3db7a802598dc704806ade942208dbea Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Fri, 20 Mar 2020 18:48:02 +0100 Subject: [PATCH 01/72] source-buffers: update SourceBufferStore to move the native buffer synchronization logic out of init --- src/core/api/public_api.ts | 9 +- .../orchestrator/buffer_orchestrator.ts | 6 +- src/core/buffers/period/period_buffer.ts | 22 ++-- src/core/init/load_on_media_source.ts | 44 +------ .../source_buffers/source_buffers_store.ts | 120 ++++++++++++++++-- 5 files changed, 134 insertions(+), 67 deletions(-) diff --git a/src/core/api/public_api.ts b/src/core/api/public_api.ts index 194dce0703..ec2d2a857e 100644 --- a/src/core/api/public_api.ts +++ b/src/core/api/public_api.ts @@ -1873,10 +1873,11 @@ class Player extends EventEmitter { { return null; } - const queuedSourceBuffer = this._priv_contentInfos - .sourceBuffersStore.get(bufferType); - return queuedSourceBuffer === null ? null : - queuedSourceBuffer.getInventory(); + const sourceBufferStatus = this._priv_contentInfos + .sourceBuffersStore.getStatus(bufferType); + return sourceBufferStatus.type === "set" ? + sourceBufferStatus.value.getInventory() : + null; } /** diff --git a/src/core/buffers/orchestrator/buffer_orchestrator.ts b/src/core/buffers/orchestrator/buffer_orchestrator.ts index b8c23b0cdf..16c9f44728 100644 --- a/src/core/buffers/orchestrator/buffer_orchestrator.ts +++ b/src/core/buffers/orchestrator/buffer_orchestrator.ts @@ -281,15 +281,17 @@ export default function BufferOrchestrator( const handleDecipherabilityUpdate$ = fromEvent(manifest, "decipherabilityUpdate") .pipe(mergeMap((updates) => { - const queuedSourceBuffer = sourceBuffersStore.get(bufferType); + const sourceBufferStatus = sourceBuffersStore.getStatus(bufferType); const hasType = updates.some(update => update.adaptation.type === bufferType); - if (!hasType || queuedSourceBuffer == null) { + if (!hasType || sourceBufferStatus.type !== "set") { return EMPTY; // no need to stop the current buffers } + const queuedSourceBuffer = sourceBufferStatus.value; const rangesToClean = getBlacklistedRanges(queuedSourceBuffer, updates); enableOutOfBoundsCheck = false; destroyBuffers$.next(); return observableConcat( + sourceBuffersStore.onSourceBuffersReady().pipe(ignoreElements()), ...rangesToClean.map(({ start, end }) => queuedSourceBuffer.removeBuffer(start, end).pipe(ignoreElements())), clock$.pipe(take(1), mergeMap((lastTick) => { diff --git a/src/core/buffers/period/period_buffer.ts b/src/core/buffers/period/period_buffer.ts index 60ab546557..4ba910c48b 100644 --- a/src/core/buffers/period/period_buffer.ts +++ b/src/core/buffers/period/period_buffer.ts @@ -114,18 +114,22 @@ export default function PeriodBuffer({ const adaptation$ = new ReplaySubject(1); return adaptation$.pipe( switchMap((adaptation) => { - if (adaptation == null) { + if (adaptation === null) { log.info(`Buffer: Set no ${bufferType} Adaptation`, period); - const previousQSourceBuffer = sourceBuffersStore.get(bufferType); + const sourceBufferStatus = sourceBuffersStore.getStatus(bufferType); let cleanBuffer$ : Observable; - if (previousQSourceBuffer != null) { + if (sourceBufferStatus.type === "set") { log.info(`Buffer: Clearing previous ${bufferType} SourceBuffer`); + const previousQSourceBuffer = sourceBufferStatus.value; cleanBuffer$ = previousQSourceBuffer .removeBuffer(period.start, period.end == null ? Infinity : period.end); } else { + if (sourceBufferStatus.type === "unset") { + sourceBuffersStore.disableSourceBuffer(bufferType); + } cleanBuffer$ = observableOf(null); } @@ -161,7 +165,9 @@ export default function PeriodBuffer({ const bufferGarbageCollector$ = garbageCollectors.get(qSourceBuffer); const adaptationBuffer$ = createAdaptationBuffer(adaptation, qSourceBuffer); - return observableConcat(cleanBuffer$, + return observableConcat(sourceBuffersStore.onSourceBuffersReady() + .pipe(ignoreElements()), + cleanBuffer$, observableMerge(adaptationBuffer$, bufferGarbageCollector$)); })); @@ -232,10 +238,10 @@ function createOrReuseQueuedSourceBuffer( adaptation : Adaptation, options: { textTrackOptions? : ITextTrackSourceBufferOptions } ) : QueuedSourceBuffer { - const currentQSourceBuffer = sourceBuffersStore.get(bufferType); - if (currentQSourceBuffer != null) { + const sourceBufferStatus = sourceBuffersStore.getStatus(bufferType); + if (sourceBufferStatus.type === "set") { log.info("Buffer: Reusing a previous SourceBuffer for the type", bufferType); - return currentQSourceBuffer; + return sourceBufferStatus.value; } const codec = getFirstDeclaredMimeType(adaptation); const sbOptions = bufferType === "text" ? options.textTrackOptions : undefined; @@ -243,7 +249,7 @@ function createOrReuseQueuedSourceBuffer( } /** - * Get mimetype string of the first representation declared in the given + * Get mime-type string of the first representation declared in the given * adaptation. * @param {Adaptation} adaptation * @returns {string} diff --git a/src/core/init/load_on_media_source.ts b/src/core/init/load_on_media_source.ts index 196895b7b9..6938a0a1c2 100644 --- a/src/core/init/load_on_media_source.ts +++ b/src/core/init/load_on_media_source.ts @@ -33,9 +33,7 @@ import { } from "rxjs/operators"; import { MediaError } from "../../errors"; import log from "../../log"; -import Manifest, { - Period, -} from "../../manifest"; +import Manifest from "../../manifest"; import ABRManager from "../abr"; import BufferOrchestrator, { IBufferOrchestratorEvent, @@ -135,18 +133,6 @@ export default function createMediaSourceLoader({ // single SourceBuffer per type. const sourceBuffersStore = new SourceBuffersStore(mediaElement, mediaSource); - // Initialize all native source buffers from the first period at the same - // time. - // We cannot lazily create native sourcebuffers since the spec does not - // allow adding them during playback. - // - // From https://w3c.github.io/media-source/#methods - // For example, a user agent may throw a QuotaExceededError - // exception if the media element has reached the HAVE_METADATA - // readyState. This can occur if the user agent's media engine - // does not support adding more tracks during playback. - createNativeSourceBuffersForPeriod(sourceBuffersStore, initialPeriod); - const { seek$, load$ } = seekAndLoadOnMediaEvents({ clock$, mediaElement, startTime: initialTime, @@ -243,31 +229,3 @@ export default function createMediaSourceLoader({ })); }; } - -/** - * Create all native SourceBuffers needed for a given Period. - * - * Native Buffers have the particulary to need to be created at the beginning of - * the content. - * Custom source buffers (entirely managed in JS) can generally be created and - * disposed at will during the lifecycle of the content. - * @param {SourceBuffersStore} sourceBuffersStore - * @param {Period} period - */ -function createNativeSourceBuffersForPeriod( - sourceBuffersStore : SourceBuffersStore, - period : Period -) : void { - Object.keys(period.adaptations).forEach(bufferType => { - if (SourceBuffersStore.isNative(bufferType)) { - const adaptations = period.adaptations[bufferType]; - const representations = adaptations != null && - adaptations.length > 0 ? adaptations[0].representations : - []; - if (representations.length > 0) { - const codec = representations[0].getMimeTypeString(); - sourceBuffersStore.createSourceBuffer(bufferType, codec); - } - } - }); -} diff --git a/src/core/source_buffers/source_buffers_store.ts b/src/core/source_buffers/source_buffers_store.ts index 86b14ed5f5..2295650648 100644 --- a/src/core/source_buffers/source_buffers_store.ts +++ b/src/core/source_buffers/source_buffers_store.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import { + Observable, + of as observableOf, +} from "rxjs"; import { ICustomSourceBuffer } from "../../compat"; import { MediaError } from "../../errors"; import features from "../../features"; @@ -81,6 +85,10 @@ type INativeSourceBufferType = "audio" | "video"; * The returned SourceBuffer is actually a QueuedSourceBuffer instance which * wrap a SourceBuffer implementation to queue all its actions. * + * To be able to use a native SourceBuffer, you will first need to wait until + * both of these are either created or disabled. The Observable returned by + * `onSourceBuffersReady` will emit when that is the case. + * * @class SourceBuffersStore */ export default class SourceBuffersStore { @@ -99,12 +107,30 @@ export default class SourceBuffersStore { private readonly _mediaSource : MediaSource; private _initializedSourceBuffers : { - audio? : QueuedSourceBuffer; - video? : QueuedSourceBuffer; - text? : QueuedSourceBuffer; - image? : QueuedSourceBuffer; + audio? : QueuedSourceBuffer | + null; + video? : QueuedSourceBuffer | + null; + text? : QueuedSourceBuffer | + null; + image? : QueuedSourceBuffer | + null; }; + /** + * Callbacks called when a native SourceBuffers is either created or disabled. + * Used for example to trigger the `this.onSourceBuffersReady` Observable. + */ + private _onNativeSourceBufferAddedOrDisabled : Array<() => void>; + /** * @param {HTMLMediaElement} mediaElement * @param {MediaSource} mediaSource @@ -114,25 +140,93 @@ export default class SourceBuffersStore { this._mediaElement = mediaElement; this._mediaSource = mediaSource; this._initializedSourceBuffers = {}; + this._onNativeSourceBufferAddedOrDisabled = []; } /** - * Returns the created QueuedSourceBuffer for the given type. - * Returns null if no QueuedSourceBuffer were created for the given type. + * Returns the current "status" of the buffer in the SourceBuffer. + * + * This function will return an object containing the key `type` which can + * be equal to either one of those three value: + * + * - "set": A SourceBuffer has been created. You will in this case also have + * a second key, `value`, which will contain the related + * QueuedSourceBuffer instance. + * Please note that you will need to wait until `this.onSourceBuffersReady()` + * has emitted before updating a native SourceBuffer. + * + * - "disabled": The SourceBuffer has been explicitely disabled for this + * type. + * + * - "unset": No action has been taken yet for that SourceBuffer. * * @param {string} bufferType * @returns {QueuedSourceBuffer|null} */ - public get(bufferType : IBufferType) : QueuedSourceBuffer|null { + public getStatus(bufferType : IBufferType) : { type : "set"; + value : QueuedSourceBuffer; } | + { type : "unset" } | + { type : "disabled" } + { const initializedBuffer = this._initializedSourceBuffers[bufferType]; - return initializedBuffer != null ? initializedBuffer : - null; + return initializedBuffer === undefined ? { type: "unset" } : + initializedBuffer === null ? { type: "disabled" } : + { type: "set", + value: initializedBuffer }; + } + + /** + * Native SourceBuffers (audio and video) need to all be created before they + * can be used. + * + * From https://w3c.github.io/media-source/#methods + * For example, a user agent may throw a QuotaExceededError + * exception if the media element has reached the HAVE_METADATA + * readyState. This can occur if the user agent's media engine + * does not support adding more tracks during playback. + * + * This function will return an Observable emitting when each of these + * SourceBuffers is either created or disabled. + * @return {Observable} + */ + public onSourceBuffersReady() : Observable { + if (this._areNativeSourceBuffersReady()) { + return observableOf(undefined); + } + return new Observable(obs => { + this._onNativeSourceBufferAddedOrDisabled.push(() => { + if (this._areNativeSourceBuffersReady()) { + obs.next(undefined); + obs.complete(); + } + }); + }); + } + + /** + * Explicitely set no SourceBuffer for a given buffer type. + * A call to this function is needed at least for unused native buffer types + * ("audio" and "video"), to be able to emit through `onSourceBuffersReady` + * when both of those are set. + * @param {string} + */ + public disableSourceBuffer(bufferType : IBufferType) : void { + if (this._initializedSourceBuffers[bufferType] !== undefined) { + throw new Error("Cannot disable an active SourceBuffer."); + } + this._initializedSourceBuffers[bufferType] = null; + if (SourceBuffersStore.isNative(bufferType)) { + this._onNativeSourceBufferAddedOrDisabled.forEach(cb => cb()); + } } /** * Creates a new QueuedSourceBuffer for the SourceBuffer type. * Reuse an already created one if a QueuedSourceBuffer for the given type * already exists. + * + * Please note that you will need to wait until `this.onSourceBuffersReady()` + * has emitted before updating a native SourceBuffer. * @param {string} bufferType * @param {string} codec * @param {Object|undefined} options @@ -159,6 +253,7 @@ export default class SourceBuffersStore { this._mediaSource, codec); this._initializedSourceBuffers[bufferType] = nativeSourceBuffer; + this._onNativeSourceBufferAddedOrDisabled.forEach(cb => cb()); return nativeSourceBuffer; } @@ -239,11 +334,16 @@ export default class SourceBuffersStore { */ public disposeAll() { POSSIBLE_BUFFER_TYPES.forEach((bufferType : IBufferType) => { - if (this.get(bufferType) != null) { + if (this.getStatus(bufferType).type === "set") { this.disposeSourceBuffer(bufferType); } }); } + + private _areNativeSourceBuffersReady() { + return this._initializedSourceBuffers.audio !== undefined && + this._initializedSourceBuffers.video !== undefined; + } } /** From 5d83741fe9e840c89b7d3bec5f0f3db1a370b8f0 Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Mon, 23 Mar 2020 14:16:40 +0100 Subject: [PATCH 02/72] buffers: remove unnecessary onSourceBuffersReady call --- src/core/buffers/orchestrator/buffer_orchestrator.ts | 1 - src/core/buffers/period/period_buffer.ts | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/core/buffers/orchestrator/buffer_orchestrator.ts b/src/core/buffers/orchestrator/buffer_orchestrator.ts index 16c9f44728..6052e56ed3 100644 --- a/src/core/buffers/orchestrator/buffer_orchestrator.ts +++ b/src/core/buffers/orchestrator/buffer_orchestrator.ts @@ -291,7 +291,6 @@ export default function BufferOrchestrator( enableOutOfBoundsCheck = false; destroyBuffers$.next(); return observableConcat( - sourceBuffersStore.onSourceBuffersReady().pipe(ignoreElements()), ...rangesToClean.map(({ start, end }) => queuedSourceBuffer.removeBuffer(start, end).pipe(ignoreElements())), clock$.pipe(take(1), mergeMap((lastTick) => { diff --git a/src/core/buffers/period/period_buffer.ts b/src/core/buffers/period/period_buffer.ts index 4ba910c48b..3ea5486177 100644 --- a/src/core/buffers/period/period_buffer.ts +++ b/src/core/buffers/period/period_buffer.ts @@ -165,11 +165,11 @@ export default function PeriodBuffer({ const bufferGarbageCollector$ = garbageCollectors.get(qSourceBuffer); const adaptationBuffer$ = createAdaptationBuffer(adaptation, qSourceBuffer); - return observableConcat(sourceBuffersStore.onSourceBuffersReady() - .pipe(ignoreElements()), - cleanBuffer$, - observableMerge(adaptationBuffer$, - bufferGarbageCollector$)); + return sourceBuffersStore.onSourceBuffersReady().pipe(mergeMap(() => { + return observableConcat(cleanBuffer$, + observableMerge(adaptationBuffer$, + bufferGarbageCollector$)); + })); })); return observableConcat( From 978a9ba3ae73be2971124fa632bbdb964380e7dd Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Wed, 25 Mar 2020 18:03:27 +0100 Subject: [PATCH 03/72] source-buffers: do not throw when re-disabling a SourceBuffer --- src/core/source_buffers/source_buffers_store.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/core/source_buffers/source_buffers_store.ts b/src/core/source_buffers/source_buffers_store.ts index 2295650648..146e70d461 100644 --- a/src/core/source_buffers/source_buffers_store.ts +++ b/src/core/source_buffers/source_buffers_store.ts @@ -211,7 +211,12 @@ export default class SourceBuffersStore { * @param {string} */ public disableSourceBuffer(bufferType : IBufferType) : void { - if (this._initializedSourceBuffers[bufferType] !== undefined) { + const currentValue = this._initializedSourceBuffers[bufferType]; + if (currentValue === null) { + log.warn(`SBS: The ${bufferType} SourceBuffer was already disabled.`); + return; + } + if (currentValue !== undefined) { throw new Error("Cannot disable an active SourceBuffer."); } this._initializedSourceBuffers[bufferType] = null; @@ -340,6 +345,10 @@ export default class SourceBuffersStore { }); } + /** + * Returns `true` when we're ready to push and decode contents through our + * native SourceBuffers. + */ private _areNativeSourceBuffersReady() { return this._initializedSourceBuffers.audio !== undefined && this._initializedSourceBuffers.video !== undefined; From b0bcefee48765540b23fe10a9ea150ca28a9f3e2 Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Wed, 25 Mar 2020 18:19:21 +0100 Subject: [PATCH 04/72] source-buffers: do not start a content if both audio and video are disabled --- src/core/buffers/period/period_buffer.ts | 2 +- .../source_buffers/source_buffers_store.ts | 50 ++++++++++++------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/core/buffers/period/period_buffer.ts b/src/core/buffers/period/period_buffer.ts index 3ea5486177..b0acaed1a5 100644 --- a/src/core/buffers/period/period_buffer.ts +++ b/src/core/buffers/period/period_buffer.ts @@ -165,7 +165,7 @@ export default function PeriodBuffer({ const bufferGarbageCollector$ = garbageCollectors.get(qSourceBuffer); const adaptationBuffer$ = createAdaptationBuffer(adaptation, qSourceBuffer); - return sourceBuffersStore.onSourceBuffersReady().pipe(mergeMap(() => { + return sourceBuffersStore.waitForUsableSourceBuffers().pipe(mergeMap(() => { return observableConcat(cleanBuffer$, observableMerge(adaptationBuffer$, bufferGarbageCollector$)); diff --git a/src/core/source_buffers/source_buffers_store.ts b/src/core/source_buffers/source_buffers_store.ts index 146e70d461..ea0397fa4a 100644 --- a/src/core/source_buffers/source_buffers_store.ts +++ b/src/core/source_buffers/source_buffers_store.ts @@ -76,8 +76,9 @@ type INativeSourceBufferType = "audio" | "video"; * * Only one SourceBuffer per type is allowed at the same time: * - * - source buffers for native types (which depends on the native - * SourceBuffer implementation), are reused if one is re-created. + * - source buffers for native types (which are "audio" and "video" and which + * depend on the native SourceBuffer implementation) are reused if one is + * re-created. * * - source buffers for custom types are aborted each time a new one of the * same type is created. @@ -86,8 +87,10 @@ type INativeSourceBufferType = "audio" | "video"; * wrap a SourceBuffer implementation to queue all its actions. * * To be able to use a native SourceBuffer, you will first need to wait until - * both of these are either created or disabled. The Observable returned by - * `onSourceBuffersReady` will emit when that is the case. + * it is created - of course - but also until the other one is either created or + * disabled. + * The Observable returned by `waitForUsableSourceBuffers` will emit when + * that is the case. * * @class SourceBuffersStore */ @@ -127,7 +130,8 @@ export default class SourceBuffersStore { /** * Callbacks called when a native SourceBuffers is either created or disabled. - * Used for example to trigger the `this.onSourceBuffersReady` Observable. + * Used for example to trigger the `this.waitForUsableSourceBuffers` + * Observable. */ private _onNativeSourceBufferAddedOrDisabled : Array<() => void>; @@ -152,8 +156,9 @@ export default class SourceBuffersStore { * - "set": A SourceBuffer has been created. You will in this case also have * a second key, `value`, which will contain the related * QueuedSourceBuffer instance. - * Please note that you will need to wait until `this.onSourceBuffersReady()` - * has emitted before updating a native SourceBuffer. + * Please note that you will need to wait until + * `this.waitForUsableSourceBuffers()` has emitted before updating a + * native SourceBuffer. * * - "disabled": The SourceBuffer has been explicitely disabled for this * type. @@ -185,17 +190,17 @@ export default class SourceBuffersStore { * readyState. This can occur if the user agent's media engine * does not support adding more tracks during playback. * - * This function will return an Observable emitting when each of these - * SourceBuffers is either created or disabled. + * This function will return an Observable emitting when any and all native + * Source Buffers through this store can be used. * @return {Observable} */ - public onSourceBuffersReady() : Observable { - if (this._areNativeSourceBuffersReady()) { + public waitForUsableSourceBuffers() : Observable { + if (this._areNativeSourceBuffersUsable()) { return observableOf(undefined); } return new Observable(obs => { this._onNativeSourceBufferAddedOrDisabled.push(() => { - if (this._areNativeSourceBuffersReady()) { + if (this._areNativeSourceBuffersUsable()) { obs.next(undefined); obs.complete(); } @@ -206,8 +211,8 @@ export default class SourceBuffersStore { /** * Explicitely set no SourceBuffer for a given buffer type. * A call to this function is needed at least for unused native buffer types - * ("audio" and "video"), to be able to emit through `onSourceBuffersReady` - * when both of those are set. + * ("audio" and "video"), to be able to emit through + * `waitForUsableSourceBuffers` when conditions are met. * @param {string} */ public disableSourceBuffer(bufferType : IBufferType) : void { @@ -230,7 +235,7 @@ export default class SourceBuffersStore { * Reuse an already created one if a QueuedSourceBuffer for the given type * already exists. * - * Please note that you will need to wait until `this.onSourceBuffersReady()` + * Please note that you will need to wait until `this.waitForUsableSourceBuffers()` * has emitted before updating a native SourceBuffer. * @param {string} bufferType * @param {string} codec @@ -347,11 +352,18 @@ export default class SourceBuffersStore { /** * Returns `true` when we're ready to push and decode contents through our - * native SourceBuffers. + * created native SourceBuffers. */ - private _areNativeSourceBuffersReady() { - return this._initializedSourceBuffers.audio !== undefined && - this._initializedSourceBuffers.video !== undefined; + private _areNativeSourceBuffersUsable() { + if (this._initializedSourceBuffers.audio === undefined || + this._initializedSourceBuffers.video === undefined) + { + return false; + } + if (this._initializedSourceBuffers.video === null) { + return this._initializedSourceBuffers.audio !== null; + } + return true; } } From 2c917afaf333a34f75cde74b969e8af756ee16f6 Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Fri, 3 Apr 2020 13:54:03 +0200 Subject: [PATCH 05/72] api: add "codec" property to `preferredAudioTrack` APIs --- doc/api/index.md | 31 ++++++++++++++------ doc/api/player_options.md | 19 +++++++++++++ src/core/api/track_choice_manager.ts | 42 +++++++++++++++++++++------- 3 files changed, 73 insertions(+), 19 deletions(-) diff --git a/doc/api/index.md b/doc/api/index.md index 1239ca3aeb..7d4e9816b8 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -1327,6 +1327,25 @@ This method takes an array of objects describing the languages wanted: } ``` +Optionally, you can ask for tracks having specific codecs by adding a `codec` +property: +```js +// Example: English tracks in Dolby Digital Plus +{ + language: "eng", + audioDescription: false, + codec: { + test: /ec-3/, // RegExp validating the codec you want. + all: true, // Whether all the profiles (i.e. Representation) in a track + // should be checked to have codecs compatible to that RegExp. + // If `true`, we will only choose a track if every profiles for + // it have a codec that is validated by that RegExp. + // If `false`, we will choose a track if we know that at least + // a single profile from it has a codec validated by that RegExp. + } +} +``` + All elements in that Array should be set in preference order: from the most preferred to the least preferred. @@ -1382,15 +1401,9 @@ Returns the current list of preferred audio tracks - by order of preference. This returns the data in the same format that it was given to either the `preferredAudioTracks` constructor option or the last `setPreferredAudioTracks` -if it was called: -```js -{ - language: "fra", // {string} The wanted language - // (ISO 639-1, ISO 639-2 or ISO 639-3 language code) - audioDescription: false // {Boolean} Whether the audio track should be an - // audio description for the visually impaired -} -``` +if it was called. + +It will return an empty Array if none of those two APIs were used until now. diff --git a/doc/api/player_options.md b/doc/api/player_options.md index 0e56d0cab2..f2eea17075 100644 --- a/doc/api/player_options.md +++ b/doc/api/player_options.md @@ -240,6 +240,25 @@ This option takes an array of objects describing the languages wanted: } ``` +Optionally, you can ask for tracks having specific codecs by adding a `codec` +property: +```js +// Example: English tracks in Dolby Digital Plus +{ + language: "eng", + audioDescription: false, + codec: { + test: /ec-3/, // RegExp validating the codec you want. + all: true, // Whether all the profiles (i.e. Representation) in a track + // should be checked to have codecs compatible to that RegExp. + // If `true`, we will only choose a track if every profiles for + // it have a codec that is validated by that RegExp. + // If `false`, we will choose a track if we know that at least + // a single profile from it has a codec validated by that RegExp. + } +} +``` + All elements in that Array should be set in preference order: from the most preferred to the least preferred. diff --git a/src/core/api/track_choice_manager.ts b/src/core/api/track_choice_manager.ts index c5b23267d9..fab239b605 100644 --- a/src/core/api/track_choice_manager.ts +++ b/src/core/api/track_choice_manager.ts @@ -38,7 +38,9 @@ import takeFirstSet from "../../utils/take_first_set"; /** Single preference for an audio track Adaptation. */ export type IAudioTrackPreference = null | { language : string; - audioDescription : boolean; }; + audioDescription : boolean; + codec? : { all: boolean; + test: RegExp; }; }; /** Single preference for a text track Adaptation. */ export type ITextTrackPreference = null | @@ -106,7 +108,9 @@ interface ITMPeriodInfos { period : Period; /** Audio track preference once normalized by the TrackChoiceManager. */ type INormalizedPreferredAudioTrack = null | { normalized : string; - audioDescription : boolean; }; + audioDescription : boolean; + codec? : { all: boolean; + test: RegExp; }; }; /** Text track preference once normalized by the TrackChoiceManager. */ type INormalizedPreferredTextTrack = null | @@ -125,7 +129,8 @@ function normalizeAudioTracks( return tracks.map(t => t == null ? t : { normalized: normalizeLanguage(t.language), - audioDescription: t.audioDescription }); + audioDescription: t.audioDescription, + codec: t.codec }); } /** @@ -841,13 +846,30 @@ function findFirstOptimalAudioAdaptation( return null; } - const foundAdaptation = arrayFind(audioAdaptations, (audioAdaptation) => - takeFirstSet(audioAdaptation.normalizedLanguage, - "") === preferredAudioTrack.normalized && - (preferredAudioTrack.audioDescription ? - audioAdaptation.isAudioDescription === true : - audioAdaptation.isAudioDescription !== true) - ); + const foundAdaptation = arrayFind(audioAdaptations, (audioAdaptation) => { + const language = audioAdaptation.normalizedLanguage ?? ""; + if (language !== preferredAudioTrack.normalized) { + return false; + } + if (preferredAudioTrack.audioDescription) { + if (audioAdaptation.isAudioDescription !== true) { + return false; + } + } else if (audioAdaptation.isAudioDescription === true) { + return false; + } + if (preferredAudioTrack.codec === undefined) { + return true; + } + const regxp = preferredAudioTrack.codec.test; + const codecTestingFn = (rep : Representation) => + rep.codec !== undefined && regxp.test(rep.codec); + + if (preferredAudioTrack.codec.all) { + return audioAdaptation.representations.every(codecTestingFn); + } + return audioAdaptation.representations.some(codecTestingFn); + }); if (foundAdaptation !== undefined) { return foundAdaptation; From 0b5d3d70f350f095efcc3d23a5d4cbfcda8fbd13 Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Fri, 3 Apr 2020 14:43:35 +0200 Subject: [PATCH 06/72] api: all properties in preferredAudioTracks are now optional --- doc/api/index.md | 126 ++++++++++++++++++--------- doc/api/player_options.md | 108 ++++++++++++++++------- src/core/api/track_choice_manager.ts | 29 +++--- 3 files changed, 181 insertions(+), 82 deletions(-) diff --git a/doc/api/index.md b/doc/api/index.md index 7d4e9816b8..a9cb37fff0 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -1315,64 +1315,75 @@ During this period of time: _arguments_: ``Array.`` -Update the audio language preferences at any time. +Allows the RxPlayer to choose an initial audio track, based on language +preferences, codec preferences or both. -This method takes an array of objects describing the languages wanted: -```js -{ - language: "fra", // {string} The wanted language - // (ISO 639-1, ISO 639-2 or ISO 639-3 language code) - audioDescription: false // {Boolean} Whether the audio track should be an - // audio description for the visually impaired -} -``` +It is defined as an array of objects, each object describing constraints a +track should respect. + +If the first object - defining the first set of constraints - can not be +respected under the currently available audio tracks, the RxPlayer will skip +it and check with the second object and so on. +As such, this array should be sorted by order of preference: from the most +wanted constraints to the least. -Optionally, you can ask for tracks having specific codecs by adding a `codec` -property: +Here is all the possible constraints you can set in any one of those objects +(note that all properties are optional here, only those set will have an effect +on which tracks will be filtered): ```js -// Example: English tracks in Dolby Digital Plus { - language: "eng", - audioDescription: false, - codec: { - test: /ec-3/, // RegExp validating the codec you want. - all: true, // Whether all the profiles (i.e. Representation) in a track - // should be checked to have codecs compatible to that RegExp. - // If `true`, we will only choose a track if every profiles for - // it have a codec that is validated by that RegExp. + language: "fra", // {string|undefined} The language the track should be in + // (in preference as an ISO 639-1, ISO 639-2 or ISO 639-3 + // language code). + // If not set or set to `undefined`, the RxPlayer won't + // filter based on the language of the track. + + audioDescription: false // {Boolean|undefined} Whether the audio track should + // be an audio description for the visually impaired + // or not. + // If not set or set to `undefined`, the RxPlayer + // won't filter based on that status. + + codec: { // {Object|undefined} Constraints about the codec wanted. + // if not set or set to `undefined` we won't filter based on codecs. + + test: /ec-3/, // {RegExp} RegExp validating the type of codec you want. + + all: true, // {Boolean} Whether all the profiles (i.e. Representation) in a + // track should be checked against the RegExp given in `test`. + // If `true`, we will only choose a track if EVERY profiles for + // it have a codec information that is validated by that RegExp. // If `false`, we will choose a track if we know that at least - // a single profile from it has a codec validated by that RegExp. + // A SINGLE profile from it has codec information validated by + // that RegExp. } } ``` -All elements in that Array should be set in preference order: from the most -preferred to the least preferred. - -When encountering a new Period or a new content, the RxPlayer will then try to -choose its audio track by comparing what is available with your current -preferences (i.e. if the most preferred is not available, it will look if the -second one is etc.). +This logic is ran each time a new `Period` with audio tracks is loaded by the +RxPlayer. This means at the start of the content, but also when [pre-]loading a +new DASH `Period` or a new MetaPlaylist `content`. -Please note that those preferences will only apply either to the next loaded -content or to the next newly encountered Period. +Please note that those preferences won't be re-applied once the logic was +already run for a given `Period`. Simply put, once set this preference will be applied to all contents but: - the current Period being played (or the current loaded content, in the case - of Smooth streaming). In that case, the current audio preference will stay - in place. + of single-Period contents such as in Smooth streaming). + In that case, the current audio preference will stay in place. - - the Periods which have already been played in the current loaded content. - Those will keep the last set audio preference at the time it was played. + - the Periods which have already been loaded in the current content. + Those will keep their last set audio preferences (e.g. the preferred audio + tracks at the time they were first loaded). To update the current audio track in those cases, you should use the -`setAudioTrack` method. +`setAudioTrack` method once they are currently played. -#### Example + +#### Examples Let's imagine that you prefer to have french or italian over all other audio -languages. If not found, you want to fallback to english. -You will thus call ``setPreferredAudioTracks`` that way. +languages. If not found, you want to fallback to english: ```js player.setPreferredAudioTracks([ @@ -1382,6 +1393,43 @@ player.setPreferredAudioTracks([ ]) ``` +Now let's imagine that you want to have in priority a track that contain at +least one profile in Dolby Digital Plus (ec-3 codec) without caring about the +language: +```js +player.setPreferredAudioTracks([ { codec: { all: false, test: /ec-3/ } ]); +``` + +At last, let's combine both examples by preferring french over itialian, italian +over english while preferring it to be in Dolby Digital Plus: +```js + +player.setPreferredAudioTracks([ + { + language: "fra", + audioDescription: false, + codec: { all: false, test: /ec-3/ } + }, + + // We still prefer non-DD+ french over DD+ italian + { language: "fra", audioDescription: false }, + + { + language: "ita", + audioDescription: false, + codec: { all: false, test: /ec-3/ } + }, + { language: "ita", audioDescription: false }, + + { + language: "eng", + audioDescription: false, + codec: { all: false, test: /ec-3/ } + }, + { language: "eng", audioDescription: false } +]); +``` + --- :warning: This option will have no effect in _DirectFile_ mode diff --git a/doc/api/player_options.md b/doc/api/player_options.md index f2eea17075..a2f0ff9107 100644 --- a/doc/api/player_options.md +++ b/doc/api/player_options.md @@ -224,53 +224,60 @@ mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). ### preferredAudioTracks ####################################################### -_type_: ``Array.`` +_type_: ``Array.`` _defaults_: ``[]`` -Set the initial audio tracks preferences. +This option allows to help the RxPlayer choose an initial audio track based on +either language preferences, codec preferences or both. -This option takes an array of objects describing the languages wanted: -```js -{ - language: "fra", // {string} The wanted language - // (ISO 639-1, ISO 639-2 or ISO 639-3 language code) - audioDescription: false // {Boolean} Whether the audio track should be an - // audio description for the visually impaired -} -``` +It is defined as an array of objects, each object describing constraints a +track should respect. + +If the first object - defining the first set of constraints - can not be +respected under the currently available audio tracks, the RxPlayer will skip +it and check with the second object and so on. +As such, this array should be sorted by order of preference: from the most +wanted constraints to the least. -Optionally, you can ask for tracks having specific codecs by adding a `codec` -property: +Here is all the possible constraints you can set in any one of those objects +(note that all properties are optional here, only those set will have an effect +on which tracks will be filtered): ```js -// Example: English tracks in Dolby Digital Plus { - language: "eng", - audioDescription: false, - codec: { - test: /ec-3/, // RegExp validating the codec you want. - all: true, // Whether all the profiles (i.e. Representation) in a track - // should be checked to have codecs compatible to that RegExp. - // If `true`, we will only choose a track if every profiles for - // it have a codec that is validated by that RegExp. + language: "fra", // {string|undefined} The language the track should be in + // (in preference as an ISO 639-1, ISO 639-2 or ISO 639-3 + // language code). + // If not set or set to `undefined`, the RxPlayer won't + // filter based on the language of the track. + + audioDescription: false // {Boolean|undefined} Whether the audio track should + // be an audio description for the visually impaired + // or not. + // If not set or set to `undefined`, the RxPlayer + // won't filter based on that status. + + codec: { // {Object|undefined} Constraints about the codec wanted. + // if not set or set to `undefined` we won't filter based on codecs. + + test: /ec-3/, // {RegExp} RegExp validating the type of codec you want. + + all: true, // {Boolean} Whether all the profiles (i.e. Representation) in a + // track should be checked against the RegExp given in `test`. + // If `true`, we will only choose a track if EVERY profiles for + // it have a codec information that is validated by that RegExp. // If `false`, we will choose a track if we know that at least - // a single profile from it has a codec validated by that RegExp. + // A SINGLE profile from it has codec information validated by + // that RegExp. } } ``` -All elements in that Array should be set in preference order: from the most -preferred to the least preferred. - -When loading a content, the RxPlayer will then try to choose its audio track by -comparing what is available with your current preferences (i.e. if the most -preferred is not available, it will look if the second one etc.). - This array of preferrences can be updated at any time through the ``setPreferredAudioTracks`` method, documented [here](./index.md#meth-getPreferredAudioTracks). -#### Example +#### Examples Let's imagine that you prefer to have french or italian over all other audio languages. If not found, you want to fallback to english: @@ -285,6 +292,45 @@ const player = new RxPlayer({ }); ``` +Now let's imagine that you want to have in priority a track that contain at +least one profile in Dolby Digital Plus (ec-3 codec) without caring about the +language: +```js +const player = new RxPlayer({ + preferredAudioTracks: [ { codec: { all: false, test: /ec-3/ } ] +}); +``` + +At last, let's combine both examples by preferring french over itialian, italian +over english while preferring it to be in Dolby Digital Plus: +```js +const player = new RxPlayer({ + preferredAudioTracks: [ + { + language: "fra", + audioDescription: false, + codec: { all: false, test: /ec-3/ } + }, + + // We still prefer non-DD+ french over DD+ italian + { language: "fra", audioDescription: false }, + + { + language: "ita", + audioDescription: false, + codec: { all: false, test: /ec-3/ } + }, + { language: "ita", audioDescription: false }, + + { + language: "eng", + audioDescription: false, + codec: { all: false, test: /ec-3/ } + }, + { language: "eng", audioDescription: false } + ] +``` + --- :warning: This option will have no effect for contents loaded in _DirectFile_ diff --git a/src/core/api/track_choice_manager.ts b/src/core/api/track_choice_manager.ts index fab239b605..709e990465 100644 --- a/src/core/api/track_choice_manager.ts +++ b/src/core/api/track_choice_manager.ts @@ -37,8 +37,8 @@ import takeFirstSet from "../../utils/take_first_set"; /** Single preference for an audio track Adaptation. */ export type IAudioTrackPreference = null | - { language : string; - audioDescription : boolean; + { language? : string; + audioDescription? : boolean; codec? : { all: boolean; test: RegExp; }; }; @@ -107,8 +107,8 @@ interface ITMPeriodInfos { period : Period; /** Audio track preference once normalized by the TrackChoiceManager. */ type INormalizedPreferredAudioTrack = null | - { normalized : string; - audioDescription : boolean; + { normalized? : string; + audioDescription? : boolean; codec? : { all: boolean; test: RegExp; }; }; @@ -128,7 +128,8 @@ function normalizeAudioTracks( ) : INormalizedPreferredAudioTrack[] { return tracks.map(t => t == null ? t : - { normalized: normalizeLanguage(t.language), + { normalized: t.language === undefined ? undefined : + normalizeLanguage(t.language), audioDescription: t.audioDescription, codec: t.codec }); } @@ -847,16 +848,20 @@ function findFirstOptimalAudioAdaptation( } const foundAdaptation = arrayFind(audioAdaptations, (audioAdaptation) => { - const language = audioAdaptation.normalizedLanguage ?? ""; - if (language !== preferredAudioTrack.normalized) { - return false; + if (preferredAudioTrack.normalized !== undefined) { + const language = audioAdaptation.normalizedLanguage ?? ""; + if (language !== preferredAudioTrack.normalized) { + return false; + } } - if (preferredAudioTrack.audioDescription) { - if (audioAdaptation.isAudioDescription !== true) { + if (preferredAudioTrack.audioDescription !== undefined) { + if (preferredAudioTrack.audioDescription) { + if (audioAdaptation.isAudioDescription !== true) { + return false; + } + } else if (audioAdaptation.isAudioDescription === true) { return false; } - } else if (audioAdaptation.isAudioDescription === true) { - return false; } if (preferredAudioTrack.codec === undefined) { return true; From fcb0dc73db9e8a29cf85abfc5e4c38f217810f70 Mon Sep 17 00:00:00 2001 From: Mattias Palmgren Date: Wed, 8 Apr 2020 09:13:39 +0200 Subject: [PATCH 07/72] manifest: Parse information about sign interpretation --- src/core/api/track_choice_manager.ts | 13 +++++- src/manifest/adaptation.ts | 13 +++++- .../manifest/dash/parse_adaptation_sets.ts | 40 ++++++++++++++++++- .../metaplaylist/metaplaylist_parser.ts | 1 + src/parsers/manifest/types.ts | 5 +++ 5 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/core/api/track_choice_manager.ts b/src/core/api/track_choice_manager.ts index c5b23267d9..f22deeebb5 100644 --- a/src/core/api/track_choice_manager.ts +++ b/src/core/api/track_choice_manager.ts @@ -71,6 +71,7 @@ interface ITMVideoRepresentation { id : string|number; /** Video track returned by the TrackChoiceManager. */ export interface ITMVideoTrack { id : number|string; + signInterpreted?: boolean; representations: ITMVideoRepresentation[]; } /** Audio track from a list of audio tracks returned by the TrackChoiceManager. */ @@ -579,9 +580,13 @@ export default class TrackChoiceManager { if (chosenVideoAdaptation == null) { return null; } - return { id: chosenVideoAdaptation.id, + const videoTrack: ITMVideoTrack = { id: chosenVideoAdaptation.id, representations: chosenVideoAdaptation.representations .map(parseVideoRepresentation) }; + if (chosenVideoAdaptation.isSignInterpreted === true) { + videoTrack.signInterpreted = true; + } + return videoTrack; } /** @@ -668,12 +673,16 @@ export default class TrackChoiceManager { return videoInfos.adaptations .map((adaptation) => { - return { + const formatted: ITMVideoTrackListItem = { id: adaptation.id, active: currentId === null ? false : currentId === adaptation.id, representations: adaptation.representations.map(parseVideoRepresentation), }; + if (adaptation.isSignInterpreted === true) { + formatted.signInterpreted = true; + } + return formatted; }); } diff --git a/src/manifest/adaptation.ts b/src/manifest/adaptation.ts index aed09fcddd..c098d41b5f 100644 --- a/src/manifest/adaptation.ts +++ b/src/manifest/adaptation.ts @@ -56,6 +56,7 @@ export interface IRepresentationInfos { bufferType: IAdaptationType; isAudioDescription? : boolean; isClosedCaption? : boolean; isDub? : boolean; + isSignInterpreted?: boolean; normalizedLanguage? : string; } /** Type for the `representationFilter` API. */ @@ -90,6 +91,12 @@ export default class Adaptation { /** Whether this Adaptation contains closed captions for the hard-of-hearing. */ public isClosedCaption? : boolean; + /** + * If true this Adaptation is sign interpreted: which is a + * variant in sign language. + */ + public isSignInterpreted? : boolean; + /** * If `true`, this Adaptation is a "dub", meaning it was recorded in another * language than the original one. @@ -161,6 +168,9 @@ export default class Adaptation { if (parsedAdaptation.isDub !== undefined) { this.isDub = parsedAdaptation.isDub; } + if (parsedAdaptation.isSignInterpreted !== undefined) { + this.isSignInterpreted = parsedAdaptation.isSignInterpreted; + } this.representations = argsRepresentations .map(representation => new Representation(representation)) @@ -175,7 +185,8 @@ export default class Adaptation { normalizedLanguage: this.normalizedLanguage, isClosedCaption: this.isClosedCaption, isDub: this.isDub, - isAudioDescription: this.isAudioDescription }); + isAudioDescription: this.isAudioDescription, + isSignInterpreted: this.isSignInterpreted }); }); // for manuallyAdded adaptations (not in the manifest) diff --git a/src/parsers/manifest/dash/parse_adaptation_sets.ts b/src/parsers/manifest/dash/parse_adaptation_sets.ts index 6d867ef462..084ee4a837 100644 --- a/src/parsers/manifest/dash/parse_adaptation_sets.ts +++ b/src/parsers/manifest/dash/parse_adaptation_sets.ts @@ -103,6 +103,23 @@ function isHardOfHearing( accessibility.value === "2"); } +/** + * Detect if the accessibility given defines an adaptation for the hearing impaired + * Based on DASH-IF 4.3. + * @param {Object} accessibility + * @returns {Boolean} + */ +function isHearingImpaired( + accessibility? : { schemeIdUri? : string; value? : string } +) : boolean { + if (accessibility == null) { + return false; + } + + return (accessibility.schemeIdUri === "urn:mpeg:dash:role:2011" && + accessibility.value === "sign"); +} + /** * Contruct Adaptation ID from the information we have. * @param {Object} adaptation @@ -113,7 +130,12 @@ function isHardOfHearing( function getAdaptationID( adaptation : IAdaptationSetIntermediateRepresentation, representations : IParsedRepresentation[], - infos : { isClosedCaption? : boolean; isAudioDescription? : boolean; type : string } + infos : { + isClosedCaption? : boolean; + isAudioDescription? : boolean; + isSignInterpreted?: boolean; + type : string; + } ) : string { if (isNonEmptyString(adaptation.attributes.id)) { return adaptation.attributes.id; @@ -129,6 +151,9 @@ function getAdaptationID( if (infos.isAudioDescription === true) { idString += "-ad"; } + if (infos.isSignInterpreted === true) { + idString += "-si"; + } if (isNonEmptyString(adaptation.attributes.contentType)) { idString += `-${adaptation.attributes.contentType}`; } @@ -268,10 +293,17 @@ export default function parseAdaptationSets( accessibility != null && isVisuallyImpaired(accessibility) ? true : undefined; + + const isSignInterpreted = type === "video" && + accessibility != null && + isHearingImpaired(accessibility) ? true : + undefined; + const adaptationID = newID = getAdaptationID(adaptation, - representations, + representations, { isClosedCaption, isAudioDescription, + isSignInterpreted, type }); const parsedAdaptationSet : IParsedAdaptation = { id: adaptationID, representations, @@ -289,6 +321,10 @@ export default function parseAdaptationSets( parsedAdaptationSet.isDub = true; } + if (isSignInterpreted === true) { + parsedAdaptationSet.isSignInterpreted = true; + } + const adaptationsOfTheSameType = parsedAdaptations[type]; if (adaptationsOfTheSameType === undefined) { parsedAdaptations[type] = [parsedAdaptationSet]; diff --git a/src/parsers/manifest/metaplaylist/metaplaylist_parser.ts b/src/parsers/manifest/metaplaylist/metaplaylist_parser.ts index 09496e4461..e02c823ec9 100644 --- a/src/parsers/manifest/metaplaylist/metaplaylist_parser.ts +++ b/src/parsers/manifest/metaplaylist/metaplaylist_parser.ts @@ -228,6 +228,7 @@ function createManifest( closedCaption: currentAdaptation.isClosedCaption, isDub: currentAdaptation.isDub, language: currentAdaptation.language, + isSignInterpreted: currentAdaptation.isSignInterpreted, }); acc[type] = adaptationsForCurrentType; } diff --git a/src/parsers/manifest/types.ts b/src/parsers/manifest/types.ts index 8260e0886e..cf81854193 100644 --- a/src/parsers/manifest/types.ts +++ b/src/parsers/manifest/types.ts @@ -123,6 +123,11 @@ export interface IParsedAdaptation { * than the original(s) one(s). */ isDub? : boolean; + /** + * If true this Adaptation is in a sign interpreted: which is a variant of the + * video with sign language. + */ + isSignInterpreted? : boolean; /** * Language the `Adaptation` is in. * Not set if unknown or if it makes no sense for the current track. From f5c96e6c684dde81afdd93538da478552ce7597e Mon Sep 17 00:00:00 2001 From: Paul Rosset Date: Fri, 27 Sep 2019 16:34:18 +0200 Subject: [PATCH 08/72] feat(audio-only): Create an audio only MediaSource --- demo/standalone/index.html | 30 +++++++++++++++++++++++- src/core/api/public_api.ts | 20 ++++++++++++++++ src/core/api/track_choice_manager.ts | 15 ++++++++++++ src/core/buffers/period/period_buffer.ts | 16 ++++++++----- 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/demo/standalone/index.html b/demo/standalone/index.html index 5b74b49076..190f04ad5f 100644 --- a/demo/standalone/index.html +++ b/demo/standalone/index.html @@ -6,13 +6,41 @@ RxPlayer - CANAL+ (stand-alone demo) - + + diff --git a/src/core/api/public_api.ts b/src/core/api/public_api.ts index 5cb92c093f..6099648101 100644 --- a/src/core/api/public_api.ts +++ b/src/core/api/public_api.ts @@ -1800,6 +1800,26 @@ class Player extends EventEmitter { return this._priv_preferredTextTracks.next(tracks); } + setPreferredVideoTracks() : void { + console.warn("setPreferredVideoTracks"); + } + + disableVideoTrack() : void { + console.warn("CONTENTINFOS", this._priv_contentInfos) + if (this._priv_contentInfos === null) { + return; + } + const { currentPeriod } = this._priv_contentInfos; + if (this._priv_trackChoiceManager === null || currentPeriod === null) { + console.warn("POLOO") + return; + } + console.warn(currentPeriod) + return this._priv_trackChoiceManager.disableVideoTrack(currentPeriod); + // return this._priv_trackChoiceManager.disableTextTrack(currentPeriod); + // console.warn("disableVideoTrack"); + } + /** * @returns {Array.|null} * @deprecated diff --git a/src/core/api/track_choice_manager.ts b/src/core/api/track_choice_manager.ts index 54e09a1fb0..4e90dfe735 100644 --- a/src/core/api/track_choice_manager.ts +++ b/src/core/api/track_choice_manager.ts @@ -498,6 +498,21 @@ export default class TrackChoiceManager { textInfos.adaptation$.next(null); } + public disableVideoTrack(period : Period) : void { + const periodItem = getPeriodItem(this._periods, period); + console.warn("ITEM", periodItem); + const videoInfos = periodItem && periodItem.video; + if (!videoInfos) { + throw new Error("TrackManager: Given Period not found."); + } + const chosenVideoAdaptation = this._videoChoiceMemory.get(period); + if (chosenVideoAdaptation === null) { + return; + } + this._videoChoiceMemory.set(period, null); + videoInfos.adaptation$.next(null); + } + /** * Returns an object describing the chosen audio track for the given audio * Period. diff --git a/src/core/buffers/period/period_buffer.ts b/src/core/buffers/period/period_buffer.ts index 979045e7e7..1cab20c5d1 100644 --- a/src/core/buffers/period/period_buffer.ts +++ b/src/core/buffers/period/period_buffer.ts @@ -120,12 +120,16 @@ export default function PeriodBuffer({ let cleanBuffer$ : Observable; if (sourceBufferStatus.type === "set") { - log.info(`Buffer: Clearing previous ${bufferType} SourceBuffer`); - const previousQSourceBuffer = sourceBufferStatus.value; - cleanBuffer$ = previousQSourceBuffer - .removeBuffer(period.start, - period.end == null ? Infinity : - period.end); + // log.info(`Buffer: Clearing previous ${bufferType} SourceBuffer`); + // const previousQSourceBuffer = sourceBufferStatus.value; + // cleanBuffer$ = previousQSourceBuffer + // .removeBuffer(period.start, + // period.end == null ? Infinity : + // period.end); + return observableOf(EVENTS.needsMediaSourceReload({ + currentTime: 20, + isPaused: false, + })); } else { if (sourceBufferStatus.type === "unset") { sourceBuffersStore.disableSourceBuffer(bufferType); From 35fe8ecff7dfa85f05ff3688e7c8f9fe42b786a0 Mon Sep 17 00:00:00 2001 From: Paul Rosset Date: Tue, 1 Oct 2019 19:48:20 +0200 Subject: [PATCH 09/72] feat(audio-only): Possible to play a content in audio only while playing --- demo/standalone/index.html | 19 +++++----- src/core/api/public_api.ts | 20 ++++++----- src/core/api/track_choice_manager.ts | 4 +-- .../buffers/adaptation/adaptation_buffer.ts | 7 ++-- src/core/buffers/events_generators.ts | 8 +++-- src/core/buffers/period/period_buffer.ts | 36 +++++++++++++------ src/core/buffers/types.ts | 3 +- src/core/init/load_on_media_source.ts | 4 +++ 8 files changed, 64 insertions(+), 37 deletions(-) diff --git a/demo/standalone/index.html b/demo/standalone/index.html index 190f04ad5f..ffc09148c9 100644 --- a/demo/standalone/index.html +++ b/demo/standalone/index.html @@ -6,12 +6,13 @@ RxPlayer - CANAL+ (stand-alone demo) - + + From a0feba2e69861175facdb9e4c040f25122dd65c4 Mon Sep 17 00:00:00 2001 From: Paul Rosset Date: Fri, 4 Oct 2019 15:18:27 +0200 Subject: [PATCH 12/72] feat(audio-only): adjust naming and add doc --- doc/api/index.md | 23 +++++++++++++++++++++++ src/core/api/public_api.ts | 2 +- src/core/api/track_choice_manager.ts | 1 - 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/doc/api/index.md b/doc/api/index.md index bb4e5adb8c..9490048c96 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -63,6 +63,8 @@ - [getPreferredAudioTracks](#meth-getPreferredAudioTracks) - [setPreferredTextTracks](#meth-setPreferredTextTracks) - [getPreferredTextTracks](#meth-getPreferredTextTracks) + - [disableVideoTrack](#meth-disableVideoTrack) + - [disableAudioOnly](#meth-disableAudioOnly) - [getCurrentAdaptations](#meth-getCurrentAdaptations) - [getCurrentRepresentations](#meth-getCurrentRepresentations) - [dispose](#meth-dispose) @@ -1550,6 +1552,27 @@ it was called: } ``` + +### disableVideoTrack ##################################################### + +_return value_: ``void`` + +Disable the video track adaptation. + +This permits to play in **Audio only** mode. + +> Will enter in `RELOADING` state for a short period. + + +### disableAudioOnly ##################################################### + +_return value_: ``void`` + +Disable the **Audio only** mode, by switching from audio adaptation only to video/audio adaptations. + +This permits to play in **normal** mode with video. + +> Will enter in `RELOADING` state for a short period. ### getManifest ################################################################ diff --git a/src/core/api/public_api.ts b/src/core/api/public_api.ts index e1f410992a..cfbaa44d09 100644 --- a/src/core/api/public_api.ts +++ b/src/core/api/public_api.ts @@ -1811,7 +1811,7 @@ class Player extends EventEmitter { return this._priv_trackChoiceManager.disableVideoTrack(currentPeriod); } - enableVideoTrack() { + disableAudioOnly() : void { if (!this._priv_contentInfos) { return; } diff --git a/src/core/api/track_choice_manager.ts b/src/core/api/track_choice_manager.ts index 8569492503..e67c6cd93e 100644 --- a/src/core/api/track_choice_manager.ts +++ b/src/core/api/track_choice_manager.ts @@ -500,7 +500,6 @@ export default class TrackChoiceManager { public disableVideoTrack(period : Period) : void { const periodItem = getPeriodItem(this._periods, period); - console.warn("ITEM", periodItem); const videoInfos = periodItem && periodItem.video; if (!videoInfos) { throw new Error("TrackManager: Given Period not found."); From ed0a6972e99fb83b3eca709b20c69cd6797c5072 Mon Sep 17 00:00:00 2001 From: grenault73 Date: Thu, 2 Apr 2020 18:40:04 +0200 Subject: [PATCH 13/72] misc: make disapear the 'audio only' notion --- doc/api/index.md | 15 +------------- src/core/api/public_api.ts | 11 ---------- src/core/api/track_choice_manager.ts | 10 +++++----- .../buffers/adaptation/adaptation_buffer.ts | 6 +----- src/core/buffers/events_generators.ts | 8 +++----- src/core/buffers/period/period_buffer.ts | 20 ++++--------------- src/core/buffers/types.ts | 3 +-- 7 files changed, 15 insertions(+), 58 deletions(-) diff --git a/doc/api/index.md b/doc/api/index.md index 9490048c96..ca768fb880 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -1557,20 +1557,7 @@ it was called: _return value_: ``void`` -Disable the video track adaptation. - -This permits to play in **Audio only** mode. - -> Will enter in `RELOADING` state for a short period. - - -### disableAudioOnly ##################################################### - -_return value_: ``void`` - -Disable the **Audio only** mode, by switching from audio adaptation only to video/audio adaptations. - -This permits to play in **normal** mode with video. +Deactivate the current text track, if one. > Will enter in `RELOADING` state for a short period. diff --git a/src/core/api/public_api.ts b/src/core/api/public_api.ts index cfbaa44d09..8608f430e0 100644 --- a/src/core/api/public_api.ts +++ b/src/core/api/public_api.ts @@ -1811,17 +1811,6 @@ class Player extends EventEmitter { return this._priv_trackChoiceManager.disableVideoTrack(currentPeriod); } - disableAudioOnly() : void { - if (!this._priv_contentInfos) { - return; - } - const { currentPeriod } = this._priv_contentInfos; - if (this._priv_trackChoiceManager === null || currentPeriod === null) { - return; - } - return this._priv_trackChoiceManager.setInitialVideoTrack(currentPeriod, true); - } - /** * @returns {Array.|null} * @deprecated diff --git a/src/core/api/track_choice_manager.ts b/src/core/api/track_choice_manager.ts index e67c6cd93e..f584fc465e 100644 --- a/src/core/api/track_choice_manager.ts +++ b/src/core/api/track_choice_manager.ts @@ -361,7 +361,7 @@ export default class TrackChoiceManager { * - the last choice for this period, if one * @param {Period} period - The concerned Period. */ - public setInitialVideoTrack(period : Period, isAudioOnly = false) : void { + public setInitialVideoTrack(period : Period) : void { const periodItem = getPeriodItem(this._periods, period); const videoInfos = periodItem != null ? periodItem.video : null; @@ -374,11 +374,11 @@ export default class TrackChoiceManager { period.adaptations.video; const chosenVideoAdaptation = this._videoChoiceMemory.get(period); - if (chosenVideoAdaptation === null && !isAudioOnly) { + if (chosenVideoAdaptation === null) { // If the Period was previously without video, keep it that way videoInfos.adaptation$.next(null); } else if (chosenVideoAdaptation === undefined || - !arrayIncludes(videoAdaptations, chosenVideoAdaptation) + !arrayIncludes(videoAdaptations, chosenVideoAdaptation) ) { const optimalAdaptation = videoAdaptations[0]; this._videoChoiceMemory.set(period, optimalAdaptation); @@ -500,8 +500,8 @@ export default class TrackChoiceManager { public disableVideoTrack(period : Period) : void { const periodItem = getPeriodItem(this._periods, period); - const videoInfos = periodItem && periodItem.video; - if (!videoInfos) { + const videoInfos = periodItem?.video; + if (videoInfos === undefined) { throw new Error("TrackManager: Given Period not found."); } const chosenVideoAdaptation = this._videoChoiceMemory.get(period); diff --git a/src/core/buffers/adaptation/adaptation_buffer.ts b/src/core/buffers/adaptation/adaptation_buffer.ts index a2c4e760c2..4e65ef48ca 100644 --- a/src/core/buffers/adaptation/adaptation_buffer.ts +++ b/src/core/buffers/adaptation/adaptation_buffer.ts @@ -196,11 +196,7 @@ export default function AdaptationBuffer({ // To do that properly, we need to reload the MediaSource if (directManualBitrateSwitching && estimate.manual && i !== 0) { return clock$.pipe(take(1), - map(t => EVENTS.needsMediaSourceReload({ - currentTime: t.currentTime, - isPaused: t.isPaused, - isAudioOnly: false, - }))); + map(t => EVENTS.needsMediaSourceReload(t))); } const representationChange$ = diff --git a/src/core/buffers/events_generators.ts b/src/core/buffers/events_generators.ts index 05799f1a51..1432e0a793 100644 --- a/src/core/buffers/events_generators.ts +++ b/src/core/buffers/events_generators.ts @@ -125,13 +125,11 @@ const EVENTS = { needsMediaSourceReload( { currentTime, - isPaused, - isAudioOnly } : { currentTime : number; - isPaused : boolean, - isAudioOnly : boolean } + isPaused } : { currentTime : number; + isPaused : boolean; } ) : INeedsMediaSourceReload { return { type: "needs-media-source-reload", - value: { currentTime, isPaused, isAudioOnly } }; + value: { currentTime, isPaused } }; }, needsDecipherabilityFlush( diff --git a/src/core/buffers/period/period_buffer.ts b/src/core/buffers/period/period_buffer.ts index fe89fab7d2..7bbe685446 100644 --- a/src/core/buffers/period/period_buffer.ts +++ b/src/core/buffers/period/period_buffer.ts @@ -125,11 +125,7 @@ export default function PeriodBuffer({ .removeBuffer(period.start, period.end == null ? Infinity : period.end); - return clock$.pipe(map(tick => EVENTS.needsMediaSourceReload({ - currentTime: tick.currentTime, - isPaused: tick.isPaused, - isAudioOnly: true, - }))); + return clock$.pipe(map(tick => EVENTS.needsMediaSourceReload(tick))); } else { if (sourceBufferStatus.type === "unset") { sourceBuffersStore.disableSourceBuffer(bufferType); @@ -144,13 +140,9 @@ export default function PeriodBuffer({ } // Check if we are in AudioOnly mode, if yes, revert to normal mode - const videoStatus = sourceBuffersStore.getStatus('video'); + const videoStatus = sourceBuffersStore.getStatus("video"); if (videoStatus.type === "disabled" && adaptation.type === "video") { - return clock$.pipe(map(tick => EVENTS.needsMediaSourceReload({ - currentTime: tick.currentTime, - isPaused: tick.isPaused, - isAudioOnly: false, - }))); + return clock$.pipe(map(tick => EVENTS.needsMediaSourceReload(tick))); } log.info(`Buffer: Updating ${bufferType} adaptation`, adaptation, period); @@ -167,11 +159,7 @@ export default function PeriodBuffer({ adaptation, tick); if (strategy.type === "needs-reload") { - return observableOf(EVENTS.needsMediaSourceReload({ - currentTime: tick.currentTime, - isPaused: tick.isPaused, - isAudioOnly: false, - })); + return observableOf(EVENTS.needsMediaSourceReload(tick)); } const cleanBuffer$ = strategy.type === "clean-buffer" ? diff --git a/src/core/buffers/types.ts b/src/core/buffers/types.ts index bed8c31080..66af9009e2 100644 --- a/src/core/buffers/types.ts +++ b/src/core/buffers/types.ts @@ -157,8 +157,7 @@ export interface ICompletedBufferEvent { type: "complete-buffer"; // The MediaSource needs to be reloaded to continue export interface INeedsMediaSourceReload { type: "needs-media-source-reload"; value: { currentTime : number; - isPaused : boolean; - isAudioOnly : boolean }; } + isPaused : boolean; }; } // Emitted after the buffers have been cleaned due to an update of the // decipherability status of some segment. From f3dcbcbf18134a124c080e2825bd698dcffc36ea Mon Sep 17 00:00:00 2001 From: grenault73 Date: Fri, 10 Apr 2020 18:03:02 +0200 Subject: [PATCH 14/72] core: better null adaptation handling in period buffer --- doc/api/index.md | 21 ++++++++-------- src/core/api/public_api.ts | 25 +++++++++++-------- src/core/api/track_choice_manager.ts | 5 ++++ .../buffers/adaptation/adaptation_buffer.ts | 1 + src/core/buffers/period/period_buffer.ts | 10 +++++--- 5 files changed, 37 insertions(+), 25 deletions(-) diff --git a/doc/api/index.md b/doc/api/index.md index ca768fb880..0af8017c73 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -59,12 +59,11 @@ - [setTextTrack](#meth-setTextTrack) - [disableTextTrack](#meth-disableTextTrack) - [setVideoTrack](#meth-setVideoTrack) + - [disableVideoTrack](#meth-disableVideoTrack) - [setPreferredAudioTracks](#meth-setPreferredAudioTracks) - [getPreferredAudioTracks](#meth-getPreferredAudioTracks) - [setPreferredTextTracks](#meth-setPreferredTextTracks) - [getPreferredTextTracks](#meth-getPreferredTextTracks) - - [disableVideoTrack](#meth-disableVideoTrack) - - [disableAudioOnly](#meth-disableAudioOnly) - [getCurrentAdaptations](#meth-getCurrentAdaptations) - [getCurrentRepresentations](#meth-getCurrentRepresentations) - [dispose](#meth-dispose) @@ -1323,6 +1322,16 @@ During this period of time: --- + +### disableVideoTrack ##################################################### + +_return value_: ``void`` + +Deactivate the current video track, if one. + +> Will enter in `RELOADING` state for a short period. + + ### setPreferredAudioTracks #################################################### @@ -1552,14 +1561,6 @@ it was called: } ``` - -### disableVideoTrack ##################################################### - -_return value_: ``void`` - -Deactivate the current text track, if one. - -> Will enter in `RELOADING` state for a short period. ### getManifest ################################################################ diff --git a/src/core/api/public_api.ts b/src/core/api/public_api.ts index 8608f430e0..c5dd9f2fe6 100644 --- a/src/core/api/public_api.ts +++ b/src/core/api/public_api.ts @@ -1760,6 +1760,20 @@ class Player extends EventEmitter { } } + /** + * Disable video track for the current content. + */ + disableVideoTrack() : void { + if (this._priv_contentInfos === null) { + return; + } + const { currentPeriod } = this._priv_contentInfos; + if (this._priv_trackChoiceManager === null || currentPeriod === null) { + return; + } + return this._priv_trackChoiceManager.disableVideoTrack(currentPeriod); + } + /** * Returns the current list of preferred audio tracks, in preference order. * @returns {Array.} @@ -1800,17 +1814,6 @@ class Player extends EventEmitter { return this._priv_preferredTextTracks.next(tracks); } - disableVideoTrack() : void { - if (this._priv_contentInfos === null) { - return; - } - const { currentPeriod } = this._priv_contentInfos; - if (this._priv_trackChoiceManager === null || currentPeriod === null) { - return; - } - return this._priv_trackChoiceManager.disableVideoTrack(currentPeriod); - } - /** * @returns {Array.|null} * @deprecated diff --git a/src/core/api/track_choice_manager.ts b/src/core/api/track_choice_manager.ts index f584fc465e..2ebd90c9b9 100644 --- a/src/core/api/track_choice_manager.ts +++ b/src/core/api/track_choice_manager.ts @@ -498,6 +498,11 @@ export default class TrackChoiceManager { textInfos.adaptation$.next(null); } + /** + * Disable the current video track for a given period. + * @param {Object} period + * @throws Error - Throws if the period given has not been added + */ public disableVideoTrack(period : Period) : void { const periodItem = getPeriodItem(this._periods, period); const videoInfos = periodItem?.video; diff --git a/src/core/buffers/adaptation/adaptation_buffer.ts b/src/core/buffers/adaptation/adaptation_buffer.ts index 4e65ef48ca..c87127fee2 100644 --- a/src/core/buffers/adaptation/adaptation_buffer.ts +++ b/src/core/buffers/adaptation/adaptation_buffer.ts @@ -124,6 +124,7 @@ export default function AdaptationBuffer({ } : IAdaptationBufferArguments) : Observable> { const directManualBitrateSwitching = options.manualBitrateSwitchingMode === "direct"; const { manifest, period, adaptation } = content; + // The buffer goal ratio limits the wanted buffer ahead to determine the // buffer goal. // diff --git a/src/core/buffers/period/period_buffer.ts b/src/core/buffers/period/period_buffer.ts index 7bbe685446..7f2936d0f3 100644 --- a/src/core/buffers/period/period_buffer.ts +++ b/src/core/buffers/period/period_buffer.ts @@ -125,7 +125,9 @@ export default function PeriodBuffer({ .removeBuffer(period.start, period.end == null ? Infinity : period.end); - return clock$.pipe(map(tick => EVENTS.needsMediaSourceReload(tick))); + if (SourceBuffersStore.isNative(bufferType)) { + return clock$.pipe(map(tick => EVENTS.needsMediaSourceReload(tick))); + } } else { if (sourceBufferStatus.type === "unset") { sourceBuffersStore.disableSourceBuffer(bufferType); @@ -139,9 +141,9 @@ export default function PeriodBuffer({ ); } - // Check if we are in AudioOnly mode, if yes, revert to normal mode - const videoStatus = sourceBuffersStore.getStatus("video"); - if (videoStatus.type === "disabled" && adaptation.type === "video") { + if (SourceBuffersStore.isNative(bufferType) && + sourceBuffersStore.getStatus(bufferType).type === "disabled") + { return clock$.pipe(map(tick => EVENTS.needsMediaSourceReload(tick))); } From 3a1040f52abd4fdaab2c251181d0ddea5ed262d0 Mon Sep 17 00:00:00 2001 From: grenault73 Date: Tue, 14 Apr 2020 17:12:10 +0200 Subject: [PATCH 15/72] api: media element video track disabling --- doc/api/index.md | 13 ++++++++++++- doc/api/player_events.md | 4 +++- src/core/api/media_element_track_choice_manager.ts | 10 ++++++++++ src/core/api/public_api.ts | 5 ++++- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/doc/api/index.md b/doc/api/index.md index 0af8017c73..3a1309e377 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -1329,7 +1329,18 @@ _return value_: ``void`` Deactivate the current video track, if one. -> Will enter in `RELOADING` state for a short period. +Will enter in `RELOADING` state for a short period, if not in _DirectFile_ + +--- + +:warning: This option will have no effect in _DirectFile_ mode +(see [loadVideo options](./loadVideo_options.md#prop-transport)) when either : +- No audio track API was supported on the current browser +- The media file tracks are not supported on the browser + +/!\ On Safari browser in _DirectFile_, video playback may continue on same track +even if video tracks are disabled. +--- diff --git a/doc/api/player_events.md b/doc/api/player_events.md index 1e06e8b32b..6cabf1252c 100644 --- a/doc/api/player_events.md +++ b/doc/api/player_events.md @@ -326,7 +326,9 @@ properties: - ``frameRate`` (``string|undefined``): The video framerate. - +A null payload means that video track has been disabled. +/!\ On Safari in _DirectFile_, a video track may have been disabled by user but +still playing on screen. ### availableAudioBitratesChange ############################################### diff --git a/src/core/api/media_element_track_choice_manager.ts b/src/core/api/media_element_track_choice_manager.ts index 88b5a54870..c4244b962a 100644 --- a/src/core/api/media_element_track_choice_manager.ts +++ b/src/core/api/media_element_track_choice_manager.ts @@ -285,6 +285,16 @@ export default class MediaElementTrackChoiceManager } } + /** + * Disable the currently-active video track, if one. + */ + public disableVideoTrack(): void { + for (let i = 0; i < this._videoTracks.length; i++) { + const { nativeTrack } = this._videoTracks[i]; + nativeTrack.selected = false; + } + } + /** * Update the currently active video track by setting the wanted video track's * ID property. diff --git a/src/core/api/public_api.ts b/src/core/api/public_api.ts index c5dd9f2fe6..c57075cf88 100644 --- a/src/core/api/public_api.ts +++ b/src/core/api/public_api.ts @@ -1767,7 +1767,10 @@ class Player extends EventEmitter { if (this._priv_contentInfos === null) { return; } - const { currentPeriod } = this._priv_contentInfos; + const { currentPeriod, isDirectFile } = this._priv_contentInfos; + if (isDirectFile && this._priv_mediaElementTrackChoiceManager !== null) { + return this._priv_mediaElementTrackChoiceManager.disableVideoTrack(); + } if (this._priv_trackChoiceManager === null || currentPeriod === null) { return; } From 1a75165452455c459589713a4804a5527ab49671 Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Thu, 2 Apr 2020 18:45:21 +0200 Subject: [PATCH 16/72] manifest: add the concept of playable Adaptation and Representation and avoid removing unsupported codecs from the Manifest structure --- src/core/api/track_choice_manager.ts | 29 +- .../buffers/adaptation/adaptation_buffer.ts | 7 +- src/manifest/__tests__/adaptation.test.ts | 564 +++++++++--------- .../filter_supported_representations.test.ts | 140 ----- src/manifest/__tests__/representation.test.ts | 256 +++++--- src/manifest/adaptation.ts | 97 +-- .../filter_supported_representations.ts | 46 -- src/manifest/index.ts | 2 +- src/manifest/manifest.ts | 6 +- src/manifest/period.ts | 17 +- src/manifest/representation.ts | 13 +- src/manifest/types.ts | 3 + .../initialization_segment_cache.test.ts | 2 + 13 files changed, 573 insertions(+), 609 deletions(-) delete mode 100644 src/manifest/__tests__/filter_supported_representations.test.ts delete mode 100644 src/manifest/filter_supported_representations.ts diff --git a/src/core/api/track_choice_manager.ts b/src/core/api/track_choice_manager.ts index ecbf63212a..8c3039b21e 100644 --- a/src/core/api/track_choice_manager.ts +++ b/src/core/api/track_choice_manager.ts @@ -230,10 +230,7 @@ export default class TrackChoiceManager { adaptation$ : Subject ) : void { const periodItem = getPeriodItem(this._periods, period); - let adaptations = period.adaptations[bufferType]; - if (adaptations == null) { - adaptations = []; - } + const adaptations = period.getPlayableAdaptations(bufferType); if (periodItem != null) { if (periodItem[bufferType] != null) { log.warn(`TrackChoiceManager: ${bufferType} already added for period`, period); @@ -309,9 +306,7 @@ export default class TrackChoiceManager { } const preferredAudioTracks = this._preferredAudioTracks.getValue(); - const audioAdaptations = period.adaptations.audio === undefined ? - [] : - period.adaptations.audio; + const audioAdaptations = period.getPlayableAdaptations("audio"); const chosenAudioAdaptation = this._audioChoiceMemory.get(period); if (chosenAudioAdaptation === null) { @@ -347,9 +342,7 @@ export default class TrackChoiceManager { } const preferredTextTracks = this._preferredTextTracks.getValue(); - const textAdaptations = period.adaptations.text === undefined ? - [] : - period.adaptations.text; + const textAdaptations = period.getPlayableAdaptations("text"); const chosenTextAdaptation = this._textChoiceMemory.get(period); if (chosenTextAdaptation === null) { // If the Period was previously without text, keep it that way @@ -383,9 +376,7 @@ export default class TrackChoiceManager { } const preferredVideoTracks = this._preferredVideoTracks.getValue(); - const videoAdaptations = period.adaptations.video === undefined ? - [] : - period.adaptations.video; + const videoAdaptations = period.getPlayableAdaptations("video"); const chosenVideoAdaptation = this._videoChoiceMemory.get(period); if (chosenVideoAdaptation === null) { @@ -745,9 +736,7 @@ export default class TrackChoiceManager { const { period, audio: audioItem } = periodItem; - const audioAdaptations = period.adaptations.audio === undefined ? - [] : - period.adaptations.audio; + const audioAdaptations = period.getPlayableAdaptations("audio"); const chosenAudioAdaptation = this._audioChoiceMemory.get(period); if (chosenAudioAdaptation === null || @@ -793,9 +782,7 @@ export default class TrackChoiceManager { const { period, text: textItem } = periodItem; - const textAdaptations = period.adaptations.text === undefined ? - [] : - period.adaptations.text; + const textAdaptations = period.getPlayableAdaptations("text"); const chosenTextAdaptation = this._textChoiceMemory.get(period); if (chosenTextAdaptation === null || @@ -838,9 +825,7 @@ export default class TrackChoiceManager { } const { period, video: videoItem, } = periodItem; - const videoAdaptations = period.adaptations.video === undefined ? - [] : - period.adaptations.video; + const videoAdaptations = period.getPlayableAdaptations("video"); const chosenVideoAdaptation = this._videoChoiceMemory.get(period); if (chosenVideoAdaptation === null || diff --git a/src/core/buffers/adaptation/adaptation_buffer.ts b/src/core/buffers/adaptation/adaptation_buffer.ts index de0a5d2da5..f5e25a18b1 100644 --- a/src/core/buffers/adaptation/adaptation_buffer.ts +++ b/src/core/buffers/adaptation/adaptation_buffer.ts @@ -185,10 +185,9 @@ export default function AdaptationBuffer({ const requestsEvents$ = new Subject(); const abrEvents$ = observableMerge(bufferEvents$, requestsEvents$); - const decipherableRepresentations = adaptation.representations - .filter((representation) => representation.decipherable !== false); + const playableRepresentations = adaptation.getPlayableRepresentations(); - if (decipherableRepresentations.length <= 0) { + if (playableRepresentations.length <= 0) { const noRepErr = new MediaError("NO_PLAYABLE_REPRESENTATION", "No Representation in the chosen " + "Adaptation can be played"); @@ -196,7 +195,7 @@ export default function AdaptationBuffer({ } const abr$ : Observable = abrManager.get$(adaptation.type, - decipherableRepresentations, + playableRepresentations, clock$, abrEvents$) .pipe(deferSubscriptions(), share()); diff --git a/src/manifest/__tests__/adaptation.test.ts b/src/manifest/__tests__/adaptation.test.ts index 3ed3c6f639..a3302bb905 100644 --- a/src/manifest/__tests__/adaptation.test.ts +++ b/src/manifest/__tests__/adaptation.test.ts @@ -19,39 +19,38 @@ import { } from "../adaptation"; import Representation from "../representation"; -const minimalRepresentationIndex = { - getInitSegment() { return null; }, - getSegments() { return []; }, - shouldRefresh() { return false; }, - getFirstPosition() : undefined { return ; }, - getLastPosition() : undefined { return ; }, - checkDiscontinuity() { return -1; }, - isSegmentStillAvailable() : undefined { return ; }, - canBeOutOfSyncError() : true { return true; }, - isFinished() : true { return true; }, - _replace() { /* noop */ }, - _update() { /* noop */ }, - _addSegments() { /* noop */ }, -}; - /* tslint:disable no-unsafe-any */ + +const minimalRepresentationIndex = { getInitSegment() { return null; }, + getSegments() { return []; }, + shouldRefresh() { return false; }, + getFirstPosition() : undefined { return ; }, + getLastPosition() : undefined { return ; }, + checkDiscontinuity() { return -1; }, + isSegmentStillAvailable() : undefined { return ; }, + canBeOutOfSyncError() : true { return true; }, + isFinished() : true { return true; }, + _replace() { /* noop */ }, + _update() { /* noop */ }, + _addSegments() { /* noop */ } }; +const defaultRepresentationSpy = jest.fn(arg => { + return { bitrate: arg.bitrate, + id: arg.id, + isSupported: true, + index: arg.index }; +}); + describe("Manifest - Adaptation", () => { beforeEach(() => { jest.resetModules(); }); + afterEach(() => { + defaultRepresentationSpy.mockClear(); + }); it("should be able to create a minimal Adaptation", () => { - const representationSpy = jest.fn(arg => arg); - const filterSpy = jest.fn((_type, arg) => arg); - - jest.mock("../representation", () => ({ - __esModule: true, - default: representationSpy, - })); - jest.mock("../filter_supported_representations", () => ({ - __esModule: true, - default: filterSpy, - })); + jest.mock("../representation", () => ({ __esModule: true, + default: defaultRepresentationSpy })); const Adaptation = require("../adaptation").default; const args = { id: "12", representations: [], type: "video" }; @@ -68,18 +67,17 @@ describe("Manifest - Adaptation", () => { expect(adaptation.getAvailableBitrates()).toEqual([]); expect(adaptation.getRepresentation("")).toBe(undefined); - expect(representationSpy).not.toHaveBeenCalled(); + expect(defaultRepresentationSpy).not.toHaveBeenCalled(); }); it("should throw if the given adaptation type is not supported", () => { - const representationSpy = jest.fn(arg => arg); - const filterSpy = jest.fn((_type, arg) => arg); - - jest.mock("../representation", () => ({ default: representationSpy })); - jest.mock("../filter_supported_representations", () => ({ default: filterSpy })); + jest.mock("../representation", () => ({ __esModule: true, + default: defaultRepresentationSpy })); const Adaptation = require("../adaptation").default; - const args = { id: "12", representations: [], type: "foo" }; + const args = { id: "12", + representations: [], + type: "foo" }; let adaptation = null; let error = null; try { @@ -94,30 +92,17 @@ describe("Manifest - Adaptation", () => { }); it("should normalize a given language", () => { - const representationSpy = jest.fn(arg => arg); - const filterSpy = jest.fn((_type, arg) => arg); + jest.mock("../representation", () => ({ __esModule: true, + default: defaultRepresentationSpy })); const normalizeSpy = jest.fn((lang : string) => lang + "foo"); - - jest.mock("../representation", () => ({ - __esModule: true, - default: representationSpy, - })); - jest.mock("../filter_supported_representations", () => ({ - __esModule: true, - default: filterSpy, - })); - jest.mock("../../utils/languages", () => ({ - __esModule: true, - default: normalizeSpy, - })); + jest.mock("../../utils/languages", () => ({ __esModule: true, + default: normalizeSpy })); const Adaptation = require("../adaptation").default; - const args1 = { - id: "12", - representations: [], - language: "fr", - type: "video"as "video", - }; + const args1 = { id: "12", + representations: [], + language: "fr", + type: "video"as "video" }; const adaptation1 = new Adaptation(args1); expect(adaptation1.language).toBe("fr"); expect(adaptation1.normalizedLanguage).toBe("frfoo"); @@ -125,12 +110,10 @@ describe("Manifest - Adaptation", () => { expect(normalizeSpy).toHaveBeenCalledWith("fr"); normalizeSpy.mockClear(); - const args2 = { - id: "12", - representations: [], - language: "toto", - type: "video", - }; + const args2 = { id: "12", + representations: [], + language: "toto", + type: "video" }; const adaptation2 = new Adaptation(args2); expect(adaptation2.language).toBe("toto"); expect(adaptation2.normalizedLanguage).toBe("totofoo"); @@ -139,29 +122,16 @@ describe("Manifest - Adaptation", () => { }); it("should not call normalize if no language is given", () => { - const representationSpy = jest.fn(arg => arg); - const filterSpy = jest.fn((_type, arg) => arg); + jest.mock("../representation", () => ({ __esModule: true, + default: defaultRepresentationSpy })); const normalizeSpy = jest.fn((lang : string) => lang + "foo"); - - jest.mock("../representation", () => ({ - __esModule: true, - default: representationSpy, - })); - jest.mock("../filter_supported_representations", () => ({ - __esModule: true, - default: filterSpy, - })); - jest.mock("../../utils/languages", () => ({ - __esModule: true, - default: normalizeSpy, - })); + jest.mock("../../utils/languages", () => ({ __esModule: true, + default: normalizeSpy })); const Adaptation = require("../adaptation").default; - const args1 = { - id: "12", - representations: [], - type: "video", - }; + const args1 = { id: "12", + representations: [], + type: "video" }; const adaptation1 = new Adaptation(args1); expect(adaptation1.language).toBe(undefined); expect(adaptation1.normalizedLanguage).toBe(undefined); @@ -169,64 +139,71 @@ describe("Manifest - Adaptation", () => { }); it("should create and sort the corresponding Representations", () => { - const representationSpy = jest.fn(arg => arg); - const filterSpy = jest.fn((_type, arg) => arg); - - jest.mock("../representation", () => ({ - __esModule: true, - default: representationSpy, - })); - jest.mock("../filter_supported_representations", () => ({ - __esModule: true, - default: filterSpy, - })); + jest.mock("../representation", () => ({ __esModule: true, + default: defaultRepresentationSpy })); const Adaptation = require("../adaptation").default; - const rep1 = { bitrate: 10, id: "rep1", index: minimalRepresentationIndex }; - const rep2 = { bitrate: 30, id: "rep2", index: minimalRepresentationIndex }; - const rep3 = { bitrate: 20, id: "rep3", index: minimalRepresentationIndex }; + const rep1 = { bitrate: 10, + id: "rep1", + index: minimalRepresentationIndex }; + const rep2 = { bitrate: 30, + id: "rep2", + index: minimalRepresentationIndex }; + const rep3 = { bitrate: 20, + id: "rep3", + index: minimalRepresentationIndex }; const representations = [rep1, rep2, rep3]; - const args = { id: "12", representations, type: "text" as "text" }; + const args = { id: "12", + representations, + type: "text" as const }; const adaptation = new Adaptation(args); const parsedRepresentations = adaptation.representations; - expect(representationSpy).toHaveBeenCalledTimes(3); - expect(representationSpy).toHaveBeenNthCalledWith(1, rep1); - expect(representationSpy).toHaveBeenNthCalledWith(2, rep2); - expect(representationSpy).toHaveBeenNthCalledWith(3, rep3); - expect(filterSpy).toHaveReturnedTimes(1); - expect(filterSpy).toHaveBeenCalledWith("text", representations); + expect(defaultRepresentationSpy).toHaveBeenCalledTimes(3); + expect(defaultRepresentationSpy).toHaveBeenNthCalledWith(1, rep1, { type: "text" }); + expect(defaultRepresentationSpy).toHaveBeenNthCalledWith(2, rep2, { type: "text" }); + expect(defaultRepresentationSpy).toHaveBeenNthCalledWith(3, rep3, { type: "text" }); expect(adaptation.parsingErrors).toEqual([]); expect(parsedRepresentations.length).toBe(3); - expect(parsedRepresentations[0]).toEqual(new Representation(rep1)); - expect(parsedRepresentations[1]).toEqual(new Representation(rep3)); - expect(parsedRepresentations[2]).toEqual(new Representation(rep2)); + expect(parsedRepresentations[0].id).toEqual("rep1"); + expect(parsedRepresentations[1].id).toEqual("rep3"); + expect(parsedRepresentations[2].id).toEqual("rep2"); expect(adaptation.getAvailableBitrates()).toEqual([10, 20, 30]); - expect(adaptation.getRepresentation("rep2")).toEqual(new Representation(rep2)); + expect(adaptation.getRepresentation("rep2").bitrate).toEqual(30); }); it("should execute the representationFilter if given", () => { - const representationSpy = jest.fn(arg => arg); - const filterSpy = jest.fn((_type, arg) => arg); - - jest.mock("../representation", () => ({ - __esModule: true, - default: representationSpy, - })); - jest.mock("../filter_supported_representations", () => ({ - __esModule: true, - default: filterSpy, - })); + const representationSpy = jest.fn(arg => { + return { bitrate: arg.bitrate, + id: arg.id, + isSupported: arg.id !== "rep4", + index: arg.index }; + }); + + jest.mock("../representation", () => ({ __esModule: true, + default: representationSpy })); const Adaptation = require("../adaptation").default; - const rep1 = { bitrate: 10, id: "rep1", index: minimalRepresentationIndex }; - const rep2 = { bitrate: 20, id: "rep2", index: minimalRepresentationIndex }; - const rep3 = { bitrate: 30, id: "rep3", index: minimalRepresentationIndex }; - const rep4 = { bitrate: 40, id: "rep4", index: minimalRepresentationIndex }; - const rep5 = { bitrate: 50, id: "rep5", index: minimalRepresentationIndex }; - const rep6 = { bitrate: 60, id: "rep6", index: minimalRepresentationIndex }; + const rep1 = { bitrate: 10, + id: "rep1", + index: minimalRepresentationIndex }; + const rep2 = { bitrate: 20, + id: "rep2", + index: minimalRepresentationIndex }; + const rep3 = { bitrate: 30, + id: "rep3", + index: minimalRepresentationIndex }; + const rep4 = { bitrate: 40, + id: "rep4", + index: minimalRepresentationIndex }; + const rep5 = { bitrate: 50, + id: "rep5", + index: minimalRepresentationIndex }; + const rep6 = { bitrate: 60, + id: "rep6", + index: minimalRepresentationIndex }; const representations = [rep1, rep2, rep3, rep4, rep5, rep6]; const representationFilter = jest.fn(( @@ -238,7 +215,10 @@ describe("Manifest - Adaptation", () => { } return true; }); - const args = { id: "12", language: "fr", representations, type: "text" as "text" }; + const args = { id: "12", + language: "fr", + representations, + type: "text" as "text" }; const adaptation = new Adaptation(args, { representationFilter }); const parsedRepresentations = adaptation.representations; @@ -246,37 +226,39 @@ describe("Manifest - Adaptation", () => { expect(adaptation.parsingErrors).toEqual([]); expect(parsedRepresentations.length).toBe(3); - expect(parsedRepresentations[0]).toEqual(new Representation(rep4)); - expect(parsedRepresentations[1]).toEqual(new Representation(rep5)); - expect(parsedRepresentations[2]).toEqual(new Representation(rep6)); + expect(parsedRepresentations[0].id).toEqual("rep4"); + expect(parsedRepresentations[1].id).toEqual("rep5"); + expect(parsedRepresentations[2].id).toEqual("rep6"); expect(adaptation.getAvailableBitrates()).toEqual([40, 50, 60]); expect(adaptation.getRepresentation("rep2")).toBe(undefined); - expect(adaptation.getRepresentation("rep4")).toEqual(new Representation(rep4)); + expect(adaptation.getRepresentation("rep4").id).toEqual("rep4"); }); it("should set a parsing error if no Representation is supported", () => { - const representationSpy = jest.fn(arg => arg); - const filterSpy = jest.fn(() => []); - - jest.mock("../representation", () => ({ - __esModule: true, - default: representationSpy, - })); - jest.mock("../filter_supported_representations", () => ({ - __esModule: true, - default: filterSpy, - })); - + const representationSpy = jest.fn(arg => { + return { bitrate: arg.bitrate, + id: arg.id, + isSupported: false, + index: arg.index }; + }); + jest.mock("../representation", () => ({ __esModule: true, + default: representationSpy })); const Adaptation = require("../adaptation").default; - const rep1 = { bitrate: 10, id: "rep1", index: minimalRepresentationIndex }; - const rep2 = { bitrate: 20, id: "rep2", index: minimalRepresentationIndex }; + const rep1 = { bitrate: 10, + id: "rep1", + index: minimalRepresentationIndex }; + const rep2 = { bitrate: 20, + id: "rep2", + index: minimalRepresentationIndex }; const representations = [rep1, rep2]; - const args = { id: "12", representations, type: "text" as "text" }; + const args = { id: "12", + representations, + type: "text" as "text" }; const adaptation = new Adaptation(args); const parsedRepresentations = adaptation.representations; - expect(parsedRepresentations.length).toBe(0); + expect(parsedRepresentations.length).toBe(2); expect(adaptation.parsingErrors).toHaveLength(1); const error = adaptation.parsingErrors[0]; @@ -285,36 +267,31 @@ describe("Manifest - Adaptation", () => { }); it("should not set a parsing error if we had no Representation", () => { - const representationSpy = jest.fn(arg => arg); - const filterSpy = jest.fn(() => []); - - jest.mock("../representation", () => ({ - __esModule: true, - default: representationSpy, - })); - jest.mock("../filter_supported_representations", () => ({ - __esModule: true, - default: filterSpy, - })); + const representationSpy = jest.fn(arg => { + return { bitrate: arg.bitrate, + id: arg.id, + isSupported: false, + index: arg.index }; + }); + jest.mock("../representation", () => ({ __esModule: true, + default: representationSpy })); const Adaptation = require("../adaptation").default; - const args = { id: "12", representations: [], type: "text" as "text" }; + const args = { id: "12", + representations: [], + type: "text" as "text" }; const adaptation = new Adaptation(args); const parsedRepresentations = adaptation.representations; expect(parsedRepresentations.length).toBe(0); expect(adaptation.parsingErrors).toEqual([]); + expect(representationSpy).not.toHaveBeenCalled(); }); it("should set an isDub value if one", () => { - const representationSpy = jest.fn(arg => arg); - const filterSpy = jest.fn((_type, arg) => arg); - const normalizeSpy = jest.fn((lang : string) => lang + "foo"); - jest.mock("../representation", () => ({ __esModule: true, - default: representationSpy })); - jest.mock("../filter_supported_representations", () => ({ __esModule: true, - default: filterSpy })); + default: defaultRepresentationSpy })); + const normalizeSpy = jest.fn((lang : string) => lang + "foo"); jest.mock("../../utils/languages", () => ({ __esModule: true, default: normalizeSpy })); @@ -342,43 +319,28 @@ describe("Manifest - Adaptation", () => { }); it("should set an isClosedCaption value if one", () => { - const representationSpy = jest.fn(arg => arg); - const filterSpy = jest.fn((_type, arg) => arg); + jest.mock("../representation", () => ({ __esModule: true, + default: defaultRepresentationSpy })); const normalizeSpy = jest.fn((lang : string) => lang + "foo"); - - jest.mock("../representation", () => ({ - __esModule: true, - default: representationSpy, - })); - jest.mock("../filter_supported_representations", () => ({ - __esModule: true, - default: filterSpy, - })); - jest.mock("../../utils/languages", () => ({ - __esModule: true, - default: normalizeSpy, - })); + jest.mock("../../utils/languages", () => ({ __esModule: true, + default: normalizeSpy })); const Adaptation = require("../adaptation").default; - const args1 = { - id: "12", - representations: [], - closedCaption: false, - type: "video", - }; + const args1 = { id: "12", + representations: [], + closedCaption: false, + type: "video" }; const adaptation1 = new Adaptation(args1); expect(adaptation1.language).toBe(undefined); expect(adaptation1.normalizedLanguage).toBe(undefined); expect(adaptation1.isClosedCaption).toEqual(false); expect(normalizeSpy).not.toHaveBeenCalled(); - const args2 = { - id: "12", - representations: [], - closedCaption: true, - type: "video", - }; + const args2 = { id: "12", + representations: [], + closedCaption: true, + type: "video" }; const adaptation2 = new Adaptation(args2); expect(adaptation2.language).toBe(undefined); expect(adaptation2.normalizedLanguage).toBe(undefined); @@ -387,43 +349,29 @@ describe("Manifest - Adaptation", () => { }); it("should set an isAudioDescription value if one", () => { - const representationSpy = jest.fn(arg => arg); - const filterSpy = jest.fn((_type, arg) => arg); + jest.mock("../representation", () => ({ __esModule: true, + default: defaultRepresentationSpy })); const normalizeSpy = jest.fn((lang : string) => lang + "foo"); - jest.mock("../representation", () => ({ - __esModule: true, - default: representationSpy, - })); - jest.mock("../filter_supported_representations", () => ({ - __esModule: true, - default: filterSpy, - })); - jest.mock("../../utils/languages", () => ({ - __esModule: true, - default: normalizeSpy, - })); + jest.mock("../../utils/languages", () => ({ __esModule: true, + default: normalizeSpy })); const Adaptation = require("../adaptation").default; - const args1 = { - id: "12", - representations: [], - audioDescription: false, - type: "video", - }; + const args1 = { id: "12", + representations: [], + audioDescription: false, + type: "video" }; const adaptation1 = new Adaptation(args1); expect(adaptation1.language).toBe(undefined); expect(adaptation1.normalizedLanguage).toBe(undefined); expect(adaptation1.isAudioDescription).toEqual(false); expect(normalizeSpy).not.toHaveBeenCalled(); - const args2 = { - id: "12", - representations: [], - audioDescription: true, - type: "video", - }; + const args2 = { id: "12", + representations: [], + audioDescription: true, + type: "video" }; const adaptation2 = new Adaptation(args2); expect(adaptation2.language).toBe(undefined); expect(adaptation2.normalizedLanguage).toBe(undefined); @@ -432,41 +380,26 @@ describe("Manifest - Adaptation", () => { }); it("should set a manuallyAdded value if one", () => { - const representationSpy = jest.fn(arg => arg); - const filterSpy = jest.fn((_type, arg) => arg); + jest.mock("../representation", () => ({ __esModule: true, + default: defaultRepresentationSpy })); const normalizeSpy = jest.fn((lang : string) => lang + "foo"); - - jest.mock("../representation", () => ({ - __esModule: true, - default: representationSpy, - })); - jest.mock("../filter_supported_representations", () => ({ - __esModule: true, - default: filterSpy, - })); - jest.mock("../../utils/languages", () => ({ - __esModule: true, - default: normalizeSpy, - })); + jest.mock("../../utils/languages", () => ({ __esModule: true, + default: normalizeSpy })); const Adaptation = require("../adaptation").default; - const args1 = { - id: "12", - representations: [], - type: "video", - }; + const args1 = { id: "12", + representations: [], + type: "video" }; const adaptation1 = new Adaptation(args1, { isManuallyAdded: false }); expect(adaptation1.language).toBe(undefined); expect(adaptation1.normalizedLanguage).toBe(undefined); expect(adaptation1.manuallyAdded).toEqual(false); expect(normalizeSpy).not.toHaveBeenCalled(); - const args2 = { - id: "12", - representations: [], - type: "video", - }; + const args2 = { id: "12", + representations: [], + type: "video" }; const adaptation2 = new Adaptation(args2, { isManuallyAdded: true }); expect(adaptation2.language).toBe(undefined); expect(adaptation2.normalizedLanguage).toBe(undefined); @@ -478,29 +411,26 @@ describe("Manifest - Adaptation", () => { it("should filter Representation with duplicate bitrates in getAvailableBitrates", () => { /* tslint:enable:max-line-length */ - const representationSpy = jest.fn(arg => arg); - const filterSpy = jest.fn((_type, arg) => arg); + jest.mock("../representation", () => ({ __esModule: true, + default: defaultRepresentationSpy })); const uniqSpy = jest.fn(() => [45, 92]); - - jest.mock("../../utils/uniq", () => ({ - __esModule: true, - default: uniqSpy, - })); - jest.mock("../representation", () => ({ - __esModule: true, - default: representationSpy, - })); - jest.mock("../filter_supported_representations", () => ({ - __esModule: true, - default: filterSpy, - })); + jest.mock("../../utils/uniq", () => ({ __esModule: true, + default: uniqSpy })); const Adaptation = require("../adaptation").default; - const rep1 = { bitrate: 10, id: "rep1", index: minimalRepresentationIndex }; - const rep2 = { bitrate: 20, id: "rep2", index: minimalRepresentationIndex }; - const rep3 = { bitrate: 20, id: "rep3", index: minimalRepresentationIndex }; + const rep1 = { bitrate: 10, + id: "rep1", + index: minimalRepresentationIndex }; + const rep2 = { bitrate: 20, + id: "rep2", + index: minimalRepresentationIndex }; + const rep3 = { bitrate: 20, + id: "rep3", + index: minimalRepresentationIndex }; const representations = [rep1, rep2, rep3]; - const args = { id: "12", representations, type: "text" as "text" }; + const args = { id: "12", + representations, + type: "text" as "text" }; const adaptation = new Adaptation(args); const parsedRepresentations = adaptation.representations; @@ -516,55 +446,111 @@ describe("Manifest - Adaptation", () => { it("should return the first Representation with the given Id with `getRepresentation`", () => { /* tslint:enable:max-line-length */ - const representationSpy = jest.fn(arg => arg); - const filterSpy = jest.fn((_type, arg) => arg); - - jest.mock("../representation", () => ({ - __esModule: true, - default: representationSpy, - })); - jest.mock("../filter_supported_representations", () => ({ - __esModule: true, - default: filterSpy, - })); + jest.mock("../representation", () => ({ __esModule: true, + default: defaultRepresentationSpy })); const Adaptation = require("../adaptation").default; - const rep1 = { bitrate: 10, id: "rep1", index: minimalRepresentationIndex }; - const rep2 = { bitrate: 20, id: "rep2", index: minimalRepresentationIndex }; - const rep3 = { bitrate: 30, id: "rep2", index: minimalRepresentationIndex }; + const rep1 = { bitrate: 10, + id: "rep1", + index: minimalRepresentationIndex }; + const rep2 = { bitrate: 20, + id: "rep2", + index: minimalRepresentationIndex }; + const rep3 = { bitrate: 30, + id: "rep2", + index: minimalRepresentationIndex }; const representations = [rep1, rep2, rep3]; const args = { id: "12", representations, type: "text" as "text" }; const adaptation = new Adaptation(args); - expect(adaptation.getRepresentation("rep1")).toBe(rep1); - expect(adaptation.getRepresentation("rep2")).toBe(rep2); + expect(adaptation.getRepresentation("rep1").bitrate).toEqual(10); + expect(adaptation.getRepresentation("rep2").bitrate).toEqual(20); }); /* tslint:disable:max-line-length */ it("should return undefined in `getRepresentation` if no representation is found with this Id", () => { /* tslint:enable:max-line-length */ - const representationSpy = jest.fn(arg => arg); - const filterSpy = jest.fn((_type, arg) => arg); - - jest.mock("../representation", () => ({ - __esModule: true, - default: representationSpy, - })); - jest.mock("../filter_supported_representations", () => ({ - __esModule: true, - default: filterSpy, - })); + jest.mock("../representation", () => ({ __esModule: true, + default: defaultRepresentationSpy })); const Adaptation = require("../adaptation").default; - const rep1 = { bitrate: 10, id: "rep1", index: minimalRepresentationIndex }; - const rep2 = { bitrate: 20, id: "rep2", index: minimalRepresentationIndex }; - const rep3 = { bitrate: 30, id: "rep2", index: minimalRepresentationIndex }; + const rep1 = { bitrate: 10, + id: "rep1", + index: minimalRepresentationIndex }; + const rep2 = { bitrate: 20, + id: "rep2", + index: minimalRepresentationIndex }; + const rep3 = { bitrate: 30, + id: "rep2", + index: minimalRepresentationIndex }; const representations = [rep1, rep2, rep3]; const args = { id: "12", representations, type: "text" as "text" }; const adaptation = new Adaptation(args); expect(adaptation.getRepresentation("rep5")).toBe(undefined); }); + + /* tslint:disable:max-line-length */ + it("should return only supported and decipherable representation when calling `getPlayableRepresentations`", () => { + /* tslint:enable:max-line-length */ + const representationSpy = jest.fn(arg => { + return { bitrate: arg.bitrate, + id: arg.id, + isSupported: arg.id !== "rep3" && arg.id !== "rep8", + decipherable: arg.id === "rep6" ? false : + arg.id === "rep8" ? false : + arg.id === "rep4" ? true : + undefined, + index: arg.index }; + }); + jest.mock("../representation", () => ({ __esModule: true, + default: representationSpy })); + + const Adaptation = require("../adaptation").default; + const rep1 = { bitrate: 10, + id: "rep1", + index: minimalRepresentationIndex }; + const rep2 = { bitrate: 20, + id: "rep2", + index: minimalRepresentationIndex }; + const rep3 = { bitrate: 30, + id: "rep3", + index: minimalRepresentationIndex }; + const rep4 = { bitrate: 40, + id: "rep4", + index: minimalRepresentationIndex }; + const rep5 = { bitrate: 50, + id: "rep5", + index: minimalRepresentationIndex }; + const rep6 = { bitrate: 60, + id: "rep6", + index: minimalRepresentationIndex }; + const rep7 = { bitrate: 70, + id: "rep7", + index: minimalRepresentationIndex }; + const rep8 = { bitrate: 80, + id: "rep8", + index: minimalRepresentationIndex }; + const representations = [rep1, + rep2, + rep3, + rep4, + rep5, + rep6, + rep7, + rep8]; + const args = { id: "12", representations, type: "text" as "text" }; + const adaptation = new Adaptation(args); + + const playableRepresentations = adaptation.getPlayableRepresentations(); + expect(playableRepresentations.length).toEqual(5); + expect(playableRepresentations[0].id).toEqual("rep1"); + expect(playableRepresentations[1].id).toEqual("rep2"); + expect(playableRepresentations[2].id).toEqual("rep4"); + expect(playableRepresentations[3].id).toEqual("rep5"); + expect(playableRepresentations[4].id).toEqual("rep7"); + expect(adaptation.parsingErrors).toEqual([]); + }); }); /* tslint:enable no-unsafe-any */ diff --git a/src/manifest/__tests__/filter_supported_representations.test.ts b/src/manifest/__tests__/filter_supported_representations.test.ts deleted file mode 100644 index 4283752a53..0000000000 --- a/src/manifest/__tests__/filter_supported_representations.test.ts +++ /dev/null @@ -1,140 +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. - */ - -/* tslint:disable no-unsafe-any */ -describe("Manifest - filterSupportedRepresentations", () => { - beforeEach(() => { - jest.resetModules(); - }); - - it("should not filter text adaptations", () => { - const isCodecSupportedSpy = jest.fn(() => false); - jest.mock("../../compat", () => ({ isCodecSupported: isCodecSupportedSpy })); - - const filterSupportedRepresentations = require("../filter_supported_representations") - .default; - - const rep1 = { id: "54", bitrate: 100 }; - const rep2 = { id: "55", bitrate: 101 }; - expect(filterSupportedRepresentations("text", [rep1, rep2])) - .toEqual([rep1, rep2]); - - expect(isCodecSupportedSpy).not.toHaveBeenCalled(); - }); - - it("should not filter image adaptations", () => { - const isCodecSupportedSpy = jest.fn(() => false); - jest.mock("../../compat", () => ({ isCodecSupported: isCodecSupportedSpy })); - - const filterSupportedRepresentations = require("../filter_supported_representations") - .default; - - const rep1 = { id: "54", bitrate: 100 }; - const rep2 = { id: "55", bitrate: 101 }; - expect(filterSupportedRepresentations("image", [rep1, rep2])) - .toEqual([rep1, rep2]); - - expect(isCodecSupportedSpy).not.toHaveBeenCalled(); - }); - - it("should filter video adaptations based on the MIME type and codec", () => { - const isCodecSupportedSpy = jest.fn((arg : string) => arg.indexOf("webm") !== -1); - jest.mock("../../compat", () => ({ isCodecSupported: isCodecSupportedSpy })); - - const filterSupportedRepresentations = require("../filter_supported_representations") - .default; - - const rep1 = { id: "54", bitrate: 100, mimeType: "video/mp4", codecs: "avc2" }; - const rep2 = { id: "55", bitrate: 101, mimeType: "video/webm", codecs: "vp9" }; - expect(filterSupportedRepresentations("video", [rep1, rep2])) - .toEqual([rep2]); - - expect(isCodecSupportedSpy).toHaveBeenCalledTimes(2); - expect(isCodecSupportedSpy).toHaveBeenCalledWith("video/mp4;codecs=\"avc2\""); - expect(isCodecSupportedSpy).toHaveBeenCalledWith("video/webm;codecs=\"vp9\""); - }); - - it("should filter audio adaptations based on the MIME type and codec", () => { - const isCodecSupportedSpy = jest.fn((arg : string) => arg.indexOf("aac") !== -1); - jest.mock("../../compat", () => ({ isCodecSupported: isCodecSupportedSpy })); - - const filterSupportedRepresentations = require("../filter_supported_representations") - .default; - - const rep1 = { id: "54", bitrate: 100, mimeType: "audio/mp4", codecs: "aac" }; - const rep2 = { id: "55", bitrate: 101, mimeType: "audio/webm", codecs: "ogg" }; - expect(filterSupportedRepresentations("video", [rep1, rep2])) - .toEqual([rep1]); - - expect(isCodecSupportedSpy).toHaveBeenCalledTimes(2); - expect(isCodecSupportedSpy).toHaveBeenCalledWith("audio/mp4;codecs=\"aac\""); - expect(isCodecSupportedSpy).toHaveBeenCalledWith("audio/webm;codecs=\"ogg\""); - }); - - it("should filter audio adaptations based on the MIME type and codec", () => { - const isCodecSupportedSpy = jest.fn((arg : string) => arg.indexOf("aac") !== -1); - jest.mock("../../compat", () => ({ isCodecSupported: isCodecSupportedSpy })); - - const filterSupportedRepresentations = require("../filter_supported_representations") - .default; - - const rep1 = { id: "54", bitrate: 100, mimeType: "audio/mp4", codecs: "aac" }; - const rep2 = { id: "55", bitrate: 101, mimeType: "audio/webm", codecs: "ogg" }; - expect(filterSupportedRepresentations("video", [rep1, rep2])) - .toEqual([rep1]); - - expect(isCodecSupportedSpy).toHaveBeenCalledTimes(2); - expect(isCodecSupportedSpy).toHaveBeenCalledWith("audio/mp4;codecs=\"aac\""); - expect(isCodecSupportedSpy).toHaveBeenCalledWith("audio/webm;codecs=\"ogg\""); - }); - - it("should set default MIME type and codecs for video adaptations", () => { - const isCodecSupportedSpy = jest.fn(() => false); - jest.mock("../../compat", () => ({ isCodecSupported: isCodecSupportedSpy })); - - const filterSupportedRepresentations = require("../filter_supported_representations") - .default; - - const rep1 = { id: "54", bitrate: 100, mimeType: "video/mp4" }; - const rep2 = { id: "55", bitrate: 101, codecs: "vp9" }; - const rep3 = { id: "55", bitrate: 101 }; - filterSupportedRepresentations("video", [rep1, rep2, rep3]); - - expect(isCodecSupportedSpy).toHaveBeenCalledTimes(3); - expect(isCodecSupportedSpy).toHaveBeenCalledWith("video/mp4;codecs=\"\""); - expect(isCodecSupportedSpy).toHaveBeenCalledWith(";codecs=\"vp9\""); - expect(isCodecSupportedSpy).toHaveBeenCalledWith(";codecs=\"\""); - }); - - it("should set default MIME type and codecs for audio adaptations", () => { - const isCodecSupportedSpy = jest.fn(() => false); - jest.mock("../../compat", () => ({ isCodecSupported: isCodecSupportedSpy })); - - const filterSupportedRepresentations = require("../filter_supported_representations") - .default; - - const rep1 = { id: "54", bitrate: 100, mimeType: "audio/mp4" }; - const rep2 = { id: "55", bitrate: 101, codecs: "ogg" }; - const rep3 = { id: "55", bitrate: 101 }; - filterSupportedRepresentations("audio", [rep1, rep2, rep3]); - - expect(isCodecSupportedSpy).toHaveBeenCalledTimes(3); - expect(isCodecSupportedSpy).toHaveBeenCalledWith("audio/mp4;codecs=\"\""); - expect(isCodecSupportedSpy).toHaveBeenCalledWith(";codecs=\"ogg\""); - expect(isCodecSupportedSpy).toHaveBeenCalledWith(";codecs=\"\""); - }); -}); -/* tslint:enable no-unsafe-any */ diff --git a/src/manifest/__tests__/representation.test.ts b/src/manifest/__tests__/representation.test.ts index 5a51f228cd..83088dcbd3 100644 --- a/src/manifest/__tests__/representation.test.ts +++ b/src/manifest/__tests__/representation.test.ts @@ -14,27 +14,38 @@ * limitations under the License. */ -import Representation from "../representation"; - -const minimalIndex = { - getInitSegment() { return null; }, - getSegments() { return []; }, - shouldRefresh() { return false; }, - getFirstPosition() : undefined { return ; }, - getLastPosition() : undefined { return ; }, - checkDiscontinuity() { return -1; }, - isSegmentStillAvailable() : undefined { return ; }, - canBeOutOfSyncError() : true { return true; }, - isFinished() : true { return true; }, - _replace() { /* noop */ }, - _update() { /* noop */ }, - _addSegments() { /* noop */ }, -}; +/* tslint:disable no-unsafe-any */ +const minimalIndex = { getInitSegment() { return null; }, + getSegments() { return []; }, + shouldRefresh() { return false; }, + getFirstPosition() : undefined { return ; }, + getLastPosition() : undefined { return ; }, + checkDiscontinuity() { return -1; }, + isSegmentStillAvailable() : undefined { return ; }, + canBeOutOfSyncError() : true { return true; }, + isFinished() : true { return true; }, + _replace() { /* noop */ }, + _update() { /* noop */ }, + _addSegments() { /* noop */ } }; + +const defaultIsCodecSupported = jest.fn(() => true); describe("Manifest - Representation", () => { + beforeEach(() => { + jest.resetModules(); + }); + afterEach(() => { + defaultIsCodecSupported.mockClear(); + }); + it("should be able to create Representation with the minimum arguments given", () => { - const args = { bitrate: 12, id: "test", index: minimalIndex }; - const representation = new Representation(args); + jest.mock("../../compat", () => ({ __esModule: true, + isCodecSupported: defaultIsCodecSupported })); + const Representation = require("../representation").default; + const args = { bitrate: 12, + id: "test", + index: minimalIndex }; + const representation = new Representation(args, { type: "audio" }); expect(representation.id).toBe("test"); expect(representation.bitrate).toBe(12); expect(representation.index).toBe(minimalIndex); @@ -44,12 +55,18 @@ describe("Manifest - Representation", () => { expect(representation.height).toBe(undefined); expect(representation.mimeType).toBe(undefined); expect(representation.width).toBe(undefined); - expect(representation.getMimeTypeString()).toBe("undefined;codecs=\"undefined\""); + expect(representation.getMimeTypeString()).toBe(";codecs=\"\""); + expect(representation.isSupported).toBe(true); + expect(representation.decipherable).toBe(undefined); + expect(defaultIsCodecSupported).toHaveBeenCalledTimes(1); }); it("should be able to add a height attribute", () => { + jest.mock("../../compat", () => ({ __esModule: true, + isCodecSupported: defaultIsCodecSupported })); + const Representation = require("../representation").default; const args = { bitrate: 12, id: "test", height: 57, index: minimalIndex }; - const representation = new Representation(args); + const representation = new Representation(args, { type: "video" }); expect(representation.id).toBe("test"); expect(representation.bitrate).toBe(12); expect(representation.index).toBe(minimalIndex); @@ -59,12 +76,18 @@ describe("Manifest - Representation", () => { expect(representation.height).toBe(57); expect(representation.mimeType).toBe(undefined); expect(representation.width).toBe(undefined); - expect(representation.getMimeTypeString()).toBe("undefined;codecs=\"undefined\""); + expect(representation.getMimeTypeString()).toBe(";codecs=\"\""); + expect(representation.isSupported).toBe(true); + expect(representation.decipherable).toBe(undefined); + expect(defaultIsCodecSupported).toHaveBeenCalledTimes(1); }); it("should be able to add a width attribute", () => { + jest.mock("../../compat", () => ({ __esModule: true, + isCodecSupported: defaultIsCodecSupported })); + const Representation = require("../representation").default; const args = { bitrate: 12, id: "test", width: 2, index: minimalIndex }; - const representation = new Representation(args); + const representation = new Representation(args, { type: "video" }); expect(representation.id).toBe("test"); expect(representation.bitrate).toBe(12); expect(representation.index).toBe(minimalIndex); @@ -74,12 +97,21 @@ describe("Manifest - Representation", () => { expect(representation.height).toBe(undefined); expect(representation.mimeType).toBe(undefined); expect(representation.width).toBe(2); - expect(representation.getMimeTypeString()).toBe("undefined;codecs=\"undefined\""); + expect(representation.getMimeTypeString()).toBe(";codecs=\"\""); + expect(representation.isSupported).toBe(true); + expect(representation.decipherable).toBe(undefined); + expect(defaultIsCodecSupported).toHaveBeenCalledTimes(1); }); it("should be able to add a codecs attribute", () => { - const args = { bitrate: 12, id: "test", codecs: "vp9", index: minimalIndex }; - const representation = new Representation(args); + jest.mock("../../compat", () => ({ __esModule: true, + isCodecSupported: defaultIsCodecSupported })); + const Representation = require("../representation").default; + const args = { bitrate: 12, + id: "test", + codecs: "vp9", + index: minimalIndex }; + const representation = new Representation(args, { type: "audio" }); expect(representation.id).toBe("test"); expect(representation.bitrate).toBe(12); expect(representation.index).toBe(minimalIndex); @@ -89,12 +121,21 @@ describe("Manifest - Representation", () => { expect(representation.height).toBe(undefined); expect(representation.mimeType).toBe(undefined); expect(representation.width).toBe(undefined); - expect(representation.getMimeTypeString()).toBe("undefined;codecs=\"vp9\""); + expect(representation.getMimeTypeString()).toBe(";codecs=\"vp9\""); + expect(representation.isSupported).toBe(true); + expect(representation.decipherable).toBe(undefined); + expect(defaultIsCodecSupported).toHaveBeenCalledTimes(1); }); it("should be able to add a mimeType attribute", () => { - const args = { bitrate: 12, id: "test", mimeType: "audio/mp4", index: minimalIndex }; - const representation = new Representation(args); + jest.mock("../../compat", () => ({ __esModule: true, + isCodecSupported: defaultIsCodecSupported })); + const Representation = require("../representation").default; + const args = { bitrate: 12, + id: "test", + mimeType: "audio/mp4", + index: minimalIndex }; + const representation = new Representation(args, { type: "audio" }); expect(representation.id).toBe("test"); expect(representation.bitrate).toBe(12); expect(representation.index).toBe(minimalIndex); @@ -104,68 +145,149 @@ describe("Manifest - Representation", () => { expect(representation.height).toBe(undefined); expect(representation.mimeType).toBe("audio/mp4"); expect(representation.width).toBe(undefined); - expect(representation.getMimeTypeString()).toBe("audio/mp4;codecs=\"undefined\""); + expect(representation.getMimeTypeString()).toBe("audio/mp4;codecs=\"\""); + expect(representation.isSupported).toBe(true); + expect(representation.decipherable).toBe(undefined); + expect(defaultIsCodecSupported).toHaveBeenCalledTimes(1); }); it("should be able to add a contentProtections attribute", () => { - const args = { - bitrate: 12, - id: "test", - index: minimalIndex, - contentProtections: { - keyIds: [{ keyId: new Uint8Array([45]) }], - initData: { cenc: [{ systemId: "EDEF", data: new Uint8Array([78]) }] }, - }, - }; - const representation = new Representation(args); + jest.mock("../../compat", () => ({ __esModule: true, + isCodecSupported: defaultIsCodecSupported })); + const Representation = require("../representation").default; + const args = { bitrate: 12, + id: "test", + index: minimalIndex, + mimeType: "video/mp4", + codecs: "vp12", + contentProtections: { keyIds: [{ keyId: new Uint8Array([45]) }], + initData: { + cenc: [{ + systemId: "EDEF", + data: new Uint8Array([78]), + }], + } } }; + const representation = new Representation(args, { type: "video" }); expect(representation.id).toBe("test"); expect(representation.bitrate).toBe(12); expect(representation.index).toBe(minimalIndex); - expect(representation.codec).toBe(undefined); + expect(representation.codec).toBe("vp12"); expect(representation.contentProtections).toBe(args.contentProtections); expect(representation.frameRate).toBe(undefined); expect(representation.height).toBe(undefined); - expect(representation.mimeType).toBe(undefined); + expect(representation.mimeType).toBe("video/mp4"); expect(representation.width).toBe(undefined); - expect(representation.getMimeTypeString()).toBe("undefined;codecs=\"undefined\""); + expect(representation.getMimeTypeString()).toBe("video/mp4;codecs=\"vp12\""); + expect(representation.isSupported).toBe(true); + expect(representation.decipherable).toBe(undefined); + expect(defaultIsCodecSupported).toHaveBeenCalledTimes(1); }); it("should be able to add a frameRate attribute", () => { - const args = { bitrate: 12, id: "test", frameRate: "1/60", index: minimalIndex }; - const representation = new Representation(args); + jest.mock("../../compat", () => ({ __esModule: true, + isCodecSupported: defaultIsCodecSupported })); + const Representation = require("../representation").default; + const args = { bitrate: 12, + id: "test", + frameRate: "1/60", + mimeType: "audio/mp4", + codecs: "mp4a.40.2", + index: minimalIndex }; + const representation = new Representation(args, { type: "audio" }); expect(representation.id).toBe("test"); expect(representation.bitrate).toBe(12); expect(representation.index).toBe(minimalIndex); - expect(representation.codec).toBe(undefined); + expect(representation.codec).toBe("mp4a.40.2"); expect(representation.contentProtections).toBe(undefined); expect(representation.frameRate).toBe("1/60"); expect(representation.height).toBe(undefined); - expect(representation.mimeType).toBe(undefined); + expect(representation.mimeType).toBe("audio/mp4"); expect(representation.width).toBe(undefined); - expect(representation.getMimeTypeString()).toBe("undefined;codecs=\"undefined\""); + expect(representation.getMimeTypeString()).toBe("audio/mp4;codecs=\"mp4a.40.2\""); + expect(representation.isSupported).toBe(true); + expect(representation.decipherable).toBe(undefined); + expect(defaultIsCodecSupported).toHaveBeenCalledTimes(1); }); it("should be able to return an exploitable codecs + mimeType string", () => { - const args1 = { bitrate: 12, id: "test", index: minimalIndex }; - expect(new Representation(args1).getMimeTypeString()) - .toBe("undefined;codecs=\"undefined\""); - - const args2 = { bitrate: 12, id: "test", mimeType: "foo", index: minimalIndex }; - expect(new Representation(args2).getMimeTypeString()) - .toBe("foo;codecs=\"undefined\""); - - const args3 = { bitrate: 12, id: "test", codecs: "bar", index: minimalIndex }; - expect(new Representation(args3).getMimeTypeString()) - .toBe("undefined;codecs=\"bar\""); - - const args4 = { - bitrate: 12, - id: "test", - mimeType: "foo", - codecs: "bar", - index: minimalIndex, - }; - expect(new Representation(args4).getMimeTypeString()) + jest.mock("../../compat", () => ({ __esModule: true, + isCodecSupported: defaultIsCodecSupported })); + const Representation = require("../representation").default; + const args1 = { bitrate: 12, + id: "test", + index: minimalIndex }; + expect(new Representation(args1, { type: "audio" }).getMimeTypeString()) + .toBe(";codecs=\"\""); + + const args2 = { bitrate: 12, + id: "test", + mimeType: "foo", + index: minimalIndex }; + expect(new Representation(args2, { type: "audio" }).getMimeTypeString()) + .toBe("foo;codecs=\"\""); + + const args3 = { bitrate: 12, + id: "test", + codecs: "bar", + index: minimalIndex }; + expect(new Representation(args3, { type: "audio" }).getMimeTypeString()) + .toBe(";codecs=\"bar\""); + + const args4 = { bitrate: 12, + id: "test", + mimeType: "foo", + codecs: "bar", + index: minimalIndex }; + expect(new Representation(args4, { type: "audio" }).getMimeTypeString()) .toBe("foo;codecs=\"bar\""); }); + + it("should set `isSupported` of non-supported codecs or mime-type to `false`", () => { + const notSupportedSpy = jest.fn(() => false); + jest.mock("../../compat", () => ({ __esModule: true, + isCodecSupported: notSupportedSpy })); + const Representation = require("../representation").default; + const args = { bitrate: 12, + id: "test", + frameRate: "1/60", + mimeType: "audio/mp4", + codecs: "mp4a.40.2", + index: minimalIndex }; + const representation = new Representation(args, { type: "audio" }); + expect(representation.id).toBe("test"); + expect(representation.bitrate).toBe(12); + expect(representation.index).toBe(minimalIndex); + expect(representation.codec).toBe("mp4a.40.2"); + expect(representation.contentProtections).toBe(undefined); + expect(representation.frameRate).toBe("1/60"); + expect(representation.height).toBe(undefined); + expect(representation.mimeType).toBe("audio/mp4"); + expect(representation.width).toBe(undefined); + expect(representation.getMimeTypeString()).toBe("audio/mp4;codecs=\"mp4a.40.2\""); + expect(representation.isSupported).toBe(false); + expect(representation.decipherable).toBe(undefined); + expect(notSupportedSpy).toHaveBeenCalledTimes(1); + expect(notSupportedSpy).toHaveBeenCalledWith("audio/mp4;codecs=\"mp4a.40.2\""); + }); + + it("should not check support for a custom SourceBuffer", () => { + const notSupportedSpy = jest.fn(() => false); + jest.mock("../../compat", () => ({ __esModule: true, + isCodecSupported: notSupportedSpy })); + const Representation = require("../representation").default; + const args = { bitrate: 12, + id: "test", + frameRate: "1/60", + mimeType: "bip", + codecs: "boop", + index: minimalIndex }; + const representation = new Representation(args, { type: "foo" }); + expect(representation.codec).toBe("boop"); + expect(representation.mimeType).toBe("bip"); + expect(representation.getMimeTypeString()).toBe("bip;codecs=\"boop\""); + expect(representation.isSupported).toBe(true); + expect(representation.decipherable).toBe(undefined); + expect(notSupportedSpy).toHaveBeenCalledTimes(0); + }); }); +/* tslint:enable no-unsafe-any */ diff --git a/src/manifest/adaptation.ts b/src/manifest/adaptation.ts index c098d41b5f..ee6eab2d4f 100644 --- a/src/manifest/adaptation.ts +++ b/src/manifest/adaptation.ts @@ -22,13 +22,12 @@ import log from "../log"; import { IParsedAdaptation } from "../parsers/manifest"; import arrayFind from "../utils/array_find"; import arrayIncludes from "../utils/array_includes"; +import isNullOrUndefined from "../utils/is_null_or_undefined"; import normalizeLanguage from "../utils/languages"; import uniq from "../utils/uniq"; -import filterSupportedRepresentations from "./filter_supported_representations"; +// import filterSupportedRepresentations from "./filter_supported_representations"; import Representation from "./representation"; - -/** Every possible value for the Adaptation's `type` property. */ -export type IAdaptationType = "video" | "audio" | "text" | "image"; +import { IAdaptationType } from "./types"; /** List in an array every possible value for the Adaptation's `type` property. */ export const SUPPORTED_ADAPTATIONS_TYPE: IAdaptationType[] = [ "audio", @@ -91,10 +90,7 @@ export default class Adaptation { /** Whether this Adaptation contains closed captions for the hard-of-hearing. */ public isClosedCaption? : boolean; - /** - * If true this Adaptation is sign interpreted: which is a - * variant in sign language. - */ + /** If true this Adaptation contains sign interpretation. */ public isSignInterpreted? : boolean; /** @@ -115,6 +111,16 @@ export default class Adaptation { */ public manuallyAdded? : boolean; + /** + * `false` if from all Representation from this Adaptation, none is decipherable. + * `true` if at least one is known to be decipherable. + * `undefined` if this is not known for at least a single Representation. + */ + public decipherable? : boolean; + + /** `true` if at least one Representation is in a supported codec. `false` otherwise. */ + public isSupported : boolean; + /** * Array containing every errors that happened when the Adaptation has been * created, in the order they have happened. @@ -142,18 +148,6 @@ export default class Adaptation { } this.type = parsedAdaptation.type; - const hadRepresentations = parsedAdaptation.representations.length !== 0; - const argsRepresentations = - filterSupportedRepresentations(parsedAdaptation.type, - parsedAdaptation.representations); - - if (hadRepresentations && argsRepresentations.length === 0) { - log.warn("Incompatible codecs for adaptation", parsedAdaptation); - const error = new MediaError("MANIFEST_INCOMPATIBLE_CODECS_ERROR", - "An Adaptation contains only incompatible codecs."); - this.parsingErrors.push(error); - } - if (parsedAdaptation.language !== undefined) { this.language = parsedAdaptation.language; this.normalizedLanguage = normalizeLanguage(parsedAdaptation.language); @@ -172,25 +166,47 @@ export default class Adaptation { this.isSignInterpreted = parsedAdaptation.isSignInterpreted; } - this.representations = argsRepresentations - .map(representation => new Representation(representation)) - .sort((a, b) => a.bitrate - b.bitrate) - .filter(representation => { - if (representationFilter == null) { - return true; + const argsRepresentations = parsedAdaptation.representations; + const representations : Representation[] = []; + let decipherable : boolean | undefined = false; + let isSupported : boolean = false; + for (let i = 0; i < argsRepresentations.length; i++) { + const representation = new Representation(argsRepresentations[i], + { type: this.type }); + const shouldAdd = + isNullOrUndefined(representationFilter) || + representationFilter(representation, + { bufferType: this.type, + language: this.language, + normalizedLanguage: this.normalizedLanguage, + isClosedCaption: this.isClosedCaption, + isDub: this.isDub, + isAudioDescription: this.isAudioDescription, + isSignInterpreted: this.isSignInterpreted }); + if (shouldAdd) { + representations.push(representation); + if (decipherable === false && representation.decipherable !== false) { + decipherable = representation.decipherable; } - return representationFilter(representation, - { bufferType: this.type, - language: this.language, - normalizedLanguage: this.normalizedLanguage, - isClosedCaption: this.isClosedCaption, - isDub: this.isDub, - isAudioDescription: this.isAudioDescription, - isSignInterpreted: this.isSignInterpreted }); - }); + if (!isSupported && representation.isSupported) { + isSupported = true; + } + } + } + this.representations = representations.sort((a, b) => a.bitrate - b.bitrate); + + this.decipherable = decipherable; + this.isSupported = isSupported; // for manuallyAdded adaptations (not in the manifest) this.manuallyAdded = isManuallyAdded === true; + + if (this.representations.length > 0 && !isSupported) { + log.warn("Incompatible codecs for adaptation", parsedAdaptation); + const error = new MediaError("MANIFEST_INCOMPATIBLE_CODECS_ERROR", + "An Adaptation contains only incompatible codecs."); + this.parsingErrors.push(error); + } } /** @@ -208,6 +224,17 @@ export default class Adaptation { return uniq(bitrates); } + /** + * Returns all Representation in this Adaptation that can be played (that is: + * not undecipherable and with a supported codec). + * @returns {Array.} + */ + getPlayableRepresentations() : Representation[] { + return this.representations.filter(rep => { + return rep.isSupported && rep.decipherable !== false; + }); + } + /** * Returns the Representation linked to the given ID. * @param {number|string} wantedId diff --git a/src/manifest/filter_supported_representations.ts b/src/manifest/filter_supported_representations.ts deleted file mode 100644 index 307b55d37e..0000000000 --- a/src/manifest/filter_supported_representations.ts +++ /dev/null @@ -1,46 +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 { isCodecSupported } from "../compat"; -import { IParsedRepresentation } from "../parsers/manifest"; - -/** - * Only keep Representations for which the codec is currently supported. - * @param {string} adaptationType - * @param {Array.} representations - * @returns {Array.} - */ -export default function filterSupportedRepresentations( - adaptationType : string, - representations : IParsedRepresentation[] -) : IParsedRepresentation[] { - if (adaptationType === "audio" || adaptationType === "video") { - return representations - .filter((representation) => isCodecSupported(getCodec(representation))); - } - - return representations; // TODO for the other types? -} - -/** - * Construct the codec string from given codecs and mimetype. - * @param {Object} representation - * @returns {string} - */ -function getCodec(representation : IParsedRepresentation) : string { - const { codecs = "", mimeType = "" } = representation; - return `${mimeType};codecs="${codecs}"`; -} diff --git a/src/manifest/index.ts b/src/manifest/index.ts index 94d72c6e99..df30784386 100644 --- a/src/manifest/index.ts +++ b/src/manifest/index.ts @@ -15,7 +15,6 @@ */ import Adaptation, { - IAdaptationType, IRepresentationFilter, SUPPORTED_ADAPTATIONS_TYPE, } from "./adaptation"; @@ -33,6 +32,7 @@ import IRepresentationIndex, { ISegment, StaticRepresentationIndex, } from "./representation_index"; +import { IAdaptationType } from "./types"; export default Manifest; export { diff --git a/src/manifest/manifest.ts b/src/manifest/manifest.ts index 1201f4fcbd..d99f43eeb7 100644 --- a/src/manifest/manifest.ts +++ b/src/manifest/manifest.ts @@ -23,7 +23,6 @@ import EventEmitter from "../utils/event_emitter"; import idGenerator from "../utils/id_generator"; import warnOnce from "../utils/warn_once"; import Adaptation, { - IAdaptationType, IRepresentationFilter, } from "./adaptation"; import Period, { @@ -31,7 +30,10 @@ import Period, { } from "./period"; import Representation from "./representation"; import { StaticRepresentationIndex } from "./representation_index"; -import { MANIFEST_UPDATE_TYPE } from "./types"; +import { + IAdaptationType, + MANIFEST_UPDATE_TYPE, +} from "./types"; import { replacePeriods, updatePeriods, diff --git a/src/manifest/period.ts b/src/manifest/period.ts index b51ad465b8..93de030a76 100644 --- a/src/manifest/period.ts +++ b/src/manifest/period.ts @@ -22,9 +22,9 @@ import { IParsedPeriod } from "../parsers/manifest"; import arrayFind from "../utils/array_find"; import objectValues from "../utils/object_values"; import Adaptation, { - IAdaptationType, IRepresentationFilter, } from "./adaptation"; +import { IAdaptationType } from "./types"; /** Structure listing every `Adaptation` in a Period. */ export type IManifestAdaptations = Partial>; @@ -164,4 +164,19 @@ export default class Period { getAdaptation(wantedId : string) : Adaptation|undefined { return arrayFind(this.getAdaptations(), ({ id }) => wantedId === id); } + + getPlayableAdaptations(type? : IAdaptationType) { + if (type === undefined) { + return this.getAdaptations().filter(ada => { + return ada.isSupported && ada.decipherable !== false; + }); + } + const adaptationsForType = this.adaptations[type]; + if (adaptationsForType === undefined) { + return []; + } + return adaptationsForType.filter(ada => { + return ada.isSupported && ada.decipherable !== false; + }); + } } diff --git a/src/manifest/representation.ts b/src/manifest/representation.ts index f32f850196..3680b259df 100644 --- a/src/manifest/representation.ts +++ b/src/manifest/representation.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { isCodecSupported } from "../compat"; import log from "../log"; import { IContentProtections, @@ -22,6 +23,7 @@ import { import areArraysOfNumbersEqual from "../utils/are_arrays_of_numbers_equal"; import { concat } from "../utils/byte_parsing"; import IRepresentationIndex from "./representation_index"; +import { IAdaptationType } from "./types"; export interface IContentProtectionsInitDataObject { type : string; @@ -90,10 +92,13 @@ class Representation { */ public decipherable? : boolean; + /** `true` if the Representation is in a supported codec, false otherwise. */ + public isSupported : boolean; + /** * @param {Object} args */ - constructor(args : IParsedRepresentation) { + constructor(args : IParsedRepresentation, opts : { type : IAdaptationType }) { this.id = args.id; this.bitrate = args.bitrate; this.codec = args.codecs; @@ -119,6 +124,10 @@ class Representation { } this.index = args.index; + this.isSupported = opts.type === "audio" || + opts.type === "video" ? + isCodecSupported(this.getMimeTypeString()) : + true; // TODO for other types } /** @@ -127,7 +136,7 @@ class Representation { * @returns {string} */ getMimeTypeString() : string { - return `${this.mimeType};codecs="${this.codec}"`; + return `${this.mimeType ?? ""};codecs="${this.codec ?? ""}"`; } /** diff --git a/src/manifest/types.ts b/src/manifest/types.ts index 8967f8d68a..f81bc73cc8 100644 --- a/src/manifest/types.ts +++ b/src/manifest/types.ts @@ -21,3 +21,6 @@ export enum MANIFEST_UPDATE_TYPE { /** Only a shortened version of the Manifest has been downloaded. */ Partial, } + +/** Every possible value for the Adaptation's `type` property. */ +export type IAdaptationType = "video" | "audio" | "text" | "image"; diff --git a/src/utils/__tests__/initialization_segment_cache.test.ts b/src/utils/__tests__/initialization_segment_cache.test.ts index 4750f2cd27..6137b94a55 100644 --- a/src/utils/__tests__/initialization_segment_cache.test.ts +++ b/src/utils/__tests__/initialization_segment_cache.test.ts @@ -20,6 +20,7 @@ const representation1 = { bitrate: 12, id: "r1", getMimeTypeString() : string { return ""; }, + isSupported: true, index: { getInitSegment() : null { return null; }, getSegments() : never { throw new Error("Not implemented"); }, @@ -42,6 +43,7 @@ const representation2 = { bitrate: 14, id: "r2", getMimeTypeString() : string { return ""; }, + isSupported: true, index: { getInitSegment() : null { return null; }, getSegments() : never { throw new Error("Not implemented"); }, From 53c78777792b600dc57389f1c936e290b9e72acb Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Tue, 14 Apr 2020 16:54:44 +0200 Subject: [PATCH 17/72] api: add `preferredVideoTracks` loadVideo options and the `setPreferredVideoTracks` and `getPreferredVideoTracks` methods --- doc/api/index.md | 105 +++++++++++++++++- doc/api/player_options.md | 74 +++++++++++- src/core/api/__tests__/option_parsers.test.ts | 12 ++ src/core/api/index.ts | 1 + src/core/api/option_parsers.ts | 16 +++ src/core/api/public_api.ts | 31 +++++- src/core/api/track_choice_manager.ts | 76 ++++++++++++- src/public_types.ts | 1 + 8 files changed, 304 insertions(+), 12 deletions(-) diff --git a/doc/api/index.md b/doc/api/index.md index 0db59d7f82..f32fd234cf 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -64,6 +64,8 @@ - [getPreferredAudioTracks](#meth-getPreferredAudioTracks) - [setPreferredTextTracks](#meth-setPreferredTextTracks) - [getPreferredTextTracks](#meth-getPreferredTextTracks) + - [setPreferredVideoTracks](#meth-setPreferredVideoTracks) + - [getPreferredVideoTracks](#meth-getPreferredVideoTracks) - [getCurrentAdaptations](#meth-getCurrentAdaptations) - [getCurrentRepresentations](#meth-getCurrentRepresentations) - [dispose](#meth-dispose) @@ -1316,7 +1318,7 @@ During this period of time: :warning: This option will have no effect in _DirectFile_ mode (see [loadVideo options](./loadVideo_options.md#prop-transport)) when either : -- No video track API was supported on the current browser +- No video track API is supported on the current browser - The media file tracks are not supported on the browser --- @@ -1363,7 +1365,7 @@ preferences, codec preferences or both. It is defined as an array of objects, each object describing constraints a track should respect. -If the first object - defining the first set of constraints - can not be +If the first object - defining the first set of constraints - cannot be respected under the currently available audio tracks, the RxPlayer will skip it and check with the second object and so on. As such, this array should be sorted by order of preference: from the most @@ -1476,7 +1478,7 @@ player.setPreferredAudioTracks([ :warning: This option will have no effect in _DirectFile_ mode (see [loadVideo options](./loadVideo_options.md#prop-transport)) when either : -- No audio track API was supported on the current browser +- No audio track API is supported on the current browser - The media file tracks are not supported on the browser --- @@ -1555,7 +1557,7 @@ player.setPreferredTextTracks([ :warning: This option will have no effect in _DirectFile_ mode (see [loadVideo options](./loadVideo_options.md#prop-transport)) when either : -- No text track API was supported on the current browser +- No text track API is supported on the current browser - The media file tracks are not supported on the browser --- @@ -1582,6 +1584,101 @@ it was called: ``` + +### setPreferredVideoTracks #################################################### + +_arguments_: ``Array.`` + +Allows the RxPlayer to choose an initial video track. + +It is defined as an array of objects, each object describing constraints a +track should respect. + +If the first object - defining the first set of constraints - cannot be +respected under the currently available video tracks, the RxPlayer will skip +it and check with the second object and so on. +As such, this array should be sorted by order of preference: from the most +wanted constraints to the least. + +Here is all the possible constraints you can set in any one of those objects +(note that all properties are optional here, only those set will have an effect +on which tracks will be filtered): +```js +{ + codec: { // {Object|undefined} Constraints about the codec wanted. + // if not set or set to `undefined` we won't filter based on codecs. + + test: /hvc/, // {RegExp} RegExp validating the type of codec you want. + + all: true, // {Boolean} Whether all the profiles (i.e. Representation) in a + // track should be checked against the RegExp given in `test`. + // If `true`, we will only choose a track if EVERY profiles for + // it have a codec information that is validated by that RegExp. + // If `false`, we will choose a track if we know that at least + // A SINGLE profile from it has codec information validated by + // that RegExp. + } +} +``` + +This logic is ran each time a new `Period` with video tracks is loaded by the +RxPlayer. This means at the start of the content, but also when [pre-]loading a +new DASH `Period` or a new MetaPlaylist `content`. + +Please note that those preferences won't be re-applied once the logic was +already run for a given `Period`. +Simply put, once set this preference will be applied to all contents but: + + - the current Period being played (or the current loaded content, in the case + of single-Period contents such as in Smooth streaming). + In that case, the current video track preference will stay in place. + + - the Periods which have already been loaded in the current content. + Those will keep their last set video track preferences (e.g. the preferred + video tracks at the time they were first loaded). + +To update the current video track in those cases, you should use the +`setVideoTrack` method once they are currently played. + + +#### Examples + +Let's imagine that you prefer to have a track which contains only H265 +profiles. You can do: +```js +player.setPreferredVideoTracks([ { codec: { all: false, test: /^hvc/ } } ]); +``` + +Now let's imagine you want to start without any video track enabled (e.g. to +start in an audio-only mode). To do that, you can simply do: +```js +player.setPreferredVideoTracks([null]); +``` + +--- + +:warning: This option will have no effect in _DirectFile_ mode +(see [loadVideo options](./loadVideo_options.md#prop-transport)) when either : +- No video track API is supported on the current browser +- The media file tracks are not supported on the browser + +--- + + + +### getPreferredVideoTracks #################################################### + +_return value_: ``Array.`` + +Returns the current list of preferred video tracks - by order of preference. + +This returns the data in the same format that it was given to either the +`preferredVideoTracks` constructor option or the last `setPreferredVideoTracks` +if it was called. + +It will return an empty Array if none of those two APIs were used until now. + + ### getManifest ################################################################ diff --git a/doc/api/player_options.md b/doc/api/player_options.md index a2f0ff9107..7d26f63b93 100644 --- a/doc/api/player_options.md +++ b/doc/api/player_options.md @@ -12,6 +12,7 @@ - [wantedBufferAhead](#prop-wantedBufferAhead) - [preferredAudioTracks](#prop-preferredAudioTracks) - [preferredTextTracks](#prop-preferredTextTracks) + - [preferredVideoTracks](#prop-preferredVideoTracks) - [maxBufferAhead](#prop-maxBufferAhead) - [maxBufferBehind](#prop-maxBufferBehind) - [limitVideoWidth](#prop-limitVideoWidth) @@ -234,7 +235,7 @@ either language preferences, codec preferences or both. It is defined as an array of objects, each object describing constraints a track should respect. -If the first object - defining the first set of constraints - can not be +If the first object - defining the first set of constraints - cannot be respected under the currently available audio tracks, the RxPlayer will skip it and check with the second object and so on. As such, this array should be sorted by order of preference: from the most @@ -394,6 +395,77 @@ mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). --- + +### preferredVideoTracks ####################################################### + +_type_: ``Array.`` + +_defaults_: ``[]`` + +This option allows to help the RxPlayer choose an initial video track. + +It is defined as an array of objects, each object describing constraints a +track should respect. + +If the first object - defining the first set of constraints - cannot be +respected under the currently available video tracks, the RxPlayer will skip +it and check with the second object and so on. +As such, this array should be sorted by order of preference: from the most +wanted constraints to the least. + +Here is all the possible constraints you can set in any one of those objects +(note that all properties are optional here, only those set will have an effect +on which tracks will be filtered): +```js +{ + codec: { // {Object|undefined} Constraints about the codec wanted. + // if not set or set to `undefined` we won't filter based on codecs. + + test: /hvc/, // {RegExp} RegExp validating the type of codec you want. + + all: true, // {Boolean} Whether all the profiles (i.e. Representation) in a + // track should be checked against the RegExp given in `test`. + // If `true`, we will only choose a track if EVERY profiles for + // it have a codec information that is validated by that RegExp. + // If `false`, we will choose a track if we know that at least + // A SINGLE profile from it has codec information validated by + // that RegExp. + } +} +``` + +This array of preferrences can be updated at any time through the +``setPreferredVideoTracks`` method, documented +[here](./index.md#meth-getPreferredVideoTracks). + +#### Examples + +Let's imagine that you prefer to have a track which contains at least one H265 +profile. + +You can do: +```js +const player = new RxPlayer({ + preferredVideoTracks: [ { codec: { all: false, test: /^hvc/ } } ] +}); +``` + +Now let's imagine you want to start without any video track enabled (e.g. to +start in an audio-only mode). To do that, you can simply do: +```js +const player = new RxPlayer({ + preferredVideoTracks: [null] +}); +``` + +--- + +:warning: This option will have no effect for contents loaded in _DirectFile_ +mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). + +--- + + ### maxBufferAhead ############################################################# diff --git a/src/core/api/__tests__/option_parsers.test.ts b/src/core/api/__tests__/option_parsers.test.ts index e687343cf9..50bf41f80d 100644 --- a/src/core/api/__tests__/option_parsers.test.ts +++ b/src/core/api/__tests__/option_parsers.test.ts @@ -81,6 +81,7 @@ describe("API - parseConstructorOptions", () => { stopAtEnd: true, preferredAudioTracks: [], preferredTextTracks: [], + preferredVideoTracks: [], }; it("should create default values if no option is given", () => { @@ -337,6 +338,17 @@ describe("API - parseConstructorOptions", () => { }); }); + it("should authorize setting a preferredVideoTracks option", () => { + const preferredVideoTracks = [ + { codec: { all: true, test: /hvc/ } }, + null, + ]; + expect(parseConstructorOptions({ preferredVideoTracks })).toEqual({ + ...defaultConstructorOptions, + preferredVideoTracks, + }); + }); + it("should throw if the maxBufferAhead given is not a number", () => { expect(() => parseConstructorOptions({ maxBufferAhead: "a" as any })).toThrow(); expect(() => parseConstructorOptions({ maxBufferAhead: /a/ as any })).toThrow(); diff --git a/src/core/api/index.ts b/src/core/api/index.ts index 0c61eea752..474ae39ac5 100644 --- a/src/core/api/index.ts +++ b/src/core/api/index.ts @@ -40,5 +40,6 @@ export { IAudioTrackPreference, ITextTrackPreference, + IVideoTrackPreference, } from "./track_choice_manager"; export default Player; diff --git a/src/core/api/option_parsers.ts b/src/core/api/option_parsers.ts index e58be7df28..5cb6c8add2 100644 --- a/src/core/api/option_parsers.ts +++ b/src/core/api/option_parsers.ts @@ -37,6 +37,7 @@ import { IKeySystemOption } from "../eme"; import { IAudioTrackPreference, ITextTrackPreference, + IVideoTrackPreference, } from "./track_choice_manager"; const { DEFAULT_AUTO_PLAY, @@ -192,6 +193,7 @@ export interface IConstructorOptions { maxBufferAhead? : number; preferredAudioTracks? : IAudioTrackPreference[]; preferredTextTracks? : ITextTrackPreference[]; + preferredVideoTracks? : IVideoTrackPreference[]; videoElement? : HTMLMediaElement; initialVideoBitrate? : number; @@ -212,6 +214,7 @@ export interface IParsedConstructorOptions { preferredAudioTracks : IAudioTrackPreference[]; preferredTextTracks : ITextTrackPreference[]; + preferredVideoTracks : IVideoTrackPreference[]; videoElement : HTMLMediaElement; initialVideoBitrate : number; @@ -311,6 +314,7 @@ function parseConstructorOptions( let preferredAudioTracks : IAudioTrackPreference[]; let preferredTextTracks : ITextTrackPreference[]; + let preferredVideoTracks : IVideoTrackPreference[]; let videoElement : HTMLMediaElement; let initialVideoBitrate : number; @@ -392,6 +396,17 @@ function parseConstructorOptions( preferredAudioTracks = []; } + if (options.preferredVideoTracks !== undefined) { + if (!Array.isArray(options.preferredVideoTracks)) { + warnOnce("Invalid `preferredVideoTracks` option, it should be an Array"); + preferredVideoTracks = []; + } else { + preferredVideoTracks = options.preferredVideoTracks; + } + } else { + preferredVideoTracks = []; + } + if (options.videoElement == null) { videoElement = document.createElement("video"); } else if (options.videoElement instanceof HTMLMediaElement) { @@ -454,6 +469,7 @@ function parseConstructorOptions( throttleVideoBitrateWhenHidden, preferredAudioTracks, preferredTextTracks, + preferredVideoTracks, initialAudioBitrate, initialVideoBitrate, maxAudioBitrate, diff --git a/src/core/api/public_api.ts b/src/core/api/public_api.ts index c57075cf88..721dacd4cd 100644 --- a/src/core/api/public_api.ts +++ b/src/core/api/public_api.ts @@ -122,7 +122,8 @@ import TrackChoiceManager, { ITMTextTrack, ITMTextTrackListItem, ITMVideoTrack, - ITMVideoTrackListItem + ITMVideoTrackListItem, + IVideoTrackPreference, } from "./track_choice_manager"; const { DEFAULT_UNMUTED_VOLUME } = config; @@ -354,6 +355,9 @@ class Player extends EventEmitter { /** List of favorite text tracks, in preference order. */ private _priv_preferredTextTracks : BehaviorSubject; + /** List of favorite video tracks, in preference order. */ + private _priv_preferredVideoTracks : BehaviorSubject; + /** * TrackChoiceManager instance linked to the current content. * `null` if no content has been loaded or if the current content loaded @@ -440,6 +444,7 @@ class Player extends EventEmitter { maxVideoBitrate, preferredAudioTracks, preferredTextTracks, + preferredVideoTracks, throttleWhenHidden, throttleVideoBitrateWhenHidden, videoElement, @@ -539,6 +544,7 @@ class Player extends EventEmitter { this._priv_preferredAudioTracks = new BehaviorSubject(preferredAudioTracks); this._priv_preferredTextTracks = new BehaviorSubject(preferredTextTracks); + this._priv_preferredVideoTracks = new BehaviorSubject(preferredVideoTracks); } /** @@ -1793,6 +1799,14 @@ class Player extends EventEmitter { return this._priv_preferredTextTracks.getValue(); } + /** + * Returns the current list of preferred text tracks, in preference order. + * @returns {Array.} + */ + getPreferredVideoTracks() : IVideoTrackPreference[] { + return this._priv_preferredVideoTracks.getValue(); + } + /** * Set the list of preferred audio tracks, in preference order. * @param {Array.} tracks @@ -1817,6 +1831,18 @@ class Player extends EventEmitter { return this._priv_preferredTextTracks.next(tracks); } + /** + * Set the list of preferred text tracks, in preference order. + * @param {Array.} tracks + */ + setPreferredVideoTracks(tracks : IVideoTrackPreference[]) : void { + if (!Array.isArray(tracks)) { + throw new Error("Invalid `setPreferredVideoTracks` argument. " + + "Should have been an Array."); + } + return this._priv_preferredVideoTracks.next(tracks); + } + /** * @returns {Array.|null} * @deprecated @@ -2064,6 +2090,9 @@ class Player extends EventEmitter { preferredTextTracks: initialTextTrack === undefined ? this._priv_preferredTextTracks : new BehaviorSubject([initialTextTrack]), + preferredVideoTracks: initialTextTrack === undefined ? + this._priv_preferredVideoTracks : + new BehaviorSubject([] as IVideoTrackPreference[]), }); fromEvent(manifest, "manifestUpdate") diff --git a/src/core/api/track_choice_manager.ts b/src/core/api/track_choice_manager.ts index 2ebd90c9b9..1b1ce26953 100644 --- a/src/core/api/track_choice_manager.ts +++ b/src/core/api/track_choice_manager.ts @@ -47,6 +47,11 @@ export type ITextTrackPreference = null | { language : string; closedCaption : boolean; }; +/** Single preference for a video track Adaptation. */ +export type IVideoTrackPreference = null | + { codec? : { all: boolean; + test: RegExp; }; }; + /** Audio track returned by the TrackChoiceManager. */ export interface ITMAudioTrack { language : string; normalized : string; @@ -163,7 +168,7 @@ export default class TrackChoiceManager { private _periods : SortedList; /** - * Array of preferred languages for audio tracks. + * Array of preferred settings for audio tracks. * Sorted by order of preference descending. */ private _preferredAudioTracks : BehaviorSubject; @@ -174,6 +179,12 @@ export default class TrackChoiceManager { */ private _preferredTextTracks : BehaviorSubject; + /** + * Array of preferred settings for video tracks. + * Sorted by order of preference descending. + */ + private _preferredVideoTracks : BehaviorSubject; + /** Memoization of the previously-chosen audio Adaptation for each Period. */ private _audioChoiceMemory : WeakMap; @@ -192,16 +203,17 @@ export default class TrackChoiceManager { constructor(defaults : { preferredAudioTracks : BehaviorSubject; preferredTextTracks : BehaviorSubject; + preferredVideoTracks : BehaviorSubject; }) { - const { preferredAudioTracks, preferredTextTracks } = defaults; this._periods = new SortedList((a, b) => a.period.start - b.period.start); this._audioChoiceMemory = new WeakMap(); this._textChoiceMemory = new WeakMap(); this._videoChoiceMemory = new WeakMap(); - this._preferredAudioTracks = preferredAudioTracks; - this._preferredTextTracks = preferredTextTracks; + this._preferredAudioTracks = defaults.preferredAudioTracks; + this._preferredTextTracks = defaults.preferredTextTracks; + this._preferredVideoTracks = defaults.preferredVideoTracks; } /** @@ -369,6 +381,7 @@ export default class TrackChoiceManager { throw new Error("TrackChoiceManager: Given Period not found."); } + const preferredVideoTracks = this._preferredVideoTracks.getValue(); const videoAdaptations = period.adaptations.video === undefined ? [] : period.adaptations.video; @@ -380,7 +393,8 @@ export default class TrackChoiceManager { } else if (chosenVideoAdaptation === undefined || !arrayIncludes(videoAdaptations, chosenVideoAdaptation) ) { - const optimalAdaptation = videoAdaptations[0]; + const optimalAdaptation = findFirstOptimalVideoAdaptation(videoAdaptations, + preferredVideoTracks); this._videoChoiceMemory.set(period, optimalAdaptation); videoInfos.adaptation$.next(optimalAdaptation); } else { @@ -808,6 +822,7 @@ export default class TrackChoiceManager { } private _updateVideoTrackChoices() { + const preferredVideoTracks = this._preferredVideoTracks.getValue(); const recursiveUpdateVideoTrack = (index : number) : void => { if (index >= this._periods.length()) { // we did all video Buffers, exit @@ -838,7 +853,8 @@ export default class TrackChoiceManager { return; } - const optimalAdaptation = videoAdaptations[0]; + const optimalAdaptation = findFirstOptimalVideoAdaptation(videoAdaptations, + preferredVideoTracks); this._videoChoiceMemory.set(period, optimalAdaptation); videoItem.adaptation$.next(optimalAdaptation); @@ -955,6 +971,54 @@ function findFirstOptimalTextAdaptation( return null; } +/** + * Find an optimal video adaptation given their list and the array of preferred + * video tracks sorted from the most preferred to the least preferred. + * + * `null` if the most optimal video adaptation is no video adaptation. + * @param {Array.} videoAdaptations + * @param {Array.} preferredvideoTracks + * @returns {Adaptation|null} + */ +function findFirstOptimalVideoAdaptation( + videoAdaptations : Adaptation[], + preferredVideoTracks : IVideoTrackPreference[] +) : Adaptation|null { + if (videoAdaptations.length === 0) { + return null; + } + + for (let i = 0; i < preferredVideoTracks.length; i++) { + const preferredVideoTrack = preferredVideoTracks[i]; + + if (preferredVideoTrack === null) { + return null; + } + + const foundAdaptation = arrayFind(videoAdaptations, (videoAdaptation) => { + if (preferredVideoTrack.codec === undefined) { + return true; + } + const regxp = preferredVideoTrack.codec.test; + const codecTestingFn = (rep : Representation) => + rep.codec !== undefined && regxp.test(rep.codec); + + if (preferredVideoTrack.codec.all) { + return videoAdaptation.representations.every(codecTestingFn); + } + return videoAdaptation.representations.some(codecTestingFn); + }); + + if (foundAdaptation !== undefined) { + return foundAdaptation; + } + + } + + // no optimal adaptation, just return the first one + return videoAdaptations[0]; +} + /** * Returns the index of the given `period` in the given `periods` * SortedList. diff --git a/src/public_types.ts b/src/public_types.ts index 4543e4a834..aa86ae6605 100644 --- a/src/public_types.ts +++ b/src/public_types.ts @@ -43,4 +43,5 @@ export { IAudioTrackPreference, ITextTrackPreference, + IVideoTrackPreference, } from "./core/api"; From cc685f3d5eaee0c2fcc24431aef14e9d24776bb2 Mon Sep 17 00:00:00 2001 From: grenault73 Date: Wed, 15 Apr 2020 12:13:09 +0200 Subject: [PATCH 18/72] doc: fixed disableVideoTrack doc --- doc/api/index.md | 10 +++++----- src/core/buffers/period/period_buffer.ts | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/api/index.md b/doc/api/index.md index 3a1309e377..7597748907 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -1323,23 +1323,23 @@ During this period of time: -### disableVideoTrack ##################################################### +### disableVideoTrack ########################################################## _return value_: ``void`` Deactivate the current video track, if one. -Will enter in `RELOADING` state for a short period, if not in _DirectFile_ +Might enter in `RELOADING` state for a short period, if not in _DirectFile_ --- :warning: This option will have no effect in _DirectFile_ mode (see [loadVideo options](./loadVideo_options.md#prop-transport)) when either : -- No audio track API was supported on the current browser +- No video track API was supported on the current browser - The media file tracks are not supported on the browser -/!\ On Safari browser in _DirectFile_, video playback may continue on same track -even if video tracks are disabled. +:warning: On Safari browser in _DirectFile_, video playback may continue on same +track even if video tracks are disabled. --- diff --git a/src/core/buffers/period/period_buffer.ts b/src/core/buffers/period/period_buffer.ts index 7f2936d0f3..04139d194b 100644 --- a/src/core/buffers/period/period_buffer.ts +++ b/src/core/buffers/period/period_buffer.ts @@ -121,13 +121,13 @@ export default function PeriodBuffer({ if (sourceBufferStatus.type === "set") { log.info(`Buffer: Clearing previous ${bufferType} SourceBuffer`); + if (SourceBuffersStore.isNative(bufferType)) { + return clock$.pipe(map(tick => EVENTS.needsMediaSourceReload(tick))); + } cleanBuffer$ = sourceBufferStatus.value .removeBuffer(period.start, period.end == null ? Infinity : period.end); - if (SourceBuffersStore.isNative(bufferType)) { - return clock$.pipe(map(tick => EVENTS.needsMediaSourceReload(tick))); - } } else { if (sourceBufferStatus.type === "unset") { sourceBuffersStore.disableSourceBuffer(bufferType); From f0abb3412fe6014b029a7eb55e7cbe0bf5771e45 Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Tue, 14 Apr 2020 18:50:03 +0200 Subject: [PATCH 19/72] doc: add documentation on the `signInterpreted` property on video tracks --- doc/api/index.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/doc/api/index.md b/doc/api/index.md index 1ebe44e91f..bb4e5adb8c 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -1102,6 +1102,12 @@ Each of the objects in the returned array have the following properties: - ``frameRate`` (``string|undefined``): The video framerate. + - ``signInterpreted`` (``Boolean|undefined``): If set to `true`, the track is + known to contain an interpretation in sign language. + If set to `false`, the track is known to not contain that type of content. + If not set or set to undefined we don't know whether that video track + contains an interpretation in sign language. + In _DirectFile_ mode (see [loadVideo options](./loadVideo_options.md#prop-transport)), if there are @@ -1193,7 +1199,6 @@ The track is an object with the following properties: - ``id`` (``string``): The id used to identify the track. Use it for setting the track via ``setVideoTrack``. - - ``representations`` (``Array.``): [Representations](../terms.md#representation) of this video track, with attributes: @@ -1212,6 +1217,12 @@ The track is an object with the following properties: - ``frameRate`` (``string|undefined``): The video framerate. + - ``signInterpreted`` (``Boolean|undefined``): If set to `true`, the track is + known to contain an interpretation in sign language. + If set to `false`, the track is known to not contain that type of content. + If not set or set to undefined we don't know whether that video track + contains an interpretation in sign language. + ``undefined`` if no content has been loaded yet. From 6622cef9f7bfe49aeaf0fc791743965c4e009d5a Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Thu, 2 Apr 2020 18:46:50 +0200 Subject: [PATCH 20/72] parsers/manifest/metaplaylist: update documentation style --- .../metaplaylist/representation_index.ts | 78 ++++++++++++++++++- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/src/parsers/manifest/metaplaylist/representation_index.ts b/src/parsers/manifest/metaplaylist/representation_index.ts index 5f03138100..23b0e8c865 100644 --- a/src/parsers/manifest/metaplaylist/representation_index.ts +++ b/src/parsers/manifest/metaplaylist/representation_index.ts @@ -23,20 +23,40 @@ import { import objectAssign from "../../../utils/object_assign"; /** - * The MetaRepresentationIndex is wrapper for all kind of indexes (dash, smooth, etc) + * The MetaRepresentationIndex is wrapper for all kind of RepresentationIndex (from + * dash, smooth, etc) * - * It wraps methods from origin indexes, while taking into account of the offset induced - * by metaplaylist. It makes a bridge between the metaplaylist timeline, and the original + * It wraps methods from original RepresentationIndex, while taking into account + * the time offset introduced by the MetaPlaylist content. + * + * It makes a bridge between the MetaPlaylist timeline, and the original * timeline of content. (e.g. the segment whose "meta" time is 1500, is actually a * segment whose original time is 200, played with an offset of 1300) + * @class MetaRepresentationIndex */ export default class MetaRepresentationIndex implements IRepresentationIndex { + /** Real underlying RepresentationIndex implementation. */ protected _wrappedIndex : IRepresentationIndex; + /** Offset time to add to the start of the Representation, in seconds. */ private _timeOffset : number; + /** Absolute end of the Representation, in the seconds. */ private _contentEnd : number | undefined; + /** Underlying transport for the Representation (e.g. "dash" or "smooth"). */ private _transport : string; + /** Various information about the real underlying Representation. */ private _baseContentInfos : IBaseContentInfos; + /** + * Create a new `MetaRepresentationIndex`. + * @param {Object} wrappedIndex - "Real" RepresentationIndex implementation of + * the concerned Representation. + * @param {Array.} contentBounds - Start time and end time + * the Representation will be played between, in seconds. + * @param {string} transport - Transport for the "real" RepresentationIndex + * (e.g. "dash" or "smooth"). + * @param {Object} baseContentInfos - Various information about the "real" + * Representation. + */ constructor( wrappedIndex: IRepresentationIndex, contentBounds: [number, number|undefined], @@ -50,6 +70,9 @@ export default class MetaRepresentationIndex implements IRepresentationIndex { this._baseContentInfos = baseContentInfos; } + /** + * Returns information about the initialization segment. + */ public getInitSegment() { const segment = this._wrappedIndex.getInitSegment(); if (segment === null) { @@ -65,6 +88,12 @@ export default class MetaRepresentationIndex implements IRepresentationIndex { return segment; } + /** + * Returns information about the segments asked. + * @param {number} up - Starting time wanted, in seconds. + * @param {Number} duration - Amount of time wanted, in seconds + * @returns {Array.} + */ public getSegments(up : number, duration : number) : ISegment[] { return this._wrappedIndex.getSegments(up - this._timeOffset, duration) .map((segment) => { @@ -80,22 +109,45 @@ export default class MetaRepresentationIndex implements IRepresentationIndex { }); } - public shouldRefresh() : boolean { + /** + * Whether this RepresentationIndex should be refreshed now. + * Returns `false` as MetaPlaylist contents do not support underlying live + * contents yet. + * @returns {Boolean} + */ + public shouldRefresh() : false { return false; } + /** + * Returns first possible position the first segment plays at, in seconds. + * `undefined` if we do not know this value. + * @return {Number|undefined} + */ public getFirstPosition(): number|undefined { const wrappedFirstPosition = this._wrappedIndex.getFirstPosition(); return wrappedFirstPosition != null ? wrappedFirstPosition + this._timeOffset : undefined; } + /** + * Returns last possible position the last segment plays at, in seconds. + * `undefined` if we do not know this value. + * @return {Number|undefined} + */ public getLastPosition(): number|undefined { const wrappedLastPosition = this._wrappedIndex.getLastPosition(); return wrappedLastPosition != null ? wrappedLastPosition + this._timeOffset : undefined; } + /** + * Returns `false` if that segment is not currently available in the Manifest + * (e.g. it corresponds to a segment which is before the current buffer + * depth). + * @param {Object} segment + * @returns {boolean|undefined} + */ public isSegmentStillAvailable(segment : ISegment) : boolean | undefined { const offset = this._timeOffset * segment.timescale; const updatedSegment = objectAssign({}, @@ -112,14 +164,25 @@ export default class MetaRepresentationIndex implements IRepresentationIndex { return this._wrappedIndex.canBeOutOfSyncError(error); } + /** + * + * @param {Number} time + * @returns {Number} + */ public checkDiscontinuity(time: number): number { return this._wrappedIndex.checkDiscontinuity(time - this._timeOffset); } + /** + * @returns {Boolean} + */ public isFinished() : boolean { return this._wrappedIndex.isFinished(); } + /** + * @param {Object} newIndex + */ public _replace(newIndex : IRepresentationIndex): void { if (!(newIndex instanceof MetaRepresentationIndex)) { throw new Error("A MetaPlaylist can only be replaced with another MetaPlaylist"); @@ -127,6 +190,9 @@ export default class MetaRepresentationIndex implements IRepresentationIndex { this._wrappedIndex._replace(newIndex._wrappedIndex); } + /** + * @param {Object} newIndex + */ public _update(newIndex : IRepresentationIndex): void { if (!(newIndex instanceof MetaRepresentationIndex)) { throw new Error("A MetaPlaylist can only be updated with another MetaPlaylist"); @@ -134,6 +200,10 @@ export default class MetaRepresentationIndex implements IRepresentationIndex { this._wrappedIndex._update(newIndex._wrappedIndex); } + /** + * @param {Array.} nextSegments + * @param {Object} currentSegment + */ public _addSegments( nextSegments : Array<{ time : number; duration : number; From 9ad6cf8a3d2ea2a83e5a10ed5c5d36627f05ff22 Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Wed, 15 Apr 2020 19:43:05 +0200 Subject: [PATCH 21/72] doc: update `Period` term definition --- doc/terms.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/doc/terms.md b/doc/terms.md index 0d723d76f4..b742f2055b 100644 --- a/doc/terms.md +++ b/doc/terms.md @@ -169,9 +169,19 @@ content a single time. ### Period ##################################################################### A Period is an element of the [Manifest](#manifest) which describes the media -to play at a certain points in time. +to play at a given point in time. -They are directly a DASH' concept, also called _Period_. +Depending on the transport used, they correspond to different concepts: + - for DASH contents, it is linked to an MPD's Period element + - for "local" contents, it corresponds to a single object from the `periods` + array. + - for "MetaPlaylist" contents, it corresponds to all the Period elements we + retrieved bwhen parsing the corresponding [Manifest](#manifest) from the + elements of the `contents` array. + - any other transport will have a single Period, describing the whole content. + +Despite having a different source depending on the transport used, a Period is +a single concept in the RxPlayer. Simply put, it allows to set various types of content successively in the same manifest. @@ -195,7 +205,8 @@ This can be done by putting each in a different Period: TV Show Italian Film American Blockbuster ``` -Each of these Periods will be linked to different audio, video and text tracks. +Each of these Periods will be linked to different audio, video and text tracks, +themselves linked to different qualities. From 3a32a65661d1d40615877853b21e2faa95b2029c Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Tue, 14 Apr 2020 17:08:38 +0200 Subject: [PATCH 22/72] doc: add notion about setting the preferredVideoTracks to null --- doc/api/index.md | 6 +++++- doc/api/player_options.md | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/doc/api/index.md b/doc/api/index.md index f32fd234cf..d25059f0c8 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -1587,7 +1587,7 @@ it was called: ### setPreferredVideoTracks #################################################### -_arguments_: ``Array.`` +_arguments_: ``Array.`` Allows the RxPlayer to choose an initial video track. @@ -1600,6 +1600,10 @@ it and check with the second object and so on. As such, this array should be sorted by order of preference: from the most wanted constraints to the least. +When the next encountered constraint is set to `null`, the player will simply +disable the video track. If you want to disable the video track by default, +you can just set `null` as the first element of this array (e.g. `[null]`). + Here is all the possible constraints you can set in any one of those objects (note that all properties are optional here, only those set will have an effect on which tracks will be filtered): diff --git a/doc/api/player_options.md b/doc/api/player_options.md index 7d26f63b93..29caa63e72 100644 --- a/doc/api/player_options.md +++ b/doc/api/player_options.md @@ -413,6 +413,10 @@ it and check with the second object and so on. As such, this array should be sorted by order of preference: from the most wanted constraints to the least. +When the next encountered constraint is set to `null`, the player will simply +disable the video track. If you want to disable the video track by default, +you can just set `null` as the first element of this array (e.g. `[null]`). + Here is all the possible constraints you can set in any one of those objects (note that all properties are optional here, only those set will have an effect on which tracks will be filtered): From e06df12235d4911cdafbb07f21ff9d694ac5b0e0 Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Thu, 2 Apr 2020 18:49:09 +0200 Subject: [PATCH 23/72] manifest: canBeOutOfSyncError now also takes the corresponding segment in argument --- src/core/buffers/representation/representation_buffer.ts | 2 +- src/manifest/representation_index/types.ts | 8 +++++--- src/parsers/manifest/metaplaylist/representation_index.ts | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/core/buffers/representation/representation_buffer.ts b/src/core/buffers/representation/representation_buffer.ts index 87e958428d..f3a2b99635 100644 --- a/src/core/buffers/representation/representation_buffer.ts +++ b/src/core/buffers/representation/representation_buffer.ts @@ -451,7 +451,7 @@ export default function RepresentationBuffer({ const { index } = representation; if (index.isSegmentStillAvailable(retriedSegment) === false) { reCheckNeededSegments$.next(); - } else if (index.canBeOutOfSyncError(evt.value.error)) { + } else if (index.canBeOutOfSyncError(evt.value.error, retriedSegment)) { return observableOf(EVENTS.manifestMightBeOufOfSync()); } return EMPTY; diff --git a/src/manifest/representation_index/types.ts b/src/manifest/representation_index/types.ts index 8fa3ad1062..b025031106 100644 --- a/src/manifest/representation_index/types.ts +++ b/src/manifest/representation_index/types.ts @@ -184,14 +184,16 @@ export default interface IRepresentationIndex { isSegmentStillAvailable(segment : ISegment) : boolean | undefined; /** - * Returns true if the Error given can indicate that the local index became - * "unsynchronized" with the server. + * Returns true if the `error` given following the request of `segment` can + * indicate that the index became "unsynchronized" with the server. * Some transport cannot become unsynchronized and can return false directly. * Note: This API assumes that the user first checked that the segment is * still available through `isSegmentStillAvailable`. + * @param {Error} error + * @param {Object} segment * @returns {Boolean} */ - canBeOutOfSyncError(error : ICustomError) : boolean; + canBeOutOfSyncError(error : ICustomError, segment : ISegment) : boolean; /** * Checks if the given time - in seconds - is in a discontinuity. That is: diff --git a/src/parsers/manifest/metaplaylist/representation_index.ts b/src/parsers/manifest/metaplaylist/representation_index.ts index 23b0e8c865..13558e2f1e 100644 --- a/src/parsers/manifest/metaplaylist/representation_index.ts +++ b/src/parsers/manifest/metaplaylist/representation_index.ts @@ -158,10 +158,11 @@ export default class MetaRepresentationIndex implements IRepresentationIndex { /** * @param {Error} error + * @param {Object} segment * @returns {Boolean} */ - public canBeOutOfSyncError(error : ICustomError) : boolean { - return this._wrappedIndex.canBeOutOfSyncError(error); + public canBeOutOfSyncError(error : ICustomError, segment : ISegment) : boolean { + return this._wrappedIndex.canBeOutOfSyncError(error, segment); } /** From 43ed73441364c7233bec27bc6d43b89175b0b78c Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Wed, 15 Apr 2020 19:43:28 +0200 Subject: [PATCH 24/72] doc: update api documentation landing page to be easier to read --- doc/api/index.md | 1891 ++++++++++++++++++++++++++++------------------ 1 file changed, 1149 insertions(+), 742 deletions(-) diff --git a/doc/api/index.md b/doc/api/index.md index 2ca82a5b22..6c38792eff 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -10,9 +10,8 @@ - [ErrorTypes](#static-ErrorTypes) - [ErrorCodes](#static-ErrorCodes) - [LogLevel](#static-LogLevel) -- [Methods](#meth) +- [Basic methods](#meth-group-basic) - [loadVideo](#meth-loadVideo) - - [getVideoElement](#meth-getVideoElement) - [getPlayerState](#meth-getPlayerState) - [addEventListener](#meth-addEventListener) - [removeEventListener](#meth-removeEventListener) @@ -21,40 +20,29 @@ - [stop](#meth-stop) - [getPosition](#meth-getPosition) - [getWallClockTime](#meth-getWallClockTime) + - [seekTo](#meth-seekTo) + - [getMinimumPosition](#meth-getMinimumPosition) + - [getMaximumPosition](#meth-getMaximumPosition) - [getVideoDuration](#meth-getVideoDuration) - - [getVolume](#meth-getVolume) - [getError](#meth-getError) - - [seekTo](#meth-seekTo) - - [isLive](#meth-isLive) - - [getUrl](#meth-getUrl) - - [getAvailableVideoBitrates](#meth-getAvailableVideoBitrates) - - [getAvailableAudioBitrates](#meth-getAvailableAudioBitrates) - - [getVideoBitrate](#meth-getVideoBitrate) - - [getAudioBitrate](#meth-getAudioBitrate) - - [getMaxVideoBitrate](#meth-getMaxVideoBitrate) - - [getMaxAudioBitrate](#meth-getMaxAudioBitrate) - - [setVideoBitrate](#meth-setVideoBitrate) - - [setAudioBitrate](#meth-setAudioBitrate) - - [getManualVideoBitrate](#meth-getManualVideoBitrate) - - [getManualAudioBitrate](#meth-getManualAudioBitrate) - - [setWantedBufferAhead](#meth-setWantedBufferAhead) - - [getWantedBufferAhead](#meth-getWantedBufferAhead) - - [setMaxBufferBehind](#meth-setMaxBufferBehind) - - [getMaxBufferBehind](#meth-getMaxBufferBehind) - - [setMaxBufferAhead](#meth-setMaxBufferAhead) - - [getMaxBufferAhead](#meth-getMaxBufferAhead) - - [setMaxVideoBitrate](#meth-setMaxVideoBitrate) - - [setMaxAudioBitrate](#meth-setMaxAudioBitrate) + - [getVideoElement](#meth-getVideoElement) + - [dispose](#meth-dispose) + - [Speed control](#meth-group-speed-control) + - [setPlaybackRate](#meth-setPlaybackRate) + - [getPlaybackRate](#meth-getPlaybackRate) + - [Volume control](#meth-group-volume-control) - [setVolume](#meth-setVolume) + - [getVolume](#meth-getVolume) - [mute](#meth-mute) - [unMute](#meth-unMute) - [isMute](#meth-isMute) - - [getAvailableAudioTracks](#meth-getAvailableAudioTracks) - - [getAvailableTextTracks](#meth-getAvailableTextTracks) - - [getAvailableVideoTracks](#meth-getAvailableVideoTracks) + - [Track selection](#meth-track-selection) - [getAudioTrack](#meth-getAudioTrack) - [getTextTrack](#meth-getTextTrack) - [getVideoTrack](#meth-getVideoTrack) + - [getAvailableAudioTracks](#meth-getAvailableAudioTracks) + - [getAvailableTextTracks](#meth-getAvailableTextTracks) + - [getAvailableVideoTracks](#meth-getAvailableVideoTracks) - [setAudioTrack](#meth-setAudioTrack) - [setTextTrack](#meth-setTextTrack) - [disableTextTrack](#meth-disableTextTrack) @@ -66,17 +54,38 @@ - [getPreferredTextTracks](#meth-getPreferredTextTracks) - [setPreferredVideoTracks](#meth-setPreferredVideoTracks) - [getPreferredVideoTracks](#meth-getPreferredVideoTracks) - - [getCurrentAdaptations](#meth-getCurrentAdaptations) - - [getCurrentRepresentations](#meth-getCurrentRepresentations) - - [dispose](#meth-dispose) + - [Bitrate selection](#meth-group-bitrate-selection) + - [getAvailableVideoBitrates](#meth-getAvailableVideoBitrates) + - [getAvailableAudioBitrates](#meth-getAvailableAudioBitrates) + - [getVideoBitrate](#meth-getVideoBitrate) + - [getAudioBitrate](#meth-getAudioBitrate) + - [setMaxVideoBitrate](#meth-setMaxVideoBitrate) + - [setMaxAudioBitrate](#meth-setMaxAudioBitrate) + - [getMaxVideoBitrate](#meth-getMaxVideoBitrate) + - [getMaxAudioBitrate](#meth-getMaxAudioBitrate) + - [setVideoBitrate](#meth-setVideoBitrate) + - [setAudioBitrate](#meth-setAudioBitrate) + - [getManualVideoBitrate](#meth-getManualVideoBitrate) + - [getManualAudioBitrate](#meth-getManualAudioBitrate) + - [Buffer control](#meth-group-buffer-control) + - [setWantedBufferAhead](#meth-setWantedBufferAhead) + - [getWantedBufferAhead](#meth-getWantedBufferAhead) + - [setMaxBufferBehind](#meth-setMaxBufferBehind) + - [getMaxBufferBehind](#meth-getMaxBufferBehind) + - [setMaxBufferAhead](#meth-setMaxBufferAhead) + - [getMaxBufferAhead](#meth-getMaxBufferAhead) + - [Buffer information](#meth-group-buffer-info) - [getVideoLoadedTime](#meth-getVideoLoadedTime) - [getVideoPlayedTime](#meth-getVideoPlayedTime) - [getVideoBufferGap](#meth-getVideoBufferGap) - - [getPlaybackRate](#meth-getPlaybackRate) - - [setPlaybackRate](#meth-setPlaybackRate) + - [Content information](#meth-group-content-info) + - [isLive](#meth-isLive) + - [getUrl](#meth-getUrl) + - [getManifest](#meth-getManifest) + - [getCurrentAdaptations](#meth-getCurrentAdaptations) + - [getCurrentRepresentations](#meth-getCurrentRepresentations) - [getCurrentKeySystem](#meth-getCurrentKeySystem) - - [getMinimumPosition](#meth-getMinimumPosition) - - [getMaximumPosition](#meth-getMaximumPosition) + - [Deprecated](#meth-group-deprecated) - [getImageTrackData (deprecated)](#meth-getImageTrackData) - [setFullscreen (deprecated)](#meth-setFullscreen) - [exitFullscreen (deprecated)](#meth-exitFullscreen) @@ -133,6 +142,9 @@ page](./player_options.md). ## Static properties ########################################################### +This chapter documents the static properties that can be found on the RxPlayer +class. + ### version #################################################################### @@ -190,6 +202,7 @@ If the value set to this property is different than those, it will be automatically set to ``"NONE"``. #### Example + ```js import RxPlayer from "rx-player"; RxPlayer.LogLevel = "WARNING"; @@ -198,7 +211,10 @@ RxPlayer.LogLevel = "WARNING"; -## Methods ##################################################################### +## Basic methods ############################################################### + +In this chapter, we will go through the basic methods you will need to use when +playing a content through the RxPlayer. ### loadVideo ################################################################## @@ -206,12 +222,16 @@ RxPlayer.LogLevel = "WARNING"; _arguments_: - _options_ (``Object``) -Loads a new video described in the argument. +Loads the content described in the argument. +This is the central method to use when you want to play a new content. The options possible as arguments are all defined in [this page](./loadVideo_options.md). +Despite its name, this method can also load audio-only content. + #### Example + ```js player.loadVideo({ url: "http://vm2.dashif.org/livesim-dev/segtimeline_1/testpic_6s/Manifest.mpd", @@ -221,28 +241,12 @@ player.loadVideo({ ``` - -### getVideoElement ############################################################ - -_return value_: ``HTMLMediaElement`` - -Returns the video element used by the player. - -You're not encouraged to use its API, you should always prefer the Player's API. - -#### Example -```js -const videoElement = player.getVideoElement(); -videoElement.className = "my-video-element"; -``` - - ### getPlayerState ############################################################# _return value_: ``string`` -The current player's state. +The "state" the player is currently in. Can be either one of those strings: - ``"STOPPED"``: The player is idle. No content is loading nor is loaded. @@ -278,6 +282,7 @@ As it is a central part of our API and can be difficult concept to understand, we have a special [page of documentation on player states](./states.md). #### Example + ```js switch (player.getPlayerState()) { case "STOPPED": @@ -328,10 +333,15 @@ _arguments_: Add an event listener to trigger a callback as it happens. The callback will have the event payload as a single argument. +The RxPlayer API is heavily event-based. As an example: to know when a content +is loaded, the most straightforward way is to add an event listener for the +`"playerStateChange"` event. This can be done only through this method. + To have the complete list of player events, consult the [Player events page](./player_events.md). #### Example + ```js player.addEventListener("Error", function(err) { console.log(`The player crashed: ${err.message}`); @@ -347,14 +357,16 @@ _arguments_: - _callback_ (optional) (``Function``): The callback given when calling the corresponding ``addEventListener`` API. -Remove an event listener. That is, stop your registered callback (with -``addEventListener``) to be called as events happen and free up ressources. +Remove an event listener. +That is, remove a callback previously registered with ``addEventListener`` from +being triggered on the corresponding event. This also free-up the corresponding +ressources. The callback given is optional: if not given, _every_ registered callback to -that event will be removed. That's why using both arguments is recommended for -most usecase. +that event will be removed. #### Example + ```js player.removeEventListener("playerStateChange", listenerCallback); ``` @@ -367,6 +379,10 @@ _return value_: ``Promise.`` Play/resume the current video. Equivalent to a video element's play method. +You might want to call that method either to start playing (when the content is +in the `"LOADED"` state and auto-play has not been enabled in the last +`loadVideo` call) or to resume when the content has been paused. + The returned Promise informs you on the result: - if playback succeeds, the Promise is fulfilled @@ -386,6 +402,7 @@ implementation has the exact same implementation than [ES2015 Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). #### Example + ```js const resumeContent = () => { player.play(); @@ -402,6 +419,7 @@ Note that a content can be paused even if its current state is ``BUFFERING`` or ``SEEKING``. #### Example + ```js const pauseContent = () => { player.pause(); @@ -414,7 +432,11 @@ const pauseContent = () => { Stop playback of the current content if one. +This will totaly un-load the current content. To re-start playing the same +content, you will need to call `loadVideo` again. + #### Example + ```js const stopVideo = () => { player.stop(); @@ -427,12 +449,24 @@ const stopVideo = () => { _return value_: ``Number`` -Returns the video element's current position, in seconds. +Returns the current media element's playing position, in seconds. + +For live contents, the returned position will not be re-scaled to correspond to +a live timestamp. If you want that behavior, you can call `getWallClockTime` +instead. + +This is the only difference between the two. Generally, you can follow the +following rule: -The difference with the ``getWallClockTime`` method is that for live contents -the position is not re-calculated to match a live timestamp. + - if you want to use that current position to use it with the other APIs + (like `seekTo`, `getMinimumPosition`, `getMaximumPosition` + etc.) use `getPosition`. + + - if you want to display the current position to the viewer/listener, use + `getWallClockTime` instead. #### Example + ```js const pos = player.getPosition(); console.log(`The video element's current position is: ${pos} second(s)`); @@ -444,15 +478,21 @@ console.log(`The video element's current position is: ${pos} second(s)`); _return value_: ``Number`` -Returns the wall-clock-time of the current position in seconds. +Returns the current "wall-clock" playing position in seconds. That is: - - for live content, get a timestamp in seconds of the current position. - - for static content, returns the position from beginning, also in seconds. + + - for live contents, this is the current position scaled to correspond to a + live timestamp, in seconds. + + - for non-live contents, returns the position from the absolute beginning time + of the content, also in seconds. In the absolute majority of cases this will + be equal to the value returned by `getPosition`. Use this method to display the current position to the user. #### Example + ```js const wallClockTime = player.getWallClockTime(); const nowInSeconds = Date.now() / 1000; @@ -466,75 +506,12 @@ if (delta < 5) { // (5 seconds of margin) ``` - -### getVideoDuration ########################################################### - -_return value_: ``Number`` - -Returns the duration of the current video, directly from the video element. - -#### Example -```js -const pos = player.getPosition(); -const dur = player.getVideoDuration(); - -console.log(`current position: ${pos} / ${dur}`); -``` - - - -### getVolume ################################################################## - -_return value_: ``Number`` - -Current volume of the player, from 0 (no sound) to 1 (maximum sound). 0 if muted -(different than videoElement.muted). - -#### Example -```js -const volume = player.getVolume(); - -if (volume === 1) { - console.log("You're playing at maximum volume"); -} else if (volume === 0) { - console.log("You're playing at no volume"); -} else if (volume > 0.5) { - console.log("You're playing at a high volume"); -} else { - console.log("You're playing at a low volume"); -} -``` - - - -### getError ################################################################### - -_return value_: ``Error|null`` - -Returns the fatal error if it happened. null otherwise. - -See [the Player Error documentation](./errors.md) for more information. - -#### Example -```js -const error = player.getError(); - -if (!error) { - console.log("The player did not crash"); -} else if (error.code === "PIPELINE_LOAD_ERROR") { - console.error("The player crashed due to a failing request"); -} else { - console.error(`The player crashed: ${error.code}`); -} -``` - - ### seekTo ##################################################################### _arguments_: ``Object|Number`` -Seek in the current content. +Seek in the current content (i.e. change the current position). The argument can be an object with a single ``Number`` property, either: @@ -549,7 +526,8 @@ The argument can be an object with a single ``Number`` property, either: The argument can also just be a ``Number`` property, which will have the same effect than the ``position`` property (absolute position). -#### Example +#### Examples + ```js // seeking to 54 seconds from the start of the content player.seekTo({ position: 54 }); @@ -568,434 +546,456 @@ player.seekTo({ wallClockTime: Date.now() / 1000 }); ``` - -### isLive ##################################################################### + +### getMinimumPosition ######################################################### -_return value_: ``Boolean`` +_return value_: ``Number|null`` -Returns ``true`` if the content is a "live" content (e.g. a live TV Channel). -``false`` otherwise. +The minimum seekable player position. ``null`` if no content is loaded. -Also ``false`` if no content is loaded yet. +This is useful for live contents, where server-side buffer size are often not +infinite. This method allows thus to seek at the earliest possible time. #### Example + ```js -if (player.isLive()) { - console.log("We're playing a live content"); -} +// seeking to the earliest position possible (beginning of the buffer for live +// contents, position '0' for non-live contents). +player.seekTo({ position: player.getMinimumPosition() }); ``` - -### getUrl ##################################################################### - -_return value_: ``string|undefined`` + +### getMaximumPosition ######################################################### -Returns the URL of the downloaded [Manifest](../terms.md#manifest). +_return value_: ``Number|null`` -In _DirectFile_ mode (see [loadVideo -options](./loadVideo_options.md#prop-transport)), returns the URL of the content -being played. +The maximum seekable player position. ``null`` if no content is loaded. -Returns ``undefined`` if no content is loaded yet. +This is useful for live contents, where the buffer end updates continously. +This method allows thus to seek directly at the live edge of the content. #### Example + ```js -const url = player.getUrl(); -if (url) { - console.log("We are playing the following content:", url); -} +// seeking to the end +player.seekTo({ + position: player.getMaximumPosition() +}); ``` - -### getAvailableVideoBitrates ################################################## + +### getVideoDuration ########################################################### -_return value_: ``Array.`` +_return value_: ``Number`` -The different bitrates available for the current video -[Adaptation](../terms.md#adaptation), in bits per seconds. +Returns the duration of the current video as taken from the video element. + +:warning: This duration is in fact the maximum position possible for the +content. As such, for contents not starting at `0`, this value will not be equal +to the difference between the maximum and minimum possible position, as would +normally be expected from a property named "duration". -In _DirectFile_ mode (see [loadVideo -options](./loadVideo_options.md#prop-transport)), returns an empty Array. #### Example + + ```js -const videoBitrates = player.getAvailableVideoBitrates(); -if (videoBitrates.length) { - console.log( - "The current video is available in the following bitrates", - videoBitrates.join(", ") - ); -} +const pos = player.getPosition(); +const dur = player.getVideoDuration(); + +console.log(`current position: ${pos} / ${dur}`); ``` - -### getAvailableAudioBitrates ################################################## + +### getError ################################################################### -_return value_: ``Array.`` +_return value_: ``Error|null`` -The different bitrates available for the current audio -[Adaptation](../terms.md#adaptation), in bits per seconds. +Returns the current "fatal error" if one happenned for the last loaded content. -In _DirectFile_ mode (see [loadVideo -options](./loadVideo_options.md#prop-transport)), returns an empty Array. +Returns `null` otherwise. + +A "fatal error" is an error which led the current loading/loaded content to +completely stop. +Such errors are usually also sent through the `"error"` event when they happen. + +See [the Player Error documentation](./errors.md) for more information. #### Example + ```js -const audioBitrates = player.getAvailableAudioBitrates(); -if (audioBitrates.length) { - console.log( - "The current audio is available in the following bitrates", - audioBitrates.join(", ") - ); +const error = player.getError(); + +if (!error) { + console.log("The player did not crash"); +} else if (error.code === "PIPELINE_LOAD_ERROR") { + console.error("The player crashed due to a failing request"); +} else { + console.error(`The player crashed: ${error.code}`); } ``` - -### getVideoBitrate ############################################################ - -_return value_: ``Number|undefined`` + +### getVideoElement ############################################################ -Returns the video bitrate of the last downloaded video segment, in bits per -seconds. +_return value_: ``HTMLMediaElement`` -Returns ``undefined`` if no content is loaded. +Returns the media element used by the RxPlayer. -In _DirectFile_ mode (see [loadVideo -options](./loadVideo_options.md#prop-transport)), returns ``undefined``. +You're not encouraged to use its APIs as they can enter in conflict with the +RxPlayer's API. +Despite its name, this method can also return an audio element if the RxPlayer +was instantiated with one. - -### getAudioBitrate ############################################################ +#### Example -_return value_: ``Number|undefined`` +```js +const videoElement = player.getVideoElement(); +videoElement.className = "my-video-element"; +``` -Returns the audio bitrate of the last downloaded audio segment, in bits per -seconds. -Returns ``undefined`` if no content is loaded. + +### dispose #################################################################### -In _DirectFile_ mode (see [loadVideo -options](./loadVideo_options.md#prop-transport)), returns ``undefined``. +Free the ressources used by the player. +You can call this method if you know you won't need the RxPlayer anymore. - -### getMaxVideoBitrate ######################################################### +:warning: The player won't work correctly after calling this method. -_return value_: ``Number|undefined`` -Returns the maximum set video bitrate to which switching is possible, in bits -per seconds. -This only affects adaptive strategies (you can bypass this limit by calling -``setVideoBitrate``), and is set to ``Infinity`` when no limit has been set. + +## Speed control ############################################################### +The following methods allows to update the current speed of playback (also +called the "playback rate"). - -### getMaxAudioBitrate ######################################################### -_return value_: ``Number"undefined`` + +### setPlaybackRate ############################################################ -Returns the maximum set audio bitrate to which switching is possible, in bits -per seconds. +_arguments_: ``Number`` -This only affects adaptive strategies (you can bypass this limit by calling -``setAudioBitrate``), and is set to ``Infinity`` when no limit has been set. +Updates the current playback rate. +Setting that value to `1` reset the playback rate to its "normal" rythm. - -### setVideoBitrate ############################################################ +Setting it to `2` allows to play at a speed multiplied by 2 relatively to +regular playback. -_arguments_: ``Number`` +Setting it to `0.5` allows to play at half the speed relatively to regular +playback. -Force the current video track to be of a certain bitrate. +etc. -If an video [Representation](../terms.md#representation) (in the current video -[Adaptation](../terms.md#adaptation)) is found with the exact same bitrate, this -Representation will be set. +#### Example -If no video Representation is found with the exact same bitrate, either: +```js +// plays three times faster than normal +player.setPlaybackRate(3); +``` - - the video Representation immediately inferior to it will be chosen instead - (the closest inferior) - - if no video Representation has a bitrate lower than that value, the video - Representation with the lowest bitrate will be chosen instead. + +### getPlaybackRate ############################################################ +_return value_: ``Number`` -Set to ``-1`` to deactivate (and thus re-activate adaptive streaming for video -tracks). +Returns the current video playback rate. ``1`` for normal playback, ``2`` when +playing at double the speed, etc. -When active (called with a positive value), adaptive streaming for video tracks -will be disabled to stay in the chosen Representation. +#### Example -You can use ``getAvailableVideoBitrates`` to get the list of available bitrates -you can set on the current content. +```js +const currentPlaybackRate = player.getPlaybackRate(); +console.log(`Playing at a x${currentPlaybackRate}} speed`); +``` -Note that the value set is persistent between ``loadVideo`` calls. -As such, this method can also be called when no content is playing (the same -rules apply for future contents). ---- -:warning: This option will have no effect for contents loaded in _DirectFile_ -mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). + +## Volume control ############################################################## ---- +Those methods allows to have control over the current audio volume of playing +contents. - -### setAudioBitrate ############################################################ + +### setVolume ################################################################## _arguments_: ``Number`` -Force the current audio track to be of a certain bitrate. +Set the current volume, from 0 (no sound) to 1 (the maximum sound level). + +Note that the volume set here is persisted even when loading another content. +As such, this method can also be called when no content is currently playing. -If an audio [Representation](../terms.md#representation) (in the current audio -[Adaptation](../terms.md#adaptation)) is found with the exact same bitrate, this -Representation will be set. +#### Example + +```js +// set the full volume +player.setVolume(1); +``` -If no audio Representation is found with the exact same bitrate, either: - - the audio Representation immediately inferior to it will be chosen instead - (the closest inferior) + +### getVolume ################################################################## - - if no audio Representation has a bitrate lower than that value, the audio - Representation with the lowest bitrate will be chosen instead. +_return value_: ``Number`` +Current volume of the player, from 0 (no sound) to 1 (maximum sound). +0 if muted through the `mute` API. -Set to ``-1`` to deactivate (and thus re-activate adaptive streaming for audio -tracks). +As the volume is not dependent on a single content (it is persistent), this +method can also be called when no content is playing. -When active (called with a positive value), adaptive streaming for audio tracks -will be disabled to stay in the chosen Representation. +#### Example -You can use ``getAvailableAudioBitrates`` to get the list of available bitrates -you can set on the current content. +```js +const volume = player.getVolume(); -Note that the value set is persistent between ``loadVideo`` calls. -As such, this method can also be called when no content is playing (the same -rules apply for future contents). +if (volume === 1) { + console.log("You're playing at maximum volume"); +} else if (volume === 0) { + console.log("You're playing at no volume"); +} else if (volume > 0.5) { + console.log("You're playing at a high volume"); +} else { + console.log("You're playing at a low volume"); +} +``` ---- -:warning: This option will have no effect for contents loaded in _DirectFile_ -mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). + +### mute ####################################################################### ---- +Mute the volume. +Basically set the volume to 0 while keeping in memory the previous volume to +reset it at the next `unMute` call. +As the volume is not dependent on a single content (it is persistent), this +method can also be called when no content is playing. - -### getManualVideoBitrate ###################################################### +#### Example -_arguments_: ``Number`` +```js +// mute the current volume +player.mute(); +``` -Get the last video bitrate manually set. Either via ``setVideoBitrate`` or via -the ``initialVideoBitrate`` constructor option. -This value can be different than the one returned by ``getVideoBitrate``: - - ``getManualVideoBitrate`` returns the last bitrate set manually by the user - - ``getVideoBitrate`` returns the actual bitrate of the current video track + +### unMute ##################################################################### -``-1`` when no video bitrate is forced. +When muted, restore the volume to the one previous to the last ``mute`` call. +When the volume is already superior to `0`, this call won't do anything. - -### getManualAudioBitrate ###################################################### +As the volume is not dependent on a single content (it is persistent), this +method can also be called when no content is playing. -_arguments_: ``Number`` +#### Example -Get the last audio bitrate manually set. Either via ``setAudioBitrate`` or via -the ``initialAudioBitrate`` constructor option. +```js +// mute the current volume +player.mute(); -This value can be different than the one returned by ``getAudioBitrate``: - - ``getManualAudioBitrate`` returns the last bitrate set manually by the user - - ``getAudioBitrate`` returns the actual bitrate of the current audio track +// unmute and restore the previous volume +player.unMute(); +``` -``-1`` when no audio bitrate is forced. + +### isMute ##################################################################### - -### setMaxVideoBitrate ######################################################### +_returns_: ``Boolean`` -_arguments_: ``Number`` +Returns true if the volume is set to `0`. -Set the maximum video bitrate reachable through adaptive streaming. The player -will never automatically switch to a video -[Representation](../terms.md#representation) with a higher bitrate. +#### Example -This limit can be removed by setting it to ``Infinity``: ```js -// remove video bitrate limit -player.setMaxVideoBitrate(Infinity); +if (player.isMute()) { + console.log("The content plays with no sound."); +} ``` -This only affects adaptive strategies (you can bypass this limit by calling -``setVideoBitrate``). - ---- -:warning: This option will have no effect for contents loaded in _DirectFile_ -mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). ---- + +## Track selection ############################################################# +The following methods allows to choose the right video audio or text track and +to obtain information about the currently playing tracks. - -### setMaxAudioBitrate ######################################################### -_arguments_: ``Number`` + +### getAudioTrack ############################################################## -Set the maximum audio bitrate reachable through adaptive streaming. The player -will never automatically switch to a audio -[Representation](../terms.md#representation) with a higher bitrate. +_returns_: ``Object|null|undefined`` -This limit can be removed by setting it to ``Infinity``: -```js -// remove audio bitrate limit -player.setMaxAudioBitrate(Infinity); -``` +Get information about the audio track currently set. +``null`` if no audio track is enabled right now. -This only affects adaptive strategies (you can bypass this limit by calling -``setAudioBitrate``). +If an audio track is set and information about it is known, this method will +return an object with the following properties: ---- + - ``id`` (``Number|string``): The id used to identify this track. No other + audio track for the same [Period](../terms.md#period) will have the + same `id`. -:warning: This option will have no effect for contents loaded in _DirectFile_ -mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). + This can be useful when setting the track through the `setAudioTrack` + method. ---- + - ``language`` (``string``): The language the audio track is in, as set in the + [Manifest](../terms.md#manifest). + - ``normalized`` (``string``): An attempt to translate the ``language`` + property into an ISO 639-3 language code (for now only support translations + from ISO 639-1 and ISO 639-3 language codes). If the translation attempt + fails (no corresponding ISO 639-3 language code is found), it will equal the + value of ``language`` - -### setWantedBufferAhead ####################################################### + - ``audioDescription`` (``Boolean``): Whether the track is an audio + description (for the visually impaired or not). -_arguments_: ``Number`` + - ``dub`` (``Boolean|undefined``): If set to `true`, this audio track is a + "dub", meaning it was recorded in another language than the original. + If set to `false`, we know that this audio track is in an original language. + This property is `undefined` if we do not known whether it is in an original + language. -Set the buffering goal, as a duration ahead of the current position, in seconds. -Once this size of buffer reached, the player won't try to download new video -segments anymore. ---- +``undefined`` if no audio content has been loaded yet or if its information is +unknown. -:warning: This option will have no effect for contents loaded in _DirectFile_ -mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). +-- ---- +Note for multi-Period contents: +This method will only return the chosen audio track for the +[Period](../terms.md#period) that is currently playing. - -### getWantedBufferAhead ####################################################### +__ -_return value_: ``Number`` -_defaults_: ``30`` +In _DirectFile_ mode +(see [loadVideo options](./loadVideo_options.md#prop-transport)), if there is +no audio tracks API in the browser, this method will return ``undefined``. -returns the buffering goal, as a duration ahead of the current position, in -seconds. + +### getTextTrack ############################################################### - -### setMaxBufferBehind ######################################################### +_returns_: ``Object|null|undefined`` -_arguments_: ``Number`` +Get information about the text track currently set. +``null`` if no audio track is enabled right now. -Set the maximum kept past buffer, in seconds. -Everything before that limit (``currentPosition - maxBufferBehind``) will be -automatically garbage collected. +If a text track is set and information about it is known, this method will +return an object with the following properties: -This feature is not necessary as the browser is already supposed to deallocate -memory from old segments if/when the memory is scarce. + - ``id`` (``Number|string``): The id used to identify this track. No other + text track for the same [Period](../terms.md#period) will have the same + `id`. -However on some custom targets, or just to better control the memory imprint -of the player, you might want to set this limit. You can set it to -``Infinity`` to remove any limit and just let the browser do this job. + This can be useful when setting the track through the `setTextTrack` method. ---- + - ``language`` (``string``): The language the text track is in, as set in the + [Manifest](../terms.md#manifest). -:warning: This option will have no effect for contents loaded in _DirectFile_ -mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). + - ``normalized`` (``string``): An attempt to translate the ``language`` + property into an ISO 639-3 language code (for now only support translations + from ISO 639-1 and ISO 639-3 language codes). If the translation attempt + fails (no corresponding ISO 639-3 language code is found), it will equal the + value of ``language`` ---- + - ``closedCaption`` (``Boolean``): Whether the track is specially adapted for + the hard of hearing or not. - -### getMaxBufferBehind ######################################################### +``undefined`` if no text content has been loaded yet or if its information is +unknown. -_return value_: ``Number`` -_defaults_: ``Infinity`` +-- -Returns the maximum kept past buffer, in seconds. +Note for multi-Period contents: +This method will only return the chosen text track for the +[Period](../terms.md#period) that is currently playing. - -### setMaxBufferAhead ########################################################## +__ -_arguments_: ``Number`` +In _DirectFile_ mode +(see [loadVideo options](./loadVideo_options.md#prop-transport)), if there is +no text tracks API in the browser, this method will return ``undefined``. -Set the maximum kept buffer ahead of the current position, in seconds. -Everything superior to that limit (``currentPosition + maxBufferAhead``) will -be automatically garbage collected. This feature is not necessary as -the browser is already supposed to deallocate memory from old segments if/when -the memory is scarce. -However on some custom targets, or just to better control the memory imprint of -the player, you might want to set this limit. You can set it to ``Infinity`` to -remove any limit and just let the browser do this job. + +### getVideoTrack ############################################################## -The minimum value between this one and the one returned by -``getWantedBufferAhead`` will be considered when downloading new segments. +_returns_: ``Object|null|undefined`` -:warning: Bear in mind that a too-low configuration there (e.g. inferior to -``10``) might prevent the browser to play the content at all. +Get information about the video track currently set. +``null`` if no audio track is enabled right now. ---- +If a video track is set and information about it is known, this method will +return an object with the following properties: -:warning: This option will have no effect for contents loaded in _DirectFile_ -mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). + - ``id`` (``Number|string``): The id used to identify this track. No other + video track for the same [Period](../terms.md#period) will have the same + `id`. ---- + This can be useful when setting the track through the `setVideoTrack` + method. + - ``representations`` (``Array.``): + [Representations](../terms.md#representation) of this video track, with + attributes: - -### getMaxBufferAhead ########################################################## + - ``id`` (``string``): The id used to identify this Representation. + No other Representation from this track will have the same `id`. -_return value_: ``Number`` -_defaults_: ``Infinity`` + - ``bitrate`` (``Number``): The bitrate of this Representation, in bits per + seconds. -Returns the maximum kept buffer ahead of the current position, in seconds. + - ``width`` (``Number|undefined``): The width of video, in pixels. + - ``height`` (``Number|undefined``): The height of video, in pixels. - -### setVolume ################################################################## + - ``codec`` (``string|undefined``): The codec given in standard MIME type + format. -_arguments_: ``Number`` + - ``frameRate`` (``string|undefined``): The video frame rate. -Set the new volume, from 0 (no sound) to 1 (the maximum sound level). + - ``signInterpreted`` (``Boolean|undefined``): If set to `true`, the track is + known to contain an interpretation in sign language. + If set to `false`, the track is known to not contain that type of content. + If not set or set to undefined we don't know whether that video track + contains an interpretation in sign language. - -### mute ####################################################################### +``undefined`` if no video content has been loaded yet or if its information is +unknown. -Cut the volume. Basically set the volume to 0 while keeping in memory the -previous volume. +-- +Note for multi-Period contents: - -### unMute ##################################################################### +This method will only return the chosen video track for the +[Period](../terms.md#period) that is currently playing. -Restore the volume when it has been muted, to the one previous the ``mute`` -call. +-- - - -### isMute ##################################################################### - -_returns_: ``Boolean`` - -Returns true if the volume is muted i.e., set to 0. +In _DirectFile_ mode +(see [loadVideo options](./loadVideo_options.md#prop-transport)), if there is +no video tracks API in the browser, this method will return ``undefined``. @@ -1008,7 +1008,7 @@ Returns the list of available audio tracks for the current content. Each of the objects in the returned array have the following properties: - ``active`` (``Boolean``): Whether the track is the one currently active or - not. + not. Only maximum one audio track can be active at a time. - ``id`` (``string``): The id used to identify the track. Use it for setting the track via ``setAudioTrack``. @@ -1031,10 +1031,19 @@ Each of the objects in the returned array have the following properties: This property is `undefined` if we do not known whether it is in an original language. +-- -In _DirectFile_ mode -(see [loadVideo options](./loadVideo_options.md#prop-transport)), if there are -no supported tracks in the file or no track management API : returns an empty +Note for multi-Period contents: + +This method will only return the available tracks of the +[Period](../terms.md#period) that is currently playing. + +-- + + +In _DirectFile_ mode (see [loadVideo +options](./loadVideo_options.md#prop-transport)), if there are no supported +tracks in the file or no track management API this method will return an empty Array. @@ -1065,10 +1074,19 @@ Each of the objects in the returned array have the following properties: - ``active`` (``Boolean``): Whether the track is the one currently active or not. +-- -In _DirectFile_ mode -(see [loadVideo options](./loadVideo_options.md#prop-transport)), if there are -no supported tracks in the file or no track management API : returns an empty +Note for multi-Period contents: + +This method will only return the available tracks of the +[Period](../terms.md#period) that is currently playing. + +-- + + +In _DirectFile_ mode (see [loadVideo +options](./loadVideo_options.md#prop-transport)), if there are no supported +tracks in the file or no track management API this method will return an empty Array. @@ -1111,137 +1129,46 @@ Each of the objects in the returned array have the following properties: If not set or set to undefined we don't know whether that video track contains an interpretation in sign language. +-- -In _DirectFile_ mode -(see [loadVideo options](./loadVideo_options.md#prop-transport)), if there are -no supported tracks in the file or no track management API : returns an empty -Array. - - - -### getAudioTrack ############################################################## - -_returns_: ``Object|null|undefined`` - -Get the audio track currently set. ``null`` if no audio track is enabled right -now. - -The track is an object with the following properties: - - - ``id`` (``Number|string``): The id used to identify the track. Use it for - setting the track via ``setAudioTrack``. - - - ``language`` (``string``): The language the audio track is in, as set in the - [Manifest](../terms.md#manifest). - - - ``normalized`` (``string``): An attempt to translate the ``language`` - property into an ISO 639-3 language code (for now only support translations - from ISO 639-1 and ISO 639-3 language codes). If the translation attempt - fails (no corresponding ISO 639-3 language code is found), it will equal the - value of ``language`` - - - ``audioDescription`` (``Boolean``): Whether the track is an audio - description (for the visually impaired or not). - - - ``dub`` (``Boolean|undefined``): If set to `true`, this audio track is a - "dub", meaning it was recorded in another language than the original. - If set to `false`, we know that this audio track is in an original language. - This property is `undefined` if we do not known whether it is in an original - language. - - -``undefined`` if no content has been loaded yet. - -In _DirectFile_ mode -(see [loadVideo options](./loadVideo_options.md#prop-transport)), if there is -no audio tracks API in the browser, return ``undefined``. - - -### getTextTrack ############################################################### - -_returns_: ``Object|null`` - -Get the audio track currently set. ``null`` if no text track is enabled right -now. - -The track is an object with the following properties: - - - ``id`` (``Number|string``): The id used to identify the track. Use it for - setting the track via ``setTextTrack``. - - - ``language`` (``string``): The language the text track is in, as set in the - [Manifest](../terms.md#manifest). - - - ``normalized`` (``string``): An attempt to translate the ``language`` - property into an ISO 639-3 language code (for now only support translations - from ISO 639-1 and ISO 639-3 language codes). If the translation attempt - fails (no corresponding ISO 639-3 language code is found), it will equal the - value of ``language`` - - - ``closedCaption`` (``Boolean``): Whether the track is specially adapted for - the hard of hearing or not. - - -``undefined`` if no content has been loaded yet. - -In _DirectFile_ mode -(see [loadVideo options](./loadVideo_options.md#prop-transport)), if there is -no text tracks API in the browser, return ``undefined``. - - - -### getVideoTrack ############################################################## - -_returns_: ``Object|null|undefined`` - -Get the video track currently set. ``null`` if no video track is enabled right -now. - -The track is an object with the following properties: - - - ``id`` (``string``): The id used to identify the track. Use it for setting - the track via ``setVideoTrack``. - - - ``representations`` (``Array.``): - [Representations](../terms.md#representation) of this video track, with - attributes: - - - ``id`` (``string``): The id used to identify this Representation. - - - ``bitrate`` (``Number``): The bitrate of this Representation, in bits per - seconds. - - - ``width`` (``Number|undefined``): The width of video, in pixels. +Note for multi-Period contents: - - ``height`` (``Number|undefined``): The height of video, in pixels. +This method will only return the available tracks of the +[Period](../terms.md#period) that is currently playing. - - ``codec`` (``string|undefined``): The codec given in standard MIME type - format. +-- - - ``frameRate`` (``string|undefined``): The video framerate. +In _DirectFile_ mode (see [loadVideo +options](./loadVideo_options.md#prop-transport)), if there are no supported +tracks in the file or no track management API this method will return an empty +Array. - - ``signInterpreted`` (``Boolean|undefined``): If set to `true`, the track is - known to contain an interpretation in sign language. - If set to `false`, the track is known to not contain that type of content. - If not set or set to undefined we don't know whether that video track - contains an interpretation in sign language. -``undefined`` if no content has been loaded yet. + +### setAudioTrack ############################################################## +_arguments_: ``string|Number`` -In _DirectFile_ mode -(see [loadVideo options](./loadVideo_options.md#prop-transport)), if there is -no video tracks API in the browser, return ``undefined``. +Change the current audio track. +The argument to this method is the wanted track's `id` property. This `id` can +for example be obtained on the corresponding track object returned by the +``getAvailableAudioTracks`` method. - -### setAudioTrack ############################################################## +-- -_arguments_: ``string|Number`` +Note for multi-Period contents: -Set a new audio track from its id, recuperated from ``getAvailableAudioTracks``. +This method will only have an effect on the [Period](../terms.md#period) that is +currently playing. +If you want to update the track for other Periods as well, you might want to +either: + - update the preferred audio tracks through the + [setPreferredAudioTracks](#meth-setPreferredAudioTracks) method. + - update the current audio track once a `"periodChange"` event has been + received. ---- +-- :warning: If used on Safari, in _DirectFile_ mode, the track change may change the track on other track type (e.g. changing video track may change subtitle @@ -1251,10 +1178,9 @@ This has two potential reasons : - Safari may decide to enable a track for accessibility or user language convenience (e.g. Safari may switch subtitle to your OS language if you pick another audio language) -The user may know through the [audioTrackChange] -(./player_events.md#events-audioTrackChange) event that the track has changed. +You can know if another track has changed by listening to the corresponding +events that the tracks have changed. ---- @@ -1262,9 +1188,27 @@ The user may know through the [audioTrackChange] _arguments_: ``string`` -Set a new text track from its id, recuperated from ``getAvailableTextTracks``. +Change the current text (subtitles) track. ---- +The argument to this method is the wanted track's `id` property. This `id` can +for example be obtained on the corresponding track object returned by the +``getAvailableTextTracks`` method. + +-- + +Note for multi-Period contents: + +This method will only have an effect on the [Period](../terms.md#period) that is +currently playing. +If you want to update the track for other Periods as well, you might want to +either: + - update the preferred text tracks through the + [setPreferredTextTracks](#meth-setPreferredTextTracks) method. + - update the current text track once a `"periodChange"` event has been + received. + + +-- :warning: If used on Safari, in _DirectFile_ mode, the track change may change the track on other track type (e.g. changing video track may change subtitle @@ -1274,16 +1218,30 @@ This has two potential reasons : - Safari may decide to enable a track for accessibility or user language convenience (e.g. Safari may switch subtitle to your OS language if you pick another audio language) -The user may know through the [textTrackChange] -(./player_events.md#events-textTrackChange) event that the track has changed. - ---- +You can know if another track has changed by listening to the corresponding +events that the tracks have changed. ### disableTextTrack ########################################################### -Deactivate the current text track, if one. +Disable the current text track, if one. + +After calling that method, no subtitles track will be displayed until +`setTextTrack` is called. + +-- + +Note for multi-Period contents: + +This method will only have an effect on the [Period](../terms.md#period) that is +currently playing. +If you want to disable the text track for other Periods as well, you might want +to either: + - update the preferred text tracks through the + [setPreferredTextTracks](#meth-setPreferredTextTracks) method to include + `null` (which meaning that you want no text track). + - call `disableTextTrack` once a `"periodChange"` event has been received. @@ -1291,7 +1249,11 @@ Deactivate the current text track, if one. _arguments_: ``string|Number`` -Set a new video track from its id, recuperated from ``getAvailableVideoTracks``. +Change the current video track. + +The argument to this method is the wanted track's `id` property. This `id` can +for example be obtained on the corresponding track object returned by the +``getAvailableAudioTracks`` method. Setting a new video track when a previous one was already playing can lead the rx-player to "reload" this content. @@ -1314,26 +1276,50 @@ During this period of time: - ``setAudioTrack`` will throw - ``setTextTrack`` will throw ---- +-- + +Note for multi-Period contents: + +This method will only have an effect on the [Period](../terms.md#period) that is +currently playing. +If you want to update the track for other Periods as well, you might want to +either: + - update the preferred video tracks through the + [setPreferredVideoTracks](#meth-setPreferredVideoTracks) method. + - update the current video track once a `"periodChange"` event has been + received. + +-- :warning: This option will have no effect in _DirectFile_ mode (see [loadVideo options](./loadVideo_options.md#prop-transport)) when either : - No video track API is supported on the current browser - The media file tracks are not supported on the browser ---- - ### disableVideoTrack ########################################################## _return value_: ``void`` -Deactivate the current video track, if one. +Disable the current video track, if one. Might enter in `RELOADING` state for a short period after calling this API. ---- +-- + +Note for multi-Period contents: + +This method will only have an effect on the [Period](../terms.md#period) that is +currently playing. +If you want to disable the video track for other Periods as well, you might want +to either: + - update the preferred video tracks through the + [setPreferredVideoTracks](#meth-setPreferredVideoTracks) method to include + `null` (which meaning that you want no video track). + - call `disableVideoTrack` once a `"periodChange"` event has been received. + +-- :warning: This option may have no effect in _DirectFile_ mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). @@ -1351,8 +1337,6 @@ Due to this fact, we do not recommend using this API in directfile mode for now. You might even receive a reassuring `videoTrackChange` event (with a `null` payload) while the video track is still actually active. ---- - ### setPreferredAudioTracks #################################################### @@ -1421,7 +1405,7 @@ Simply put, once set this preference will be applied to all contents but: tracks at the time they were first loaded). To update the current audio track in those cases, you should use the -`setAudioTrack` method once they are currently played. +`setAudioTrack` method once they are currently playing. #### Examples @@ -1437,149 +1421,653 @@ player.setPreferredAudioTracks([ ]) ``` -Now let's imagine that you want to have in priority a track that contain at -least one profile in Dolby Digital Plus (ec-3 codec) without caring about the -language: -```js -player.setPreferredAudioTracks([ { codec: { all: false, test: /ec-3/ } ]); -``` +Now let's imagine that you want to have in priority a track that contain at +least one profile in Dolby Digital Plus (ec-3 codec) without caring about the +language: +```js +player.setPreferredAudioTracks([ { codec: { all: false, test: /ec-3/ } ]); +``` + +At last, let's combine both examples by preferring french over itialian, italian +over english while preferring it to be in Dolby Digital Plus: +```js + +player.setPreferredAudioTracks([ + { + language: "fra", + audioDescription: false, + codec: { all: false, test: /ec-3/ } + }, + + // We still prefer non-DD+ french over DD+ italian + { language: "fra", audioDescription: false }, + + { + language: "ita", + audioDescription: false, + codec: { all: false, test: /ec-3/ } + }, + { language: "ita", audioDescription: false }, + + { + language: "eng", + audioDescription: false, + codec: { all: false, test: /ec-3/ } + }, + { language: "eng", audioDescription: false } +]); +``` + +-- + +:warning: This option will have no effect in _DirectFile_ mode +(see [loadVideo options](./loadVideo_options.md#prop-transport)) when either : +- No audio track API is supported on the current browser +- The media file tracks are not supported on the browser + + + +### getPreferredAudioTracks #################################################### + +_return value_: ``Array.`` + +Returns the current list of preferred audio tracks - by order of preference. + +This returns the data in the same format that it was given to either the +`preferredAudioTracks` constructor option or the last `setPreferredAudioTracks` +if it was called. + +It will return an empty Array if none of those two APIs were used until now. + + + +### setPreferredTextTracks ##################################################### + +_arguments_: ``Array.`` + +Update the text track (or subtitles) preferences at any time. + +This method takes an array of objects describing the languages wanted: +```js +{ + language: "fra", // {string} The wanted language + // (ISO 639-1, ISO 639-2 or ISO 639-3 language code) + closedCaption: false // {Boolean} Whether the text track should be a closed + // caption for the hard of hearing +} +``` + +All elements in that Array should be set in preference order: from the most +preferred to the least preferred. You can set `null` for no subtitles. + +When encountering a new Period or a new content, the RxPlayer will then try to +choose its text track by comparing what is available with your current +preferences (i.e. if the most preferred is not available, it will look if the +second one is etc.). + +Please note that those preferences will only apply either to the next loaded +content or to the next newly encountered Period. +Simply put, once set this preference will be applied to all contents but: + + - the current Period being played (or the current loaded content, in the case + of Smooth streaming). In that case, the current text track preference will + stay in place. + + - the Periods which have already been played in the current loaded content. + Those will keep the last set text track preference at the time it was + played. + +To update the current text track in those cases, you should use the +`setTextTrack` method. + +#### Example + +Let's imagine that you prefer to have french or italian subtitles.If not found, +you want no subtitles at all. + +You will thus call ``setPreferredTextTracks`` that way. + +```js +player.setPreferredTextTracks([ + { language: "fra", closedCaption: false }, + { language: "ita", closedCaption: false }, + null +]) +``` + +-- + +:warning: This option will have no effect in _DirectFile_ mode +(see [loadVideo options](./loadVideo_options.md#prop-transport)) when either : +- No text track API is supported on the current browser +- The media file tracks are not supported on the browser + + + +### getPreferredTextTracks ##################################################### + +_return value_: ``Array.`` + +Returns the current list of preferred text tracks - by order of preference. + +This returns the data in the same format that it was given to either the +`preferredTextTracks` constructor option or the last `setPreferredTextTracks` if +it was called: + +```js +{ + language: "fra", // {string} The wanted language + // (ISO 639-1, ISO 639-2 or ISO 639-3 language code) + closedCaption: false // {Boolean} Whether the text track should be a closed + // caption for the hard of hearing +} +``` + + + + +## Bitrate selection ########################################################### + +The following methods allows to choose a given bitrate for audio or video +content. It can also enable or disable an adaptive bitrate logic or influence +it. + + + +### getAvailableVideoBitrates ################################################## + +_return value_: ``Array.`` + +The different bitrates available for the current video track in bits per +seconds. + +-- + +Note for multi-Period contents: + +This method will only return the available video bitrates of the +[Period](../terms.md#period) that is currently playing. + +-- + +In _DirectFile_ mode (see [loadVideo +options](./loadVideo_options.md#prop-transport)), returns an empty Array. + +#### Example + +```js +const videoBitrates = player.getAvailableVideoBitrates(); +if (videoBitrates.length) { + console.log( + "The current video is available in the following bitrates", + videoBitrates.join(", ") + ); +} +``` + + + +### getAvailableAudioBitrates ################################################## + +_return value_: ``Array.`` + +The different bitrates available for the current audio track in bits per +seconds. + +-- + +Note for multi-Period contents: + +This method will only return the available audio bitrates of the +[Period](../terms.md#period) that is currently playing. + +-- + +In _DirectFile_ mode (see [loadVideo +options](./loadVideo_options.md#prop-transport)), returns an empty Array. + +#### Example + +```js +const audioBitrates = player.getAvailableAudioBitrates(); +if (audioBitrates.length) { + console.log( + "The current audio is available in the following bitrates", + audioBitrates.join(", ") + ); +} +``` + + + +### getVideoBitrate ############################################################ + +_return value_: ``Number|undefined`` + +Returns the bitrate of the video quality currently set, in bits per second. + +Returns ``undefined`` if no content is loaded. + +-- + +Note for multi-Period contents: + +This method will only return the chosen video bitrate for the +[Period](../terms.md#period) that is currently playing. + +-- + +In _DirectFile_ mode (see [loadVideo +options](./loadVideo_options.md#prop-transport)), returns ``undefined``. + + + +### getAudioBitrate ############################################################ + +_return value_: ``Number|undefined`` + +Returns the bitrate of the audio quality currently set, in bits per second. + +Returns ``undefined`` if no content is loaded. + +-- + +Note for multi-Period contents: + +This method will only return the chosen audio bitrate for the +[Period](../terms.md#period) that is currently playing. + +-- + +In _DirectFile_ mode (see [loadVideo +options](./loadVideo_options.md#prop-transport)), returns ``undefined``. + + + +### setMaxVideoBitrate ######################################################### + +_arguments_: ``Number`` + +Set the maximum video bitrate reachable through adaptive streaming. The player +will never automatically switch to a video quality with a higher bitrate. + +This limit can be removed by setting it to ``Infinity``: +```js +// remove video bitrate limit +player.setMaxVideoBitrate(Infinity); +``` + +The effect of this method is persisted from content to content. As such, it can +even be called when no content is currently loaded. + +Note that this only affects adaptive strategies (you can bypass this limit by +calling ``setVideoBitrate``). + +-- + +:warning: This option will have no effect for contents loaded in _DirectFile_ +mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). + + + +### setMaxAudioBitrate ######################################################### + +_arguments_: ``Number`` + +Set the maximum audio bitrate reachable through adaptive streaming. The player +will never automatically switch to a audio +[Representation](../terms.md#representation) with a higher bitrate. + +This limit can be removed by setting it to ``Infinity``: +```js +// remove audio bitrate limit +player.setMaxAudioBitrate(Infinity); +``` + +The effect of this method is persisted from content to content. As such, it can +even be called when no content is currently loaded. + +Note that this only affects adaptive strategies (you can bypass this limit by +calling ``setAudioBitrate``). + +-- + +:warning: This option will have no effect for contents loaded in _DirectFile_ +mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). + + + +### getMaxVideoBitrate ######################################################### + +_return value_: ``Number`` + +Returns the maximum video bitrate reachable through adaptive streaming, in bits +per seconds. + +This limit can be updated by calling the +[setMaxVideoBitrate](#meth-setMaxVideoBitrate) method. + +This limit only affects adaptive strategies (you can bypass this limit by +calling ``setVideoBitrate``), and is set to ``Infinity`` when no limit has been +set. + + + +### getMaxAudioBitrate ######################################################### + +_return value_: ``Number`` + +Returns the maximum audio bitrate reachable through adaptive streaming, in bits +per seconds. + +This limit can be updated by calling the +[setMaxAudioBitrate](#meth-setMaxAudioBitrate) method. + +This limit only affects adaptive strategies (you can bypass this limit by +calling ``setAudioBitrate``), and is set to ``Infinity`` when no limit has been +set. + + + +### setVideoBitrate ############################################################ + +_arguments_: ``Number`` + +Force the current video track to be of a certain bitrate. + +If an video quality in the current track is found with the exact same bitrate, +this quality will be set. + +If no video quality is found with the exact same bitrate, either: + + - the video quality with the closest bitrate inferior to that value will be + chosen. + + - if no video quality has a bitrate lower than that value, the video + quality with the lowest bitrate will be chosen instead. + +By calling this method with an argument set to ``-1``, this setting will be +disabled and the RxPlayer will chose the right quality according to its adaptive +logic. + +You can use ``getAvailableVideoBitrates`` to get the list of available bitrates +for the current video track. + +Note that the value set is persistent between ``loadVideo`` calls. +As such, this method can also be called when no content is playing (the same +rules apply for future contents). + +-- + +:warning: This option will have no effect for contents loaded in _DirectFile_ +mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). + + + +### setAudioBitrate ############################################################ + +_arguments_: ``Number`` + +Force the current audio track to be of a certain bitrate. + +If an audio quality in the current track is found with the exact same bitrate, +this quality will be set. + +If no audio quality is found with the exact same bitrate, either: + + - the audio quality with the closest bitrate inferior to that value will be + chosen. + + - if no audio quality has a bitrate lower than that value, the audio + quality with the lowest bitrate will be chosen instead. + +By calling this method with an argument set to ``-1``, this setting will be +disabled and the RxPlayer will chose the right quality according to its adaptive +logic. + +You can use ``getAvailableAudioBitrates`` to get the list of available bitrates +for the current audio track. + +Note that the value set is persistent between ``loadVideo`` calls. +As such, this method can also be called when no content is playing (the same +rules apply for future contents). + +-- + +:warning: This option will have no effect for contents loaded in _DirectFile_ +mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). + + + +### getManualVideoBitrate ###################################################### + +_arguments_: ``Number`` + +Get the last video bitrate manually set. Either via ``setVideoBitrate`` or via +the ``initialVideoBitrate`` constructor option. + +This value can be different than the one returned by ``getVideoBitrate``: + - ``getManualVideoBitrate`` returns the last bitrate set manually by the user + - ``getVideoBitrate`` returns the actual bitrate of the current video track + +``-1`` when no video bitrate is forced. + + + +### getManualAudioBitrate ###################################################### + +_arguments_: ``Number`` + +Get the last audio bitrate manually set. Either via ``setAudioBitrate`` or via +the ``initialAudioBitrate`` constructor option. + +This value can be different than the one returned by ``getAudioBitrate``: + - ``getManualAudioBitrate`` returns the last bitrate set manually by the user + - ``getAudioBitrate`` returns the actual bitrate of the current audio track + +``-1`` when no audio bitrate is forced. + + + + +## Buffer control ############################################################## + +The methods in this chapter allow to get and set limits on how the current +buffer can grow. + + + +### setWantedBufferAhead ####################################################### + +_arguments_: ``Number`` + +Set the buffering goal, as a duration ahead of the current position, in seconds. + +Once this size of buffer reached, the player won't try to download new segments +anymore. + +-- + +:warning: This option will have no effect for contents loaded in _DirectFile_ +mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). + + + +### getWantedBufferAhead ####################################################### + +_return value_: ``Number`` +_defaults_: ``30`` + +returns the buffering goal, as a duration ahead of the current position, in +seconds. + + + +### setMaxBufferBehind ######################################################### + +_arguments_: ``Number`` + +Set the maximum kept buffer before the current position, in seconds. + +Everything before that limit (``currentPosition - maxBufferBehind``) will be +automatically garbage collected. + +This feature is not necessary as the browser should by default correctly +remove old segments from memory if/when the memory is scarce. + +However on some custom targets, or just to better control the memory footprint +of the player, you might want to set this limit. + +You can set it to ``Infinity`` to remove this limit and just let the browser do +this job instead. + +-- + +:warning: This option will have no effect for contents loaded in _DirectFile_ +mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). + + + +### getMaxBufferBehind ######################################################### + +_return value_: ``Number`` +_defaults_: ``Infinity`` + +Returns the maximum kept buffer before the current position, in seconds. + +This setting can be updated either by: + - calling the `setMaxBufferBehind` method. + - instanciating an RxPlayer with a `maxBufferBehind` property set. + + + +### setMaxBufferAhead ########################################################## + +_arguments_: ``Number`` + +Set the maximum kept buffer ahead of the current position, in seconds. + +Everything superior to that limit (``currentPosition + maxBufferAhead``) will +be automatically garbage collected. + +This feature is not necessary as the browser should by default correctly +remove old segments from memory if/when the memory is scarce. + +However on some custom targets, or just to better control the memory footprint +of the player, you might want to set this limit. + +You can set it to ``Infinity`` to remove any limit and just let the browser do +this job instead. + +The minimum value between this one and the one returned by +``getWantedBufferAhead`` will be considered when downloading new segments. + +:warning: Bear in mind that a too-low configuration there (e.g. inferior to +``10``) might prevent the browser to play the content at all. + +-- + +:warning: This option will have no effect for contents loaded in _DirectFile_ +mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). + -At last, let's combine both examples by preferring french over itialian, italian -over english while preferring it to be in Dolby Digital Plus: -```js + +### getMaxBufferAhead ########################################################## -player.setPreferredAudioTracks([ - { - language: "fra", - audioDescription: false, - codec: { all: false, test: /ec-3/ } - }, +_return value_: ``Number`` +_defaults_: ``Infinity`` - // We still prefer non-DD+ french over DD+ italian - { language: "fra", audioDescription: false }, +Returns the maximum kept buffer ahead of the current position, in seconds. - { - language: "ita", - audioDescription: false, - codec: { all: false, test: /ec-3/ } - }, - { language: "ita", audioDescription: false }, +This setting can be updated either by: + - calling the `setMaxBufferAhead` method. + - instanciating an RxPlayer with a `maxBufferAhead` property set. - { - language: "eng", - audioDescription: false, - codec: { all: false, test: /ec-3/ } - }, - { language: "eng", audioDescription: false } -]); -``` ---- -:warning: This option will have no effect in _DirectFile_ mode -(see [loadVideo options](./loadVideo_options.md#prop-transport)) when either : -- No audio track API is supported on the current browser -- The media file tracks are not supported on the browser + +### Buffer information ######################################################### ---- +The methods in this chapter allows to retrieve information about what is +currently buffered. - -### getPreferredAudioTracks #################################################### + +### getVideoLoadedTime ######################################################### -_return value_: ``Array.`` +_return value_: ``Number`` -Returns the current list of preferred audio tracks - by order of preference. +Returns in seconds the difference between: + - the start of the current contiguous loaded range. + - the end of it. -This returns the data in the same format that it was given to either the -`preferredAudioTracks` constructor option or the last `setPreferredAudioTracks` -if it was called. -It will return an empty Array if none of those two APIs were used until now. + +### getVideoPlayedTime ######################################################### +_return value_: ``Number`` - -### setPreferredTextTracks ##################################################### +Returns in seconds the difference between: + - the start of the current contiguous loaded range. + - the current time. -_arguments_: ``Array.`` -Update the text track (or subtitles) preferences at any time. + +### getVideoBufferGap ########################################################## -This method takes an array of objects describing the languages wanted: -```js -{ - language: "fra", // {string} The wanted language - // (ISO 639-1, ISO 639-2 or ISO 639-3 language code) - closedCaption: false // {Boolean} Whether the text track should be a closed - // caption for the hard of hearing -} -``` +_return value_: ``Number`` -All elements in that Array should be set in preference order: from the most -preferred to the least preferred. You can set `null` for no subtitles. +Returns in seconds the difference between: + - the current time. + - the end of the current contiguous loaded range. -When encountering a new Period or a new content, the RxPlayer will then try to -choose its text track by comparing what is available with your current -preferences (i.e. if the most preferred is not available, it will look if the -second one is etc.). -Please note that those preferences will only apply either to the next loaded -content or to the next newly encountered Period. -Simply put, once set this preference will be applied to all contents but: - - the current Period being played (or the current loaded content, in the case - of Smooth streaming). In that case, the current text track preference will - stay in place. + +## Content information ######################################################### - - the Periods which have already been played in the current loaded content. - Those will keep the last set text track preference at the time it was - played. +The methods documented in this chapter allows to obtain general information +about the current loaded content. -To update the current text track in those cases, you should use the -`setTextTrack` method. -#### Example + +### isLive ##################################################################### -Let's imagine that you prefer to have french or italian subtitles.If not found, -you want no subtitles at all. +_return value_: ``Boolean`` -You will thus call ``setPreferredTextTracks`` that way. +Returns ``true`` if the content is a "live" content (e.g. a live TV Channel). +``false`` otherwise. + +Also ``false`` if no content is loaded yet. + +#### Example ```js -player.setPreferredTextTracks([ - { language: "fra", closedCaption: false }, - { language: "ita", closedCaption: false }, - null -]) +if (player.isLive()) { + console.log("We're playing a live content"); +} ``` ---- - -:warning: This option will have no effect in _DirectFile_ mode -(see [loadVideo options](./loadVideo_options.md#prop-transport)) when either : -- No text track API is supported on the current browser -- The media file tracks are not supported on the browser ---- + +### getUrl ##################################################################### +_return value_: ``string|undefined`` - -### getPreferredTextTracks ##################################################### +Returns the URL of the downloaded [Manifest](../terms.md#manifest). -_return value_: ``Array.`` +In _DirectFile_ mode (see [loadVideo +options](./loadVideo_options.md#prop-transport)), returns the URL of the content +being played. -Returns the current list of preferred text tracks - by order of preference. +Returns ``undefined`` if no content is loaded yet. -This returns the data in the same format that it was given to either the -`preferredTextTracks` constructor option or the last `setPreferredTextTracks` if -it was called: +#### Example ```js -{ - language: "fra", // {string} The wanted language - // (ISO 639-1, ISO 639-2 or ISO 639-3 language code) - closedCaption: false // {Boolean} Whether the text track should be a closed - // caption for the hard of hearing +const url = player.getUrl(); +if (url) { + console.log("We are playing the following content:", url); } ``` @@ -1770,61 +2258,6 @@ An Representation object structure is relatively complex and is described in the options](./loadVideo_options.md#prop-transport)). - -### dispose #################################################################### - -Free the ressources used by the player. - -!warning!: The player won't work correctly after calling this method. - - - -### getVideoLoadedTime ######################################################### - -_return value_: ``Number`` - -Returns in seconds the difference between: - - the start of the current contiguous loaded range. - - the end of it. - - - -### getVideoPlayedTime ######################################################### - -_return value_: ``Number`` - -Returns in seconds the difference between: - - the start of the current contiguous loaded range. - - the current time. - - - -### getVideoBufferGap ########################################################## - -_return value_: ``Number`` - -Returns in seconds the difference between: - - the current time. - - the end of the current contiguous loaded range. - - - -### getPlaybackRate ############################################################ - -_return value_: ``Number`` - -Returns the current video normal playback rate (speed when playing). ``1`` for -normal playback, ``2`` when playing *2, etc. - - - -### setPlaybackRate ############################################################ - -_arguments_: ``Number`` - -Updates the "normal" (when playing) playback rate for the video. - - ### getCurrentKeySystem ######################################################## @@ -1833,54 +2266,23 @@ _return value_: ``string|undefined`` Returns the type of keySystem used for DRM-protected contents. - -### getMinimumPosition ######################################################### - -_return value_: ``Number|null`` - -The minimum seekable player position. ``null`` if no content is loaded. - -This is useful for live contents, where server-side buffer size are often not -infinite. This method allows thus to seek at the earliest possible time. - -#### Example -```js -// seeking to the earliest position possible (beginning of the buffer for live -// contents, position '0' for non-live contents). -player.seekTo({ - position: player.getMinimumPosition() -}); -``` - - - -### getMaximumPosition ######################################################### - -_return value_: ``Number|null`` - -The maximum seekable player position. ``null`` if no content is loaded. -This is useful for live contents, where the buffer end updates continously. -This method allows thus to seek directly at the live edge of the content. + +## Deprecated ################################################################## -#### Example -```js -// seeking to the end -player.seekTo({ - position: player.getMaximumPosition() -}); -``` +The following methods are deprecated. They are still supported but we advise +users to not use those as they might become not supported in the future. ### getImageTrackData ########################################################## ---- +-- :warning: This method is deprecated, it will disappear in the next major release ``v4.0.0`` (see [Deprecated APIs](./deprecated.md)). ---- +-- _return value_: ``Array.|null`` @@ -1897,12 +2299,12 @@ options](./loadVideo_options.md#prop-transport)). ### setFullscreen ############################################################## ---- +-- :warning: This method is deprecated, it will disappear in the next major release ``v4.0.0`` (see [Deprecated APIs](./deprecated.md)). ---- +-- _arguments_: ``Boolean`` @@ -1921,12 +2323,12 @@ prefer to implement your own method to include your controls in the final UI. ### exitFullscreen ############################################################# ---- +-- :warning: This method is deprecated, it will disappear in the next major release ``v4.0.0`` (see [Deprecated APIs](./deprecated.md)). ---- +-- Exit fullscreen mode. Same than ``setFullscreen(false)``. @@ -1934,12 +2336,12 @@ Exit fullscreen mode. Same than ``setFullscreen(false)``. ### isFullscreen ############################################################### ---- +-- :warning: This method is deprecated, it will disappear in the next major release ``v4.0.0`` (see [Deprecated APIs](./deprecated.md)). ---- +-- _return value_: ``Boolean`` @@ -1947,6 +2349,7 @@ Returns ``true`` if the video element is in fullscreen mode, ``false`` otherwise. #### Example + ```js if (player.isFullscreen()) { console.log("The player is in fullscreen mode"); @@ -1957,12 +2360,12 @@ if (player.isFullscreen()) { ### getNativeTextTrack ######################################################### ---- +-- :warning: This method is deprecated, it will disappear in the next major release ``v4.0.0`` (see [Deprecated APIs](./deprecated.md)). ---- +-- _return value_: ``TextTrack|null`` @@ -1979,17 +2382,21 @@ const textTrack = el.textTracks.length ? el.textTracks[0] : null; ## Tools ####################################################################### +The RxPlayer has several "tools", which are utils which can be imported without +importing the whole RxPlayer itself. + +They are all documented here. ### MediaCapabilitiesProber #################################################### ---- +-- :warning: This tool is experimental. This only means that its API can change at any new RxPlayer version (with all the details in the corresponding release note). ---- +-- An experimental tool to probe browser media capabilities: @@ -2004,13 +2411,13 @@ You can find its documentation [here](./mediaCapabilitiesProber.md). ### TextTrackRenderer ########################################################## ---- +-- :warning: This tool is experimental. This only means that its API can change at any new RxPlayer version (with all the details in the corresponding release note). ---- +-- The TextTrackRenderer allows to easily render subtitles synchronized to a video element. @@ -2024,13 +2431,13 @@ This tool is documented [here](./TextTrackRenderer.md). ### parseBifThumbnails ######################################################### ---- +-- :warning: This tool is experimental. This only means that its API can change at any new RxPlayer version (with all the details in the corresponding release note). ---- +-- The `parseBifThumbnails` function parses BIF files, which is a format created by Canal+ to declare thumbnails linked to a given content. @@ -2040,13 +2447,13 @@ This tool is documented [here](./parseBifThumbnails.md). ### createMetaplaylist ######################################################### ---- +-- :warning: This tool is experimental. This only means that its API can change at any new RxPlayer version (with all the details in the corresponding release note). ---- +-- The `createMetaplaylist` function build a metaplaylist object from given informations about contents. From 53deb9c5fb7278e3cc6d4c35ebf02e23690f52a8 Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Tue, 14 Apr 2020 17:27:20 +0200 Subject: [PATCH 25/72] api: add `signInterpreted` to the `preferredVideoTracks` --- doc/api/index.md | 36 ++++++++++++++++++++++-- doc/api/player_options.md | 42 ++++++++++++++++++++++++---- src/core/api/track_choice_manager.ts | 8 +++++- 3 files changed, 78 insertions(+), 8 deletions(-) diff --git a/doc/api/index.md b/doc/api/index.md index d25059f0c8..2ca82a5b22 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -1622,6 +1622,13 @@ on which tracks will be filtered): // A SINGLE profile from it has codec information validated by // that RegExp. } + signInterpreted: true, // {Boolean|undefined} If set to `true`, only tracks + // which are known to contains a sign language + // interpretation will be considered. + // If set to `false`, only tracks which are known + // to not contain it will be considered. + // if not set or set to `undefined` we won't filter + // based on that status. } ``` @@ -1653,8 +1660,33 @@ profiles. You can do: player.setPreferredVideoTracks([ { codec: { all: false, test: /^hvc/ } } ]); ``` -Now let's imagine you want to start without any video track enabled (e.g. to -start in an audio-only mode). To do that, you can simply do: +With that same constraint, let's no consider that the current user prefer in any +case to have a sign language interpretation on screen: +```js +player.setPreferredVideoTracks([ + // first let's consider the best case: H265 + sign language interpretation + { + codec: { all: false, test: /^hvc/ } + signInterpreted: true, + }, + + // If not available, we still prefer a sign interpreted track without H265 + { signInterpreted: true }, + + // If not available either, we would prefer an H265 content + { codec: { all: false, test: /^hvc/ } }, + + // Note: If this is also available, we will here still have a video track + // but which do not respect any of the constraints set here. +]); +would thus prefer the video to contain a sign language interpretation. +We could set both the previous and that new constraint that way: + +--- + +For a totally different example, let's imagine you want to start without any +video track enabled (e.g. to start in an audio-only mode). To do that, you can +simply do: ```js player.setPreferredVideoTracks([null]); ``` diff --git a/doc/api/player_options.md b/doc/api/player_options.md index 29caa63e72..2790d21e3e 100644 --- a/doc/api/player_options.md +++ b/doc/api/player_options.md @@ -435,6 +435,13 @@ on which tracks will be filtered): // A SINGLE profile from it has codec information validated by // that RegExp. } + signInterpreted: true, // {Boolean|undefined} If set to `true`, only tracks + // which are known to contains a sign language + // interpretation will be considered. + // If set to `false`, only tracks which are known + // to not contain it will be considered. + // if not set or set to `undefined` we won't filter + // based on that status. } ``` @@ -445,17 +452,42 @@ This array of preferrences can be updated at any time through the #### Examples Let's imagine that you prefer to have a track which contains at least one H265 -profile. - -You can do: +profile. You can do: ```js const player = new RxPlayer({ preferredVideoTracks: [ { codec: { all: false, test: /^hvc/ } } ] }); ``` -Now let's imagine you want to start without any video track enabled (e.g. to -start in an audio-only mode). To do that, you can simply do: +With that same constraint, let's no consider that the current user is deaf and +would thus prefer the video to contain a sign language interpretation. +We could set both the previous and that new constraint that way: +```js +const player = new RxPlayer({ + preferredVideoTracks: [ + // first let's consider the best case: H265 + sign language interpretation + { + codec: { all: false, test: /^hvc/ } + signInterpreted: true, + }, + + // If not available, we still prefer a sign interpreted track without H265 + { signInterpreted: true }, + + // If not available either, we would prefer an H265 content + { codec: { all: false, test: /^hvc/ } }, + + // Note: If this is also available, we will here still have a video track + // but which do not respect any of the constraints set here. + ] +}); +``` + +--- + +For a totally different example, let's imagine you want to start without any +video track enabled (e.g. to start in an audio-only mode). To do that, you can +simply do: ```js const player = new RxPlayer({ preferredVideoTracks: [null] diff --git a/src/core/api/track_choice_manager.ts b/src/core/api/track_choice_manager.ts index 1b1ce26953..ecbf63212a 100644 --- a/src/core/api/track_choice_manager.ts +++ b/src/core/api/track_choice_manager.ts @@ -50,7 +50,8 @@ export type ITextTrackPreference = null | /** Single preference for a video track Adaptation. */ export type IVideoTrackPreference = null | { codec? : { all: boolean; - test: RegExp; }; }; + test: RegExp; }; + signInterpreted? : boolean; }; /** Audio track returned by the TrackChoiceManager. */ export interface ITMAudioTrack { language : string; @@ -996,6 +997,11 @@ function findFirstOptimalVideoAdaptation( } const foundAdaptation = arrayFind(videoAdaptations, (videoAdaptation) => { + if (preferredVideoTrack.signInterpreted !== undefined && + preferredVideoTrack.signInterpreted !== videoAdaptation.isSignInterpreted) + { + return false; + } if (preferredVideoTrack.codec === undefined) { return true; } From 038577f4480757519d3c68ba550a576e7ee2f466 Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Wed, 15 Apr 2020 15:49:35 +0200 Subject: [PATCH 26/72] doc: add warnings for video tracks API in directfile mode on Safari --- doc/api/index.md | 7 +++++-- doc/api/player_events.md | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/doc/api/index.md b/doc/api/index.md index 7597748907..2d66caf9c6 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -1338,8 +1338,11 @@ Might enter in `RELOADING` state for a short period, if not in _DirectFile_ - No video track API was supported on the current browser - The media file tracks are not supported on the browser -:warning: On Safari browser in _DirectFile_, video playback may continue on same -track even if video tracks are disabled. +:warning: On Safari, and only for contents played in the _DirectFile_ mode (see +[loadVideo options](./loadVideo_options.md#prop-transport)), a video track may +have been disabled but still displaying on screen. +Based on our understanding of the standard, we consider this to be a Safari bug. + --- diff --git a/doc/api/player_events.md b/doc/api/player_events.md index 6cabf1252c..1ca0cbb321 100644 --- a/doc/api/player_events.md +++ b/doc/api/player_events.md @@ -327,8 +327,11 @@ properties: - ``frameRate`` (``string|undefined``): The video framerate. A null payload means that video track has been disabled. -/!\ On Safari in _DirectFile_, a video track may have been disabled by user but -still playing on screen. + +:warning: On Safari, and only for contents played in the _DirectFile_ mode (see +[loadVideo options](./loadVideo_options.md#prop-transport)), a video track may +have been disabled but still displaying on screen. +Based on our understanding of the standard, we consider this to be a Safari bug. ### availableAudioBitratesChange ############################################### From 41839b7d535e5a0da2f98bb983a18a0172e550a2 Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Thu, 2 Apr 2020 18:50:49 +0200 Subject: [PATCH 27/72] transports: always request DASH and Smooth Manifest as texts to have a better knowledge of their parsing time vs loading time --- src/transports/dash/pipelines.ts | 2 +- src/transports/smooth/pipelines.ts | 2 +- .../{document_manifest_loader.ts => text_manifest_loader.ts} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/transports/utils/{document_manifest_loader.ts => text_manifest_loader.ts} (97%) diff --git a/src/transports/dash/pipelines.ts b/src/transports/dash/pipelines.ts index 110f07e6a7..8a6e1a31cc 100644 --- a/src/transports/dash/pipelines.ts +++ b/src/transports/dash/pipelines.ts @@ -18,7 +18,7 @@ import { ITransportOptions, ITransportPipelines, } from "../types"; -import generateManifestLoader from "../utils/document_manifest_loader"; +import generateManifestLoader from "../utils/text_manifest_loader"; import { imageLoader, imageParser, diff --git a/src/transports/smooth/pipelines.ts b/src/transports/smooth/pipelines.ts index a4b5cf3dbe..051909183c 100644 --- a/src/transports/smooth/pipelines.ts +++ b/src/transports/smooth/pipelines.ts @@ -57,7 +57,7 @@ import { ITransportPipelines, } from "../types"; import checkISOBMFFIntegrity from "../utils/check_isobmff_integrity"; -import generateManifestLoader from "../utils/document_manifest_loader"; +import generateManifestLoader from "../utils/text_manifest_loader"; import extractTimingsInfos from "./extract_timings_infos"; import { patchSegment } from "./isobmff"; import generateSegmentLoader from "./segment_loader"; diff --git a/src/transports/utils/document_manifest_loader.ts b/src/transports/utils/text_manifest_loader.ts similarity index 97% rename from src/transports/utils/document_manifest_loader.ts rename to src/transports/utils/text_manifest_loader.ts index 3f6f355007..81dbdf0818 100644 --- a/src/transports/utils/document_manifest_loader.ts +++ b/src/transports/utils/text_manifest_loader.ts @@ -34,7 +34,7 @@ function regularManifestLoader( if (url === undefined) { throw new Error("Cannot perform HTTP(s) request. URL not known"); } - return request({ url, responseType: "document" }); + return request({ url, responseType: "text" }); } /** From 4c1f31ce51a6354d7ebbe13e0547bc1f4b6049b5 Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Mon, 20 Apr 2020 10:00:21 +0200 Subject: [PATCH 28/72] doc: update terms and definitions --- doc/terms.md | 119 ++++++++++++++++++++++++++++----------------------- 1 file changed, 66 insertions(+), 53 deletions(-) diff --git a/doc/terms.md b/doc/terms.md index b742f2055b..8a85a6c82e 100644 --- a/doc/terms.md +++ b/doc/terms.md @@ -36,8 +36,10 @@ obvious right along. ### Adaptation ################################################################## -An Adaptation is an element of a [Period](#period) (and by extension of the -[Manifest](#manifest)) which represents a single type of media. +Simply put, what we call an "Adaptation" is just an audio, video or text track. + +More technically, it is an element of a [Period](#period) (and by extension of +the [Manifest](#manifest)) which represents a single type of media. An adaptation can be for example any of those things: - A video track @@ -63,37 +65,43 @@ You can find more infos on it [here](./dash_rxplayer_adaptation_difference.md). In the RxPlayer, a bitrate of a [Representation](#representation) indicates the number of bits per second of content described by that Representation. -For example, a video [Adaptation](#adaptation) could have two Representation: - 1. one with a bitrate: ``1,000,000`` - 2. the other with the bitrate: ``500,000`` +For example, let's imagine a video [Adaptation](#adaptation) with two +Representation: + 1. one with a bitrate at `1,000,000` (which is 1 Megabit) + 2. the other with a bitrate at `500,000` (which is 500 kilobits) + +Each seconds of content described by the first Representation will be +represented by 1 megabit of data -The first representation here will be considered to encode each second of -content by a million bits (or 1Mb). +Each seconds for the second Representation will be represented by 500 kilobits. -The second one will represent the same content for the same time and duration -for hald the bits (500kb). +Both will represent the same data, but the first one will need that the RxPlayer +fetch more data to show the same amount of content. -The second one is thus more attracting for situations where the current network -conditions are too poor to play the first one smoothly. The catch is that most -often a Representation with a lower bitrate will describe a content of a lower -quality. +In most cases, a higher bitrate means a higher quality. That's why the RxPlayer +has to compromise between having the best quality and choosing a Representation +having a low-enough bitrate to be able to play on the user's computer without +needing to pause due to poor network conditions. ### Buffer ###################################################################### -RxPlayer's Buffer can describe two things, depending on the context: +When we talk about the "buffer" in the RxPlayer, it most likely mean one of two +things, depending on the context: + + - The media data currently loaded by the browser, ready to be decoded. + + In the API, that's what we mean when we talk about the buffer. - modules in the RxPlayer downloading [media segments](#segment) and doing what is needed to play them. - - The part in the browser storing the segments downloaded, waiting for it to - be decoded. - + Here you will mostly encounter that term in the architecture documentation. -### Buffer Type ################################################################# +### Buffer type ################################################################# RxPlayer's buffer types describe a single "type" of media. @@ -101,10 +109,9 @@ Example of such types are: - "video": which represents only the video content - "audio": the audio content without the video - "text": the subtitles, for example - - "image": the thumbnail tracks. -Those are called buffer types here (or simply "types") as each type will have a -single, uncorellated [Buffer](#buffer) in the RxPlayer. +Those are called buffer types here (or simply "types") as each type will have +its own [Buffer](#buffer) (in both sense of the term) in the RxPlayer. @@ -115,26 +122,26 @@ Segment](#segment) or the Media segment itself. -### Initialization Segment ###################################################### +### Initialization segment ###################################################### -An initialization Segment is a [Media Segment](#segment), which includes -metadata necessary to play the other segment of the same -[Representation](#representation). +An initialization segment is a specific type of [media segment](#segment), which +includes metadata necessary to initialize the browser's internal decoder. -This is used by multiple Streaming Technologies to avoid inserting the same -data in multiple Media Segments. +Those are sometimes needed before we can actually begin to push any "real" media +segment from the corresponding [Representation](#representation). -As such initialization Segments are the first segment downloaded, to indicate -the metadata of the following content. +As such, when one is needed, the initialization segment is the first segment +downloaded for a given Representation. ### Manifest ################################################################### -Document which describes the content you want to play. +The Manifest is the generic name for the document which describes the content +you want to play. This is equivalent to the DASH's _Media Presentation Description_ (or _MPD_), -the Microsoft Smooth Streaming's Manifest and the HLS' playlist. +the Microsoft Smooth Streaming's _Manifest_ and the HLS' _Master Playlist_. Such document can describe for example: - multiple qualities for the same video or audio tracks @@ -147,7 +154,7 @@ file. -### Media Segment ############################################################### +### Media segment ############################################################### A media segment (or simply segment), is a small chunk of media data. @@ -161,43 +168,48 @@ features: - adaptive streaming When you play a content with the RxPlayer, it will most of the time download -media segments of different types (audio, video, text...) rather than the whole -content a single time. +media segments of different types (audio, video, text...) progressively rather +than the whole content at a single time. ### Period ##################################################################### -A Period is an element of the [Manifest](#manifest) which describes the media -to play at a given point in time. +Simply put, a Period defines what the content will be from a starting time to +an ending time. It is an element contained in the [Manifest](#manifest)) and it +will contain the [Adaptations](#adaptation) available for the corresponding +time period. Depending on the transport used, they correspond to different concepts: - - for DASH contents, it is linked to an MPD's Period element + - for DASH contents, it is more or less the same thing than an MPD's + `` element - for "local" contents, it corresponds to a single object from the `periods` array. - for "MetaPlaylist" contents, it corresponds to all the Period elements we - retrieved bwhen parsing the corresponding [Manifest](#manifest) from the + retrieved after parsing the corresponding [Manifest](#manifest) from the elements of the `contents` array. - any other transport will have a single Period, describing the whole content. -Despite having a different source depending on the transport used, a Period is -a single concept in the RxPlayer. - -Simply put, it allows to set various types of content successively in the same -manifest. +-- -For example, let's take a manifest describing a live content with +As an example, let's take a manifest describing a live content with chronologically: 1. an english TV Show 2. an old italian film with subtitles 3. an American blockbuster with closed captions. -Those contents are drastically different (they have different languages, the -american blockbuster might have more available bitrates than the old italian -one). +Let's say that those sub-contents are drastically different: + - they are all in different languages + - the american blockbuster has more available video bitrates than the old + italian one + +Because the available tracks and available qualities are different from +sub-content to sub-content, we cannot just give a single list of Adaptations +valid for all of them. They have to be in some way separated in the Manifest +object. -As such, they have to be considered separately in the Manifest. -This can be done by putting each in a different Period: +That's a case where Periods will be used. +Here is a visual representation of how the Periods would be divided here: ``` Period 1 Period 2 Period 3 08h05 09h00 10h30 now @@ -205,8 +217,8 @@ This can be done by putting each in a different Period: TV Show Italian Film American Blockbuster ``` -Each of these Periods will be linked to different audio, video and text tracks, -themselves linked to different qualities. +Each of these Periods will be linked to different audio, video and text +Adaptations, themselves linked to different Representations. @@ -216,7 +228,7 @@ A Representation is an element of an [Adaptation](#adaptation), and by extension of the [Manifest](#manifest)) that describes an interchangeable way to represent the parent Adaptation. -For example, a video Adaptation can have several representation, each having +For example, a video Adaptation can have several Representations, each having its own bitrate, its own width or its own height. The idea behind a Representation is that it can be changed by any other one in the same Adaptation as the content plays. @@ -229,4 +241,5 @@ A Representation has its equivalent in multiple Streaming technologies. It is roughly the same as: - DASH's _Representation_ - Microsoft Smooth Streaming's _QualityIndex_ - - HLS' _variant_ + - HLS' _variant_ (the notion of variant is actually a little more complex, + so here it's not an exact comparison) From a2d5cdbee0b30a3de8d6d392e48db893cead56ac Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Tue, 14 Apr 2020 18:41:05 +0200 Subject: [PATCH 29/72] api: also handle preferredVideoTracks in directfile mode --- doc/api/player_options.md | 18 ++++--- ...media_element_track_choice_manager.test.ts | 6 +++ .../api/media_element_track_choice_manager.ts | 48 ++++++++++++++++--- src/core/api/public_api.ts | 3 +- 4 files changed, 62 insertions(+), 13 deletions(-) diff --git a/doc/api/player_options.md b/doc/api/player_options.md index 2790d21e3e..3927440d45 100644 --- a/doc/api/player_options.md +++ b/doc/api/player_options.md @@ -334,8 +334,10 @@ const player = new RxPlayer({ --- -:warning: This option will have no effect for contents loaded in _DirectFile_ -mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). +:warning: This option will have no effect in _DirectFile_ mode +(see [loadVideo options](./loadVideo_options.md#prop-transport)) when either : +- No audio track API is supported on the current browser +- The media file tracks are not supported on the browser --- @@ -389,8 +391,10 @@ const player = new RxPlayer({ --- -:warning: This option will have no effect for contents loaded in _DirectFile_ -mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). +:warning: This option will have no effect in _DirectFile_ mode +(see [loadVideo options](./loadVideo_options.md#prop-transport)) when either : +- No text track API is supported on the current browser +- The media file tracks are not supported on the browser --- @@ -496,8 +500,10 @@ const player = new RxPlayer({ --- -:warning: This option will have no effect for contents loaded in _DirectFile_ -mode (see [loadVideo options](./loadVideo_options.md#prop-transport)). +:warning: This option will have no effect in _DirectFile_ mode +(see [loadVideo options](./loadVideo_options.md#prop-transport)) when either : +- No video track API is supported on the current browser +- The media file tracks are not supported on the browser --- diff --git a/src/core/api/__tests__/media_element_track_choice_manager.test.ts b/src/core/api/__tests__/media_element_track_choice_manager.test.ts index 02f65fd3be..44c3b9afcf 100644 --- a/src/core/api/__tests__/media_element_track_choice_manager.test.ts +++ b/src/core/api/__tests__/media_element_track_choice_manager.test.ts @@ -41,6 +41,7 @@ describe("API - MediaElementTrackChoiceManager", () => { { preferredAudioTracks: new BehaviorSubject([] as any[]), preferredTextTracks: new BehaviorSubject([] as any[]), + preferredVideoTracks: new BehaviorSubject([] as any[]), }, fakeMediaElement as any ); @@ -79,6 +80,7 @@ describe("API - MediaElementTrackChoiceManager", () => { { preferredAudioTracks: new BehaviorSubject([] as any[]), preferredTextTracks: new BehaviorSubject([] as any[]), + preferredVideoTracks: new BehaviorSubject([] as any[]), }, fakeMediaElement as any ); @@ -108,6 +110,7 @@ describe("API - MediaElementTrackChoiceManager", () => { { preferredAudioTracks: new BehaviorSubject([] as any[]), preferredTextTracks: new BehaviorSubject([] as any[]), + preferredVideoTracks: new BehaviorSubject([] as any[]), }, fakeMediaElement as any ); @@ -134,6 +137,7 @@ describe("API - MediaElementTrackChoiceManager", () => { { preferredAudioTracks: new BehaviorSubject([] as any[]), preferredTextTracks: new BehaviorSubject([] as any[]), + preferredVideoTracks: new BehaviorSubject([] as any[]), }, fakeMediaElement as any ); @@ -157,6 +161,7 @@ describe("API - MediaElementTrackChoiceManager", () => { { preferredAudioTracks: new BehaviorSubject([] as any[]), preferredTextTracks: new BehaviorSubject([] as any[]), + preferredVideoTracks: new BehaviorSubject([] as any[]), }, fakeMediaElement as any ); @@ -183,6 +188,7 @@ describe("API - MediaElementTrackChoiceManager", () => { { preferredAudioTracks: new BehaviorSubject([] as any[]), preferredTextTracks: new BehaviorSubject([] as any[]), + preferredVideoTracks: new BehaviorSubject([] as any[]), }, fakeMediaElement as any ); diff --git a/src/core/api/media_element_track_choice_manager.ts b/src/core/api/media_element_track_choice_manager.ts index c4244b962a..dac738e5b1 100644 --- a/src/core/api/media_element_track_choice_manager.ts +++ b/src/core/api/media_element_track_choice_manager.ts @@ -32,6 +32,7 @@ import { ITMTextTrackListItem, ITMVideoTrack, ITMVideoTrackListItem, + IVideoTrackPreference, } from "./track_choice_manager"; /** Events emitted by the MediaElementTrackChoiceManager. */ @@ -169,7 +170,7 @@ function createVideoTracks( export default class MediaElementTrackChoiceManager extends EventEmitter { /** - * Array of preferred languages for audio tracks. + * Array of preferred settings for audio tracks. * Sorted by order of preference descending. */ private _preferredAudioTracks : BehaviorSubject; @@ -180,6 +181,12 @@ export default class MediaElementTrackChoiceManager */ private _preferredTextTracks : BehaviorSubject; + /** + * Array of preferred settings for video tracks. + * Sorted by order of preference descending. + */ + private _preferredVideoTracks : BehaviorSubject; + /** List every available audio tracks available on the media element. */ private _audioTracks : Array<{ track: ITMAudioTrack; nativeTrack: AudioTrack }>; /** List every available text tracks available on the media element. */ @@ -203,14 +210,15 @@ export default class MediaElementTrackChoiceManager constructor( defaults : { preferredAudioTracks : BehaviorSubject; - preferredTextTracks : BehaviorSubject; }, + preferredTextTracks : BehaviorSubject; + preferredVideoTracks : BehaviorSubject; }, mediaElement: HTMLMediaElement ) { super(); - const { preferredAudioTracks, preferredTextTracks } = defaults; - this._preferredAudioTracks = preferredAudioTracks; - this._preferredTextTracks = preferredTextTracks; + this._preferredAudioTracks = defaults.preferredAudioTracks; + this._preferredTextTracks = defaults.preferredTextTracks; + this._preferredVideoTracks = defaults.preferredVideoTracks; // TODO In practice, the audio/video/text tracks API are not always implemented on // the media element, although Typescript HTMLMediaElement types tend to mean @@ -301,7 +309,7 @@ export default class MediaElementTrackChoiceManager * Throws if the wanted video track is not found. * @param {string|number|undefined} id */ - public setVideoTrackById(id?: string): void { + public setVideoTrackById(id?: string | number): void { for (let i = 0; i < this._videoTracks.length; i++) { const { track, nativeTrack } = this._videoTracks[i]; if (track.id === id) { @@ -532,6 +540,32 @@ export default class MediaElementTrackChoiceManager } } + /** + * Iterate over every available video tracks on the media element and over + * every set video track preferences to activate the preferred video track + * on the media element. + */ + private _setPreferredVideoTrack() : void { + // NOTE: As we cannot access either codec information or sign interpretation + // information easily about the different codecs. It is the same case than + // if we had only tracks where those were set to undefined. + // Based on that, we should disable the video track as long as one of the + // set preferrence is "no video track" (i.e. `null`) as this is the only + // constraint that we know we can respect. + // Else, just chose the first track. + const preferredVideoTracks = this._preferredVideoTracks.getValue(); + const hasNullPreference = preferredVideoTracks.some(p => p === null); + if (hasNullPreference) { + this.disableVideoTrack(); + return; + } + + if (this._videoTracks.length === 0) { + return; + } + this.setVideoTrackById(this._videoTracks[0].track.id); + } + /** * Monitor native tracks add, remove and change callback and trigger the * change events. @@ -647,6 +681,7 @@ export default class MediaElementTrackChoiceManager const newVideoTracks = createVideoTracks(this._nativeVideoTracks); if (areTrackArraysDifferent(this._videoTracks, newVideoTracks)) { this._videoTracks = newVideoTracks; + this._setPreferredVideoTrack(); this.trigger("availableVideoTracksChange", this.getAvailableVideoTracks()); const chosenVideoTrack = this._getPrivateChosenVideoTrack(); if (chosenVideoTrack?.nativeTrack !== this._lastEmittedNativeVideoTrack) { @@ -661,6 +696,7 @@ export default class MediaElementTrackChoiceManager const newVideoTracks = createVideoTracks(this._nativeVideoTracks); if (areTrackArraysDifferent(this._videoTracks, newVideoTracks)) { this._videoTracks = newVideoTracks; + this._setPreferredVideoTrack(); this.trigger("availableVideoTracksChange", this.getAvailableVideoTracks()); const chosenVideoTrack = this._getPrivateChosenVideoTrack(); if (chosenVideoTrack?.nativeTrack !== this._lastEmittedNativeVideoTrack) { diff --git a/src/core/api/public_api.ts b/src/core/api/public_api.ts index 721dacd4cd..1f648072d1 100644 --- a/src/core/api/public_api.ts +++ b/src/core/api/public_api.ts @@ -734,7 +734,8 @@ class Player extends EventEmitter { new BehaviorSubject([defaultAudioTrack]), preferredTextTracks: defaultTextTrack === undefined ? this._priv_preferredTextTracks : - new BehaviorSubject([defaultTextTrack]) }, + new BehaviorSubject([defaultTextTrack]), + preferredVideoTracks: this._priv_preferredVideoTracks }, this.videoElement ); From bb35a6a3299d7dc1fb320c699b45115f25de9154 Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Wed, 15 Apr 2020 16:37:03 +0200 Subject: [PATCH 30/72] doc: update warning relative to disabling the current video track in directfile mode --- doc/api/index.md | 26 ++++++++++++++++---------- doc/api/player_events.md | 11 ++++++----- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/doc/api/index.md b/doc/api/index.md index 2d66caf9c6..0db59d7f82 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -1329,19 +1329,25 @@ _return value_: ``void`` Deactivate the current video track, if one. -Might enter in `RELOADING` state for a short period, if not in _DirectFile_ +Might enter in `RELOADING` state for a short period after calling this API. --- -:warning: This option will have no effect in _DirectFile_ mode -(see [loadVideo options](./loadVideo_options.md#prop-transport)) when either : -- No video track API was supported on the current browser -- The media file tracks are not supported on the browser - -:warning: On Safari, and only for contents played in the _DirectFile_ mode (see -[loadVideo options](./loadVideo_options.md#prop-transport)), a video track may -have been disabled but still displaying on screen. -Based on our understanding of the standard, we consider this to be a Safari bug. +:warning: This option may have no effect in _DirectFile_ mode +(see [loadVideo options](./loadVideo_options.md#prop-transport)). +The directfile mode is a special case here because when in it, the RxPlayer +depends for track selection on the [corresponding HTML +standard](https://html.spec.whatwg.org/multipage/media.html) as implemented by +the different browsers. +Though this standard says nothing about not being able to disable the video +track (or to stay more in line with their terms: to not select any video track), +no browser implementation actually seem to be able to do it, even when the +corresponding browser APIs show that no video track is currently selected. +This might be a bug on their parts. + +Due to this fact, we do not recommend using this API in directfile mode for +now. You might even receive a reassuring `videoTrackChange` event (with a `null` +payload) while the video track is still actually active. --- diff --git a/doc/api/player_events.md b/doc/api/player_events.md index 1ca0cbb321..dd08322e4d 100644 --- a/doc/api/player_events.md +++ b/doc/api/player_events.md @@ -326,12 +326,13 @@ properties: - ``frameRate`` (``string|undefined``): The video framerate. -A null payload means that video track has been disabled. +A `null` payload means that video track has been disabled. -:warning: On Safari, and only for contents played in the _DirectFile_ mode (see -[loadVideo options](./loadVideo_options.md#prop-transport)), a video track may -have been disabled but still displaying on screen. -Based on our understanding of the standard, we consider this to be a Safari bug. +:warning: In _DirectFile_ mode, a `null` payload may be received even if the +video track is still visually active. +This seems due to difficult-to-detect browser bugs. We recommend not disabling +the video track when in directfile mode to avoid that case (this is documented +in the corresponding APIs). ### availableAudioBitratesChange ############################################### From 86b8406edcb83d31760a7de60a21b68cc2ea70a4 Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Thu, 2 Apr 2020 18:54:02 +0200 Subject: [PATCH 31/72] init: postpone next Manifest refresh when less than 3 seconds if the parsing takes too much time --- src/core/init/manifest_update_scheduler.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/core/init/manifest_update_scheduler.ts b/src/core/init/manifest_update_scheduler.ts index 9ca36aed7e..d5926cf836 100644 --- a/src/core/init/manifest_update_scheduler.ts +++ b/src/core/init/manifest_update_scheduler.ts @@ -117,11 +117,17 @@ export default function manifestUpdateScheduler({ if (manifest.lifetime === undefined || manifest.lifetime < 0) { autoRefresh$ = EMPTY; } else { - const { parsingTime, updatingTime } = manifestInfos; let autoRefreshInterval = manifest.lifetime * 1000 - timeSinceRequest; - if (parsingTime + (updatingTime ?? 0) >= (manifest.lifetime * 1000) / 4) { - const newInterval = Math.max(autoRefreshInterval, 0) - + parsingTime + (updatingTime ?? 0); + if (manifest.lifetime < 3 && totalUpdateTime >= 100) { + const defaultDelay = (3 - manifest.lifetime) * 1000 + autoRefreshInterval; + const newInterval = Math.max(defaultDelay, + Math.max(autoRefreshInterval, 0) + totalUpdateTime); + log.info("MUS: Manifest update rythm is too frequent. Postponing next request.", + autoRefreshInterval, + newInterval); + autoRefreshInterval = newInterval; + } else if (totalUpdateTime >= (manifest.lifetime * 1000) / 10) { + const newInterval = Math.max(autoRefreshInterval, 0) + totalUpdateTime; log.info("MUS: Manifest took too long to parse. Postponing next request", autoRefreshInterval, newInterval); From c1f0437fccf2b008d1904a41b34a97f0caedfde2 Mon Sep 17 00:00:00 2001 From: peaBerberian Date: Mon, 20 Apr 2020 10:01:07 +0200 Subject: [PATCH 32/72] doc: simplify player_options.md --- doc/api/player_options.md | 144 ++++++++++++++++++-------------------- 1 file changed, 70 insertions(+), 74 deletions(-) diff --git a/doc/api/player_options.md b/doc/api/player_options.md index 3927440d45..ccc6dfa573 100644 --- a/doc/api/player_options.md +++ b/doc/api/player_options.md @@ -25,11 +25,11 @@ ## Overview #################################################################### -Player options are options given to the player on instantiation. It's an object -with multiple properties. +Player options are options given to the player on instantiation. -None of them are mandatory. For most usecase though, you might want to set at -least the associated video element via the ``videoElement`` property. +It's an object with multiple properties. None of them are mandatory. +For most usecase though, you might want to set at least the associated video +element via the ``videoElement`` property. @@ -41,7 +41,9 @@ least the associated video element via the ``videoElement`` property. _type_: ``HTMLMediaElement|undefined`` -The video element the player will use. +The media element the player will use. + +Note that this can be a `