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 };
39 changes: 38 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,36 @@ export default class SegmentQueue<T> extends EventEmitter<ISegmentQueueEvent<T>>
*/
private _currentContentInfo: ISegmentQueueContentInfo | null;

/**
* Indicates whether the segment queue is inturrupted or if
* it is authorized to download segment data.
* Updating this value to `true` should stop the
* loading of new media data. Updating this value to `false` should
* restart the downloading queue.
* Note: this does not affect the downloading of init segment as
* they are always downloaded, regardless of this property value.
*
* This option can be used to temporarly reduce the usage of the
* network.
*/
private isMediaSegmentQueueInterrupted: SharedReference<boolean>;

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

/**
Expand Down Expand Up @@ -139,6 +159,19 @@ export default class SegmentQueue<T> extends EventEmitter<ISegmentQueueEvent<T>>
};
this._currentContentInfo = currentContentInfo;

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

const recursivelyRequestSegments = (): void => {
if (this.isMediaSegmentQueueInterrupted.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
6 changes: 5 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 @@ -89,13 +90,16 @@ export default class SegmentQueueCreator {
* @param {string} bufferType - The type of buffer concerned (e.g. "audio",
* "video", etc.)
* @param {Object} eventListeners
* @param {Object} isMediaSegmentQueueInterrupted - Wheter the downloading of media
* segment should be interrupted or not.
* @returns {Object} - `SegmentQueue`, which is an abstraction allowing to
* perform a queue of segment requests for a given media type (here defined by
* `bufferType`) with associated priorities.
*/
public createSegmentQueue(
bufferType: IBufferType,
eventListeners: ISegmentFetcherLifecycleCallbacks,
isMediaSegmentQueueInterrupted: SharedReference<boolean>,
): SegmentQueue<unknown> {
const requestOptions = getSegmentFetcherRequestOptions(this._backoffOptions);
const pipelines = this._transport[bufferType];
Expand All @@ -113,7 +117,7 @@ export default class SegmentQueueCreator {
this._prioritizer,
segmentFetcher,
);
return new SegmentQueue(prioritizedSegmentFetcher);
return new SegmentQueue(prioritizedSegmentFetcher, isMediaSegmentQueueInterrupted);
}
}

Expand Down
17 changes: 17 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,22 @@ export default function AdaptationStream(
adapStreamCanceller.signal,
);

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

/** Allows a `RepresentationStream` to easily fetch media segments. */
const segmentQueue = segmentQueueCreator.createSegmentQueue(
adaptation.type,
Expand All @@ -126,6 +142,7 @@ export default function AdaptationStream(
onProgress: abrCallbacks.requestProgress,
onMetrics: abrCallbacks.metrics,
},
isMediaSegmentQueueInterrupted,
);
/* 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
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