Skip to content

Commit

Permalink
Merge pull request #1165 from canalplus/misc/better-multi-url-prioriz…
Browse files Browse the repository at this point in the history
…ation

[Proposal] [WIP] Improve multi-URL priorization
  • Loading branch information
peaBerberian committed Oct 13, 2022
2 parents 63afc8d + 167c5ec commit cf259ef
Show file tree
Hide file tree
Showing 84 changed files with 1,676 additions and 1,071 deletions.
16 changes: 6 additions & 10 deletions src/core/api/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ import {
IManifestFetcherParsedResult,
IManifestFetcherWarningEvent,
ManifestFetcher,
SegmentFetcherCreator,
} from "../fetchers";
import initializeMediaSourcePlayback, {
IInitEvent,
Expand Down Expand Up @@ -790,14 +789,6 @@ class Player extends EventEmitter<IPublicAPIEvent> {
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<IManifestFetcherParsedResult |
IManifestFetcherWarningEvent>;
Expand Down Expand Up @@ -897,6 +888,10 @@ class Player extends EventEmitter<IPublicAPIEvent> {
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,
Expand All @@ -908,9 +903,10 @@ class Player extends EventEmitter<IPublicAPIEvent> {
manifestFetcher,
mediaElement: videoElement,
minimumManifestUpdateInterval,
segmentFetcherCreator,
segmentRequestOptions,
speed: this._priv_speed,
startAt,
transport: transportPipelines,
textTrackOptions })
.pipe(takeUntil(stoppedContent$));

Expand Down
198 changes: 198 additions & 0 deletions src/core/fetchers/cdn_prioritizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/**
* 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 { ICdnMetadata } from "../../parsers/manifest";
import { IPlayerError } from "../../public_types";
import arrayFindIndex from "../../utils/array_find_index";
import EventEmitter from "../../utils/event_emitter";
import { CancellationSignal } from "../../utils/task_canceller";

/**
* Class signaling the priority between multiple CDN available for any given
* resource.
*
* 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<ICdnPrioritizerEvents> {
/**
* 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, sorted by the time at which they have
* been downgraded.
*/
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[];
};

/**
* @param {Object} destroySignal
*/
constructor(destroySignal : CancellationSignal) {
super();
this._downgradedCdnList = { metadata: [], timeouts: [] };
destroySignal.register(() => {
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.<string>} everyCdnForResource - Array of ALL available CDN
* able to reach the wanted resource - even those which might not be used in
* the end.
* @returns {Array.<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.
*/
public getCdnPreferenceForResource(
everyCdnForResource : ICdnMetadata[]
) : ICdnMetadata[] {
if (everyCdnForResource.length <= 1) {
// The huge majority of contents have only one CDN available.
// Here, prioritizing make no sense.
return everyCdnForResource;
}

return 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 = 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.<string>} everyCdnForResource - Array of ALL available CDN
* able to reach the wanted resource - even those which might not be used in
* the end.
* @returns {Array.<string>} - 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[] {
const [allowedInOrder, downgradedInOrder] = everyCdnForResource
.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);
}

/**
* @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.<Object>} arr
* @param {Object} elt
* @returns {number}
*/
function indexOfMetadata(
arr : ICdnMetadata[],
elt : ICdnMetadata
) : number {
if (arr.length === 0) {
return -1;
}
return elt.id !== undefined ? arrayFindIndex(arr, m => m.id === elt.id) :
arrayFindIndex(arr, m => m.baseUrl === elt.baseUrl);
}
18 changes: 7 additions & 11 deletions src/core/fetchers/manifest/manifest_fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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);
}
});
}
Expand Down Expand Up @@ -349,9 +345,9 @@ export default class ManifestFetcher {
performRequest : () => Promise<T>
) : Promise<T> {
try {
const data = await tryRequestPromiseWithBackoff(performRequest,
backoffSettings,
canceller.signal);
const data = await scheduleRequestPromise(performRequest,
backoffSettings,
canceller.signal);
return data;
} catch (err) {
throw errorSelector(err);
Expand Down
22 changes: 12 additions & 10 deletions src/core/fetchers/segment/segment_fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import Manifest, {
Period,
Representation,
} from "../../../manifest";
import { ICdnMetadata } from "../../../parsers/manifest";
import { IPlayerError } from "../../../public_types";
import {
IChunkCompleteInformation,
Expand All @@ -49,8 +50,9 @@ import {
IRequestProgressCallbackPayload,
} from "../../adaptive";
import { IBufferType } from "../../segment_buffers";
import CdnPrioritizer from "../cdn_prioritizer";
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. */
Expand All @@ -71,6 +73,7 @@ const generateRequestID = idGenerator();
export default function createSegmentFetcher<TLoadedFormat, TSegmentDataType>(
bufferType : IBufferType,
pipeline : ISegmentPipeline<TLoadedFormat, TSegmentDataType>,
cdnPrioritizer : CdnPrioritizer | null,
lifecycleCallbacks : ISegmentFetcherLifecycleCallbacks,
options : ISegmentFetcherOptions
) : ISegmentFetcher<TSegmentDataType> {
Expand Down Expand Up @@ -102,8 +105,6 @@ export default function createSegmentFetcher<TLoadedFormat, TSegmentDataType>(
fetcherCallbacks : ISegmentFetcherCallbacks<TSegmentDataType>,
cancellationSignal : CancellationSignal
) : Promise<void> {
const { segment } = content;

// used by logs
const segmentIdString = getLoggableSegmentId(content);
const requestId = generateRequestID();
Expand Down Expand Up @@ -193,10 +194,11 @@ export default function createSegmentFetcher<TLoadedFormat, TSegmentDataType>(
});

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;
Expand Down Expand Up @@ -236,13 +238,13 @@ export default function createSegmentFetcher<TLoadedFormat, TSegmentDataType>(

/**
* 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<ISegmentLoader<TLoadedFormat>> {
return loadSegment(url,
return loadSegment(cdnMetadata,
content,
requestOptions,
cancellationSignal,
Expand Down
Loading

0 comments on commit cf259ef

Please sign in to comment.