diff --git a/src/components/buttons/Subtitles.ts b/src/components/buttons/Subtitles.ts index 0ccb907..3923644 100644 --- a/src/components/buttons/Subtitles.ts +++ b/src/components/buttons/Subtitles.ts @@ -18,8 +18,8 @@ const icons = { @customElement("video-subtitles-button") export class SubtitlesButton extends VideoButton { - @connect("activeTextTrack") - activeTrack: string; + @connect("activeTextTrackId") + activeTrackId: string; @connect("textTracks") textTracks: State["textTracks"]; @@ -38,8 +38,8 @@ export class SubtitlesButton extends VideoButton { if (!this.textTracks?.length) return null; return html` - - ${this.activeTrack ? icons.solid : icons.outline} + + ${this.activeTrackId ? icons.solid : icons.outline} `; } @@ -76,21 +76,21 @@ export class SubtitlesButton extends VideoButton { }; handleItemClick = (e: any) => { - const lang = e.detail.value; + const trackId = e.detail.value; this.command(Types.Command.enableTextTrack, { - lang: lang === "off" ? "" : lang, + trackId: trackId === "off" ? "" : trackId, }); this.removeMenu(); }; get getMenuItems(): any { - const active = this.activeTrack || ""; - return [{ label: "Off", lang: "" }, ...(this.textTracks || [])].map( + const active = this.activeTrackId || "off"; + return [{ label: "Off", lang: "", id: "off" }, ...(this.textTracks || [])].map( (track) => ({ ...track, - value: track.lang || "off", - isActive: active === track.lang, - iconAfter: active === track.lang ? icons.check : null, + value: track.id, + isActive: active === track.id, + iconAfter: active === track.id ? icons.check : null, }), ); } diff --git a/src/components/video-button/Video-button.styles.css b/src/components/video-button/Video-button.styles.css index efdb5cd..6d87d41 100644 --- a/src/components/video-button/Video-button.styles.css +++ b/src/components/video-button/Video-button.styles.css @@ -12,7 +12,8 @@ --menu-item-color-active: $menu-item-color-active; --menu-item-background-hover: var(--primary, $menu-item-background-hover); --menu-item-color-hover: $menu-item-color-hover; - --menu-max-height: auto; + --menu-max-height: var(--video-menu-max-height, auto); + --menu-max-height-subtract: 110px; --size: var(--button-size); } @@ -72,10 +73,10 @@ button { white-space: nowrap; background: var(--tooltip-background); height: 0; - overflow: hidden; user-select: none; - max-height: var(--menu-max-height); - overflow: auto; + max-height: calc(var(--menu-max-height) - var(--menu-max-height-subtract)); + overflow-x: hidden; + overflow-y: auto; & .inner { padding: 6px 12px; } @@ -85,8 +86,9 @@ button { } @media (max-width: 640px) { - :host(:not([is-fullscreen])) .menu { - --menu-max-height: 150px; + .tooltip, + .menu { + --menu-max-height-subtract: 90px; } } diff --git a/src/components/video-chromecast/Video-chromecast.component.ts b/src/components/video-chromecast/Video-chromecast.component.ts index 73904b6..8ba4b4a 100644 --- a/src/components/video-chromecast/Video-chromecast.component.ts +++ b/src/components/video-chromecast/Video-chromecast.component.ts @@ -31,8 +31,8 @@ export class VideoChromecast extends LitElement { @connect("textTracks") cues: State["textTracks"]; - @connect("activeTextTrack") - activeTextTrack: string; + @connect("activeTextTrackId") + activeTextTrackId: string; @state() targetDevise: string; @@ -100,7 +100,7 @@ export class VideoChromecast extends LitElement { if (!media) return; - const index = this.cues.findIndex((s) => this.activeTextTrack === s.lang); + const index = this.cues.findIndex((s) => this.activeTextTrackId === s.id); const request = new window.chrome.cast.media.EditTracksInfoRequest([index]); media.editTracksInfo(request, void 0, void 0); @@ -148,9 +148,9 @@ export class VideoChromecast extends LitElement { const request = new window.chrome.cast.media.LoadRequest(media); - if (this.activeTextTrack) { + if (this.activeTextTrackId) { const subtitlesLanguageIdx = this.cues.findIndex( - ({ lang }) => this.activeTextTrack === lang, + ({ id }) => this.activeTextTrackId === id, ); request.activeTrackIds = diff --git a/src/components/video-container/Video-container.component.ts b/src/components/video-container/Video-container.component.ts index 391c58c..b174ef5 100644 --- a/src/components/video-container/Video-container.component.ts +++ b/src/components/video-container/Video-container.component.ts @@ -194,11 +194,11 @@ export class VideoContainer extends LitElement { } @listen(Types.Command.enableTextTrack) - enableTextTrack({ lang }: { lang: string }) { + enableTextTrack({ trackId }: { trackId: string }) { dispatch(this, Types.Action.selectTextTrack, { - activeTextTrack: lang, + activeTextTrackId: trackId, }); - this.subtitles.enableTextTrack(lang); + this.subtitles.enableTextTrack(trackId); } @listen(Types.Command.setPlaybackRate, { canPlay: true }) @@ -340,7 +340,7 @@ export class VideoContainer extends LitElement { this, this.videos[0], this.hls, - this._storageProvider.get().activeTextTrack, + this._storageProvider.get().activeTextTrackId, ); }, ); @@ -463,8 +463,8 @@ export class VideoContainer extends LitElement { value = +params.playbackRate; break; case Types.Command.enableTextTrack: - key = "activeTextTrack"; - value = params.lang; + key = "activeTextTrackId"; + value = params.trackId; break; } const currentVal = this._storageProvider.get(); @@ -494,7 +494,7 @@ export class VideoContainer extends LitElement { this, this.videos[0], this.hls, - savedSettings.activeTextTrack, + savedSettings.activeTextTrackId, ); } diff --git a/src/components/video-container/subtitles.ts b/src/components/video-container/subtitles.ts index 0708c35..52c8e2d 100644 --- a/src/components/video-container/subtitles.ts +++ b/src/components/video-container/subtitles.ts @@ -1,11 +1,16 @@ import { dispatch, Types } from "../../state"; import { VideoContainer } from "./Video-container.component"; import { mapCueListToState } from "../../helpers/cue"; +import debounce from "../../helpers/debounce" import type Hls from "hls.js"; import _debug from "debug"; +import { MediaPlaylist } from "hls.js"; const subtitlesDebug = _debug("player:subtitles"); +const buildTrackId = (track: TextTrack) => `${track.label}-${track.language}`; +const buildHlsTrackId = (track: MediaPlaylist) => `${track.name}-${track.lang}`; + /** * Util to manage vide text tracks */ @@ -34,22 +39,21 @@ const videoTextTtracksManager = (video: HTMLVideoElement, hls: Hls) => { src: langToSrcMapping[t.language] || "", lang: t.language || t.label, label: t.label, - })), + id: buildTrackId(t), + })).sort((a, b) => a.lang.localeCompare(b.lang, undefined, { sensitivity: 'base' })) }); - const showTracks = (lang: string) => { + const showTracks = (trackId: string) => { if (hls) { hls.subtitleTracks.forEach((t) => { - const tLang = t.lang || t.name; - if (tLang === lang) { + if (buildHlsTrackId(t) === trackId) { hls.subtitleTrack = t.id; hls.subtitleDisplay = true; } }); } getTracks().forEach((t) => { - const tLang = t.language || t.label; - if (tLang === lang) { + if (buildTrackId(t) === trackId) { t.mode = "hidden"; } else { t.mode = "disabled"; @@ -77,7 +81,7 @@ export const subtitlesController = ( host: VideoContainer, video: HTMLVideoElement, hls: Hls, - defaultTextTrack?: string, + defaultTextTrackId?: string, ) => { if (hls) { // Disable subtitles by default @@ -85,7 +89,7 @@ export const subtitlesController = ( hls.subtitleDisplay = false; } - let activeTextTrack = defaultTextTrack; + let activeTextTrackId = defaultTextTrackId; const tracksManager = videoTextTtracksManager(video, hls); @@ -93,7 +97,6 @@ export const subtitlesController = ( tracksManager.removeNativeTextTracks() } - dispatch(host, Types.Action.update, tracksManager.tracksToStoreState()); const onCueChange = (event: Event & { target: TextTrack }) => { subtitlesDebug( @@ -102,16 +105,16 @@ export const subtitlesController = ( event.target.kind, event.target.mode, ); - const targetLang = event.target.language || event.target.label; + const targetTrackId = buildTrackId(event.target) - if (event.target.mode === "showing" && targetLang !== activeTextTrack) { - activeTextTrack = targetLang; + if (event.target.mode === "showing" && targetTrackId !== activeTextTrackId) { + activeTextTrackId = targetTrackId; dispatch(host, Types.Action.selectTextTrack, { - activeTextTrack: targetLang, + activeTextTrackId: targetTrackId, }); } - if (targetLang === activeTextTrack) { + if (targetTrackId === activeTextTrackId) { const cues = mapCueListToState(event.target.activeCues); dispatch(host, Types.Action.cues, { cues }); } @@ -121,7 +124,13 @@ export const subtitlesController = ( t.oncuechange = onCueChange; }); - tracksManager.showTracks(activeTextTrack); + tracksManager.showTracks(activeTextTrackId); + + const updateTracksListSate = debounce(() => { + dispatch(host, Types.Action.update, tracksManager.tracksToStoreState()); + }, 100) + + updateTracksListSate() const onTextTrackAdded = (data: TrackEvent) => { subtitlesDebug( @@ -133,20 +142,20 @@ export const subtitlesController = ( if (!tracksManager.isTrackNative(data.track)) { tracksManager.removeNativeTextTracks() data.track.oncuechange = onCueChange; - tracksManager.showTracks(activeTextTrack); - dispatch(host, Types.Action.update, tracksManager.tracksToStoreState()); + tracksManager.showTracks(activeTextTrackId); + updateTracksListSate() } }; video.textTracks.addEventListener("addtrack", onTextTrackAdded); return { - enableTextTrack: (lang: string) => { - activeTextTrack = lang; - tracksManager.showTracks(activeTextTrack); + enableTextTrack: (trackId: string) => { + activeTextTrackId = trackId; + tracksManager.showTracks(activeTextTrackId); const activeTrack = tracksManager .getTracks() - .find((t) => (t.language || t.label) === activeTextTrack); + .find((t) => buildTrackId(t) === activeTextTrackId); if (activeTrack && activeTrack.activeCues) { dispatch(host, Types.Action.cues, { cues: mapCueListToState(activeTrack.activeCues), diff --git a/src/components/video-controls/Video-controls.component.ts b/src/components/video-controls/Video-controls.component.ts index eb5f0df..2457299 100644 --- a/src/components/video-controls/Video-controls.component.ts +++ b/src/components/video-controls/Video-controls.component.ts @@ -1,5 +1,5 @@ import { connect } from "../../state"; -import { unsafeCSS, LitElement, html } from "lit"; +import { unsafeCSS, LitElement, html, PropertyValues } from "lit"; import { customElement, property } from "lit/decorators.js"; import { DependentPropsMixin } from "../../mixins/DependentProps"; import styles from "./Video-controls.styles.css?inline"; @@ -39,6 +39,28 @@ export class VideoControls extends DependentPropsMixin(LitElement) { @property({ type: Boolean, reflect: true }) custom = false; + private resizeObserver: ResizeObserver; + + connectedCallback(): void { + super.connectedCallback(); + this.resizeObserver = new ResizeObserver((entries) => { + const [entry] = entries + if (entry?.contentBoxSize) { + const { blockSize } = entry.contentBoxSize[0] + this.style.cssText = `${this.style.cssText}; --video-menu-max-height: ${Math.round(blockSize)}px;` + } + }); + } + + protected firstUpdated(_changedProperties: PropertyValues): void { + this.resizeObserver.observe(this.parentElement) + } + + disconnectedCallback(): void { + super.disconnectedCallback() + this.resizeObserver?.disconnect() + } + render() { return html``; } diff --git a/src/components/video-cues/Video-cues.component.ts b/src/components/video-cues/Video-cues.component.ts index 19c674d..b2cf00c 100644 --- a/src/components/video-cues/Video-cues.component.ts +++ b/src/components/video-cues/Video-cues.component.ts @@ -18,8 +18,8 @@ export class VideoCues extends LitElement { /** * The currently active text track (e.g., subtitles or captions). */ - @connect("activeTextTrack") - activeTextTrack: string; + @connect("activeTextTrackId") + activeTextTrackId: string; /** * An array of cues or subtitles to be displayed during video playback. @@ -41,7 +41,7 @@ export class VideoCues extends LitElement { isFullscreen: false; render() { - if ((this.isIos && this.isFullscreen) || !this.activeTextTrack) return null; + if ((this.isIos && this.isFullscreen) || !this.activeTextTrackId) return null; return this.cues.map( (cue) => html` diff --git a/src/helpers/debounce.ts b/src/helpers/debounce.ts new file mode 100644 index 0000000..943a31c --- /dev/null +++ b/src/helpers/debounce.ts @@ -0,0 +1,14 @@ +function debounce void>(func: T, wait: number): (...args: Parameters) => void { + let timeout: ReturnType | null; + + return function(...args: Parameters): void { + if (timeout) { + clearTimeout(timeout); + } + timeout = setTimeout(() => { + func(...args); + }, wait); + }; +} + +export default debounce; \ No newline at end of file diff --git a/src/helpers/storage.ts b/src/helpers/storage.ts index e05e2cb..477a579 100644 --- a/src/helpers/storage.ts +++ b/src/helpers/storage.ts @@ -6,7 +6,7 @@ export type StorageValue = Partial< | "isMuted" | "volume" | "activeQualityLevel" - | "activeTextTrack" + | "activeTextTrackId" | "playbackRate" > >; diff --git a/src/types.ts b/src/types.ts index 18d161f..1aff71f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -116,7 +116,7 @@ export type State = Partial< isAutoplay: boolean; isSourceSupported: boolean; isFullscreen: boolean; - activeTextTrack: string; + activeTextTrackId: string; activeQualityLevel: number; playbackRate: number; customHLS: boolean; @@ -132,6 +132,7 @@ export type State = Partial< label: string; src: string; lang: string; + id: string; }[]; qualityLevels: { name: string;