From 734a0cbc1b7cdcc8901140ddcd06503be4bf5fb7 Mon Sep 17 00:00:00 2001 From: Paul Berberian Date: Thu, 4 Aug 2022 11:03:06 +0200 Subject: [PATCH 1/6] Implement Content Steering for DASH contents --- src/Content_Steering.md | 90 ++++ src/core/api/public_api.ts | 16 +- .../fetchers/manifest/manifest_fetcher.ts | 18 +- src/core/fetchers/segment/segment_fetcher.ts | 22 +- .../segment/segment_fetcher_creator.ts | 6 + src/core/fetchers/steering_manifest/index.ts | 26 + .../steering_manifest_fetcher.ts | 184 +++++++ src/core/fetchers/utils/schedule_request.ts | 457 ++++++++++++++++++ .../fetchers/utils/try_urls_with_backoff.ts | 287 ----------- src/core/init/cdn_prioritizer.ts | 410 ++++++++++++++++ src/core/init/initialize_media_source.ts | 44 +- src/default_config.ts | 27 ++ .../video_thumbnail_loader.ts | 2 + src/manifest/manifest.ts | 22 +- src/manifest/representation.ts | 15 + .../__tests__/static.test.ts | 2 +- src/manifest/representation_index/static.ts | 6 +- src/manifest/representation_index/types.ts | 48 +- src/manifest/update_period_in_place.ts | 1 + .../SteeringManifest/DCSM/parse_dcsm.ts | 67 +++ src/parsers/SteeringManifest/index.ts | 18 + src/parsers/SteeringManifest/types.ts | 21 + .../manifest/dash/common/indexes/base.ts | 45 +- .../dash/common/indexes/get_init_segment.ts | 10 +- .../indexes/get_segments_from_timeline.ts | 10 +- .../manifest/dash/common/indexes/list.ts | 39 +- .../manifest/dash/common/indexes/template.ts | 50 +- .../timeline/timeline_representation_index.ts | 48 +- .../manifest/dash/common/indexes/tokens.ts | 25 +- src/parsers/manifest/dash/common/parse_mpd.ts | 22 +- .../dash/common/parse_representation_index.ts | 7 +- .../dash/common/parse_representations.ts | 9 + .../manifest/dash/common/resolve_base_urls.ts | 13 +- .../dash/js-parser/node_parsers/BaseURL.ts | 24 +- .../js-parser/node_parsers/ContentSteering.ts | 63 +++ .../dash/js-parser/node_parsers/MPD.ts | 12 +- .../__tests__/AdaptationSet.test.ts | 14 +- .../manifest/dash/node_parser_types.ts | 41 +- .../manifest/dash/wasm-parser/rs/events.rs | 11 + .../wasm-parser/rs/processor/attributes.rs | 23 +- .../dash/wasm-parser/rs/processor/mod.rs | 42 ++ .../dash/wasm-parser/ts/generators/BaseURL.ts | 15 +- .../ts/generators/ContentSteering.ts | 59 +++ .../dash/wasm-parser/ts/generators/MPD.ts | 12 + .../manifest/dash/wasm-parser/ts/types.ts | 11 + .../manifest/local/parse_local_manifest.ts | 2 + .../manifest/local/representation_index.ts | 4 +- .../metaplaylist/metaplaylist_parser.ts | 9 +- src/parsers/manifest/smooth/create_parser.ts | 31 +- .../manifest/smooth/representation_index.ts | 4 +- src/parsers/manifest/types.ts | 35 ++ .../get_first_time_from_adaptations.test.ts | 15 + .../get_last_time_from_adaptation.test.ts | 15 + src/transports/dash/construct_segment_url.ts | 28 ++ src/transports/dash/image_pipelines.ts | 7 +- src/transports/dash/pipelines.ts | 8 +- src/transports/dash/segment_loader.ts | 7 +- .../dash/steering_manifest_pipeline.ts | 61 +++ src/transports/dash/text_loader.ts | 7 +- src/transports/dash/text_parser.ts | 4 +- src/transports/local/pipelines.ts | 3 +- src/transports/local/segment_loader.ts | 5 +- src/transports/metaplaylist/pipelines.ts | 40 +- src/transports/smooth/pipelines.ts | 21 +- src/transports/smooth/utils.ts | 14 +- src/transports/types.ts | 97 +++- src/utils/__tests__/event_emitter.test.ts | 139 +++--- .../initialization_segment_cache.test.ts | 14 +- src/utils/__tests__/resolve_url.test.ts | 50 +- src/utils/event_emitter.ts | 4 +- src/utils/resolve_url.ts | 19 +- src/utils/sync_or_async.ts | 72 +++ .../DASH_dynamic_SegmentTemplate/infos.js | 4 +- .../no_time_shift_buffer_depth.js | 4 +- .../DASH_dynamic_SegmentTimeline/infos.js | 16 +- .../no_time_shift_buffer_depth.js | 12 +- .../DASH_dynamic_UTCTimings/with_direct.js | 4 +- .../with_direct_and_http.js | 4 +- .../DASH_dynamic_UTCTimings/with_http.js | 4 +- .../without_timings.js | 4 +- .../DASH_static_SegmentBase/broken_sidx.js | 4 +- .../DASH_static_SegmentBase/multi_codecs.js | 56 +-- .../different_types_infos.js | 60 +-- .../discontinuity_between_periods_infos.js | 60 +-- .../infos.js | 60 +-- .../discontinuity.js | 40 +- .../DASH_static_SegmentTimeline/infos.js | 40 +- .../not_starting_at_0.js | 40 +- .../segment_template_inheritance_as_rep.js | 40 +- .../segment_template_inheritance_period_as.js | 40 +- .../DASH_static_SegmentTimeline/trickmode.js | 40 +- .../DASH_static_broken_cenc_in_MPD/infos.js | 4 +- .../Smooth_static/custom_attributes.js | 18 +- .../Smooth_static/empty_text_track.js | 18 +- .../Smooth_static/not_starting_at_0.js | 18 +- tests/contents/Smooth_static/regular.js | 18 +- tests/integration/scenarios/dash_live.js | 74 +-- .../scenarios/dash_live_SegmentTemplate.js | 42 +- .../utils/launch_tests_for_content.js | 29 +- 99 files changed, 2808 insertions(+), 1054 deletions(-) create mode 100644 src/Content_Steering.md create mode 100644 src/core/fetchers/steering_manifest/index.ts create mode 100644 src/core/fetchers/steering_manifest/steering_manifest_fetcher.ts create mode 100644 src/core/fetchers/utils/schedule_request.ts delete mode 100644 src/core/fetchers/utils/try_urls_with_backoff.ts create mode 100644 src/core/init/cdn_prioritizer.ts create mode 100644 src/parsers/SteeringManifest/DCSM/parse_dcsm.ts create mode 100644 src/parsers/SteeringManifest/index.ts create mode 100644 src/parsers/SteeringManifest/types.ts create mode 100644 src/parsers/manifest/dash/js-parser/node_parsers/ContentSteering.ts create mode 100644 src/parsers/manifest/dash/wasm-parser/ts/generators/ContentSteering.ts create mode 100644 src/transports/dash/construct_segment_url.ts create mode 100644 src/transports/dash/steering_manifest_pipeline.ts create mode 100644 src/utils/sync_or_async.ts diff --git a/src/Content_Steering.md b/src/Content_Steering.md new file mode 100644 index 0000000000..76ffc896cd --- /dev/null +++ b/src/Content_Steering.md @@ -0,0 +1,90 @@ +# Content Steering implementation + +__LAST UPDATE: 2022-08-04__ + +## Overview + +Content steering is a mechanism allowing a content provider to deterministically +prioritize a, or multiple, CDN over others - even during content playback - on +the server-side when multiple CDNs are available to load a given content. + +For example, a distributor may want to rebalance load between multiple servers +while final users are watching the corresponding stream, though many other use +cases and reasons exist. + +As of now, content steering only exist for HLS and DASH OTT streaming +technologies. +In both cases it takes the form of a separate file, in DASH called the "DASH +Content Steering Manifest" (or DCSM), giving the current priority. +This separate file has its own syntax, semantic and refreshing logic. + + +## Architecture in the RxPlayer + + +``` + ./src/parsers/SteeringManifest + +----------------------------------+ + | Content Steering Manifest parser | Parse DCSM[1] into a + +----------------------------------+ transport-agnostic steering + ^ Manifest structure + | + | Uses when parsing + | + | + | ./src/transports + +---------------------------+ + | Transport | + | | + | new functions: | + | - loadSteeringManifest | Construct DCSM[1]'s URL, performs + | - parseSteeringManifest | requests and parses it. + +---------------------------+ + ^ + | + | Relies on + | + | + ./src/core/init | ./src/core/fetchers/steering_manifest + +---------+ +-------------------------+ + | | -----------> | SteeringManifestFetcher | Fetches and parses a Content Steering + | | Creates +-------------------------+ Manifest in a transport-agnostic way + | | ^ + handle retries and error formatting + | | | + | | | Uses an instance of to load, parse and refresh the + | | | Steering Manifest periodically according to its TTL[2] + | | | + | | | + | Init | | ./src/core/init/cdn_prioritizer.ts + | | +----------------+ Signals the priority between multiple + | | -----------> | CdnPrioritizer | potential CDNs for each resource. + | | Creates +----------------+ (This is done on demand, the `CdnPrioritizer` + | | ^ knows of no resource in advance). + | | | + | | | Asks to sort a segment's available base urls by order of + | | | priority (and to filter out those that should not be + | | | used). + | | | Also signals when it should prevent a base url from + | | | being used temporarily (e.g. due to request issues). + | | | + | | | + | | | ./src/core/fetchers/segment + | | +----------------+ + | | -----------> | SegmentFetcher | Fetches and parses a segment in a + +---------+ Creates +----------------+ transport-agnostic way + ^ + handle retries and error formatting + | + | Ask to load segment(s) + | + | ./src/core/stream/representation + +----------------+ + | Representation | Logic behind finding the right segment to + | Stream | load, loading it and pushing it to the buffer. + +----------------+ One RepresentationStream is created per + actively-loaded Period and one per + actively-loaded buffer type. + + +[1] DCSM: DASH Content Steering Manifest +[2] TTL: Time To Live: a delay after which a Content Steering Manifest should be refreshed +``` diff --git a/src/core/api/public_api.ts b/src/core/api/public_api.ts index 4497d11209..4ca8c65f99 100644 --- a/src/core/api/public_api.ts +++ b/src/core/api/public_api.ts @@ -120,7 +120,6 @@ import { IManifestFetcherParsedResult, IManifestFetcherWarningEvent, ManifestFetcher, - SegmentFetcherCreator, } from "../fetchers"; import initializeMediaSourcePlayback, { IInitEvent, @@ -790,14 +789,6 @@ class Player extends EventEmitter { maxRetryOffline: offlineRetry, requestTimeout: manifestRequestTimeout }); - /** Interface used to download segments. */ - const segmentFetcherCreator = new SegmentFetcherCreator( - transportPipelines, - { lowLatencyMode, - maxRetryOffline: offlineRetry, - maxRetryRegular: segmentRetry, - requestTimeout: segmentRequestTimeout }); - /** Observable emitting the initial Manifest */ let manifest$ : Observable; @@ -897,6 +888,10 @@ class Player extends EventEmitter { onCodecSwitch }, this._priv_bufferOptions); + const segmentRequestOptions = { regularError: segmentRetry, + requestTimeout: segmentRequestTimeout, + offlineError: offlineRetry }; + // We've every options set up. Start everything now const init$ = initializeMediaSourcePlayback({ adaptiveOptions, autoPlay, @@ -908,9 +903,10 @@ class Player extends EventEmitter { manifestFetcher, mediaElement: videoElement, minimumManifestUpdateInterval, - segmentFetcherCreator, + segmentRequestOptions, speed: this._priv_speed, startAt, + transport: transportPipelines, textTrackOptions }) .pipe(takeUntil(stoppedContent$)); diff --git a/src/core/fetchers/manifest/manifest_fetcher.ts b/src/core/fetchers/manifest/manifest_fetcher.ts index 6f7b9d682a..f0221e78b2 100644 --- a/src/core/fetchers/manifest/manifest_fetcher.ts +++ b/src/core/fetchers/manifest/manifest_fetcher.ts @@ -33,8 +33,8 @@ import TaskCanceller from "../../../utils/task_canceller"; import errorSelector from "../utils/error_selector"; import { IBackoffSettings, - tryRequestPromiseWithBackoff, -} from "../utils/try_urls_with_backoff"; + scheduleRequestPromise, +} from "../utils/schedule_request"; /** What will be sent once parsed. */ @@ -228,9 +228,7 @@ export default class ManifestFetcher { const { resolveManifestUrl } = pipelines; assert(resolveManifestUrl !== undefined); const callResolver = () => resolveManifestUrl(resolverUrl, canceller.signal); - return tryRequestPromiseWithBackoff(callResolver, - backoffSettings, - canceller.signal); + return scheduleRequestPromise(callResolver, backoffSettings, canceller.signal); } /** @@ -252,9 +250,7 @@ export default class ManifestFetcher { const callLoader = () => loadManifest(manifestUrl, { timeout: requestTimeout }, canceller.signal); - return tryRequestPromiseWithBackoff(callLoader, - backoffSettings, - canceller.signal); + return scheduleRequestPromise(callLoader, backoffSettings, canceller.signal); } }); } @@ -349,9 +345,9 @@ export default class ManifestFetcher { performRequest : () => Promise ) : Promise { try { - const data = await tryRequestPromiseWithBackoff(performRequest, - backoffSettings, - canceller.signal); + const data = await scheduleRequestPromise(performRequest, + backoffSettings, + canceller.signal); return data; } catch (err) { throw errorSelector(err); diff --git a/src/core/fetchers/segment/segment_fetcher.ts b/src/core/fetchers/segment/segment_fetcher.ts index 003bbd0216..a1bf12feed 100644 --- a/src/core/fetchers/segment/segment_fetcher.ts +++ b/src/core/fetchers/segment/segment_fetcher.ts @@ -24,6 +24,7 @@ import Manifest, { Period, Representation, } from "../../../manifest"; +import { ICdnMetadata } from "../../../parsers/manifest"; import { IPlayerError } from "../../../public_types"; import { IChunkCompleteInformation, @@ -48,9 +49,10 @@ import { IRequestEndCallbackPayload, IRequestProgressCallbackPayload, } from "../../adaptive"; +import CdnPrioritizer from "../../init/cdn_prioritizer"; import { IBufferType } from "../../segment_buffers"; import errorSelector from "../utils/error_selector"; -import { tryURLsWithBackoff } from "../utils/try_urls_with_backoff"; +import { scheduleRequestWithCdns } from "../utils/schedule_request"; /** Allows to generate a unique identifies for each request. */ @@ -71,6 +73,7 @@ const generateRequestID = idGenerator(); export default function createSegmentFetcher( bufferType : IBufferType, pipeline : ISegmentPipeline, + cdnPrioritizer : CdnPrioritizer | null, lifecycleCallbacks : ISegmentFetcherLifecycleCallbacks, options : ISegmentFetcherOptions ) : ISegmentFetcher { @@ -102,8 +105,6 @@ export default function createSegmentFetcher( fetcherCallbacks : ISegmentFetcherCallbacks, cancellationSignal : CancellationSignal ) : Promise { - const { segment } = content; - // used by logs const segmentIdString = getLoggableSegmentId(content); const requestId = generateRequestID(); @@ -193,10 +194,11 @@ export default function createSegmentFetcher( }); try { - const res = await tryURLsWithBackoff(segment.mediaURLs ?? [null], - callLoaderWithUrl, - objectAssign({ onRetry }, options), - cancellationSignal); + const res = await scheduleRequestWithCdns(content.representation.cdnMetadata, + cdnPrioritizer, + callLoaderWithUrl, + objectAssign({ onRetry }, options), + cancellationSignal); if (res.resultType === "segment-loaded") { const loadedData = res.resultData.responseData; @@ -236,13 +238,13 @@ export default function createSegmentFetcher( /** * Call a segment loader for the given URL with the right arguments. - * @param {string|null} url + * @param {Object|null} cdnMetadata * @returns {Promise} */ function callLoaderWithUrl( - url : string | null + cdnMetadata : ICdnMetadata | null ) : ReturnType> { - return loadSegment(url, + return loadSegment(cdnMetadata, content, requestOptions, cancellationSignal, diff --git a/src/core/fetchers/segment/segment_fetcher_creator.ts b/src/core/fetchers/segment/segment_fetcher_creator.ts index 6aa46b0edc..7b1fb576d2 100644 --- a/src/core/fetchers/segment/segment_fetcher_creator.ts +++ b/src/core/fetchers/segment/segment_fetcher_creator.ts @@ -19,6 +19,7 @@ import { ISegmentPipeline, ITransportPipelines, } from "../../../transports"; +import CdnPrioritizer from "../../init/cdn_prioritizer"; import { IBufferType } from "../../segment_buffers"; import applyPrioritizerToSegmentFetcher, { IPrioritizedSegmentFetcher, @@ -81,11 +82,14 @@ export default class SegmentFetcherCreator { */ private readonly _backoffOptions : ISegmentFetcherCreatorBackoffOptions; + private readonly _cdnPrioritizer : CdnPrioritizer; + /** * @param {Object} transport */ constructor( transport : ITransportPipelines, + cdnPrioritizer : CdnPrioritizer, options : ISegmentFetcherCreatorBackoffOptions ) { const { MIN_CANCELABLE_PRIORITY, @@ -95,6 +99,7 @@ export default class SegmentFetcherCreator { prioritySteps: { high: MAX_HIGH_PRIORITY_LEVEL, low: MIN_CANCELABLE_PRIORITY }, }); + this._cdnPrioritizer = cdnPrioritizer; this._backoffOptions = options; } @@ -116,6 +121,7 @@ export default class SegmentFetcherCreator { const segmentFetcher = createSegmentFetcher( bufferType, pipelines as ISegmentPipeline, + this._cdnPrioritizer, callbacks, backoffOptions ); diff --git a/src/core/fetchers/steering_manifest/index.ts b/src/core/fetchers/steering_manifest/index.ts new file mode 100644 index 0000000000..f591e5c158 --- /dev/null +++ b/src/core/fetchers/steering_manifest/index.ts @@ -0,0 +1,26 @@ +/** + * 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 SteeringManifestFetcher, { + ISteeringManifestFetcherSettings, + ISteeringManifestParser, +} from "./steering_manifest_fetcher"; + +export default SteeringManifestFetcher; +export { + ISteeringManifestFetcherSettings, + ISteeringManifestParser, +}; diff --git a/src/core/fetchers/steering_manifest/steering_manifest_fetcher.ts b/src/core/fetchers/steering_manifest/steering_manifest_fetcher.ts new file mode 100644 index 0000000000..f74e41690c --- /dev/null +++ b/src/core/fetchers/steering_manifest/steering_manifest_fetcher.ts @@ -0,0 +1,184 @@ +/** + * 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 config from "../../../config"; +import { formatError } from "../../../errors"; +import { ISteeringManifest } from "../../../parsers/SteeringManifest"; +import { IPlayerError } from "../../../public_types"; +import { + IRequestedData, + ITransportSteeringManifestPipeline, +} from "../../../transports"; +import { CancellationSignal } from "../../../utils/task_canceller"; +import errorSelector from "../utils/error_selector"; +import { + IBackoffSettings, + scheduleRequestPromise, +} from "../utils/schedule_request"; + +/** Response emitted by a SteeringManifestFetcher fetcher. */ +export type ISteeringManifestParser = + /** Allows to parse a fetched Steering Manifest into a `ISteeringManifest` structure. */ + (onWarnings : ((warnings : IPlayerError[]) => void)) => ISteeringManifest; + +/** Options used by the `SteeringManifestFetcher`. */ +export interface ISteeringManifestFetcherSettings { + /** Maximum number of time a request on error will be retried. */ + maxRetryRegular : number | undefined; + /** Maximum number of time a request be retried when the user is offline. */ + maxRetryOffline : number | undefined; +} + +/** + * Class allowing to facilitate the task of loading and parsing a Content + * Steering Manifest, which is an optional document associated to a content, + * communicating the priority between several CDN. + * @class SteeringManifestFetcher + */ +export default class SteeringManifestFetcher { + private _settings : ISteeringManifestFetcherSettings; + private _pipelines : ITransportSteeringManifestPipeline; + + /** + * Construct a new SteeringManifestFetcher. + * @param {Object} pipelines - Transport pipelines used to perform the + * Content Steering Manifest loading and parsing operations. + * @param {Object} settings - Configure the `SteeringManifestFetcher`. + */ + constructor( + pipelines : ITransportSteeringManifestPipeline, + settings : ISteeringManifestFetcherSettings + ) { + this._pipelines = pipelines; + this._settings = settings; + } + + /** + * (re-)Load the Content Steering Manifest. + * This method does not yet parse it, parsing will then be available through + * a callback available on the response. + * + * You can set an `url` on which that Content Steering Manifest will be + * requested. + * If not set, the regular Content Steering Manifest url - defined on the + * `SteeringManifestFetcher` instanciation - will be used instead. + * + * @param {string|undefined} url + * @param {Function} onRetry + * @param {Object} cancelSignal + * @returns {Promise} + */ + public async fetch( + url : string, + onRetry : (error : IPlayerError) => void, + cancelSignal : CancellationSignal + ) : Promise { + const pipelines = this._pipelines; + const backoffSettings = this._getBackoffSetting((err) => { + onRetry(errorSelector(err)); + }); + const callLoader = () => pipelines.loadSteeringManifest(url, cancelSignal); + const response = await scheduleRequestPromise(callLoader, + backoffSettings, + cancelSignal); + return (onWarnings : ((error : IPlayerError[]) => void)) => { + return this._parseSteeringManifest(response, onWarnings); + }; + } + + /** + * Parse an already loaded Content Steering Manifest. + * + * This method should be reserved for Content Steering Manifests for which no + * request has been done. + * In other cases, it's preferable to go through the `fetch` method, so + * information on the request can be used by the parsing process. + * @param {*} steeringManifest + * @param {Function} onWarnings + * @returns {Observable} + */ + public parse( + steeringManifest : unknown, + onWarnings : (error : IPlayerError[]) => void + ) : ISteeringManifest { + return this._parseSteeringManifest({ responseData: steeringManifest, + size: undefined, + requestDuration: undefined }, + onWarnings); + } + + /** + * Parse a Content Steering Manifest. + * @param {Object} loaded - Information about the loaded Content Steering Manifest. + * @param {Function} onWarnings + * @returns {Observable} + */ + private _parseSteeringManifest( + loaded : IRequestedData, + onWarnings : (error : IPlayerError[]) => void + ) : ISteeringManifest { + try { + return this._pipelines.parseSteeringManifest( + loaded, + function onTransportWarnings(errs) { + const warnings = errs.map(e => formatParsingError(e)); + onWarnings(warnings); + } + ); + } catch (err) { + throw formatParsingError(err); + } + + /** + * Format the given Error and emit it through `obs`. + * Either through a `"warning"` event, if `isFatal` is `false`, or through + * a fatal Observable error, if `isFatal` is set to `true`. + * @param {*} err + * @returns {Error} + */ + function formatParsingError(err : unknown) : IPlayerError { + return formatError(err, { + defaultCode: "PIPELINE_PARSE_ERROR", + defaultReason: "Unknown error when parsing the Content Steering Manifest", + }); + } + } + + /** + * Construct "backoff settings" that can be used with a range of functions + * allowing to perform multiple request attempts + * @param {Function} onRetry + * @returns {Object} + */ + private _getBackoffSetting(onRetry : (err : unknown) => void) : IBackoffSettings { + const { DEFAULT_MAX_CONTENT_STEERING_MANIFEST_REQUEST_RETRY, + DEFAULT_MAX_REQUESTS_RETRY_ON_OFFLINE, + INITIAL_BACKOFF_DELAY_BASE, + MAX_BACKOFF_DELAY_BASE } = config.getCurrent(); + const { maxRetryRegular : ogRegular, + maxRetryOffline : ogOffline } = this._settings; + const baseDelay = INITIAL_BACKOFF_DELAY_BASE.REGULAR; + const maxDelay = MAX_BACKOFF_DELAY_BASE.REGULAR; + const maxRetryRegular = ogRegular ?? + DEFAULT_MAX_CONTENT_STEERING_MANIFEST_REQUEST_RETRY; + const maxRetryOffline = ogOffline ?? DEFAULT_MAX_REQUESTS_RETRY_ON_OFFLINE; + return { onRetry, + baseDelay, + maxDelay, + maxRetryRegular, + maxRetryOffline }; + } +} diff --git a/src/core/fetchers/utils/schedule_request.ts b/src/core/fetchers/utils/schedule_request.ts new file mode 100644 index 0000000000..2b297b6112 --- /dev/null +++ b/src/core/fetchers/utils/schedule_request.ts @@ -0,0 +1,457 @@ +/** + * 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 { isOffline } from "../../../compat"; +import { + CustomLoaderError, + isKnownError, + NetworkErrorTypes, + RequestError, +} from "../../../errors"; +import log from "../../../log"; +import { ICdnMetadata } from "../../../parsers/manifest"; +import cancellableSleep from "../../../utils/cancellable_sleep"; +import getFuzzedDelay from "../../../utils/get_fuzzed_delay"; +import noop from "../../../utils/noop"; +import SyncOrAsync, { + ISyncOrAsyncValue, +} from "../../../utils/sync_or_async"; +import TaskCanceller, { + CancellationSignal, +} from "../../../utils/task_canceller"; +import CdnPrioritizer from "../../init/cdn_prioritizer"; + +/** + * Called on a loader error. + * Returns whether the loader request should be retried. + * + * TODO the notion of retrying or not could be transport-specific (e.g. 412 are + * mainly used for Smooth contents) and thus as part of the transport code (e.g. + * by rejecting with an error always having a `canRetry` property?). + * Or not, to ponder. + * + * @param {Error} error + * @returns {Boolean} - If true, the request can be retried. + */ +function shouldRetry(error : unknown) : boolean { + if (error instanceof RequestError) { + if (error.type === NetworkErrorTypes.ERROR_HTTP_CODE) { + return error.status >= 500 || + error.status === 404 || + error.status === 415 || // some CDN seems to use that code when + // requesting low-latency segments too much + // in advance + error.status === 412; + } + return error.type === NetworkErrorTypes.TIMEOUT || + error.type === NetworkErrorTypes.ERROR_EVENT; + } else if (error instanceof CustomLoaderError) { + if (typeof error.canRetry === "boolean") { + return error.canRetry; + } + if (error.xhr !== undefined) { + return error.xhr.status >= 500 || + error.xhr.status === 404 || + error.xhr.status === 415 || // some CDN seems to use that code when + // requesting low-latency segments too much + // in advance + error.xhr.status === 412; + } + return false; + } + return isKnownError(error) && error.code === "INTEGRITY_ERROR"; +} + +/** + * Returns true if we're pretty sure that the current error is due to the + * user being offline. + * @param {Error} error + * @returns {Boolean} + */ +function isOfflineRequestError(error : unknown) : boolean { + if (error instanceof RequestError) { + return error.type === NetworkErrorTypes.ERROR_EVENT && + isOffline(); + } else if (error instanceof CustomLoaderError) { + return error.isOfflineError; + } + return false; // under doubt, return false +} + +/** Settings to give to the backoff functions to configure their behavior. */ +export interface IBackoffSettings { + /** + * Initial delay to wait if a request fails before making a new request, in + * milliseconds. + */ + baseDelay : number; + /** + * Maximum delay to wait if a request fails before making a new request, in + * milliseconds. + */ + maxDelay : number; + /** + * Maximum number of retries to perform on "regular" errors (e.g. due to HTTP + * status, integrity errors, timeouts...). + */ + maxRetryRegular : number; + /** + * Maximum number of retries to perform when it appears that the user is + * currently offline. + */ + maxRetryOffline : number; + /** Callback called when a request is retried. */ + onRetry : (err : unknown) => void; +} + +const enum REQUEST_ERROR_TYPES { None, + Regular, + Offline } + +/** + * Guess the type of error obtained. + * @param {*} error + * @returns {number} + */ +function getRequestErrorType(error : unknown) : REQUEST_ERROR_TYPES { + return isOfflineRequestError(error) ? REQUEST_ERROR_TYPES.Offline : + REQUEST_ERROR_TYPES.Regular; +} + +/** + * Specific algorithm used to perform segment and manifest requests. + * + * Here how it works: + * + * 1. You give it one or multiple of the CDN available for the resource you + * want to request (from the most important one to the least important), + * a callback doing the request with the chosen CDN in argument, and some + * options. + * + * 2. it tries to call the request callback with the most prioritized CDN + * first: + * - if it works as expected, it resolves the returned Promise with that + * request's response. + * - if it fails, it calls ther `onRetry` callback given with the + * corresponding error, un-prioritize that CDN and try with the new + * most prioritized CDN. + * + * Each CDN might be retried multiple times, depending on the nature of the + * error and the Configuration given. + * + * Multiple retries of the same CDN are done after a delay to avoid + * overwhelming it, this is what we call a "backoff". That delay raises + * exponentially as multiple consecutive errors are encountered on this + * CDN. + * + * @param {Array.|null} cdns - The different CDN on which the + * wanted resource is available. `scheduleRequestWithCdns` will call the + * `performRequest` callback with the right element from that array if different + * from `null`. + * + * Can be set to `null` when that resource is not reachable through a CDN, in + * which case the `performRequest` callback may be called with `null`. + * @param {Object|null} cdnPrioritizer - Interface allowing to give the priority + * between multiple CDNs. + * @param {Function} performRequest - Callback implementing the request in + * itself. Resolving when the resource request succeed and rejecting with the + * corresponding error when the request failed. + * @param {Object} options - Configuration allowing to tweak the number on which + * the algorithm behind `scheduleRequestWithCdns` bases itself. + * @param {Object} cancellationSignal - CancellationSignal allowing to cancel + * the logic of `scheduleRequestWithCdns`. + * To trigger if the resource is not needed anymore. + * @returns {Promise} - Promise resolving, with the corresponding + * `performRequest`'s data, when the resource request succeed and rejecting in + * the following scenarios: + * - `scheduleRequestWithCdns` has been cancelled due to `cancellationSignal` + * being triggered. In that case a `CancellationError` is thrown. + * + * - The resource request(s) failed and will not be retried anymore. + */ +export async function scheduleRequestWithCdns( + cdns : ICdnMetadata[] | null, + cdnPrioritizer : CdnPrioritizer | null, + performRequest : ( + cdn : ICdnMetadata | null, + cancellationSignal : CancellationSignal + ) => Promise, + options : IBackoffSettings, + cancellationSignal : CancellationSignal +) : Promise { + if (cancellationSignal.cancellationError !== null) { + return Promise.reject(cancellationSignal.cancellationError); + } + + const { baseDelay, + maxDelay, + maxRetryRegular, + maxRetryOffline, + onRetry } = options; + + if (cdns !== null && cdns.length === 0) { + log.warn("Fetchers: no CDN given to `scheduleRequestWithCdns`."); + } + + const missedAttempts : Map = new Map(); + const cdnsResponse = getSortedCdnsToRequest(); + const initialCdnsToRequest = cdnsResponse.syncValue ?? + await cdnsResponse.getValueAsAsync(); + if (initialCdnsToRequest.length === 0) { + throw new Error("No CDN to request"); + } + return requestCdn(initialCdnsToRequest[0]); + + /** + * Returns a sorted and filtered array representing the resource's left + * request-able CDN, by order of preference. + * This array might be empty, in which case there's no CDN left to request + * that resource. + * + * This array might contain a `null` value, which indicates that the resource + * can be requested through another mean than by doing an HTTP request. + * + * @returns {Object} + */ + function getSortedCdnsToRequest() : ISyncOrAsyncValue> { + if (cdns === null) { + const nullAttemptObject = missedAttempts.get(null); + if (nullAttemptObject !== undefined && nullAttemptObject.isBlacklisted) { + return SyncOrAsync.createSync([]); + } + return SyncOrAsync.createSync([null]); + } else if (cdnPrioritizer === null) { + return SyncOrAsync.createSync( + cdns.filter(c => missedAttempts.get(c)?.isBlacklisted !== true) + ); + } else { + const prioritized = cdnPrioritizer.getCdnPreferenceForResource(cdns); + if (prioritized.syncValue !== null) { + return SyncOrAsync.createSync(prioritized.syncValue + .filter(u => missedAttempts.get(u)?.isBlacklisted !== true)); + } + return SyncOrAsync.createAsync(prioritized.getValueAsAsync().then(v => + v.filter(u => missedAttempts.get(u)?.isBlacklisted !== true) + )); + } + } + + /** + * Perform immediately the request for the given CDN. + * + * If it fails, forbid the CDN from being used - optionally and in some + * conditions, only temporarily, then try the next CDN according to + * previously-set delays (with a potential sleep before to respect them). + * + * Reject if both the request fails and there's no CDN left to use. + * @param {string|null} cdn + * @returns {Promise} + */ + async function requestCdn(cdn : ICdnMetadata | null) : Promise { + try { + const res = await performRequest(cdn, cancellationSignal); + return res; + } catch (error : unknown) { + if (TaskCanceller.isCancellationError(error)) { + throw error; + } + + if (cdn !== null && cdnPrioritizer !== null) { + // We failed requesting the resource on this CDN. + // Globally give priority to the next CDN through the CdnPrioritizer. + cdnPrioritizer.downgradeCdn(cdn); + } + + const currentErrorType = getRequestErrorType(error); + + let missedAttemptsObj = missedAttempts.get(cdn); + if (missedAttemptsObj === undefined) { + missedAttemptsObj = { errorCounter: 1, + lastErrorType: currentErrorType, + blockedUntil: undefined, + isBlacklisted: false }; + missedAttempts.set(cdn, missedAttemptsObj); + } else { + if (currentErrorType !== missedAttemptsObj.lastErrorType) { + missedAttemptsObj.errorCounter = 1; + missedAttemptsObj.lastErrorType = currentErrorType; + } else { + missedAttemptsObj.errorCounter++; + } + } + + if (!shouldRetry(error)) { + missedAttemptsObj.blockedUntil = undefined; + missedAttemptsObj.isBlacklisted = true; + return retryWithNextCdn(error); + } + + const maxRetry = currentErrorType === REQUEST_ERROR_TYPES.Offline ? + maxRetryOffline : + maxRetryRegular; + + if (missedAttemptsObj.errorCounter > maxRetry) { + missedAttemptsObj.blockedUntil = undefined; + missedAttemptsObj.isBlacklisted = true; + } else { + const errorCounter = missedAttemptsObj.errorCounter; + const delay = Math.min(baseDelay * Math.pow(2, errorCounter - 1), + maxDelay); + const fuzzedDelay = getFuzzedDelay(delay); + missedAttemptsObj.blockedUntil = performance.now() + fuzzedDelay; + } + + return retryWithNextCdn(error); + } + } + + /** + * After a request error, find the new most prioritary CDN and perform the + * request with it, optionally after a delay. + * + * If there's no CDN left to test, reject the original request error. + * @param {*} prevRequestError + * @returns {Promise} + */ + async function retryWithNextCdn(prevRequestError : unknown) : Promise { + const currCdnsResponse = getSortedCdnsToRequest(); + const sortedCdns = currCdnsResponse.syncValue ?? + await currCdnsResponse.getValueAsAsync(); + + if (cancellationSignal.isCancelled) { + throw cancellationSignal.cancellationError; + } + + if (sortedCdns.length === 0) { + throw prevRequestError; + } + + onRetry(prevRequestError); + if (cancellationSignal.isCancelled) { + throw cancellationSignal.cancellationError; + } + + const nextWantedCdn = sortedCdns[0]; + return waitPotentialBackoffAndRequest(nextWantedCdn, prevRequestError); + } + + /** + * Request the corresponding CDN after the optional backoff needed before + * requesting it. + * + * If a new CDN become prioritary in the meantime, request it instead, again + * awaiting its optional backoff delay if it exists. + * @param {string|null} nextWantedCdn + * @param {*} prevRequestError + * @returns {Promise} + */ + function waitPotentialBackoffAndRequest( + nextWantedCdn: ICdnMetadata | null, + prevRequestError : unknown + ) : Promise { + const nextCdnAttemptObj = missedAttempts.get(nextWantedCdn); + if (nextCdnAttemptObj === undefined || + nextCdnAttemptObj.blockedUntil === undefined) + { + return requestCdn(nextWantedCdn); + } + + const now = performance.now(); + const blockedFor = nextCdnAttemptObj.blockedUntil - now; + if (blockedFor <= 0) { + return requestCdn(nextWantedCdn); + } + + const canceller = new TaskCanceller({ cancelOn: cancellationSignal }); + return new Promise((res, rej) => { + /* eslint-disable-next-line @typescript-eslint/no-misused-promises */ + cdnPrioritizer?.addEventListener("priorityChange", async () => { + const newCdnsResponse = getSortedCdnsToRequest(); + const newSortedCdns = newCdnsResponse.syncValue ?? + await newCdnsResponse.getValueAsAsync(); + if (cancellationSignal.isCancelled) { + throw cancellationSignal.cancellationError; + } + if (newSortedCdns.length === 0) { + return rej(prevRequestError); + } + if (newSortedCdns[0] !== nextWantedCdn) { + canceller.cancel(); + waitPotentialBackoffAndRequest(newSortedCdns[0], prevRequestError) + .then(res, rej); + } + }, canceller.signal); + + cancellableSleep(blockedFor, canceller.signal) + .then(() => requestCdn(nextWantedCdn).then(res, rej), noop); + }); + } +} + +/** + * Lightweight version of the request algorithm, this time with only a simple + * Promise given. + * @param {Function} performRequest + * @param {Object} options + * @returns {Promise} + */ +export function scheduleRequestPromise( + performRequest : () => Promise, + options : IBackoffSettings, + cancellationSignal : CancellationSignal +) : Promise { + // same than for a single unknown CDN + return scheduleRequestWithCdns(null, + null, + performRequest, + options, + cancellationSignal); +} + +/** + * Metadata associated to attempt(s) of requesting a resource through the same + * CDN. + * + * Each `ICdnAttemptMetadata` object should concern only one CDN. + */ +interface ICdnAttemptMetadata { + /** + * Count the amount of consecutive times an error of type `lastErrorType` has + * been encountered while requesting this resource though the concerned CDN. + * + * For example `1` means that the request through this CDN either failed for + * the first consecutive time or that it already failed just before but for an + * error with a different `lastErrorType` value. + * `2` means that after requesting this CDN two consecutive times, the request + * failed with an error with the same `lastErrorType` value. + * etc. + */ + errorCounter : number; + /** The last type of error encountered when requesting through that CDN. */ + lastErrorType : REQUEST_ERROR_TYPES; + /** + * Timestamp, in terms of `performance.now()`, until which it should be + * forbidden to request this CDN. + * Enforcing this delay allows to prevent making too much requests to a given + * CDN. + * + * `undefined` when either there is no enforced delay or when the CDN is + * blacklisted anyway (@see isBlacklisted) + */ + blockedUntil : number | undefined; + /** If `true`, that request should not be requested at all anymore. */ + isBlacklisted : boolean; +} diff --git a/src/core/fetchers/utils/try_urls_with_backoff.ts b/src/core/fetchers/utils/try_urls_with_backoff.ts deleted file mode 100644 index 42749c678b..0000000000 --- a/src/core/fetchers/utils/try_urls_with_backoff.ts +++ /dev/null @@ -1,287 +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 { isOffline } from "../../../compat"; -import { - CustomLoaderError, - isKnownError, - NetworkErrorTypes, - RequestError, -} from "../../../errors"; -import log from "../../../log"; -import cancellableSleep from "../../../utils/cancellable_sleep"; -import getFuzzedDelay from "../../../utils/get_fuzzed_delay"; -import TaskCanceller, { - CancellationSignal, -} from "../../../utils/task_canceller"; - -/** - * Called on a loader error. - * Returns whether the loader request should be retried. - * - * TODO the notion of retrying or not could be transport-specific (e.g. 412 are - * mainly used for Smooth contents) and thus as part of the transport code (e.g. - * by rejecting with an error always having a `canRetry` property?). - * Or not, to ponder. - * - * @param {Error} error - * @returns {Boolean} - If true, the request can be retried. - */ -function shouldRetry(error : unknown) : boolean { - if (error instanceof RequestError) { - if (error.type === NetworkErrorTypes.ERROR_HTTP_CODE) { - return error.status >= 500 || - error.status === 404 || - error.status === 415 || // some CDN seems to use that code when - // requesting low-latency segments too much - // in advance - error.status === 412; - } - return error.type === NetworkErrorTypes.TIMEOUT || - error.type === NetworkErrorTypes.ERROR_EVENT; - } else if (error instanceof CustomLoaderError) { - if (typeof error.canRetry === "boolean") { - return error.canRetry; - } - if (error.xhr !== undefined) { - return error.xhr.status >= 500 || - error.xhr.status === 404 || - error.xhr.status === 415 || // some CDN seems to use that code when - // requesting low-latency segments too much - // in advance - error.xhr.status === 412; - } - return false; - } - return isKnownError(error) && error.code === "INTEGRITY_ERROR"; -} - -/** - * Returns true if we're pretty sure that the current error is due to the - * user being offline. - * @param {Error} error - * @returns {Boolean} - */ -function isOfflineRequestError(error : unknown) : boolean { - if (error instanceof RequestError) { - return error.type === NetworkErrorTypes.ERROR_EVENT && - isOffline(); - } else if (error instanceof CustomLoaderError) { - return error.isOfflineError; - } - return false; // under doubt, return false -} - -/** Settings to give to the backoff functions to configure their behavior. */ -export interface IBackoffSettings { - /** - * Initial delay to wait if a request fails before making a new request, in - * milliseconds. - */ - baseDelay : number; - /** - * Maximum delay to wait if a request fails before making a new request, in - * milliseconds. - */ - maxDelay : number; - /** - * Maximum number of retries to perform on "regular" errors (e.g. due to HTTP - * status, integrity errors, timeouts...). - */ - maxRetryRegular : number; - /** - * Maximum number of retries to perform when it appears that the user is - * currently offline. - */ - maxRetryOffline : number; - /** Callback called when a request is retried. */ - onRetry : (err : unknown) => void; -} - -const enum REQUEST_ERROR_TYPES { None, - Regular, - Offline } - -/** - * Guess the type of error obtained. - * @param {*} error - * @returns {number} - */ -function getRequestErrorType(error : unknown) : REQUEST_ERROR_TYPES { - return isOfflineRequestError(error) ? REQUEST_ERROR_TYPES.Offline : - REQUEST_ERROR_TYPES.Regular; -} - -/** - * Specific algorithm used to perform segment and manifest requests. - * - * Here how it works: - * - * 1. You give it one or multiple URLs available for the resource you want to - * request (from the most important URL to the least important), the - * request callback itself, and some options. - * - * 2. it tries to call the request callback with the first URL: - * - if it works as expected, it resolves the returned Promise with that - * request's response. - * - if it fails, it calls ther `onRetry` callback given with the - * corresponding error and try with the next URL. - * - * 3. When all URLs have been tested (and failed), it decides - according to - * the error counters, configuration and errors received - if it can retry - * at least one of them, in the same order: - * - If it can, it increments the corresponding error counter, wait a - * delay (based on an exponential backoff) and restart the same logic - * for all retry-able URL. - * - If it can't it just reject the error through the returned Promise. - * - * Note that there are in fact two separate counters: - * - one for "offline" errors - * - one for other xhr errors - * Both counters are resetted if the error type changes from an error to the - * next. - * - * @param {Array.} urls - * @param {Function} performRequest - * @param {Object} options - Configuration options. - * @param {Object} cancellationSignal - * @returns {Promise} - */ -export function tryURLsWithBackoff( - urls : Array, - performRequest : ( - url : string | null, - cancellationSignal : CancellationSignal - ) => Promise, - options : IBackoffSettings, - cancellationSignal : CancellationSignal -) : Promise { - if (cancellationSignal.isCancelled) { - return Promise.reject(cancellationSignal.cancellationError); - } - - const { baseDelay, - maxDelay, - maxRetryRegular, - maxRetryOffline, - onRetry } = options; - let retryCount = 0; - let lastError = REQUEST_ERROR_TYPES.None; - - const urlsToTry = urls.slice(); - if (urlsToTry.length === 0) { - log.warn("Fetchers: no URL given to `tryURLsWithBackoff`."); - return Promise.reject(new Error("No URL to request")); - } - return tryURLsRecursively(urlsToTry[0], 0); - - /** - * Try to do the request of a given `url` which corresponds to the `index` - * argument in the `urlsToTry` Array. - * - * If it fails try the next one. - * - * If all URLs fail, start a timer and retry the first element in that array - * by following the configuration. - * - * @param {string|null} url - * @param {number} index - * @returns {Promise} - */ - async function tryURLsRecursively( - url : string | null, - index : number - ) : Promise { - try { - const res = await performRequest(url, cancellationSignal); - return res; - } catch (error : unknown) { - if (TaskCanceller.isCancellationError(error)) { - throw error; - } - - if (!shouldRetry(error)) { - // ban this URL - if (urlsToTry.length <= 1) { // This was the last one, throw - throw error; - } - - // else, remove that element from the array and go the next URL - urlsToTry.splice(index, 1); - const newIndex = index >= urlsToTry.length - 1 ? 0 : - index; - onRetry(error); - if (cancellationSignal.isCancelled) { - throw cancellationSignal.cancellationError; - } - return tryURLsRecursively(urlsToTry[newIndex], newIndex); - } - - const currentError = getRequestErrorType(error); - const maxRetry = currentError === REQUEST_ERROR_TYPES.Offline ? maxRetryOffline : - maxRetryRegular; - - if (currentError !== lastError) { - retryCount = 0; - lastError = currentError; - } - - if (index < urlsToTry.length - 1) { // there is still URLs to test - const newIndex = index + 1; - onRetry(error); - if (cancellationSignal.isCancelled) { - throw cancellationSignal.cancellationError; - } - return tryURLsRecursively(urlsToTry[newIndex], newIndex); - } - - // Here, we were using the last element of the `urlsToTry` array. - // Increment counter and restart with the first URL - - retryCount++; - if (retryCount > maxRetry) { - throw error; - } - const delay = Math.min(baseDelay * Math.pow(2, retryCount - 1), - maxDelay); - const fuzzedDelay = getFuzzedDelay(delay); - const nextURL = urlsToTry[0]; - onRetry(error); - if (cancellationSignal.isCancelled) { - throw cancellationSignal.cancellationError; - } - - await cancellableSleep(fuzzedDelay, cancellationSignal); - return tryURLsRecursively(nextURL, 0); - } - } -} - -/** - * Lightweight version of the request algorithm, this time with only a simple - * Promise given. - * @param {Function} performRequest - * @param {Object} options - * @returns {Promise} - */ -export function tryRequestPromiseWithBackoff( - performRequest : () => Promise, - options : IBackoffSettings, - cancellationSignal : CancellationSignal -) : Promise { - // same than for a single unknown URL - return tryURLsWithBackoff([null], performRequest, options, cancellationSignal); -} diff --git a/src/core/init/cdn_prioritizer.ts b/src/core/init/cdn_prioritizer.ts new file mode 100644 index 0000000000..06f7e7fb09 --- /dev/null +++ b/src/core/init/cdn_prioritizer.ts @@ -0,0 +1,410 @@ +/** + * 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 config from "../../config"; +import { formatError } from "../../errors"; +import log from "../../log"; +import Manifest from "../../manifest"; +import { + ICdnMetadata, + IContentSteeringMetadata, +} from "../../parsers/manifest"; +import { ISteeringManifest } from "../../parsers/SteeringManifest"; +import { IPlayerError } from "../../public_types"; +import arrayFindIndex from "../../utils/array_find_index"; +import arrayIncludes from "../../utils/array_includes"; +import EventEmitter from "../../utils/event_emitter"; +import createSharedReference, { + ISharedReference, +} from "../../utils/reference"; +import SyncOrAsync, { + ISyncOrAsyncValue, +} from "../../utils/sync_or_async"; +import TaskCanceller, { + CancellationError, + CancellationSignal, +} from "../../utils/task_canceller"; +import SteeringManifestFetcher from "../fetchers/steering_manifest"; + +/** + * Class signaling the priority between multiple CDN available for any given + * resource. + * + * It might rely behind the hood on a fetched document giving priorities such as + * a Content Steering Manifest and also on issues that appeared with some given + * CDN in the [close] past. + * + * This class might perform requests and schedule timeouts by itself to keep its + * internal list of CDN priority up-to-date. + * When it is not needed anymore, you should call the `dispose` method to clear + * all resources. + * + * @class CdnPrioritizer + */ +export default class CdnPrioritizer extends EventEmitter { + /** + * Metadata parsed from the last Content Steering Manifest loaded. + * + * `null` either if there's no such Manifest or if it is currently being + * loaded for the first time. + */ + private _lastSteeringManifest : ISteeringManifest | null; + + private _defaultCdnId : string | undefined; + + /** + * Structure keeping a list of CDN currently downgraded. + * Downgraded CDN immediately have a lower priority than any non-downgraded + * CDN for a specific amount of time. + */ + private _downgradedCdnList : { + /** Metadata of downgraded CDN, in no important order. */ + metadata : ICdnMetadata[]; + /** + * Timeout ID (to give to `clearTimeout`) of elements in the `metadata` + * array, for the element at the same index in the `metadata` array. + * + * This structure has been writted as an object of two arrays of the same + * length, instead of an array of objects, to simplify the usage of the + * `metadata` array which is used considerably more than the `timeouts` + * array. + */ + timeouts : number[]; + }; + + /** + * TaskCanceller allowing to abort the process of loading and refreshing the + * Content Steering Manifest. + * Set to `null` when no such process is pending. + */ + private _steeringManifestUpdateCanceller : TaskCanceller | null; + + private _readyState : ISharedReference<"not-ready" | "ready" | "disposed">; + + /** + * @param {Object} manifest + * @param {Object|null} steeringManifestFetcher + * @param {Object} destroySignal + */ + constructor( + manifest : Manifest, + steeringManifestFetcher : SteeringManifestFetcher | null, + destroySignal : CancellationSignal + ) { + super(); + this._lastSteeringManifest = null; + this._downgradedCdnList = { metadata: [], timeouts: [] }; + this._steeringManifestUpdateCanceller = null; + this._defaultCdnId = manifest.contentSteering?.defaultId; + + let lastContentSteering = manifest.contentSteering; + + manifest.addEventListener("manifestUpdate", () => { + const prevContentSteering = lastContentSteering; + lastContentSteering = manifest.contentSteering; + if (prevContentSteering === null) { + if (lastContentSteering !== null) { + if (steeringManifestFetcher === null) { + log.warn("CP: Steering manifest declared but no way to fetch it"); + } else { + log.info("CP: A Steering Manifest is declared in a new Manifest"); + this._autoRefreshSteeringManifest(steeringManifestFetcher, + lastContentSteering); + } + } + } else if (lastContentSteering === null) { + log.info("CP: A Steering Manifest is removed in a new Manifest"); + this._steeringManifestUpdateCanceller?.cancel(); + this._steeringManifestUpdateCanceller = null; + } else if (prevContentSteering.url !== lastContentSteering.url || + prevContentSteering.proxyUrl !== lastContentSteering.proxyUrl) + { + log.info("CP: A Steering Manifest's information changed in a new Manifest"); + this._steeringManifestUpdateCanceller?.cancel(); + this._steeringManifestUpdateCanceller = null; + if (steeringManifestFetcher === null) { + log.warn("CP: Steering manifest changed but no way to fetch it"); + } else { + this._autoRefreshSteeringManifest(steeringManifestFetcher, + lastContentSteering); + } + } + }, destroySignal); + + if (manifest.contentSteering !== null) { + if (steeringManifestFetcher === null) { + log.warn("CP: Steering Manifest initially present but no way to fetch it."); + this._readyState = createSharedReference("ready"); + } else { + const readyState = manifest.contentSteering.queryBeforeStart ? "not-ready" : + "ready"; + this._readyState = createSharedReference(readyState); + this._autoRefreshSteeringManifest(steeringManifestFetcher, + manifest.contentSteering); + } + } else { + this._readyState = createSharedReference("ready"); + } + destroySignal.register(() => { + this._readyState.setValue("disposed"); + this._readyState.finish(); + this._steeringManifestUpdateCanceller?.cancel(); + this._steeringManifestUpdateCanceller = null; + this._lastSteeringManifest = null; + for (const timeout of this._downgradedCdnList.timeouts) { + clearTimeout(timeout); + } + this._downgradedCdnList = { metadata: [], timeouts: [] }; + }); + } + + /** + * From the list of __ALL__ CDNs available to a resource, return them in the + * order in which requests should be performed. + * + * Note: It is VERY important to include all CDN that are able to reach the + * wanted resource, even those which will in the end not be used anyway. + * If some CDN are not communicated, the `CdnPrioritizer` might wrongly + * consider that the current resource don't have any of the CDN prioritized + * internally and return other CDN which should have been forbidden if it knew + * about the other, non-used, ones. + * + * @param {Array.} everyCdnForResource - Array of ALL available CDN + * able to reach the wanted resource - even those which might not be used in + * the end. + * @returns {Object} - Array of CDN that can be tried to reach the + * resource, sorted by order of CDN preference, according to the + * `CdnPrioritizer`'s own list of priorities. + * + * This value is wrapped in a `ISyncOrAsyncValue` as in relatively rare + * scenarios, the order can only be known once the steering Manifest has been + * fetched. + */ + public getCdnPreferenceForResource( + everyCdnForResource : ICdnMetadata[] + ) : ISyncOrAsyncValue { + if (everyCdnForResource.length <= 1) { + // The huge majority of contents have only one CDN available. + // Here, prioritizing make no sense. + return SyncOrAsync.createSync(everyCdnForResource); + } + + if (this._readyState.getValue() === "not-ready") { + const val = new Promise((res, rej) => { + this._readyState.onUpdate((readyState) => { + if (readyState === "ready") { + res(this._innerGetCdnPreferenceForResource(everyCdnForResource)); + } else if (readyState === "disposed") { + rej(new CancellationError()); + } + }); + }); + return SyncOrAsync.createAsync(val); + } + return SyncOrAsync.createSync( + this._innerGetCdnPreferenceForResource(everyCdnForResource) + ); + } + + /** + * Limit usage of the CDN for a configured amount of time. + * Call this method if you encountered an issue with that CDN which leads you + * to want to prevent its usage currently. + * + * Note that the CDN can still be the preferred one if no other CDN exist for + * a wanted resource. + * @param {string} metadata + */ + public downgradeCdn(metadata : ICdnMetadata) : void { + const indexOf = indexOfMetadata(this._downgradedCdnList.metadata, metadata); + if (indexOf >= 0) { + this._removeIndexFromDowngradeList(indexOf); + } + + const { DEFAULT_CDN_DOWNGRADE_TIME } = config.getCurrent(); + const downgradeTime = this._lastSteeringManifest?.lifetime ?? + DEFAULT_CDN_DOWNGRADE_TIME; + this._downgradedCdnList.metadata.push(metadata); + const timeout = window.setTimeout(() => { + const newIndex = indexOfMetadata(this._downgradedCdnList.metadata, metadata); + if (newIndex >= 0) { + this._removeIndexFromDowngradeList(newIndex); + } + this.trigger("priorityChange", null); + }, downgradeTime); + this._downgradedCdnList.timeouts.push(timeout); + this.trigger("priorityChange", null); + } + + /** + * From the list of __ALL__ CDNs available to a resource, return them in the + * order in which requests should be performed. + * + * Note: It is VERY important to include all CDN that are able to reach the + * wanted resource, even those which will in the end not be used anyway. + * If some CDN are not communicated, the `CdnPrioritizer` might wrongly + * consider that the current resource don't have any of the CDN prioritized + * internally and return other CDN which should have been forbidden if it knew + * about the other, non-used, ones. + * + * @param {Array.} everyCdnForResource - Array of ALL available CDN + * able to reach the wanted resource - even those which might not be used in + * the end. + * @returns {Array.} - Array of CDN that can be tried to reach the + * resource, sorted by order of CDN preference, according to the + * `CdnPrioritizer`'s own list of priorities. + */ + private _innerGetCdnPreferenceForResource( + everyCdnForResource : ICdnMetadata[] + ) : ICdnMetadata[] { + let cdnBase; + if (this._lastSteeringManifest !== null) { + const priorities = this._lastSteeringManifest.priorities; + const inSteeringManifest = everyCdnForResource.filter(available => + available.id !== undefined && arrayIncludes(priorities, available.id)); + if (inSteeringManifest.length > 0) { + cdnBase = inSteeringManifest; + } + } + + // (If using the SteeringManifest gave nothing, or if it just didn't exist.) */ + if (cdnBase === undefined) { + // (If a default CDN was indicated, try to use it) */ + if (this._defaultCdnId !== undefined) { + const indexOf = arrayFindIndex(everyCdnForResource, (x) => + x.id !== undefined && x.id === this._defaultCdnId); + if (indexOf >= 0) { + const elem = everyCdnForResource.splice(indexOf, 1)[0]; + everyCdnForResource.unshift(elem); + } + } + + if (cdnBase === undefined) { + cdnBase = everyCdnForResource; + } + } + const [allowedInOrder, downgradedInOrder] = cdnBase + .reduce((acc : [ICdnMetadata[], ICdnMetadata[]], elt : ICdnMetadata) => { + if (this._downgradedCdnList.metadata.some(c => c.id === elt.id && + c.baseUrl === elt.baseUrl)) + { + acc[1].push(elt); + } else { + acc[0].push(elt); + } + return acc; + }, [[], []]); + return allowedInOrder.concat(downgradedInOrder); + } + + private _autoRefreshSteeringManifest( + steeringManifestFetcher : SteeringManifestFetcher, + contentSteering : IContentSteeringMetadata + ) { + if (this._steeringManifestUpdateCanceller === null) { + const steeringManifestUpdateCanceller = new TaskCanceller(); + this._steeringManifestUpdateCanceller = steeringManifestUpdateCanceller; + } + const canceller : TaskCanceller = this._steeringManifestUpdateCanceller; + steeringManifestFetcher.fetch(contentSteering.url, + (err : IPlayerError) => this.trigger("warnings", [err]), + canceller.signal) + .then((parse) => { + const parsed = parse((errs) => this.trigger("warnings", errs)); + const prevSteeringManifest = this._lastSteeringManifest; + this._lastSteeringManifest = parsed; + if (parsed.lifetime > 0) { + const timeout = window.setTimeout(() => { + canceller.signal.deregister(onTimeoutEnd); + this._autoRefreshSteeringManifest(steeringManifestFetcher, contentSteering); + }, parsed.lifetime * 1000); + const onTimeoutEnd = () => { + clearTimeout(timeout); + }; + canceller.signal.register(onTimeoutEnd); + } + if (this._readyState.getValue() === "not-ready") { + this._readyState.setValue("ready"); + } + if (canceller.isUsed) { + return; + } + if (prevSteeringManifest === null || + prevSteeringManifest.priorities.length !== parsed.priorities.length || + prevSteeringManifest.priorities + .some((val, idx) => val !== parsed.priorities[idx])) + { + this.trigger("priorityChange", null); + } + }) + .catch((err) => { + if (err instanceof CancellationError) { + return; + } + const formattedError = formatError(err, { + defaultCode: "NONE", + defaultReason: "Unknown error when fetching and parsing the steering Manifest", + }); + this.trigger("warnings", [formattedError]); + }); + } + + /** + * @param {number} index + */ + private _removeIndexFromDowngradeList(index : number) : void { + this._downgradedCdnList.metadata.splice(index, 1); + const oldTimeout = this._downgradedCdnList.timeouts.splice(index, 1); + clearTimeout(oldTimeout[0]); + } +} + +export interface ICdnPrioritizerEvents { + warnings : IPlayerError[]; + priorityChange : null; +} + +/** + * Find the index of the given CDN metadata in a CDN metadata array. + * Returns `-1` if not found. + * @param {Array.} arr + * @param {Object} elt + * @returns {number} + */ +function indexOfMetadata( + arr : ICdnMetadata[], + elt : ICdnMetadata +) : number { + if (arr.length === 0) { + return -1; + } + if (elt.id !== undefined) { + for (let i = 0; i < arr.length; i++) { + const m = arr[i]; + if (m.id === elt.id) { + return i; + } + } + } else { + for (let i = 0; i < arr.length; i++) { + const m = arr[i]; + if (m.baseUrl === elt.baseUrl) { + return i; + } + } + } + return -1; +} diff --git a/src/core/init/initialize_media_source.ts b/src/core/init/initialize_media_source.ts index 32582b39c1..85b5cca9b3 100644 --- a/src/core/init/initialize_media_source.ts +++ b/src/core/init/initialize_media_source.ts @@ -36,11 +36,13 @@ import { shouldReloadMediaSourceOnDecipherabilityUpdate } from "../../compat"; import config from "../../config"; import log from "../../log"; import { IKeySystemOption } from "../../public_types"; +import { ITransportPipelines } from "../../transports"; import deferSubscriptions from "../../utils/defer_subscriptions"; import { fromEvent } from "../../utils/event_emitter"; import filterMap from "../../utils/filter_map"; import objectAssign from "../../utils/object_assign"; import { IReadOnlySharedReference } from "../../utils/reference"; +import TaskCanceller from "../../utils/task_canceller"; import AdaptiveRepresentationSelector, { IAdaptiveRepresentationSelectorArguments, } from "../adaptive"; @@ -55,8 +57,10 @@ import { ManifestFetcher, SegmentFetcherCreator, } from "../fetchers"; +import SteeringManifestFetcher from "../fetchers/steering_manifest"; import { ITextTrackSegmentBufferOptions } from "../segment_buffers"; import { IAudioTrackSwitchingMode } from "../stream"; +import CdnPrioritizer from "./cdn_prioritizer"; import openMediaSource from "./create_media_source"; import EVENTS from "./events_generators"; import getInitialTime, { @@ -125,8 +129,21 @@ export interface IInitializeArguments { mediaElement : HTMLMediaElement; /** Limit the frequency of Manifest updates. */ minimumManifestUpdateInterval : number; - /** Interface allowing to load segments */ - segmentFetcherCreator : SegmentFetcherCreator; + /** Interface allowing to interact with the transport protocol */ + transport : ITransportPipelines; + /** Configuration for the segment requesting logic. */ + segmentRequestOptions : { + /** Maximum number of time a request on error will be retried. */ + regularError : number | undefined; + /** Maximum number of time a request be retried when the user is offline. */ + offlineError : number | undefined; + /** + * Amount of time after which a request should be aborted. + * `undefined` indicates that a default value is wanted. + * `-1` indicates no timeout. + */ + requestTimeout : number | undefined; + }; /** Emit the playback rate (speed) set by the user. */ speed : IReadOnlySharedReference; /** The configured starting position. */ @@ -177,14 +194,17 @@ export default function InitializeOnMediaSource( mediaElement, minimumManifestUpdateInterval, playbackObserver, - segmentFetcherCreator, + segmentRequestOptions, speed, startAt, + transport, textTrackOptions } : IInitializeArguments ) : Observable { /** Choose the right "Representation" for a given "Adaptation". */ const representationEstimator = AdaptiveRepresentationSelector(adaptiveOptions); + const contentPlaybackCanceller = new TaskCanceller(); + /** * Create and open a new MediaSource object on the given media element on * subscription. @@ -244,6 +264,21 @@ export default function InitializeOnMediaSource( const initialTime = getInitialTime(manifest, lowLatencyMode, startAt); log.debug("Init: Initial time calculated:", initialTime); + const steeringManifestFetcher = transport.steeringManifest === null ? + null : + new SteeringManifestFetcher(transport.steeringManifest, + { maxRetryOffline: undefined, + maxRetryRegular: undefined }); + const cdnPrioritizer = new CdnPrioritizer(manifest, + steeringManifestFetcher, + contentPlaybackCanceller.signal); + const requestOptions = { lowLatencyMode, + requestTimeout: segmentRequestOptions.requestTimeout, + maxRetryRegular: segmentRequestOptions.regularError, + maxRetryOffline: segmentRequestOptions.offlineError }; + const segmentFetcherCreator = + new SegmentFetcherCreator(transport, cdnPrioritizer, requestOptions); + const mediaSourceLoader = createMediaSourceLoader({ bufferOptions: objectAssign({ textTrackOptions, drmSystemId }, bufferOptions), @@ -358,5 +393,6 @@ export default function InitializeOnMediaSource( } })); - return observableMerge(loadContent$, mediaError$, drmEvents$.pipe(ignoreElements())); + return observableMerge(loadContent$, mediaError$, drmEvents$.pipe(ignoreElements())) + .pipe(finalize(() => { contentPlaybackCanceller.cancel(); })); } diff --git a/src/default_config.ts b/src/default_config.ts index c72338fffd..d4bf3e5bdc 100644 --- a/src/default_config.ts +++ b/src/default_config.ts @@ -395,6 +395,33 @@ const DEFAULT_CONFIG = { */ DEFAULT_MAX_MANIFEST_REQUEST_RETRY: 4, + /** + * The default number of times a Content Steering Manifest request will be + * re-performed when loaded/refreshed if the request finishes on an error + * which justify an retry. + * + * Note that some errors do not use this counter: + * - if the error is not due to the xhr, no retry will be peformed + * - if the error is an HTTP error code, but not a 500-smthg or a 404, no + * retry will be performed. + * - if it has a high chance of being due to the user being offline, a + * separate counter is used (see DEFAULT_MAX_REQUESTS_RETRY_ON_OFFLINE). + * @type Number + */ + DEFAULT_MAX_CONTENT_STEERING_MANIFEST_REQUEST_RETRY: 4, + + /** + * Default delay, in seconds, during which a CDN will be "downgraded". + * + * For example in case of media content being available on multiple CDNs, the + * RxPlayer may decide that a CDN is less reliable (for example, it returned a + * server error) and should thus be avoided, at least for some time + * + * This value is the amount of time this CDN will be "less considered" than the + * alternatives. + */ + DEFAULT_CDN_DOWNGRADE_TIME: 60, + /** * The default number of times a segment request will be re-performed when * on error which justify a retry. diff --git a/src/experimental/tools/VideoThumbnailLoader/video_thumbnail_loader.ts b/src/experimental/tools/VideoThumbnailLoader/video_thumbnail_loader.ts index c18959ca01..8d9104d023 100644 --- a/src/experimental/tools/VideoThumbnailLoader/video_thumbnail_loader.ts +++ b/src/experimental/tools/VideoThumbnailLoader/video_thumbnail_loader.ts @@ -170,6 +170,8 @@ export default class VideoThumbnailLoader { const segmentFetcher = createSegmentFetcher( "video", loader.video, + // TODO implement ContentSteering for the VideoThumbnailLoader? + null, // We don't care about the SegmentFetcher's lifecycle events {}, { baseDelay: 0, diff --git a/src/manifest/manifest.ts b/src/manifest/manifest.ts index cbea93c662..fdc4f90b32 100644 --- a/src/manifest/manifest.ts +++ b/src/manifest/manifest.ts @@ -15,7 +15,10 @@ */ import { MediaError } from "../errors"; -import { IParsedManifest } from "../parsers/manifest"; +import { + IContentSteeringMetadata, + IParsedManifest, +} from "../parsers/manifest"; import { IPlayerError, IRepresentationFilter, @@ -23,6 +26,7 @@ import { import arrayFind from "../utils/array_find"; import EventEmitter from "../utils/event_emitter"; import idGenerator from "../utils/id_generator"; +import { getFilenameIndexInUrl } from "../utils/resolve_url"; import warnOnce from "../utils/warn_once"; import Adaptation from "./adaptation"; import Period, { @@ -241,6 +245,8 @@ export default class Manifest extends EventEmitter { */ public clockOffset : number | undefined; + public contentSteering : IContentSteeringMetadata | null; + /** * Data allowing to calculate the minimum and maximum seekable positions at * any given time. @@ -380,6 +386,7 @@ export default class Manifest extends EventEmitter { this.suggestedPresentationDelay = parsedManifest.suggestedPresentationDelay; this.availabilityStartTime = parsedManifest.availabilityStartTime; this.publishTime = parsedManifest.publishTime; + this.contentSteering = parsedManifest.contentSteering; if (supplementaryImageTracks.length > 0) { this._addSupplementaryImageAdaptations(supplementaryImageTracks); } @@ -612,14 +619,18 @@ export default class Manifest extends EventEmitter { const newImageTracks = _imageTracks.map(({ mimeType, url }) => { const adaptationID = "gen-image-ada-" + generateSupplementaryTrackID(); const representationID = "gen-image-rep-" + generateSupplementaryTrackID(); + const indexOfFilename = getFilenameIndexInUrl(url); + const cdnUrl = url.substring(0, indexOfFilename); + const filename = url.substring(indexOfFilename); const newAdaptation = new Adaptation({ id: adaptationID, type: "image", representations: [{ bitrate: 0, + cdnMetadata: [ { baseUrl: cdnUrl } ], id: representationID, mimeType, index: new StaticRepresentationIndex({ - media: url, + media: filename, }) }] }, { isManuallyAdded: true }); if (newAdaptation.representations.length > 0 && !newAdaptation.isSupported) { @@ -664,6 +675,9 @@ export default class Manifest extends EventEmitter { languages != null ? languages : []; + const indexOfFilename = getFilenameIndexInUrl(url); + const cdnUrl = url.substring(0, indexOfFilename); + const filename = url.substring(indexOfFilename); return allSubs.concat(langsToMapOn.map((_language) => { const adaptationID = "gen-text-ada-" + generateSupplementaryTrackID(); const representationID = "gen-text-rep-" + generateSupplementaryTrackID(); @@ -673,11 +687,12 @@ export default class Manifest extends EventEmitter { closedCaption, representations: [{ bitrate: 0, + cdnMetadata: [{ baseUrl: cdnUrl }], id: representationID, mimeType, codecs, index: new StaticRepresentationIndex({ - media: url, + media: filename, }) }] }, { isManuallyAdded: true }); if (newAdaptation.representations.length > 0 && !newAdaptation.isSupported) { @@ -716,6 +731,7 @@ export default class Manifest extends EventEmitter { this.suggestedPresentationDelay = newManifest.suggestedPresentationDelay; this.transport = newManifest.transport; this.publishTime = newManifest.publishTime; + this.contentSteering = newManifest.contentSteering; if (updateType === MANIFEST_UPDATE_TYPE.Full) { this._timeBounds = newManifest._timeBounds; diff --git a/src/manifest/representation.ts b/src/manifest/representation.ts index 21141e632f..df96a8b7a0 100644 --- a/src/manifest/representation.ts +++ b/src/manifest/representation.ts @@ -17,6 +17,7 @@ import { isCodecSupported } from "../compat"; import log from "../log"; import { + ICdnMetadata, IContentProtections, IParsedRepresentation, } from "../parsers/manifest"; @@ -41,6 +42,18 @@ class Representation { */ public index : IRepresentationIndex; + /** + * Information on the CDN(s) on which requests should be done to request this + * Representation's initialization and media segments. + * + * `null` if there's no CDN involved here (e.g. resources are not requested + * through the network). + * + * An empty array means that no CDN are left to request the resource. As such, + * no resource can be loaded in that situation. + */ + public cdnMetadata : ICdnMetadata[] | null; + /** Bitrate this Representation is in, in bits per seconds. */ public bitrate : number; @@ -129,6 +142,8 @@ class Representation { this.hdrInfo = args.hdrInfo; } + this.cdnMetadata = args.cdnMetadata; + this.index = args.index; this.isSupported = opts.type === "audio" || opts.type === "video" ? diff --git a/src/manifest/representation_index/__tests__/static.test.ts b/src/manifest/representation_index/__tests__/static.test.ts index a71012b10f..f0315327f7 100644 --- a/src/manifest/representation_index/__tests__/static.test.ts +++ b/src/manifest/representation_index/__tests__/static.test.ts @@ -34,7 +34,7 @@ describe("manifest - StaticRepresentationIndex", () => { end: Number.MAX_VALUE, timescale: 1, privateInfos: {}, - mediaURLs: ["foo"] }]); + url: "foo" }]); }); it("should return no first position", () => { diff --git a/src/manifest/representation_index/static.ts b/src/manifest/representation_index/static.ts index e333bfc287..4b47ccc4b5 100644 --- a/src/manifest/representation_index/static.ts +++ b/src/manifest/representation_index/static.ts @@ -28,13 +28,13 @@ export interface IStaticRepresentationIndexInfos { media: string } */ export default class StaticRepresentationIndex implements IRepresentationIndex { /** URL at which the content is available. */ - private readonly _mediaURLs: string; + private readonly _url : string; /** * @param {Object} infos */ constructor(infos : IStaticRepresentationIndexInfos) { - this._mediaURLs = infos.media; + this._url = infos.media; } /** @@ -54,7 +54,7 @@ export default class StaticRepresentationIndex implements IRepresentationIndex { return [{ id: "0", isInit: false, number: 0, - mediaURLs: [this._mediaURLs], + url: this._url, time: 0, end: Number.MAX_VALUE, duration: Number.MAX_VALUE, diff --git a/src/manifest/representation_index/types.ts b/src/manifest/representation_index/types.ts index 17874d4a13..ca9ad29fa1 100644 --- a/src/manifest/representation_index/types.ts +++ b/src/manifest/representation_index/types.ts @@ -50,7 +50,7 @@ export interface ISmoothInitSegmentPrivateInfos { * Supplementary information specific to Smooth media segments (that is, every * segments but the initialization segment). */ -export interface ISmoothSegmentPrivateInfos { +export interface ISmoothMediaSegmentPrivateInfos { /** * Start time of the segment as announced in the Manifest, in the same * timescale than the one indicated through `ISmoothInitSegmentPrivateInfos`. @@ -113,28 +113,28 @@ export interface ILocalManifestSegmentPrivateInfos { */ export interface IPrivateInfos { /** Smooth-specific information allowing to generate an initialization segment. */ - smoothInitSegment? : ISmoothInitSegmentPrivateInfos | undefined; + smoothInitSegment? : ISmoothInitSegmentPrivateInfos; /** Smooth-specific information linked to all Smooth media segments. */ - smoothMediaSegment? : ISmoothSegmentPrivateInfos | undefined; + smoothMediaSegment? : ISmoothMediaSegmentPrivateInfos; /** Information that should be present on all MetaPlaylist segments. */ - metaplaylistInfos? : IMetaPlaylistPrivateInfos | undefined; + metaplaylistInfos? : IMetaPlaylistPrivateInfos; /** * Local Manifest-specific information allowing to request the * initialization segment. */ - localManifestInitSegment? : ILocalManifestInitSegmentPrivateInfos | undefined; + localManifestInitSegment? : ILocalManifestInitSegmentPrivateInfos; /** Local Manifest-specific information allowing to request any media segment. */ - localManifestSegment? : ILocalManifestSegmentPrivateInfos | undefined; + localManifestSegment? : ILocalManifestSegmentPrivateInfos; /** * Function allowing to know if a given emsg's event name has been * explicitely authorized. */ - isEMSGWhitelisted? : ((evt: IEMSG) => boolean) | undefined; + isEMSGWhitelisted? : ((evt: IEMSG) => boolean); } /** Represent a single Segment from a Representation. */ @@ -160,15 +160,10 @@ export interface ISegment { */ isInit : boolean; /** - * URLs where this segment is available. From the most to least prioritary. - * `null` if no URL exists. + * Store supplementary information on a segment that can be later exploited by + * the transport logic. */ - mediaURLs : string[]|null; - /** - * Allows to store supplementary information on a segment that can be later - * exploited by the transport logic. - */ - privateInfos : IPrivateInfos | undefined; + privateInfos : IPrivateInfos; /** * Estimated time, in seconds, at which the concerned segment should be * offseted when decoded. @@ -230,6 +225,29 @@ export interface ISegment { * generated. */ complete : boolean; + /** + * Optional relative or absolute URL to load the resource. + * + * If the URL is absolute (it contains the scheme at the beginning), it is the + * complete URL on which the resource should be requested. + * + * If the URL is relative (it does not contain a scheme at the beginning), it + * should be relative to the chosen CDN that should be recuperated through + * another mean + * + * If `null`, it means either: + * + * - that there is no way to reach the resource through a URL. + * + * - that there may be an URL, but is already communicated through another + * mean, like the currently chosen CDN. + * + * An empty string is equivalent to indicating that the chosen CDN's URL + * should be directly requested instead. + * In that way, it is equivalent to setting it to `null`. + */ + url : string | null; + /** * Optional byte range to retrieve the Segment from its URL(s). * TODO this should probably moved to `privateInfos` as this is diff --git a/src/manifest/update_period_in_place.ts b/src/manifest/update_period_in_place.ts index 54d942711a..2cb8889f75 100644 --- a/src/manifest/update_period_in_place.ts +++ b/src/manifest/update_period_in_place.ts @@ -59,6 +59,7 @@ export default function updatePeriodInPlace(oldPeriod : Period, log.warn(`Manifest: Representation "${oldRepresentations[k].id}" ` + "not found when merging."); } else { + oldRepresentation.cdnMetadata = newRepresentation.cdnMetadata; if (updateType === MANIFEST_UPDATE_TYPE.Full) { oldRepresentation.index._replace(newRepresentation.index); } else { diff --git a/src/parsers/SteeringManifest/DCSM/parse_dcsm.ts b/src/parsers/SteeringManifest/DCSM/parse_dcsm.ts new file mode 100644 index 0000000000..becaa8b01f --- /dev/null +++ b/src/parsers/SteeringManifest/DCSM/parse_dcsm.ts @@ -0,0 +1,67 @@ +/** + * 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 { ISteeringManifest } from "../types"; + +export default function parseDashContentSteeringManifest( + input : string | Partial> +) : [ISteeringManifest, Error[]] { + const warnings : Error[] = []; + let json; + if (typeof input === "string") { + json = JSON.parse(input) as Partial>; + } else { + json = input; + } + + if (json.VERSION !== 1) { + throw new Error("Unhandled DCSM version. Only `1` can be proccessed."); + } + + const initialPriorities = json["SERVICE-LOCATION-PRIORITY"]; + if (!Array.isArray(initialPriorities)) { + throw new Error("The DCSM's SERVICE-LOCATION-URI in in the wrong format"); + } else if (initialPriorities.length === 0) { + warnings.push( + new Error("The DCSM's SERVICE-LOCATION-URI should contain at least one element") + ); + } + + const priorities : string[] = initialPriorities.filter((elt) : elt is string => + typeof elt === "string" + ); + if (priorities.length !== initialPriorities.length) { + warnings.push( + new Error("The DCSM's SERVICE-LOCATION-URI contains URI in a wrong format") + ); + } + let lifetime = 300; + + if (typeof json.TTL === "number") { + lifetime = json.TTL; + } else if (json.TTL !== undefined) { + warnings.push(new Error("The DCSM's TTL in in the wrong format")); + } + + let reloadUri; + if (typeof json["RELOAD-URI"] === "string") { + reloadUri = json["RELOAD-URI"]; + } else if (json["RELOAD-URI"] !== undefined) { + warnings.push(new Error("The DCSM's RELOAD-URI in in the wrong format")); + } + + return [{ lifetime, reloadUri, priorities }, warnings]; +} diff --git a/src/parsers/SteeringManifest/index.ts b/src/parsers/SteeringManifest/index.ts new file mode 100644 index 0000000000..5246b359fe --- /dev/null +++ b/src/parsers/SteeringManifest/index.ts @@ -0,0 +1,18 @@ +/** + * 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. + */ + +export { ISteeringManifest } from "./types"; + diff --git a/src/parsers/SteeringManifest/types.ts b/src/parsers/SteeringManifest/types.ts new file mode 100644 index 0000000000..4b0a2a7332 --- /dev/null +++ b/src/parsers/SteeringManifest/types.ts @@ -0,0 +1,21 @@ +/** + * 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. + */ + +export interface ISteeringManifest { + lifetime: number; + reloadUri? : string | undefined; + priorities : string[]; +} diff --git a/src/parsers/manifest/dash/common/indexes/base.ts b/src/parsers/manifest/dash/common/indexes/base.ts index d5e050b1da..3d40040d15 100644 --- a/src/parsers/manifest/dash/common/indexes/base.ts +++ b/src/parsers/manifest/dash/common/indexes/base.ts @@ -26,10 +26,9 @@ import { IIndexSegment, toIndexTime, } from "../../../utils/index_helpers"; -import { IResolvedBaseUrl } from "../resolve_base_urls"; import getInitSegment from "./get_init_segment"; import getSegmentsFromTimeline from "./get_segments_from_timeline"; -import { createIndexURLs } from "./tokens"; +import { constructRepresentationUrl } from "./tokens"; /** * Index property defined for a SegmentBase RepresentationIndex @@ -53,17 +52,21 @@ export interface IBaseIndex { */ indexTimeOffset : number; /** Information on the initialization segment. */ - initialization? : { - /** URLs to access the initialization segment. */ - mediaURLs: string[] | null; + initialization : { + /** + * URL path, to add to the wanted CDN, to access the initialization segment. + * `null` if no URL exists. + */ + url: string | null; /** possible byte range to request it. */ range?: [number, number] | undefined; } | undefined; /** - * Base URL(s) to access any segment. Can contain tokens to replace to convert - * it to real URLs. + * URL base to access any segment. + * Can contain token to replace to convert it to real URLs. + * `null` if no URL exists. */ - mediaURLs : string[] | null; + segmentUrlTemplate : string | null; /** Number from which the first segments in this index starts with. */ startNumber? : number | undefined; /** Every segments defined in this index. */ @@ -110,8 +113,6 @@ export interface IBaseIndexContextArgument { periodStart : number; /** End of the period concerned by this RepresentationIndex, in seconds. */ periodEnd : number|undefined; - /** Base URL for the Representation concerned. */ - representationBaseURLs : IResolvedBaseUrl[]; /** ID of the Representation concerned. */ representationId? : string | undefined; /** Bitrate of the Representation concerned. */ @@ -185,7 +186,6 @@ export default class BaseRepresentationIndex implements IRepresentationIndex { constructor(index : IBaseIndexIndexArgument, context : IBaseIndexContextArgument) { const { periodStart, periodEnd, - representationBaseURLs, representationId, representationBitrate, isEMSGWhitelisted } = context; @@ -196,13 +196,15 @@ export default class BaseRepresentationIndex implements IRepresentationIndex { const indexTimeOffset = presentationTimeOffset - periodStart * timescale; - const urlSources : string[] = representationBaseURLs.map(b => b.url); - const mediaURLs = createIndexURLs(urlSources, - index.initialization !== undefined ? - index.initialization.media : - undefined, - representationId, - representationBitrate); + const initializationUrl = index.initialization?.media === undefined ? + null : + constructRepresentationUrl(index.initialization.media, + representationId, + representationBitrate); + + const segmentUrlTemplate = index.media === undefined ? + null : + constructRepresentationUrl(index.media, representationId, representationBitrate); // TODO If indexRange is either undefined or behind the initialization segment // the following logic will not work. @@ -217,11 +219,8 @@ export default class BaseRepresentationIndex implements IRepresentationIndex { this._index = { indexRange: index.indexRange, indexTimeOffset, - initialization: { mediaURLs, range }, - mediaURLs: createIndexURLs(urlSources, - index.media, - representationId, - representationBitrate), + initialization: { url: initializationUrl, range }, + segmentUrlTemplate, startNumber: index.startNumber, timeline: index.timeline ?? [], timescale }; diff --git a/src/parsers/manifest/dash/common/indexes/get_init_segment.ts b/src/parsers/manifest/dash/common/indexes/get_init_segment.ts index 9bb5d2104d..8485fd5518 100644 --- a/src/parsers/manifest/dash/common/indexes/get_init_segment.ts +++ b/src/parsers/manifest/dash/common/indexes/get_init_segment.ts @@ -15,6 +15,7 @@ */ import { ISegment } from "../../../../../manifest"; +import { IPrivateInfos } from "../../../../../manifest/representation_index/types"; import { IEMSG } from "../../../../containers/isobmff"; /** @@ -25,7 +26,7 @@ import { IEMSG } from "../../../../containers/isobmff"; */ export default function getInitSegment( index: { timescale: number; - initialization?: { mediaURLs: string[] | null; + initialization?: { url: string | null; range?: [number, number] | undefined; } | undefined; indexRange?: [number, number] | undefined; @@ -33,10 +34,11 @@ export default function getInitSegment( isEMSGWhitelisted?: (inbandEvent: IEMSG) => boolean ) : ISegment { const { initialization } = index; - let privateInfos; + const privateInfos : IPrivateInfos = {}; if (isEMSGWhitelisted !== undefined) { - privateInfos = { isEMSGWhitelisted }; + privateInfos.isEMSGWhitelisted = isEMSGWhitelisted; } + return { id: "init", isInit: true, time: 0, @@ -46,7 +48,7 @@ export default function getInitSegment( range: initialization != null ? initialization.range : undefined, indexRange: index.indexRange, - mediaURLs: initialization?.mediaURLs ?? null, + url: initialization?.url ?? null, complete: true, privateInfos, timestampOffset: -(index.indexTimeOffset / index.timescale) }; diff --git a/src/parsers/manifest/dash/common/indexes/get_segments_from_timeline.ts b/src/parsers/manifest/dash/common/indexes/get_segments_from_timeline.ts index d344ed0bde..bb5c87872a 100644 --- a/src/parsers/manifest/dash/common/indexes/get_segments_from_timeline.ts +++ b/src/parsers/manifest/dash/common/indexes/get_segments_from_timeline.ts @@ -53,7 +53,7 @@ function getWantedRepeatIndex( */ export default function getSegmentsFromTimeline( index : { availabilityTimeComplete? : boolean | undefined; - mediaURLs : string[] | null; + segmentUrlTemplate : string | null; startNumber? : number | undefined; timeline : IIndexSegment[]; timescale : number; @@ -65,7 +65,7 @@ export default function getSegmentsFromTimeline( ) : ISegment[] { const scaledUp = toIndexTime(from, index); const scaledTo = toIndexTime(from + durationWanted, index); - const { timeline, timescale, mediaURLs, startNumber } = index; + const { timeline, timescale, segmentUrlTemplate, startNumber } = index; let currentNumber = startNumber ?? 1; const segments : ISegment[] = []; @@ -85,9 +85,9 @@ export default function getSegmentsFromTimeline( while (segmentTime < scaledTo && segmentNumberInCurrentRange <= repeat) { const segmentNumber = currentNumber + segmentNumberInCurrentRange; - const detokenizedURLs = mediaURLs === null ? + const detokenizedURL = segmentUrlTemplate === null ? null : - mediaURLs.map(createDashUrlDetokenizer(segmentTime, segmentNumber)); + createDashUrlDetokenizer(segmentTime, segmentNumber)(segmentUrlTemplate); let time = segmentTime - index.indexTimeOffset; let realDuration = duration; @@ -103,7 +103,7 @@ export default function getSegmentsFromTimeline( isInit: false, range, timescale: 1 as const, - mediaURLs: detokenizedURLs, + url: detokenizedURL, number: segmentNumber, timestampOffset: -(index.indexTimeOffset / timescale), complete, diff --git a/src/parsers/manifest/dash/common/indexes/list.ts b/src/parsers/manifest/dash/common/indexes/list.ts index 41b945bbf7..a73ce5dbce 100644 --- a/src/parsers/manifest/dash/common/indexes/list.ts +++ b/src/parsers/manifest/dash/common/indexes/list.ts @@ -21,9 +21,8 @@ import { } from "../../../../../manifest"; import { IEMSG } from "../../../../containers/isobmff"; import { getTimescaledRange } from "../../../utils/index_helpers"; -import { IResolvedBaseUrl } from "../resolve_base_urls"; import getInitSegment from "./get_init_segment"; -import { createIndexURLs } from "./tokens"; +import { constructRepresentationUrl } from "./tokens"; /** * Index property defined for a SegmentList RepresentationIndex @@ -53,18 +52,21 @@ export interface IListIndex { indexTimeOffset : number; /** Information on the initialization segment. */ initialization? : { - /** URLs to access the initialization segment. */ - mediaURLs: string[] | null; + /** + * URL path, to add to the wanted CDN, to access the initialization segment. + * `null` if no URL exists. + */ + url: string | null; /** possible byte range to request it. */ range?: [number, number] | undefined; } | undefined; /** Information on the list of segments for this index. */ list: Array<{ /** - * URLs of the segment. + * URL path, to add to the wanted CDN, to access this media segment. * `null` if no URL exists. */ - mediaURLs : string[] | null; + url : string | null; /** Possible byte-range of the segment. */ mediaRange? : [number, number] | undefined; }>; @@ -111,8 +113,6 @@ export interface IListIndexContextArgument { periodStart : number; /** End of the period concerned by this RepresentationIndex, in seconds. */ periodEnd : number | undefined; - /** Base URL for the Representation concerned. */ - representationBaseURLs : IResolvedBaseUrl[]; /** ID of the Representation concerned. */ representationId? : string | undefined; /** Bitrate of the Representation concerned. */ @@ -142,7 +142,6 @@ export default class ListRepresentationIndex implements IRepresentationIndex { const { periodStart, periodEnd, - representationBaseURLs, representationId, representationBitrate, isEMSGWhitelisted } = context; @@ -155,12 +154,15 @@ export default class ListRepresentationIndex implements IRepresentationIndex { const timescale = index.timescale ?? 1; const indexTimeOffset = presentationTimeOffset - periodStart * timescale; - const urlSources : string[] = representationBaseURLs.map(b => b.url); - const list = index.list.map((lItem) => ({ - mediaURLs: createIndexURLs(urlSources, - lItem.media, + const initializationUrl = index.initialization?.media === undefined ? + null : + constructRepresentationUrl(index.initialization.media, representationId, - representationBitrate), + representationBitrate); + const list = index.list.map((lItem) => ({ + url: lItem.media === undefined ? + null : + constructRepresentationUrl(lItem.media, representationId, representationBitrate), mediaRange: lItem.mediaRange })); this._index = { list, timescale, @@ -169,10 +171,7 @@ export default class ListRepresentationIndex implements IRepresentationIndex { indexRange: index.indexRange, initialization: index.initialization == null ? undefined : - { mediaURLs: createIndexURLs(urlSources, - index.initialization.media, - representationId, - representationBitrate), + { url: initializationUrl, range: index.initialization.range } }; } @@ -206,7 +205,7 @@ export default class ListRepresentationIndex implements IRepresentationIndex { let i = Math.floor(up / duration); while (i <= length) { const range = list[i].mediaRange; - const mediaURLs = list[i].mediaURLs; + const url = list[i].url; const time = i * durationInSeconds + this._periodStart; const segment = { id: String(i), @@ -216,7 +215,7 @@ export default class ListRepresentationIndex implements IRepresentationIndex { duration: durationInSeconds, timescale: 1 as const, end: time + durationInSeconds, - mediaURLs, + url, timestampOffset: -(index.indexTimeOffset / timescale), complete: true, privateInfos: { isEMSGWhitelisted: diff --git a/src/parsers/manifest/dash/common/indexes/template.ts b/src/parsers/manifest/dash/common/indexes/template.ts index 61a99c5fdd..8bf3541cc3 100644 --- a/src/parsers/manifest/dash/common/indexes/template.ts +++ b/src/parsers/manifest/dash/common/indexes/template.ts @@ -22,11 +22,10 @@ import { import assert from "../../../../../utils/assert"; import { IEMSG } from "../../../../containers/isobmff"; import ManifestBoundsCalculator from "../manifest_bounds_calculator"; -import { IResolvedBaseUrl } from "../resolve_base_urls"; import getInitSegment from "./get_init_segment"; import { createDashUrlDetokenizer, - createIndexURLs, + constructRepresentationUrl, } from "./tokens"; import { getSegmentTimeRoundingError } from "./utils"; @@ -52,8 +51,10 @@ export interface ITemplateIndex { indexRange?: [number, number] | undefined; /** Information on the initialization segment. */ initialization? : { - /** URLs to access the initialization segment. */ - mediaURLs: string[] | null; + /** + * URL path, to add to the wanted CDN, to access the initialization segment. + */ + url: string | null; /** possible byte range to request it. */ range?: [number, number] | undefined; } | undefined; @@ -62,7 +63,7 @@ export interface ITemplateIndex { * Can contain token to replace to convert it to real URLs. * `null` if no URL exists. */ - mediaURLs : string[] | null; + url : string | null; /** * Temporal offset, in the current timescale (see timescale), to add to the * presentation time (time a segment has at decoding time) to obtain the @@ -124,8 +125,6 @@ export interface ITemplateIndexContextArgument { periodEnd : number|undefined; /** Whether the corresponding Manifest can be updated and changed. */ isDynamic : boolean; - /** Base URL for the Representation concerned. */ - representationBaseURLs : IResolvedBaseUrl[]; /** ID of the Representation concerned. */ representationId? : string | undefined; /** Bitrate of the Representation concerned. */ @@ -174,18 +173,12 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex isDynamic, periodEnd, periodStart, - representationBaseURLs, representationId, representationBitrate, isEMSGWhitelisted } = context; const timescale = index.timescale ?? 1; - const minBaseUrlAto = representationBaseURLs.length === 0 ? - 0 : - representationBaseURLs.reduce((acc, rbu) => { - return Math.min(acc, rbu.availabilityTimeOffset); - }, Infinity); - this._availabilityTimeOffset = availabilityTimeOffset + minBaseUrlAto; + this._availabilityTimeOffset = availabilityTimeOffset; this._manifestBoundsCalculator = manifestBoundsCalculator; this._aggressiveMode = aggressiveMode; @@ -200,22 +193,25 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex throw new Error("Invalid SegmentTemplate: no duration"); } - const urlSources : string[] = representationBaseURLs.map(b => b.url); + const initializationUrl = index.initialization?.media === undefined ? + null : + constructRepresentationUrl(index.initialization.media, + representationId, + representationBitrate); + + const segmentUrlTemplate = index.media === undefined ? + null : + constructRepresentationUrl(index.media, representationId, representationBitrate); + this._index = { duration: index.duration, timescale, indexRange: index.indexRange, indexTimeOffset, initialization: index.initialization == null ? undefined : - { mediaURLs: createIndexURLs(urlSources, - index.initialization.media, - representationId, - representationBitrate), + { url: initializationUrl, range: index.initialization.range }, - mediaURLs: createIndexURLs(urlSources, - index.media, - representationId, - representationBitrate), + url: segmentUrlTemplate, presentationTimeOffset, startNumber: index.startNumber }; this._isDynamic = isDynamic; @@ -244,7 +240,7 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex const { duration, startNumber, timescale, - mediaURLs } = index; + url } = index; const scaledStart = this._periodStart * timescale; const scaledEnd = this._scaledRelativePeriodEnd; @@ -288,9 +284,9 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex const realTime = timeFromPeriodStart + scaledStart; const manifestTime = timeFromPeriodStart + this._index.presentationTimeOffset; - const detokenizedURLs = mediaURLs === null ? + const detokenizedURL = url === null ? null : - mediaURLs.map(createDashUrlDetokenizer(manifestTime, realNumber)); + createDashUrlDetokenizer(manifestTime, realNumber)(url); const args = { id: String(realNumber), number: realNumber, @@ -300,7 +296,7 @@ export default class TemplateRepresentationIndex implements IRepresentationIndex timescale: 1 as const, isInit: false, scaledDuration: realDuration / timescale, - mediaURLs: detokenizedURLs, + url: detokenizedURL, timestampOffset: -(index.indexTimeOffset / timescale), complete: true, privateInfos: { 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 8ca09f9010..32162aeb8a 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 @@ -37,10 +37,9 @@ import isSegmentStillAvailable from "../../../../utils/is_segment_still_availabl import updateSegmentTimeline from "../../../../utils/update_segment_timeline"; import { ISegmentTimelineElement } from "../../../node_parser_types"; import ManifestBoundsCalculator from "../../manifest_bounds_calculator"; -import { IResolvedBaseUrl } from "../../resolve_base_urls"; import getInitSegment from "../get_init_segment"; import getSegmentsFromTimeline from "../get_segments_from_timeline"; -import { createIndexURLs } from "../tokens"; +import { constructRepresentationUrl } from "../tokens"; import { getSegmentTimeRoundingError } from "../utils"; import constructTimelineFromElements from "./construct_timeline_from_elements"; // eslint-disable-next-line max-len @@ -71,16 +70,22 @@ export interface ITimelineIndex { indexTimeOffset : number; /** Information on the initialization segment. */ initialization? : { - /** URLs to access the initialization segment. */ - mediaURLs: string[] | null; + /** + * URL path, to add to the wanted CDN, to access the initialization segment. + * `null` if no URL exists. + */ + url: string | null; /** possible byte range to request it. */ range?: [number, number] | undefined; } | undefined; /** - * Base URL(s) to access any segment. Can contain tokens to replace to convert - * it to real URLs. + * Template for the URL suffix (to concatenate to the wanted CDN), to access any + * media segment. + * Can contain tokens to replace to convert it to real URLs. + * + * `null` if no URL exists. */ - mediaURLs : string[] | null ; + segmentUrlTemplate : string | null ; /** Number from which the first segments in this index starts with. */ startNumber? : number | undefined; /** @@ -158,8 +163,6 @@ export interface ITimelineIndexContextArgument { * index was received */ receivedTime? : number | undefined; - /** Base URL for the Representation concerned. */ - representationBaseURLs : IResolvedBaseUrl[]; /** ID of the Representation concerned. */ representationId? : string | undefined; /** Bitrate of the Representation concerned. */ @@ -247,7 +250,6 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex manifestBoundsCalculator, isDynamic, isLastPeriod, - representationBaseURLs, representationId, representationBitrate, periodStart, @@ -284,23 +286,25 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex this._isDynamic = isDynamic; this._parseTimeline = index.timelineParser ?? null; - const urlSources : string[] = representationBaseURLs.map(b => b.url); + const initializationUrl = index.initialization?.media === undefined ? + null : + constructRepresentationUrl(index.initialization.media, + representationId, + representationBitrate); + + const segmentUrlTemplate = index.media === undefined ? + null : + constructRepresentationUrl(index.media, representationId, representationBitrate); this._index = { availabilityTimeComplete, indexRange: index.indexRange, indexTimeOffset, initialization: index.initialization == null ? undefined : { - mediaURLs: createIndexURLs(urlSources, - index.initialization.media, - representationId, - representationBitrate), + url: initializationUrl, range: index.initialization.range, }, - mediaURLs: createIndexURLs(urlSources, - index.media, - representationId, - representationBitrate), + segmentUrlTemplate, startNumber: index.startNumber, timeline: index.timeline ?? null, timescale }; @@ -330,12 +334,12 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex } // destructuring to please TypeScript - const { mediaURLs, + const { segmentUrlTemplate, startNumber, timeline, timescale, indexTimeOffset } = this._index; - return getSegmentsFromTimeline({ mediaURLs, + return getSegmentsFromTimeline({ segmentUrlTemplate, startNumber, timeline, timescale, @@ -348,8 +352,6 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex /** * Returns true if the index should be refreshed. - * @param {Number} _up - * @param {Number} to * @returns {Boolean} */ shouldRefresh() : false { diff --git a/src/parsers/manifest/dash/common/indexes/tokens.ts b/src/parsers/manifest/dash/common/indexes/tokens.ts index f6d3dd7080..a0c4a7ed37 100644 --- a/src/parsers/manifest/dash/common/indexes/tokens.ts +++ b/src/parsers/manifest/dash/common/indexes/tokens.ts @@ -15,7 +15,6 @@ */ import isNonEmptyString from "../../../../../utils/is_non_empty_string"; -import resolveURL from "../../../../../utils/resolve_url"; /** * Pad with 0 in the left of the given n argument to reach l length @@ -47,27 +46,17 @@ function processFormatedToken( } /** - * @param {string} baseURLs - * @param {string|undefined} media - * @param {string|undefined} id + * @param {string} urlTemplate + * @param {string|undefined} representationId * @param {number|undefined} bitrate * @returns {string} */ -export function createIndexURLs( - baseURLs : string[], - media?: string, - id?: string, +export function constructRepresentationUrl( + urlTemplate : string, + representationId?: string, bitrate?: number -): string[] | null { - if (baseURLs.length === 0) { - return media !== undefined ? [replaceRepresentationDASHTokens(media, id, bitrate)] : - null; - } - return baseURLs.map(baseURL => { - return replaceRepresentationDASHTokens(resolveURL(baseURL, media), - id, - bitrate); - }); +): string { + return replaceRepresentationDASHTokens(urlTemplate, representationId, bitrate); } /** diff --git a/src/parsers/manifest/dash/common/parse_mpd.ts b/src/parsers/manifest/dash/common/parse_mpd.ts index 90b2be974e..b12abc8b7e 100644 --- a/src/parsers/manifest/dash/common/parse_mpd.ts +++ b/src/parsers/manifest/dash/common/parse_mpd.ts @@ -18,8 +18,11 @@ import config from "../../../../config"; import log from "../../../../log"; import Manifest from "../../../../manifest"; import arrayFind from "../../../../utils/array_find"; -import { normalizeBaseURL } from "../../../../utils/resolve_url"; -import { IParsedManifest } from "../../types"; +import { getFilenameIndexInUrl } from "../../../../utils/resolve_url"; +import { + IContentSteeringMetadata, + IParsedManifest, +} from "../../types"; import { IMPDIntermediateRepresentation, IPeriodIntermediateRepresentation, @@ -241,9 +244,7 @@ function parseCompleteIntermediateRepresentation( attributes: rootAttributes } = mpdIR; const isDynamic : boolean = rootAttributes.type === "dynamic"; const initialBaseUrl : IResolvedBaseUrl[] = args.url !== undefined ? - [{ url: normalizeBaseURL(args.url), - availabilityTimeOffset: 0, - availabilityTimeComplete: true }] : + [{ url: args.url.substring(0, getFilenameIndexInUrl(args.url)) }] : []; const mpdBaseUrls = resolveBaseURLs(initialBaseUrl, rootChildren.baseURLs); const availabilityStartTime = parseAvailabilityStartTime(rootAttributes, @@ -275,6 +276,16 @@ function parseCompleteIntermediateRepresentation( livePosition : number | undefined; time : number; }; + let contentSteering : IContentSteeringMetadata | null = null; + if (rootChildren.contentSteering !== undefined) { + const { attributes } = rootChildren.contentSteering; + contentSteering = { url: rootChildren.contentSteering.value, + defaultId: attributes.defaultServiceLocation, + queryBeforeStart: attributes.queryBeforeStart === true, + proxyUrl: attributes.proxyServerUrl }; + + } + if (rootAttributes.minimumUpdatePeriod !== undefined && rootAttributes.minimumUpdatePeriod >= 0) { @@ -369,6 +380,7 @@ function parseCompleteIntermediateRepresentation( const parsedMPD : IParsedManifest = { availabilityStartTime, clockOffset: args.externalClockOffset, + contentSteering, isDynamic, isLive: isDynamic, isLastPeriodKnown, diff --git a/src/parsers/manifest/dash/common/parse_representation_index.ts b/src/parsers/manifest/dash/common/parse_representation_index.ts index 0bd6a561dc..b1a107409a 100644 --- a/src/parsers/manifest/dash/common/parse_representation_index.ts +++ b/src/parsers/manifest/dash/common/parse_representation_index.ts @@ -33,9 +33,7 @@ import { TimelineRepresentationIndex, } from "./indexes"; import ManifestBoundsCalculator from "./manifest_bounds_calculator"; -import resolveBaseURLs, { - IResolvedBaseUrl, -} from "./resolve_base_urls"; +import { IResolvedBaseUrl } from "./resolve_base_urls"; /** * Parse the specific segment indexing information found in a representation @@ -48,8 +46,6 @@ export default function parseRepresentationIndex( representation : IRepresentationIntermediateRepresentation, context : IRepresentationIndexContext ) : IRepresentationIndex { - const representationBaseURLs = resolveBaseURLs(context.baseURLs, - representation.children.baseURLs); const { aggressiveMode, availabilityTimeOffset, manifestBoundsCalculator, @@ -80,7 +76,6 @@ export default function parseRepresentationIndex( periodEnd, periodStart, receivedTime, - representationBaseURLs, representationBitrate: representation.attributes.bitrate, representationId: representation.attributes.id, timeShiftBufferDepth }; diff --git a/src/parsers/manifest/dash/common/parse_representations.ts b/src/parsers/manifest/dash/common/parse_representations.ts index 12371b5745..b8d17bfd06 100644 --- a/src/parsers/manifest/dash/common/parse_representations.ts +++ b/src/parsers/manifest/dash/common/parse_representations.ts @@ -20,6 +20,7 @@ import { IHDRInformation } from "../../../../public_types"; import arrayFind from "../../../../utils/array_find"; import objectAssign from "../../../../utils/object_assign"; import { + ICdnMetadata, IContentProtections, IParsedRepresentation, } from "../../types"; @@ -33,6 +34,7 @@ import { getWEBMHDRInformation } from "./get_hdr_information"; import parseRepresentationIndex, { IRepresentationIndexContext, } from "./parse_representation_index"; +import resolveBaseURLs from "./resolve_base_urls"; /** * Combine inband event streams from representation and @@ -159,9 +161,16 @@ export default function parseRepresentations( representationBitrate = representation.attributes.bitrate; } + const representationBaseURLs = resolveBaseURLs(context.baseURLs, + representation.children.baseURLs); + const cdnMetadata : ICdnMetadata[] = representationBaseURLs.map(x => + ({ baseUrl: x.url, id: x.serviceLocation })); + + // Construct Representation Base const parsedRepresentation : IParsedRepresentation = { bitrate: representationBitrate, + cdnMetadata, index: representationIndex, id: representationID }; diff --git a/src/parsers/manifest/dash/common/resolve_base_urls.ts b/src/parsers/manifest/dash/common/resolve_base_urls.ts index c670d3ab29..35ca7c494f 100644 --- a/src/parsers/manifest/dash/common/resolve_base_urls.ts +++ b/src/parsers/manifest/dash/common/resolve_base_urls.ts @@ -19,8 +19,7 @@ import { IBaseUrlIntermediateRepresentation } from "../node_parser_types"; export interface IResolvedBaseUrl { url : string; - availabilityTimeOffset : number; - availabilityTimeComplete : boolean; + serviceLocation? : string | undefined; } /** @@ -38,8 +37,7 @@ export default function resolveBaseURLs( const newBaseUrls : IResolvedBaseUrl[] = newBaseUrlsIR.map(ir => { return { url: ir.value, - availabilityTimeOffset: ir.attributes.availabilityTimeOffset ?? 0, - availabilityTimeComplete: ir.attributes.availabilityTimeComplete ?? true }; + serviceLocation: ir.attributes.serviceLocation }; }); if (currentBaseURLs.length === 0) { return newBaseUrls; @@ -51,11 +49,10 @@ export default function resolveBaseURLs( for (let j = 0; j < newBaseUrls.length; j++) { const newBaseUrl = newBaseUrls[j]; const newUrl = resolveURL(curBaseUrl.url, newBaseUrl.url); - const newAvailabilityTimeOffset = curBaseUrl.availabilityTimeOffset + - newBaseUrl.availabilityTimeOffset; result.push({ url: newUrl, - availabilityTimeOffset: newAvailabilityTimeOffset, - availabilityTimeComplete: newBaseUrl.availabilityTimeComplete }); + serviceLocation: newBaseUrl.serviceLocation ?? + curBaseUrl.serviceLocation }); + } } return result; diff --git a/src/parsers/manifest/dash/js-parser/node_parsers/BaseURL.ts b/src/parsers/manifest/dash/js-parser/node_parsers/BaseURL.ts index 6d4d33dd79..80678ab46c 100644 --- a/src/parsers/manifest/dash/js-parser/node_parsers/BaseURL.ts +++ b/src/parsers/manifest/dash/js-parser/node_parsers/BaseURL.ts @@ -15,27 +15,19 @@ */ import { IBaseUrlIntermediateRepresentation } from "../../node_parser_types"; -import { - parseBoolean, - parseMPDFloat, - ValueParser, -} from "./utils"; /** * Parse an BaseURL element into an BaseURL intermediate * representation. - * @param {Element} adaptationSetElement - The BaseURL root element. + * @param {Element} root - The BaseURL root element. * @returns {Array.} */ export default function parseBaseURL( root: Element ) : [IBaseUrlIntermediateRepresentation | undefined, Error[]] { - const attributes : { availabilityTimeOffset?: number; - availabilityTimeComplete?: boolean; } = {}; + const attributes : { serviceLocation? : string } = {}; const value = root.textContent; - const warnings : Error[] = []; - const parseValue = ValueParser(attributes, warnings); if (value === null || value.length === 0) { return [undefined, warnings]; } @@ -43,16 +35,8 @@ export default function parseBaseURL( const attribute = root.attributes[i]; switch (attribute.name) { - case "availabilityTimeOffset": - parseValue(attribute.value, { asKey: "availabilityTimeOffset", - parser: parseMPDFloat, - dashName: "availabilityTimeOffset" }); - break; - - case "availabilityTimeComplete": - parseValue(attribute.value, { asKey: "availabilityTimeComplete", - parser: parseBoolean, - dashName: "availabilityTimeComplete" }); + case "serviceLocation": + attributes.serviceLocation = attribute.value; break; } } diff --git a/src/parsers/manifest/dash/js-parser/node_parsers/ContentSteering.ts b/src/parsers/manifest/dash/js-parser/node_parsers/ContentSteering.ts new file mode 100644 index 0000000000..fe4a9bda87 --- /dev/null +++ b/src/parsers/manifest/dash/js-parser/node_parsers/ContentSteering.ts @@ -0,0 +1,63 @@ +/** + * 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 { IContentSteeringIntermediateRepresentation } from "../../node_parser_types"; +import { + parseBoolean, + ValueParser, +} from "./utils"; + +/** + * Parse an ContentSteering element into an ContentSteering intermediate + * representation. + * @param {Element} root - The ContentSteering root element. + * @returns {Array.} + */ +export default function parseContentSteering( + root: Element +) : [IContentSteeringIntermediateRepresentation | undefined, Error[]] { + const attributes : { defaultServiceLocation? : string; + queryBeforeStart? : boolean; + proxyServerUrl? : string; } = {}; + const value = root.textContent; + const warnings : Error[] = []; + if (value === null || value.length === 0) { + return [undefined, warnings]; + } + const parseValue = ValueParser(attributes, warnings); + for (let i = 0; i < root.attributes.length; i++) { + const attribute = root.attributes[i]; + + switch (attribute.name) { + case "defaultServiceLocation": + attributes.defaultServiceLocation = attribute.value; + break; + + case "queryBeforeStart": + parseValue(attribute.value, { asKey: "queryBeforeStart", + parser: parseBoolean, + dashName: "queryBeforeStart" }); + break; + + case "proxyServerUrl": + attributes.proxyServerUrl = attribute.value; + break; + } + } + + return [ { value, attributes }, + warnings ]; +} diff --git a/src/parsers/manifest/dash/js-parser/node_parsers/MPD.ts b/src/parsers/manifest/dash/js-parser/node_parsers/MPD.ts index 6330c71529..6c50e820d6 100644 --- a/src/parsers/manifest/dash/js-parser/node_parsers/MPD.ts +++ b/src/parsers/manifest/dash/js-parser/node_parsers/MPD.ts @@ -16,6 +16,7 @@ import { IBaseUrlIntermediateRepresentation, + IContentSteeringIntermediateRepresentation, IMPDAttributes, IMPDChildren, IMPDIntermediateRepresentation, @@ -23,6 +24,7 @@ import { IScheme, } from "../../node_parser_types"; import parseBaseURL from "./BaseURL"; +import parseContentSteering from "./ContentSteering"; import { createPeriodIntermediateRepresentation, } from "./Period"; @@ -45,6 +47,7 @@ function parseMPDChildren( const locations : string[] = []; const periods : IPeriodIntermediateRepresentation[] = []; const utcTimings : IScheme[] = []; + let contentSteering : IContentSteeringIntermediateRepresentation | undefined; let warnings : Error[] = []; for (let i = 0; i < mpdChildren.length; i++) { @@ -61,6 +64,13 @@ function parseMPDChildren( warnings = warnings.concat(baseURLWarnings); break; + case "ContentSteering": + const [ contentSteeringObj, + contentSteeringWarnings ] = parseContentSteering(currentNode); + contentSteering = contentSteeringObj; + warnings = warnings.concat(contentSteeringWarnings); + break; + case "Location": locations.push(currentNode.textContent === null ? "" : @@ -82,7 +92,7 @@ function parseMPDChildren( } } - return [ { baseURLs, locations, periods, utcTimings }, + return [ { baseURLs, contentSteering, locations, periods, utcTimings }, warnings ]; } diff --git a/src/parsers/manifest/dash/js-parser/node_parsers/__tests__/AdaptationSet.test.ts b/src/parsers/manifest/dash/js-parser/node_parsers/__tests__/AdaptationSet.test.ts index b85141334f..caf53cf96a 100644 --- a/src/parsers/manifest/dash/js-parser/node_parsers/__tests__/AdaptationSet.test.ts +++ b/src/parsers/manifest/dash/js-parser/node_parsers/__tests__/AdaptationSet.test.ts @@ -359,13 +359,13 @@ describe("DASH Node Parsers - AdaptationSet", () => { it("should correctly parse a non-empty baseURLs", () => { const element1 = new DOMParser() // eslint-disable-next-line max-len - .parseFromString("a", "text/xml") + .parseFromString("a", "text/xml") .childNodes[0] as Element; expect(createAdaptationSetIntermediateRepresentation(element1)) .toEqual([ { attributes: {}, - children: { baseURLs: [{ attributes: { availabilityTimeOffset: Infinity }, + children: { baseURLs: [{ attributes: { serviceLocation: "foo" }, value: "a" }], representations: [] }, }, @@ -375,14 +375,14 @@ describe("DASH Node Parsers - AdaptationSet", () => { const element2 = new DOMParser() .parseFromString( // eslint-disable-next-line max-len - "foo bar", + "foo bar", "text/xml" ).childNodes[0] as Element; expect(createAdaptationSetIntermediateRepresentation(element2)) .toEqual([ { attributes: {}, - children: { baseURLs: [{ attributes: { availabilityTimeOffset: 4 }, + children: { baseURLs: [{ attributes: { serviceLocation: "4" }, value: "foo bar" }], representations: [] }, }, @@ -393,15 +393,15 @@ describe("DASH Node Parsers - AdaptationSet", () => { it("should correctly parse multiple non-empty baseURLs", () => { const element1 = new DOMParser() // eslint-disable-next-line max-len - .parseFromString("ab", "text/xml") + .parseFromString("ab", "text/xml") .childNodes[0] as Element; expect(createAdaptationSetIntermediateRepresentation(element1)) .toEqual([ { attributes: {}, - children: { baseURLs: [ { attributes: { availabilityTimeOffset: Infinity }, + children: { baseURLs: [ { attributes: { serviceLocation: "" }, value: "a" }, - { attributes: { availabilityTimeOffset: 12 }, + { attributes: { serviceLocation: "http://test.com" }, value: "b" } ], representations: [] }, }, diff --git a/src/parsers/manifest/dash/node_parser_types.ts b/src/parsers/manifest/dash/node_parser_types.ts index fc30d82ebb..a756d71419 100644 --- a/src/parsers/manifest/dash/node_parser_types.ts +++ b/src/parsers/manifest/dash/node_parser_types.ts @@ -41,6 +41,11 @@ export interface IMPDChildren { * from the first encountered to the last encountered. */ baseURLs : IBaseUrlIntermediateRepresentation[]; + /** + * Information on a potential Content Steering Manifest linked to this + * content. + */ + contentSteering? : IContentSteeringIntermediateRepresentation | undefined; /** * Location(s) at which the Manifest can be refreshed. * @@ -371,9 +376,39 @@ export interface IBaseUrlIntermediateRepresentation { /** Attributes assiociated to the BaseURL node. */ attributes: { - /** availabilityTimeOffset attribute assiociated to that BaseURL node. */ - availabilityTimeOffset?: number; - availabilityTimeComplete?: boolean; + /** + * Potential value for a `serviceLocation` attribute, used in content + * steering mechanisms. + */ + serviceLocation? : string; + }; +} + +/** Intermediate representation for a ContentSteering node. */ +export interface IContentSteeringIntermediateRepresentation { + /** + * The Content Steering Manifest's URL. + * + * This is the inner content of a ContentSteering node. + */ + value: string; + + /** Attributes assiociated to the ContentSteering node. */ + attributes: { + /** Default ServiceLocation to be used. */ + defaultServiceLocation? : string; + /** + * If `true`, the Content Steering Manifest should be loaded before the + * first resources depending on it are loaded. + */ + queryBeforeStart? : boolean; + /** + * If set, a proxy URL has been configured. + * Requests for the Content Steering Manifest should actually go through + * this proxy, the node URL being added to an `url` query parameter + * alongside potential other query parameters. + */ + proxyServerUrl? : string; }; } diff --git a/src/parsers/manifest/dash/wasm-parser/rs/events.rs b/src/parsers/manifest/dash/wasm-parser/rs/events.rs index f22ce13ad2..620d5c76f4 100644 --- a/src/parsers/manifest/dash/wasm-parser/rs/events.rs +++ b/src/parsers/manifest/dash/wasm-parser/rs/events.rs @@ -90,6 +90,9 @@ pub enum TagName { /// Indicate a node SegmentUrl = 20, + + /// Indicate a node + ContentSteering = 21 } #[derive(PartialEq, Clone, Copy)] @@ -280,6 +283,14 @@ pub enum AttributeName { Namespace = 70, Label = 71, // String + + ServiceLocation = 72, // String + + QueryBeforeStart = 73, // Boolean + + ProxyServerUrl = 74, // String + + DefaultServiceLocation = 75, } impl TagName { diff --git a/src/parsers/manifest/dash/wasm-parser/rs/processor/attributes.rs b/src/parsers/manifest/dash/wasm-parser/rs/processor/attributes.rs index 242a810d60..9e9d6282fe 100644 --- a/src/parsers/manifest/dash/wasm-parser/rs/processor/attributes.rs +++ b/src/parsers/manifest/dash/wasm-parser/rs/processor/attributes.rs @@ -142,14 +142,21 @@ pub fn report_base_url_attrs(tag_bs : &quick_xml::events::BytesStart) { for res_attr in tag_bs.attributes() { match res_attr { Ok(attr) => match attr.key { - b"availabilityTimeOffset" => { - match attr.value.as_ref() { - b"INF" => AvailabilityTimeOffset.report(f64::INFINITY), - _ => AvailabilityTimeOffset.try_report_as_u64(&attr), - } - }, - b"availabilityTimeComplete" => - AvailabilityTimeComplete.try_report_as_bool(&attr), + b"serviceLocation" => ServiceLocation.try_report_as_string(&attr), + _ => {}, + }, + Err(err) => ParsingError::from(err).report_err(), + }; + }; +} + +pub fn report_content_steering_attrs(tag_bs : &quick_xml::events::BytesStart) { + for res_attr in tag_bs.attributes() { + match res_attr { + Ok(attr) => match attr.key { + b"serviceLocation" => ServiceLocation.try_report_as_string(&attr), + b"proxyServerUrl" => ProxyServerUrl.try_report_as_string(&attr), + b"queryBeforeStart" => QueryBeforeStart.try_report_as_bool(&attr), _ => {}, }, Err(err) => ParsingError::from(err).report_err(), diff --git a/src/parsers/manifest/dash/wasm-parser/rs/processor/mod.rs b/src/parsers/manifest/dash/wasm-parser/rs/processor/mod.rs index bd5989ee1c..aa272a5118 100644 --- a/src/parsers/manifest/dash/wasm-parser/rs/processor/mod.rs +++ b/src/parsers/manifest/dash/wasm-parser/rs/processor/mod.rs @@ -112,6 +112,11 @@ impl MPDProcessor { attributes::report_base_url_attrs(&tag); self.process_base_url_element(); }, + b"ContentSteering" => { + TagName::ContentSteering.report_tag_open(); + attributes::report_content_steering_attrs(&tag); + self.process_content_steering_element(); + }, b"cenc:pssh" => self.process_cenc_element(), b"Location" => self.process_location_element(), b"Label" => self.process_label_element(), @@ -335,6 +340,43 @@ impl MPDProcessor { } } + fn process_content_steering_element(&mut self) { + // Count inner ContentSteering tags if it exists. + // Allowing to not close the current node when it is an inner that is closed + let mut inner_tag : u32 = 0; + + loop { + match self.read_next_event() { + Ok(Event::Text(t)) => if t.len() > 0 { + match t.unescaped() { + Ok(unescaped) => AttributeName::Text.report(unescaped), + Err(err) => ParsingError::from(err).report_err(), + } + }, + Ok(Event::Start(tag)) if tag.name() == b"ContentSteering" => inner_tag += 1, + Ok(Event::End(tag)) if tag.name() == b"ContentSteering" => { + if inner_tag > 0 { + inner_tag -= 1; + } else { + TagName::ContentSteering.report_tag_close(); + break; + } + }, + Ok(Event::Eof) => { + ParsingError("Unexpected end of file in a ContentSteering.".to_owned()) + .report_err(); + break; + } + Err(e) => { + ParsingError::from(e).report_err(); + break; + }, + _ => (), + } + self.reader_buf.clear(); + } + } + fn process_cenc_element(&mut self) { // Count inner cenc:pssh tags if it exists. // Allowing to not close the current node when it is an inner that is closed diff --git a/src/parsers/manifest/dash/wasm-parser/ts/generators/BaseURL.ts b/src/parsers/manifest/dash/wasm-parser/ts/generators/BaseURL.ts index c47931d887..646597b049 100644 --- a/src/parsers/manifest/dash/wasm-parser/ts/generators/BaseURL.ts +++ b/src/parsers/manifest/dash/wasm-parser/ts/generators/BaseURL.ts @@ -30,22 +30,17 @@ export function generateBaseUrlAttrParser( linearMemory : WebAssembly.Memory ) : IAttributeParser { const textDecoder = new TextDecoder(); - let dataView; return function onMPDAttribute(attr : number, ptr : number, len : number) { switch (attr) { case AttributeName.Text: baseUrlAttrs.value = parseString(textDecoder, linearMemory.buffer, ptr, len); break; - case AttributeName.AvailabilityTimeOffset: { - dataView = new DataView(linearMemory.buffer); - baseUrlAttrs.attributes.availabilityTimeOffset = dataView.getFloat64(ptr, true); - break; - } - - case AttributeName.AvailabilityTimeComplete: { - baseUrlAttrs.attributes.availabilityTimeComplete = - new DataView(linearMemory.buffer).getUint8(0) === 0; + case AttributeName.ServiceLocation: { + baseUrlAttrs.attributes.serviceLocation = parseString(textDecoder, + linearMemory.buffer, + ptr, + len); break; } } diff --git a/src/parsers/manifest/dash/wasm-parser/ts/generators/ContentSteering.ts b/src/parsers/manifest/dash/wasm-parser/ts/generators/ContentSteering.ts new file mode 100644 index 0000000000..d638d6e13f --- /dev/null +++ b/src/parsers/manifest/dash/wasm-parser/ts/generators/ContentSteering.ts @@ -0,0 +1,59 @@ +/** + * 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 { IContentSteeringIntermediateRepresentation } from "../../../node_parser_types"; +import { IAttributeParser } from "../parsers_stack"; +import { AttributeName } from "../types"; +import { parseString } from "../utils"; + +/** + * Generate an "attribute parser" once inside a `ContentSteering` node. + * @param {Object} contentSteeringAttrs + * @param {WebAssembly.Memory} linearMemory + * @returns {Function} + */ +export function generateContentSteeringAttrParser( + contentSteeringAttrs : IContentSteeringIntermediateRepresentation, + linearMemory : WebAssembly.Memory +) : IAttributeParser { + const textDecoder = new TextDecoder(); + return function onMPDAttribute(attr : number, ptr : number, len : number) { + switch (attr) { + case AttributeName.Text: + contentSteeringAttrs.value = + parseString(textDecoder, linearMemory.buffer, ptr, len); + break; + + case AttributeName.ServiceLocation: { + contentSteeringAttrs.attributes.defaultServiceLocation = + parseString(textDecoder, linearMemory.buffer, ptr, len); + break; + } + + case AttributeName.QueryBeforeStart: { + contentSteeringAttrs.attributes.queryBeforeStart = + new DataView(linearMemory.buffer).getUint8(0) === 0; + break; + } + + case AttributeName.ProxyServerUrl: { + contentSteeringAttrs.attributes.proxyServerUrl = + parseString(textDecoder, linearMemory.buffer, ptr, len); + break; + } + } + }; +} diff --git a/src/parsers/manifest/dash/wasm-parser/ts/generators/MPD.ts b/src/parsers/manifest/dash/wasm-parser/ts/generators/MPD.ts index d51533713f..85fa304b37 100644 --- a/src/parsers/manifest/dash/wasm-parser/ts/generators/MPD.ts +++ b/src/parsers/manifest/dash/wasm-parser/ts/generators/MPD.ts @@ -29,6 +29,7 @@ import { } from "../types"; import { parseString } from "../utils"; import { generateBaseUrlAttrParser } from "./BaseURL"; +import { generateContentSteeringAttrParser } from "./ContentSteering"; import { generatePeriodAttrParser, generatePeriodChildrenParser, @@ -62,6 +63,17 @@ export function generateMPDChildrenParser( break; } + case TagName.ContentSteering: { + const contentSteering = { value: "", attributes: {} }; + mpdChildren.contentSteering = contentSteering; + + const childrenParser = noop; // ContentSteering have no sub-element + const attributeParser = + generateContentSteeringAttrParser(contentSteering, linearMemory); + parsersStack.pushParsers(nodeId, childrenParser, attributeParser); + break; + } + case TagName.Period: { const period = { children: { adaptations: [], baseURLs: [], diff --git a/src/parsers/manifest/dash/wasm-parser/ts/types.ts b/src/parsers/manifest/dash/wasm-parser/ts/types.ts index f9e595b5f5..6fc5aefb8c 100644 --- a/src/parsers/manifest/dash/wasm-parser/ts/types.ts +++ b/src/parsers/manifest/dash/wasm-parser/ts/types.ts @@ -108,6 +108,9 @@ export const enum TagName { /// Indicate a node SegmentUrl = 20, + + /// Indicate a node + ContentSteering = 21 } /** @@ -282,4 +285,12 @@ export const enum AttributeName { Namespace = 70, Label = 71, // String + + ServiceLocation = 72, // String + + QueryBeforeStart = 73, // Boolean + + ProxyServerUrl = 74, // String + + DefaultServiceLocation = 75, } diff --git a/src/parsers/manifest/local/parse_local_manifest.ts b/src/parsers/manifest/local/parse_local_manifest.ts index 5fcf385b6c..70fb3bd614 100644 --- a/src/parsers/manifest/local/parse_local_manifest.ts +++ b/src/parsers/manifest/local/parse_local_manifest.ts @@ -54,6 +54,7 @@ export default function parseLocalManifest( .map(period => parsePeriod(period, { periodIdGenerator })); return { availabilityStartTime: 0, + contentSteering: null, expired: localManifest.expired, transportType: "local", isDynamic: !isFinished, @@ -133,6 +134,7 @@ function parseRepresentation( undefined : formatContentProtections(representation.contentProtections); return { id, + cdnMetadata: null, bitrate: representation.bitrate, height: representation.height, width: representation.width, diff --git a/src/parsers/manifest/local/representation_index.ts b/src/parsers/manifest/local/representation_index.ts index d9ca0b47d9..49ba6d36e8 100644 --- a/src/parsers/manifest/local/representation_index.ts +++ b/src/parsers/manifest/local/representation_index.ts @@ -43,7 +43,7 @@ export default class LocalRepresentationIndex implements IRepresentationIndex { end: 0, duration: 0, timescale: 1, - mediaURLs: null, + url: null, complete: true, privateInfos: { localManifestInitSegment: { load: this._index.loadInitSegment } }, @@ -81,7 +81,7 @@ export default class LocalRepresentationIndex implements IRepresentationIndex { duration: wantedSegment.duration, timescale: 1, timestampOffset: wantedSegment.timestampOffset, - mediaURLs: null, + url: null, complete: true, privateInfos: { localManifestSegment: { load: this._index.loadSegment, diff --git a/src/parsers/manifest/metaplaylist/metaplaylist_parser.ts b/src/parsers/manifest/metaplaylist/metaplaylist_parser.ts index 6932684890..d6d9de86b1 100644 --- a/src/parsers/manifest/metaplaylist/metaplaylist_parser.ts +++ b/src/parsers/manifest/metaplaylist/metaplaylist_parser.ts @@ -20,6 +20,7 @@ import Manifest, { SUPPORTED_ADAPTATIONS_TYPE, } from "../../../manifest"; import idGenerator from "../../../utils/id_generator"; +import { getFilenameIndexInUrl } from "../../../utils/resolve_url"; import { IParsedAdaptation, IParsedAdaptations, @@ -213,6 +214,7 @@ function createManifest( representations.push({ bitrate: currentRepresentation.bitrate, index: newIndex, + cdnMetadata: currentRepresentation.cdnMetadata, id: currentRepresentation.id, height: currentRepresentation.height, width: currentRepresentation.width, @@ -244,6 +246,9 @@ function createManifest( const newTextAdaptations : IParsedAdaptation[] = textTracks.map((track) => { const adaptationID = "gen-text-ada-" + generateAdaptationID(); const representationID = "gen-text-rep-" + generateRepresentationID(); + const indexOfFilename = getFilenameIndexInUrl(track.url); + const cdnUrl = track.url.substring(0, indexOfFilename); + const filename = track.url.substring(indexOfFilename); return { id: adaptationID, type: "text", @@ -252,10 +257,11 @@ function createManifest( manuallyAdded: true, representations: [ { bitrate: 0, + cdnMetadata: [{ baseUrl: cdnUrl }], id: representationID, mimeType: track.mimeType, codecs: track.codecs, - index: new StaticRepresentationIndex({ media: track.url }), + index: new StaticRepresentationIndex({ media: filename }), }, ], }; @@ -300,6 +306,7 @@ function createManifest( manifests[manifests.length - 1].isLastPeriodKnown); const manifest = { availabilityStartTime: 0, clockOffset, + contentSteering: null, suggestedPresentationDelay: 10, periods, transportType: "metaplaylist", diff --git a/src/parsers/manifest/smooth/create_parser.ts b/src/parsers/manifest/smooth/create_parser.ts index a473bee75f..2a609ed101 100644 --- a/src/parsers/manifest/smooth/create_parser.ts +++ b/src/parsers/manifest/smooth/create_parser.ts @@ -24,9 +24,7 @@ import { } from "../../../utils/byte_parsing"; import isNonEmptyString from "../../../utils/is_non_empty_string"; import objectAssign from "../../../utils/object_assign"; -import resolveURL, { - normalizeBaseURL, -} from "../../../utils/resolve_url"; +import { getFilenameIndexInUrl } from "../../../utils/resolve_url"; import { hexToBytes } from "../../../utils/string_parsing"; import takeFirstSet from "../../../utils/take_first_set"; import { createBox } from "../../containers/isobmff"; @@ -60,7 +58,7 @@ import { replaceRepresentationSmoothTokens } from "./utils/tokens"; const DEFAULT_AGGRESSIVE_MODE = false; interface IAdaptationParserArguments { root : Element; - rootURL : string; + baseUrl : string; timescale : number; protections : IContentProtectionSmooth[]; isLive : boolean; @@ -280,7 +278,7 @@ function createSmoothStreamingParser( function parseAdaptation(args: IAdaptationParserArguments) : IParsedAdaptation|null { const { root, timescale, - rootURL, + baseUrl, protections, timeShiftBufferDepth, manifestReceivedTime, @@ -301,11 +299,11 @@ function createSmoothStreamingParser( const subType = root.getAttribute("Subtype"); const language = root.getAttribute("Language"); - const baseURLAttr = root.getAttribute("Url"); - const baseURL = baseURLAttr === null ? "" : - baseURLAttr; + const UrlAttr = root.getAttribute("Url"); + const UrlPathWithTokens = UrlAttr === null ? "" : + UrlAttr; if (__ENVIRONMENT__.CURRENT_ENV === __ENVIRONMENT__.DEV as number) { - assert(baseURL !== ""); + assert(UrlPathWithTokens !== ""); } const { qualityLevels, cNodes } = @@ -347,11 +345,10 @@ function createSmoothStreamingParser( ""); const representations = qualityLevels.map((qualityLevel) => { - const path = resolveURL(rootURL, baseURL); const repIndex = { timeline: index.timeline, timescale: index.timescale, - media: replaceRepresentationSmoothTokens(path, + media: replaceRepresentationSmoothTokens(UrlPathWithTokens, qualityLevel.bitrate, qualityLevel.customAttributes), }; @@ -404,6 +401,9 @@ function createSmoothStreamingParser( const representation : IParsedRepresentation = objectAssign({}, qualityLevel, { index: reprIndex, + cdnMetadata: [ + { baseUrl }, + ], mimeType, codecs, id }); @@ -452,7 +452,11 @@ function createSmoothStreamingParser( url? : string, manifestReceivedTime? : number ) : IParsedManifest { - const rootURL = normalizeBaseURL(url == null ? "" : url); + let baseUrl : string = ""; + if (url !== undefined) { + const filenameIdx = getFilenameIndexInUrl(url); + baseUrl = url.substring(0, filenameIdx); + } const root = doc.documentElement; if (root == null || root.nodeName !== "SmoothStreamingMedia") { throw new Error("document root should be SmoothStreamingMedia"); @@ -510,7 +514,7 @@ function createSmoothStreamingParser( const adaptations: IParsedAdaptations = adaptationNodes .reduce((acc: IParsedAdaptations, node : Element) => { const adaptation = parseAdaptation({ root: node, - rootURL, + baseUrl, timescale, protections, isLive, @@ -646,6 +650,7 @@ function createSmoothStreamingParser( 0 : availabilityStartTime, clockOffset: serverTimeOffset, + contentSteering: null, isLive, isDynamic: isLive, isLastPeriodKnown: true, diff --git a/src/parsers/manifest/smooth/representation_index.ts b/src/parsers/manifest/smooth/representation_index.ts index 80d9fa3c95..91537aa91b 100644 --- a/src/parsers/manifest/smooth/representation_index.ts +++ b/src/parsers/manifest/smooth/representation_index.ts @@ -307,7 +307,7 @@ export default class SmoothRepresentationIndex implements IRepresentationIndex { return { id: "init", isInit: true, privateInfos: { smoothInitSegment: this._initSegmentInfos }, - mediaURLs: null, + url: null, time: 0, end: 0, duration: 0, @@ -361,7 +361,7 @@ export default class SmoothRepresentationIndex implements IRepresentationIndex { duration: duration / timescale, timescale: 1 as const, number, - mediaURLs: [replaceSegmentSmoothTokens(media, time)], + url: replaceSegmentSmoothTokens(media, time), complete: true, privateInfos: { smoothMediaSegment: { time, duration } } }; diff --git a/src/parsers/manifest/types.ts b/src/parsers/manifest/types.ts index 8650ea2fb8..73252a94e6 100644 --- a/src/parsers/manifest/types.ts +++ b/src/parsers/manifest/types.ts @@ -80,10 +80,37 @@ export interface IContentProtections { initData : IContentProtectionInitData[]; } +/** Represents metadata of a CDN which can serve resources. */ +export interface ICdnMetadata { + /** + * The base URL on which resources can be requested though this CDN. + * In most transports, you will want to add the wanted media resource's URL + * to that one to request it. + */ + baseUrl : string; + + /** + * Identifier that might be re-used in other documents, for example a + * Content Steering Manifest, to identify this CDN. + */ + id? : string | undefined; +} + /** Representation of a "quality" available in an Adaptation. */ export interface IParsedRepresentation { /** Maximum bitrate the Representation is available in, in bits per seconds. */ bitrate : number; + /** + * Information on the CDN(s) on which requests should be done to request this + * Representation's initialization and media segments. + * + * `null` if there's no CDN involved here (e.g. resources are not + * requested through the network). + * + * An empty array means that no CDN are left to request the resource. As such, + * no resource can be loaded in that situation. + */ + cdnMetadata : ICdnMetadata[] | null; /** * Interface to get information about segments associated with this * Representation, @@ -351,5 +378,13 @@ export interface IParsedManifest { suggestedPresentationDelay? : number | undefined; /** URIs where the manifest can be refreshed by order of importance. */ uris? : string[] | undefined; + + contentSteering : IContentSteeringMetadata | null; } +export interface IContentSteeringMetadata { + url : string; + defaultId : string | undefined; + queryBeforeStart : boolean; + proxyUrl : string | undefined; +} 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 11b2b7a6b8..99e184f8e1 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 @@ -49,12 +49,15 @@ describe("parsers utils - getFirstPositionFromAdaptation", function() { it("should return the first position if a single representation is present", () => { const representation1 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(37) }; const representation2 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(undefined) }; const representation3 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(null) }; expect(getFirstPositionFromAdaptation({ id: "0", type: "audio", @@ -75,12 +78,15 @@ describe("parsers utils - getFirstPositionFromAdaptation", function() { /* eslint-enable max-len */ const representation1 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(37) }; const representation2 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(137) }; const representation3 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(57) }; expect(getFirstPositionFromAdaptation({ id: "0", type: "audio", @@ -93,12 +99,15 @@ describe("parsers utils - getFirstPositionFromAdaptation", function() { it("should return undefined if one of the first position is", () => { const representation1 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(37) }; const representation2 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(137) }; const representation3 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(undefined) }; expect(getFirstPositionFromAdaptation({ id: "0", type: "audio", @@ -111,12 +120,15 @@ describe("parsers utils - getFirstPositionFromAdaptation", function() { it("should not consider null first positions if not all of them have one", () => { const representation1 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(37) }; const representation2 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(137) }; const representation3 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(null) }; expect(getFirstPositionFromAdaptation({ id: "0", type: "audio", @@ -129,12 +141,15 @@ describe("parsers utils - getFirstPositionFromAdaptation", function() { it("should return null if every first positions are", () => { const representation1 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(null) }; const representation2 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(null) }; const representation3 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(null) }; expect(getFirstPositionFromAdaptation({ id: "0", type: "audio", 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 009a247965..ea401d4821 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 @@ -49,12 +49,15 @@ describe("parsers utils - getLastPositionFromAdaptation", function() { it("should return the last position if a single representation is present", () => { const representation1 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(37) }; const representation2 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(undefined) }; const representation3 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(null) }; expect(getLastPositionFromAdaptation({ id: "0", type: "audio", @@ -75,12 +78,15 @@ describe("parsers utils - getLastPositionFromAdaptation", function() { /* eslint-enable max-len */ const representation1 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(37) }; const representation2 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(137) }; const representation3 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(57) }; expect(getLastPositionFromAdaptation({ id: "0", type: "audio", @@ -93,12 +99,15 @@ describe("parsers utils - getLastPositionFromAdaptation", function() { it("should return undefined if one of the first position is", () => { const representation1 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(37) }; const representation2 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(137) }; const representation3 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(undefined) }; expect(getLastPositionFromAdaptation({ id: "0", type: "audio", @@ -111,12 +120,15 @@ describe("parsers utils - getLastPositionFromAdaptation", function() { it("should not consider null first positions if not all of them have one", () => { const representation1 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(37) }; const representation2 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(137) }; const representation3 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(null) }; expect(getLastPositionFromAdaptation({ id: "0", type: "audio", @@ -129,12 +141,15 @@ describe("parsers utils - getLastPositionFromAdaptation", function() { it("should return null if every first positions are", () => { const representation1 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(null) }; const representation2 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(null) }; const representation3 = { id: "1", bitrate: 12, + cdnMetadata: [], index: generateRepresentationIndex(null) }; expect(getLastPositionFromAdaptation({ id: "0", type: "audio", diff --git a/src/transports/dash/construct_segment_url.ts b/src/transports/dash/construct_segment_url.ts new file mode 100644 index 0000000000..d46a6f5f50 --- /dev/null +++ b/src/transports/dash/construct_segment_url.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2015 CANAL+ Group + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ISegment } from "../../manifest"; +import { ICdnMetadata } from "../../parsers/manifest"; +import resolveURL from "../../utils/resolve_url"; + +export default function constructSegmentUrl( + wantedCdn : ICdnMetadata | null, + segment : ISegment +) : string | null { + return wantedCdn === null ? null : + segment.url === null ? wantedCdn.baseUrl : + resolveURL(wantedCdn.baseUrl, segment.url); +} diff --git a/src/transports/dash/image_pipelines.ts b/src/transports/dash/image_pipelines.ts index 4f005cfa46..0506c62929 100644 --- a/src/transports/dash/image_pipelines.ts +++ b/src/transports/dash/image_pipelines.ts @@ -15,6 +15,7 @@ */ import features from "../../features"; +import { ICdnMetadata } from "../../parsers/manifest"; import request from "../../utils/request"; import takeFirstSet from "../../utils/take_first_set"; import { CancellationSignal } from "../../utils/task_canceller"; @@ -29,10 +30,11 @@ import { ISegmentParserParsedInitChunk, ISegmentParserParsedMediaChunk, } from "../types"; +import constructSegmentUrl from "./construct_segment_url"; /** * Loads an image segment. - * @param {string|null} url + * @param {Object|null} wantedCdn * @param {Object} content * @param {Object} options * @param {Object} cancelSignal @@ -40,7 +42,7 @@ import { * @returns {Promise} */ export async function imageLoader( - url : string | null, + wantedCdn : ICdnMetadata | null, content : ISegmentContext, options : ISegmentLoaderOptions, cancelSignal : CancellationSignal, @@ -49,6 +51,7 @@ export async function imageLoader( ISegmentLoaderResultSegmentCreated> { const { segment } = content; + const url = constructSegmentUrl(wantedCdn, segment); if (segment.isInit || url === null) { return { resultType: "segment-created", resultData: null }; diff --git a/src/transports/dash/pipelines.ts b/src/transports/dash/pipelines.ts index 24619cd1b0..a83ae9b714 100644 --- a/src/transports/dash/pipelines.ts +++ b/src/transports/dash/pipelines.ts @@ -27,6 +27,10 @@ import { import generateManifestParser from "./manifest_parser"; import generateSegmentLoader from "./segment_loader"; import generateAudioVideoSegmentParser from "./segment_parser"; +import { + loadSteeringManifest, + parseSteeringManifest, +} from "./steering_manifest_pipeline"; import generateTextTrackLoader from "./text_loader"; import generateTextTrackParser from "./text_parser"; @@ -57,7 +61,9 @@ export default function(options : ITransportOptions) : ITransportPipelines { text: { loadSegment: textTrackLoader, parseSegment: textTrackParser }, image: { loadSegment: imageLoader, - parseSegment: imageParser } }; + parseSegment: imageParser }, + steeringManifest: { loadSteeringManifest, + parseSteeringManifest } }; } /** diff --git a/src/transports/dash/segment_loader.ts b/src/transports/dash/segment_loader.ts index 4fb71016fe..6f450dc19b 100644 --- a/src/transports/dash/segment_loader.ts +++ b/src/transports/dash/segment_loader.ts @@ -15,6 +15,7 @@ */ import { CustomLoaderError } from "../../errors"; +import { ICdnMetadata } from "../../parsers/manifest"; import { ISegmentLoader as ICustomSegmentLoader } from "../../public_types"; import request, { fetchIsSupported, @@ -37,6 +38,7 @@ import { import byteRange from "../utils/byte_range"; import inferSegmentContainer from "../utils/infer_segment_container"; import addSegmentIntegrityChecks from "./add_segment_integrity_checks_to_loader"; +import constructSegmentUrl from "./construct_segment_url"; import initSegmentLoader from "./init_segment_loader"; import lowLatencySegmentLoader from "./low_latency_segment_loader"; @@ -104,11 +106,11 @@ export default function generateSegmentLoader( addSegmentIntegrityChecks(segmentLoader); /** - * @param {Object} content + * @param {Object|null} wantedCdn * @returns {Observable} */ function segmentLoader( - url : string | null, + wantedCdn : ICdnMetadata | null, content : ISegmentContext, options : ISegmentLoaderOptions, cancelSignal : CancellationSignal, @@ -117,6 +119,7 @@ export default function generateSegmentLoader( ISegmentLoaderResultSegmentCreated | ISegmentLoaderResultChunkedComplete> { + const url = constructSegmentUrl(wantedCdn, content.segment); if (url == null) { return Promise.resolve({ resultType: "segment-created", resultData: null }); diff --git a/src/transports/dash/steering_manifest_pipeline.ts b/src/transports/dash/steering_manifest_pipeline.ts new file mode 100644 index 0000000000..0d8ad372a3 --- /dev/null +++ b/src/transports/dash/steering_manifest_pipeline.ts @@ -0,0 +1,61 @@ +/** + * 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 { ISteeringManifest } from "../../parsers/SteeringManifest"; +/* eslint-disable-next-line max-len */ +import parseDashContentSteeringManifest from "../../parsers/SteeringManifest/DCSM/parse_dcsm"; +import request from "../../utils/request"; +import { CancellationSignal } from "../../utils/task_canceller"; +import { IRequestedData } from "../types"; + +/** + * Loads DASH's Content Steering Manifest. + * @param {string|null} url + * @param {Object} cancelSignal + * @returns {Promise} + */ +export async function loadSteeringManifest( + url : string, + cancelSignal : CancellationSignal +) : Promise> { + return request({ url, + responseType: "text", + cancelSignal }); +} + +/** + * Parses DASH's Content Steering Manifest. + * @param {Object} loadedSegment + * @param {Function} onWarnings + * @returns {Object} + */ +export function parseSteeringManifest( + { responseData } : IRequestedData, + onWarnings : (warnings : Error[]) => void +) : ISteeringManifest { + if ( + typeof responseData !== "string" && + (typeof responseData !== "object" || responseData === null) + ) { + throw new Error("Invalid loaded format for DASH's Content Steering Manifest."); + } + + const parsed = parseDashContentSteeringManifest(responseData); + if (parsed[1].length > 0) { + onWarnings(parsed[1]); + } + return parsed[0]; +} diff --git a/src/transports/dash/text_loader.ts b/src/transports/dash/text_loader.ts index 2261b6f101..be99fc08c6 100644 --- a/src/transports/dash/text_loader.ts +++ b/src/transports/dash/text_loader.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { ICdnMetadata } from "../../parsers/manifest"; import request, { fetchIsSupported, } from "../../utils/request"; @@ -32,6 +33,7 @@ import { import byteRange from "../utils/byte_range"; import inferSegmentContainer from "../utils/infer_segment_container"; import addSegmentIntegrityChecks from "./add_segment_integrity_checks_to_loader"; +import constructSegmentUrl from "./construct_segment_url"; import initSegmentLoader from "./init_segment_loader"; import lowLatencySegmentLoader from "./low_latency_segment_loader"; @@ -49,7 +51,7 @@ export default function generateTextTrackLoader( addSegmentIntegrityChecks(textTrackLoader); /** - * @param {string|null} url + * @param {Object|null} wantedCdn * @param {Object} content * @param {Object} options * @param {Object} cancelSignal @@ -57,7 +59,7 @@ export default function generateTextTrackLoader( * @returns {Promise} */ function textTrackLoader( - url : string | null, + wantedCdn : ICdnMetadata | null, content : ISegmentContext, options : ISegmentLoaderOptions, cancelSignal : CancellationSignal, @@ -69,6 +71,7 @@ export default function generateTextTrackLoader( const { adaptation, representation, segment } = content; const { range } = segment; + const url = constructSegmentUrl(wantedCdn, segment); if (url === null) { return Promise.resolve({ resultType: "segment-created", resultData: null }); diff --git a/src/transports/dash/text_parser.ts b/src/transports/dash/text_parser.ts index 099bc80af9..186b10d432 100644 --- a/src/transports/dash/text_parser.ts +++ b/src/transports/dash/text_parser.ts @@ -185,7 +185,9 @@ export default function generateTextTrackParser( ITextTrackSegmentData | null > { /** * Parse TextTrack data. - * @param {Object} infos + * @param {Object} loadedSegment + * @param {Object} content + * @param {number|undefined} initTimescale * @returns {Observable.} */ return function textTrackParser( diff --git a/src/transports/local/pipelines.ts b/src/transports/local/pipelines.ts index 01c4ecb150..14f3a780c3 100644 --- a/src/transports/local/pipelines.ts +++ b/src/transports/local/pipelines.ts @@ -96,5 +96,6 @@ export default function getLocalManifestPipelines( audio: segmentPipeline, video: segmentPipeline, text: textTrackPipeline, - image: imageTrackPipeline }; + image: imageTrackPipeline, + steeringManifest: null }; } diff --git a/src/transports/local/segment_loader.ts b/src/transports/local/segment_loader.ts index ff9c0f6a52..0175439961 100644 --- a/src/transports/local/segment_loader.ts +++ b/src/transports/local/segment_loader.ts @@ -15,6 +15,7 @@ */ import { CustomLoaderError } from "../../errors"; +import { ICdnMetadata } from "../../parsers/manifest"; import { ILocalManifestInitSegmentLoader, ILocalManifestSegmentLoader, @@ -180,14 +181,14 @@ function loadSegment( /** * Generic segment loader for the local Manifest. - * @param {string | null} _url + * @param {string | null} _wantedCdn * @param {Object} content * @param {Object} cancelSignal * @param {Object} _callbacks * @returns {Promise} */ export default function segmentLoader( - _url : string | null, + _wantedCdn : ICdnMetadata | null, content : ISegmentContext, _loaderOptions : ISegmentLoaderOptions, // TODO use timeout? cancelSignal : CancellationSignal, diff --git a/src/transports/metaplaylist/pipelines.ts b/src/transports/metaplaylist/pipelines.ts index 292fcbce5f..55b6a134d1 100644 --- a/src/transports/metaplaylist/pipelines.ts +++ b/src/transports/metaplaylist/pipelines.ts @@ -26,7 +26,10 @@ import Manifest, { import parseMetaPlaylist, { IParserResponse as IMPLParserResponse, } from "../../parsers/manifest/metaplaylist"; -import { IParsedManifest } from "../../parsers/manifest/types"; +import { + ICdnMetadata, + IParsedManifest, +} from "../../parsers/manifest/types"; import isNullOrUndefined from "../../utils/is_null_or_undefined"; import objectAssign from "../../utils/object_assign"; import { CancellationSignal } from "../../utils/task_canceller"; @@ -248,7 +251,7 @@ export default function(options : ITransportOptions): ITransportPipelines { const audioPipeline = { loadSegment( - url : string | null, + wantedCdn : ICdnMetadata | null, content : ISegmentContext, loaderOptions : ISegmentLoaderOptions, cancelToken : CancellationSignal, @@ -260,7 +263,11 @@ export default function(options : ITransportOptions): ITransportPipelines { const { segment } = content; const { audio } = getTransportPipelinesFromSegment(segment); const ogContent = getOriginalContent(segment); - return audio.loadSegment(url, ogContent, loaderOptions, cancelToken, callbacks); + return audio.loadSegment(wantedCdn, + ogContent, + loaderOptions, + cancelToken, + callbacks); }, parseSegment( @@ -286,7 +293,7 @@ export default function(options : ITransportOptions): ITransportPipelines { const videoPipeline = { loadSegment( - url : string | null, + wantedCdn : ICdnMetadata | null, content : ISegmentContext, loaderOptions : ISegmentLoaderOptions, cancelToken : CancellationSignal, @@ -298,7 +305,11 @@ export default function(options : ITransportOptions): ITransportPipelines { const { segment } = content; const { video } = getTransportPipelinesFromSegment(segment); const ogContent = getOriginalContent(segment); - return video.loadSegment(url, ogContent, loaderOptions, cancelToken, callbacks); + return video.loadSegment(wantedCdn, + ogContent, + loaderOptions, + cancelToken, + callbacks); }, parseSegment( @@ -324,7 +335,7 @@ export default function(options : ITransportOptions): ITransportPipelines { const textTrackPipeline = { loadSegment( - url : string | null, + wantedCdn : ICdnMetadata | null, content : ISegmentContext, loaderOptions : ISegmentLoaderOptions, cancelToken : CancellationSignal, @@ -336,7 +347,11 @@ export default function(options : ITransportOptions): ITransportPipelines { const { segment } = content; const { text } = getTransportPipelinesFromSegment(segment); const ogContent = getOriginalContent(segment); - return text.loadSegment(url, ogContent, loaderOptions, cancelToken, callbacks); + return text.loadSegment(wantedCdn, + ogContent, + loaderOptions, + cancelToken, + callbacks); }, parseSegment( @@ -362,7 +377,7 @@ export default function(options : ITransportOptions): ITransportPipelines { const imageTrackPipeline = { loadSegment( - url : string | null, + wantedCdn : ICdnMetadata | null, content : ISegmentContext, loaderOptions : ISegmentLoaderOptions, cancelToken : CancellationSignal, @@ -374,7 +389,11 @@ export default function(options : ITransportOptions): ITransportPipelines { const { segment } = content; const { image } = getTransportPipelinesFromSegment(segment); const ogContent = getOriginalContent(segment); - return image.loadSegment(url, ogContent, loaderOptions, cancelToken, callbacks); + return image.loadSegment(wantedCdn, + ogContent, + loaderOptions, + cancelToken, + callbacks); }, parseSegment( @@ -402,5 +421,6 @@ export default function(options : ITransportOptions): ITransportPipelines { audio: audioPipeline, video: videoPipeline, text: textTrackPipeline, - image: imageTrackPipeline }; + image: imageTrackPipeline, + steeringManifest: null }; } diff --git a/src/transports/smooth/pipelines.ts b/src/transports/smooth/pipelines.ts index a283c62ef4..e6292b95ba 100644 --- a/src/transports/smooth/pipelines.ts +++ b/src/transports/smooth/pipelines.ts @@ -21,6 +21,7 @@ import Manifest, { ISegment, } from "../../manifest"; import { getMDAT } from "../../parsers/containers/isobmff"; +import { ICdnMetadata } from "../../parsers/manifest"; import createSmoothManifestParser, { SmoothRepresentationIndex, } from "../../parsers/manifest/smooth"; @@ -59,6 +60,7 @@ import extractTimingsInfos, { import { patchSegment } from "./isobmff"; import generateSegmentLoader from "./segment_loader"; import { + constructSegmentUrl, extractISML, extractToken, isMP4EmbeddedTrack, @@ -167,7 +169,7 @@ export default function(transportOptions : ITransportOptions) : ITransportPipeli const audioVideoPipeline = { /** * Load a Smooth audio/video segment. - * @param {string|null} url + * @param {Object|null} wantedCdn * @param {Object} content * @param {Object} loaderOptions * @param {Object} cancelSignal @@ -175,7 +177,7 @@ export default function(transportOptions : ITransportOptions) : ITransportPipeli * @returns {Promise} */ loadSegment( - url : string | null, + wantedCdn : ICdnMetadata | null, content : ISegmentContext, loaderOptions : ISegmentLoaderOptions, cancelSignal : CancellationSignal, @@ -183,6 +185,7 @@ export default function(transportOptions : ITransportOptions) : ITransportPipeli ) : Promise | ISegmentLoaderResultSegmentCreated> { + const url = constructSegmentUrl(wantedCdn, content.segment); return segmentLoader(url, content, loaderOptions, cancelSignal, callbacks); }, @@ -257,7 +260,7 @@ export default function(transportOptions : ITransportOptions) : ITransportPipeli const textTrackPipeline = { loadSegment( - url : string | null, + wantedCdn : ICdnMetadata | null, content : ISegmentContext, loaderOptions : ISegmentLoaderOptions, cancelSignal : CancellationSignal, @@ -265,6 +268,7 @@ export default function(transportOptions : ITransportOptions) : ITransportPipeli ) : Promise | ISegmentLoaderResultSegmentCreated> { const { segment, representation } = content; + const url = constructSegmentUrl(wantedCdn, segment); if (segment.isInit || url === null) { return Promise.resolve({ resultType: "segment-created", resultData: null }); @@ -447,19 +451,21 @@ export default function(transportOptions : ITransportOptions) : ITransportPipeli const imageTrackPipeline = { async loadSegment( - url : string | null, + wantedCdn : ICdnMetadata | null, content : ISegmentContext, loaderOptions : ISegmentLoaderOptions, cancelSignal : CancellationSignal, callbacks : ISegmentLoaderCallbacks ) : Promise | ISegmentLoaderResultSegmentCreated> { - if (content.segment.isInit || url === null) { + + const { segment } = content; + const url = constructSegmentUrl(wantedCdn, segment); + if (segment.isInit || url === null) { // image do not need an init segment. Passthrough directly to the parser return { resultType: "segment-created" as const, resultData: null }; } - const data = await request({ url, responseType: "arraybuffer", timeout: loaderOptions.timeout, @@ -523,5 +529,6 @@ export default function(transportOptions : ITransportOptions) : ITransportPipeli audio: audioVideoPipeline, video: audioVideoPipeline, text: textTrackPipeline, - image: imageTrackPipeline }; + image: imageTrackPipeline, + steeringManifest: null }; } diff --git a/src/transports/smooth/utils.ts b/src/transports/smooth/utils.ts index 2290640a3e..c59d6f2216 100644 --- a/src/transports/smooth/utils.ts +++ b/src/transports/smooth/utils.ts @@ -14,8 +14,10 @@ * limitations under the License. */ -import { Representation } from "../../manifest"; +import { ISegment, Representation } from "../../manifest"; +import { ICdnMetadata } from "../../parsers/manifest"; import isNonEmptyString from "../../utils/is_non_empty_string"; +import resolveURL from "../../utils/resolve_url"; import warnOnce from "../../utils/warn_once"; const ISM_REG = /(\.isml?)(\?token=\S+)?$/; @@ -85,7 +87,17 @@ function isMP4EmbeddedTrack(representation : Representation) : boolean { representation.mimeType.indexOf("mp4") >= 0; } +function constructSegmentUrl( + wantedCdn : ICdnMetadata | null, + segment : ISegment +) : string | null { + return wantedCdn === null ? null : + segment.url === null ? wantedCdn.baseUrl : + resolveURL(wantedCdn.baseUrl, segment.url); +} + export { + constructSegmentUrl, extractISML, extractToken, isMP4EmbeddedTrack, diff --git a/src/transports/types.ts b/src/transports/types.ts index 12eda7b909..98b91eba4b 100644 --- a/src/transports/types.ts +++ b/src/transports/types.ts @@ -23,6 +23,8 @@ import Manifest, { Period, Representation, } from "../manifest"; +import { ICdnMetadata } from "../parsers/manifest"; +import { ISteeringManifest } from "../parsers/SteeringManifest"; import { IBifThumbnail, ILoadedManifestFormat, @@ -52,6 +54,7 @@ export type ITransportFunction = (options : ITransportOptions) => export interface ITransportPipelines { /** Functions allowing to load an parse the Manifest for this transport. */ manifest : ITransportManifestPipeline; + /** Functions allowing to load an parse audio segments. */ audio : ISegmentPipeline; @@ -64,6 +67,21 @@ export interface ITransportPipelines { /** Functions allowing to load an parse image (e.g. thumbnails) segments. */ image : ISegmentPipeline; + + /** + * Functions allowing to load and parse a Content Steering Manifest for this + * transport. + * + * A Content Steering Manifest is an external document allowing to obtain the + * current priority between multiple available CDN. A Content Steering + * Manifest also may or may not be available depending on the content. You + * might know its availability by parsing the content's Manifest or any other + * resource. + * + * `null` if the notion of a Content Steering Manifest does not exist for this + * transport or if it does but it isn't handled right now. + */ + steeringManifest : ITransportSteeringManifestPipeline | null; } /** Functions allowing to load and parse the Manifest. */ @@ -210,6 +228,71 @@ export interface IManifestLoaderOptions { timeout? : number | undefined; } +/** + * Functions allowing to load and parse a potential Content Steering Manifest, + * which gives an order of preferred CDN to serve the content. + */ +export interface ITransportSteeringManifestPipeline { + /** + * "Loader" of the Steering Manifest pipeline, allowing to request a Steering + * Manifest so it can later be parsed by the `parseSteeringManifest` function. + * + * @param {string} url - URL of the Steering Manifest we want to load. + * @param {CancellationSignal} cancellationSignal - Signal which will allow to + * cancel the loading operation if the Steering Manifest is not needed anymore + * (for example, if the content has just been stopped). + * When cancelled, the promise returned by this function will reject with a + * `CancellationError`. + * @returns {Promise.} - Promise emitting the loaded Steering + * Manifest, that then can be parsed through the `parseSteeringManifest` + * function. + * + * Rejects in two cases: + * - The loading operation has been cancelled through the `cancelSignal` + * given in argument. + * In that case, this Promise will reject with a `CancellationError`. + * - The loading operation failed, most likely due to a request error. + * In that case, this Promise will reject with the corresponding Error. + */ + loadSteeringManifest : ( + url : string, + cancelSignal : CancellationSignal, + ) => Promise>>>; + + /** + * "Parser" of the Steering Manifest pipeline, allowing to parse a loaded + * Steering Manifest so it can be exploited by the rest of the RxPlayer's + * logic. + * + * @param {Object} data - Response obtained from the `loadSteeringManifest` + * function. + * @param {Function} onWarnings - Callbacks called when minor Steering + * Manifest parsing errors are found. + * @param {CancellationSignal} cancelSignal - Cancellation signal which will + * allow to abort the parsing operation if you do not want the Steering + * Manifest anymore. + * + * That cancellationSignal can be triggered at any time, such as: + * - after a warning is received + * - while a request scheduled through the `scheduleRequest` argument is + * pending. + * + * `parseSteeringManifest` will interrupt all operations if the signal has + * been triggered in one of those scenarios, and will automatically reject + * with the corresponding `CancellationError` instance. + * @returns {Object | Promise.} - Returns the Steering Manifest data. + * Throws if a fatal error happens while doing so. + * + * If this error is due to a cancellation (indicated through the + * `cancelSignal` argument), then the rejected error should be the + * corresponding `CancellationError` instance. + */ + parseSteeringManifest : ( + data : IRequestedData, + onWarnings : (warnings : Error[]) => void, + ) => ISteeringManifest; +} + /** Functions allowing to load and parse segments of any type. */ export interface ISegmentPipeline< TLoadedFormat, @@ -222,8 +305,9 @@ export interface ISegmentPipeline< /** * Segment loader function, allowing to load a segment of any type. - * @param {string|null} url - URL at which the segment should be downloaded. - * `null` if we do not have an URL (in which case the segment should be loaded + * @param {string|null} wantedCdn - CDN metadata for the CDN on which the + * segment should be downloaded. + * `null` if we do not have such CDN (in which case the segment should be loaded * through other means, such as information taken from the segment's content). * @param {Object} content - Content linked to the wanted segment. * @param {CancellationSignal} cancelSignal - Cancellation signal which will @@ -237,7 +321,7 @@ export interface ISegmentPipeline< * the segment. */ export type ISegmentLoader = ( - url : string | null, + wantedCdn : ICdnMetadata | null, content : ISegmentContext, options : ISegmentLoaderOptions, cancelSignal : CancellationSignal, @@ -402,6 +486,13 @@ export interface IManifestParserResult { url? : string | undefined; } +export interface IDASHContentSteeringManifest { + VERSION : number; // REQUIRED, must be an integer + TTL? : number; // REQUIRED, number of seconds + ["RELOAD-URI"]? : string; // OPTIONAL, URI + ["SERVICE-LOCATION-PRIORITY"] : string[]; // REQUIRED, array of ServiceLocation +} + /** * Allow the parser to ask for loading supplementary ressources while still * profiting from the same retries and error management than the loader. diff --git a/src/utils/__tests__/event_emitter.test.ts b/src/utils/__tests__/event_emitter.test.ts index 2f8b3d95c3..527d658a20 100644 --- a/src/utils/__tests__/event_emitter.test.ts +++ b/src/utils/__tests__/event_emitter.test.ts @@ -14,6 +14,11 @@ * limitations under the License. */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { take } from "rxjs"; import log from "../../log"; import EventEmitter, { @@ -32,9 +37,9 @@ describe("utils - EventEmitter", () => { }); expect(wasCalled).toEqual(0); - eventEmitter.trigger("something", undefined); + (eventEmitter as any).trigger("something", undefined); expect(wasCalled).toEqual(1); - eventEmitter.trigger("nope", undefined); + (eventEmitter as any).trigger("nope", undefined); expect(wasCalled).toEqual(1); eventEmitter.removeEventListener(); }); @@ -59,33 +64,33 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString).toEqual(0); expect(wasCalledWithObject).toEqual(0); - eventEmitter.trigger("something", undefined); + (eventEmitter as any).trigger("something", undefined); expect(wasCalledWithString).toEqual(0); expect(wasCalledWithObject).toEqual(0); - eventEmitter.trigger("something", "a"); + (eventEmitter as any).trigger("something", "a"); expect(wasCalledWithString).toEqual(1); expect(wasCalledWithObject).toEqual(0); - eventEmitter.trigger("nope", undefined); + (eventEmitter as any).trigger("nope", undefined); expect(wasCalledWithString).toEqual(1); expect(wasCalledWithObject).toEqual(0); - eventEmitter.trigger("nope", undefined); + (eventEmitter as any).trigger("nope", undefined); expect(wasCalledWithString).toEqual(1); expect(wasCalledWithObject).toEqual(0); - eventEmitter.trigger("something", { a: "b" }); + (eventEmitter as any).trigger("something", { a: "b" }); expect(wasCalledWithString).toEqual(1); expect(wasCalledWithObject).toEqual(1); - eventEmitter.trigger("something", "a"); - eventEmitter.trigger("something", "a"); - eventEmitter.trigger("something", "a"); + (eventEmitter as any).trigger("something", "a"); + (eventEmitter as any).trigger("something", "a"); + (eventEmitter as any).trigger("something", "a"); expect(wasCalledWithString).toEqual(4); expect(wasCalledWithObject).toEqual(1); - eventEmitter.trigger("nope", undefined); + (eventEmitter as any).trigger("nope", undefined); expect(wasCalledWithString).toEqual(4); expect(wasCalledWithObject).toEqual(1); eventEmitter.removeEventListener(); @@ -111,38 +116,38 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString).toEqual(0); expect(wasCalledWithObject).toEqual(0); - eventEmitter.trigger("something", undefined); + (eventEmitter as any).trigger("something", undefined); expect(wasCalledWithString).toEqual(0); expect(wasCalledWithObject).toEqual(0); - eventEmitter.trigger("something", "a"); + (eventEmitter as any).trigger("something", "a"); expect(wasCalledWithString).toEqual(1); expect(wasCalledWithObject).toEqual(0); - eventEmitter.trigger("nope", undefined); + (eventEmitter as any).trigger("nope", undefined); expect(wasCalledWithString).toEqual(1); expect(wasCalledWithObject).toEqual(0); - eventEmitter.trigger("nope", "a"); + (eventEmitter as any).trigger("nope", "a"); expect(wasCalledWithString).toEqual(2); expect(wasCalledWithObject).toEqual(0); - eventEmitter.trigger("something", { a: "b" }); + (eventEmitter as any).trigger("something", { a: "b" }); expect(wasCalledWithString).toEqual(2); expect(wasCalledWithObject).toEqual(1); eventEmitter.removeEventListener("something", callback); - eventEmitter.trigger("something", "a"); - eventEmitter.trigger("something", "a"); - eventEmitter.trigger("something", "a"); + (eventEmitter as any).trigger("something", "a"); + (eventEmitter as any).trigger("something", "a"); + (eventEmitter as any).trigger("something", "a"); expect(wasCalledWithString).toEqual(2); expect(wasCalledWithObject).toEqual(1); - eventEmitter.trigger("nope", undefined); + (eventEmitter as any).trigger("nope", undefined); expect(wasCalledWithString).toEqual(2); expect(wasCalledWithObject).toEqual(1); - eventEmitter.trigger("nope", "a"); + (eventEmitter as any).trigger("nope", "a"); expect(wasCalledWithString).toEqual(3); expect(wasCalledWithObject).toEqual(1); eventEmitter.removeEventListener(); @@ -192,7 +197,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(0); expect(wasCalledWithObject3).toEqual(0); - eventEmitter.trigger("something", undefined); + (eventEmitter as any).trigger("something", undefined); expect(wasCalledWithString1).toEqual(0); expect(wasCalledWithObject1).toEqual(0); expect(wasCalledWithString2).toEqual(0); @@ -200,7 +205,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(0); expect(wasCalledWithObject3).toEqual(0); - eventEmitter.trigger("something", "a"); + (eventEmitter as any).trigger("something", "a"); expect(wasCalledWithString1).toEqual(1); expect(wasCalledWithObject1).toEqual(0); expect(wasCalledWithString2).toEqual(1); @@ -216,7 +221,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(0); expect(wasCalledWithObject3).toEqual(0); - eventEmitter.trigger("something", "a"); + (eventEmitter as any).trigger("something", "a"); expect(wasCalledWithString1).toEqual(2); expect(wasCalledWithObject1).toEqual(0); expect(wasCalledWithString2).toEqual(2); @@ -224,7 +229,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(1); expect(wasCalledWithObject3).toEqual(0); - eventEmitter.trigger("nope", undefined); + (eventEmitter as any).trigger("nope", undefined); expect(wasCalledWithString1).toEqual(2); expect(wasCalledWithObject1).toEqual(0); expect(wasCalledWithString2).toEqual(2); @@ -232,7 +237,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(1); expect(wasCalledWithObject3).toEqual(0); - eventEmitter.trigger("nope", "a"); + (eventEmitter as any).trigger("nope", "a"); expect(wasCalledWithString1).toEqual(3); expect(wasCalledWithObject1).toEqual(0); expect(wasCalledWithString2).toEqual(2); @@ -240,7 +245,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(2); expect(wasCalledWithObject3).toEqual(0); - eventEmitter.trigger("something", { a: "b" }); + (eventEmitter as any).trigger("something", { a: "b" }); expect(wasCalledWithString1).toEqual(3); expect(wasCalledWithObject1).toEqual(1); expect(wasCalledWithString2).toEqual(2); @@ -256,7 +261,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(2); expect(wasCalledWithObject3).toEqual(1); - eventEmitter.trigger("something", { a: "b" }); + (eventEmitter as any).trigger("something", { a: "b" }); expect(wasCalledWithString1).toEqual(3); expect(wasCalledWithObject1).toEqual(2); expect(wasCalledWithString2).toEqual(2); @@ -264,7 +269,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(2); expect(wasCalledWithObject3).toEqual(2); - eventEmitter.trigger("nope", { a: "b" }); + (eventEmitter as any).trigger("nope", { a: "b" }); expect(wasCalledWithString1).toEqual(3); expect(wasCalledWithObject1).toEqual(3); expect(wasCalledWithString2).toEqual(2); @@ -320,7 +325,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(0); expect(wasCalledWithObject3).toEqual(0); - eventEmitter.trigger("something", undefined); + (eventEmitter as any).trigger("something", undefined); expect(wasCalledWithString1).toEqual(0); expect(wasCalledWithObject1).toEqual(0); expect(wasCalledWithString2).toEqual(0); @@ -328,7 +333,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(0); expect(wasCalledWithObject3).toEqual(0); - eventEmitter.trigger("something", "a"); + (eventEmitter as any).trigger("something", "a"); expect(wasCalledWithString1).toEqual(1); expect(wasCalledWithObject1).toEqual(0); expect(wasCalledWithString2).toEqual(1); @@ -344,7 +349,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(0); expect(wasCalledWithObject3).toEqual(0); - eventEmitter.trigger("something", "a"); + (eventEmitter as any).trigger("something", "a"); expect(wasCalledWithString1).toEqual(2); expect(wasCalledWithObject1).toEqual(0); expect(wasCalledWithString2).toEqual(2); @@ -352,7 +357,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(1); expect(wasCalledWithObject3).toEqual(0); - eventEmitter.trigger("nope", undefined); + (eventEmitter as any).trigger("nope", undefined); expect(wasCalledWithString1).toEqual(2); expect(wasCalledWithObject1).toEqual(0); expect(wasCalledWithString2).toEqual(2); @@ -360,7 +365,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(1); expect(wasCalledWithObject3).toEqual(0); - eventEmitter.trigger("nope", "a"); + (eventEmitter as any).trigger("nope", "a"); expect(wasCalledWithString1).toEqual(3); expect(wasCalledWithObject1).toEqual(0); expect(wasCalledWithString2).toEqual(2); @@ -368,7 +373,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(2); expect(wasCalledWithObject3).toEqual(0); - eventEmitter.trigger("something", { a: "b" }); + (eventEmitter as any).trigger("something", { a: "b" }); expect(wasCalledWithString1).toEqual(3); expect(wasCalledWithObject1).toEqual(1); expect(wasCalledWithString2).toEqual(2); @@ -384,7 +389,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(2); expect(wasCalledWithObject3).toEqual(1); - eventEmitter.trigger("something", { a: "b" }); + (eventEmitter as any).trigger("something", { a: "b" }); expect(wasCalledWithString1).toEqual(3); expect(wasCalledWithObject1).toEqual(1); expect(wasCalledWithString2).toEqual(2); @@ -392,7 +397,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(2); expect(wasCalledWithObject3).toEqual(1); - eventEmitter.trigger("nope", { a: "b" }); + (eventEmitter as any).trigger("nope", { a: "b" }); expect(wasCalledWithString1).toEqual(3); expect(wasCalledWithObject1).toEqual(2); expect(wasCalledWithString2).toEqual(2); @@ -448,7 +453,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(0); expect(wasCalledWithObject3).toEqual(0); - eventEmitter.trigger("something", undefined); + (eventEmitter as any).trigger("something", undefined); expect(wasCalledWithString1).toEqual(0); expect(wasCalledWithObject1).toEqual(0); expect(wasCalledWithString2).toEqual(0); @@ -456,7 +461,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(0); expect(wasCalledWithObject3).toEqual(0); - eventEmitter.trigger("something", "a"); + (eventEmitter as any).trigger("something", "a"); expect(wasCalledWithString1).toEqual(1); expect(wasCalledWithObject1).toEqual(0); expect(wasCalledWithString2).toEqual(1); @@ -472,7 +477,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(0); expect(wasCalledWithObject3).toEqual(0); - eventEmitter.trigger("something", "a"); + (eventEmitter as any).trigger("something", "a"); expect(wasCalledWithString1).toEqual(2); expect(wasCalledWithObject1).toEqual(0); expect(wasCalledWithString2).toEqual(2); @@ -480,7 +485,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(1); expect(wasCalledWithObject3).toEqual(0); - eventEmitter.trigger("nope", undefined); + (eventEmitter as any).trigger("nope", undefined); expect(wasCalledWithString1).toEqual(2); expect(wasCalledWithObject1).toEqual(0); expect(wasCalledWithString2).toEqual(2); @@ -488,7 +493,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(1); expect(wasCalledWithObject3).toEqual(0); - eventEmitter.trigger("nope", "a"); + (eventEmitter as any).trigger("nope", "a"); expect(wasCalledWithString1).toEqual(3); expect(wasCalledWithObject1).toEqual(0); expect(wasCalledWithString2).toEqual(2); @@ -496,7 +501,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(2); expect(wasCalledWithObject3).toEqual(0); - eventEmitter.trigger("something", { a: "b" }); + (eventEmitter as any).trigger("something", { a: "b" }); expect(wasCalledWithString1).toEqual(3); expect(wasCalledWithObject1).toEqual(1); expect(wasCalledWithString2).toEqual(2); @@ -512,7 +517,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(2); expect(wasCalledWithObject3).toEqual(1); - eventEmitter.trigger("something", { a: "b" }); + (eventEmitter as any).trigger("something", { a: "b" }); expect(wasCalledWithString1).toEqual(3); expect(wasCalledWithObject1).toEqual(1); expect(wasCalledWithString2).toEqual(2); @@ -520,7 +525,7 @@ describe("utils - EventEmitter", () => { expect(wasCalledWithString3).toEqual(2); expect(wasCalledWithObject3).toEqual(1); - eventEmitter.trigger("nope", { a: "b" }); + (eventEmitter as any).trigger("nope", { a: "b" }); expect(wasCalledWithString1).toEqual(3); expect(wasCalledWithObject1).toEqual(1); expect(wasCalledWithString2).toEqual(2); @@ -568,7 +573,7 @@ describe("utils - EventEmitter", () => { eventEmitter.addEventListener("t", cb); expect(spy).toHaveBeenCalledTimes(0); - eventEmitter.trigger("t", undefined); + (eventEmitter as any).trigger("t", undefined); expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledWith(errMessage, thrownErr); @@ -595,23 +600,23 @@ describe("utils - fromEvent", () => { } }, complete() { - eventEmitter.trigger("fooba", 6); + (eventEmitter as any).trigger("fooba", 6); expect(numberItemsReceived).toBe(2); expect(stringItemsReceived).toBe(3); done(); }, }); - eventEmitter.trigger("test", undefined); - eventEmitter.trigger("fooba", undefined); - eventEmitter.trigger("fooba", 5); - eventEmitter.trigger("fooba", "a"); - eventEmitter.trigger("test", undefined); - eventEmitter.trigger("test", undefined); - eventEmitter.trigger("test", undefined); - eventEmitter.trigger("fooba", "b"); - eventEmitter.trigger("fooba", "c"); - eventEmitter.trigger("fooba", 6); + (eventEmitter as any).trigger("test", undefined); + (eventEmitter as any).trigger("fooba", undefined); + (eventEmitter as any).trigger("fooba", 5); + (eventEmitter as any).trigger("fooba", "a"); + (eventEmitter as any).trigger("test", undefined); + (eventEmitter as any).trigger("test", undefined); + (eventEmitter as any).trigger("test", undefined); + (eventEmitter as any).trigger("fooba", "b"); + (eventEmitter as any).trigger("fooba", "c"); + (eventEmitter as any).trigger("fooba", 6); }); it("should remove the event listener on unsubscription", () => { @@ -631,17 +636,17 @@ describe("utils - fromEvent", () => { } }); - eventEmitter.trigger("test", undefined); - eventEmitter.trigger("fooba", undefined); - eventEmitter.trigger("fooba", 5); - eventEmitter.trigger("fooba", "a"); + (eventEmitter as any).trigger("test", undefined); + (eventEmitter as any).trigger("fooba", undefined); + (eventEmitter as any).trigger("fooba", 5); + (eventEmitter as any).trigger("fooba", "a"); subscription.unsubscribe(); - eventEmitter.trigger("test", undefined); - eventEmitter.trigger("test", undefined); - eventEmitter.trigger("test", undefined); - eventEmitter.trigger("fooba", "b"); - eventEmitter.trigger("fooba", "c"); - eventEmitter.trigger("fooba", 6); + (eventEmitter as any).trigger("test", undefined); + (eventEmitter as any).trigger("test", undefined); + (eventEmitter as any).trigger("test", undefined); + (eventEmitter as any).trigger("fooba", "b"); + (eventEmitter as any).trigger("fooba", "c"); + (eventEmitter as any).trigger("fooba", 6); expect(stringItemsReceived).toBe(1); expect(numberItemsReceived).toBe(1); diff --git a/src/utils/__tests__/initialization_segment_cache.test.ts b/src/utils/__tests__/initialization_segment_cache.test.ts index b43e5fa5a5..4495d367c3 100644 --- a/src/utils/__tests__/initialization_segment_cache.test.ts +++ b/src/utils/__tests__/initialization_segment_cache.test.ts @@ -40,11 +40,11 @@ const representation2 = { const initSegment1 = { id: "init1", isInit: true, + url: "some.URLinit1", time: 0, end: 0, duration: 0, timescale: 1 as const, - mediaURLs: ["http://www.example.com/some.URLinit1"], complete: true, privateInfos: {}, }; @@ -52,11 +52,11 @@ const initSegment1 = { const initSegment2 = { id: "init2", isInit: true, + url: "some.URLinit2", time: 0, end: 0, duration: 0, timescale: 1 as const, - mediaURLs: ["http://www.example.com/some.URLinit2"], complete: true, privateInfos: {}, }; @@ -64,11 +64,11 @@ const initSegment2 = { const initSegment3 = { id: "init3", isInit: true, + url: "some.URLinit3", time: 0, end: 0, duration: 0, timescale: 1 as const, - mediaURLs: ["http://www.example.com/some.URLinit3"], complete: true, privateInfos: {}, }; @@ -76,11 +76,11 @@ const initSegment3 = { const segment1 = { id: "seg1", isInit: false, + url: "some.URL1", time: 0, duration: 2, end: 2, timescale: 1 as const, - mediaURLs: ["http://www.example.com/some.URL2"], complete: true, privateInfos: {}, }; @@ -88,11 +88,11 @@ const segment1 = { const segment2 = { id: "seg2", isInit: false, + url: "some.URL2", time: 2, duration: 2, end: 4, timescale: 1 as const, - mediaURLs: ["http://www.example.com/some.URL2"], complete: true, privateInfos: {}, }; @@ -100,11 +100,11 @@ const segment2 = { const segment3 = { id: "seg3", isInit: false, + url: "some.URL3", time: 4, duration: 2, end: 6, timescale: 1 as const, - mediaURLs: ["http://www.example.com/some.URL3"], complete: true, privateInfos: {}, }; @@ -112,11 +112,11 @@ const segment3 = { const segment4 = { id: "seg4", isInit: false, + url: "some.URL4", time: 6, duration: 2, end: 8, timescale: 1 as const, - mediaURLs: ["http://www.example.com/some.URL4"], complete: true, privateInfos: {}, }; diff --git a/src/utils/__tests__/resolve_url.test.ts b/src/utils/__tests__/resolve_url.test.ts index 2c191d0417..99067cac73 100644 --- a/src/utils/__tests__/resolve_url.test.ts +++ b/src/utils/__tests__/resolve_url.test.ts @@ -15,7 +15,7 @@ */ import resolveURL, { - normalizeBaseURL, + getFilenameIndexInUrl, } from "../resolve_url"; describe("utils - resolveURL", () => { @@ -66,31 +66,35 @@ describe("utils - resolveURL", () => { }); }); -describe("utils - normalizeBaseURL", () => { - it("should do nothing if there is no / in the given string", () => { - expect(normalizeBaseURL(";.<;L'dl'02984lirsahg;oliwr")) - .toBe(";.<;L'dl'02984lirsahg;oliwr"); +describe("utils - getFilenameIndexInUrl", () => { + it("should return the length for a string without slash", () => { + const str = ";.<;L'dl'02984lirsahg;oliwr"; + expect(getFilenameIndexInUrl(str)) + .toEqual(str.length); }); - it("should remove the content of a string after the last /", () => { - expect(normalizeBaseURL(";ojdsfgje/eprowig/tohjroj/9ohyjwoij/s")) - .toBe(";ojdsfgje/eprowig/tohjroj/9ohyjwoij/"); + it("should return the index after the last / if one in URL", () => { + expect(getFilenameIndexInUrl(";ojdsfgje/eprowig/tohjroj/9ohyjwoij/s")) + .toEqual(36); }); - it("should do nothing if the only slash are part of the protocol", () => { - expect(normalizeBaseURL("http://www.example.com")) - .toBe("http://www.example.com"); - expect(normalizeBaseURL("https://a.t")) - .toBe("https://a.t"); - expect(normalizeBaseURL("ftp://s")) - .toBe("ftp://s"); + it("should return length if the only slash are part of the protocol", () => { + const url1 = "http://www.example.com"; + expect(getFilenameIndexInUrl(url1)) + .toEqual(url1.length); + const url2 = "https://a.t"; + expect(getFilenameIndexInUrl(url2)) + .toEqual(url2.length); + const url3 = "ftp://s"; + expect(getFilenameIndexInUrl(url3)) + .toEqual(url3.length); }); it("should not include slash coming in query parameters", () => { - expect(normalizeBaseURL("http://www.example.com?test/toto")) - .toBe("http://www.example.com"); - expect(normalizeBaseURL("https://ww/ddd?test/toto/efewf/ffe/")) - .toBe("https://ww/"); - expect(normalizeBaseURL("https://ww/rr/d?test/toto/efewf/ffe/")) - .toBe("https://ww/rr/"); - expect(normalizeBaseURL("https://ww/rr/d/?test/toto/efewf/ffe/")) - .toBe("https://ww/rr/d/"); + expect(getFilenameIndexInUrl("http://www.example.com?test/toto")) + .toEqual(22); + expect(getFilenameIndexInUrl("https://ww/ddd?test/toto/efewf/ffe/")) + .toEqual(11); + expect(getFilenameIndexInUrl("https://ww/rr/d?test/toto/efewf/ffe/")) + .toEqual(14); + expect(getFilenameIndexInUrl("https://ww/rr/d/?test/toto/efewf/ffe/")) + .toEqual(16); }); }); diff --git a/src/utils/event_emitter.ts b/src/utils/event_emitter.ts index 29c87f65fa..e03a9cbc89 100644 --- a/src/utils/event_emitter.ts +++ b/src/utils/event_emitter.ts @@ -29,8 +29,6 @@ export interface IEventEmitter { removeEventListener(evt : TEventName, fn : IListener) : void; - trigger?(evt : TEventName, - arg : IArgs) : void; } // Type of the argument in the listener's callback @@ -131,7 +129,7 @@ export default class EventEmitter implements IEventEmitter { * @param {*} arg - The eventual payload for that event. All triggered * callbacks will recieve this payload as argument. */ - public trigger( + protected trigger( evt : TEventName, arg : IArgs ) : void { diff --git a/src/utils/resolve_url.ts b/src/utils/resolve_url.ts index ce2972d9bf..0c2a5baf2e 100644 --- a/src/utils/resolve_url.ts +++ b/src/utils/resolve_url.ts @@ -91,14 +91,17 @@ export default function resolveURL(...args : Array) : string { } /** - * Remove string after the last '/'. + * In a given URL, find the index at which the filename begins. + * That is, this function finds the index of the last `/` character and returns + * the index after it, returning the length of the whole URL if no `/` was found + * after the scheme (i.e. in `http://`, the slashes are not considered). * @param {string} url - * @returns {string} + * @returns {number} */ -function normalizeBaseURL(url : string) : string { +function getFilenameIndexInUrl(url : string) : number { const indexOfLastSlash = url.lastIndexOf("/"); if (indexOfLastSlash < 0) { - return url; + return url.length; } if (schemeRe.test(url)) { @@ -106,7 +109,7 @@ function normalizeBaseURL(url : string) : string { if (firstSlashIndex >= 0 && indexOfLastSlash === firstSlashIndex + 1) { // The "/" detected is actually the one from the protocol part of the URL // ("https://") - return url; + return url.length; } } @@ -114,10 +117,10 @@ function normalizeBaseURL(url : string) : string { if (indexOfQuestionMark >= 0 && indexOfQuestionMark < indexOfLastSlash) { // There are query parameters. Let's ignore them and re-run the logic // without - return normalizeBaseURL(url.substring(0, indexOfQuestionMark)); + return getFilenameIndexInUrl(url.substring(0, indexOfQuestionMark)); } - return url.substring(0, indexOfLastSlash + 1); + return indexOfLastSlash + 1; } -export { normalizeBaseURL }; +export { getFilenameIndexInUrl }; diff --git a/src/utils/sync_or_async.ts b/src/utils/sync_or_async.ts new file mode 100644 index 0000000000..ac8ae7871c --- /dev/null +++ b/src/utils/sync_or_async.ts @@ -0,0 +1,72 @@ +/** + * 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. + */ + +/** + * Type wrapping an underlying value that might either be obtained synchronously + * (a "sync" value) or asynchronously by awaiting a Promise (an "async" value). + * + * This type was created instead of just relying on Promises everytime, to + * avoid the necessity of always having the overhead and more complex + * always-async behavior of a Promise for a value that might be in most time + * obtainable synchronously. + * + * @example + * ```ts + * const val1 = SyncOrAsync.createAsync(Promise.resolve("foo")); + * const val2 = SyncOrAsync.createSync("bar"); + * + * async function logVal(val : ISyncOrAsyncValue) : void { + * // The following syntax allows to only await asynchronous values + * console.log(val.syncValue ?? await val.getValueAsAsync()); + * } + * + * logVal(val1); + * logVal(val2); + * + * // Here this will first log in the console "bar" directly and synchronously. + * // Then asychronously through a microtask (as Promises and awaited values + * // always are), "foo" will be logged. + * ``` + */ +export interface ISyncOrAsyncValue { + /** + * Set to the underlying value in the case where it was set synchronously. + * Set to `null` if the value is set asynchronously. + */ + syncValue : T | null; + /** + * Obtain the value asynchronously. + * This works even when the value is actually set synchronously, by embedding it + * value in a Promise. + */ + getValueAsAsync() : Promise; +} + +export default { + createSync(val : T) : ISyncOrAsyncValue { + return { + syncValue: val, + getValueAsAsync() { return Promise.resolve(val); }, + }; + }, + + createAsync(val : Promise) : ISyncOrAsyncValue { + return { + syncValue: null, + getValueAsAsync() { return val; }, + }; + }, +}; diff --git a/tests/contents/DASH_dynamic_SegmentTemplate/infos.js b/tests/contents/DASH_dynamic_SegmentTemplate/infos.js index 72506e3296..d6afd8748f 100644 --- a/tests/contents/DASH_dynamic_SegmentTemplate/infos.js +++ b/tests/contents/DASH_dynamic_SegmentTemplate/infos.js @@ -28,7 +28,7 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "A48/init.mp4"], + url: "A48/init.mp4", }, segments: [ ], @@ -52,7 +52,7 @@ export default { frameRate: "60/2", index: { init: { - mediaURLs: [BASE_URL + "V300/init.mp4"], + url: "V300/init.mp4", }, segments: [ ], diff --git a/tests/contents/DASH_dynamic_SegmentTemplate/no_time_shift_buffer_depth.js b/tests/contents/DASH_dynamic_SegmentTemplate/no_time_shift_buffer_depth.js index 7fcf172525..0a0d854587 100644 --- a/tests/contents/DASH_dynamic_SegmentTemplate/no_time_shift_buffer_depth.js +++ b/tests/contents/DASH_dynamic_SegmentTemplate/no_time_shift_buffer_depth.js @@ -26,7 +26,7 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "A48/init.mp4"], + url: "A48/init.mp4", }, segments: [ ], @@ -47,7 +47,7 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "V300/init.mp4"], + url: "V300/init.mp4", }, segments: [ ], diff --git a/tests/contents/DASH_dynamic_SegmentTimeline/infos.js b/tests/contents/DASH_dynamic_SegmentTimeline/infos.js index ed0d019985..66f796ab4c 100644 --- a/tests/contents/DASH_dynamic_SegmentTimeline/infos.js +++ b/tests/contents/DASH_dynamic_SegmentTimeline/infos.js @@ -28,26 +28,26 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "A48/init.mp4"], + url: "A48/init.mp4", }, segments: [ // { // time: 73320372578304 / 48000, // duration: 288768 / 48000, // timescale: 1, - // mediaURLs: [BASE_URL + "A48/t73320372578304.m4s"], + // url: "A48/t73320372578304.m4s", // }, { time: 73320372867072 / 48000, duration: 287744 / 48000, timescale: 1, - mediaURLs: [BASE_URL + "A48/t73320372867072.m4s"], + url: "A48/t73320372867072.m4s", }, { time: 73320373154816 / 48000, duration: 288768 / 48000, timescale: 1, - mediaURLs: ["http://127.0.0.1:3000/DASH_dynamic_SegmentTimeline/media/A48/t73320373154816.m4s"], + url: "A48/t73320373154816.m4s", }, ], // ... @@ -70,26 +70,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "V300/init.mp4"], + url: "V300/init.mp4", }, segments: [ // { // time: 137475698580000 / 90000, // duration: 540000 / 90000, // timescale: 1, - // mediaURLs: [BASE_URL + "V300/t137475698580000.m4s"], + // url: "V300/t137475698580000.m4s", // }, { time: 137475699120000 / 90000, duration: 540000 / 90000, timescale: 1, - mediaURLs: [BASE_URL + "V300/t137475699120000.m4s"], + url: "V300/t137475699120000.m4s", }, { time: 137475699660000 / 90000, duration: 540000 / 90000, timescale: 1, - mediaURLs: ["http://127.0.0.1:3000/DASH_dynamic_SegmentTimeline/media/V300/t137475699660000.m4s"], + url: "V300/t137475699660000.m4s", }, ], // ... diff --git a/tests/contents/DASH_dynamic_SegmentTimeline/no_time_shift_buffer_depth.js b/tests/contents/DASH_dynamic_SegmentTimeline/no_time_shift_buffer_depth.js index 627c2b824d..f2fbf4df46 100644 --- a/tests/contents/DASH_dynamic_SegmentTimeline/no_time_shift_buffer_depth.js +++ b/tests/contents/DASH_dynamic_SegmentTimeline/no_time_shift_buffer_depth.js @@ -28,20 +28,20 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "A48/init.mp4"], + url: "A48/init.mp4", }, segments: [ { time: 73320372578304 / 48000, duration: 288768 / 48000, timescale: 1, - mediaURLs: [BASE_URL + "A48/t73320372578304.m4s"], + url: "A48/t73320372578304.m4s", }, { time: 73320372867072 / 48000, duration: 287744 / 48000, timescale: 1, - mediaURLs: [BASE_URL + "A48/t73320372867072.m4s"], + url: "A48/t73320372867072.m4s", }, ], // ... @@ -64,20 +64,20 @@ export default { frameRate: "60/2", index: { init: { - mediaURLs: [BASE_URL + "V300/init.mp4"], + url: "V300/init.mp4", }, segments: [ { time: 137475698580000 / 90000, duration: 540000 / 90000, timescale: 1, - mediaURLs: [BASE_URL + "V300/t137475698580000.m4s"], + url: "V300/t137475698580000.m4s", }, { time: 137475699120000 / 90000, duration: 540000 / 90000, timescale: 1, - mediaURLs: [BASE_URL + "V300/t137475699120000.m4s"], + url: "V300/t137475699120000.m4s", }, ], // ... diff --git a/tests/contents/DASH_dynamic_UTCTimings/with_direct.js b/tests/contents/DASH_dynamic_UTCTimings/with_direct.js index 6562117c4c..d61cc72ae0 100644 --- a/tests/contents/DASH_dynamic_UTCTimings/with_direct.js +++ b/tests/contents/DASH_dynamic_UTCTimings/with_direct.js @@ -26,7 +26,7 @@ const manifestInfos = { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "A48/init.mp4"], + url: "A48/init.mp4", }, segments: [ ], @@ -47,7 +47,7 @@ const manifestInfos = { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "V300/init.mp4"], + url: "V300/init.mp4", }, segments: [ ], diff --git a/tests/contents/DASH_dynamic_UTCTimings/with_direct_and_http.js b/tests/contents/DASH_dynamic_UTCTimings/with_direct_and_http.js index 03e8964e06..24ff70f18c 100644 --- a/tests/contents/DASH_dynamic_UTCTimings/with_direct_and_http.js +++ b/tests/contents/DASH_dynamic_UTCTimings/with_direct_and_http.js @@ -26,7 +26,7 @@ const manifestInfos = { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "A48/init.mp4"], + url: "A48/init.mp4", }, segments: [ ], @@ -47,7 +47,7 @@ const manifestInfos = { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "V300/init.mp4"], + url: "V300/init.mp4", }, segments: [ ], diff --git a/tests/contents/DASH_dynamic_UTCTimings/with_http.js b/tests/contents/DASH_dynamic_UTCTimings/with_http.js index 42b3d24a9e..9d60b55e73 100644 --- a/tests/contents/DASH_dynamic_UTCTimings/with_http.js +++ b/tests/contents/DASH_dynamic_UTCTimings/with_http.js @@ -26,7 +26,7 @@ const manifestInfos = { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "A48/init.mp4"], + url: "A48/init.mp4", }, segments: [ ], @@ -47,7 +47,7 @@ const manifestInfos = { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "V300/init.mp4"], + url: "V300/init.mp4", }, segments: [ ], diff --git a/tests/contents/DASH_dynamic_UTCTimings/without_timings.js b/tests/contents/DASH_dynamic_UTCTimings/without_timings.js index cc7ca71a30..584e790758 100644 --- a/tests/contents/DASH_dynamic_UTCTimings/without_timings.js +++ b/tests/contents/DASH_dynamic_UTCTimings/without_timings.js @@ -26,7 +26,7 @@ const manifestInfos = { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "A48/init.mp4"], + url: "A48/init.mp4", }, segments: [ ], @@ -47,7 +47,7 @@ const manifestInfos = { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "V300/init.mp4"], + url: "V300/init.mp4", }, segments: [ ], diff --git a/tests/contents/DASH_static_SegmentBase/broken_sidx.js b/tests/contents/DASH_static_SegmentBase/broken_sidx.js index 986837c58c..f2459888f3 100644 --- a/tests/contents/DASH_static_SegmentBase/broken_sidx.js +++ b/tests/contents/DASH_static_SegmentBase/broken_sidx.js @@ -33,7 +33,7 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "a-eng-0128k-aac.mp4"], + url: "a-eng-0128k-aac.mp4", range: [0, 745], }, segments: [], @@ -54,7 +54,7 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "v-0144p-0100k-libx264_broken_sidx.mp4"], + url: "v-0144p-0100k-libx264_broken_sidx.mp4", range: [0, 806], }, segments: [], diff --git a/tests/contents/DASH_static_SegmentBase/multi_codecs.js b/tests/contents/DASH_static_SegmentBase/multi_codecs.js index 387602c06d..9c64afae23 100644 --- a/tests/contents/DASH_static_SegmentBase/multi_codecs.js +++ b/tests/contents/DASH_static_SegmentBase/multi_codecs.js @@ -35,7 +35,7 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "a-eng-0128k-aac.mp4"], + url: null, range: [0, 745], }, segments: [], @@ -56,7 +56,7 @@ export default { mimeType: "audio/webm", index: { init: { - mediaURLs: [BASE_URL + "a-eng-0128k-libopus.webm"], + url: null, range: [0, 319], }, segments: [], @@ -77,7 +77,7 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "a-spa-0128k-aac.mp4"], + url: null, range: [0, 745], }, segments: [], @@ -98,7 +98,7 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "a-deu-0128k-aac.mp4"], + url: null, range: [0, 745], }, segments: [], @@ -119,7 +119,7 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "a-fra-0128k-aac.mp4"], + url: null, range: [0, 745], }, segments: [], @@ -140,7 +140,7 @@ export default { mimeType: "audio/webm", index: { init: { - mediaURLs: [BASE_URL + "a-fra-0128k-libopus.webm"], + url: null, range: [0, 319], }, segments: [], @@ -161,7 +161,7 @@ export default { mimeType: "audio/webm", index: { init: { - mediaURLs: [BASE_URL + "a-deu-0128k-libopus.webm"], + url: null, range: [0, 319], }, segments: [], @@ -182,7 +182,7 @@ export default { mimeType: "audio/webm", index: { init: { - mediaURLs: [BASE_URL + "a-ita-0128k-libopus.webm"], + url: null, range: [0, 319], }, segments: [], @@ -203,7 +203,7 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "a-ita-0128k-aac.mp4"], + url: null, range: [0, 745], }, segments: [], @@ -224,7 +224,7 @@ export default { mimeType: "audio/webm", index: { init: { - mediaURLs: [BASE_URL + "a-spa-0128k-libopus.webm"], + url: null, range: [0, 319], }, segments: [], @@ -246,11 +246,11 @@ export default { mimeType: "text/vtt", index: { init: { - mediaURLs: null, + url: null, }, segments: [ { - mediaURLs: [BASE_URL + "s-en.webvtt"], + url: "", time: 0, duration: 60.022, timescale: 1, @@ -272,11 +272,11 @@ export default { mimeType: "text/vtt", index: { init: { - mediaURLs: null, + url: null, }, segments: [ { - mediaURLs: [BASE_URL + "s-el.webvtt"], + url: "", time: 0, duration: 60.022, timescale: 1, @@ -298,11 +298,11 @@ export default { mimeType: "text/vtt", index: { init: { - mediaURLs: null, + url: null, }, segments: [ { - mediaURLs: [BASE_URL + "s-fr.webvtt"], + url: "", time: 0, duration: 60.022, timescale: 1, @@ -324,11 +324,11 @@ export default { mimeType: "text/vtt", index: { init: { - mediaURLs: null, + url: null, }, segments: [ { - mediaURLs: [BASE_URL + "s-pt-BR.webvtt"], + url: "", time: 0, duration: 60.022, timescale: 1, @@ -353,7 +353,7 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "v-0144p-0100k-libx264.mp4"], + url: null, range: [0, 806], }, segments: [], @@ -369,7 +369,7 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "v-0240p-0400k-libx264.mp4"], + url: null, range: [0, 808], }, segments: [], @@ -385,7 +385,7 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "v-0360p-0750k-libx264.mp4"], + url: null, range: [0, 809], }, segments: [], @@ -401,7 +401,7 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "v-0480p-1000k-libx264.mp4"], + url: null, range: [0, 808], }, segments: [], @@ -417,7 +417,7 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "v-0576p-1400k-libx264.mp4"], + url: null, range: [0, 807], }, segments: [], @@ -438,7 +438,7 @@ export default { mimeType: "video/webm", index: { init: { - mediaURLs: [BASE_URL + "v-0144p-0100k-vp9.webm"], + url: null, range: [0, 293], }, segments: [], @@ -454,7 +454,7 @@ export default { mimeType: "video/webm", index: { init: { - mediaURLs: [BASE_URL + "v-0240p-0300k-vp9.webm"], + url: null, range: [0, 295], }, segments: [], @@ -470,7 +470,7 @@ export default { mimeType: "video/webm", index: { init: { - mediaURLs: [BASE_URL + "v-0360p-0550k-vp9.webm"], + url: null, range: [0, 297], }, segments: [], @@ -486,7 +486,7 @@ export default { mimeType: "video/webm", index: { init: { - mediaURLs: [BASE_URL + "v-0480p-0750k-vp9.webm"], + url: null, range: [0, 297], }, segments: [], @@ -502,7 +502,7 @@ export default { mimeType: "video/webm", index: { init: { - mediaURLs: [BASE_URL + "v-0576p-1000k-vp9.webm"], + url: null, range: [0, 297], }, segments: [], diff --git a/tests/contents/DASH_static_SegmentTemplate_Multi_Periods/different_types_infos.js b/tests/contents/DASH_static_SegmentTemplate_Multi_Periods/different_types_infos.js index bacb312027..8d764863d8 100644 --- a/tests/contents/DASH_static_SegmentTemplate_Multi_Periods/different_types_infos.js +++ b/tests/contents/DASH_static_SegmentTemplate_Multi_Periods/different_types_infos.js @@ -33,20 +33,20 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-aaclc-.mp4"], + url: "mp4-live-periods-aaclc-.mp4", }, segments: [ { time: 0, duration: 440029 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-aaclc-1.m4s"], + url: "mp4-live-periods-aaclc-1.m4s", }, { time: 440029 / 44100, duration: 440029 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-aaclc-2.m4s"], + url: "mp4-live-periods-aaclc-2.m4s", }, ], // ... @@ -69,20 +69,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_low-.mp4"], + url: "mp4-live-periods-h264bl_low-.mp4", }, segments: [ { time: 0, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_low-1.m4s"], + url: "mp4-live-periods-h264bl_low-1.m4s", }, { time: 250000 / 25000, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_low-2.m4s"], + url: "mp4-live-periods-h264bl_low-2.m4s", }, // ... ], @@ -99,20 +99,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_mid-.mp4"], + url: "mp4-live-periods-h264bl_mid-.mp4", }, segments: [ { time: 0, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_mid-1.m4s"], + url: "mp4-live-periods-h264bl_mid-1.m4s", }, { time: 250000 / 25000, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_mid-2.m4s"], + url: "mp4-live-periods-h264bl_mid-2.m4s", }, // ... ], @@ -129,20 +129,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_hd-.mp4"], + url: "mp4-live-periods-h264bl_hd-.mp4", }, segments: [ { time: 0, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_hd-1.m4s"], + url: "mp4-live-periods-h264bl_hd-1.m4s", }, { time: 250000 / 25000, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_hd-2.m4s"], + url: "mp4-live-periods-h264bl_hd-2.m4s", }, // ... ], @@ -159,20 +159,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_full-.mp4"], + url: "mp4-live-periods-h264bl_full-.mp4", }, segments: [ { time: 0, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_full-1.m4s"], + url: "mp4-live-periods-h264bl_full-1.m4s", }, { time: 250000 / 25000, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_full-2.m4s"], + url: "mp4-live-periods-h264bl_full-2.m4s", }, // ... ], @@ -201,20 +201,20 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-aaclc-.mp4"], + url: "mp4-live-periods-aaclc-.mp4", }, segments: [ { time: 120, duration: 440029 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-aaclc-13.m4s"], + url: "mp4-live-periods-aaclc-13.m4s", }, { time: 120 + (440029 / 44100), duration: 440029 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-aaclc-14.m4s"], + url: "mp4-live-periods-aaclc-14.m4s", }, ], // ... @@ -237,20 +237,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_low-.mp4"], + url: "mp4-live-periods-h264bl_low-.mp4", }, segments: [ { time: 12 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_low-13.m4s"], + url: "mp4-live-periods-h264bl_low-13.m4s", }, { time: 13 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_low-14.m4s"], + url: "mp4-live-periods-h264bl_low-14.m4s", }, // ... ], @@ -267,20 +267,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_mid-.mp4"], + url: "mp4-live-periods-h264bl_mid-.mp4", }, segments: [ { time: 12 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_mid-13.m4s"], + url: "mp4-live-periods-h264bl_mid-13.m4s", }, { time: 13 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_mid-14.m4s"], + url: "mp4-live-periods-h264bl_mid-14.m4s", }, // ... ], @@ -297,20 +297,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_hd-.mp4"], + url: "mp4-live-periods-h264bl_hd-.mp4", }, segments: [ { time: 12 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_hd-13.m4s"], + url: "mp4-live-periods-h264bl_hd-13.m4s", }, { time: 13 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_hd-14.m4s"], + url: "mp4-live-periods-h264bl_hd-14.m4s", }, // ... ], @@ -327,20 +327,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_full-.mp4"], + url: "mp4-live-periods-h264bl_full-.mp4", }, segments: [ { time: 12 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_full-13.m4s"], + url: "mp4-live-periods-h264bl_full-13.m4s", }, { time: 13 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_full-14.m4s"], + url: "mp4-live-periods-h264bl_full-14.m4s", }, // ... ], diff --git a/tests/contents/DASH_static_SegmentTemplate_Multi_Periods/discontinuity_between_periods_infos.js b/tests/contents/DASH_static_SegmentTemplate_Multi_Periods/discontinuity_between_periods_infos.js index 0fb1d7ef93..3f8d220e93 100644 --- a/tests/contents/DASH_static_SegmentTemplate_Multi_Periods/discontinuity_between_periods_infos.js +++ b/tests/contents/DASH_static_SegmentTemplate_Multi_Periods/discontinuity_between_periods_infos.js @@ -31,20 +31,20 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-aaclc-.mp4"], + url: "mp4-live-periods-aaclc-.mp4", }, segments: [ { time: 0, duration: 440029 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-aaclc-1.m4s"], + url: "mp4-live-periods-aaclc-1.m4s", }, { time: 440029 / 44100, duration: 440029 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-aaclc-2.m4s"], + url: "mp4-live-periods-aaclc-2.m4s", }, ], // ... @@ -66,20 +66,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_low-.mp4"], + url: "mp4-live-periods-h264bl_low-.mp4", }, segments: [ { time: 0, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_low-1.m4s"], + url: "mp4-live-periods-h264bl_low-1.m4s", }, { time: 250000 / 25000, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_low-2.m4s"], + url: "mp4-live-periods-h264bl_low-2.m4s", }, // ... ], @@ -95,20 +95,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_mid-.mp4"], + url: "mp4-live-periods-h264bl_mid-.mp4", }, segments: [ { time: 0, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_mid-1.m4s"], + url: "mp4-live-periods-h264bl_mid-1.m4s", }, { time: 250000 / 25000, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_mid-2.m4s"], + url: "mp4-live-periods-h264bl_mid-2.m4s", }, // ... ], @@ -124,20 +124,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_hd-.mp4"], + url: "mp4-live-periods-h264bl_hd-.mp4", }, segments: [ { time: 0, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_hd-1.m4s"], + url: "mp4-live-periods-h264bl_hd-1.m4s", }, { time: 250000 / 25000, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_hd-2.m4s"], + url: "mp4-live-periods-h264bl_hd-2.m4s", }, // ... ], @@ -153,20 +153,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_full-.mp4"], + url: "mp4-live-periods-h264bl_full-.mp4", }, segments: [ { time: 0, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_full-1.m4s"], + url: "mp4-live-periods-h264bl_full-1.m4s", }, { time: 250000 / 25000, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_full-2.m4s"], + url: "mp4-live-periods-h264bl_full-2.m4s", }, // ... ], @@ -193,20 +193,20 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-aaclc-.mp4"], + url: "mp4-live-periods-aaclc-.mp4", }, segments: [ { time: 120, duration: 440029 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-aaclc-13.m4s"], + url: "mp4-live-periods-aaclc-13.m4s", }, { time: 120 + (440029 / 44100), duration: 440029 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-aaclc-14.m4s"], + url: "mp4-live-periods-aaclc-14.m4s", }, ], // ... @@ -228,20 +228,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_low-.mp4"], + url: "mp4-live-periods-h264bl_low-.mp4", }, segments: [ { time: 12 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_low-13.m4s"], + url: "mp4-live-periods-h264bl_low-13.m4s", }, { time: 13 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_low-14.m4s"], + url: "mp4-live-periods-h264bl_low-14.m4s", }, // ... ], @@ -257,20 +257,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_mid-.mp4"], + url: "mp4-live-periods-h264bl_mid-.mp4", }, segments: [ { time: 12 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_mid-13.m4s"], + url: "mp4-live-periods-h264bl_mid-13.m4s", }, { time: 13 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_mid-14.m4s"], + url: "mp4-live-periods-h264bl_mid-14.m4s", }, // ... ], @@ -286,20 +286,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_hd-.mp4"], + url: "mp4-live-periods-h264bl_hd-.mp4", }, segments: [ { time: 12 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_hd-13.m4s"], + url: "mp4-live-periods-h264bl_hd-13.m4s", }, { time: 13 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_hd-14.m4s"], + url: "mp4-live-periods-h264bl_hd-14.m4s", }, // ... ], @@ -315,20 +315,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_full-.mp4"], + url: "mp4-live-periods-h264bl_full-.mp4", }, segments: [ { time: 12 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_full-13.m4s"], + url: "mp4-live-periods-h264bl_full-13.m4s", }, { time: 13 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_full-14.m4s"], + url: "mp4-live-periods-h264bl_full-14.m4s", }, // ... ], diff --git a/tests/contents/DASH_static_SegmentTemplate_Multi_Periods/infos.js b/tests/contents/DASH_static_SegmentTemplate_Multi_Periods/infos.js index c9dbdb897f..c909cab8a7 100644 --- a/tests/contents/DASH_static_SegmentTemplate_Multi_Periods/infos.js +++ b/tests/contents/DASH_static_SegmentTemplate_Multi_Periods/infos.js @@ -33,20 +33,20 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-aaclc-.mp4"], + url: "mp4-live-periods-aaclc-.mp4", }, segments: [ { time: 0, duration: 440029 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-aaclc-1.m4s"], + url: "mp4-live-periods-aaclc-1.m4s", }, { time: 440029 / 44100, duration: 440029 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-aaclc-2.m4s"], + url: "mp4-live-periods-aaclc-2.m4s", }, ], // ... @@ -69,20 +69,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_low-.mp4"], + url: "mp4-live-periods-h264bl_low-.mp4", }, segments: [ { time: 0, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_low-1.m4s"], + url: "mp4-live-periods-h264bl_low-1.m4s", }, { time: 250000 / 25000, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_low-2.m4s"], + url: "mp4-live-periods-h264bl_low-2.m4s", }, // ... ], @@ -99,20 +99,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_mid-.mp4"], + url: "mp4-live-periods-h264bl_mid-.mp4", }, segments: [ { time: 0, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_mid-1.m4s"], + url: "mp4-live-periods-h264bl_mid-1.m4s", }, { time: 250000 / 25000, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_mid-2.m4s"], + url: "mp4-live-periods-h264bl_mid-2.m4s", }, // ... ], @@ -129,20 +129,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_hd-.mp4"], + url: "mp4-live-periods-h264bl_hd-.mp4", }, segments: [ { time: 0, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_hd-1.m4s"], + url: "mp4-live-periods-h264bl_hd-1.m4s", }, { time: 250000 / 25000, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_hd-2.m4s"], + url: "mp4-live-periods-h264bl_hd-2.m4s", }, // ... ], @@ -159,20 +159,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_full-.mp4"], + url: "mp4-live-periods-h264bl_full-.mp4", }, segments: [ { time: 0, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_full-1.m4s"], + url: "mp4-live-periods-h264bl_full-1.m4s", }, { time: 250000 / 25000, duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_full-2.m4s"], + url: "mp4-live-periods-h264bl_full-2.m4s", }, // ... ], @@ -200,20 +200,20 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-aaclc-.mp4"], + url: "mp4-live-periods-aaclc-.mp4", }, segments: [ { time: 120, duration: 440029 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-aaclc-13.m4s"], + url: "mp4-live-periods-aaclc-13.m4s", }, { time: 120 + (440029 / 44100), duration: 440029 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-aaclc-14.m4s"], + url: "mp4-live-periods-aaclc-14.m4s", }, ], // ... @@ -236,20 +236,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_low-.mp4"], + url: "mp4-live-periods-h264bl_low-.mp4", }, segments: [ { time: 12 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_low-13.m4s"], + url: "mp4-live-periods-h264bl_low-13.m4s", }, { time: 13 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_low-14.m4s"], + url: "mp4-live-periods-h264bl_low-14.m4s", }, // ... ], @@ -266,20 +266,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_mid-.mp4"], + url: "mp4-live-periods-h264bl_mid-.mp4", }, segments: [ { time: 12 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_mid-13.m4s"], + url: "mp4-live-periods-h264bl_mid-13.m4s", }, { time: 13 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_mid-14.m4s"], + url: "mp4-live-periods-h264bl_mid-14.m4s", }, // ... ], @@ -296,20 +296,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_hd-.mp4"], + url: "mp4-live-periods-h264bl_hd-.mp4", }, segments: [ { time: 12 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_hd-13.m4s"], + url: "mp4-live-periods-h264bl_hd-13.m4s", }, { time: 13 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_hd-14.m4s"], + url: "mp4-live-periods-h264bl_hd-14.m4s", }, // ... ], @@ -326,20 +326,20 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_full-.mp4"], + url: "mp4-live-periods-h264bl_full-.mp4", }, segments: [ { time: 12 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_full-13.m4s"], + url: "mp4-live-periods-h264bl_full-13.m4s", }, { time: 13 * (250000 / 25000), duration: 250000 / 25000, timescale: 1, - mediaURLs: [BASE_URL + "mp4-live-periods-h264bl_full-14.m4s"], + url: "mp4-live-periods-h264bl_full-14.m4s", }, // ... ], diff --git a/tests/contents/DASH_static_SegmentTimeline/discontinuity.js b/tests/contents/DASH_static_SegmentTimeline/discontinuity.js index 0a1fd92116..97910b1e69 100644 --- a/tests/contents/DASH_static_SegmentTimeline/discontinuity.js +++ b/tests/contents/DASH_static_SegmentTimeline/discontinuity.js @@ -30,26 +30,26 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-audio=128000.dash"], + url: "ateam-audio=128000.dash", }, segments: [ { time: 0, duration: 177341 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-audio=128000-0.dash"], + url: "ateam-audio=128000-0.dash", }, { time: 177341 / 44100, duration: 176128 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-audio=128000-177341.dash"], + url: "ateam-audio=128000-177341.dash", }, { time: 353469 / 44100, duration: 177152 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-audio=128000-353469.dash"], + url: "ateam-audio=128000-353469.dash", }, ], // ... @@ -70,26 +70,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=400000.dash"], + url: "ateam-video=400000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=400000-0.dash"], + url: "ateam-video=400000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=400000-360360.dash"], + url: "ateam-video=400000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=400000-720720.dash"], + url: "ateam-video=400000-720720.dash", }, // ... ], @@ -104,26 +104,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=795000.dash"], + url: "ateam-video=795000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=795000-0.dash"], + url: "ateam-video=795000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=795000-360360.dash"], + url: "ateam-video=795000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=795000-720720.dash"], + url: "ateam-video=795000-720720.dash", }, // ... ], @@ -138,26 +138,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=1193000.dash"], + url: "ateam-video=1193000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1193000-0.dash"], + url: "ateam-video=1193000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1193000-360360.dash"], + url: "ateam-video=1193000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1193000-720720.dash"], + url: "ateam-video=1193000-720720.dash", }, // ... ], @@ -172,26 +172,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=1996000.dash"], + url: "ateam-video=1996000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1996000-0.dash"], + url: "ateam-video=1996000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1996000-360360.dash"], + url: "ateam-video=1996000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1996000-720720.dash"], + url: "ateam-video=1996000-720720.dash", }, // ... ], diff --git a/tests/contents/DASH_static_SegmentTimeline/infos.js b/tests/contents/DASH_static_SegmentTimeline/infos.js index 6d067aad3b..53c679479a 100644 --- a/tests/contents/DASH_static_SegmentTimeline/infos.js +++ b/tests/contents/DASH_static_SegmentTimeline/infos.js @@ -32,26 +32,26 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-audio=128000.dash"], + url: "ateam-audio=128000.dash", }, segments: [ { time: 0, duration: 177341 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-audio=128000-0.dash"], + url: "ateam-audio=128000-0.dash", }, { time: 177341 / 44100, duration: 176128 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-audio=128000-177341.dash"], + url: "ateam-audio=128000-177341.dash", }, { time: 353469 / 44100, duration: 177152 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-audio=128000-353469.dash"], + url: "ateam-audio=128000-353469.dash", }, ], // ... @@ -73,26 +73,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=400000.dash"], + url: "ateam-video=400000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=400000-0.dash"], + url: "ateam-video=400000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=400000-360360.dash"], + url: "ateam-video=400000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=400000-720720.dash"], + url: "ateam-video=400000-720720.dash", }, // ... ], @@ -108,26 +108,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=795000.dash"], + url: "ateam-video=795000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=795000-0.dash"], + url: "ateam-video=795000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=795000-360360.dash"], + url: "ateam-video=795000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=795000-720720.dash"], + url: "ateam-video=795000-720720.dash", }, // ... ], @@ -143,26 +143,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=1193000.dash"], + url: "ateam-video=1193000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1193000-0.dash"], + url: "ateam-video=1193000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1193000-360360.dash"], + url: "ateam-video=1193000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1193000-720720.dash"], + url: "ateam-video=1193000-720720.dash", }, // ... ], @@ -178,26 +178,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=1996000.dash"], + url: "ateam-video=1996000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1996000-0.dash"], + url: "ateam-video=1996000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1996000-360360.dash"], + url: "ateam-video=1996000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1996000-720720.dash"], + url: "ateam-video=1996000-720720.dash", }, // ... ], diff --git a/tests/contents/DASH_static_SegmentTimeline/not_starting_at_0.js b/tests/contents/DASH_static_SegmentTimeline/not_starting_at_0.js index 0368da748e..57a384c21e 100644 --- a/tests/contents/DASH_static_SegmentTimeline/not_starting_at_0.js +++ b/tests/contents/DASH_static_SegmentTimeline/not_starting_at_0.js @@ -32,26 +32,26 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-audio=128000.dash"], + url: "ateam-audio=128000.dash", }, segments: [ { time: 530621 / 44100, duration: 176128 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-audio=128000-530621.dash"], + url: "ateam-audio=128000-530621.dash", }, { time: 706749 / 44100, duration: 177152 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-audio=128000-706749.dash"], + url: "ateam-audio=128000-706749.dash", }, { time: 883901 / 44100, duration: 176128 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-audio=128000-883901.dash"], + url: "ateam-audio=128000-883901.dash", }, ], // ... @@ -74,26 +74,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=400000.dash"], + url: "ateam-video=400000.dash", }, segments: [ { time: 12012 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=400000-12012.dash"], + url: "ateam-video=400000-12012.dash", }, { time: 16016 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=400000-16016.dash"], + url: "ateam-video=400000-16016.dash", }, { time: 20020 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=400000-20020.dash"], + url: "ateam-video=400000-20020.dash", }, // ... ], @@ -109,26 +109,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=795000.dash"], + url: "ateam-video=795000.dash", }, segments: [ { time: 12012 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=795000-12012.dash"], + url: "ateam-video=795000-12012.dash", }, { time: 16016 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=795000-16016.dash"], + url: "ateam-video=795000-16016.dash", }, { time: 20020 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=795000-20020.dash"], + url: "ateam-video=795000-20020.dash", }, // ... ], @@ -144,26 +144,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=1193000.dash"], + url: "ateam-video=1193000.dash", }, segments: [ { time: 12012 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1193000-12012.dash"], + url: "ateam-video=1193000-12012.dash", }, { time: 16016 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1193000-16016.dash"], + url: "ateam-video=1193000-16016.dash", }, { time: 20020 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1193000-20020.dash"], + url: "ateam-video=1193000-20020.dash", }, // ... ], @@ -179,26 +179,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=1996000.dash"], + url: "ateam-video=1996000.dash", }, segments: [ { time: 12012 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1996000-12012.dash"], + url: "ateam-video=1996000-12012.dash", }, { time: 16016 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1996000-16016.dash"], + url: "ateam-video=1996000-16016.dash", }, { time: 20020 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1996000-20020.dash"], + url: "ateam-video=1996000-20020.dash", }, // ... ], diff --git a/tests/contents/DASH_static_SegmentTimeline/segment_template_inheritance_as_rep.js b/tests/contents/DASH_static_SegmentTimeline/segment_template_inheritance_as_rep.js index 56e0bc4b8c..234d9cb6c4 100644 --- a/tests/contents/DASH_static_SegmentTimeline/segment_template_inheritance_as_rep.js +++ b/tests/contents/DASH_static_SegmentTimeline/segment_template_inheritance_as_rep.js @@ -32,26 +32,26 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-audio=128000.dash"], + url: "ateam-audio=128000.dash", }, segments: [ { time: 0, duration: 177341 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-audio=128000-0.dash"], + url: "ateam-audio=128000-0.dash", }, { time: 177341 / 44100, duration: 176128 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-audio=128000-177341.dash"], + url: "ateam-audio=128000-177341.dash", }, { time: 353469 / 44100, duration: 177152 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-audio=128000-353469.dash"], + url: "ateam-audio=128000-353469.dash", }, ], // ... @@ -73,26 +73,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=400000.dash"], + url: "ateam-video=400000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=400000-0.dash"], + url: "ateam-video=400000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=400000-360360.dash"], + url: "ateam-video=400000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=400000-720720.dash"], + url: "ateam-video=400000-720720.dash", }, // ... ], @@ -108,26 +108,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=795000.dash"], + url: "ateam-video=795000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=795000-0.dash"], + url: "ateam-video=795000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=795000-360360.dash"], + url: "ateam-video=795000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=795000-720720.dash"], + url: "ateam-video=795000-720720.dash", }, // ... ], @@ -143,26 +143,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=1193000.dash"], + url: "ateam-video=1193000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1193000-0.dash"], + url: "ateam-video=1193000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1193000-360360.dash"], + url: "ateam-video=1193000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1193000-720720.dash"], + url: "ateam-video=1193000-720720.dash", }, // ... ], @@ -178,26 +178,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=1996000.dash"], + url: "ateam-video=1996000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1996000-0.dash"], + url: "ateam-video=1996000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1996000-360360.dash"], + url: "ateam-video=1996000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1996000-720720.dash"], + url: "ateam-video=1996000-720720.dash", }, // ... ], diff --git a/tests/contents/DASH_static_SegmentTimeline/segment_template_inheritance_period_as.js b/tests/contents/DASH_static_SegmentTimeline/segment_template_inheritance_period_as.js index 56e0bc4b8c..234d9cb6c4 100644 --- a/tests/contents/DASH_static_SegmentTimeline/segment_template_inheritance_period_as.js +++ b/tests/contents/DASH_static_SegmentTimeline/segment_template_inheritance_period_as.js @@ -32,26 +32,26 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-audio=128000.dash"], + url: "ateam-audio=128000.dash", }, segments: [ { time: 0, duration: 177341 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-audio=128000-0.dash"], + url: "ateam-audio=128000-0.dash", }, { time: 177341 / 44100, duration: 176128 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-audio=128000-177341.dash"], + url: "ateam-audio=128000-177341.dash", }, { time: 353469 / 44100, duration: 177152 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-audio=128000-353469.dash"], + url: "ateam-audio=128000-353469.dash", }, ], // ... @@ -73,26 +73,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=400000.dash"], + url: "ateam-video=400000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=400000-0.dash"], + url: "ateam-video=400000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=400000-360360.dash"], + url: "ateam-video=400000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=400000-720720.dash"], + url: "ateam-video=400000-720720.dash", }, // ... ], @@ -108,26 +108,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=795000.dash"], + url: "ateam-video=795000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=795000-0.dash"], + url: "ateam-video=795000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=795000-360360.dash"], + url: "ateam-video=795000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=795000-720720.dash"], + url: "ateam-video=795000-720720.dash", }, // ... ], @@ -143,26 +143,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=1193000.dash"], + url: "ateam-video=1193000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1193000-0.dash"], + url: "ateam-video=1193000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1193000-360360.dash"], + url: "ateam-video=1193000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1193000-720720.dash"], + url: "ateam-video=1193000-720720.dash", }, // ... ], @@ -178,26 +178,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=1996000.dash"], + url: "ateam-video=1996000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1996000-0.dash"], + url: "ateam-video=1996000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1996000-360360.dash"], + url: "ateam-video=1996000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1996000-720720.dash"], + url: "ateam-video=1996000-720720.dash", }, // ... ], diff --git a/tests/contents/DASH_static_SegmentTimeline/trickmode.js b/tests/contents/DASH_static_SegmentTimeline/trickmode.js index 26724aea4a..387420d6d1 100644 --- a/tests/contents/DASH_static_SegmentTimeline/trickmode.js +++ b/tests/contents/DASH_static_SegmentTimeline/trickmode.js @@ -32,26 +32,26 @@ export default { mimeType: "audio/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-audio=128000.dash"], + url: "ateam-audio=128000.dash", }, segments: [ { time: 0, duration: 177341 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-audio=128000-0.dash"], + url: "ateam-audio=128000-0.dash", }, { time: 177341 / 44100, duration: 176128 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-audio=128000-177341.dash"], + url: "ateam-audio=128000-177341.dash", }, { time: 353469 / 44100, duration: 177152 / 44100, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-audio=128000-353469.dash"], + url: "ateam-audio=128000-353469.dash", }, ], // ... @@ -73,26 +73,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=400000.dash"], + url: "ateam-video=400000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=400000-0.dash"], + url: "ateam-video=400000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=400000-360360.dash"], + url: "ateam-video=400000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=400000-720720.dash"], + url: "ateam-video=400000-720720.dash", }, // ... ], @@ -108,26 +108,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=795000.dash"], + url: "ateam-video=795000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=795000-0.dash"], + url: "ateam-video=795000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=795000-360360.dash"], + url: "ateam-video=795000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=795000-720720.dash"], + url: "ateam-video=795000-720720.dash", }, // ... ], @@ -143,26 +143,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=1193000.dash"], + url: "ateam-video=1193000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1193000-0.dash"], + url: "ateam-video=1193000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1193000-360360.dash"], + url: "ateam-video=1193000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1193000-720720.dash"], + url: "ateam-video=1193000-720720.dash", }, // ... ], @@ -178,26 +178,26 @@ export default { mimeType: "video/mp4", index: { init: { - mediaURLs: [BASE_URL + "dash/ateam-video=1996000.dash"], + url: "ateam-video=1996000.dash", }, segments: [ { time: 0, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1996000-0.dash"], + url: "ateam-video=1996000-0.dash", }, { time: 4004 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1996000-360360.dash"], + url: "ateam-video=1996000-360360.dash", }, { time: 8008 / 1000, duration: 4004 / 1000, timescale: 1, - mediaURLs: [BASE_URL + "dash/ateam-video=1996000-720720.dash"], + url: "ateam-video=1996000-720720.dash", }, // ... ], diff --git a/tests/contents/DASH_static_broken_cenc_in_MPD/infos.js b/tests/contents/DASH_static_broken_cenc_in_MPD/infos.js index 45c7c967ca..0e09eb72ce 100644 --- a/tests/contents/DASH_static_broken_cenc_in_MPD/infos.js +++ b/tests/contents/DASH_static_broken_cenc_in_MPD/infos.js @@ -27,7 +27,7 @@ export default { { bitrate: 260700, codec: "mp4a.40.2", mimeType: "audio/mp4", - index: { init: { mediaURLs: [BASE_URL + "audio.mp4"] }, + index: { init: { url: "audio.mp4" }, segments: [] } }, ] }, ], @@ -39,7 +39,7 @@ export default { width: 960, codec: "avc1.4D401F", mimeType: "video/mp4", - index: { init: { mediaURLs: [BASE_URL + "video.mp4"] }, + index: { init: { url: "video.mp4" }, segments: [] } }, ], }, diff --git a/tests/contents/Smooth_static/custom_attributes.js b/tests/contents/Smooth_static/custom_attributes.js index e3570d6098..d4db1fda33 100644 --- a/tests/contents/Smooth_static/custom_attributes.js +++ b/tests/contents/Smooth_static/custom_attributes.js @@ -37,7 +37,7 @@ const manifestInfos = { time: 0, duration: 20053333 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels96000/Fragmentsaudio_und=0"], + url: "QualityLevels96000/Fragmentsaudio_und=0", }, ], // ... @@ -66,7 +66,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels300000,hardwareProfile=1000/Fragmentsvideo=0"], + url: "QualityLevels300000,hardwareProfile=1000/Fragmentsvideo=0", }, // ... ], @@ -86,7 +86,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels750000,hardwareProfile=2000/Fragmentsvideo=0"], + url: "QualityLevels750000,hardwareProfile=2000/Fragmentsvideo=0", }, // ... ], @@ -106,7 +106,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels1100000,hardwareProfile=3000/Fragmentsvideo=0"], + url: "QualityLevels1100000,hardwareProfile=3000/Fragmentsvideo=0", }, // ... ], @@ -126,7 +126,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels1500000,hardwareProfile=4000/Fragmentsvideo=0"], + url: "QualityLevels1500000,hardwareProfile=4000/Fragmentsvideo=0", }, // ... ], @@ -146,7 +146,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels2100000,hardwareProfile=5000/Fragmentsvideo=0"], + url: "QualityLevels2100000,hardwareProfile=5000/Fragmentsvideo=0", }, // ... ], @@ -166,7 +166,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels3400000,hardwareProfile=6000/Fragmentsvideo=0"], + url: "QualityLevels3400000,hardwareProfile=6000/Fragmentsvideo=0", }, // ... ], @@ -186,7 +186,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels4000000,hardwareProfile=7000/Fragmentsvideo=0"], + url: "QualityLevels4000000,hardwareProfile=7000/Fragmentsvideo=0", }, // ... ], @@ -206,7 +206,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels5000000,hardwareProfile=8000/Fragmentsvideo=0"], + url: "QualityLevels5000000,hardwareProfile=8000/Fragmentsvideo=0", }, // ... ], diff --git a/tests/contents/Smooth_static/empty_text_track.js b/tests/contents/Smooth_static/empty_text_track.js index c299c6f19c..18f7eaf3bc 100644 --- a/tests/contents/Smooth_static/empty_text_track.js +++ b/tests/contents/Smooth_static/empty_text_track.js @@ -56,7 +56,7 @@ const manifestInfos = { time: 0, duration: 20053333 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels96000/Fragmentsaudio_und=0"], + url: "QualityLevels96000/Fragmentsaudio_und=0", }, ], // ... @@ -85,7 +85,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels300000/Fragmentsvideo=0"], + url: "QualityLevels300000/Fragmentsvideo=0", }, // ... ], @@ -105,7 +105,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels750000/Fragmentsvideo=0"], + url: "QualityLevels750000/Fragmentsvideo=0", }, // ... ], @@ -125,7 +125,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels1100000/Fragmentsvideo=0"], + url: "QualityLevels1100000/Fragmentsvideo=0", }, // ... ], @@ -145,7 +145,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels1500000/Fragmentsvideo=0"], + url: "QualityLevels1500000/Fragmentsvideo=0", }, // ... ], @@ -165,7 +165,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels2100000/Fragmentsvideo=0"], + url: "QualityLevels2100000/Fragmentsvideo=0", }, // ... ], @@ -185,7 +185,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels3400000/Fragmentsvideo=0"], + url: "QualityLevels3400000/Fragmentsvideo=0", }, // ... ], @@ -205,7 +205,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels4000000/Fragmentsvideo=0"], + url: "QualityLevels4000000/Fragmentsvideo=0", }, // ... ], @@ -225,7 +225,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels5000000/Fragmentsvideo=0"], + url: "QualityLevels5000000/Fragmentsvideo=0", }, // ... ], diff --git a/tests/contents/Smooth_static/not_starting_at_0.js b/tests/contents/Smooth_static/not_starting_at_0.js index 20fe6a334d..f7792a96b9 100644 --- a/tests/contents/Smooth_static/not_starting_at_0.js +++ b/tests/contents/Smooth_static/not_starting_at_0.js @@ -37,7 +37,7 @@ const manifestInfos = { time: 60160000 / 10000000, duration: 19840000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels96000/Fragmentsaudio_und=60160000"], + url: "QualityLevels96000/Fragmentsaudio_und=60160000", }, ], // ... @@ -66,7 +66,7 @@ const manifestInfos = { time: 60000000 / 10000000, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels300000/Fragmentsvideo=60000000"], + url: "QualityLevels300000/Fragmentsvideo=60000000", }, // ... ], @@ -86,7 +86,7 @@ const manifestInfos = { time: 60000000 / 10000000, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels750000/Fragmentsvideo=60000000"], + url: "QualityLevels750000/Fragmentsvideo=60000000", }, // ... ], @@ -106,7 +106,7 @@ const manifestInfos = { time: 60000000 / 10000000, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels1100000/Fragmentsvideo=60000000"], + url: "QualityLevels1100000/Fragmentsvideo=60000000", }, // ... ], @@ -126,7 +126,7 @@ const manifestInfos = { time: 60000000 / 10000000, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels1500000/Fragmentsvideo=60000000"], + url: "QualityLevels1500000/Fragmentsvideo=60000000", }, // ... ], @@ -146,7 +146,7 @@ const manifestInfos = { time: 60000000 / 10000000, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels2100000/Fragmentsvideo=60000000"], + url: "QualityLevels2100000/Fragmentsvideo=60000000", }, // ... ], @@ -166,7 +166,7 @@ const manifestInfos = { time: 60000000 / 10000000, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels3400000/Fragmentsvideo=60000000"], + url: "QualityLevels3400000/Fragmentsvideo=60000000", }, // ... ], @@ -186,7 +186,7 @@ const manifestInfos = { time: 60000000 / 10000000, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels4000000/Fragmentsvideo=60000000"], + url: "QualityLevels4000000/Fragmentsvideo=60000000", }, // ... ], @@ -206,7 +206,7 @@ const manifestInfos = { time: 60000000 / 10000000, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels5000000/Fragmentsvideo=60000000"], + url: "QualityLevels5000000/Fragmentsvideo=60000000", }, // ... ], diff --git a/tests/contents/Smooth_static/regular.js b/tests/contents/Smooth_static/regular.js index 71a78fe34c..5d589725e4 100644 --- a/tests/contents/Smooth_static/regular.js +++ b/tests/contents/Smooth_static/regular.js @@ -37,7 +37,7 @@ const manifestInfos = { time: 0, duration: 20053333 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels96000/Fragmentsaudio_und=0"], + url: "QualityLevels96000/Fragmentsaudio_und=0", }, ], // ... @@ -66,7 +66,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels300000/Fragmentsvideo=0"], + url: "QualityLevels300000/Fragmentsvideo=0", }, // ... ], @@ -86,7 +86,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels750000/Fragmentsvideo=0"], + url: "QualityLevels750000/Fragmentsvideo=0", }, // ... ], @@ -106,7 +106,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels1100000/Fragmentsvideo=0"], + url: "QualityLevels1100000/Fragmentsvideo=0", }, // ... ], @@ -126,7 +126,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels1500000/Fragmentsvideo=0"], + url: "QualityLevels1500000/Fragmentsvideo=0", }, // ... ], @@ -146,7 +146,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels2100000/Fragmentsvideo=0"], + url: "QualityLevels2100000/Fragmentsvideo=0", }, // ... ], @@ -166,7 +166,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels3400000/Fragmentsvideo=0"], + url: "QualityLevels3400000/Fragmentsvideo=0", }, // ... ], @@ -186,7 +186,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels4000000/Fragmentsvideo=0"], + url: "QualityLevels4000000/Fragmentsvideo=0", }, // ... ], @@ -206,7 +206,7 @@ const manifestInfos = { time: 0, duration: 20000000 / 10000000, timescale: 1, - mediaURLs: [BASE_URL + "QualityLevels5000000/Fragmentsvideo=0"], + url: "QualityLevels5000000/Fragmentsvideo=0", }, // ... ], diff --git a/tests/integration/scenarios/dash_live.js b/tests/integration/scenarios/dash_live.js index e54d99b1d1..031a452fed 100644 --- a/tests/integration/scenarios/dash_live.js +++ b/tests/integration/scenarios/dash_live.js @@ -89,8 +89,8 @@ describe("DASH live content (SegmentTimeline)", function () { const audioRepresentationIndexInfos = audioRepresentationInfos.index; const initAudioSegment = audioRepresentationIndex.getInitSegment(); expect(typeof initAudioSegment.id).to.equal("string"); - expect(initAudioSegment.mediaURLs).to - .deep.equal(audioRepresentationIndexInfos.init.mediaURLs); + expect(initAudioSegment.url).to + .equal(audioRepresentationIndexInfos.init.url); const nextAudioSegment1 = audioRepresentationIndex .getSegments(1527507769, 4); @@ -103,8 +103,8 @@ describe("DASH live content (SegmentTimeline)", function () { .to.equal(audioRepresentationIndexInfos.segments[0].time); expect(nextAudioSegment1[0].timescale) .to.equal(audioRepresentationIndexInfos.segments[0].timescale); - expect(nextAudioSegment1[0].mediaURLs) - .to.deep.equal(audioRepresentationIndexInfos.segments[0].mediaURLs); + expect(nextAudioSegment1[0].url) + .to.equal(audioRepresentationIndexInfos.segments[0].url); const nextAudioSegment2 = audioRepresentationIndex .getSegments(1527507769, 10); @@ -117,8 +117,8 @@ describe("DASH live content (SegmentTimeline)", function () { .to.equal(audioRepresentationIndexInfos.segments[1].time); expect(nextAudioSegment2[1].timescale) .to.equal(audioRepresentationIndexInfos.segments[1].timescale); - expect(nextAudioSegment2[1].mediaURLs) - .to.deep.equal(audioRepresentationIndexInfos.segments[1].mediaURLs); + expect(nextAudioSegment2[1].url) + .to.equal(audioRepresentationIndexInfos.segments[1].url); expect(audioRepresentationIndex.getSegments(1527507769, 287).length) .to.equal(48); @@ -150,8 +150,8 @@ describe("DASH live content (SegmentTimeline)", function () { const initVideoSegment = videoRepresentationIndex.getInitSegment(); expect(typeof initVideoSegment.id).to.equal("string"); - expect(initVideoSegment.mediaURLs) - .to.deep.equal(videoRepresentationIndexInfos.init.mediaURLs); + expect(initVideoSegment.url) + .to.equal(videoRepresentationIndexInfos.init.url); const nextVideoSegment1 = videoRepresentationIndex .getSegments(1527507769, 4); @@ -164,8 +164,8 @@ describe("DASH live content (SegmentTimeline)", function () { .to.equal(videoRepresentationIndexInfos.segments[0].time); expect(nextVideoSegment1[0].timescale) .to.equal(videoRepresentationIndexInfos.segments[0].timescale); - expect(nextVideoSegment1[0].mediaURLs) - .to.deep.equal(videoRepresentationIndexInfos.segments[0].mediaURLs); + expect(nextVideoSegment1[0].url) + .to.equal(videoRepresentationIndexInfos.segments[0].url); const nextVideoSegment2 = videoRepresentationIndex .getSegments(1527507769, 10); @@ -178,8 +178,8 @@ describe("DASH live content (SegmentTimeline)", function () { .to.equal(videoRepresentationIndexInfos.segments[1].time); expect(nextVideoSegment2[1].timescale) .to.equal(videoRepresentationIndexInfos.segments[1].timescale); - expect(nextVideoSegment2[1].mediaURLs) - .to.deep.equal(videoRepresentationIndexInfos.segments[1].mediaURLs); + expect(nextVideoSegment2[1].url) + .to.equal(videoRepresentationIndexInfos.segments[1].url); expect(videoRepresentationIndex.getSegments(1527507769, 287).length) .to.equal(48); @@ -191,10 +191,15 @@ describe("DASH live content (SegmentTimeline)", function () { expect(xhrMock.getLockedXHR().length).to.be.at.least(2); const requestsDone = xhrMock.getLockedXHR().map(r => r.url); - expect(requestsDone) - .to.include(videoRepresentationIndexInfos.init.mediaURLs[0]); - expect(requestsDone) - .to.include(audioRepresentationIndexInfos.init.mediaURLs[0]); + + const hasRequestedVideoInitSegment = requestsDone.some(r => { + return r.endsWith(videoRepresentationIndexInfos.init.url); + }); + const hasRequestedAudioInitSegment = requestsDone.some(r => { + return r.endsWith(audioRepresentationIndexInfos.init.url); + }); + expect(hasRequestedVideoInitSegment).to.equal(true); + expect(hasRequestedAudioInitSegment).to.equal(true); }); it("should list the right bitrates", async function () { @@ -485,8 +490,8 @@ describe("DASH live content with no timeShiftBufferDepth (SegmentTimeline)", fun const audioRepresentationIndexInfos = audioRepresentationInfos.index; const initAudioSegment = audioRepresentationIndex.getInitSegment(); expect(typeof initAudioSegment.id).to.equal("string"); - expect(initAudioSegment.mediaURLs).to - .deep.equal(audioRepresentationIndexInfos.init.mediaURLs); + expect(initAudioSegment.url).to + .equal(audioRepresentationIndexInfos.init.url); const nextAudioSegment1 = audioRepresentationIndex .getSegments(1527507762, 5); @@ -499,8 +504,8 @@ describe("DASH live content with no timeShiftBufferDepth (SegmentTimeline)", fun .to.equal(audioRepresentationIndexInfos.segments[0].time); expect(nextAudioSegment1[0].timescale) .to.equal(audioRepresentationIndexInfos.segments[0].timescale); - expect(nextAudioSegment1[0].mediaURLs) - .to.deep.equal(audioRepresentationIndexInfos.segments[0].mediaURLs); + expect(nextAudioSegment1[0].url) + .to.equal(audioRepresentationIndexInfos.segments[0].url); const nextAudioSegment2 = audioRepresentationIndex .getSegments(1527507762, 11); @@ -513,8 +518,8 @@ describe("DASH live content with no timeShiftBufferDepth (SegmentTimeline)", fun .to.equal(audioRepresentationIndexInfos.segments[1].time); expect(nextAudioSegment2[1].timescale) .to.equal(audioRepresentationIndexInfos.segments[1].timescale); - expect(nextAudioSegment2[1].mediaURLs) - .to.deep.equal(audioRepresentationIndexInfos.segments[1].mediaURLs); + expect(nextAudioSegment2[1].url) + .to.equal(audioRepresentationIndexInfos.segments[1].url); expect(audioRepresentationIndex.getSegments(1527507762, 294).length) .to.equal(49); @@ -547,8 +552,8 @@ describe("DASH live content with no timeShiftBufferDepth (SegmentTimeline)", fun const initVideoSegment = videoRepresentationIndex.getInitSegment(); expect(typeof initVideoSegment.id).to.equal("string"); - expect(initVideoSegment.mediaURLs) - .to.deep.equal(videoRepresentationIndexInfos.init.mediaURLs); + expect(initVideoSegment.url) + .to.equal(videoRepresentationIndexInfos.init.url); const nextVideoSegment1 = videoRepresentationIndex .getSegments(1527507762, 5); @@ -561,8 +566,8 @@ describe("DASH live content with no timeShiftBufferDepth (SegmentTimeline)", fun .to.equal(videoRepresentationIndexInfos.segments[0].time); expect(nextVideoSegment1[0].timescale) .to.equal(videoRepresentationIndexInfos.segments[0].timescale); - expect(nextVideoSegment1[0].mediaURLs) - .to.deep.equal(videoRepresentationIndexInfos.segments[0].mediaURLs); + expect(nextVideoSegment1[0].url) + .to.equal(videoRepresentationIndexInfos.segments[0].url); const nextVideoSegment2 = videoRepresentationIndex .getSegments(1527507762, 11); @@ -575,8 +580,8 @@ describe("DASH live content with no timeShiftBufferDepth (SegmentTimeline)", fun .to.equal(videoRepresentationIndexInfos.segments[1].time); expect(nextVideoSegment2[1].timescale) .to.equal(videoRepresentationIndexInfos.segments[1].timescale); - expect(nextVideoSegment2[1].mediaURLs) - .to.deep.equal(videoRepresentationIndexInfos.segments[1].mediaURLs); + expect(nextVideoSegment2[1].url) + .to.equal(videoRepresentationIndexInfos.segments[1].url); expect(videoRepresentationIndex.getSegments(1527507762, 294).length) .to.equal(49); @@ -588,10 +593,15 @@ describe("DASH live content with no timeShiftBufferDepth (SegmentTimeline)", fun expect(xhrMock.getLockedXHR().length).to.be.at.least(2); const requestsDone = xhrMock.getLockedXHR().map(r => r.url); - expect(requestsDone) - .to.include(videoRepresentationIndexInfos.init.mediaURLs[0]); - expect(requestsDone) - .to.include(audioRepresentationIndexInfos.init.mediaURLs[0]); + + const hasRequestedVideoInitSegment = requestsDone.some(r => { + return r.endsWith(videoRepresentationIndexInfos.init.url); + }); + const hasRequestedAudioInitSegment = requestsDone.some(r => { + return r.endsWith(audioRepresentationIndexInfos.init.url); + }); + expect(hasRequestedVideoInitSegment).to.equal(true); + expect(hasRequestedAudioInitSegment).to.equal(true); }); it("should list the right bitrates", async function () { diff --git a/tests/integration/scenarios/dash_live_SegmentTemplate.js b/tests/integration/scenarios/dash_live_SegmentTemplate.js index 93ece6fa22..f2a76b0f7b 100644 --- a/tests/integration/scenarios/dash_live_SegmentTemplate.js +++ b/tests/integration/scenarios/dash_live_SegmentTemplate.js @@ -90,8 +90,8 @@ describe("DASH live content (SegmentTemplate)", function() { const audioRepresentationIndexInfos = audioRepresentationInfos.index; const initAudioSegment = audioRepresentationIndex.getInitSegment(); expect(typeof initAudioSegment.id).to.equal("string"); - expect(initAudioSegment.mediaURLs).to - .deep.equal(audioRepresentationIndexInfos.init.mediaURLs); + expect(initAudioSegment.url).to + .equal(audioRepresentationIndexInfos.init.url); const videoRepresentation = adaptations.video[0].representations[0]; const videoRepresentationInfos = firstVideoAdaptationInfos @@ -116,15 +116,20 @@ describe("DASH live content (SegmentTemplate)", function() { const initVideoSegment = videoRepresentationIndex.getInitSegment(); expect(typeof initVideoSegment.id).to.equal("string"); - expect(initVideoSegment.mediaURLs) - .to.deep.equal(videoRepresentationIndexInfos.init.mediaURLs); + expect(initVideoSegment.url) + .to.equal(videoRepresentationIndexInfos.init.url); expect(xhrMock.getLockedXHR().length).to.be.at.least(2); const requestsDone = xhrMock.getLockedXHR().map(r => r.url); - expect(requestsDone) - .to.include(videoRepresentationIndexInfos.init.mediaURLs[0]); - expect(requestsDone) - .to.include(audioRepresentationIndexInfos.init.mediaURLs[0]); + + const hasRequestedVideoInitSegment = requestsDone.some(r => { + return r.endsWith(videoRepresentationIndexInfos.init.url); + }); + const hasRequestedAudioInitSegment = requestsDone.some(r => { + return r.endsWith(audioRepresentationIndexInfos.init.url); + }); + expect(hasRequestedVideoInitSegment).to.equal(true); + expect(hasRequestedAudioInitSegment).to.equal(true); }); it("should list the right bitrates", async function () { @@ -414,8 +419,8 @@ describe("DASH live content without timeShiftBufferDepth (SegmentTemplate)", fun const audioRepresentationIndexInfos = audioRepresentationInfos.index; const initAudioSegment = audioRepresentationIndex.getInitSegment(); expect(typeof initAudioSegment.id).to.equal("string"); - expect(initAudioSegment.mediaURLs).to - .deep.equal(audioRepresentationIndexInfos.init.mediaURLs); + expect(initAudioSegment.url).to + .equal(audioRepresentationIndexInfos.init.url); const videoRepresentation = adaptations.video[0].representations[0]; const videoRepresentationInfos = firstVideoAdaptationInfos @@ -440,15 +445,20 @@ describe("DASH live content without timeShiftBufferDepth (SegmentTemplate)", fun const initVideoSegment = videoRepresentationIndex.getInitSegment(); expect(typeof initVideoSegment.id).to.equal("string"); - expect(initVideoSegment.mediaURLs) - .to.deep.equal(videoRepresentationIndexInfos.init.mediaURLs); + expect(initVideoSegment.url) + .to.equal(videoRepresentationIndexInfos.init.url); expect(xhrMock.getLockedXHR().length).to.be.at.least(2); const requestsDone = xhrMock.getLockedXHR().map(r => r.url); - expect(requestsDone) - .to.include(videoRepresentationIndexInfos.init.mediaURLs[0]); - expect(requestsDone) - .to.include(audioRepresentationIndexInfos.init.mediaURLs[0]); + + const hasRequestedVideoInitSegment = requestsDone.some(r => { + return r.endsWith(videoRepresentationIndexInfos.init.url); + }); + const hasRequestedAudioInitSegment = requestsDone.some(r => { + return r.endsWith(audioRepresentationIndexInfos.init.url); + }); + expect(hasRequestedVideoInitSegment).to.equal(true); + expect(hasRequestedAudioInitSegment).to.equal(true); }); it("should list the right bitrates", async function () { diff --git a/tests/integration/utils/launch_tests_for_content.js b/tests/integration/utils/launch_tests_for_content.js index 1debad6fdf..2783aaa15e 100644 --- a/tests/integration/utils/launch_tests_for_content.js +++ b/tests/integration/utils/launch_tests_for_content.js @@ -39,12 +39,12 @@ import XHRMock from "../../utils/request_mock"; * .width? {number} * .index * .init - * .mediaURLs {string} + * .url {string} * .segments[] * .time {number} * .timescale {number} * .duration {number} - * .mediaURLs {string} + * .url {string} * ``` */ export default function launchTestsForContent(manifestInfos) { @@ -127,20 +127,25 @@ export default function launchTestsForContent(manifestInfos) { expect(xhrMock.getLockedXHR().length) .to.be.at.least(2, "should request two init segments"); const requestsDone = xhrMock.getLockedXHR().map(({ url }) => url); - expect(requestsDone) - .to.include(videoRepresentationInfos.index.init.mediaURLs[0]); - expect(requestsDone) - .to.include(audioRepresentationInfos.index.init.mediaURLs[0]); + + const hasRequestedVideoInitSegment = requestsDone.some(r => { + return r.endsWith(videoRepresentationInfos.index.init.url ?? ""); + }); + const hasRequestedAudioInitSegment = requestsDone.some(r => { + return r.endsWith(audioRepresentationInfos.index.init.url ?? ""); + }); + expect(hasRequestedVideoInitSegment).to.equal(true); + expect(hasRequestedAudioInitSegment).to.equal(true); } else if (!( audioRepresentationInfos && audioRepresentationInfos.index.init) ) { expect(xhrMock.getLockedXHR().length).to.equal(1); expect(xhrMock.getLockedXHR()[0].url).to - .equal(videoRepresentationInfos.index.init.mediaURLs[0]); + .equal(videoRepresentationInfos.index.init.url); } else { expect(xhrMock.getLockedXHR().length).to.equal(1); expect(xhrMock.getLockedXHR()[0].url).to - .equal(audioRepresentationInfos.index.init.mediaURLs[0]); + .equal(audioRepresentationInfos.index.init.url); } } }); @@ -291,8 +296,8 @@ export default function launchTestsForContent(manifestInfos) { const initSegment = reprIndex.getInitSegment(); const initSegmentInfos = reprIndexInfos.init; if (initSegmentInfos) { - expect(initSegment.mediaURLs) - .to.deep.equal(initSegmentInfos.mediaURLs); + expect(initSegment.url) + .to.equal(initSegmentInfos.url); expect(typeof initSegment.id).to.equal("string"); } @@ -319,8 +324,8 @@ export default function launchTestsForContent(manifestInfos) { expect(firstSegment.timescale) .to.equal(reprIndexInfos.segments[0].timescale); - expect(firstSegment.mediaURLs) - .to.deep.equal(reprIndexInfos.segments[0].mediaURLs); + expect(firstSegment.url) + .to.equal(reprIndexInfos.segments[0].url); } } } From f2f07a1a94e7f5a4e6dbabfbb46a4f457caf0d80 Mon Sep 17 00:00:00 2001 From: Paul Berberian Date: Thu, 4 Aug 2022 13:45:04 +0200 Subject: [PATCH 2/6] Move the CDNPrioritizer to the fetchers code --- src/Content_Steering.md | 120 +++++++++--------- .../{init => fetchers}/cdn_prioritizer.ts | 2 +- src/core/fetchers/segment/segment_fetcher.ts | 2 +- .../segment/segment_fetcher_creator.ts | 19 ++- src/core/fetchers/utils/schedule_request.ts | 2 +- src/core/init/initialize_media_source.ts | 20 +-- 6 files changed, 85 insertions(+), 80 deletions(-) rename src/core/{init => fetchers}/cdn_prioritizer.ts (99%) diff --git a/src/Content_Steering.md b/src/Content_Steering.md index 76ffc896cd..3db14d9b4d 100644 --- a/src/Content_Steering.md +++ b/src/Content_Steering.md @@ -23,66 +23,66 @@ This separate file has its own syntax, semantic and refreshing logic. ``` - ./src/parsers/SteeringManifest - +----------------------------------+ - | Content Steering Manifest parser | Parse DCSM[1] into a - +----------------------------------+ transport-agnostic steering - ^ Manifest structure - | - | Uses when parsing - | - | - | ./src/transports - +---------------------------+ - | Transport | - | | - | new functions: | - | - loadSteeringManifest | Construct DCSM[1]'s URL, performs - | - parseSteeringManifest | requests and parses it. - +---------------------------+ - ^ - | - | Relies on - | - | - ./src/core/init | ./src/core/fetchers/steering_manifest - +---------+ +-------------------------+ - | | -----------> | SteeringManifestFetcher | Fetches and parses a Content Steering - | | Creates +-------------------------+ Manifest in a transport-agnostic way - | | ^ + handle retries and error formatting - | | | - | | | Uses an instance of to load, parse and refresh the - | | | Steering Manifest periodically according to its TTL[2] - | | | - | | | - | Init | | ./src/core/init/cdn_prioritizer.ts - | | +----------------+ Signals the priority between multiple - | | -----------> | CdnPrioritizer | potential CDNs for each resource. - | | Creates +----------------+ (This is done on demand, the `CdnPrioritizer` - | | ^ knows of no resource in advance). - | | | - | | | Asks to sort a segment's available base urls by order of - | | | priority (and to filter out those that should not be - | | | used). - | | | Also signals when it should prevent a base url from - | | | being used temporarily (e.g. due to request issues). - | | | - | | | - | | | ./src/core/fetchers/segment - | | +----------------+ - | | -----------> | SegmentFetcher | Fetches and parses a segment in a - +---------+ Creates +----------------+ transport-agnostic way - ^ + handle retries and error formatting - | - | Ask to load segment(s) - | - | ./src/core/stream/representation - +----------------+ - | Representation | Logic behind finding the right segment to - | Stream | load, loading it and pushing it to the buffer. - +----------------+ One RepresentationStream is created per - actively-loaded Period and one per - actively-loaded buffer type. + /parsers/SteeringManifest + +----------------------------------+ + | Content Steering Manifest parser | Parse DCSM[1] into a + +----------------------------------+ transport-agnostic steering + ^ Manifest structure + | + | Uses when parsing + | + | + | /transports + +---------------------------+ + | Transport | + | | + | new functions: | + | - loadSteeringManifest | Construct DCSM[1]'s URL, performs + | - parseSteeringManifest | requests and parses it. + +---------------------------+ + ^ + | + | Relies on + | + | + | /core/fetchers/steering_manifest + +-------------------------+ + | SteeringManifestFetcher | Fetches and parses a Content Steering + +-------------------------+ Manifest in a transport-agnostic way + ^ + handle retries and error formatting + | + | Uses an instance of to load, parse and refresh the + | Steering Manifest periodically according to its TTL[2] + | + | + | /core/fetchers/cdn_prioritizer.ts + +----------------+ Signals the priority between multiple + | CdnPrioritizer | potential CDNs for each resource. + +----------------+ (This is done on demand, the `CdnPrioritizer` + ^ knows of no resource in advance). + | + | Asks to sort a segment's available base urls by order of + | priority (and to filter out those that should not be + | used). + | Also signals when it should prevent a base url from + | being used temporarily (e.g. due to request issues). + | + | + | /core/fetchers/segment + +----------------+ + | SegmentFetcher | Fetches and parses a segment in a + +----------------+ transport-agnostic way + ^ + handle retries and error formatting + | + | Ask to load segment(s) + | + | /core/stream/representation + +----------------+ + | Representation | Logic behind finding the right segment to + | Stream | load, loading it and pushing it to the buffer. + +----------------+ One RepresentationStream is created per + actively-loaded Period and one per + actively-loaded buffer type. [1] DCSM: DASH Content Steering Manifest diff --git a/src/core/init/cdn_prioritizer.ts b/src/core/fetchers/cdn_prioritizer.ts similarity index 99% rename from src/core/init/cdn_prioritizer.ts rename to src/core/fetchers/cdn_prioritizer.ts index 06f7e7fb09..6342f4488b 100644 --- a/src/core/init/cdn_prioritizer.ts +++ b/src/core/fetchers/cdn_prioritizer.ts @@ -37,7 +37,7 @@ import TaskCanceller, { CancellationError, CancellationSignal, } from "../../utils/task_canceller"; -import SteeringManifestFetcher from "../fetchers/steering_manifest"; +import SteeringManifestFetcher from "./steering_manifest"; /** * Class signaling the priority between multiple CDN available for any given diff --git a/src/core/fetchers/segment/segment_fetcher.ts b/src/core/fetchers/segment/segment_fetcher.ts index a1bf12feed..45c4097973 100644 --- a/src/core/fetchers/segment/segment_fetcher.ts +++ b/src/core/fetchers/segment/segment_fetcher.ts @@ -49,8 +49,8 @@ import { IRequestEndCallbackPayload, IRequestProgressCallbackPayload, } from "../../adaptive"; -import CdnPrioritizer from "../../init/cdn_prioritizer"; import { IBufferType } from "../../segment_buffers"; +import CdnPrioritizer from "../cdn_prioritizer"; import errorSelector from "../utils/error_selector"; import { scheduleRequestWithCdns } from "../utils/schedule_request"; diff --git a/src/core/fetchers/segment/segment_fetcher_creator.ts b/src/core/fetchers/segment/segment_fetcher_creator.ts index 7b1fb576d2..aa97bc2e63 100644 --- a/src/core/fetchers/segment/segment_fetcher_creator.ts +++ b/src/core/fetchers/segment/segment_fetcher_creator.ts @@ -15,12 +15,15 @@ */ import config from "../../../config"; +import Manifest from "../../../manifest"; import { ISegmentPipeline, ITransportPipelines, } from "../../../transports"; -import CdnPrioritizer from "../../init/cdn_prioritizer"; +import { CancellationSignal } from "../../../utils/task_canceller"; import { IBufferType } from "../../segment_buffers"; +import CdnPrioritizer from "../cdn_prioritizer"; +import SteeringManifestFetcher from "../steering_manifest"; import applyPrioritizerToSegmentFetcher, { IPrioritizedSegmentFetcher, } from "./prioritized_segment_fetcher"; @@ -89,9 +92,19 @@ export default class SegmentFetcherCreator { */ constructor( transport : ITransportPipelines, - cdnPrioritizer : CdnPrioritizer, - options : ISegmentFetcherCreatorBackoffOptions + manifest : Manifest, + options : ISegmentFetcherCreatorBackoffOptions, + cancelSignal : CancellationSignal ) { + const steeringManifestFetcher = transport.steeringManifest === null ? + null : + new SteeringManifestFetcher(transport.steeringManifest, + { maxRetryOffline: undefined, + maxRetryRegular: undefined }); + const cdnPrioritizer = new CdnPrioritizer(manifest, + steeringManifestFetcher, + cancelSignal); + const { MIN_CANCELABLE_PRIORITY, MAX_HIGH_PRIORITY_LEVEL } = config.getCurrent(); this._transport = transport; diff --git a/src/core/fetchers/utils/schedule_request.ts b/src/core/fetchers/utils/schedule_request.ts index 2b297b6112..78e3753b93 100644 --- a/src/core/fetchers/utils/schedule_request.ts +++ b/src/core/fetchers/utils/schedule_request.ts @@ -32,7 +32,7 @@ import SyncOrAsync, { import TaskCanceller, { CancellationSignal, } from "../../../utils/task_canceller"; -import CdnPrioritizer from "../../init/cdn_prioritizer"; +import CdnPrioritizer from "../cdn_prioritizer"; /** * Called on a loader error. diff --git a/src/core/init/initialize_media_source.ts b/src/core/init/initialize_media_source.ts index 85b5cca9b3..8e7fd5c257 100644 --- a/src/core/init/initialize_media_source.ts +++ b/src/core/init/initialize_media_source.ts @@ -57,10 +57,8 @@ import { ManifestFetcher, SegmentFetcherCreator, } from "../fetchers"; -import SteeringManifestFetcher from "../fetchers/steering_manifest"; import { ITextTrackSegmentBufferOptions } from "../segment_buffers"; import { IAudioTrackSwitchingMode } from "../stream"; -import CdnPrioritizer from "./cdn_prioritizer"; import openMediaSource from "./create_media_source"; import EVENTS from "./events_generators"; import getInitialTime, { @@ -203,7 +201,7 @@ export default function InitializeOnMediaSource( /** Choose the right "Representation" for a given "Adaptation". */ const representationEstimator = AdaptiveRepresentationSelector(adaptiveOptions); - const contentPlaybackCanceller = new TaskCanceller(); + const playbackCanceller = new TaskCanceller(); /** * Create and open a new MediaSource object on the given media element on @@ -264,20 +262,14 @@ export default function InitializeOnMediaSource( const initialTime = getInitialTime(manifest, lowLatencyMode, startAt); log.debug("Init: Initial time calculated:", initialTime); - const steeringManifestFetcher = transport.steeringManifest === null ? - null : - new SteeringManifestFetcher(transport.steeringManifest, - { maxRetryOffline: undefined, - maxRetryRegular: undefined }); - const cdnPrioritizer = new CdnPrioritizer(manifest, - steeringManifestFetcher, - contentPlaybackCanceller.signal); const requestOptions = { lowLatencyMode, requestTimeout: segmentRequestOptions.requestTimeout, maxRetryRegular: segmentRequestOptions.regularError, maxRetryOffline: segmentRequestOptions.offlineError }; - const segmentFetcherCreator = - new SegmentFetcherCreator(transport, cdnPrioritizer, requestOptions); + const segmentFetcherCreator = new SegmentFetcherCreator(transport, + manifest, + requestOptions, + playbackCanceller.signal); const mediaSourceLoader = createMediaSourceLoader({ bufferOptions: objectAssign({ textTrackOptions, drmSystemId }, @@ -394,5 +386,5 @@ export default function InitializeOnMediaSource( })); return observableMerge(loadContent$, mediaError$, drmEvents$.pipe(ignoreElements())) - .pipe(finalize(() => { contentPlaybackCanceller.cancel(); })); + .pipe(finalize(() => { playbackCanceller.cancel(); })); } From 6b0f376e827ee0343c3cf3ae0a59db0658e3fa47 Mon Sep 17 00:00:00 2001 From: Paul Berberian Date: Thu, 4 Aug 2022 16:35:53 +0200 Subject: [PATCH 3/6] tests: do not use null coalescing in tests --- tests/integration/utils/launch_tests_for_content.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/integration/utils/launch_tests_for_content.js b/tests/integration/utils/launch_tests_for_content.js index 2783aaa15e..1ea1b6544f 100644 --- a/tests/integration/utils/launch_tests_for_content.js +++ b/tests/integration/utils/launch_tests_for_content.js @@ -129,10 +129,18 @@ export default function launchTestsForContent(manifestInfos) { const requestsDone = xhrMock.getLockedXHR().map(({ url }) => url); const hasRequestedVideoInitSegment = requestsDone.some(r => { - return r.endsWith(videoRepresentationInfos.index.init.url ?? ""); + const relativeUrl = + videoRepresentationInfos.index.init.url === null ? + "" : + videoRepresentationInfos.index.init.url; + return r.endsWith(relativeUrl); }); const hasRequestedAudioInitSegment = requestsDone.some(r => { - return r.endsWith(audioRepresentationInfos.index.init.url ?? ""); + const relativeUrl = + audioRepresentationInfos.index.init.url === null ? + "" : + audioRepresentationInfos.index.init.url; + return r.endsWith(relativeUrl); }); expect(hasRequestedVideoInitSegment).to.equal(true); expect(hasRequestedAudioInitSegment).to.equal(true); From 169e85405ab8541fb8480a50496eb433427441c3 Mon Sep 17 00:00:00 2001 From: Paul Berberian Date: Mon, 29 Aug 2022 11:02:23 +0200 Subject: [PATCH 4/6] Move SteeringManifestFetcher to CdnPrioritizer --- src/core/fetchers/cdn_prioritizer.ts | 47 ++++++++----------- .../segment/segment_fetcher_creator.ts | 10 +--- 2 files changed, 21 insertions(+), 36 deletions(-) diff --git a/src/core/fetchers/cdn_prioritizer.ts b/src/core/fetchers/cdn_prioritizer.ts index 6342f4488b..9a044ceaa6 100644 --- a/src/core/fetchers/cdn_prioritizer.ts +++ b/src/core/fetchers/cdn_prioritizer.ts @@ -24,6 +24,7 @@ import { } from "../../parsers/manifest"; import { ISteeringManifest } from "../../parsers/SteeringManifest"; import { IPlayerError } from "../../public_types"; +import { ITransportPipelines } from "../../transports"; import arrayFindIndex from "../../utils/array_find_index"; import arrayIncludes from "../../utils/array_includes"; import EventEmitter from "../../utils/event_emitter"; @@ -96,12 +97,12 @@ export default class CdnPrioritizer extends EventEmitter /** * @param {Object} manifest - * @param {Object|null} steeringManifestFetcher + * @param {Object} transport * @param {Object} destroySignal */ constructor( manifest : Manifest, - steeringManifestFetcher : SteeringManifestFetcher | null, + transport : ITransportPipelines, destroySignal : CancellationSignal ) { super(); @@ -110,27 +111,33 @@ export default class CdnPrioritizer extends EventEmitter this._steeringManifestUpdateCanceller = null; this._defaultCdnId = manifest.contentSteering?.defaultId; - let lastContentSteering = manifest.contentSteering; + const steeringManifestFetcher = transport.steeringManifest === null ? + null : + new SteeringManifestFetcher(transport.steeringManifest, + { maxRetryOffline: undefined, + maxRetryRegular: undefined }); + + let currentContentSteering = manifest.contentSteering; manifest.addEventListener("manifestUpdate", () => { - const prevContentSteering = lastContentSteering; - lastContentSteering = manifest.contentSteering; + const prevContentSteering = currentContentSteering; + currentContentSteering = manifest.contentSteering; if (prevContentSteering === null) { - if (lastContentSteering !== null) { + if (currentContentSteering !== null) { if (steeringManifestFetcher === null) { log.warn("CP: Steering manifest declared but no way to fetch it"); } else { log.info("CP: A Steering Manifest is declared in a new Manifest"); this._autoRefreshSteeringManifest(steeringManifestFetcher, - lastContentSteering); + currentContentSteering); } } - } else if (lastContentSteering === null) { + } else if (currentContentSteering === null) { log.info("CP: A Steering Manifest is removed in a new Manifest"); this._steeringManifestUpdateCanceller?.cancel(); this._steeringManifestUpdateCanceller = null; - } else if (prevContentSteering.url !== lastContentSteering.url || - prevContentSteering.proxyUrl !== lastContentSteering.proxyUrl) + } else if (prevContentSteering.url !== currentContentSteering.url || + prevContentSteering.proxyUrl !== currentContentSteering.proxyUrl) { log.info("CP: A Steering Manifest's information changed in a new Manifest"); this._steeringManifestUpdateCanceller?.cancel(); @@ -139,7 +146,7 @@ export default class CdnPrioritizer extends EventEmitter log.warn("CP: Steering manifest changed but no way to fetch it"); } else { this._autoRefreshSteeringManifest(steeringManifestFetcher, - lastContentSteering); + currentContentSteering); } } }, destroySignal); @@ -391,20 +398,6 @@ function indexOfMetadata( if (arr.length === 0) { return -1; } - if (elt.id !== undefined) { - for (let i = 0; i < arr.length; i++) { - const m = arr[i]; - if (m.id === elt.id) { - return i; - } - } - } else { - for (let i = 0; i < arr.length; i++) { - const m = arr[i]; - if (m.baseUrl === elt.baseUrl) { - return i; - } - } - } - return -1; + return elt.id !== undefined ? arrayFindIndex(arr, m => m.id === elt.id) : + arrayFindIndex(arr, m => m.baseUrl === elt.baseUrl); } diff --git a/src/core/fetchers/segment/segment_fetcher_creator.ts b/src/core/fetchers/segment/segment_fetcher_creator.ts index aa97bc2e63..25a65e5557 100644 --- a/src/core/fetchers/segment/segment_fetcher_creator.ts +++ b/src/core/fetchers/segment/segment_fetcher_creator.ts @@ -23,7 +23,6 @@ import { import { CancellationSignal } from "../../../utils/task_canceller"; import { IBufferType } from "../../segment_buffers"; import CdnPrioritizer from "../cdn_prioritizer"; -import SteeringManifestFetcher from "../steering_manifest"; import applyPrioritizerToSegmentFetcher, { IPrioritizedSegmentFetcher, } from "./prioritized_segment_fetcher"; @@ -96,14 +95,7 @@ export default class SegmentFetcherCreator { options : ISegmentFetcherCreatorBackoffOptions, cancelSignal : CancellationSignal ) { - const steeringManifestFetcher = transport.steeringManifest === null ? - null : - new SteeringManifestFetcher(transport.steeringManifest, - { maxRetryOffline: undefined, - maxRetryRegular: undefined }); - const cdnPrioritizer = new CdnPrioritizer(manifest, - steeringManifestFetcher, - cancelSignal); + const cdnPrioritizer = new CdnPrioritizer(manifest, transport, cancelSignal); const { MIN_CANCELABLE_PRIORITY, MAX_HIGH_PRIORITY_LEVEL } = config.getCurrent(); From 0962423bc449e3caf62b66b86171113993927ac2 Mon Sep 17 00:00:00 2001 From: Paul Berberian Date: Thu, 15 Sep 2022 20:12:20 +0200 Subject: [PATCH 5/6] Improve Cdn priorization when every Cdn has already been attempted --- src/core/fetchers/cdn_prioritizer.ts | 5 +- src/core/fetchers/utils/schedule_request.ts | 111 ++++++++++++++------ 2 files changed, 80 insertions(+), 36 deletions(-) diff --git a/src/core/fetchers/cdn_prioritizer.ts b/src/core/fetchers/cdn_prioritizer.ts index 9a044ceaa6..eb6a315018 100644 --- a/src/core/fetchers/cdn_prioritizer.ts +++ b/src/core/fetchers/cdn_prioritizer.ts @@ -72,7 +72,10 @@ export default class CdnPrioritizer extends EventEmitter * CDN for a specific amount of time. */ private _downgradedCdnList : { - /** Metadata of downgraded CDN, in no important order. */ + /** + * Metadata of downgraded CDN, sorted by the time at which they have + * been downgraded. + */ metadata : ICdnMetadata[]; /** * Timeout ID (to give to `clearTimeout`) of elements in the `metadata` diff --git a/src/core/fetchers/utils/schedule_request.ts b/src/core/fetchers/utils/schedule_request.ts index 78e3753b93..012f0af88d 100644 --- a/src/core/fetchers/utils/schedule_request.ts +++ b/src/core/fetchers/utils/schedule_request.ts @@ -207,45 +207,43 @@ export async function scheduleRequestWithCdns( } const missedAttempts : Map = new Map(); - const cdnsResponse = getSortedCdnsToRequest(); - const initialCdnsToRequest = cdnsResponse.syncValue ?? - await cdnsResponse.getValueAsAsync(); - if (initialCdnsToRequest.length === 0) { + const cdnsResponse = getCdnToRequest(); + const initialCdnToRequest = cdnsResponse.syncValue ?? + await cdnsResponse.getValueAsAsync(); + if (initialCdnToRequest === undefined) { throw new Error("No CDN to request"); } - return requestCdn(initialCdnsToRequest[0]); + return requestCdn(initialCdnToRequest); /** - * Returns a sorted and filtered array representing the resource's left - * request-able CDN, by order of preference. - * This array might be empty, in which case there's no CDN left to request - * that resource. + * Returns what is now the most prioritary CDN to request the wanted resource. * - * This array might contain a `null` value, which indicates that the resource - * can be requested through another mean than by doing an HTTP request. + * A return value of `null` indicates that the resource can be requested + * through another mean than by doing an HTTP request. * - * @returns {Object} + * A return value of `undefined` indicates that there's no CDN left to request + * the resource. + * @returns {Object|null|undefined} */ - function getSortedCdnsToRequest() : ISyncOrAsyncValue> { + function getCdnToRequest() : ISyncOrAsyncValue { if (cdns === null) { const nullAttemptObject = missedAttempts.get(null); if (nullAttemptObject !== undefined && nullAttemptObject.isBlacklisted) { - return SyncOrAsync.createSync([]); + return SyncOrAsync.createSync(undefined); } - return SyncOrAsync.createSync([null]); + return SyncOrAsync.createSync(null); } else if (cdnPrioritizer === null) { - return SyncOrAsync.createSync( - cdns.filter(c => missedAttempts.get(c)?.isBlacklisted !== true) - ); + return SyncOrAsync.createSync(getPrioritaryRequestableCdnFromSortedList(cdns)); } else { const prioritized = cdnPrioritizer.getCdnPreferenceForResource(cdns); + // TODO order by `blockedUntil` DESC if `missedAttempts` is not empty if (prioritized.syncValue !== null) { - return SyncOrAsync.createSync(prioritized.syncValue - .filter(u => missedAttempts.get(u)?.isBlacklisted !== true)); + return SyncOrAsync.createSync( + getPrioritaryRequestableCdnFromSortedList(prioritized.syncValue) + ); } - return SyncOrAsync.createAsync(prioritized.getValueAsAsync().then(v => - v.filter(u => missedAttempts.get(u)?.isBlacklisted !== true) - )); + return SyncOrAsync.createAsync(prioritized.getValueAsAsync() + .then(v => getPrioritaryRequestableCdnFromSortedList(v))); } } @@ -327,15 +325,15 @@ export async function scheduleRequestWithCdns( * @returns {Promise} */ async function retryWithNextCdn(prevRequestError : unknown) : Promise { - const currCdnsResponse = getSortedCdnsToRequest(); - const sortedCdns = currCdnsResponse.syncValue ?? - await currCdnsResponse.getValueAsAsync(); + const currCdnResponse = getCdnToRequest(); + const nextCdn = currCdnResponse.syncValue ?? + await currCdnResponse.getValueAsAsync(); if (cancellationSignal.isCancelled) { throw cancellationSignal.cancellationError; } - if (sortedCdns.length === 0) { + if (nextCdn === undefined) { throw prevRequestError; } @@ -344,8 +342,7 @@ export async function scheduleRequestWithCdns( throw cancellationSignal.cancellationError; } - const nextWantedCdn = sortedCdns[0]; - return waitPotentialBackoffAndRequest(nextWantedCdn, prevRequestError); + return waitPotentialBackoffAndRequest(nextCdn, prevRequestError); } /** @@ -379,18 +376,18 @@ export async function scheduleRequestWithCdns( return new Promise((res, rej) => { /* eslint-disable-next-line @typescript-eslint/no-misused-promises */ cdnPrioritizer?.addEventListener("priorityChange", async () => { - const newCdnsResponse = getSortedCdnsToRequest(); - const newSortedCdns = newCdnsResponse.syncValue ?? - await newCdnsResponse.getValueAsAsync(); + const newCdnsResponse = getCdnToRequest(); + const updatedPrioritaryCdn = newCdnsResponse.syncValue ?? + await newCdnsResponse.getValueAsAsync(); if (cancellationSignal.isCancelled) { throw cancellationSignal.cancellationError; } - if (newSortedCdns.length === 0) { + if (updatedPrioritaryCdn === undefined) { return rej(prevRequestError); } - if (newSortedCdns[0] !== nextWantedCdn) { + if (updatedPrioritaryCdn !== nextWantedCdn) { canceller.cancel(); - waitPotentialBackoffAndRequest(newSortedCdns[0], prevRequestError) + waitPotentialBackoffAndRequest(updatedPrioritaryCdn, prevRequestError) .then(res, rej); } }, canceller.signal); @@ -399,6 +396,50 @@ export async function scheduleRequestWithCdns( .then(() => requestCdn(nextWantedCdn).then(res, rej), noop); }); } + + /** + * Takes in input the list of CDN that can be used to request the resource, in + * a general preference order. + * + * Returns the actual most prioritary Cdn to request, based on the current + * attempts already done for that resource. + * + * Returns `undefined` if there's no Cdn left to request the resource. + * @param {Array.} + * @returns {Object|undefined} + */ + function getPrioritaryRequestableCdnFromSortedList( + sortedCdns : ICdnMetadata[] + ) : ICdnMetadata | undefined { + if (missedAttempts.size === 0) { + return sortedCdns[0]; + } + const now = performance.now(); + return sortedCdns + .filter(c => missedAttempts.get(c)?.isBlacklisted !== true) + .reduce(( + acc : [ICdnMetadata, number | undefined] | undefined, + x : ICdnMetadata + ) : [ICdnMetadata, number | undefined] => { + let blockedUntil = missedAttempts.get(x)?.blockedUntil; + if (blockedUntil !== undefined && blockedUntil <= now) { + blockedUntil = undefined; + } + if (acc === undefined) { + return [x, blockedUntil]; + } + if (blockedUntil === undefined) { + if (acc[1] === undefined) { + return acc; + } + return [x, undefined]; + } + + return acc[1] === undefined ? acc : + blockedUntil < acc[1] ? [x, blockedUntil] : + acc; + }, undefined)?.[0]; + } } /** From f9ca32739d9b2a3e8f2d33738f420ed1afab4fdc Mon Sep 17 00:00:00 2001 From: Paul Berberian Date: Wed, 28 Sep 2022 16:07:01 +0200 Subject: [PATCH 6/6] Remove ContentSteering logic to just keep better Cdn priorization for now --- src/Content_Steering.md | 90 ------- src/core/fetchers/cdn_prioritizer.ts | 226 +----------------- .../segment/segment_fetcher_creator.ts | 4 +- src/core/fetchers/steering_manifest/index.ts | 26 -- .../steering_manifest_fetcher.ts | 184 -------------- src/core/fetchers/utils/schedule_request.ts | 34 +-- src/core/init/initialize_media_source.ts | 1 - src/default_config.ts | 15 -- .../video_thumbnail_loader.ts | 1 - src/manifest/manifest.ts | 9 +- .../SteeringManifest/DCSM/parse_dcsm.ts | 67 ------ src/parsers/SteeringManifest/index.ts | 18 -- src/parsers/SteeringManifest/types.ts | 21 -- src/parsers/manifest/dash/common/parse_mpd.ts | 16 +- .../manifest/dash/common/resolve_base_urls.ts | 3 +- .../dash/js-parser/node_parsers/BaseURL.ts | 13 +- .../js-parser/node_parsers/ContentSteering.ts | 63 ----- .../dash/js-parser/node_parsers/MPD.ts | 12 +- .../__tests__/AdaptationSet.test.ts | 12 +- .../manifest/dash/node_parser_types.ts | 42 ---- .../manifest/dash/wasm-parser/rs/events.rs | 11 +- .../wasm-parser/rs/processor/attributes.rs | 14 -- .../dash/wasm-parser/rs/processor/mod.rs | 42 ---- .../dash/wasm-parser/ts/generators/BaseURL.ts | 8 - .../ts/generators/ContentSteering.ts | 59 ----- .../dash/wasm-parser/ts/generators/MPD.ts | 12 - .../manifest/dash/wasm-parser/ts/types.ts | 3 - .../manifest/local/parse_local_manifest.ts | 1 - .../metaplaylist/metaplaylist_parser.ts | 1 - src/parsers/manifest/smooth/create_parser.ts | 1 - src/parsers/manifest/types.ts | 12 +- src/transports/dash/pipelines.ts | 8 +- .../dash/steering_manifest_pipeline.ts | 61 ----- src/transports/local/pipelines.ts | 3 +- src/transports/metaplaylist/pipelines.ts | 3 +- src/transports/smooth/pipelines.ts | 3 +- src/transports/types.ts | 88 ------- src/utils/sync_or_async.ts | 72 ------ 38 files changed, 34 insertions(+), 1225 deletions(-) delete mode 100644 src/Content_Steering.md delete mode 100644 src/core/fetchers/steering_manifest/index.ts delete mode 100644 src/core/fetchers/steering_manifest/steering_manifest_fetcher.ts delete mode 100644 src/parsers/SteeringManifest/DCSM/parse_dcsm.ts delete mode 100644 src/parsers/SteeringManifest/index.ts delete mode 100644 src/parsers/SteeringManifest/types.ts delete mode 100644 src/parsers/manifest/dash/js-parser/node_parsers/ContentSteering.ts delete mode 100644 src/parsers/manifest/dash/wasm-parser/ts/generators/ContentSteering.ts delete mode 100644 src/transports/dash/steering_manifest_pipeline.ts delete mode 100644 src/utils/sync_or_async.ts diff --git a/src/Content_Steering.md b/src/Content_Steering.md deleted file mode 100644 index 3db14d9b4d..0000000000 --- a/src/Content_Steering.md +++ /dev/null @@ -1,90 +0,0 @@ -# Content Steering implementation - -__LAST UPDATE: 2022-08-04__ - -## Overview - -Content steering is a mechanism allowing a content provider to deterministically -prioritize a, or multiple, CDN over others - even during content playback - on -the server-side when multiple CDNs are available to load a given content. - -For example, a distributor may want to rebalance load between multiple servers -while final users are watching the corresponding stream, though many other use -cases and reasons exist. - -As of now, content steering only exist for HLS and DASH OTT streaming -technologies. -In both cases it takes the form of a separate file, in DASH called the "DASH -Content Steering Manifest" (or DCSM), giving the current priority. -This separate file has its own syntax, semantic and refreshing logic. - - -## Architecture in the RxPlayer - - -``` - /parsers/SteeringManifest - +----------------------------------+ - | Content Steering Manifest parser | Parse DCSM[1] into a - +----------------------------------+ transport-agnostic steering - ^ Manifest structure - | - | Uses when parsing - | - | - | /transports - +---------------------------+ - | Transport | - | | - | new functions: | - | - loadSteeringManifest | Construct DCSM[1]'s URL, performs - | - parseSteeringManifest | requests and parses it. - +---------------------------+ - ^ - | - | Relies on - | - | - | /core/fetchers/steering_manifest - +-------------------------+ - | SteeringManifestFetcher | Fetches and parses a Content Steering - +-------------------------+ Manifest in a transport-agnostic way - ^ + handle retries and error formatting - | - | Uses an instance of to load, parse and refresh the - | Steering Manifest periodically according to its TTL[2] - | - | - | /core/fetchers/cdn_prioritizer.ts - +----------------+ Signals the priority between multiple - | CdnPrioritizer | potential CDNs for each resource. - +----------------+ (This is done on demand, the `CdnPrioritizer` - ^ knows of no resource in advance). - | - | Asks to sort a segment's available base urls by order of - | priority (and to filter out those that should not be - | used). - | Also signals when it should prevent a base url from - | being used temporarily (e.g. due to request issues). - | - | - | /core/fetchers/segment - +----------------+ - | SegmentFetcher | Fetches and parses a segment in a - +----------------+ transport-agnostic way - ^ + handle retries and error formatting - | - | Ask to load segment(s) - | - | /core/stream/representation - +----------------+ - | Representation | Logic behind finding the right segment to - | Stream | load, loading it and pushing it to the buffer. - +----------------+ One RepresentationStream is created per - actively-loaded Period and one per - actively-loaded buffer type. - - -[1] DCSM: DASH Content Steering Manifest -[2] TTL: Time To Live: a delay after which a Content Steering Manifest should be refreshed -``` diff --git a/src/core/fetchers/cdn_prioritizer.ts b/src/core/fetchers/cdn_prioritizer.ts index eb6a315018..78eedb164d 100644 --- a/src/core/fetchers/cdn_prioritizer.ts +++ b/src/core/fetchers/cdn_prioritizer.ts @@ -15,39 +15,16 @@ */ import config from "../../config"; -import { formatError } from "../../errors"; -import log from "../../log"; -import Manifest from "../../manifest"; -import { - ICdnMetadata, - IContentSteeringMetadata, -} from "../../parsers/manifest"; -import { ISteeringManifest } from "../../parsers/SteeringManifest"; +import { ICdnMetadata } from "../../parsers/manifest"; import { IPlayerError } from "../../public_types"; -import { ITransportPipelines } from "../../transports"; import arrayFindIndex from "../../utils/array_find_index"; -import arrayIncludes from "../../utils/array_includes"; import EventEmitter from "../../utils/event_emitter"; -import createSharedReference, { - ISharedReference, -} from "../../utils/reference"; -import SyncOrAsync, { - ISyncOrAsyncValue, -} from "../../utils/sync_or_async"; -import TaskCanceller, { - CancellationError, - CancellationSignal, -} from "../../utils/task_canceller"; -import SteeringManifestFetcher from "./steering_manifest"; +import { CancellationSignal } from "../../utils/task_canceller"; /** * Class signaling the priority between multiple CDN available for any given * resource. * - * It might rely behind the hood on a fetched document giving priorities such as - * a Content Steering Manifest and also on issues that appeared with some given - * CDN in the [close] past. - * * This class might perform requests and schedule timeouts by itself to keep its * internal list of CDN priority up-to-date. * When it is not needed anymore, you should call the `dispose` method to clear @@ -56,16 +33,6 @@ import SteeringManifestFetcher from "./steering_manifest"; * @class CdnPrioritizer */ export default class CdnPrioritizer extends EventEmitter { - /** - * Metadata parsed from the last Content Steering Manifest loaded. - * - * `null` either if there's no such Manifest or if it is currently being - * loaded for the first time. - */ - private _lastSteeringManifest : ISteeringManifest | null; - - private _defaultCdnId : string | undefined; - /** * Structure keeping a list of CDN currently downgraded. * Downgraded CDN immediately have a lower priority than any non-downgraded @@ -90,90 +57,12 @@ export default class CdnPrioritizer extends EventEmitter }; /** - * TaskCanceller allowing to abort the process of loading and refreshing the - * Content Steering Manifest. - * Set to `null` when no such process is pending. - */ - private _steeringManifestUpdateCanceller : TaskCanceller | null; - - private _readyState : ISharedReference<"not-ready" | "ready" | "disposed">; - - /** - * @param {Object} manifest - * @param {Object} transport * @param {Object} destroySignal */ - constructor( - manifest : Manifest, - transport : ITransportPipelines, - destroySignal : CancellationSignal - ) { + constructor(destroySignal : CancellationSignal) { super(); - this._lastSteeringManifest = null; this._downgradedCdnList = { metadata: [], timeouts: [] }; - this._steeringManifestUpdateCanceller = null; - this._defaultCdnId = manifest.contentSteering?.defaultId; - - const steeringManifestFetcher = transport.steeringManifest === null ? - null : - new SteeringManifestFetcher(transport.steeringManifest, - { maxRetryOffline: undefined, - maxRetryRegular: undefined }); - - let currentContentSteering = manifest.contentSteering; - - manifest.addEventListener("manifestUpdate", () => { - const prevContentSteering = currentContentSteering; - currentContentSteering = manifest.contentSteering; - if (prevContentSteering === null) { - if (currentContentSteering !== null) { - if (steeringManifestFetcher === null) { - log.warn("CP: Steering manifest declared but no way to fetch it"); - } else { - log.info("CP: A Steering Manifest is declared in a new Manifest"); - this._autoRefreshSteeringManifest(steeringManifestFetcher, - currentContentSteering); - } - } - } else if (currentContentSteering === null) { - log.info("CP: A Steering Manifest is removed in a new Manifest"); - this._steeringManifestUpdateCanceller?.cancel(); - this._steeringManifestUpdateCanceller = null; - } else if (prevContentSteering.url !== currentContentSteering.url || - prevContentSteering.proxyUrl !== currentContentSteering.proxyUrl) - { - log.info("CP: A Steering Manifest's information changed in a new Manifest"); - this._steeringManifestUpdateCanceller?.cancel(); - this._steeringManifestUpdateCanceller = null; - if (steeringManifestFetcher === null) { - log.warn("CP: Steering manifest changed but no way to fetch it"); - } else { - this._autoRefreshSteeringManifest(steeringManifestFetcher, - currentContentSteering); - } - } - }, destroySignal); - - if (manifest.contentSteering !== null) { - if (steeringManifestFetcher === null) { - log.warn("CP: Steering Manifest initially present but no way to fetch it."); - this._readyState = createSharedReference("ready"); - } else { - const readyState = manifest.contentSteering.queryBeforeStart ? "not-ready" : - "ready"; - this._readyState = createSharedReference(readyState); - this._autoRefreshSteeringManifest(steeringManifestFetcher, - manifest.contentSteering); - } - } else { - this._readyState = createSharedReference("ready"); - } destroySignal.register(() => { - this._readyState.setValue("disposed"); - this._readyState.finish(); - this._steeringManifestUpdateCanceller?.cancel(); - this._steeringManifestUpdateCanceller = null; - this._lastSteeringManifest = null; for (const timeout of this._downgradedCdnList.timeouts) { clearTimeout(timeout); } @@ -195,38 +84,20 @@ export default class CdnPrioritizer extends EventEmitter * @param {Array.} everyCdnForResource - Array of ALL available CDN * able to reach the wanted resource - even those which might not be used in * the end. - * @returns {Object} - Array of CDN that can be tried to reach the + * @returns {Array.} - Array of CDN that can be tried to reach the * resource, sorted by order of CDN preference, according to the * `CdnPrioritizer`'s own list of priorities. - * - * This value is wrapped in a `ISyncOrAsyncValue` as in relatively rare - * scenarios, the order can only be known once the steering Manifest has been - * fetched. */ public getCdnPreferenceForResource( everyCdnForResource : ICdnMetadata[] - ) : ISyncOrAsyncValue { + ) : ICdnMetadata[] { if (everyCdnForResource.length <= 1) { // The huge majority of contents have only one CDN available. // Here, prioritizing make no sense. - return SyncOrAsync.createSync(everyCdnForResource); + return everyCdnForResource; } - if (this._readyState.getValue() === "not-ready") { - const val = new Promise((res, rej) => { - this._readyState.onUpdate((readyState) => { - if (readyState === "ready") { - res(this._innerGetCdnPreferenceForResource(everyCdnForResource)); - } else if (readyState === "disposed") { - rej(new CancellationError()); - } - }); - }); - return SyncOrAsync.createAsync(val); - } - return SyncOrAsync.createSync( - this._innerGetCdnPreferenceForResource(everyCdnForResource) - ); + return this._innerGetCdnPreferenceForResource(everyCdnForResource); } /** @@ -245,8 +116,7 @@ export default class CdnPrioritizer extends EventEmitter } const { DEFAULT_CDN_DOWNGRADE_TIME } = config.getCurrent(); - const downgradeTime = this._lastSteeringManifest?.lifetime ?? - DEFAULT_CDN_DOWNGRADE_TIME; + const downgradeTime = DEFAULT_CDN_DOWNGRADE_TIME; this._downgradedCdnList.metadata.push(metadata); const timeout = window.setTimeout(() => { const newIndex = indexOfMetadata(this._downgradedCdnList.metadata, metadata); @@ -280,33 +150,7 @@ export default class CdnPrioritizer extends EventEmitter private _innerGetCdnPreferenceForResource( everyCdnForResource : ICdnMetadata[] ) : ICdnMetadata[] { - let cdnBase; - if (this._lastSteeringManifest !== null) { - const priorities = this._lastSteeringManifest.priorities; - const inSteeringManifest = everyCdnForResource.filter(available => - available.id !== undefined && arrayIncludes(priorities, available.id)); - if (inSteeringManifest.length > 0) { - cdnBase = inSteeringManifest; - } - } - - // (If using the SteeringManifest gave nothing, or if it just didn't exist.) */ - if (cdnBase === undefined) { - // (If a default CDN was indicated, try to use it) */ - if (this._defaultCdnId !== undefined) { - const indexOf = arrayFindIndex(everyCdnForResource, (x) => - x.id !== undefined && x.id === this._defaultCdnId); - if (indexOf >= 0) { - const elem = everyCdnForResource.splice(indexOf, 1)[0]; - everyCdnForResource.unshift(elem); - } - } - - if (cdnBase === undefined) { - cdnBase = everyCdnForResource; - } - } - const [allowedInOrder, downgradedInOrder] = cdnBase + const [allowedInOrder, downgradedInOrder] = everyCdnForResource .reduce((acc : [ICdnMetadata[], ICdnMetadata[]], elt : ICdnMetadata) => { if (this._downgradedCdnList.metadata.some(c => c.id === elt.id && c.baseUrl === elt.baseUrl)) @@ -320,58 +164,6 @@ export default class CdnPrioritizer extends EventEmitter return allowedInOrder.concat(downgradedInOrder); } - private _autoRefreshSteeringManifest( - steeringManifestFetcher : SteeringManifestFetcher, - contentSteering : IContentSteeringMetadata - ) { - if (this._steeringManifestUpdateCanceller === null) { - const steeringManifestUpdateCanceller = new TaskCanceller(); - this._steeringManifestUpdateCanceller = steeringManifestUpdateCanceller; - } - const canceller : TaskCanceller = this._steeringManifestUpdateCanceller; - steeringManifestFetcher.fetch(contentSteering.url, - (err : IPlayerError) => this.trigger("warnings", [err]), - canceller.signal) - .then((parse) => { - const parsed = parse((errs) => this.trigger("warnings", errs)); - const prevSteeringManifest = this._lastSteeringManifest; - this._lastSteeringManifest = parsed; - if (parsed.lifetime > 0) { - const timeout = window.setTimeout(() => { - canceller.signal.deregister(onTimeoutEnd); - this._autoRefreshSteeringManifest(steeringManifestFetcher, contentSteering); - }, parsed.lifetime * 1000); - const onTimeoutEnd = () => { - clearTimeout(timeout); - }; - canceller.signal.register(onTimeoutEnd); - } - if (this._readyState.getValue() === "not-ready") { - this._readyState.setValue("ready"); - } - if (canceller.isUsed) { - return; - } - if (prevSteeringManifest === null || - prevSteeringManifest.priorities.length !== parsed.priorities.length || - prevSteeringManifest.priorities - .some((val, idx) => val !== parsed.priorities[idx])) - { - this.trigger("priorityChange", null); - } - }) - .catch((err) => { - if (err instanceof CancellationError) { - return; - } - const formattedError = formatError(err, { - defaultCode: "NONE", - defaultReason: "Unknown error when fetching and parsing the steering Manifest", - }); - this.trigger("warnings", [formattedError]); - }); - } - /** * @param {number} index */ diff --git a/src/core/fetchers/segment/segment_fetcher_creator.ts b/src/core/fetchers/segment/segment_fetcher_creator.ts index 25a65e5557..dd0fb785b8 100644 --- a/src/core/fetchers/segment/segment_fetcher_creator.ts +++ b/src/core/fetchers/segment/segment_fetcher_creator.ts @@ -15,7 +15,6 @@ */ import config from "../../../config"; -import Manifest from "../../../manifest"; import { ISegmentPipeline, ITransportPipelines, @@ -91,11 +90,10 @@ export default class SegmentFetcherCreator { */ constructor( transport : ITransportPipelines, - manifest : Manifest, options : ISegmentFetcherCreatorBackoffOptions, cancelSignal : CancellationSignal ) { - const cdnPrioritizer = new CdnPrioritizer(manifest, transport, cancelSignal); + const cdnPrioritizer = new CdnPrioritizer(cancelSignal); const { MIN_CANCELABLE_PRIORITY, MAX_HIGH_PRIORITY_LEVEL } = config.getCurrent(); diff --git a/src/core/fetchers/steering_manifest/index.ts b/src/core/fetchers/steering_manifest/index.ts deleted file mode 100644 index f591e5c158..0000000000 --- a/src/core/fetchers/steering_manifest/index.ts +++ /dev/null @@ -1,26 +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 SteeringManifestFetcher, { - ISteeringManifestFetcherSettings, - ISteeringManifestParser, -} from "./steering_manifest_fetcher"; - -export default SteeringManifestFetcher; -export { - ISteeringManifestFetcherSettings, - ISteeringManifestParser, -}; diff --git a/src/core/fetchers/steering_manifest/steering_manifest_fetcher.ts b/src/core/fetchers/steering_manifest/steering_manifest_fetcher.ts deleted file mode 100644 index f74e41690c..0000000000 --- a/src/core/fetchers/steering_manifest/steering_manifest_fetcher.ts +++ /dev/null @@ -1,184 +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 config from "../../../config"; -import { formatError } from "../../../errors"; -import { ISteeringManifest } from "../../../parsers/SteeringManifest"; -import { IPlayerError } from "../../../public_types"; -import { - IRequestedData, - ITransportSteeringManifestPipeline, -} from "../../../transports"; -import { CancellationSignal } from "../../../utils/task_canceller"; -import errorSelector from "../utils/error_selector"; -import { - IBackoffSettings, - scheduleRequestPromise, -} from "../utils/schedule_request"; - -/** Response emitted by a SteeringManifestFetcher fetcher. */ -export type ISteeringManifestParser = - /** Allows to parse a fetched Steering Manifest into a `ISteeringManifest` structure. */ - (onWarnings : ((warnings : IPlayerError[]) => void)) => ISteeringManifest; - -/** Options used by the `SteeringManifestFetcher`. */ -export interface ISteeringManifestFetcherSettings { - /** Maximum number of time a request on error will be retried. */ - maxRetryRegular : number | undefined; - /** Maximum number of time a request be retried when the user is offline. */ - maxRetryOffline : number | undefined; -} - -/** - * Class allowing to facilitate the task of loading and parsing a Content - * Steering Manifest, which is an optional document associated to a content, - * communicating the priority between several CDN. - * @class SteeringManifestFetcher - */ -export default class SteeringManifestFetcher { - private _settings : ISteeringManifestFetcherSettings; - private _pipelines : ITransportSteeringManifestPipeline; - - /** - * Construct a new SteeringManifestFetcher. - * @param {Object} pipelines - Transport pipelines used to perform the - * Content Steering Manifest loading and parsing operations. - * @param {Object} settings - Configure the `SteeringManifestFetcher`. - */ - constructor( - pipelines : ITransportSteeringManifestPipeline, - settings : ISteeringManifestFetcherSettings - ) { - this._pipelines = pipelines; - this._settings = settings; - } - - /** - * (re-)Load the Content Steering Manifest. - * This method does not yet parse it, parsing will then be available through - * a callback available on the response. - * - * You can set an `url` on which that Content Steering Manifest will be - * requested. - * If not set, the regular Content Steering Manifest url - defined on the - * `SteeringManifestFetcher` instanciation - will be used instead. - * - * @param {string|undefined} url - * @param {Function} onRetry - * @param {Object} cancelSignal - * @returns {Promise} - */ - public async fetch( - url : string, - onRetry : (error : IPlayerError) => void, - cancelSignal : CancellationSignal - ) : Promise { - const pipelines = this._pipelines; - const backoffSettings = this._getBackoffSetting((err) => { - onRetry(errorSelector(err)); - }); - const callLoader = () => pipelines.loadSteeringManifest(url, cancelSignal); - const response = await scheduleRequestPromise(callLoader, - backoffSettings, - cancelSignal); - return (onWarnings : ((error : IPlayerError[]) => void)) => { - return this._parseSteeringManifest(response, onWarnings); - }; - } - - /** - * Parse an already loaded Content Steering Manifest. - * - * This method should be reserved for Content Steering Manifests for which no - * request has been done. - * In other cases, it's preferable to go through the `fetch` method, so - * information on the request can be used by the parsing process. - * @param {*} steeringManifest - * @param {Function} onWarnings - * @returns {Observable} - */ - public parse( - steeringManifest : unknown, - onWarnings : (error : IPlayerError[]) => void - ) : ISteeringManifest { - return this._parseSteeringManifest({ responseData: steeringManifest, - size: undefined, - requestDuration: undefined }, - onWarnings); - } - - /** - * Parse a Content Steering Manifest. - * @param {Object} loaded - Information about the loaded Content Steering Manifest. - * @param {Function} onWarnings - * @returns {Observable} - */ - private _parseSteeringManifest( - loaded : IRequestedData, - onWarnings : (error : IPlayerError[]) => void - ) : ISteeringManifest { - try { - return this._pipelines.parseSteeringManifest( - loaded, - function onTransportWarnings(errs) { - const warnings = errs.map(e => formatParsingError(e)); - onWarnings(warnings); - } - ); - } catch (err) { - throw formatParsingError(err); - } - - /** - * Format the given Error and emit it through `obs`. - * Either through a `"warning"` event, if `isFatal` is `false`, or through - * a fatal Observable error, if `isFatal` is set to `true`. - * @param {*} err - * @returns {Error} - */ - function formatParsingError(err : unknown) : IPlayerError { - return formatError(err, { - defaultCode: "PIPELINE_PARSE_ERROR", - defaultReason: "Unknown error when parsing the Content Steering Manifest", - }); - } - } - - /** - * Construct "backoff settings" that can be used with a range of functions - * allowing to perform multiple request attempts - * @param {Function} onRetry - * @returns {Object} - */ - private _getBackoffSetting(onRetry : (err : unknown) => void) : IBackoffSettings { - const { DEFAULT_MAX_CONTENT_STEERING_MANIFEST_REQUEST_RETRY, - DEFAULT_MAX_REQUESTS_RETRY_ON_OFFLINE, - INITIAL_BACKOFF_DELAY_BASE, - MAX_BACKOFF_DELAY_BASE } = config.getCurrent(); - const { maxRetryRegular : ogRegular, - maxRetryOffline : ogOffline } = this._settings; - const baseDelay = INITIAL_BACKOFF_DELAY_BASE.REGULAR; - const maxDelay = MAX_BACKOFF_DELAY_BASE.REGULAR; - const maxRetryRegular = ogRegular ?? - DEFAULT_MAX_CONTENT_STEERING_MANIFEST_REQUEST_RETRY; - const maxRetryOffline = ogOffline ?? DEFAULT_MAX_REQUESTS_RETRY_ON_OFFLINE; - return { onRetry, - baseDelay, - maxDelay, - maxRetryRegular, - maxRetryOffline }; - } -} diff --git a/src/core/fetchers/utils/schedule_request.ts b/src/core/fetchers/utils/schedule_request.ts index 012f0af88d..e67024060f 100644 --- a/src/core/fetchers/utils/schedule_request.ts +++ b/src/core/fetchers/utils/schedule_request.ts @@ -26,9 +26,6 @@ import { ICdnMetadata } from "../../../parsers/manifest"; import cancellableSleep from "../../../utils/cancellable_sleep"; import getFuzzedDelay from "../../../utils/get_fuzzed_delay"; import noop from "../../../utils/noop"; -import SyncOrAsync, { - ISyncOrAsyncValue, -} from "../../../utils/sync_or_async"; import TaskCanceller, { CancellationSignal, } from "../../../utils/task_canceller"; @@ -207,9 +204,7 @@ export async function scheduleRequestWithCdns( } const missedAttempts : Map = new Map(); - const cdnsResponse = getCdnToRequest(); - const initialCdnToRequest = cdnsResponse.syncValue ?? - await cdnsResponse.getValueAsAsync(); + const initialCdnToRequest = getCdnToRequest(); if (initialCdnToRequest === undefined) { throw new Error("No CDN to request"); } @@ -225,25 +220,18 @@ export async function scheduleRequestWithCdns( * the resource. * @returns {Object|null|undefined} */ - function getCdnToRequest() : ISyncOrAsyncValue { + function getCdnToRequest() : ICdnMetadata | null | undefined { if (cdns === null) { const nullAttemptObject = missedAttempts.get(null); if (nullAttemptObject !== undefined && nullAttemptObject.isBlacklisted) { - return SyncOrAsync.createSync(undefined); + return undefined; } - return SyncOrAsync.createSync(null); + return null; } else if (cdnPrioritizer === null) { - return SyncOrAsync.createSync(getPrioritaryRequestableCdnFromSortedList(cdns)); + return getPrioritaryRequestableCdnFromSortedList(cdns); } else { const prioritized = cdnPrioritizer.getCdnPreferenceForResource(cdns); - // TODO order by `blockedUntil` DESC if `missedAttempts` is not empty - if (prioritized.syncValue !== null) { - return SyncOrAsync.createSync( - getPrioritaryRequestableCdnFromSortedList(prioritized.syncValue) - ); - } - return SyncOrAsync.createAsync(prioritized.getValueAsAsync() - .then(v => getPrioritaryRequestableCdnFromSortedList(v))); + return getPrioritaryRequestableCdnFromSortedList(prioritized); } } @@ -325,9 +313,7 @@ export async function scheduleRequestWithCdns( * @returns {Promise} */ async function retryWithNextCdn(prevRequestError : unknown) : Promise { - const currCdnResponse = getCdnToRequest(); - const nextCdn = currCdnResponse.syncValue ?? - await currCdnResponse.getValueAsAsync(); + const nextCdn = getCdnToRequest(); if (cancellationSignal.isCancelled) { throw cancellationSignal.cancellationError; @@ -375,10 +361,8 @@ export async function scheduleRequestWithCdns( const canceller = new TaskCanceller({ cancelOn: cancellationSignal }); return new Promise((res, rej) => { /* eslint-disable-next-line @typescript-eslint/no-misused-promises */ - cdnPrioritizer?.addEventListener("priorityChange", async () => { - const newCdnsResponse = getCdnToRequest(); - const updatedPrioritaryCdn = newCdnsResponse.syncValue ?? - await newCdnsResponse.getValueAsAsync(); + cdnPrioritizer?.addEventListener("priorityChange", () => { + const updatedPrioritaryCdn = getCdnToRequest(); if (cancellationSignal.isCancelled) { throw cancellationSignal.cancellationError; } diff --git a/src/core/init/initialize_media_source.ts b/src/core/init/initialize_media_source.ts index 8e7fd5c257..e159f334c8 100644 --- a/src/core/init/initialize_media_source.ts +++ b/src/core/init/initialize_media_source.ts @@ -267,7 +267,6 @@ export default function InitializeOnMediaSource( maxRetryRegular: segmentRequestOptions.regularError, maxRetryOffline: segmentRequestOptions.offlineError }; const segmentFetcherCreator = new SegmentFetcherCreator(transport, - manifest, requestOptions, playbackCanceller.signal); diff --git a/src/default_config.ts b/src/default_config.ts index d4bf3e5bdc..357d31be6f 100644 --- a/src/default_config.ts +++ b/src/default_config.ts @@ -395,21 +395,6 @@ const DEFAULT_CONFIG = { */ DEFAULT_MAX_MANIFEST_REQUEST_RETRY: 4, - /** - * The default number of times a Content Steering Manifest request will be - * re-performed when loaded/refreshed if the request finishes on an error - * which justify an retry. - * - * Note that some errors do not use this counter: - * - if the error is not due to the xhr, no retry will be peformed - * - if the error is an HTTP error code, but not a 500-smthg or a 404, no - * retry will be performed. - * - if it has a high chance of being due to the user being offline, a - * separate counter is used (see DEFAULT_MAX_REQUESTS_RETRY_ON_OFFLINE). - * @type Number - */ - DEFAULT_MAX_CONTENT_STEERING_MANIFEST_REQUEST_RETRY: 4, - /** * Default delay, in seconds, during which a CDN will be "downgraded". * diff --git a/src/experimental/tools/VideoThumbnailLoader/video_thumbnail_loader.ts b/src/experimental/tools/VideoThumbnailLoader/video_thumbnail_loader.ts index 8d9104d023..0392fa0cda 100644 --- a/src/experimental/tools/VideoThumbnailLoader/video_thumbnail_loader.ts +++ b/src/experimental/tools/VideoThumbnailLoader/video_thumbnail_loader.ts @@ -170,7 +170,6 @@ export default class VideoThumbnailLoader { const segmentFetcher = createSegmentFetcher( "video", loader.video, - // TODO implement ContentSteering for the VideoThumbnailLoader? null, // We don't care about the SegmentFetcher's lifecycle events {}, diff --git a/src/manifest/manifest.ts b/src/manifest/manifest.ts index fdc4f90b32..0894bd2f71 100644 --- a/src/manifest/manifest.ts +++ b/src/manifest/manifest.ts @@ -15,10 +15,7 @@ */ import { MediaError } from "../errors"; -import { - IContentSteeringMetadata, - IParsedManifest, -} from "../parsers/manifest"; +import { IParsedManifest } from "../parsers/manifest"; import { IPlayerError, IRepresentationFilter, @@ -245,8 +242,6 @@ export default class Manifest extends EventEmitter { */ public clockOffset : number | undefined; - public contentSteering : IContentSteeringMetadata | null; - /** * Data allowing to calculate the minimum and maximum seekable positions at * any given time. @@ -386,7 +381,6 @@ export default class Manifest extends EventEmitter { this.suggestedPresentationDelay = parsedManifest.suggestedPresentationDelay; this.availabilityStartTime = parsedManifest.availabilityStartTime; this.publishTime = parsedManifest.publishTime; - this.contentSteering = parsedManifest.contentSteering; if (supplementaryImageTracks.length > 0) { this._addSupplementaryImageAdaptations(supplementaryImageTracks); } @@ -731,7 +725,6 @@ export default class Manifest extends EventEmitter { this.suggestedPresentationDelay = newManifest.suggestedPresentationDelay; this.transport = newManifest.transport; this.publishTime = newManifest.publishTime; - this.contentSteering = newManifest.contentSteering; if (updateType === MANIFEST_UPDATE_TYPE.Full) { this._timeBounds = newManifest._timeBounds; diff --git a/src/parsers/SteeringManifest/DCSM/parse_dcsm.ts b/src/parsers/SteeringManifest/DCSM/parse_dcsm.ts deleted file mode 100644 index becaa8b01f..0000000000 --- a/src/parsers/SteeringManifest/DCSM/parse_dcsm.ts +++ /dev/null @@ -1,67 +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 { ISteeringManifest } from "../types"; - -export default function parseDashContentSteeringManifest( - input : string | Partial> -) : [ISteeringManifest, Error[]] { - const warnings : Error[] = []; - let json; - if (typeof input === "string") { - json = JSON.parse(input) as Partial>; - } else { - json = input; - } - - if (json.VERSION !== 1) { - throw new Error("Unhandled DCSM version. Only `1` can be proccessed."); - } - - const initialPriorities = json["SERVICE-LOCATION-PRIORITY"]; - if (!Array.isArray(initialPriorities)) { - throw new Error("The DCSM's SERVICE-LOCATION-URI in in the wrong format"); - } else if (initialPriorities.length === 0) { - warnings.push( - new Error("The DCSM's SERVICE-LOCATION-URI should contain at least one element") - ); - } - - const priorities : string[] = initialPriorities.filter((elt) : elt is string => - typeof elt === "string" - ); - if (priorities.length !== initialPriorities.length) { - warnings.push( - new Error("The DCSM's SERVICE-LOCATION-URI contains URI in a wrong format") - ); - } - let lifetime = 300; - - if (typeof json.TTL === "number") { - lifetime = json.TTL; - } else if (json.TTL !== undefined) { - warnings.push(new Error("The DCSM's TTL in in the wrong format")); - } - - let reloadUri; - if (typeof json["RELOAD-URI"] === "string") { - reloadUri = json["RELOAD-URI"]; - } else if (json["RELOAD-URI"] !== undefined) { - warnings.push(new Error("The DCSM's RELOAD-URI in in the wrong format")); - } - - return [{ lifetime, reloadUri, priorities }, warnings]; -} diff --git a/src/parsers/SteeringManifest/index.ts b/src/parsers/SteeringManifest/index.ts deleted file mode 100644 index 5246b359fe..0000000000 --- a/src/parsers/SteeringManifest/index.ts +++ /dev/null @@ -1,18 +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. - */ - -export { ISteeringManifest } from "./types"; - diff --git a/src/parsers/SteeringManifest/types.ts b/src/parsers/SteeringManifest/types.ts deleted file mode 100644 index 4b0a2a7332..0000000000 --- a/src/parsers/SteeringManifest/types.ts +++ /dev/null @@ -1,21 +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. - */ - -export interface ISteeringManifest { - lifetime: number; - reloadUri? : string | undefined; - priorities : string[]; -} diff --git a/src/parsers/manifest/dash/common/parse_mpd.ts b/src/parsers/manifest/dash/common/parse_mpd.ts index b12abc8b7e..03e472826e 100644 --- a/src/parsers/manifest/dash/common/parse_mpd.ts +++ b/src/parsers/manifest/dash/common/parse_mpd.ts @@ -19,10 +19,7 @@ import log from "../../../../log"; import Manifest from "../../../../manifest"; import arrayFind from "../../../../utils/array_find"; import { getFilenameIndexInUrl } from "../../../../utils/resolve_url"; -import { - IContentSteeringMetadata, - IParsedManifest, -} from "../../types"; +import { IParsedManifest } from "../../types"; import { IMPDIntermediateRepresentation, IPeriodIntermediateRepresentation, @@ -276,16 +273,6 @@ function parseCompleteIntermediateRepresentation( livePosition : number | undefined; time : number; }; - let contentSteering : IContentSteeringMetadata | null = null; - if (rootChildren.contentSteering !== undefined) { - const { attributes } = rootChildren.contentSteering; - contentSteering = { url: rootChildren.contentSteering.value, - defaultId: attributes.defaultServiceLocation, - queryBeforeStart: attributes.queryBeforeStart === true, - proxyUrl: attributes.proxyServerUrl }; - - } - if (rootAttributes.minimumUpdatePeriod !== undefined && rootAttributes.minimumUpdatePeriod >= 0) { @@ -380,7 +367,6 @@ function parseCompleteIntermediateRepresentation( const parsedMPD : IParsedManifest = { availabilityStartTime, clockOffset: args.externalClockOffset, - contentSteering, isDynamic, isLive: isDynamic, isLastPeriodKnown, diff --git a/src/parsers/manifest/dash/common/resolve_base_urls.ts b/src/parsers/manifest/dash/common/resolve_base_urls.ts index 35ca7c494f..147f04d1f8 100644 --- a/src/parsers/manifest/dash/common/resolve_base_urls.ts +++ b/src/parsers/manifest/dash/common/resolve_base_urls.ts @@ -36,8 +36,7 @@ export default function resolveBaseURLs( } const newBaseUrls : IResolvedBaseUrl[] = newBaseUrlsIR.map(ir => { - return { url: ir.value, - serviceLocation: ir.attributes.serviceLocation }; + return { url: ir.value }; }); if (currentBaseURLs.length === 0) { return newBaseUrls; diff --git a/src/parsers/manifest/dash/js-parser/node_parsers/BaseURL.ts b/src/parsers/manifest/dash/js-parser/node_parsers/BaseURL.ts index 80678ab46c..03da0658c0 100644 --- a/src/parsers/manifest/dash/js-parser/node_parsers/BaseURL.ts +++ b/src/parsers/manifest/dash/js-parser/node_parsers/BaseURL.ts @@ -25,22 +25,11 @@ import { IBaseUrlIntermediateRepresentation } from "../../node_parser_types"; export default function parseBaseURL( root: Element ) : [IBaseUrlIntermediateRepresentation | undefined, Error[]] { - const attributes : { serviceLocation? : string } = {}; const value = root.textContent; const warnings : Error[] = []; if (value === null || value.length === 0) { return [undefined, warnings]; } - for (let i = 0; i < root.attributes.length; i++) { - const attribute = root.attributes[i]; - - switch (attribute.name) { - case "serviceLocation": - attributes.serviceLocation = attribute.value; - break; - } - } - - return [ { value, attributes }, + return [ { value }, warnings ]; } diff --git a/src/parsers/manifest/dash/js-parser/node_parsers/ContentSteering.ts b/src/parsers/manifest/dash/js-parser/node_parsers/ContentSteering.ts deleted file mode 100644 index fe4a9bda87..0000000000 --- a/src/parsers/manifest/dash/js-parser/node_parsers/ContentSteering.ts +++ /dev/null @@ -1,63 +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 { IContentSteeringIntermediateRepresentation } from "../../node_parser_types"; -import { - parseBoolean, - ValueParser, -} from "./utils"; - -/** - * Parse an ContentSteering element into an ContentSteering intermediate - * representation. - * @param {Element} root - The ContentSteering root element. - * @returns {Array.} - */ -export default function parseContentSteering( - root: Element -) : [IContentSteeringIntermediateRepresentation | undefined, Error[]] { - const attributes : { defaultServiceLocation? : string; - queryBeforeStart? : boolean; - proxyServerUrl? : string; } = {}; - const value = root.textContent; - const warnings : Error[] = []; - if (value === null || value.length === 0) { - return [undefined, warnings]; - } - const parseValue = ValueParser(attributes, warnings); - for (let i = 0; i < root.attributes.length; i++) { - const attribute = root.attributes[i]; - - switch (attribute.name) { - case "defaultServiceLocation": - attributes.defaultServiceLocation = attribute.value; - break; - - case "queryBeforeStart": - parseValue(attribute.value, { asKey: "queryBeforeStart", - parser: parseBoolean, - dashName: "queryBeforeStart" }); - break; - - case "proxyServerUrl": - attributes.proxyServerUrl = attribute.value; - break; - } - } - - return [ { value, attributes }, - warnings ]; -} diff --git a/src/parsers/manifest/dash/js-parser/node_parsers/MPD.ts b/src/parsers/manifest/dash/js-parser/node_parsers/MPD.ts index 6c50e820d6..6330c71529 100644 --- a/src/parsers/manifest/dash/js-parser/node_parsers/MPD.ts +++ b/src/parsers/manifest/dash/js-parser/node_parsers/MPD.ts @@ -16,7 +16,6 @@ import { IBaseUrlIntermediateRepresentation, - IContentSteeringIntermediateRepresentation, IMPDAttributes, IMPDChildren, IMPDIntermediateRepresentation, @@ -24,7 +23,6 @@ import { IScheme, } from "../../node_parser_types"; import parseBaseURL from "./BaseURL"; -import parseContentSteering from "./ContentSteering"; import { createPeriodIntermediateRepresentation, } from "./Period"; @@ -47,7 +45,6 @@ function parseMPDChildren( const locations : string[] = []; const periods : IPeriodIntermediateRepresentation[] = []; const utcTimings : IScheme[] = []; - let contentSteering : IContentSteeringIntermediateRepresentation | undefined; let warnings : Error[] = []; for (let i = 0; i < mpdChildren.length; i++) { @@ -64,13 +61,6 @@ function parseMPDChildren( warnings = warnings.concat(baseURLWarnings); break; - case "ContentSteering": - const [ contentSteeringObj, - contentSteeringWarnings ] = parseContentSteering(currentNode); - contentSteering = contentSteeringObj; - warnings = warnings.concat(contentSteeringWarnings); - break; - case "Location": locations.push(currentNode.textContent === null ? "" : @@ -92,7 +82,7 @@ function parseMPDChildren( } } - return [ { baseURLs, contentSteering, locations, periods, utcTimings }, + return [ { baseURLs, locations, periods, utcTimings }, warnings ]; } diff --git a/src/parsers/manifest/dash/js-parser/node_parsers/__tests__/AdaptationSet.test.ts b/src/parsers/manifest/dash/js-parser/node_parsers/__tests__/AdaptationSet.test.ts index caf53cf96a..f56b05e431 100644 --- a/src/parsers/manifest/dash/js-parser/node_parsers/__tests__/AdaptationSet.test.ts +++ b/src/parsers/manifest/dash/js-parser/node_parsers/__tests__/AdaptationSet.test.ts @@ -365,8 +365,7 @@ describe("DASH Node Parsers - AdaptationSet", () => { .toEqual([ { attributes: {}, - children: { baseURLs: [{ attributes: { serviceLocation: "foo" }, - value: "a" }], + children: { baseURLs: [{ value: "a" }], representations: [] }, }, [], @@ -382,8 +381,7 @@ describe("DASH Node Parsers - AdaptationSet", () => { .toEqual([ { attributes: {}, - children: { baseURLs: [{ attributes: { serviceLocation: "4" }, - value: "foo bar" }], + children: { baseURLs: [{ value: "foo bar" }], representations: [] }, }, [], @@ -399,10 +397,8 @@ describe("DASH Node Parsers - AdaptationSet", () => { .toEqual([ { attributes: {}, - children: { baseURLs: [ { attributes: { serviceLocation: "" }, - value: "a" }, - { attributes: { serviceLocation: "http://test.com" }, - value: "b" } ], + children: { baseURLs: [ { value: "a" }, + { value: "b" } ], representations: [] }, }, [], diff --git a/src/parsers/manifest/dash/node_parser_types.ts b/src/parsers/manifest/dash/node_parser_types.ts index a756d71419..7b32016b84 100644 --- a/src/parsers/manifest/dash/node_parser_types.ts +++ b/src/parsers/manifest/dash/node_parser_types.ts @@ -41,11 +41,6 @@ export interface IMPDChildren { * from the first encountered to the last encountered. */ baseURLs : IBaseUrlIntermediateRepresentation[]; - /** - * Information on a potential Content Steering Manifest linked to this - * content. - */ - contentSteering? : IContentSteeringIntermediateRepresentation | undefined; /** * Location(s) at which the Manifest can be refreshed. * @@ -373,43 +368,6 @@ export interface IBaseUrlIntermediateRepresentation { * This is the inner content of a BaseURL node. */ value: string; - - /** Attributes assiociated to the BaseURL node. */ - attributes: { - /** - * Potential value for a `serviceLocation` attribute, used in content - * steering mechanisms. - */ - serviceLocation? : string; - }; -} - -/** Intermediate representation for a ContentSteering node. */ -export interface IContentSteeringIntermediateRepresentation { - /** - * The Content Steering Manifest's URL. - * - * This is the inner content of a ContentSteering node. - */ - value: string; - - /** Attributes assiociated to the ContentSteering node. */ - attributes: { - /** Default ServiceLocation to be used. */ - defaultServiceLocation? : string; - /** - * If `true`, the Content Steering Manifest should be loaded before the - * first resources depending on it are loaded. - */ - queryBeforeStart? : boolean; - /** - * If set, a proxy URL has been configured. - * Requests for the Content Steering Manifest should actually go through - * this proxy, the node URL being added to an `url` query parameter - * alongside potential other query parameters. - */ - proxyServerUrl? : string; - }; } /** Intermediate representation for a Node following a "scheme" format. */ diff --git a/src/parsers/manifest/dash/wasm-parser/rs/events.rs b/src/parsers/manifest/dash/wasm-parser/rs/events.rs index 620d5c76f4..075470bf9f 100644 --- a/src/parsers/manifest/dash/wasm-parser/rs/events.rs +++ b/src/parsers/manifest/dash/wasm-parser/rs/events.rs @@ -90,9 +90,6 @@ pub enum TagName { /// Indicate a node SegmentUrl = 20, - - /// Indicate a node - ContentSteering = 21 } #[derive(PartialEq, Clone, Copy)] @@ -281,16 +278,10 @@ pub enum AttributeName { /// format: the browser's `DOMParser` API needs to know all potential /// namespaces that will appear in it. Namespace = 70, - + Label = 71, // String ServiceLocation = 72, // String - - QueryBeforeStart = 73, // Boolean - - ProxyServerUrl = 74, // String - - DefaultServiceLocation = 75, } impl TagName { diff --git a/src/parsers/manifest/dash/wasm-parser/rs/processor/attributes.rs b/src/parsers/manifest/dash/wasm-parser/rs/processor/attributes.rs index 9e9d6282fe..886c80c538 100644 --- a/src/parsers/manifest/dash/wasm-parser/rs/processor/attributes.rs +++ b/src/parsers/manifest/dash/wasm-parser/rs/processor/attributes.rs @@ -150,20 +150,6 @@ pub fn report_base_url_attrs(tag_bs : &quick_xml::events::BytesStart) { }; } -pub fn report_content_steering_attrs(tag_bs : &quick_xml::events::BytesStart) { - for res_attr in tag_bs.attributes() { - match res_attr { - Ok(attr) => match attr.key { - b"serviceLocation" => ServiceLocation.try_report_as_string(&attr), - b"proxyServerUrl" => ProxyServerUrl.try_report_as_string(&attr), - b"queryBeforeStart" => QueryBeforeStart.try_report_as_bool(&attr), - _ => {}, - }, - Err(err) => ParsingError::from(err).report_err(), - }; - }; -} - pub fn report_segment_template_attrs(tag_bs : &quick_xml::events::BytesStart) { for res_attr in tag_bs.attributes() { match res_attr { diff --git a/src/parsers/manifest/dash/wasm-parser/rs/processor/mod.rs b/src/parsers/manifest/dash/wasm-parser/rs/processor/mod.rs index aa272a5118..bd5989ee1c 100644 --- a/src/parsers/manifest/dash/wasm-parser/rs/processor/mod.rs +++ b/src/parsers/manifest/dash/wasm-parser/rs/processor/mod.rs @@ -112,11 +112,6 @@ impl MPDProcessor { attributes::report_base_url_attrs(&tag); self.process_base_url_element(); }, - b"ContentSteering" => { - TagName::ContentSteering.report_tag_open(); - attributes::report_content_steering_attrs(&tag); - self.process_content_steering_element(); - }, b"cenc:pssh" => self.process_cenc_element(), b"Location" => self.process_location_element(), b"Label" => self.process_label_element(), @@ -340,43 +335,6 @@ impl MPDProcessor { } } - fn process_content_steering_element(&mut self) { - // Count inner ContentSteering tags if it exists. - // Allowing to not close the current node when it is an inner that is closed - let mut inner_tag : u32 = 0; - - loop { - match self.read_next_event() { - Ok(Event::Text(t)) => if t.len() > 0 { - match t.unescaped() { - Ok(unescaped) => AttributeName::Text.report(unescaped), - Err(err) => ParsingError::from(err).report_err(), - } - }, - Ok(Event::Start(tag)) if tag.name() == b"ContentSteering" => inner_tag += 1, - Ok(Event::End(tag)) if tag.name() == b"ContentSteering" => { - if inner_tag > 0 { - inner_tag -= 1; - } else { - TagName::ContentSteering.report_tag_close(); - break; - } - }, - Ok(Event::Eof) => { - ParsingError("Unexpected end of file in a ContentSteering.".to_owned()) - .report_err(); - break; - } - Err(e) => { - ParsingError::from(e).report_err(); - break; - }, - _ => (), - } - self.reader_buf.clear(); - } - } - fn process_cenc_element(&mut self) { // Count inner cenc:pssh tags if it exists. // Allowing to not close the current node when it is an inner that is closed diff --git a/src/parsers/manifest/dash/wasm-parser/ts/generators/BaseURL.ts b/src/parsers/manifest/dash/wasm-parser/ts/generators/BaseURL.ts index 646597b049..d52adda4e1 100644 --- a/src/parsers/manifest/dash/wasm-parser/ts/generators/BaseURL.ts +++ b/src/parsers/manifest/dash/wasm-parser/ts/generators/BaseURL.ts @@ -35,14 +35,6 @@ export function generateBaseUrlAttrParser( case AttributeName.Text: baseUrlAttrs.value = parseString(textDecoder, linearMemory.buffer, ptr, len); break; - - case AttributeName.ServiceLocation: { - baseUrlAttrs.attributes.serviceLocation = parseString(textDecoder, - linearMemory.buffer, - ptr, - len); - break; - } } }; } diff --git a/src/parsers/manifest/dash/wasm-parser/ts/generators/ContentSteering.ts b/src/parsers/manifest/dash/wasm-parser/ts/generators/ContentSteering.ts deleted file mode 100644 index d638d6e13f..0000000000 --- a/src/parsers/manifest/dash/wasm-parser/ts/generators/ContentSteering.ts +++ /dev/null @@ -1,59 +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 { IContentSteeringIntermediateRepresentation } from "../../../node_parser_types"; -import { IAttributeParser } from "../parsers_stack"; -import { AttributeName } from "../types"; -import { parseString } from "../utils"; - -/** - * Generate an "attribute parser" once inside a `ContentSteering` node. - * @param {Object} contentSteeringAttrs - * @param {WebAssembly.Memory} linearMemory - * @returns {Function} - */ -export function generateContentSteeringAttrParser( - contentSteeringAttrs : IContentSteeringIntermediateRepresentation, - linearMemory : WebAssembly.Memory -) : IAttributeParser { - const textDecoder = new TextDecoder(); - return function onMPDAttribute(attr : number, ptr : number, len : number) { - switch (attr) { - case AttributeName.Text: - contentSteeringAttrs.value = - parseString(textDecoder, linearMemory.buffer, ptr, len); - break; - - case AttributeName.ServiceLocation: { - contentSteeringAttrs.attributes.defaultServiceLocation = - parseString(textDecoder, linearMemory.buffer, ptr, len); - break; - } - - case AttributeName.QueryBeforeStart: { - contentSteeringAttrs.attributes.queryBeforeStart = - new DataView(linearMemory.buffer).getUint8(0) === 0; - break; - } - - case AttributeName.ProxyServerUrl: { - contentSteeringAttrs.attributes.proxyServerUrl = - parseString(textDecoder, linearMemory.buffer, ptr, len); - break; - } - } - }; -} diff --git a/src/parsers/manifest/dash/wasm-parser/ts/generators/MPD.ts b/src/parsers/manifest/dash/wasm-parser/ts/generators/MPD.ts index 85fa304b37..d51533713f 100644 --- a/src/parsers/manifest/dash/wasm-parser/ts/generators/MPD.ts +++ b/src/parsers/manifest/dash/wasm-parser/ts/generators/MPD.ts @@ -29,7 +29,6 @@ import { } from "../types"; import { parseString } from "../utils"; import { generateBaseUrlAttrParser } from "./BaseURL"; -import { generateContentSteeringAttrParser } from "./ContentSteering"; import { generatePeriodAttrParser, generatePeriodChildrenParser, @@ -63,17 +62,6 @@ export function generateMPDChildrenParser( break; } - case TagName.ContentSteering: { - const contentSteering = { value: "", attributes: {} }; - mpdChildren.contentSteering = contentSteering; - - const childrenParser = noop; // ContentSteering have no sub-element - const attributeParser = - generateContentSteeringAttrParser(contentSteering, linearMemory); - parsersStack.pushParsers(nodeId, childrenParser, attributeParser); - break; - } - case TagName.Period: { const period = { children: { adaptations: [], baseURLs: [], diff --git a/src/parsers/manifest/dash/wasm-parser/ts/types.ts b/src/parsers/manifest/dash/wasm-parser/ts/types.ts index 6fc5aefb8c..571bb9a15a 100644 --- a/src/parsers/manifest/dash/wasm-parser/ts/types.ts +++ b/src/parsers/manifest/dash/wasm-parser/ts/types.ts @@ -108,9 +108,6 @@ export const enum TagName { /// Indicate a node SegmentUrl = 20, - - /// Indicate a node - ContentSteering = 21 } /** diff --git a/src/parsers/manifest/local/parse_local_manifest.ts b/src/parsers/manifest/local/parse_local_manifest.ts index 70fb3bd614..d2fdf3e76a 100644 --- a/src/parsers/manifest/local/parse_local_manifest.ts +++ b/src/parsers/manifest/local/parse_local_manifest.ts @@ -54,7 +54,6 @@ export default function parseLocalManifest( .map(period => parsePeriod(period, { periodIdGenerator })); return { availabilityStartTime: 0, - contentSteering: null, expired: localManifest.expired, transportType: "local", isDynamic: !isFinished, diff --git a/src/parsers/manifest/metaplaylist/metaplaylist_parser.ts b/src/parsers/manifest/metaplaylist/metaplaylist_parser.ts index d6d9de86b1..ddc9726644 100644 --- a/src/parsers/manifest/metaplaylist/metaplaylist_parser.ts +++ b/src/parsers/manifest/metaplaylist/metaplaylist_parser.ts @@ -306,7 +306,6 @@ function createManifest( manifests[manifests.length - 1].isLastPeriodKnown); const manifest = { availabilityStartTime: 0, clockOffset, - contentSteering: null, suggestedPresentationDelay: 10, periods, transportType: "metaplaylist", diff --git a/src/parsers/manifest/smooth/create_parser.ts b/src/parsers/manifest/smooth/create_parser.ts index 2a609ed101..94d839dab2 100644 --- a/src/parsers/manifest/smooth/create_parser.ts +++ b/src/parsers/manifest/smooth/create_parser.ts @@ -650,7 +650,6 @@ function createSmoothStreamingParser( 0 : availabilityStartTime, clockOffset: serverTimeOffset, - contentSteering: null, isLive, isDynamic: isLive, isLastPeriodKnown: true, diff --git a/src/parsers/manifest/types.ts b/src/parsers/manifest/types.ts index 73252a94e6..076e494167 100644 --- a/src/parsers/manifest/types.ts +++ b/src/parsers/manifest/types.ts @@ -90,8 +90,7 @@ export interface ICdnMetadata { baseUrl : string; /** - * Identifier that might be re-used in other documents, for example a - * Content Steering Manifest, to identify this CDN. + * Identifier that might be re-used in other documents. */ id? : string | undefined; } @@ -378,13 +377,4 @@ export interface IParsedManifest { suggestedPresentationDelay? : number | undefined; /** URIs where the manifest can be refreshed by order of importance. */ uris? : string[] | undefined; - - contentSteering : IContentSteeringMetadata | null; -} - -export interface IContentSteeringMetadata { - url : string; - defaultId : string | undefined; - queryBeforeStart : boolean; - proxyUrl : string | undefined; } diff --git a/src/transports/dash/pipelines.ts b/src/transports/dash/pipelines.ts index a83ae9b714..24619cd1b0 100644 --- a/src/transports/dash/pipelines.ts +++ b/src/transports/dash/pipelines.ts @@ -27,10 +27,6 @@ import { import generateManifestParser from "./manifest_parser"; import generateSegmentLoader from "./segment_loader"; import generateAudioVideoSegmentParser from "./segment_parser"; -import { - loadSteeringManifest, - parseSteeringManifest, -} from "./steering_manifest_pipeline"; import generateTextTrackLoader from "./text_loader"; import generateTextTrackParser from "./text_parser"; @@ -61,9 +57,7 @@ export default function(options : ITransportOptions) : ITransportPipelines { text: { loadSegment: textTrackLoader, parseSegment: textTrackParser }, image: { loadSegment: imageLoader, - parseSegment: imageParser }, - steeringManifest: { loadSteeringManifest, - parseSteeringManifest } }; + parseSegment: imageParser } }; } /** diff --git a/src/transports/dash/steering_manifest_pipeline.ts b/src/transports/dash/steering_manifest_pipeline.ts deleted file mode 100644 index 0d8ad372a3..0000000000 --- a/src/transports/dash/steering_manifest_pipeline.ts +++ /dev/null @@ -1,61 +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 { ISteeringManifest } from "../../parsers/SteeringManifest"; -/* eslint-disable-next-line max-len */ -import parseDashContentSteeringManifest from "../../parsers/SteeringManifest/DCSM/parse_dcsm"; -import request from "../../utils/request"; -import { CancellationSignal } from "../../utils/task_canceller"; -import { IRequestedData } from "../types"; - -/** - * Loads DASH's Content Steering Manifest. - * @param {string|null} url - * @param {Object} cancelSignal - * @returns {Promise} - */ -export async function loadSteeringManifest( - url : string, - cancelSignal : CancellationSignal -) : Promise> { - return request({ url, - responseType: "text", - cancelSignal }); -} - -/** - * Parses DASH's Content Steering Manifest. - * @param {Object} loadedSegment - * @param {Function} onWarnings - * @returns {Object} - */ -export function parseSteeringManifest( - { responseData } : IRequestedData, - onWarnings : (warnings : Error[]) => void -) : ISteeringManifest { - if ( - typeof responseData !== "string" && - (typeof responseData !== "object" || responseData === null) - ) { - throw new Error("Invalid loaded format for DASH's Content Steering Manifest."); - } - - const parsed = parseDashContentSteeringManifest(responseData); - if (parsed[1].length > 0) { - onWarnings(parsed[1]); - } - return parsed[0]; -} diff --git a/src/transports/local/pipelines.ts b/src/transports/local/pipelines.ts index 14f3a780c3..01c4ecb150 100644 --- a/src/transports/local/pipelines.ts +++ b/src/transports/local/pipelines.ts @@ -96,6 +96,5 @@ export default function getLocalManifestPipelines( audio: segmentPipeline, video: segmentPipeline, text: textTrackPipeline, - image: imageTrackPipeline, - steeringManifest: null }; + image: imageTrackPipeline }; } diff --git a/src/transports/metaplaylist/pipelines.ts b/src/transports/metaplaylist/pipelines.ts index 55b6a134d1..498ec2ab9c 100644 --- a/src/transports/metaplaylist/pipelines.ts +++ b/src/transports/metaplaylist/pipelines.ts @@ -421,6 +421,5 @@ export default function(options : ITransportOptions): ITransportPipelines { audio: audioPipeline, video: videoPipeline, text: textTrackPipeline, - image: imageTrackPipeline, - steeringManifest: null }; + image: imageTrackPipeline }; } diff --git a/src/transports/smooth/pipelines.ts b/src/transports/smooth/pipelines.ts index e6292b95ba..6d1b470f46 100644 --- a/src/transports/smooth/pipelines.ts +++ b/src/transports/smooth/pipelines.ts @@ -529,6 +529,5 @@ export default function(transportOptions : ITransportOptions) : ITransportPipeli audio: audioVideoPipeline, video: audioVideoPipeline, text: textTrackPipeline, - image: imageTrackPipeline, - steeringManifest: null }; + image: imageTrackPipeline }; } diff --git a/src/transports/types.ts b/src/transports/types.ts index 98b91eba4b..4e44519df3 100644 --- a/src/transports/types.ts +++ b/src/transports/types.ts @@ -24,7 +24,6 @@ import Manifest, { Representation, } from "../manifest"; import { ICdnMetadata } from "../parsers/manifest"; -import { ISteeringManifest } from "../parsers/SteeringManifest"; import { IBifThumbnail, ILoadedManifestFormat, @@ -67,21 +66,6 @@ export interface ITransportPipelines { /** Functions allowing to load an parse image (e.g. thumbnails) segments. */ image : ISegmentPipeline; - - /** - * Functions allowing to load and parse a Content Steering Manifest for this - * transport. - * - * A Content Steering Manifest is an external document allowing to obtain the - * current priority between multiple available CDN. A Content Steering - * Manifest also may or may not be available depending on the content. You - * might know its availability by parsing the content's Manifest or any other - * resource. - * - * `null` if the notion of a Content Steering Manifest does not exist for this - * transport or if it does but it isn't handled right now. - */ - steeringManifest : ITransportSteeringManifestPipeline | null; } /** Functions allowing to load and parse the Manifest. */ @@ -228,71 +212,6 @@ export interface IManifestLoaderOptions { timeout? : number | undefined; } -/** - * Functions allowing to load and parse a potential Content Steering Manifest, - * which gives an order of preferred CDN to serve the content. - */ -export interface ITransportSteeringManifestPipeline { - /** - * "Loader" of the Steering Manifest pipeline, allowing to request a Steering - * Manifest so it can later be parsed by the `parseSteeringManifest` function. - * - * @param {string} url - URL of the Steering Manifest we want to load. - * @param {CancellationSignal} cancellationSignal - Signal which will allow to - * cancel the loading operation if the Steering Manifest is not needed anymore - * (for example, if the content has just been stopped). - * When cancelled, the promise returned by this function will reject with a - * `CancellationError`. - * @returns {Promise.} - Promise emitting the loaded Steering - * Manifest, that then can be parsed through the `parseSteeringManifest` - * function. - * - * Rejects in two cases: - * - The loading operation has been cancelled through the `cancelSignal` - * given in argument. - * In that case, this Promise will reject with a `CancellationError`. - * - The loading operation failed, most likely due to a request error. - * In that case, this Promise will reject with the corresponding Error. - */ - loadSteeringManifest : ( - url : string, - cancelSignal : CancellationSignal, - ) => Promise>>>; - - /** - * "Parser" of the Steering Manifest pipeline, allowing to parse a loaded - * Steering Manifest so it can be exploited by the rest of the RxPlayer's - * logic. - * - * @param {Object} data - Response obtained from the `loadSteeringManifest` - * function. - * @param {Function} onWarnings - Callbacks called when minor Steering - * Manifest parsing errors are found. - * @param {CancellationSignal} cancelSignal - Cancellation signal which will - * allow to abort the parsing operation if you do not want the Steering - * Manifest anymore. - * - * That cancellationSignal can be triggered at any time, such as: - * - after a warning is received - * - while a request scheduled through the `scheduleRequest` argument is - * pending. - * - * `parseSteeringManifest` will interrupt all operations if the signal has - * been triggered in one of those scenarios, and will automatically reject - * with the corresponding `CancellationError` instance. - * @returns {Object | Promise.} - Returns the Steering Manifest data. - * Throws if a fatal error happens while doing so. - * - * If this error is due to a cancellation (indicated through the - * `cancelSignal` argument), then the rejected error should be the - * corresponding `CancellationError` instance. - */ - parseSteeringManifest : ( - data : IRequestedData, - onWarnings : (warnings : Error[]) => void, - ) => ISteeringManifest; -} - /** Functions allowing to load and parse segments of any type. */ export interface ISegmentPipeline< TLoadedFormat, @@ -486,13 +405,6 @@ export interface IManifestParserResult { url? : string | undefined; } -export interface IDASHContentSteeringManifest { - VERSION : number; // REQUIRED, must be an integer - TTL? : number; // REQUIRED, number of seconds - ["RELOAD-URI"]? : string; // OPTIONAL, URI - ["SERVICE-LOCATION-PRIORITY"] : string[]; // REQUIRED, array of ServiceLocation -} - /** * Allow the parser to ask for loading supplementary ressources while still * profiting from the same retries and error management than the loader. diff --git a/src/utils/sync_or_async.ts b/src/utils/sync_or_async.ts deleted file mode 100644 index ac8ae7871c..0000000000 --- a/src/utils/sync_or_async.ts +++ /dev/null @@ -1,72 +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. - */ - -/** - * Type wrapping an underlying value that might either be obtained synchronously - * (a "sync" value) or asynchronously by awaiting a Promise (an "async" value). - * - * This type was created instead of just relying on Promises everytime, to - * avoid the necessity of always having the overhead and more complex - * always-async behavior of a Promise for a value that might be in most time - * obtainable synchronously. - * - * @example - * ```ts - * const val1 = SyncOrAsync.createAsync(Promise.resolve("foo")); - * const val2 = SyncOrAsync.createSync("bar"); - * - * async function logVal(val : ISyncOrAsyncValue) : void { - * // The following syntax allows to only await asynchronous values - * console.log(val.syncValue ?? await val.getValueAsAsync()); - * } - * - * logVal(val1); - * logVal(val2); - * - * // Here this will first log in the console "bar" directly and synchronously. - * // Then asychronously through a microtask (as Promises and awaited values - * // always are), "foo" will be logged. - * ``` - */ -export interface ISyncOrAsyncValue { - /** - * Set to the underlying value in the case where it was set synchronously. - * Set to `null` if the value is set asynchronously. - */ - syncValue : T | null; - /** - * Obtain the value asynchronously. - * This works even when the value is actually set synchronously, by embedding it - * value in a Promise. - */ - getValueAsAsync() : Promise; -} - -export default { - createSync(val : T) : ISyncOrAsyncValue { - return { - syncValue: val, - getValueAsAsync() { return Promise.resolve(val); }, - }; - }, - - createAsync(val : Promise) : ISyncOrAsyncValue { - return { - syncValue: null, - getValueAsAsync() { return val; }, - }; - }, -};