diff --git a/doc/api/Player_Events.md b/doc/api/Player_Events.md index 3989caa644..83678e4f4a 100644 --- a/doc/api/Player_Events.md +++ b/doc/api/Player_Events.md @@ -536,6 +536,24 @@ video track when in directfile mode to avoid that case (this is documented in the corresponding APIs). +### noPlayableTrack + +_payload type_: `Object` + +Emitted when no tracks of a particular type can be selected for a period. + +The payload is an object with the following properties: + +- `trackType` (`"audio" | "video" | "text"`): The track type that appear to have no playable track. + +- `period`: (`Object`): The period in which the track is not playable. The Object has the following properties: + + - `id`: (`"string"`): The period id. + + - `start`: (`"number"`): The period start time. + + - `end`: (`"number" | undefined`): The period end time. + ## Representation selection events This chapter describes events linked to the current audio, video or Representation / diff --git a/src/main_thread/api/public_api.ts b/src/main_thread/api/public_api.ts index 5bb0cb5a79..8c7c8692a6 100644 --- a/src/main_thread/api/public_api.ts +++ b/src/main_thread/api/public_api.ts @@ -101,6 +101,7 @@ import type { ITrackType, IModeInformation, IWorkerSettings, + INoPlayableTrackEventPayload, } from "../../public_types"; import arrayFind from "../../utils/array_find"; import arrayIncludes from "../../utils/array_includes"; @@ -123,7 +124,11 @@ import { getKeySystemConfiguration, } from "../decrypt"; import type { ContentInitializer } from "../init"; -import type { IMediaElementTracksStore, ITSPeriodObject } from "../tracks_store"; +import type { + IMediaElementTracksStore, + INoPlayableTrack, + ITSPeriodObject, +} from "../tracks_store"; import TracksStore from "../tracks_store"; import type { IParsedLoadVideoOptions, IParsedStartAtOption } from "./option_utils"; import { @@ -2602,14 +2607,18 @@ class Player extends EventEmitter { this._priv_onAvailableTracksMayHaveChanged(e.trackType); } }); - contentInfos.tracksStore.addEventListener("warning", (err) => { + tracksStore.addEventListener("warning", (err) => { this.trigger("warning", err); }); - contentInfos.tracksStore.addEventListener("error", (err) => { + tracksStore.addEventListener("error", (err) => { this._priv_onFatalError(err, contentInfos); }); - contentInfos.tracksStore.onManifestUpdate(manifest); + tracksStore.addEventListener("noPlayableTrack", (trackType) => { + this.trigger("noPlayableTrack", trackType); + }); + + tracksStore.onManifestUpdate(manifest); } /** @@ -3368,6 +3377,7 @@ interface IPublicAPIEvent { streamEvent: IStreamEvent; streamEventSkip: IStreamEvent; inbandEvents: IInbandEvent[]; + noPlayableTrack: INoPlayableTrackEventPayload; } /** State linked to a particular contents loaded by the public API. */ diff --git a/src/main_thread/tracks_store/tracks_store.ts b/src/main_thread/tracks_store/tracks_store.ts index b8221bfc30..6e8a4499ac 100644 --- a/src/main_thread/tracks_store/tracks_store.ts +++ b/src/main_thread/tracks_store/tracks_store.ts @@ -510,7 +510,7 @@ export default class TracksStore extends EventEmitter { /** * Handle the noPlayableRepresentation event, trigger an error if no fallback is possible. - * and can trigger event "noPlayableTracks" + * and can trigger event "noPlayableTrack" * @param period - The period that has no playable representation * @param bufferType - The media type that is not playable */ @@ -538,7 +538,10 @@ export default class TracksStore extends EventEmitter { ) { // Audio is not playable but video may be playable, let's continue the playback. log.warn(`TS: No playable audio, continuing without audio`); - this.trigger("noPlayableTracks", bufferType); + this.trigger("noPlayableTrack", { + trackType: bufferType, + period: { id: period.id, start: period.start, end: period.end }, + }); } else if ( firstPlayableAdaptation === undefined && bufferType === "video" && @@ -546,14 +549,28 @@ export default class TracksStore extends EventEmitter { ) { // Video is not playable but audio may be playable, let's continue the playback. log.warn(`TS: No playable video, continuing with audio only`); - this.trigger("noPlayableTracks", bufferType); + this.trigger("noPlayableTrack", { + trackType: bufferType, + period: { + id: period.id, + start: period.start, + end: period.end, + }, + }); } else if (firstPlayableAdaptation === undefined) { const noRepErr = new MediaError( "NO_PLAYABLE_REPRESENTATION", `No ${bufferType} Representation can be played`, { tracks: undefined }, ); - this.trigger("noPlayableTracks", bufferType); + this.trigger("noPlayableTrack", { + trackType: bufferType, + period: { + id: period.id, + start: period.start, + end: period.end, + }, + }); this.trigger("error", noRepErr); this.dispose(); return; @@ -1760,6 +1777,16 @@ interface ITracksStoreEvents { trackUpdate: ITrackUpdateEventPayload; error: unknown; warning: IPlayerError; + noPlayableTrack: INoPlayableTrack; +} + +interface INoPlayableTrack { + trackType: ITrackType; + period: { + id: string; + start: number; + end: number | undefined; + }; } export interface IAudioRepresentationsLockSettings { diff --git a/src/public_types.ts b/src/public_types.ts index 8a4aec29a3..0bcdb3f6ce 100644 --- a/src/public_types.ts +++ b/src/public_types.ts @@ -1226,6 +1226,15 @@ export interface ITrackUpdateEventPayload { /* eslint-enable @typescript-eslint/no-redundant-type-constituents */ } +export interface INoPlayableTrackEventPayload { + trackType: ITrackType; + period: { + id: string; + start: number; + end: number | undefined; + }; +} + export interface IRepresentationListUpdateContext { period: IPeriod; trackType: ITrackType;