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

Feat: add an option to play without audio or video if audio or video failed #1624

Open
wants to merge 15 commits into
base: dev
Choose a base branch
from

Conversation

Florent-Bouisset
Copy link
Collaborator

Currently, the player throws an error if either all audio or all video tracks are not playable. This PR introduces two new options, onAudioTrackNotPlayable and onVideoTrackNotPlayable, to the loadVideo method. These options allow the player to continue playback even when all audio or video tracks are not playable. In such cases, the content will play without audio or video, respectively, instead of failing.

API

loadVideo(options) options object has two more keys:

  • [optional]onAudioTrackNotPlayable : "continue" or "error". Default if not set: "continue"
  • [optional]onVideoTrackNotPlayable : "continue" or "error". Default if not set: "continue"

Usage

  player.loadVideo({
    transport: "dash",
    url: "https://dash.akamaized.net/dash264/TestCasesDolby/1/Living_Room_1080p_20_96k_25fps.mpd",
    autoPlay: true,
    onAudioTrackNotPlayable: "continue",
    onVideoTrackNotPlayable: "error",
  });

Example

https://codesandbox.io/p/sandbox/amazing-pare-kdf65f

@Florent-Bouisset Florent-Bouisset added the API Relative to the RxPlayer's API label Jan 6, 2025
@Florent-Bouisset Florent-Bouisset force-pushed the feat/play-without-audio-or-video branch from 724b4a3 to 5a073f9 Compare January 6, 2025 09:30
@Florent-Bouisset
Copy link
Collaborator Author

relates to #1602

@peaBerberian
Copy link
Collaborator

onAudioTrackNotPlayable and onVideoTrackNotPlayable

Shouldn't it be "tracks" here?

Default if not set: "continue"

Doesn't it break the API this way? Its complex but it changes the default behavior on which some applications may rely? Wouldn't it be safer to default it to "error"?


_defaults_: `"continue"`

Specifies the behavior when all audio tracks are not playable.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could clarify by giving some example cases below

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What kind of example could be relevant here? Doesn't the description of the option provides clear indication on what the player behavior is?

  • "continue": The player will proceed to play the content without audio.

  • "error": The player will throw an error to indicate that the audio tracks could not be
    played.

Or maybe you want me to add an example that describes how all tracks can be unplayable (codec not supported, license not containing the keys for audio/video, ...)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe you want me to add an example that describes how all tracks can be unplayable (codec not supported, license not containing the keys for audio/video, ...)

Yes, this

@Florent-Bouisset
Copy link
Collaborator Author

onAudioTrackNotPlayable

Yes I think I should default it to "error" instead.

@Florent-Bouisset
Copy link
Collaborator Author

onAudioTrackNotPlayable and onVideoTrackNotPlayable

Shouldn't it be "tracks" here?

Default if not set: "continue"

Doesn't it break the API this way? Its complex but it changes the default behavior on which some applications may rely? Wouldn't it be safer to default it to "error"?

I have updated it.

