diff --git a/src/main_thread/api/public_api.ts b/src/main_thread/api/public_api.ts index 28ae0679ae..fda1ee5126 100644 --- a/src/main_thread/api/public_api.ts +++ b/src/main_thread/api/public_api.ts @@ -757,21 +757,41 @@ class Player extends EventEmitter { * Returns either an array decribing the various thumbnail tracks that can be * encountered at the given time, or `null` if no thumbnail track is available * at that time. - * @param {number} time - The position to check for thumbnail tracks, in - * seconds. + * @param {Object} arg + * @param {number|undefined} arg.time - The position to check for thumbnail + * tracks, in seconds. + * @param {string|undefined} arg.periodId * @returns {Array.|null} */ public getAvailableThumbnailTracks({ time, + periodId, }: { - time: number; + time: number | undefined; + periodId: string | undefined; }): IThumbnailTrackInfo[] | null { if (this._priv_contentInfos === null || this._priv_contentInfos.manifest === null) { return null; } - const period = getPeriodForTime(this._priv_contentInfos.manifest, time); - if (period === undefined || period.thumbnailTracks.length === 0) { - return null; + const { manifest } = this._priv_contentInfos; + let period; + if (time !== undefined) { + period = getPeriodForTime(this._priv_contentInfos.manifest, time); + if (period === undefined || period.thumbnailTracks.length === 0) { + return null; + } + } else if (periodId !== undefined) { + period = arrayFind(manifest.periods, (p) => p.id === periodId); + if (period === undefined) { + log.error("API: getAvailableThumbnailTracks: periodId not found"); + return null; + } + } else { + const { currentPeriod } = this._priv_contentInfos; + if (currentPeriod === null) { + return null; + } + period = currentPeriod; } return period.thumbnailTracks.map((t) => { return { @@ -779,6 +799,9 @@ class Player extends EventEmitter { width: Math.floor(t.width / t.horizontalTiles), height: Math.floor(t.height / t.verticalTiles), mimeType: t.mimeType, + start: t.start, + end: t.end, + thumbnailDuration: t.thumbnailDuration, }; }); } diff --git a/src/manifest/classes/__tests__/adaptation.test.ts b/src/manifest/classes/__tests__/adaptation.test.ts index 57c6feff37..66916b8233 100644 --- a/src/manifest/classes/__tests__/adaptation.test.ts +++ b/src/manifest/classes/__tests__/adaptation.test.ts @@ -51,6 +51,9 @@ const minimalRepresentationIndex: IRepresentationIndex = { addPredictedSegments() { /* noop */ }, + getTargetSegmentDuration() { + return undefined; + }, _replace() { /* noop */ }, diff --git a/src/manifest/classes/__tests__/representation.test.ts b/src/manifest/classes/__tests__/representation.test.ts index 0075702af1..9c4ff5e683 100644 --- a/src/manifest/classes/__tests__/representation.test.ts +++ b/src/manifest/classes/__tests__/representation.test.ts @@ -47,6 +47,9 @@ const minimalIndex: IRepresentationIndex = { canBeOutOfSyncError(): true { return true; }, + getTargetSegmentDuration() { + return undefined; + }, _replace() { return; }, diff --git a/src/manifest/classes/__tests__/update_period_in_place.test.ts b/src/manifest/classes/__tests__/update_period_in_place.test.ts index 3ddb4da617..65021d9e5e 100644 --- a/src/manifest/classes/__tests__/update_period_in_place.test.ts +++ b/src/manifest/classes/__tests__/update_period_in_place.test.ts @@ -174,6 +174,9 @@ function generateFakeThumbnailTrack({ id }: { id: string }) { width: 200, horizontalTiles: 5, verticalTiles: 3, + start: 0, + end: 100, + thumbnailDuration: 2, index: { _update() { /* noop */ @@ -1433,6 +1436,9 @@ describe("Manifest - updatePeriodInPlace", () => { id: "thumb-2", mimeType: "image/png", verticalTiles: 3, + start: 0, + end: 100, + thumbnailDuration: 2, width: 200, }, ], @@ -1443,6 +1449,9 @@ describe("Manifest - updatePeriodInPlace", () => { id: "thumb-1", mimeType: "image/png", verticalTiles: 3, + start: 0, + end: 100, + thumbnailDuration: 2, width: 200, }, ], @@ -1510,6 +1519,9 @@ describe("Manifest - updatePeriodInPlace", () => { id: "thumb-2", mimeType: "image/png", verticalTiles: 3, + start: 0, + end: 100, + thumbnailDuration: 2, width: 200, }, ], @@ -1520,6 +1532,9 @@ describe("Manifest - updatePeriodInPlace", () => { id: "thumb-1", mimeType: "image/png", verticalTiles: 3, + start: 0, + end: 100, + thumbnailDuration: 2, width: 200, }, ], @@ -1588,6 +1603,9 @@ describe("Manifest - updatePeriodInPlace", () => { id: "thumb-1", mimeType: "image/png", verticalTiles: 3, + start: 0, + end: 100, + thumbnailDuration: 2, width: 200, }, ], @@ -1660,6 +1678,9 @@ describe("Manifest - updatePeriodInPlace", () => { id: "thumb-1", mimeType: "image/png", verticalTiles: 3, + start: 0, + end: 100, + thumbnailDuration: 2, width: 200, }, ], diff --git a/src/manifest/classes/period.ts b/src/manifest/classes/period.ts index 05a35b34c4..45da7a6437 100644 --- a/src/manifest/classes/period.ts +++ b/src/manifest/classes/period.ts @@ -145,6 +145,9 @@ export default class Period implements IPeriodMetadata { width: thumbnailTrack.width, horizontalTiles: thumbnailTrack.horizontalTiles, verticalTiles: thumbnailTrack.verticalTiles, + start: thumbnailTrack.start, + end: thumbnailTrack.end, + thumbnailDuration: thumbnailTrack.thumbnailDuration, })); this.duration = args.duration; this.start = args.start; @@ -305,6 +308,9 @@ export default class Period implements IPeriodMetadata { width: thumbnailTrack.width, horizontalTiles: thumbnailTrack.horizontalTiles, verticalTiles: thumbnailTrack.verticalTiles, + start: thumbnailTrack.start, + end: thumbnailTrack.end, + thumbnailDuration: thumbnailTrack.thumbnailDuration, })), }; } @@ -344,4 +350,28 @@ export interface IThumbnailTrack { * images contained vertically in a whole loaded thumbnail resource. */ verticalTiles: number; + /** + * Starting `position` the first thumbnail of this thumbnail track applies to, + * if known. + */ + start: number | undefined; + /** + * Ending `position` the last thumbnail of this thumbnail track applies to, + * if known. + */ + end: number | undefined; + /** + * If set, all those thumbnail tracks' thumbnails are linked to the given + * duration of content in seconds, going from `start`, until `end`. + * + * E.g. with a `start` set to `10`, an `end` set to `17`, and a + * `thumbnailDuration` set to `2`, there should be 4 thumbnails: + * 1. Applying to 10-12 seconds + * 2. Applying to 12-14 seconds + * 3. Applying to 14-16 seconds + * 4. Applying to 16-17 seconds + * + * Set to `undefined` if a duration cannot be determined. + */ + thumbnailDuration: number | undefined; } diff --git a/src/manifest/classes/representation_index/static.ts b/src/manifest/classes/representation_index/static.ts index 4eeffc461a..5aa8f9ec08 100644 --- a/src/manifest/classes/representation_index/static.ts +++ b/src/manifest/classes/representation_index/static.ts @@ -155,6 +155,24 @@ export default class StaticRepresentationIndex implements IRepresentationIndex { log.error("A `StaticRepresentationIndex` does not need to be initialized"); } + /** + * Returns the `duration` of each segment in the context of its Manifest (i.e. + * as the Manifest anounces them, actual segment duration may be different due + * to approximations), in seconds. + * + * NOTE: we could here do a median or a mean but I chose to be lazy (and + * more performant) by returning the duration of the first element instead. + * As `isPrecize` is `false`, the rest of the code should be notified that + * this is only an approximation. + * @returns {number} + */ + getTargetSegmentDuration(): { duration: number; isPrecize: boolean } | undefined { + return { + duration: Number.MAX_VALUE, + isPrecize: false, + }; + } + addPredictedSegments(): void { log.warn("Cannot add predicted segments to a `StaticRepresentationIndex`"); } diff --git a/src/manifest/classes/representation_index/types.ts b/src/manifest/classes/representation_index/types.ts index e8497bc216..3876a8d475 100644 --- a/src/manifest/classes/representation_index/types.ts +++ b/src/manifest/classes/representation_index/types.ts @@ -429,6 +429,29 @@ export interface IRepresentationIndex { */ initialize(segmentList: ISegmentInformation[]): void; + /** + * Returns an approximate for the duration of that `RepresentationIndex`s + * segments, in seconds in the context of its Manifest (i.e. as the Manifest + * anounces them, actual segment duration may be different due to + * approximations), with the exception of the last one (that usually is + * shorter). + * @returns {number} + */ + getTargetSegmentDuration(): + | { + /** Approximate duration of any segments but the last one in seconds. */ + duration: number; + /** + * If `true`, the given duration should be relatively precize for all + * segments but the last one. + * + * If `false`, `duration` indicates only a general idea of what can be + * expected. + */ + isPrecize: boolean; + } + | undefined; + /** * Add segments to a RepresentationIndex that were predicted after parsing the * segment linked to `currentSegment`. diff --git a/src/manifest/classes/update_period_in_place.ts b/src/manifest/classes/update_period_in_place.ts index ebbfa4b127..eca8423645 100644 --- a/src/manifest/classes/update_period_in_place.ts +++ b/src/manifest/classes/update_period_in_place.ts @@ -75,6 +75,9 @@ export default function updatePeriodInPlace( oldThumbnailTrack.width = newThumbnailTrack.width; oldThumbnailTrack.horizontalTiles = newThumbnailTrack.horizontalTiles; oldThumbnailTrack.verticalTiles = newThumbnailTrack.verticalTiles; + oldThumbnailTrack.start = newThumbnailTrack.start; + oldThumbnailTrack.end = newThumbnailTrack.end; + oldThumbnailTrack.thumbnailDuration = newThumbnailTrack.thumbnailDuration; oldThumbnailTrack.cdnMetadata = newThumbnailTrack.cdnMetadata; if (updateType === MANIFEST_UPDATE_TYPE.Full) { oldThumbnailTrack.index._replace(newThumbnailTrack.index); @@ -88,6 +91,9 @@ export default function updatePeriodInPlace( width: oldThumbnailTrack.width, horizontalTiles: oldThumbnailTrack.horizontalTiles, verticalTiles: oldThumbnailTrack.verticalTiles, + start: oldThumbnailTrack.start, + end: oldThumbnailTrack.end, + thumbnailDuration: oldThumbnailTrack.thumbnailDuration, }); } } @@ -105,6 +111,9 @@ export default function updatePeriodInPlace( width: t.width, horizontalTiles: t.horizontalTiles, verticalTiles: t.verticalTiles, + start: t.start, + end: t.end, + thumbnailDuration: t.thumbnailDuration, })), ); oldPeriod.thumbnailTracks.push(...newThumbnailTracks); diff --git a/src/manifest/types.ts b/src/manifest/types.ts index 94497de68f..ffd3c926ca 100644 --- a/src/manifest/types.ts +++ b/src/manifest/types.ts @@ -286,6 +286,30 @@ export interface IThumbnailTrackMetadata { * images contained vertically in a whole loaded thumbnail resource. */ verticalTiles: number; + /** + * Starting `position` the first thumbnail of this thumbnail track applies to, + * if known. + */ + start: number | undefined; + /** + * Ending `position` the last thumbnail of this thumbnail track applies to, + * if known. + */ + end: number | undefined; + /** + * If set, all those thumbnail tracks' thumbnails are linked to the given + * duration of content in seconds, going from `start`, until `end`. + * + * E.g. with a `start` set to `10`, an `end` set to `17`, and a + * `thumbnailDuration` set to `2`, there should be 4 thumbnails: + * 1. Applying to 10-12 seconds + * 2. Applying to 12-14 seconds + * 3. Applying to 14-16 seconds + * 4. Applying to 16-17 seconds + * + * Set to `undefined` if a duration cannot be determined. + */ + thumbnailDuration: number | undefined; } export interface ILoadedThumbnailData { diff --git a/src/parsers/manifest/dash/common/indexes/base.ts b/src/parsers/manifest/dash/common/indexes/base.ts index 4fc95adfd2..f2f91b8be1 100644 --- a/src/parsers/manifest/dash/common/indexes/base.ts +++ b/src/parsers/manifest/dash/common/indexes/base.ts @@ -433,6 +433,29 @@ export default class BaseRepresentationIndex implements IRepresentationIndex { log.warn("Cannot add predicted segments to a `BaseRepresentationIndex`"); } + /** + * Returns the `duration` of each segment in the context of its Manifest (i.e. + * as the Manifest anounces them, actual segment duration may be different due + * to approximations), in seconds. + * + * NOTE: we could here do a median or a mean but I chose to be lazy (and + * more performant) by returning the duration of the first element instead. + * As `isPrecize` is `false`, the rest of the code should be notified that + * this is only an approximation. + * @returns {number} + */ + getTargetSegmentDuration(): { duration: number; isPrecize: boolean } | undefined { + const { timeline, timescale } = this._index; + const firstElementInTimeline = timeline[0]; + if (firstElementInTimeline === undefined) { + return undefined; + } + return { + duration: firstElementInTimeline.duration / timescale, + isPrecize: false, + }; + } + /** * Replace in-place this `BaseRepresentationIndex` information by the * information from another one. diff --git a/src/parsers/manifest/dash/common/indexes/list.ts b/src/parsers/manifest/dash/common/indexes/list.ts index 34eac96412..afa5fe9ea4 100644 --- a/src/parsers/manifest/dash/common/indexes/list.ts +++ b/src/parsers/manifest/dash/common/indexes/list.ts @@ -348,6 +348,25 @@ export default class ListRepresentationIndex implements IRepresentationIndex { log.warn("Cannot add predicted segments to a `ListRepresentationIndex`"); } + /** + * Returns the `duration` of each segment in the context of its Manifest (i.e. + * as the Manifest anounces them, actual segment duration may be different due + * to approximations), in seconds. + * + * NOTE: we could here do a median or a mean but I chose to be lazy (and + * more performant) by returning the duration of the first element instead. + * As `isPrecize` is `false`, the rest of the code should be notified that + * this is only an approximation. + * @returns {number} + */ + getTargetSegmentDuration(): { duration: number; isPrecize: boolean } | undefined { + const { duration, timescale } = this._index; + return { + duration: duration / timescale, + isPrecize: true, + }; + } + /** * @param {Object} newIndex */ diff --git a/src/parsers/manifest/dash/common/indexes/template.ts b/src/parsers/manifest/dash/common/indexes/template.ts index df3dca4586..e5f83cae40 100644 --- a/src/parsers/manifest/dash/common/indexes/template.ts +++ b/src/parsers/manifest/dash/common/indexes/template.ts @@ -507,6 +507,22 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex log.warn("Cannot add predicted segments to a `TemplateRepresentationIndex`"); } + /** + * Returns the `duration` of each segment in the context of its Manifest (i.e. + * as the Manifest anounces them, actual segment duration may be different due + * to approximations), in seconds. + * @returns {number} + */ + getTargetSegmentDuration(): { + duration: number; + isPrecize: boolean; + } { + return { + duration: this._index.duration / this._index.timescale, + isPrecize: true, + }; + } + /** * @param {Object} newIndex */ diff --git a/src/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.ts b/src/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.ts index 284ebaaefc..87724c2435 100644 --- a/src/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.ts +++ b/src/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.ts @@ -775,6 +775,33 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex log.warn("Cannot add predicted segments to a `TimelineRepresentationIndex`"); } + /** + * Returns the `duration` of each segment in the context of its Manifest (i.e. + * as the Manifest anounces them, actual segment duration may be different due + * to approximations), in seconds. + * + * NOTE: we could here do a median or a mean but I chose to be lazy (and + * more performant) by returning the duration of the first element instead. + * As `isPrecize` is `false`, the rest of the code should be notified that + * this is only an approximation. + * @returns {number} + */ + getTargetSegmentDuration(): { duration: number; isPrecize: boolean } | undefined { + this._refreshTimeline(); + const { timeline, timescale } = this._index; + if (timeline === null) { + return undefined; + } + const firstElementInTimeline = timeline[0]; + if (firstElementInTimeline === undefined) { + return undefined; + } + return { + duration: firstElementInTimeline.duration / timescale, + isPrecize: false, + }; + } + /** * Returns `true` if the given object can be used as an "index" argument to * create a new `TimelineRepresentationIndex`. diff --git a/src/parsers/manifest/dash/common/parse_adaptation_sets.ts b/src/parsers/manifest/dash/common/parse_adaptation_sets.ts index 4409fe9436..109aa3a382 100644 --- a/src/parsers/manifest/dash/common/parse_adaptation_sets.ts +++ b/src/parsers/manifest/dash/common/parse_adaptation_sets.ts @@ -574,6 +574,19 @@ function createThumbnailTracks( log.warn("DASH: Invalid thumbnails Representation, no width information"); continue; } + + const start = representation.index.getFirstAvailablePosition() ?? undefined; + const end = representation.index.getLastAvailablePosition() ?? undefined; + + let thumbnailDuration; + const targetDuration = representation.index.getTargetSegmentDuration(); + if (targetDuration !== undefined && targetDuration.isPrecize) { + thumbnailDuration = + targetDuration.duration / (tileInfo.verticalTiles * tileInfo.horizontalTiles); + } else { + log.warn("DASH: Cannot produce duration estimate for thumbnail track"); + } + tracks.push({ id: representation.id, cdnMetadata: representation.cdnMetadata, @@ -583,6 +596,9 @@ function createThumbnailTracks( width: representation.width, horizontalTiles: tileInfo.horizontalTiles, verticalTiles: tileInfo.verticalTiles, + start, + end, + thumbnailDuration, }); } } diff --git a/src/parsers/manifest/local/representation_index.ts b/src/parsers/manifest/local/representation_index.ts index 3db39bc7ff..338c8fa4a4 100644 --- a/src/parsers/manifest/local/representation_index.ts +++ b/src/parsers/manifest/local/representation_index.ts @@ -202,6 +202,27 @@ export default class LocalRepresentationIndex implements IRepresentationIndex { log.warn("Cannot add predicted segments to a `LocalRepresentationIndex`"); } + /** + * Returns the `duration` of each segment in the context of its Manifest (i.e. + * as the Manifest anounces them, actual segment duration may be different due + * to approximations), in seconds. + * + * NOTE: we could here do a median or a mean but I chose to be lazy (and + * more performant) by returning the duration of the first element instead. + * As `isPrecize` is `false`, the rest of the code should be notified that + * this is only an approximation. + * @returns {number} + */ + getTargetSegmentDuration(): { duration: number; isPrecize: boolean } | undefined { + if (this._index.segments.length === 0) { + return undefined; + } + return { + duration: this._index.segments[0].duration, + isPrecize: false, + }; + } + _replace(newIndex: LocalRepresentationIndex): void { this._index.segments = newIndex._index.segments; this._index.loadSegment = newIndex._index.loadSegment; diff --git a/src/parsers/manifest/metaplaylist/representation_index.ts b/src/parsers/manifest/metaplaylist/representation_index.ts index 6a8643c8a7..3a6bdd7a99 100644 --- a/src/parsers/manifest/metaplaylist/representation_index.ts +++ b/src/parsers/manifest/metaplaylist/representation_index.ts @@ -227,6 +227,16 @@ export default class MetaRepresentationIndex implements IRepresentationIndex { return this._wrappedIndex.addPredictedSegments(nextSegments, currentSegment); } + /** + * Returns the `duration` of each segment in the context of its Manifest (i.e. + * as the Manifest anounces them, actual segment duration may be different due + * to approximations), in seconds. + * @returns {number} + */ + getTargetSegmentDuration(): { duration: number; isPrecize: boolean } | undefined { + return this._wrappedIndex.getTargetSegmentDuration(); + } + /** * @param {Object} newIndex */ diff --git a/src/parsers/manifest/smooth/representation_index.ts b/src/parsers/manifest/smooth/representation_index.ts index 72a011e75d..7a33c5bbb7 100644 --- a/src/parsers/manifest/smooth/representation_index.ts +++ b/src/parsers/manifest/smooth/representation_index.ts @@ -475,6 +475,30 @@ export default class SmoothRepresentationIndex implements IRepresentationIndex { ); } + /** + * Returns the `duration` of each segment in the context of its Manifest (i.e. + * as the Manifest anounces them, actual segment duration may be different due + * to approximations), in seconds. + * + * NOTE: we could here do a median or a mean but I chose to be lazy (and + * more performant) by returning the duration of the first element instead. + * As `isPrecize` is `false`, the rest of the code should be notified that + * this is only an approximation. + * @returns {number} + */ + getTargetSegmentDuration(): { duration: number; isPrecize: boolean } | undefined { + this._refreshTimeline(); + const { timeline, timescale } = this._sharedSmoothTimeline; + const firstElementInTimeline = timeline[0]; + if (firstElementInTimeline === undefined) { + return undefined; + } + return { + duration: firstElementInTimeline.duration / timescale, + isPrecize: false, + }; + } + /** * Replace this RepresentationIndex by a newly downloaded one. * Check if the old index had more information about new segments and re-add diff --git a/src/parsers/manifest/types.ts b/src/parsers/manifest/types.ts index b1171a932a..61583cdcc7 100644 --- a/src/parsers/manifest/types.ts +++ b/src/parsers/manifest/types.ts @@ -147,6 +147,30 @@ export interface IParsedThumbnailTrack { * images contained vertically in a whole loaded thumbnail resource. */ verticalTiles: number; + /** + * Starting `position` the first thumbnail of this thumbnail track applies to, + * if known. + */ + start: number | undefined; + /** + * Ending `position` the last thumbnail of this thumbnail track applies to, + * if known. + */ + end: number | undefined; + /** + * If set, all those thumbnail tracks' thumbnails are linked to the given + * duration of content in seconds, going from `start`, until `end`. + * + * E.g. with a `start` set to `10`, an `end` set to `17`, and a + * `thumbnailDuration` set to `2`, there should be 4 thumbnails: + * 1. Applying to 10-12 seconds + * 2. Applying to 12-14 seconds + * 3. Applying to 14-16 seconds + * 4. Applying to 16-17 seconds + * + * Set to `undefined` if a duration cannot be determined. + */ + thumbnailDuration: number | undefined; } /** Representation of a "quality" available in an Adaptation. */ diff --git a/src/parsers/manifest/utils/__tests__/get_first_time_from_adaptations.test.ts b/src/parsers/manifest/utils/__tests__/get_first_time_from_adaptations.test.ts index d4ee312d83..96290da68d 100644 --- a/src/parsers/manifest/utils/__tests__/get_first_time_from_adaptations.test.ts +++ b/src/parsers/manifest/utils/__tests__/get_first_time_from_adaptations.test.ts @@ -48,6 +48,9 @@ function generateRepresentationIndex( addPredictedSegments(): void { return; }, + getTargetSegmentDuration() { + return undefined; + }, _replace() { /* noop */ }, diff --git a/src/parsers/manifest/utils/__tests__/get_last_time_from_adaptation.test.ts b/src/parsers/manifest/utils/__tests__/get_last_time_from_adaptation.test.ts index 2eada3f1c3..11979c12e0 100644 --- a/src/parsers/manifest/utils/__tests__/get_last_time_from_adaptation.test.ts +++ b/src/parsers/manifest/utils/__tests__/get_last_time_from_adaptation.test.ts @@ -48,6 +48,9 @@ function generateRepresentationIndex( canBeOutOfSyncError(): true { return true; }, + getTargetSegmentDuration() { + return undefined; + }, _replace() { /* noop */ }, diff --git a/src/public_types.ts b/src/public_types.ts index 2ae3c3cafa..bd0a9556f3 100644 --- a/src/public_types.ts +++ b/src/public_types.ts @@ -1304,6 +1304,30 @@ export interface IThumbnailTrackInfo { * `image/jpeg` or `image/png`. */ mimeType: string | undefined; + /** + * Starting `position` the first thumbnail of this thumbnail track applies to, + * if known. + */ + start: number | undefined; + /** + * Ending `position` the last thumbnail of this thumbnail track applies to, + * if known. + */ + end: number | undefined; + /** + * If set, all those thumbnail tracks' thumbnails are linked to the given + * duration of content in seconds, going from `start`, until `end`. + * + * E.g. with a `start` set to `10`, an `end` set to `17`, and a + * `thumbnailDuration` set to `2`, there should be 4 thumbnails: + * 1. Applying to 10-12 seconds + * 2. Applying to 12-14 seconds + * 3. Applying to 14-16 seconds + * 4. Applying to 16-17 seconds + * + * Set to `undefined` if a duration cannot be determined. + */ + thumbnailDuration: number | undefined; } /**