Skip to content

Commit

Permalink
Use ManagedMediaSource when available
Browse files Browse the repository at this point in the history
  • Loading branch information
robwalch committed Jun 5, 2023
1 parent f97f561 commit 403c49f
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 11 deletions.
4 changes: 4 additions & 0 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1578,12 +1578,14 @@ class Hls implements HlsEventEmitter {
on<E extends keyof HlsListeners, Context = undefined>(event: E, listener: HlsListeners[E], context?: Context): void;
// (undocumented)
once<E extends keyof HlsListeners, Context = undefined>(event: E, listener: HlsListeners[E], context?: Context): void;
pauseBuffering(): void;
get playingDate(): Date | null;
recoverMediaError(): void;
// (undocumented)
removeAllListeners<E extends keyof HlsListeners>(event?: E | undefined): void;
// (undocumented)
removeLevel(levelIndex: any, urlId?: number): void;
resumeBuffering(): void;
get startLevel(): number;
// Warning: (ae-setter-with-docs) The doc comment for the property "startLevel" must appear on the getter, not the setter.
set startLevel(newLevel: number);
Expand Down Expand Up @@ -2586,6 +2588,8 @@ export interface ManifestParsedData {
export interface MediaAttachedData {
// (undocumented)
media: HTMLMediaElement;
// (undocumented)
mediaSource?: MediaSource;
}

// Warning: (ae-missing-release-tag) "MediaAttachingData" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
Expand Down
47 changes: 45 additions & 2 deletions src/controller/buffer-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
SourceBufferName,
SourceBufferListeners,
} from '../types/buffer';
import CapLevelController from './cap-level-controller';
import type {
LevelUpdatedData,
BufferAppendingData,
Expand All @@ -29,7 +30,6 @@ import type { ChunkMetadata } from '../types/transmuxer';
import type Hls from '../hls';
import type { LevelDetails } from '../loader/level-details';

const MediaSource = getMediaSource();
const VIDEO_CODEC_PROFILE_REPLACE =
/(avc[1234]|hvc1|hev1|dvh[1e]|vp09|av01)(?:\.[^.,]+)+/;

Expand Down Expand Up @@ -157,19 +157,50 @@ export default class BufferController implements ComponentAPI {
data: MediaAttachingData
) {
const media = (this.media = data.media);
const MediaSource = getMediaSource();
if (media && MediaSource) {
const ms = (this.mediaSource = new MediaSource());
// MediaSource listeners are arrow functions with a lexical scope, and do not need to be bound
ms.addEventListener('sourceopen', this._onMediaSourceOpen);
ms.addEventListener('sourceended', this._onMediaSourceEnded);
ms.addEventListener('sourceclose', this._onMediaSourceClose);
ms.addEventListener('startstreaming', this._onStartStreaming);
ms.addEventListener('endstreaming', this._onEndStreaming);
// link video and media Source
media.src = self.URL.createObjectURL(ms);
// cache the locally generated object url
this._objectUrl = media.src;
media.addEventListener('emptied', this._onMediaEmptied);
}
}
private _onEndStreaming = (event) => {
this.hls.pauseBuffering();
};
private _onStartStreaming = (event) => {
const { hls, mediaSource } = this;
if (!hls || !mediaSource) {
return;
}
if ('quality' in mediaSource) {
if (mediaSource.quality === 'low') {
hls.autoLevelCapping = CapLevelController.getMaxLevelByMediaSize(
hls.levels,
1280,
720
);
} else if (mediaSource.quality === 'medium') {
hls.autoLevelCapping = CapLevelController.getMaxLevelByMediaSize(
hls.levels,
1920,
1080
);
} else {
// do not cap max quality
hls.autoLevelCapping = -1;
}
}
hls.resumeBuffering();
};

protected onMediaDetaching() {
const { media, mediaSource, _objectUrl } = this;
Expand All @@ -193,6 +224,8 @@ export default class BufferController implements ComponentAPI {
mediaSource.removeEventListener('sourceopen', this._onMediaSourceOpen);
mediaSource.removeEventListener('sourceended', this._onMediaSourceEnded);
mediaSource.removeEventListener('sourceclose', this._onMediaSourceClose);
mediaSource.removeEventListener('startstreaming', this._onStartStreaming);
mediaSource.removeEventListener('endstreaming', this._onEndStreaming);

// Detach properly the MediaSource from the HTMLMediaElement as
// suggested in https://github.com/w3c/media-source/issues/53.
Expand Down Expand Up @@ -777,6 +810,13 @@ export default class BufferController implements ComponentAPI {
this.addBufferListener(sbName, 'updatestart', this._onSBUpdateStart);
this.addBufferListener(sbName, 'updateend', this._onSBUpdateEnd);
this.addBufferListener(sbName, 'error', this._onSBUpdateError);
// ManagedSourceBuffer bufferedchange event
this.addBufferListener(sbName, 'bufferedchange', (event) => {
this.hls.trigger(Events.BUFFER_FLUSHED, {
type: trackName as SourceBufferName,
});
});

this.tracks[trackName] = {
buffer: sb,
codec: codec,
Expand Down Expand Up @@ -808,7 +848,10 @@ export default class BufferController implements ComponentAPI {
if (media) {
media.removeEventListener('emptied', this._onMediaEmptied);
this.updateMediaElementDuration();
this.hls.trigger(Events.MEDIA_ATTACHED, { media });
this.hls.trigger(Events.MEDIA_ATTACHED, {
media,
mediaSource: mediaSource as MediaSource,
});
}

if (mediaSource) {
Expand Down
3 changes: 1 addition & 2 deletions src/demux/transmuxer-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ import type { PlaylistLevelType } from '../types/loader';
import type { TypeSupported } from './tsdemuxer';
import type { RationalTimestamp } from '../utils/timescale-conversion';

const MediaSource = getMediaSource() || { isTypeSupported: () => false };

export default class TransmuxerInterface {
public error: Error | null = null;
private hls: Hls;
Expand Down Expand Up @@ -66,6 +64,7 @@ export default class TransmuxerInterface {
this.observer.on(Events.FRAG_DECRYPTED, forwardMessage);
this.observer.on(Events.ERROR, forwardMessage);

const MediaSource = getMediaSource() || { isTypeSupported: () => false };
const typeSupported: TypeSupported = {
mp4: MediaSource.isTypeSupported('video/mp4'),
mpeg: MediaSource.isTypeSupported('audio/mpeg'),
Expand Down
28 changes: 28 additions & 0 deletions src/hls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export default class Hls implements HlsEventEmitter {

private coreComponents: ComponentAPI[];
private networkControllers: NetworkComponentAPI[];
private started: boolean = false;
private _emitter: HlsEventEmitter = new EventEmitter();
private _autoLevelCapping: number;
private _maxHdcpLevel: HdcpLevel = null;
Expand Down Expand Up @@ -398,6 +399,7 @@ export default class Hls implements HlsEventEmitter {
*/
startLoad(startPosition: number = -1) {
logger.log(`startLoad(${startPosition})`);
this.started = true;
this.networkControllers.forEach((controller) => {
controller.startLoad(startPosition);
});
Expand All @@ -408,11 +410,37 @@ export default class Hls implements HlsEventEmitter {
*/
stopLoad() {
logger.log('stopLoad');
this.started = false;
this.networkControllers.forEach((controller) => {
controller.stopLoad();
});
}

/**
* Resumes stream controller segment loading if previously started.
*/
resumeBuffering() {
if (this.started) {
this.networkControllers.forEach((controller) => {
if ('fragmentLoader' in controller) {
controller.startLoad(-1);
}
});
}
}

/**
* Stops stream controller segment loading without changing 'started' state like stopLoad().
* This allows for media buffering to be paused without interupting playlist loading.
*/
pauseBuffering() {
this.networkControllers.forEach((controller) => {
if ('fragmentLoader' in controller) {
controller.stopLoad();
}
});
}

/**
* Swap through possible audio codecs in the stream (for example to switch from stereo to 5.1)
*/
Expand Down
1 change: 1 addition & 0 deletions src/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface MediaAttachingData {

export interface MediaAttachedData {
media: HTMLMediaElement;
mediaSource?: MediaSource;
}

export interface BufferCodecsData {
Expand Down
3 changes: 1 addition & 2 deletions src/utils/codecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,6 @@ const sampleEntryCodesISO = {
},
};

const MediaSource = getMediaSource();

export type CodecType = 'audio' | 'video';

export function isCodecType(codec: string, type: CodecType): boolean {
Expand All @@ -95,6 +93,7 @@ export function areCodecsMediaSourceSupported(
}

function isCodecMediaSourceSupported(codec: string, type: CodecType): boolean {
const MediaSource = getMediaSource();
return (
MediaSource?.isTypeSupported(`${type || 'video'}/mp4;codecs="${codec}"`) ??
false
Expand Down
6 changes: 5 additions & 1 deletion src/utils/mediasource-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@

export function getMediaSource(): typeof MediaSource | undefined {
if (typeof self === 'undefined') return undefined;
return self.MediaSource || ((self as any).WebKitMediaSource as MediaSource);
return (
((self as any).ManagedMediaSource as typeof MediaSource) ||
self.MediaSource ||
((self as any).WebKitMediaSource as typeof MediaSource)
);
}
3 changes: 1 addition & 2 deletions tests/unit/controller/content-steering-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ import { getMediaSource } from '../../../src/utils/mediasource-helper';
chai.use(sinonChai);
const expect = chai.expect;

const MediaSource = getMediaSource();

type ConentSteeringControllerTestable = Omit<
ContentSteeringController,
| 'enabled'
Expand Down Expand Up @@ -67,6 +65,7 @@ describe('ContentSteeringController', function () {
let contentSteeringController: ConentSteeringControllerTestable;

beforeEach(function () {
const MediaSource = getMediaSource();
hls = new HlsMock({
loader: MockXhr,
});
Expand Down
3 changes: 1 addition & 2 deletions tests/unit/controller/level-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ import { getMediaSource } from '../../../src/utils/mediasource-helper';
chai.use(sinonChai);
const expect = chai.expect;

const MediaSource = getMediaSource();

type LevelControllerTestable = Omit<LevelController, 'onManifestLoaded'> & {
onManifestLoaded: (event: string, data: Partial<ManifestLoadedData>) => void;
onAudioTrackSwitched: (event: string, data: { id: number }) => void;
Expand Down Expand Up @@ -86,6 +84,7 @@ describe('LevelController', function () {
let levelController: LevelControllerTestable;

beforeEach(function () {
const MediaSource = getMediaSource();
hls = new HlsMock({});
levelController = new LevelController(
hls as any,
Expand Down

0 comments on commit 403c49f

Please sign in to comment.