Skip to content

Commit 81ac894

Browse files
committed
Add declareInitSegment method to the SegmentBuffer abstraction
This commit adds the `declareInitSegment` and `freeInitSegment` methods to the RxPlayer's SegmentBuffer abstractions (which is the part of the code handling the operations on SourceBuffers, such as pushing media and init segments). The short term idea is to improve the handling of initialization segments in the SegmentBuffer. Until now, each pushed media segment lead to a check that the initialization segment it relies on is the same than the last one pushed. To be able to perform that check, a caller need to communicate again the initialization segment's data each time a chunk is pushed to the buffer. This check can be performed efficiently in most cases because we first check init segment's data equality by reference, which, by pure luck, should be equal in most cases in the current code. In cases where it isn't the same reference however, it can lead to a byte-per-byte check, which should not be an issue in terms of performance in most cases, but is still an ugly specificity which could be handled in a more optimal and understandable way. This commit now allows the definition of a `initSegmentUniqueId`, an identifier for initialization segments, on SegmentBuffers. Any pushed segments can then refer to its associated init segment by indicating which `initSegmentUniqueId` it is linked to. The SegmentBuffer will ensure behind the hood that the right initialization segment is pushed before pushing media segments, like before, excepted that this can now be done just by comparing this `initSegmentUniqueId` - it also means that the caller is no more required to keep in memory the data of the loaded initialization segment, the `SegmentBuffer` is already doing that. Previously, the initialization segment's data was kept by the `RepresentationStream`, the abstraction choosing which segments to load (which is part of the reasons why the reference mostly never changed). The declaration and "freeing" of init segment is done through a `declareInitSegment`/`freeInitSegment` pair of methods on a `SegmentBuffer`. This sadly means that memory freeing for the initialization segment is now manual, whereas we just relied on garbage collection when the initialization segment was directly used. --- Though mostly, the long term benefit is to implement the hybrid-worker mode that we plan to have in the future, where buffering is performed in a WebWorker (thus improving concurrence with an application, with the goal of preventing both UI stuttering due to heavy player tasks and rebuffering due to heavy UI tasks). In the currently-planned long term worker features we would have thus the following modes: - full worker: where both the rebuffering logic and MSE API are called in a WebWorker, allowing to avoid UI and media playback blocking each other to some extent This however requires the [MSE-in-Worker](w3c/media-source#175) feature to be available in the browser AND it also implies a more complex API, notably some callbacks (`manifestLoader`, `segmentLoader` and `representationFilter`) which will have to be updated. - hybrid mode: The buffering logic is mainly performed in a WebWorker but MSE API are still in the main thread. This allows e.g. to not fight for CPU with the UI to know which segments to download and to avoid blocking the UI when the Manifest is being parsed. Though the UI blocking could still mean that a loaded segment is waiting to be pushed in that mode. Because here MSE APIs may have to be called through `postMessage`-style message passing, the previous logic of communicating each time the same initialization segment each time a segment was pushed, with no mean to just move that data (in JavaScript linguo, to "transfer" it) was considerably worst than before. Relying on a short identifier instead seems a better solution here. - normal mode: The current mode where everything stays in main thread. However it should be noted that all of this long term objective is still in an higly experimental phase, and the gains are only theoretical for now.
1 parent 79b222f commit 81ac894

File tree

12 files changed

+199
-110
lines changed

12 files changed

+199
-110
lines changed

src/core/segment_buffers/implementations/audio_video/audio_video_segment_buffer.ts