@Florent-Bouisset Florent-Bouisset force-pushed the feat/play-without-audio-or-video branch from ef25c83 to 4b6acd7 Compare January 9, 2025 10:39
return representations[0].getMimeTypeString();
} else if (adaptation.representations.length > 0) {
return adaptation.representations[0].getMimeTypeString();
} else {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be sure, this one else should never be able to be entered no?

@@ -1469,6 +1486,25 @@ function toExposedPeriod(p: IPeriodMetadata): IPeriod {
return { start: p.start, end: p.end, id: p.id };
}

function findNextPlayableAdaptation(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From that function's point of view, I'm under the impression that this is not really the "next" playable Adaptation, just the first playable adaptation, no?

nextAdaptation === undefined &&
bufferType === "audio" &&
this.onAudioTracksNotPlayable === "continue" &&
findNextPlayableAdaptation(period, "video") !== undefined
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we doing this call? We just have to check that the current video track is defined no?

Also what happens if we were in audio-only mode here?

nextAdaptation === undefined &&
bufferType === "video" &&
this.onVideoTracksNotPlayable === "continue" &&
findNextPlayableAdaptation(period, "audio") !== undefined
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, we should just check that there's currently an audio track no?

@peaBerberian
Copy link
Collaborator

peaBerberian commented Jan 9, 2025

As this adds a similar logic in the TrackStore + Manifest + Init, and as the manifest is more or less intended to just regroup metadata, I'm wondering if it would not be easier to follow if that option was mainly handled by a part of the Init (and TrackStore I guess for its own thing) : Like the Manifest would parse the content regardless, and the Init would investigate the issues seen while parsing and decide whether to error or warn and continue based on its options.

What do you think?

Copy link

Automated performance checks have been performed on commit 168ac23a2c6e5dc5bae395c9d92809c9a52ed54c with the base branch dev.

Tests results

✅ Tests have passed.

Performance tests 1st run output

No significative change in performance for tests:

Name Mean Median
loading 19.31ms -> 19.52ms (-0.204ms, z: 0.26401) 26.55ms -> 26.70ms
seeking 14.01ms -> 11.30ms (2.710ms, z: 0.18678) 11.25ms -> 11.25ms
audio-track-reload 25.71ms -> 25.73ms (-0.022ms, z: 0.00379) 37.65ms -> 37.65ms
cold loading multithread 47.55ms -> 46.71ms (0.837ms, z: 10.77750) 69.60ms -> 68.40ms
seeking multithread 15.95ms -> 11.95ms (3.997ms, z: 0.80564) 10.50ms -> 10.50ms
audio-track-reload multithread 25.74ms -> 25.71ms (0.024ms, z: 1.49764) 37.95ms -> 37.80ms
hot loading multithread 15.04ms -> 15.02ms (0.013ms, z: 2.03549) 21.90ms -> 21.75ms

If you want to skip performance checks for latter commits, add the skip-performance-checks label to this Pull Request.

Copy link

Automated performance checks have been performed on commit 5eb15f4f29a23ec1b8774929f8e37d8b0c51b6d2 with the base branch dev.

Tests results

✅ Tests have passed.

Performance tests 1st run output

No significative change in performance for tests:

Name Mean Median
loading 19.42ms -> 19.50ms (-0.075ms, z: 1.47093) 26.85ms -> 26.70ms
seeking 16.68ms -> 17.28ms (-0.603ms, z: 0.25378) 11.25ms -> 11.25ms
audio-track-reload 25.67ms -> 25.76ms (-0.088ms, z: 0.08821) 37.65ms -> 37.65ms
cold loading multithread 47.63ms -> 46.98ms (0.658ms, z: 10.77232) 69.90ms -> 68.70ms
seeking multithread 15.95ms -> 19.94ms (-3.991ms, z: 1.73102) 10.35ms -> 10.35ms
audio-track-reload multithread 25.92ms -> 25.54ms (0.375ms, z: 2.54631) 37.80ms -> 37.65ms
hot loading multithread 15.08ms -> 14.95ms (0.130ms, z: 3.13205) 21.90ms -> 21.60ms

If you want to skip performance checks for latter commits, add the skip-performance-checks label to this Pull Request.

@Florent-Bouisset Florent-Bouisset force-pushed the feat/play-without-audio-or-video branch from 5eb15f4 to 5e29ebe Compare January 30, 2025 13:52
@Florent-Bouisset Florent-Bouisset force-pushed the feat/play-without-audio-or-video branch from 5e29ebe to dc7bfe7 Compare January 31, 2025 09:37
Copy link

Automated performance checks have been performed on commit dc7bfe735dfd50886b0db46aeb0e6a67ad1ac647 with the base branch dev.

Tests results

✅ Tests have passed.

Performance tests 1st run output

No significative change in performance for tests:

Name Mean Median
loading 19.29ms -> 19.51ms (-0.214ms, z: 3.58239) 26.40ms -> 26.85ms
seeking 14.60ms -> 14.67ms (-0.061ms, z: 2.14748) 11.25ms -> 11.40ms
audio-track-reload 25.86ms -> 25.84ms (0.027ms, z: 0.81078) 37.80ms -> 37.80ms
cold loading multithread 47.66ms -> 46.89ms (0.764ms, z: 9.64433) 69.75ms -> 68.55ms
seeking multithread 17.31ms -> 18.63ms (-1.324ms, z: 0.16955) 10.35ms -> 10.35ms
audio-track-reload multithread 25.87ms -> 25.75ms (0.118ms, z: 1.94454) 38.10ms -> 37.80ms
hot loading multithread 15.17ms -> 15.06ms (0.111ms, z: 1.80062) 22.05ms -> 21.90ms

If you want to skip performance checks for latter commits, add the skip-performance-checks label to this Pull Request.


_payload type_: `Object`

Emitted when no tracks of a particular type can be selected for a period.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to indicate that this happen when the corresponding options are set, and in which condition it arises

Else a developer won't understand what this event is for and whether it should handle it

this._priv_onFatalError(err, contentInfos);
});

