Skip to content

Commit

Permalink
feature: add the possibility to rely on managedMediaSource on iOS dev…
Browse files Browse the repository at this point in the history
…ices

add unit test for managed media source

handle start and end stream event from managedMediaSource

fix unit test

react to event endStreaming and startStreaming to immediately trigger a new observation and to download the segments sooner

review feedback

restart Loading queue if it canLoad is updated

add clearSignal to prevent memory leak

remove undefined from canLoad

only set disableRemotePlayback if it's defined by the browser

fmt
  • Loading branch information
Florent-Bouisset committed Nov 7, 2024
1 parent 7a7395a commit 90e74cc
Show file tree
Hide file tree
Showing 15 changed files with 318 additions and 16 deletions.
37 changes: 37 additions & 0 deletions src/compat/__tests__/browser_compatibility_types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe("compat - browser compatibility types", () => {
MozMediaSource?: unknown;
WebKitMediaSource?: unknown;
MSMediaSource?: unknown;
ManagedMediaSource?: unknown;
}
const gs = globalScope as IFakeWindow;
beforeEach(() => {
Expand All @@ -22,11 +23,13 @@ describe("compat - browser compatibility types", () => {
const origMozMediaSource = gs.MozMediaSource;
const origWebKitMediaSource = gs.WebKitMediaSource;
const origMSMediaSource = gs.MSMediaSource;
const origManagedMediaSource = gs.ManagedMediaSource;

gs.MediaSource = { a: 1 };
gs.MozMediaSource = { a: 2 };
gs.WebKitMediaSource = { a: 3 };
gs.MSMediaSource = { a: 4 };
gs.ManagedMediaSource = { a: 5 };

const { MediaSource_ } = await vi.importActual("../browser_compatibility_types");
expect(MediaSource_).toEqual({ a: 1 });
Expand All @@ -35,6 +38,7 @@ describe("compat - browser compatibility types", () => {
gs.MozMediaSource = origMozMediaSource;
gs.WebKitMediaSource = origWebKitMediaSource;
gs.MSMediaSource = origMSMediaSource;
gs.ManagedMediaSource = origManagedMediaSource;
});

it("should use MozMediaSource if defined and MediaSource is not", async () => {
Expand All @@ -46,6 +50,7 @@ describe("compat - browser compatibility types", () => {
const origMozMediaSource = gs.MozMediaSource;
const origWebKitMediaSource = gs.WebKitMediaSource;
const origMSMediaSource = gs.MSMediaSource;
const origManagedMediaSource = gs.ManagedMediaSource;

gs.MediaSource = undefined;
gs.MozMediaSource = { a: 2 };
Expand All @@ -59,6 +64,7 @@ describe("compat - browser compatibility types", () => {
gs.MozMediaSource = origMozMediaSource;
gs.WebKitMediaSource = origWebKitMediaSource;
gs.MSMediaSource = origMSMediaSource;
gs.ManagedMediaSource = origManagedMediaSource;
});

it("should use WebKitMediaSource if defined and MediaSource is not", async () => {
Expand All @@ -70,6 +76,7 @@ describe("compat - browser compatibility types", () => {
const origMozMediaSource = gs.MozMediaSource;
const origWebKitMediaSource = gs.WebKitMediaSource;
const origMSMediaSource = gs.MSMediaSource;
const origManagedMediaSource = gs.ManagedMediaSource;

gs.MediaSource = undefined;
gs.MozMediaSource = undefined;
Expand All @@ -83,6 +90,7 @@ describe("compat - browser compatibility types", () => {
gs.MozMediaSource = origMozMediaSource;
gs.WebKitMediaSource = origWebKitMediaSource;
gs.MSMediaSource = origMSMediaSource;
gs.ManagedMediaSource = origManagedMediaSource;
});

it("should use MSMediaSource if defined and MediaSource is not", async () => {
Expand All @@ -94,6 +102,7 @@ describe("compat - browser compatibility types", () => {
const origMozMediaSource = gs.MozMediaSource;
const origWebKitMediaSource = gs.WebKitMediaSource;
const origMSMediaSource = gs.MSMediaSource;
const origManagedMediaSource = gs.ManagedMediaSource;

gs.MediaSource = undefined;
gs.MozMediaSource = undefined;
Expand All @@ -107,5 +116,33 @@ describe("compat - browser compatibility types", () => {
gs.MozMediaSource = origMozMediaSource;
gs.WebKitMediaSource = origWebKitMediaSource;
gs.MSMediaSource = origMSMediaSource;
gs.ManagedMediaSource = origManagedMediaSource;
});

it("should use ManagedMediaSource if defined and MediaSource is not", async () => {
vi.doMock("../../utils/is_node", () => ({
default: false,
}));

const origMediaSource = gs.MediaSource;
const origMozMediaSource = gs.MozMediaSource;
const origWebKitMediaSource = gs.WebKitMediaSource;
const origMSMediaSource = gs.MSMediaSource;
const origManagedMediaSource = gs.ManagedMediaSource;

gs.MediaSource = undefined;
gs.MozMediaSource = undefined;
gs.WebKitMediaSource = undefined;
gs.MSMediaSource = undefined;
gs.ManagedMediaSource = { a: 5 };

const { MediaSource_ } = await vi.importActual("../browser_compatibility_types");
expect(MediaSource_).toEqual({ a: 5 });

gs.MediaSource = origMediaSource;
gs.MozMediaSource = origMozMediaSource;
gs.WebKitMediaSource = origWebKitMediaSource;
gs.MSMediaSource = origMSMediaSource;
gs.ManagedMediaSource = origManagedMediaSource;
});
});
25 changes: 14 additions & 11 deletions src/compat/browser_compatibility_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

import type { IListener } from "../utils/event_emitter";
import globalScope from "../utils/global_scope";
import isNullOrUndefined from "../utils/is_null_or_undefined";

/** Regular MediaKeys type + optional functions present in IE11. */
interface ICompatMediaKeysConstructor {
Expand Down Expand Up @@ -116,6 +115,8 @@ export interface IMediaSourceEventMap {
sourceopen: Event;
sourceended: Event;
sourceclose: Event;
startstreaming: Event;
endstreaming: Event;
}

/**
Expand All @@ -136,6 +137,7 @@ export interface IMediaSource extends IEventTarget<IMediaSourceEventMap> {
handle?: MediaProvider | IMediaSource | undefined;
readyState: "closed" | "open" | "ended";
sourceBuffers: ISourceBufferList;
streaming?: boolean | undefined;

addSourceBuffer(type: string): ISourceBuffer;
clearLiveSeekableRange(): void;
Expand Down Expand Up @@ -252,6 +254,7 @@ export interface IMediaElement extends IEventTarget<IMediaElementEventMap> {
srcObject?: undefined | null | MediaProvider;
textTracks: TextTrackList | never[];
volume: number;
disableRemotePlayback?: boolean;

addTextTrack: (kind: TextTrackKind) => TextTrack;
appendChild<T extends Node>(x: T): void;
Expand Down Expand Up @@ -407,15 +410,15 @@ const gs = globalScope as any;
const MediaSource_:
| { new (): IMediaSource; isTypeSupported(type: string): boolean }
| undefined =
gs === undefined
? undefined
: !isNullOrUndefined(gs.MediaSource)
? gs.MediaSource
: !isNullOrUndefined(gs.MozMediaSource)
? gs.MozMediaSource
: !isNullOrUndefined(gs.WebKitMediaSource)
? gs.WebKitMediaSource
: gs.MSMediaSource;
gs?.MediaSource ??
gs?.MozMediaSource ??
gs?.WebKitMediaSource ??
gs?.MSMediaSource ??
gs?.ManagedMediaSource ??
undefined;

const isManagedMediaSource =
MediaSource_ !== undefined && MediaSource_ === gs?.ManagedMediaSource;
/* eslint-enable */

/** List an HTMLMediaElement's possible values for its readyState property. */
Expand Down Expand Up @@ -448,4 +451,4 @@ export type {
ICompatVTTCue,
ICompatVTTCueConstructor,
};
export { MediaSource_, READY_STATES };
export { MediaSource_, isManagedMediaSource, READY_STATES };
32 changes: 32 additions & 0 deletions src/core/fetchers/segment/segment_queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export default class SegmentQueue<T> extends EventEmitter<ISegmentQueueEvent<T>>
* load segments for.
* @param {boolean} hasInitSegment - Declare that an initialization segment
* will need to be downloaded.
* @param {Object} canLoad - Indicates if the loading of new segment is authorized,
* it can be temporarily forbiddden if there is enough buffered data.
*
* A `SegmentQueue` ALWAYS wait for the initialization segment to be
* loaded and parsed before parsing a media segment.
Expand All @@ -116,6 +118,7 @@ export default class SegmentQueue<T> extends EventEmitter<ISegmentQueueEvent<T>>
public resetForContent(
content: ISegmentQueueContext,
hasInitSegment: boolean,
canLoad: SharedReference<boolean>,
): SharedReference<ISegmentQueueItem> {
this._currentContentInfo?.currentCanceller.cancel();
const downloadQueue = new SharedReference<ISegmentQueueItem>({
Expand All @@ -136,9 +139,23 @@ export default class SegmentQueue<T> extends EventEmitter<ISegmentQueueEvent<T>>
initSegmentRequest: null,
mediaSegmentRequest: null,
mediaSegmentAwaitingInitMetadata: null,
canLoad,
};
this._currentContentInfo = currentContentInfo;

this._currentContentInfo.canLoad.onUpdate(
(val) => {
if (val) {
log.debug(
"SQ: Media segment can be loaded again, restarting queue.",
content.adaptation.type,
);
this._restartMediaSegmentDownloadingQueue(currentContentInfo);
}
},
{ clearSignal: currentCanceller.signal },
);

// Listen for asked media segments
downloadQueue.onUpdate(
(queue) => {
Expand Down Expand Up @@ -257,6 +274,10 @@ export default class SegmentQueue<T> extends EventEmitter<ISegmentQueueEvent<T>>
const { downloadQueue, content, initSegmentInfoRef, currentCanceller } = contentInfo;

const recursivelyRequestSegments = (): void => {
if (!contentInfo.canLoad.getValue()) {
log.debug("SQ: Segment fetching postponed because it cannot stream now.");
return;
}
const { segmentQueue } = downloadQueue.getValue();
const startingSegment = segmentQueue[0];
if (currentCanceller !== null && currentCanceller.isUsed()) {
Expand Down Expand Up @@ -681,4 +702,15 @@ interface ISegmentQueueContentInfo {
* `null` if no segment is awaiting an init segment.
*/
mediaSegmentAwaitingInitMetadata: string | null;
/**
* Indicates whether the user agent believes it has enough buffered data to ensure
* uninterrupted playback for a meaningful period or needs more data.
* It also reflects whether the user agent can retrieve and buffer data in an
* energy-efficient manner while maintaining the desired memory usage.
* The value can be `undefined` if the user agent does not provide this indicator.
* `true` indicates that the buffer is low, and more data should be buffered.
* `false` indicates that there is enough buffered data, and no additional data needs
* to be buffered at this time.
*/
canLoad: SharedReference<boolean>;
}
10 changes: 10 additions & 0 deletions src/core/stream/adaptation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@ export interface IAdaptationStreamPlaybackObservation
duration: number;
/** Theoretical maximum position on the content that can currently be played. */
maximumPosition: number;
/**
* Indicates whether the user agent believes it has enough buffered data to ensure
* uninterrupted playback for a meaningful period or needs more data.
* It also reflects whether the user agent can retrieve and buffer data in an
* energy-efficient manner while maintaining the desired memory usage.
* `true` indicates that the buffer is low, and more data should be buffered.
* `false` indicates that there is enough buffered data, and no additional data needs
* to be buffered at this time.
*/
canStream: boolean;
}

/** Arguments given when creating a new `AdaptationStream`. */
Expand Down
10 changes: 10 additions & 0 deletions src/core/stream/period/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ export interface IPeriodStreamPlaybackObservation {
* `null` if no buffer exists for that type of media.
*/
buffered: Record<ITrackType, IRange[] | null>;
/**
* Indicates whether the user agent believes it has enough buffered data to ensure
* uninterrupted playback for a meaningful period or needs more data.
* It also reflects whether the user agent can retrieve and buffer data in an
* energy-efficient manner while maintaining the desired memory usage.
* `true` indicates that the buffer is low, and more data should be buffered.
* `false` indicates that there is enough buffered data, and no additional data needs
* to be buffered at this time.
*/
canStream: boolean;
}

/** Arguments required by the `PeriodStream`. */
Expand Down
17 changes: 16 additions & 1 deletion src/core/stream/representation/representation_stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import config from "../../../config";
import log from "../../../log";
import type { ISegment } from "../../../manifest";
import objectAssign from "../../../utils/object_assign";
import SharedReference from "../../../utils/reference";
import type { CancellationSignal } from "../../../utils/task_canceller";
import TaskCanceller, { CancellationError } from "../../../utils/task_canceller";
import type {
Expand Down Expand Up @@ -209,8 +210,22 @@ export default function RepresentationStream<TSegmentDataType>(
segmentsLoadingCanceller.signal,
);

const canStream = new SharedReference<boolean>(true);

playbackObserver.listen((observation) => {
const observationCanStream = observation.canStream ?? true;
if (canStream.getValue() !== observationCanStream) {
log.debug("Stream: observation.canStream updated to", observationCanStream);
canStream.setValue(observationCanStream);
}
});

/** Emit the last scheduled downloading queue for segments. */
const segmentsToLoadRef = segmentQueue.resetForContent(content, hasInitSegment);
const segmentsToLoadRef = segmentQueue.resetForContent(
content,
hasInitSegment,
canStream,
);

segmentsLoadingCanceller.signal.register(() => {
segmentQueue.stop();
Expand Down
10 changes: 10 additions & 0 deletions src/core/stream/representation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,16 @@ export interface IRepresentationStreamPlaybackObservation {
paused: IPausedPlaybackObservation;
/** Last "playback rate" asked by the user. */
speed: number;
/**
* Indicates whether the user agent believes it has enough buffered data to ensure
* uninterrupted playback for a meaningful period or needs more data.
* It also reflects whether the user agent can retrieve and buffer data in an
* energy-efficient manner while maintaining the desired memory usage.
* `true` indicates that the buffer is low, and more data should be buffered.
* `false` indicates that there is enough buffered data, and no additional data needs
* to be buffered at this time.
*/
canStream: boolean;
}

/** Pause-related information linked to an emitted Playback observation. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
* limitations under the License.
*/

import { MediaSource_ } from "../../../compat/browser_compatibility_types";
import {
isManagedMediaSource,
MediaSource_,
} from "../../../compat/browser_compatibility_types";
import type { IMediaElement } from "../../../compat/browser_compatibility_types";
import log from "../../../log";
import { resetMediaElement } from "../../../main_thread/init/utils/create_media_source";
Expand Down Expand Up @@ -66,6 +69,26 @@ export default function prepareSourceBuffer(
resetMediaElement(videoElement, objectURL);
});
}
if (isManagedMediaSource && "disableRemotePlayback" in videoElement) {
const disableRemotePlaybackPreviousValue = videoElement.disableRemotePlayback;
cleanUpSignal.register(() => {
/**
* Restore the disableRemotePlayback attribute to the previous value
* in order to leave the <video> element in the same state has it was set
* by the application before calling the RxPlayer.
*/
if (disableRemotePlaybackPreviousValue !== undefined) {
videoElement.disableRemotePlayback = disableRemotePlaybackPreviousValue;
}
});

/**
* Using ManagedMediaSource needs to disableRemotePlayback or to provide
* an Airplay source alternative, such as HLS.
* https://github.com/w3c/media-source/issues/320
*/
videoElement.disableRemotePlayback = true;
}

mediaSource.addEventListener("mediaSourceOpen", onSourceOpen);
return () => {
Expand Down
Loading

0 comments on commit 90e74cc

Please sign in to comment.