+56-55
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,8 @@ import {
2121
import config from "../../../../config";
2222
import log from "../../../../log";
2323
import { getLoggableSegmentId } from "../../../../manifest";
24-
import areArraysOfNumbersEqual from "../../../../utils/are_arrays_of_numbers_equal";
2524
import assertUnreachable from "../../../../utils/assert_unreachable";
26-
import { toUint8Array } from "../../../../utils/byte_parsing";
2725
import createCancellablePromise from "../../../../utils/create_cancellable_promise";
28-
import hashBuffer from "../../../../utils/hash_buffer";
2926
import noop from "../../../../utils/noop";
3027
import objectAssign from "../../../../utils/object_assign";
3128
import TaskCanceller, {
@@ -139,20 +136,26 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer {
139136
private _pendingTask : IAVSBPendingTask | null;
140137

141138
/**
142-
* Keep track of the of the latest init segment pushed in the linked
143-
* SourceBuffer.
139+
* Keep track of the unique identifier of the of the latest init segment
140+
* pushed to the linked SourceBuffer.
144141
*
145-
* This allows to be sure the right initialization segment is pushed before
146-
* any chunk is.
142+
* Such identifiers are first declared through the `declareInitSegment`
143+
* method and the corresponding initialization segment is then pushed through
144+
* the `pushChunk` method.
145+
*
146+
* Keeping track of this allows to be sure the right initialization segment is
147+
* pushed before any chunk is.
147148
*
148149
* `null` if no initialization segment have been pushed to the
149150
* `AudioVideoSegmentBuffer` yet.
150151
*/
151-
private _lastInitSegment : { /** The init segment itself. */
152-
data : Uint8Array;
153-
/** Hash of the initSegment for fast comparison */
154-
hash : number; } |
155-
null;
152+
private _lastInitSegmentUniqueId : string | null;
153+
154+
/**
155+
* Link unique identifiers for initialization segments (as communicated by
156+
* `declareInitSegment`) to the corresponding initialization data.
157+
*/
158+
private _initSegmentsMap : Map<string, BufferSource>;
156159

157160
/**
158161
* @constructor
@@ -174,8 +177,9 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer {
174177
this._sourceBuffer = sourceBuffer;
175178
this._queue = [];
176179
this._pendingTask = null;
177-
this._lastInitSegment = null;
180+
this._lastInitSegmentUniqueId = null;
178181
this.codec = codec;
182+
this._initSegmentsMap = new Map();
179183

180184
const onError = this._onPendingTaskError.bind(this);
181185
const reCheck = this._flush.bind(this);
@@ -198,6 +202,20 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer {
198202
});
199203
}
200204

205+
public declareInitSegment(
206+
uniqueId : string,
207+
initSegmentData : unknown
208+
) : void {
209+
assertDataIsBufferSource(initSegmentData);
210+
this._initSegmentsMap.set(uniqueId, initSegmentData);
211+
}
212+
213+
public freeInitSegment(
214+
uniqueId : string
215+
) : void {
216+
this._initSegmentsMap.delete(uniqueId);
217+
}
218+
201219
/**
202220
* Push a chunk of the media segment given to the attached SourceBuffer, in a
203221
* FIFO queue.
@@ -229,12 +247,12 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer {
229247
infos : IPushChunkInfos<unknown>,
230248
cancellationSignal : CancellationSignal
231249
) : Promise<void> {
232-
assertPushedDataIsBufferSource(infos);
250+
assertDataIsBufferSource(infos.data.chunk);
233251
log.debug("AVSB: receiving order to push data to the SourceBuffer",
234252
this.bufferType,
235253
getLoggableSegmentId(infos.inventoryInfos));
236254
return this._addToQueue({ type: SegmentBufferOperation.Push,
237-
value: infos },
255+
value: infos as IPushChunkInfos<BufferSource> },
238256
cancellationSignal);
239257
}
240258

@@ -350,7 +368,7 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer {
350368
* @param {Event} err
351369
*/
352370
private _onPendingTaskError(err : unknown) : void {
353-
this._lastInitSegment = null; // initialize init segment as a security
371+
this._lastInitSegmentUniqueId = null; // initialize init segment as a security
354372
if (this._pendingTask !== null) {
355373
const error = err instanceof Error ?
356374
err :
@@ -447,7 +465,7 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer {
447465
const error = e instanceof Error ?
448466
e :
449467
new Error("An unknown error occured when preparing a push operation");
450-
this._lastInitSegment = null; // initialize init segment as a security
468+
this._lastInitSegmentUniqueId = null; // initialize init segment as a security
451469
nextItem.reject(error);
452470
return;
453471
}
@@ -557,15 +575,17 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer {
557575
this._sourceBuffer.appendWindowEnd = appendWindow[1];
558576
}
559577

560-
if (data.initSegment !== null &&
561-
(hasUpdatedSourceBufferType || !this._isLastInitSegment(data.initSegment)))
578+
if (data.initSegmentUniqueId !== null &&
579+
(hasUpdatedSourceBufferType ||
580+
!this._isLastInitSegment(data.initSegmentUniqueId)))
562581
{
563582
// Push initialization segment before the media segment
564-
const segmentData = data.initSegment;
583+
const segmentData = this._initSegmentsMap.get(data.initSegmentUniqueId);
584+
if (segmentData === undefined) {
585+
throw new Error("Invalid initialization segment uniqueId");
586+
}
565587
dataToPush.push(segmentData);
566-
const initU8 = toUint8Array(segmentData);
567-
this._lastInitSegment = { data: initU8,
568-
hash: hashBuffer(initU8) };
588+
this._lastInitSegmentUniqueId = data.initSegmentUniqueId;
569589
}
570590

571591
if (data.chunk !== null) {
@@ -576,56 +596,37 @@ export default class AudioVideoSegmentBuffer extends SegmentBuffer {
576596
}
577597

578598
/**
579-
* Return `true` if the given `segmentData` is the same segment than the last
599+
* Return `true` if the given `uniqueId` is the identifier of the last
580600
* initialization segment pushed to the `AudioVideoSegmentBuffer`.
581-
* @param {BufferSource} segmentData
601+
* @param {string} uniqueId
582602
* @returns {boolean}
583603
*/
584-
private _isLastInitSegment(segmentData : BufferSource) : boolean {
585-
if (this._lastInitSegment === null) {
604+
private _isLastInitSegment(uniqueId : string) : boolean {
605+
if (this._lastInitSegmentUniqueId === null) {
586606
return false;
587607
}
588-
if (this._lastInitSegment.data === segmentData) {
589-
return true;
590-
}
591-
const oldInit = this._lastInitSegment.data;
592-
if (oldInit.byteLength === segmentData.byteLength) {
593-
const newInitU8 = toUint8Array(segmentData);
594-
if (hashBuffer(newInitU8) === this._lastInitSegment.hash &&
595-
areArraysOfNumbersEqual(oldInit, newInitU8))
596-
{
597-
return true;
598-
}
599-
}
600-
return false;
608+
return this._lastInitSegmentUniqueId === uniqueId;
601609
}
602610
}
603611

604612
/**
605613
* Throw if the given input is not in the expected format.
606614
* Allows to enforce runtime type-checking as compile-time type-checking here is
607615
* difficult to enforce.
608-
* @param {Object} pushedData
616+
* @param {Object} data
609617
*/
610-
function assertPushedDataIsBufferSource(
611-
pushedData : IPushChunkInfos<unknown>
612-
) : asserts pushedData is IPushChunkInfos<BufferSource> {
618+
function assertDataIsBufferSource(
619+
data : unknown
620+
) : asserts data is BufferSource {
613621
if (__ENVIRONMENT__.CURRENT_ENV === __ENVIRONMENT__.PRODUCTION as number) {
614622
return;
615623
}
616-
const { chunk, initSegment } = pushedData.data;
617624
if (
618-
typeof chunk !== "object" ||
619-
typeof initSegment !== "object" ||
620-
(
621-
chunk !== null &&
622-
!(chunk instanceof ArrayBuffer) &&
623-
!((chunk as ArrayBufferView).buffer instanceof ArrayBuffer)
624-
) ||
625+
typeof data !== "object" ||
625626
(
626-
initSegment !== null &&
627-
!(initSegment instanceof ArrayBuffer) &&
628-
!((initSegment as ArrayBufferView).buffer instanceof ArrayBuffer)
627+
data !== null &&
628+
!(data instanceof ArrayBuffer) &&
629+
!((data as ArrayBufferView).buffer instanceof ArrayBuffer)
629630
)
630631
) {
631632
throw new Error("Invalid data given to the AudioVideoSegmentBuffer");

src/core/segment_buffers/implementations/image/image_segment_buffer.ts

+16
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,22 @@ export default class ImageSegmentBuffer extends SegmentBuffer {
3838
this._buffered = new ManualTimeRanges();
3939
}
4040

41+
/**
42+
* @param {string} uniqueId
43+
*/
44+
public declareInitSegment(uniqueId : string): void {
45+
log.warn("ISB: Declaring initialization segment for image SegmentBuffer",
46+
uniqueId);
47+
}
48+
49+
/**
50+
* @param {string} uniqueId
51+
*/
52+
public freeInitSegment(uniqueId : string): void {
53+
log.warn("ISB: Freeing initialization segment for image SegmentBuffer",
54+
uniqueId);
55+
}
56+
4157
/**
4258
* @param {Object} data
4359
* @returns {Promise}

src/core/segment_buffers/implementations/text/html/html_text_segment_buffer.ts

+16
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,22 @@ export default class HTMLTextSegmentBuffer extends SegmentBuffer {
137137
this.autoRefreshSubtitles(this._canceller.signal);
138138
}
139139

140+
/**
141+
* @param {string} uniqueId
142+
*/
143+
public declareInitSegment(uniqueId : string): void {
144+
log.warn("ISB: Declaring initialization segment for image SegmentBuffer",
145+
uniqueId);
146+
}
147+
148+
/**
149+
* @param {string} uniqueId
150+
*/
151+
public freeInitSegment(uniqueId : string): void {
152+
log.warn("ISB: Freeing initialization segment for image SegmentBuffer",
153+
uniqueId);
154+
}
155+
140156
/**
141157
* Push text segment to the HTMLTextSegmentBuffer.
142158
* @param {Object} infos

src/core/segment_buffers/implementations/text/native/native_text_segment_buffer.ts

+16
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,22 @@ export default class NativeTextSegmentBuffer extends SegmentBuffer {
6666
this._trackElement = trackElement;
6767
}
6868

69+
/**
70+
* @param {string} uniqueId
71+
*/
72+
public declareInitSegment(uniqueId : string): void {
73+
log.warn("ISB: Declaring initialization segment for image SegmentBuffer",
74+
uniqueId);
75+
}
76+
77+
/**
78+
* @param {string} uniqueId
79+
*/
80+
public freeInitSegment(uniqueId : string): void {
81+
log.warn("ISB: Freeing initialization segment for image SegmentBuffer",
82+
uniqueId);
83+
}
84+
6985
/**
7086
* @param {Object} infos
7187
* @returns {Promise}

src/core/segment_buffers/implementations/types.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ export abstract class SegmentBuffer {
8787
this._segmentInventory = new SegmentInventory();
8888
}
8989

90+
public abstract declareInitSegment(
91+
uniqueId : string,
92+
initSegmentData : unknown
93+
) : void;
94+
95+
public abstract freeInitSegment(uniqueId : string) : void;
96+
9097
/**
9198
* Push a chunk of the media segment given to the attached buffer, in a
9299
* FIFO queue.
@@ -96,7 +103,8 @@ export abstract class SegmentBuffer {
96103
* pushed.
97104
*
98105
* Depending on the type of data appended, the pushed chunk might rely on an
99-
* initialization segment, given through the `data.initSegment` property.
106+
* initialization segment, which had to be previously declared through the
107+
* `declareInitSegment` method.
100108
*
101109
* Such initialization segment will be first pushed to the buffer if the
102110
* last pushed segment was associated to another initialization segment.
@@ -106,7 +114,7 @@ export abstract class SegmentBuffer {
106114
* reference).
107115
*
108116
* If you don't need any initialization segment to push the wanted chunk, you
109-
* can just set `data.initSegment` to `null`.
117+
* can just set the corresponding property to `null`.
110118
*
111119
* You can also only push an initialization segment by setting the
112120
* `data.chunk` argument to null.
@@ -230,12 +238,16 @@ export type IBufferType = "audio" |
230238
*/
231239
export interface IPushedChunkData<T> {
232240
/**
233-
* The whole initialization segment's data related to the chunk you want to
241+
* The `uniqueId` of the initialization segment linked to the data you want to
234242
* push.
243+
*
244+
* That identifier should previously have been declared through the
245+
* `declareInitSegment` method and not freed.
246+
*
235247
* To set to `null` either if no initialization data is needed, or if you are
236248
* confident that the last pushed one is compatible.
237249
*/
238-
initSegment: T | null;
250+
initSegmentUniqueId : string | null;
239251
/**
240252
* Chunk you want to push.
241253
* This can be the whole decodable segment's data or just a decodable sub-part

0 commit comments

Comments
 (0)