Skip to content

Commit

Permalink
Merge pull request #995 from canalplus/misc/transport-content-args-re…
Browse files Browse the repository at this point in the history
…facto

Future-proof the transport code for a v4.0.0
  • Loading branch information
peaBerberian committed Feb 18, 2022
2 parents bf4564d + 7dd08e7 commit 82c41fa
Show file tree
Hide file tree
Showing 39 changed files with 999 additions and 883 deletions.
92 changes: 51 additions & 41 deletions doc/api/Miscellaneous/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,19 @@ an XMLHttpRequest (it has no use, as our implementation does the same thing and
more):
```js
/**
* @param {Object} infos - infos about the segment to download
* @param {Object} segmentInfo - Information about the segment to download
* @param {Object} callbacks - Object containing several callbacks to indicate
* that the segment has been loaded, the loading operation has failed or to
* fallback to our default implementation. More information on this object below
* this code example.
* @returns {Function|undefined} - If a function is defined in the return value,
* it will be called if and when the request is canceled.
*/
const customSegmentLoader = (infos, callbacks) => {
const customSegmentLoader = (segmentInfo, callbacks) => {

// we will only use this custom loader for videos segments.
if (infos.adaptation.type !== "video") {
// we will also ignore edge cases where the URL is undefined.
if (segmentInfo.type !== "video" || segmentInfo.url === undefined) {
callbacks.fallback();
return;
}
Expand Down Expand Up @@ -68,10 +69,10 @@ const customSegmentLoader = (infos, callbacks) => {
callbacks.reject(err);
};

xhr.open("GET", infos.url);
xhr.open("GET", segmentInfo.url);
xhr.responseType = "arraybuffer";

const range = infos.segment.range;
const range = segmentInfo.range;
if (range) {
if (range[1] && range[1] !== Infinity) {
xhr.setRequestHeader("Range", `bytes=${range[0]}-${range[1]}`);
Expand All @@ -90,42 +91,50 @@ const customSegmentLoader = (infos, callbacks) => {

As you can see, this function takes two arguments:

1. **infos**: An Object giving information about the wanted segments.
This object contains the following properties:
1. **segmentInfo** (`object`): An Object giving information about the wanted
segments. This object contains the following properties:

- *url* (`string`): The URL the segment request should normally be
performed at.
- **url** (`string|undefined`): The URL the segment request should
normally be performed at.

- *manifest* (`Object`) - the Manifest object containing the segment.
More information on its structure can be found on the documentation
linked below [1].
This property can be `undefined` in a condition where the segment URL
either doesn't exist or has not been communicated by the Manifest.
This case is not currently possible but may be in future versions.

- *period* (`Object`) - the Period object containing the segment.
More information on its structure can be found on the documentation
linked below [2].
- *isInit* (`boolean|undefined`): If true this segment is an
initialization segment which contains no decodable data.

- *adaptation* (`Object`) - the Adaptation object containing the segment.
More information on its structure can be found on the documentation
linked below [3].
Those types of segment are mainly there for initialization
purposes, such as giving initial infos to the decoder on
subsequent media segments that will be pushed.

- *representation* (`Object`) - the Representation object containing the
segment.
More information on its structure can be found on the documentation
linked below [4].
Note that if `isInit` is false, it only means that the segment
contains decodable media, it can also contain important
initialization information.

- *segment* (`Object`) - the segment object related to this segment.
More information on its structure can be found on the documentation
linked below [5].
If `undefined`, we could not determine whether this segment was an
initialization segment.
This case is not currently possible but may be in future versions.

[1] [Manifest structure](./Manifest_Object.md#manifest)
- `range` (`Array.<number>|undefined`): If defined, it means that the
segment data is actually defined in a specific byte-range of the given
URL.

[2] [Period structure](./Manifest_Object.md#period)
Segment loaders are expected to only communicate that range of data
to the RxPlayer (if both a `range` and `indexRange` property
exists, it should communicate both).

[3] [Adaptation structure](./Manifest_Object.md#adaptation)
- `indexRange` (`Array.<number>|undefined`): If defined, it means that a
segment index can be retrieved at the associated URL by the byte range
expressed by this value.

[4] [Representation structure](./Manifest_Object.md#representation)
When this values is set - which is only done for specific contents
it is expected that the loader load both the segment's data and
this index.

[5] [Segment structure](./Manifest_Object.md#segment)
How to perform that last operation can be format-specific.
For ISOBMFF segments, both the data and the segment index can just
be concatenated.

2. **callbacks**: An object containing multiple callbacks to allow this
`segmentLoader` to communicate various events to the RxPlayer.
Expand All @@ -144,10 +153,11 @@ As you can see, this function takes two arguments:

This value may be used to estimate the ideal user bandwidth.


- *size* (`Number|undefined`) size, in bytes, of the total downloaded
response.

This value may be used to estimate the ideal user bandwidth.

- **progress** - Callback to call when progress information is available
on the current request. This callback allows to improve our adaptive
streaming logic by better predicting the bandwidth before the request
Expand Down Expand Up @@ -211,19 +221,16 @@ an XMLHttpRequest (it has no use, as our implementation does the same thing and
more):
```js
/**
* @param {string|undefined} url - the url the Manifest request should normally
* be on.
* Can be undefined in very specific conditions, like in cases when the
* `loadVideo` call had no defined URL (e.g. "local" manifest, playing a locally
* crafted "Metaplaylist" content).
* @param {Object} manifestInfo - Information about the Manifest to download
* @param {Object} callbacks - Object containing several callbacks to indicate
* that the manifest has been loaded, the loading operation has failed or to
* fallback to our default implementation. More information on this object below
* this code example.
* @returns {Function|undefined} - If a function is defined in the return value,
* it will be called if and when the request is canceled.
*/
const customManifestLoader = (url, callbacks) => {
const customManifestLoader = (manifestInfo, callbacks) => {
const { url } = manifestInfo;
const xhr = new XMLHttpRequest();
const baseTime = performance.now();

Expand Down Expand Up @@ -283,11 +290,14 @@ const customManifestLoader = (url, callbacks) => {

As you can see, this function takes two arguments:

1. **url**: The URL the Manifest request should normally be performed at.
1. **manifestInfo** (`object`): An Object giving information about the wanted
Manifest. This object contains the following properties:

- **url**: The URL the Manifest request should normally be performed at.

This argument can be `undefined` in very rare and specific conditions where
the Manifest URL doesn't exist or has not been communicated by the
application.
This argument can be `undefined` in very rare and specific conditions
where the Manifest URL doesn't exist or has not been communicated by the
application.

2. **callbacks**: An object containing multiple callbacks to allow this
`manifestLoader` to communicate the loaded Manifest or an encountered error
Expand Down
4 changes: 2 additions & 2 deletions src/core/eme/__tests__/__global__/get_license.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ describe("core - eme - global tests - getLicense", () => {
ignoreLicenseRequests: false }, done);
});

it("should be able to retry two times and succeed after a time with a resolved null", (done) => {
it("should be able to retry one time and succeed after a time with a resolved null", (done) => {
checkGetLicense({ isGetLicensePromiseBased: true,
configuredRetries: undefined,
configuredTimeout: undefined,
Expand All @@ -165,7 +165,7 @@ describe("core - eme - global tests - getLicense", () => {
ignoreLicenseRequests: true }, done);
});

it("should be able to retry two times and succeed after a time with a returned null", (done) => {
it("should be able to retry one time and succeed after a time with a returned null", (done) => {
checkGetLicense({ isGetLicensePromiseBased: false,
configuredRetries: undefined,
configuredTimeout: undefined,
Expand Down
16 changes: 13 additions & 3 deletions src/core/fetchers/segment/segment_fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export default function createSegmentFetcher<TLoadedFormat, TSegmentDataType>(
return function fetchSegment(
content : ISegmentLoaderContent
) : Observable<ISegmentFetcherEvent<TSegmentDataType>> {
const { segment } = content;
const { segment, adaptation, representation, manifest, period } = content;

// used by logs
const segmentIdString = getLoggableSegmentId(content);
Expand Down Expand Up @@ -174,6 +174,16 @@ export default function createSegmentFetcher<TLoadedFormat, TSegmentDataType>(
parse: generateParserFunction(chunkData, true) });
},
};
/** Segment context given to the transport pipelines. */
const context = { segment,
type: adaptation.type,
language: adaptation.language,
isLive: manifest.isLive,
periodStart: period.start,
periodEnd: period.end,
mimeType: representation.mimeType,
codecs: representation.codec,
manifestPublishTime: manifest.publishTime };

// Retrieve from cache if it exists
const cached = cache !== undefined ? cache.get(content) :
Expand Down Expand Up @@ -260,7 +270,7 @@ export default function createSegmentFetcher<TLoadedFormat, TSegmentDataType>(
url : string | null,
cancellationSignal: CancellationSignal
) {
return loadSegment(url, content, cancellationSignal, loaderCallbacks);
return loadSegment(url, context, cancellationSignal, loaderCallbacks);
}

/**
Expand All @@ -279,7 +289,7 @@ export default function createSegmentFetcher<TLoadedFormat, TSegmentDataType>(
const loaded = { data, isChunked };

try {
const parsed = parseSegment(loaded, content, initTimescale);
const parsed = parseSegment(loaded, context, initTimescale);

if (!parsedChunks[parsedChunkId]) {
segmentDurationAcc = segmentDurationAcc !== undefined &&
Expand Down
53 changes: 29 additions & 24 deletions src/core/stream/representation/representation_stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,40 +444,45 @@ export default function RepresentationStream<TSegmentDataType>({
IStreamNeedsManifestRefresh |
IStreamManifestMightBeOutOfSync>
{
// Supplementary encryption information might have been parsed.
for (const protInfo of evt.protectionData) {
// TODO better handle use cases like key rotation by not always grouping
// every protection data together? To check.
representation.addProtectionData(protInfo.initDataType, protInfo.initData);
}

let segmentEncryptionEvent$ : Observable<IEncryptionDataEncounteredEvent> = EMPTY;
if (!hasSentEncryptionData) {
const protData = representation.getAllEncryptionData().map(p =>
EVENTS.encryptionDataEncountered(p));
if (protData.length > 0) {
segmentEncryptionEvent$ = observableOf(...protData);
hasSentEncryptionData = true;
}
}

if (evt.segmentType === "init") {
nextTick(() => {
reCheckNeededSegments$.next();
});
if (!representation.index.isInitialized() &&
evt.segmentList !== undefined)
{
representation.index.initialize(evt.segmentList);
nextTick(() => { reCheckNeededSegments$.next(); });
}
initSegmentState.segmentData = evt.initializationData;
initSegmentState.isLoaded = true;

// Now that the initialization segment has been parsed - which may have
// included encryption information - take care of the encryption event
// if not already done.
const allEncryptionData = representation.getAllEncryptionData();
const initEncEvt$ = !hasSentEncryptionData &&
allEncryptionData.length > 0 ?
observableOf(...allEncryptionData.map(p =>
EVENTS.encryptionDataEncountered(p))) :
EMPTY;
const pushEvent$ = pushInitSegment({ playbackObserver,
content,
segment: evt.segment,
segmentData: evt.initializationData,
segmentBuffer });
return observableMerge(initEncEvt$, pushEvent$);
return observableMerge(segmentEncryptionEvent$, pushEvent$);
} else {
const { inbandEvents,
needsManifestRefresh,
protectionDataUpdate } = evt;

// TODO better handle use cases like key rotation by not always grouping
// every protection data together? To check.
const segmentEncryptionEvent$ = protectionDataUpdate &&
!hasSentEncryptionData ?
observableOf(...representation.getAllEncryptionData().map(p =>
EVENTS.encryptionDataEncountered(p))) :
EMPTY;
predictedSegments,
needsManifestRefresh } = evt;
if (predictedSegments !== undefined) {
representation.index.addPredictedSegments(predictedSegments, evt.segment);
}

const manifestRefresh$ = needsManifestRefresh === true ?
observableOf(EVENTS.needsManifestRefresh()) :
Expand Down
2 changes: 0 additions & 2 deletions src/manifest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import Manifest, {
import Period from "./period";
import Representation from "./representation";
import {
IBaseContentInfos,
IMetaPlaylistPrivateInfos,
IRepresentationIndex,
ISegment,
Expand Down Expand Up @@ -56,7 +55,6 @@ export {

// types
IAdaptationType,
IBaseContentInfos,
IHDRInformation,
IManifestParsingOptions,
IMetaPlaylistPrivateInfos,
Expand Down
2 changes: 1 addition & 1 deletion src/manifest/representation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ class Representation {
* @param {Uint8Array} data
* @returns {boolean}
*/
public _addProtectionData(
public addProtectionData(
initDataType : string,
data : Array<{
systemId : string;
Expand Down
2 changes: 0 additions & 2 deletions src/manifest/representation_index/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,12 @@

import StaticRepresentationIndex from "./static";
import {
IBaseContentInfos,
IMetaPlaylistPrivateInfos,
IRepresentationIndex,
ISegment,
} from "./types";

export {
IBaseContentInfos,
IMetaPlaylistPrivateInfos,
IRepresentationIndex,
ISegment,
Expand Down
8 changes: 8 additions & 0 deletions src/manifest/representation_index/static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,14 @@ export default class StaticRepresentationIndex implements IRepresentationIndex {
return true;
}

initialize() : void {
log.error("A `StaticRepresentationIndex` does not need to be initialized");
}

addPredictedSegments() : void {
log.warn("Cannot add predicted segments to a `StaticRepresentationIndex`");
}

_replace() : void {
log.warn("Tried to replace a static RepresentationIndex");
}
Expand Down
Loading

0 comments on commit 82c41fa

Please sign in to comment.