Skip to content

Commit

Permalink
feat: combine MEDIA_CAPTIONS_LIST into MEDIA_SUBTITLES_LIST (#546)
Browse files Browse the repository at this point in the history
This combines the media captions and subtitles lists for increased ease-of-use. In addition, it removes the "no-subtitles-fallback" attribute.

BREAKING CHANGE: remove MEDIA_CAPTIONS_LIST, MEDIA_CAPTIONS_SHOWING, no-subtitles-fallback, MEDIA_SHOW_CAPTIONS_REQUEST, MEDIA_DISABLE_CAPTIONS_REQUEST, and MEDIA_CAPTIONS_LIST & MEDIA_CAPTIONS_SHOWING change events.
  • Loading branch information
gkatsev authored Apr 20, 2023
1 parent 7efe18a commit 03e62e6
Showing 6 changed files with 67 additions and 179 deletions.
6 changes: 0 additions & 6 deletions src/js/constants.js
Original file line number Diff line number Diff line change
@@ -15,9 +15,7 @@ export const MediaUIEvents = {
MEDIA_EXIT_CAST_REQUEST: 'mediaexitcastrequest',
MEDIA_SHOW_TEXT_TRACKS_REQUEST: 'mediashowtexttracksrequest',
MEDIA_HIDE_TEXT_TRACKS_REQUEST: 'mediahidetexttracksrequest',
MEDIA_SHOW_CAPTIONS_REQUEST: 'mediashowcaptionsrequest',
MEDIA_SHOW_SUBTITLES_REQUEST: 'mediashowsubtitlesrequest',
MEDIA_DISABLE_CAPTIONS_REQUEST: 'mediadisablecaptionsrequest',
MEDIA_DISABLE_SUBTITLES_REQUEST: 'mediadisablesubtitlesrequest',
MEDIA_PLAYBACK_RATE_REQUEST: 'mediaplaybackraterequest',
MEDIA_SEEK_TO_LIVE_REQUEST: 'mediaseektoliverequest',
@@ -36,9 +34,7 @@ export const MediaStateChangeEvents = {
MEDIA_VOLUME_UNAVAILABLE: 'mediavolumeunavailablechange',
MEDIA_IS_PIP: 'mediaispipchange',
MEDIA_IS_CASTING: 'mediaiscastingchange',
MEDIA_CAPTIONS_LIST: 'mediacaptionslistchange',
MEDIA_SUBTITLES_LIST: 'mediasubtitleslistchange',
MEDIA_CAPTIONS_SHOWING: 'mediacaptionsshowingchange',
MEDIA_SUBTITLES_SHOWING: 'mediasubtitlesshowingchange',
MEDIA_IS_FULLSCREEN: 'mediaisfullscreenchange',
MEDIA_PLAYBACK_RATE: 'mediaplaybackratechange',
@@ -69,9 +65,7 @@ export const MediaUIAttributes = {
MEDIA_VOLUME_UNAVAILABLE: 'mediavolumeunavailable',
MEDIA_IS_PIP: 'mediaispip',
MEDIA_IS_CASTING: 'mediaiscasting',
MEDIA_CAPTIONS_LIST: 'mediacaptionslist',
MEDIA_SUBTITLES_LIST: 'mediasubtitleslist',
MEDIA_CAPTIONS_SHOWING: 'mediacaptionsshowing',
MEDIA_SUBTITLES_SHOWING: 'mediasubtitlesshowing',
MEDIA_IS_FULLSCREEN: 'mediaisfullscreen',
MEDIA_PLAYBACK_RATE: 'mediaplaybackrate',
61 changes: 10 additions & 51 deletions src/js/controller.js
Original file line number Diff line number Diff line change
@@ -32,27 +32,17 @@ export const volumeSupportPromise = hasVolumeSupportAsync().then((supported) =>
const StreamTypeValues = Object.values(StreamTypes);

const getSubtitleTracks = (controller) => {
return getTextTracksList(controller.media, { kind: TextTrackKinds.SUBTITLES });
};

const getCaptionTracks = (controller) => {
return getTextTracksList(controller.media, { kind: TextTrackKinds.CAPTIONS });
};

const getShowingSubtitleTracks = (controller) => {
return getTextTracksList(controller.media, {
kind: TextTrackKinds.SUBTITLES,
mode: TextTrackModes.SHOWING,
});
};

const getShowingCaptionTracks = (controller) => {
return getTextTracksList(controller.media, {
kind: TextTrackKinds.CAPTIONS,
mode: TextTrackModes.SHOWING,
});
};
return getTextTracksList(controller.media, (textTrack) => {
return [TextTrackKinds.SUBTITLES, TextTrackKinds.CAPTIONS].includes(textTrack.kind);
}).sort((a, b) => a.kind >= b.kind ? 1 : -1);
};

const getShowingSubtitleTracks = (controller) => {
return getTextTracksList(controller.media, (textTrack) => {
return textTrack.mode === TextTrackModes.SHOWING &&
[TextTrackKinds.SUBTITLES, TextTrackKinds.CAPTIONS].includes(textTrack.kind);
});
};

export const MediaUIStates = {
MEDIA_PAUSED: {
@@ -424,14 +414,6 @@ export const MediaUIStates = {
// Give a little time for the volume support promise to run
mediaEvents: ['loadstart'],
},
MEDIA_CAPTIONS_LIST: {
get: function (controller) {
// TODO: Move to non attr specific values
return stringifyTextTrackList(getCaptionTracks(controller)) || undefined;
},
mediaEvents: ['loadstart'],
trackListEvents: ['addtrack', 'removetrack'],
},
MEDIA_SUBTITLES_LIST: {
get: function (controller) {
// TODO: Move to non attr specific values
@@ -440,16 +422,6 @@ export const MediaUIStates = {
mediaEvents: ['loadstart'],
trackListEvents: ['addtrack', 'removetrack'],
},
MEDIA_CAPTIONS_SHOWING: {
get: function (controller) {
// TODO: Move to non attr specific values
return (
stringifyTextTrackList(getShowingCaptionTracks(controller)) || undefined
);
},
mediaEvents: ['loadstart'],
trackListEvents: ['addtrack', 'removetrack', 'change'],
},
MEDIA_SUBTITLES_SHOWING: {
get: function (controller) {
// TODO: Move to non attr specific values
@@ -712,19 +684,6 @@ export const MediaUIRequestHandlers = {
previewCoordsStr.split(',').join(' ')
);
},
MEDIA_SHOW_CAPTIONS_REQUEST: (media, e, controller) => {
const tracks = getCaptionTracks(controller);
const { detail: tracksToUpdate = [] } = e;
updateTracksModeTo(TextTrackModes.SHOWING, tracks, tracksToUpdate);
},
// NOTE: We're currently recommending and providing default components that will "disable" tracks when
// we don't want them shown (rather than "hiding" them).
// For a discussion why, see: https://github.com/muxinc/media-chrome/issues/60
MEDIA_DISABLE_CAPTIONS_REQUEST: (media, e, controller) => {
const tracks = getCaptionTracks(controller);
const { detail: tracksToUpdate = [] } = e;
updateTracksModeTo(TextTrackModes.DISABLED, tracks, tracksToUpdate);
},
MEDIA_SHOW_SUBTITLES_REQUEST: (media, e, controller) => {
const tracks = getSubtitleTracks(controller);
const { detail: tracksToUpdate = [] } = e;
33 changes: 8 additions & 25 deletions src/js/experimental/media-captions-listbox.js
Original file line number Diff line number Diff line change
@@ -34,7 +34,6 @@ const compareTracks = (a, b) => {

class MediaCaptionsListbox extends MediaChromeListbox {
#subs = [];
#caps = [];
#offOption;
/** @type {Element} */
#captionsIndicator;
@@ -43,8 +42,6 @@ class MediaCaptionsListbox extends MediaChromeListbox {
return [
...super.observedAttributes,
'aria-multiselectable',
MediaUIAttributes.MEDIA_CAPTIONS_LIST,
MediaUIAttributes.MEDIA_CAPTIONS_SHOWING,
MediaUIAttributes.MEDIA_SUBTITLES_LIST,
MediaUIAttributes.MEDIA_SUBTITLES_SHOWING,
];
@@ -95,12 +92,6 @@ class MediaCaptionsListbox extends MediaChromeListbox {

this.#render();

} else if (attrName === MediaUIAttributes.MEDIA_CAPTIONS_LIST && oldValue !== newValue) {

this.#caps = this.#perTypeUpdate(newValue, this.#caps);

this.#render();

} else if (attrName === MediaUIAttributes.MEDIA_SUBTITLES_SHOWING && oldValue !== newValue) {
const selectedTrack = parseTextTracksStr(newValue ?? '')[0];

@@ -109,14 +100,6 @@ class MediaCaptionsListbox extends MediaChromeListbox {
});
this.#render();

} else if (attrName === MediaUIAttributes.MEDIA_CAPTIONS_SHOWING && oldValue !== newValue) {
const selectedTrack = parseTextTracksStr(newValue ?? '')[0];

this.#caps.forEach(track => {
track.selected = track.language === selectedTrack.language && track.label === selectedTrack.label;
});
this.#render();

} else if (attrName === 'aria-multiselectable') {
// diallow aria-multiselectable
this.removeAttribute('aria-multiselectable');
@@ -166,27 +149,28 @@ class MediaCaptionsListbox extends MediaChromeListbox {
return oldItems.filter(track => !removedTracks.includes(track)).concat(newTracks);
}

#perTypeRender(tracks, type) {
#renderTracks(tracks) {
const container = this.shadowRoot.querySelector('ul slot');

tracks.forEach(track => {
let option = track.el;
let alreadyInDom = true;
const type = track.kind ?? 'subs';

if (!option) {
option = document.createElement('media-chrome-listitem');
alreadyInDom = false;

option.part.add('listitem');
option.value = type + '!' + formatTextTrackObj(track);
option.value = formatTextTrackObj(track);

const label = document.createElement('span');

label.textContent = track.label;
option.append(label);

// add CC icon for captions
if (type === 'cc') {
if (type === 'captions') {
option.append(this.#captionsIndicator.cloneNode(true));
}
}
@@ -211,28 +195,27 @@ class MediaCaptionsListbox extends MediaChromeListbox {
container.append(this.#offOption);
}

if (!this.hasAttribute(MediaUIAttributes.MEDIA_CAPTIONS_SHOWING) && !this.hasAttribute(MediaUIAttributes.MEDIA_SUBTITLES_SHOWING)) {
if (!this.hasAttribute(MediaUIAttributes.MEDIA_SUBTITLES_SHOWING)) {
this.#offOption.setAttribute('aria-selected', 'true');
this.#offOption.setAttribute('tabindex', '0');
} else {
this.#offOption.setAttribute('aria-selected', 'false');
this.#offOption.setAttribute('tabindex', '-1');
}

this.#perTypeRender(this.#caps, 'cc');
this.#perTypeRender(this.#subs, 'subs');
this.#renderTracks(this.#subs);
}

#onChange() {
const [newType, selectedOption] = this.selectedOptions[0]?.value?.split('!') ?? [];
const selectedOption = this.selectedOptions[0]?.value;

// turn off currently selected tracks
toggleSubsCaps(this, true);

if (!selectedOption) return;

const event = new window.CustomEvent(
newType === 'cc' ? MediaUIEvents.MEDIA_SHOW_CAPTIONS_REQUEST : MediaUIEvents.MEDIA_SHOW_SUBTITLES_REQUEST,
MediaUIEvents.MEDIA_SHOW_SUBTITLES_REQUEST,
{
composed: true,
bubbles: true,
62 changes: 20 additions & 42 deletions src/js/media-captions-button.js
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import MediaChromeButton from './media-chrome-button.js';
import { window, document } from './utils/server-safe-globals.js';
import { MediaUIAttributes } from './constants.js';
import { nouns } from './labels/labels.js';
import { isCCOn, toggleSubsCaps } from './utils/captions.js';
import { areSubsOn, toggleSubsCaps } from './utils/captions.js';

export const Attributes = {
DEFAULT_SHOWING: 'defaultshowing',
@@ -38,17 +38,14 @@ slotTemplate.innerHTML = `
`;

const updateAriaChecked = (el) => {
el.setAttribute('aria-checked', isCCOn(el));
el.setAttribute('aria-checked', areSubsOn(el));
};

class MediaCaptionsButton extends MediaChromeButton {
static get observedAttributes() {
return [
...super.observedAttributes,
Attributes.NO_SUBTITLES_FALLBACK,
Attributes.DEFAULT_SHOWING,
MediaUIAttributes.MEDIA_CAPTIONS_LIST,
MediaUIAttributes.MEDIA_CAPTIONS_SHOWING,
MediaUIAttributes.MEDIA_SUBTITLES_LIST,
MediaUIAttributes.MEDIA_SUBTITLES_SHOWING,
];
@@ -69,48 +66,29 @@ class MediaCaptionsButton extends MediaChromeButton {
}

attributeChangedCallback(attrName, oldValue, newValue) {
if (
[
MediaUIAttributes.MEDIA_CAPTIONS_SHOWING,
MediaUIAttributes.MEDIA_SUBTITLES_SHOWING,
].includes(attrName)
) {
if (attrName === MediaUIAttributes.MEDIA_SUBTITLES_SHOWING) {
updateAriaChecked(this);
}

if (
this.hasAttribute(Attributes.DEFAULT_SHOWING) && // we want to show captions by default
this.getAttribute('aria-checked') !== 'true' // and we aren't currently showing them
this.getAttribute('aria-checked') !== 'true' && // and we aren't currently showing them
attrName === MediaUIAttributes.MEDIA_SUBTITLES_LIST // and we have subtitles or captions
) {
// Make sure we're only checking against the relevant attributes based on whether or not we are using subtitles fallback
const subtitlesIncluded = !this.hasAttribute(Attributes.NO_SUBTITLES_FALLBACK);
const relevantAttributes = subtitlesIncluded
? [
MediaUIAttributes.MEDIA_CAPTIONS_LIST,
MediaUIAttributes.MEDIA_SUBTITLES_LIST,
]
: [MediaUIAttributes.MEDIA_CAPTIONS_LIST];
// If one of the relevant attributes changed...
if (relevantAttributes.includes(attrName)) {
// check if we went
// a) from captions (/subs) not ready to captions (/subs) ready
// b) from captions (/subs) ready to captions (/subs) not ready.
// by using a simple truthy (empty or non-empty) string check on the relevant values
// NOTE: We're using `getAttribute` here instead of `newValue` because we may care about
// multiple attributes.
const nextCaptionsReady =
!!this.getAttribute(MediaUIAttributes.MEDIA_CAPTIONS_LIST) ||
!!(
subtitlesIncluded &&
this.getAttribute(MediaUIAttributes.MEDIA_SUBTITLES_LIST)
);
// If the value changed, (re)set the internal prop
if (this._captionsReady !== nextCaptionsReady) {
this._captionsReady = nextCaptionsReady;
// If captions are currently ready, that means we went from unready to ready, so
// use the click handler to dispatch a request to turn captions on
if (this._captionsReady) {
toggleSubsCaps(this);
}
// check if we went
// a) from captions (/subs) not ready to captions (/subs) ready
// b) from captions (/subs) ready to captions (/subs) not ready.
// by using a simple truthy (empty or non-empty) string check on the relevant values
// NOTE: We're using `getAttribute` here instead of `newValue` because we may care about
// multiple attributes.
const nextCaptionsReady = newValue
// If the value changed, (re)set the internal prop
if (this._captionsReady !== nextCaptionsReady) {
this._captionsReady = nextCaptionsReady;
// If captions are currently ready, that means we went from unready to ready, so
// use the click handler to dispatch a request to turn captions on
if (this._captionsReady) {
toggleSubsCaps(this);
}
}
}
Loading

1 comment on commit 03e62e6

@vercel
Copy link

@vercel vercel bot commented on 03e62e6 Apr 20, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.