Skip to content

Commit

Permalink
fix: fix preferred track selection on Safari (#5601)
Browse files Browse the repository at this point in the history
Preferred track selection was implemented differently for MSE and native
playback. In fact, native playback even had different implementations
for audio and text. It leads to inconsistencies during track selection,
i.e. if track language contains locale, but language preference not, on
MSE we're looking for closest locale and on src= we're making direct
string comparison which leads to different results.
To unify that, both MSE and native will now use
`StreamUtils.filterStreamsByLanguageAndRole()` to find matching track.
This method is designed to use on `shaka.extern.Stream` but luckily it
uses the very same fields as defined in `shaka.extern.Track` so we can
use it without major changes.
Moreover, using this more robust method also allows us to get rid of
double-selection workaround used so far. Observe that we were first
selecting track without preferred role and then with preferred role.

Backported to v4.3.x
  • Loading branch information
tykus160 authored and joeyparrish committed Sep 5, 2023
1 parent 8017636 commit a85174a
Show file tree
Hide file tree
Showing 2 changed files with 20 additions and 67 deletions.
63 changes: 7 additions & 56 deletions lib/player.js
Expand Up @@ -2651,16 +2651,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
return;
}

this.selectAudioLanguage(preferredAudioLanguage);

const preferredVariantRole = this.config_.preferredVariantRole;

// If the user has not selected a role preference, the previous match is
// selected.
if (preferredVariantRole == '') {
return;
}

this.selectAudioLanguage(preferredAudioLanguage, preferredVariantRole);
}

Expand All @@ -2671,24 +2662,16 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
*/
setupPreferredTextOnSrc_() {
const preferredTextLanguage = this.config_.preferredTextLanguage;
const preferForcedSubs = this.config_.preferForcedSubs;

// If the user has not selected a preference, the browser preference is
// left.
if (preferredTextLanguage == '') {
return;
}

this.selectTextLanguage(preferredTextLanguage, '', preferForcedSubs);

const preferForcedSubs = this.config_.preferForcedSubs;
const preferredTextRole = this.config_.preferredTextRole;

// If the user has not selected a role preference, the previous match is
// selected.
if (preferredTextRole == '') {
return;
}

this.selectTextLanguage(preferredTextLanguage, preferredTextRole,
preferForcedSubs);
}
Expand Down Expand Up @@ -4141,8 +4124,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
* @export
*/
selectAudioLanguage(language, role, channelsCount = 0) {
const LanguageUtils = shaka.util.LanguageUtils;

if (this.manifest_ && this.playhead_) {
this.currentAdaptationSetCriteria_ =
new shaka.media.PreferenceBasedCriteria(language, role || '',
Expand Down Expand Up @@ -4180,33 +4161,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
// If we haven't switched yet, just use ABR to find a new track.
this.chooseVariantAndSwitch_();
} else if (this.video_ && this.video_.audioTracks) {
const audioTracks = Array.from(this.video_.audioTracks);
const selectedLanguage = LanguageUtils.normalize(language);

let languageMatch = null;
let languageAndRoleMatch = null;

for (const audioTrack of audioTracks) {
const track = shaka.util.StreamUtils.html5AudioTrackToTrack(audioTrack);

if (LanguageUtils.normalize(track.language) == selectedLanguage) {
languageMatch = audioTrack;

if (role) {
if (track.roles.includes(role)) {
languageAndRoleMatch = audioTrack;
}
} else { // no role
if (track.roles.length == 0) {
languageAndRoleMatch = audioTrack;
}
}
}
}
if (languageAndRoleMatch) {
this.switchHtml5Track_(languageAndRoleMatch);
} else if (languageMatch) {
this.switchHtml5Track_(languageMatch);
const track = shaka.util.StreamUtils.filterStreamsByLanguageAndRole(
this.getVariantTracks(), language, role || '', false)[0];
if (track) {
this.selectVariantTrack(track);
}
}
}
Expand All @@ -4222,8 +4180,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
* @export
*/
selectTextLanguage(language, role, forced = false) {
const LanguageUtils = shaka.util.LanguageUtils;

if (this.manifest_ && this.playhead_) {
this.currentTextLanguage_ = language;
this.currentTextRole_ = role || '';
Expand All @@ -4244,13 +4200,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
}
}
} else {
const selectedLanguage = LanguageUtils.normalize(language);

const track = this.getTextTracks().find((t) => {
return LanguageUtils.normalize(t.language) == selectedLanguage &&
(!role || t.roles.includes(role)) && t.forced == forced;
});

const track = shaka.util.StreamUtils.filterStreamsByLanguageAndRole(
this.getTextTracks(), language, role || '', forced)[0];
if (track) {
this.selectTextTrack(track);
}
Expand Down
24 changes: 13 additions & 11 deletions lib/util/stream_utils.js
Expand Up @@ -1442,22 +1442,23 @@ shaka.util.StreamUtils = class {

/**
* Chooses streams according to the given config.
* Works both for Stream and Track types due to their similarities.
*
* @param {!Array.<shaka.extern.Stream>} streams
* @param {!Array<!shaka.extern.Stream>|!Array<!shaka.extern.Track>} streams
* @param {string} preferredLanguage
* @param {string} preferredRole
* @param {boolean} preferredForced
* @return {!Array.<!shaka.extern.Stream>}
* @return {!Array<!shaka.extern.Stream>|!Array<!shaka.extern.Track>}
*/
static filterStreamsByLanguageAndRole(
streams, preferredLanguage, preferredRole, preferredForced) {
const LanguageUtils = shaka.util.LanguageUtils;

/** @type {!Array.<!shaka.extern.Stream>} */
/** @type {!Array<!shaka.extern.Stream>|!Array<!shaka.extern.Track>} */
let chosen = streams;

// Start with the set of primary streams.
/** @type {!Array.<!shaka.extern.Stream>} */
/** @type {!Array<!shaka.extern.Stream>|!Array<!shaka.extern.Track>} */
const primary = streams.filter((stream) => {
return stream.primary;
});
Expand Down Expand Up @@ -1497,7 +1498,7 @@ shaka.util.StreamUtils = class {

// Now refine the choice based on role preference.
if (preferredRole) {
const roleMatches = shaka.util.StreamUtils.filterTextStreamsByRole_(
const roleMatches = shaka.util.StreamUtils.filterStreamsByRole_(
chosen, preferredRole);
if (roleMatches.length) {
return roleMatches;
Expand Down Expand Up @@ -1525,20 +1526,21 @@ shaka.util.StreamUtils = class {
if (!allRoles.length) {
return chosen;
}
return shaka.util.StreamUtils.filterTextStreamsByRole_(chosen, allRoles[0]);
return shaka.util.StreamUtils.filterStreamsByRole_(chosen, allRoles[0]);
}


/**
* Filter text Streams by role.
* Filter Streams by role.
* Works both for Stream and Track types due to their similarities.
*
* @param {!Array.<shaka.extern.Stream>} textStreams
* @param {!Array<!shaka.extern.Stream>|!Array<!shaka.extern.Track>} streams
* @param {string} preferredRole
* @return {!Array.<shaka.extern.Stream>}
* @return {!Array<!shaka.extern.Stream>|!Array<!shaka.extern.Track>}
* @private
*/
static filterTextStreamsByRole_(textStreams, preferredRole) {
return textStreams.filter((stream) => {
static filterStreamsByRole_(streams, preferredRole) {
return streams.filter((stream) => {
return stream.roles.includes(preferredRole);
});
}
Expand Down

0 comments on commit a85174a

Please sign in to comment.