mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
fix(core): disable toggle captions when there are no captions (#1598)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { createState } from '@videojs/store';
|
||||
import { isCaptionOrSubtitleTrack } from '@videojs/utils/dom';
|
||||
import { defaults } from '@videojs/utils/object';
|
||||
import { isFunction } from '@videojs/utils/predicate';
|
||||
import type { NonNullableObject } from '@videojs/utils/types';
|
||||
@@ -66,9 +67,7 @@ export class CaptionsButtonCore {
|
||||
|
||||
getState(): CaptionsButtonState {
|
||||
const media = this.#media!;
|
||||
const availability: CaptionsButtonState['availability'] = media.textTrackList.some(
|
||||
(t) => t.kind === 'captions' || t.kind === 'subtitles'
|
||||
)
|
||||
const availability: CaptionsButtonState['availability'] = media.textTrackList.some(isCaptionOrSubtitleTrack)
|
||||
? 'available'
|
||||
: 'unavailable';
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@ export interface MediaSnapshot {
|
||||
muted?: boolean | undefined;
|
||||
fullscreen?: boolean | undefined;
|
||||
subtitlesShowing?: boolean | undefined;
|
||||
/** When false, caption toggles are unavailable and status feedback is suppressed. */
|
||||
subtitlesAvailable?: boolean | undefined;
|
||||
pip?: boolean | undefined;
|
||||
currentTime?: number | undefined;
|
||||
duration?: number | undefined;
|
||||
@@ -111,6 +113,7 @@ export function deriveStatus(
|
||||
case 'volumeStep':
|
||||
return deriveVolumeStatus(event, snapshot, labels);
|
||||
case 'toggleSubtitles': {
|
||||
if (snapshot.subtitlesAvailable === false) return null;
|
||||
const showing = snapshot.subtitlesShowing !== undefined ? !snapshot.subtitlesShowing : true;
|
||||
return {
|
||||
status: showing ? 'captions-on' : 'captions-off',
|
||||
|
||||
@@ -49,6 +49,7 @@ describe('status', () => {
|
||||
|
||||
it('derives captions, fullscreen, and picture-in-picture statuses', () => {
|
||||
expect(deriveStatus({ action: 'toggleSubtitles' }, SNAPSHOT)?.status).toBe('captions-on');
|
||||
expect(deriveStatus({ action: 'toggleSubtitles' }, { ...SNAPSHOT, subtitlesAvailable: false })).toBeNull();
|
||||
expect(deriveStatus({ action: 'toggleFullscreen' }, SNAPSHOT)?.status).toBe('fullscreen');
|
||||
expect(deriveStatus({ action: 'toggleFullscreen' }, { ...SNAPSHOT, fullscreen: true })?.status).toBe(
|
||||
'exit-fullscreen'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { isCaptionOrSubtitleTrack } from '@videojs/utils/dom';
|
||||
|
||||
import type { RemotePlaybackState } from './remote-playback';
|
||||
import type { GoogleCastMediaElement } from './types';
|
||||
import {
|
||||
@@ -141,9 +143,7 @@ export class GoogleCastProvider {
|
||||
const mediaInfo = new chrome.cast.media.MediaInfo(this.media.castSrc, this.media.castContentType ?? '');
|
||||
mediaInfo.customData = (this.media.castCustomData as object) ?? null;
|
||||
|
||||
const subtitles = [...this.media.querySelectorAll('track')].filter(
|
||||
(el) => el.src && (el.kind === 'subtitles' || el.kind === 'captions')
|
||||
);
|
||||
const subtitles = [...this.media.querySelectorAll('track')].filter((el) => el.src && isCaptionOrSubtitleTrack(el));
|
||||
|
||||
const { Track, TrackType, TextTrackType } = chrome.cast.media;
|
||||
const activeTrackIds: number[] = [];
|
||||
@@ -481,7 +481,7 @@ export class GoogleCastProvider {
|
||||
async #updateRemoteTextTrack() {
|
||||
if (!this.isCasting) return;
|
||||
|
||||
const localSubs = [...this.media.textTracks].filter(({ kind }) => kind === 'subtitles' || kind === 'captions');
|
||||
const localSubs = [...this.media.textTracks].filter(isCaptionOrSubtitleTrack);
|
||||
|
||||
const matched = (this.#remote.mediaInfo?.tracks ?? [])
|
||||
.filter(({ type }) => type === chrome.cast.media.TrackType.TEXT)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { listen } from '@videojs/utils/dom';
|
||||
import { isCaptionOrSubtitleTrack, listen } from '@videojs/utils/dom';
|
||||
import type { Constructor } from '@videojs/utils/types';
|
||||
import type { CuesParsedData, NonNativeTextTracksData } from 'hls.js';
|
||||
import Hls from 'hls.js';
|
||||
@@ -83,7 +83,7 @@ export function HlsJsMediaTextTracksMixin<Base extends Constructor<HlsEngineHost
|
||||
if (!engine.subtitleTracks.length) return;
|
||||
|
||||
const showingTrack = Array.from(media.textTracks).find((textTrack) => {
|
||||
return textTrack.id && textTrack.mode === 'showing' && ['subtitles', 'captions'].includes(textTrack.kind);
|
||||
return textTrack.id && textTrack.mode === 'showing' && isCaptionOrSubtitleTrack(textTrack);
|
||||
});
|
||||
|
||||
if (!showingTrack) return;
|
||||
@@ -165,7 +165,7 @@ function addTextTrack(
|
||||
if (defaultTrack) {
|
||||
trackEl.default = true;
|
||||
}
|
||||
trackEl.track.mode = ['subtitles', 'captions'].includes(kind) ? 'disabled' : 'hidden';
|
||||
trackEl.track.mode = isCaptionOrSubtitleTrack({ kind }) ? 'disabled' : 'hidden';
|
||||
|
||||
// Add data attribute to identify tracks that should be removed when switching sources/destroying hls.js instance.
|
||||
trackEl.setAttribute('data-removeondestroy', '');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { findTrackElement, getTextTrackList, listen } from '@videojs/utils/dom';
|
||||
import { findTrackElement, getTextTrackList, isCaptionOrSubtitleTrack, listen } from '@videojs/utils/dom';
|
||||
|
||||
import type { MediaTextCue, MediaTextTrack, MediaTextTrackState } from '../../../core/media/state';
|
||||
import type { TextTrackLike } from '../../../core/media/types';
|
||||
@@ -17,10 +17,7 @@ export const textTrackFeature = definePlayerFeature({
|
||||
const { media } = target();
|
||||
if (!isMediaTextTrackCapable(media)) return false;
|
||||
|
||||
const subtitlesTracks = getTextTrackList(
|
||||
media,
|
||||
(track) => track.kind === 'subtitles' || track.kind === 'captions'
|
||||
);
|
||||
const subtitlesTracks = getTextTrackList(media, isCaptionOrSubtitleTrack);
|
||||
if (!subtitlesTracks.length) return false;
|
||||
|
||||
const showing = subtitlesTracks.some((track) => track.mode === 'showing');
|
||||
@@ -62,7 +59,7 @@ export const textTrackFeature = definePlayerFeature({
|
||||
mode: track.mode,
|
||||
});
|
||||
|
||||
if ((track.kind === 'captions' || track.kind === 'subtitles') && track.mode === 'showing') {
|
||||
if (isCaptionOrSubtitleTrack(track) && track.mode === 'showing') {
|
||||
subtitlesShowing = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { isCaptionOrSubtitleTrack } from '@videojs/utils/dom';
|
||||
|
||||
import { IndicatorVisibilityCoordinator } from '../../core/ui/input-feedback/indicator-lifecycle';
|
||||
import type { InputActionEvent, MediaSnapshot } from '../../core/ui/input-feedback/status';
|
||||
import { getGestureCoordinator } from '../gesture/coordinator';
|
||||
@@ -34,12 +36,15 @@ export function getMediaSnapshot(store: MediaSnapshotStore | undefined): MediaSn
|
||||
const state = store.state;
|
||||
const time = selectTime(state);
|
||||
|
||||
const textTrack = selectTextTrack(state);
|
||||
|
||||
return {
|
||||
paused: selectPlayback(state)?.paused,
|
||||
volume: selectVolume(state)?.volume,
|
||||
muted: selectVolume(state)?.muted,
|
||||
fullscreen: selectFullscreen(state)?.fullscreen,
|
||||
subtitlesShowing: selectTextTrack(state)?.subtitlesShowing,
|
||||
subtitlesShowing: textTrack?.subtitlesShowing,
|
||||
subtitlesAvailable: textTrack ? (textTrack.textTrackList ?? []).some(isCaptionOrSubtitleTrack) : undefined,
|
||||
pip: selectPiP(state)?.pip,
|
||||
currentTime: time?.currentTime,
|
||||
duration: time?.duration,
|
||||
|
||||
@@ -38,6 +38,7 @@ describe('input-action', () => {
|
||||
muted: false,
|
||||
fullscreen: true,
|
||||
subtitlesShowing: true,
|
||||
textTrackList: [{ kind: 'captions', label: 'English', language: 'en', mode: 'showing' }],
|
||||
pip: false,
|
||||
currentTime: 30,
|
||||
duration: 120,
|
||||
@@ -49,6 +50,7 @@ describe('input-action', () => {
|
||||
muted: false,
|
||||
fullscreen: true,
|
||||
subtitlesShowing: true,
|
||||
subtitlesAvailable: true,
|
||||
pip: false,
|
||||
currentTime: 30,
|
||||
duration: 120,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { isCaptionOrSubtitleTrack } from '@videojs/utils/dom';
|
||||
|
||||
import type { PartiallyResolvedTextTrack, TextTrack } from '../../types';
|
||||
|
||||
/**
|
||||
@@ -44,7 +46,7 @@ export function getShowingSubtitlesTrackFromMedia(mediaElement: HTMLMediaElement
|
||||
const elements = mediaElement.querySelectorAll<HTMLTrackElement>(SPF_TRACK_SELECTOR);
|
||||
for (const el of elements) {
|
||||
const track = el.track;
|
||||
if (track.mode === 'showing' && (track.kind === 'subtitles' || track.kind === 'captions')) {
|
||||
if (track.mode === 'showing' && isCaptionOrSubtitleTrack(track)) {
|
||||
return track;
|
||||
}
|
||||
}
|
||||
@@ -72,7 +74,7 @@ export function removeAllSubtitlesTracksFromMedia(mediaElement: HTMLMediaElement
|
||||
export function syncTextTrackModes(textTracks: TextTrackList, selectedId: string | undefined): void {
|
||||
for (let i = 0; i < textTracks.length; i++) {
|
||||
const track = textTracks[i]!;
|
||||
if (track.kind !== 'subtitles' && track.kind !== 'captions') continue;
|
||||
if (!isCaptionOrSubtitleTrack(track)) continue;
|
||||
track.mode = track.id === selectedId ? 'showing' : 'disabled';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ export { getSlottedElement, querySlot } from './slotted';
|
||||
export { applyStyles, resolveCSSLength } from './style';
|
||||
export { supportsAnchorPositioning, supportsAnimationFrame, supportsIdleCallback } from './supports';
|
||||
export { createTemplate, renderTemplate } from './template';
|
||||
export { findTrackElement, getTextTrackList } from './text-track';
|
||||
export {
|
||||
type CaptionOrSubtitleKind,
|
||||
findTrackElement,
|
||||
getTextTrackList,
|
||||
isCaptionOrSubtitleTrack,
|
||||
} from './text-track';
|
||||
export { serializeTimeRanges } from './time-ranges';
|
||||
export type { CustomElement, CustomElementCallbacks } from './types';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { findTrackElement } from '../text-track';
|
||||
import { findTrackElement, isCaptionOrSubtitleTrack } from '../text-track';
|
||||
|
||||
/**
|
||||
* jsdom does not create unique TextTrack objects per <track> element,
|
||||
@@ -12,6 +12,19 @@ function mockTrackProperty(el: HTMLTrackElement): TextTrack {
|
||||
return track;
|
||||
}
|
||||
|
||||
describe('isCaptionOrSubtitleTrack', () => {
|
||||
it('returns true for captions and subtitles tracks', () => {
|
||||
expect(isCaptionOrSubtitleTrack({ kind: 'captions' })).toBe(true);
|
||||
expect(isCaptionOrSubtitleTrack({ kind: 'subtitles' })).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for other text track kinds', () => {
|
||||
expect(isCaptionOrSubtitleTrack({ kind: 'chapters' })).toBe(false);
|
||||
expect(isCaptionOrSubtitleTrack({ kind: 'metadata' })).toBe(false);
|
||||
expect(isCaptionOrSubtitleTrack({ kind: 'descriptions' })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findTrackElement', () => {
|
||||
it('returns the track element that owns the given TextTrack', () => {
|
||||
const video = document.createElement('video');
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
export type CaptionOrSubtitleKind = 'captions' | 'subtitles';
|
||||
|
||||
/** Whether a text track is a captions or subtitles track. */
|
||||
export function isCaptionOrSubtitleTrack(track: { kind: string }): track is { kind: CaptionOrSubtitleKind } {
|
||||
return track.kind === 'captions' || track.kind === 'subtitles';
|
||||
}
|
||||
|
||||
/** Find the `<track>` element that owns the given `TextTrack`. */
|
||||
export function findTrackElement(media: EventTarget, track: unknown): HTMLTrackElement | null {
|
||||
if (!(media instanceof HTMLElement)) return null;
|
||||
|
||||
Reference in New Issue
Block a user