Skip to content

Commit

Permalink
Add MEDIA_ENDED event (forwards "ended" event, or emits when stalling…
Browse files Browse the repository at this point in the history
… begins near end of VOD)
  • Loading branch information
robwalch committed Jan 26, 2024
1 parent 6b2bb87 commit 7e2ad53
Show file tree
Hide file tree
Showing 9 changed files with 70 additions and 13 deletions.
12 changes: 12 additions & 0 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,8 @@ export enum Events {
// (undocumented)
MEDIA_DETACHING = "hlsMediaDetaching",
// (undocumented)
MEDIA_ENDED = "hlsMediaEnded",
// (undocumented)
NON_NATIVE_TEXT_TRACKS_FOUND = "hlsNonNativeTextTracksFound",
// (undocumented)
STEERING_MANIFEST_LOADED = "hlsSteeringManifestLoaded",
Expand Down Expand Up @@ -1836,6 +1838,8 @@ export interface HlsListeners {
// (undocumented)
[Events.MEDIA_DETACHING]: (event: Events.MEDIA_DETACHING) => void;
// (undocumented)
[Events.MEDIA_ENDED]: (event: Events.MEDIA_ENDED, data: MediaEndedData) => void;
// (undocumented)
[Events.NON_NATIVE_TEXT_TRACKS_FOUND]: (event: Events.NON_NATIVE_TEXT_TRACKS_FOUND, data: NonNativeTextTracksData) => void;
// (undocumented)
[Events.STEERING_MANIFEST_LOADED]: (event: Events.STEERING_MANIFEST_LOADED, data: SteeringManifestLoadedData) => void;
Expand Down Expand Up @@ -2800,6 +2804,14 @@ export type MediaDecodingInfo = {
error?: Error;
};

// Warning: (ae-missing-release-tag) "MediaEndedData" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface MediaEndedData {
// (undocumented)
stalled: boolean;
}

// Warning: (ae-missing-release-tag) "MediaKeyFunc" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
Expand Down
5 changes: 5 additions & 0 deletions src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,11 @@ export default class BaseStreamController
protected onMediaEnded = () => {
// reset startPosition and lastCurrentTime to restart playback @ stream beginning
this.startPosition = this.lastCurrentTime = 0;
if (this.playlistType === PlaylistLevelType.MAIN) {
this.hls.trigger(Events.MEDIA_ENDED, {
stalled: false,
});
}
};

protected onManifestLoaded(
Expand Down
33 changes: 27 additions & 6 deletions src/controller/gap-controller.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { State } from './base-stream-controller';
import { BufferHelper } from '../utils/buffer-helper';
import { ErrorTypes, ErrorDetails } from '../errors';
import { PlaylistLevelType } from '../types/loader';
Expand All @@ -8,6 +9,7 @@ import type { BufferInfo } from '../utils/buffer-helper';
import type { HlsConfig } from '../config';
import type { Fragment } from '../loader/fragment';
import type { FragmentTracker } from './fragment-tracker';
import type { LevelDetails } from '../loader/level-details';

export const STALL_MINIMUM_DURATION_MS = 250;
export const MAX_START_GAP_JUMP = 2.0;
Expand All @@ -24,6 +26,7 @@ export default class GapController extends Logger {
private stalled: number | null = null;
private moved: boolean = false;
private seeking: boolean = false;
private ended: number = 0;

constructor(
config: HlsConfig,
Expand All @@ -50,7 +53,12 @@ export default class GapController extends Logger {
*
* @param lastCurrentTime - Previously read playhead position
*/
public poll(lastCurrentTime: number, activeFrag: Fragment | null) {
public poll(
lastCurrentTime: number,
activeFrag: Fragment | null,
levelDetails: LevelDetails | undefined,
state: string,
) {
const { config, media, stalled } = this;
if (media === null) {
return;
Expand All @@ -63,6 +71,7 @@ export default class GapController extends Logger {

// The playhead is moving, no-op
if (currentTime !== lastCurrentTime) {
this.ended = 0;
this.moved = true;
if (!seeking) {
this.nudgeRetry = 0;
Expand Down Expand Up @@ -134,12 +143,9 @@ export default class GapController extends Logger {
// When joining a live stream with audio tracks, account for live playlist window sliding by allowing
// a larger jump over start gaps caused by the audio-stream-controller buffering a start fragment
// that begins over 1 target duration after the video start position.
const level = this.hls.levels
? this.hls.levels[this.hls.currentLevel]
: null;
const isLive = level?.details?.live;
const isLive = !!levelDetails?.live;
const maxStartGapJump = isLive
? level!.details!.targetduration * 2
? levelDetails!.targetduration * 2
: MAX_START_GAP_JUMP;
const partialOrGap = this.fragmentTracker.getPartialFragment(currentTime);
if (startJump > 0 && (startJump <= maxStartGapJump || partialOrGap)) {
Expand All @@ -159,6 +165,21 @@ export default class GapController extends Logger {

const stalledDuration = tnow - stalled;
if (!seeking && stalledDuration >= STALL_MINIMUM_DURATION_MS) {
// Dispatch MEDIA_ENDED when media.ended/ended event is not signalled at end of stream
if (
state === State.ENDED &&
!(levelDetails && levelDetails.live) &&
Math.abs(currentTime - (levelDetails?.edge || 0)) < 1
) {
if (stalledDuration < 1000 || this.ended) {
return;
}
this.ended = currentTime;
this.hls.trigger(Events.MEDIA_ENDED, {
stalled: true,
});
return;
}
// Report stalling after trying to fix
this._reportStall(bufferInfo);
if (!this.media) {
Expand Down
1 change: 0 additions & 1 deletion src/controller/level-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
codecsSetSelectionPreferenceValue,
convertAVC1ToAVCOTI,
getCodecCompatibleName,
getM2TSSupportedAudioTypes,
videoCodecPreferenceValue,
} from '../utils/codecs';
import BasePlaylistController from './base-playlist-controller';
Expand Down
6 changes: 4 additions & 2 deletions src/controller/stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -932,8 +932,10 @@ export default class StreamController

if (this.loadedmetadata || !BufferHelper.getBuffered(media).length) {
// Resolve gaps using the main buffer, whose ranges are the intersections of the A/V sourcebuffers
const activeFrag = this.state !== State.IDLE ? this.fragCurrent : null;
gapController.poll(this.lastCurrentTime, activeFrag);
const state = this.state;
const activeFrag = state !== State.IDLE ? this.fragCurrent : null;
const levelDetails = this.getLevelDetails();
gapController.poll(this.lastCurrentTime, activeFrag, levelDetails, state);
}

this.lastCurrentTime = media.currentTime;
Expand Down
7 changes: 7 additions & 0 deletions src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ManifestLoadingData,
MediaAttachedData,
MediaAttachingData,
MediaEndedData,
LevelLoadingData,
LevelLoadedData,
ManifestParsedData,
Expand Down Expand Up @@ -60,6 +61,8 @@ export enum Events {
MEDIA_DETACHING = 'hlsMediaDetaching',
// Fired when MediaSource has been detached from media element
MEDIA_DETACHED = 'hlsMediaDetached',
// Fired when HTMLMediaElement dispatches "ended" event, or stalls at end of VOD program
MEDIA_ENDED = 'hlsMediaEnded',
// Fired when the buffer is going to be reset
BUFFER_RESET = 'hlsBufferReset',
// Fired when we know about the codecs that we need buffers for to push into - data: {tracks : { container, codec, levelCodec, initSegment, metadata }}
Expand Down Expand Up @@ -184,6 +187,10 @@ export interface HlsListeners {
) => void;
[Events.MEDIA_DETACHING]: (event: Events.MEDIA_DETACHING) => void;
[Events.MEDIA_DETACHED]: (event: Events.MEDIA_DETACHED) => void;
[Events.MEDIA_ENDED]: (
event: Events.MEDIA_ENDED,
data: MediaEndedData,
) => void;
[Events.BUFFER_RESET]: (event: Events.BUFFER_RESET) => void;
[Events.BUFFER_CODECS]: (
event: Events.BUFFER_CODECS,
Expand Down
1 change: 1 addition & 0 deletions src/hls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1159,6 +1159,7 @@ export type {
ManifestParsedData,
MediaAttachedData,
MediaAttachingData,
MediaEndedData,
NonNativeTextTrack,
NonNativeTextTracksData,
SteeringManifestLoadedData,
Expand Down
4 changes: 4 additions & 0 deletions src/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ export interface MediaAttachedData {
mediaSource?: MediaSource;
}

export interface MediaEndedData {
stalled: boolean;
}

export interface BufferCodecsData {
video?: Track;
audio?: Track;
Expand Down
14 changes: 10 additions & 4 deletions tests/functional/auto/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -330,10 +330,16 @@ async function testSeekOnVOD(url, config) {
});
}
};
video.onended = function () {
console.log('[test] > video "ended"');
callback({ code: 'ended', logs: self.logString });
};
self.hls.on(self.Hls.Events.MEDIA_ENDED, function (eventName, data) {
console.log(
'[test] > video "ended"' + data.stalled ? ' (stalled near end)' : ''
);
callback({
code: 'ended',
stalled: data.stalled,
logs: self.logString,
});
});

video.oncanplaythrough = video.onwaiting = function (e) {
console.log(
Expand Down

0 comments on commit 7e2ad53

Please sign in to comment.