contentInfos.tracksStore.onManifestUpdate(manifest);
tracksStore.addEventListener("noPlayableTrack", (trackType) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The payload name is not right

@@ -135,6 +98,30 @@ export default class Period implements IPeriodMetadata {
this.streamEvents = args.streamEvents === undefined ? [] : args.streamEvents;
}

createAdaptationsObject(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the intention to create a private method that is only called at the start of the Period's instantiation?

If it is, I have some issues:

  • it's not marked as a private method
  • I tend to think that it is a bad idea to call a method before the instantiation was succesfully done because we're in a partial state in which even TypeScript might not see that the current instance is not fully constructed / initialized. It's a difficult state to think about.

As we're not even relying on the Period's context it seems here, can't we just make that a regular function?

}, {});
// this.checkIfStreamIsSupported(hasSupportedMedia);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gné?

@@ -158,6 +171,49 @@ export default class TracksStore extends EventEmitter<ITracksStoreEvents> {
/** Periods which have just been added. */
const addedPeriods: ITSPeriodObject[] = [];

for (const period of periods) {
["audio" as const, "video" as const].forEach((ttype: ITrackType) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could just isolate a good chunk of it to its own function

if (
firstPlayableAdaptation === undefined &&
ttype === "audio" &&
this.onAudioTracksNotPlayable === "continue"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might have been easier to follow as something like this.onTracksNotPlayable[ttype] === continue? (suggestion)

// Video is not playable but audio may be playable, let's continue the playback.
this.trigger("warning", err);
} else if (firstPlayableAdaptation !== undefined) {
this.trigger("warning", err);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot make TypeScript understand that this does not happen?

} else if (firstPlayableAdaptation !== undefined) {
this.trigger("warning", err);
} else {
this.trigger("error", err);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we also just abort the trackstore here? That's what seem to be done for NO_PLAYABLE_REPRESENTATION

adaptation,
switchingMode: DEFAULT_VIDEO_TRACK_SWITCHING_MODE,
lockedRepresentations,
} as IVideoStoredSettings;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the as needed here?

* @param type
* @returns
*/
private resetSelectedTrackIfNotAvailableAnymore(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

@@ -481,17 +498,119 @@ export default class TracksStore extends EventEmitter<ITracksStoreEvents> {
if (
periodObj.isPeriodAdvertised &&
trackObj.dispatcher !== null &&
!trackObj.dispatcher.hasSetTrack() &&
trackObj.storedSettings !== undefined
Copy link
Collaborator

@peaBerberian peaBerberian Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait why are we removing this? I guess (though I don't remember why) it was placed on purpose as a guard and it has different semantics than null

) {
trackObj.dispatcher.updateTrack(trackObj.storedSettings);
trackObj.dispatcher.updateTrack(trackObj.storedSettings ?? null);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I get it here.

To me, if you don't want a type of media, storedSettings should always be set explicitely to null. If storedSettings is ever undefined, it should always mean that we don't know yet

noPlayableTrack: INoPlayableTrack;
}

interface INoPlayableTrack {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With an EventPayload suffix maybe?

period.adaptations[bufferType].length > 0;
const firstPlayableAdaptation = findFirstPlayableAdaptation(period, bufferType);

if (!periodHasAdaptationForType) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird case we would be in, an "onNoPlayableRepresentation" call for a buffer type we don't have

}

if (bufferType === "text") {
return;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why

) {
// 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("noPlayableTrack", {
Copy link
Collaborator

@peaBerberian peaBerberian Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/!\ this event might call the application's code synchronously and completely change the tracksStore's state:

  • it might be disposed
  • the track choices might have completely changed

I signal this because I've seen no guard against those possibilities, we just assume we're in the same state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
API Relative to the RxPlayer's API
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants