Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: add the possibility to rely on managedMediaSource on iOS devices #1562

Merged
merged 9 commits into from
Nov 18, 2024
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 };
37 changes: 36 additions & 1 deletion src/core/fetchers/segment/segment_queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,32 @@ export default class SegmentQueue<T> extends EventEmitter<ISegmentQueueEvent<T>>
*/
private _currentContentInfo: ISegmentQueueContentInfo | 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.
*/
private canLoad: SharedReference<boolean>;

/**
* Create a new `SegmentQueue`.
*
* @param {Object} segmentFetcher - Interface to facilitate the download of
* segments.
*/
constructor(segmentFetcher: IPrioritizedSegmentFetcher<T>) {
constructor(
segmentFetcher: IPrioritizedSegmentFetcher<T>,
canLoad: SharedReference<boolean>,
) {
super();
this._segmentFetcher = segmentFetcher;
this._currentContentInfo = null;
this.canLoad = canLoad;
}

/**
Expand Down Expand Up @@ -99,6 +115,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 Down Expand Up @@ -139,6 +157,19 @@ export default class SegmentQueue<T> extends EventEmitter<ISegmentQueueEvent<T>>
};
this._currentContentInfo = currentContentInfo;

this.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 +288,10 @@ export default class SegmentQueue<T> extends EventEmitter<ISegmentQueueEvent<T>>
const { downloadQueue, content, initSegmentInfoRef, currentCanceller } = contentInfo;

const recursivelyRequestSegments = (): void => {
if (!this.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
4 changes: 3 additions & 1 deletion src/core/fetchers/segment/segment_queue_creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import config from "../../../config";
import type { ISegmentPipeline, ITransportPipelines } from "../../../transports";
import type SharedReference from "../../../utils/reference";
import type { CancellationSignal } from "../../../utils/task_canceller";
import type CmcdDataBuilder from "../../cmcd";
import type { IBufferType } from "../../segment_sinks";
Expand Down Expand Up @@ -96,6 +97,7 @@ export default class SegmentQueueCreator {
public createSegmentQueue(
bufferType: IBufferType,
eventListeners: ISegmentFetcherLifecycleCallbacks,
canLoad: SharedReference<boolean>,
): SegmentQueue<unknown> {
const requestOptions = getSegmentFetcherRequestOptions(this._backoffOptions);
const pipelines = this._transport[bufferType];
Expand All @@ -113,7 +115,7 @@ export default class SegmentQueueCreator {
this._prioritizer,
segmentFetcher,
);
return new SegmentQueue(prioritizedSegmentFetcher);
return new SegmentQueue(prioritizedSegmentFetcher, canLoad);
}
}

Expand Down
14 changes: 14 additions & 0 deletions src/core/stream/adaptation/adaptation_stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,19 @@ export default function AdaptationStream(
adapStreamCanceller.signal,
);

const canStream = new SharedReference<boolean>(true);
/** Update the `canLoad` ref on observation update */
playbackObserver.listen(
(observation) => {
const observationCanStream = observation.canStream ?? true;
if (canStream.getValue() !== observationCanStream) {
log.debug("Stream: observation.canStream updated to", observationCanStream);
canStream.setValue(observationCanStream);
}
},
{ clearSignal: adapStreamCanceller.signal },
);

/** Allows a `RepresentationStream` to easily fetch media segments. */
const segmentQueue = segmentQueueCreator.createSegmentQueue(
adaptation.type,
Expand All @@ -126,6 +139,7 @@ export default function AdaptationStream(
onProgress: abrCallbacks.requestProgress,
onMetrics: abrCallbacks.metrics,
},
canStream,
);
/* eslint-enable @typescript-eslint/unbound-method */

Expand Down
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
14 changes: 14 additions & 0 deletions 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,6 +210,19 @@ 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);
}
},
{ clearSignal: segmentsLoadingCanceller.signal },
);
peaBerberian marked this conversation as resolved.
Show resolved Hide resolved

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

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 @@ -17,7 +17,10 @@
import { 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";
import {
resetMediaElement,
disableRemotePlaybackOnManagedMediaSource,
} from "../../../main_thread/init/utils/create_media_source";
import { SourceBufferType } from "../../../mse";
import type { MainSourceBufferInterface } from "../../../mse/main_media_source_interface";
import MainMediaSourceInterface from "../../../mse/main_media_source_interface";
Expand Down Expand Up @@ -66,6 +69,7 @@ export default function prepareSourceBuffer(
resetMediaElement(videoElement, objectURL);
});
}
disableRemotePlaybackOnManagedMediaSource(videoElement, cleanUpSignal);

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