mirror of
https://github.com/zoriya/v10.git
synced 2026-08-05 05:37:21 +00:00
fix(core): enable default tracks for chapters and metadata (#1216)
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
export function renderStoryboard(src?: string | undefined): string {
|
||||
return src ? `<track kind="metadata" label="thumbnails" src="${src}" />` : '';
|
||||
return src ? `<track kind="metadata" label="thumbnails" src="${src}" default />` : '';
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ type StoryboardProps = {
|
||||
|
||||
export function Storyboard({ src }: StoryboardProps) {
|
||||
if (!src) return null;
|
||||
return <track kind="metadata" label="thumbnails" src={src} />;
|
||||
return <track kind="metadata" label="thumbnails" src={src} default />;
|
||||
}
|
||||
|
||||
@@ -372,6 +372,7 @@ export function CustomMediaMixin<T extends Constructor<HTMLElement>>(
|
||||
this.#childObserver?.observe(el, { attributes: true });
|
||||
}
|
||||
this.target?.append(clone);
|
||||
this.#enableDefaultTrack(clone as HTMLTrackElement);
|
||||
});
|
||||
|
||||
removeNativeChildren.forEach((clone, el) => {
|
||||
@@ -387,11 +388,26 @@ export function CustomMediaMixin<T extends Constructor<HTMLElement>>(
|
||||
const clone = this.#childMap.get(target as MediaChild);
|
||||
if (clone && attributeName) {
|
||||
clone.setAttribute(attributeName, (target as MediaChild).getAttribute(attributeName) ?? '');
|
||||
this.#enableDefaultTrack(clone as HTMLTrackElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#enableDefaultTrack(trackEl: HTMLTrackElement): void {
|
||||
// Browsers don't honor the `default` attribute if a track is added via JS.
|
||||
// Enable default tracks for chapters or metadata.
|
||||
if (
|
||||
trackEl &&
|
||||
trackEl.localName === 'track' &&
|
||||
trackEl.default &&
|
||||
(trackEl.kind === 'chapters' || trackEl.kind === 'metadata') &&
|
||||
trackEl.track.mode === 'disabled'
|
||||
) {
|
||||
trackEl.track.mode = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
#upgradeProperty(this: typeof nativeElTest, prop: keyof typeof nativeElTest) {
|
||||
// Sets properties that are set before the custom element is upgraded.
|
||||
// https://web.dev/custom-elements-best-practices/#make-properties-lazy
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Hls, { type HlsConfig } from 'hls.js';
|
||||
import type { Delegate } from '../../../core/media/delegate';
|
||||
import { HlsJsMediaErrorsMixin } from './errors';
|
||||
import { HlsJsMediaMetadataTracksMixin } from './metadata-tracks';
|
||||
import { HlsJsMediaPreloadMixin } from './preload';
|
||||
import { HlsJsMediaTextTracksMixin } from './text-tracks';
|
||||
|
||||
@@ -55,5 +56,5 @@ class HlsJsMediaDelegateBase extends EventTarget implements Delegate {
|
||||
}
|
||||
|
||||
export class HlsJsMediaDelegate extends HlsJsMediaPreloadMixin(
|
||||
HlsJsMediaTextTracksMixin(HlsJsMediaErrorsMixin(HlsJsMediaDelegateBase))
|
||||
HlsJsMediaMetadataTracksMixin(HlsJsMediaTextTracksMixin(HlsJsMediaErrorsMixin(HlsJsMediaDelegateBase)))
|
||||
) {}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { Constructor } from '@videojs/utils/types';
|
||||
import Hls from 'hls.js';
|
||||
import type { HlsEngineHost } from './preload';
|
||||
|
||||
/**
|
||||
* Ensures user-authored metadata and chapters `<track>` elements stay loaded
|
||||
* when hls.js is active.
|
||||
*
|
||||
* hls.js forcibly clears all cues from text tracks on manifest loads and media
|
||||
* attaches. This mixin re-enables those tracks by forcing `mode = 'hidden'`
|
||||
* and reloading the track source when cues have been wiped.
|
||||
*/
|
||||
export function HlsJsMediaMetadataTracksMixin<Base extends Constructor<HlsEngineHost>>(BaseClass: Base) {
|
||||
class HlsJsMediaMetadataTracks extends (BaseClass as Constructor<HlsEngineHost>) {
|
||||
constructor(...args: any[]) {
|
||||
super(...args);
|
||||
|
||||
// Watch out here, AFTER the manifest is loaded!
|
||||
this.engine?.on(Hls.Events.MANIFEST_LOADED, () => this.#forceHiddenTracks());
|
||||
this.engine?.on(Hls.Events.MEDIA_ATTACHED, () => this.#forceHiddenTracks());
|
||||
}
|
||||
|
||||
#forceHiddenTracks(): void {
|
||||
const { target } = this;
|
||||
if (!target) return;
|
||||
|
||||
[...target.textTracks].forEach((track) => {
|
||||
if (!(track.kind === 'metadata' || track.kind === 'chapters')) return;
|
||||
|
||||
let selector = 'track';
|
||||
if (track.kind) selector += `[kind="${track.kind}"]`;
|
||||
if (track.label) selector += `[label="${track.label}"]`;
|
||||
|
||||
const trackEl = target.querySelector(selector) as HTMLTrackElement | null;
|
||||
if (!trackEl) return;
|
||||
|
||||
const src = trackEl.getAttribute('src') ?? '';
|
||||
const TRACK_LOADED = 2;
|
||||
|
||||
// Only reset the src attribute if the track was loaded before and had no cues.
|
||||
if (trackEl.readyState === TRACK_LOADED && !track.cues?.length) {
|
||||
trackEl.removeAttribute('src');
|
||||
setTimeout(() => {
|
||||
trackEl.setAttribute('src', src);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if (trackEl?.default && track.mode !== 'hidden') {
|
||||
track.mode = 'hidden';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return HlsJsMediaMetadataTracks as unknown as Base;
|
||||
}
|
||||
@@ -16,7 +16,6 @@ export const textTrackFeature = definePlayerFeature({
|
||||
target().media,
|
||||
(track) => track.kind === 'subtitles' || track.kind === 'captions'
|
||||
);
|
||||
|
||||
if (!subtitlesTracks.length) return false;
|
||||
|
||||
const showing = subtitlesTracks.some((track: TextTrack) => track.mode === 'showing');
|
||||
@@ -61,11 +60,6 @@ export const textTrackFeature = definePlayerFeature({
|
||||
}
|
||||
}
|
||||
|
||||
// Browsers don't auto-load cues for metadata/chapters tracks even with
|
||||
// the `default` attribute — mode stays 'disabled' until explicitly set.
|
||||
if (chaptersTrack && chaptersTrack.mode === 'disabled') chaptersTrack.mode = 'hidden';
|
||||
if (thumbnailTrack && thumbnailTrack.mode === 'disabled') thumbnailTrack.mode = 'hidden';
|
||||
|
||||
// VTTCue extends TextTrackCue with `text` — cast via `unknown` since
|
||||
// the CueList is typed as TextTrackCue which doesn't expose `text`.
|
||||
const chaptersCues: MediaTextCue[] = chaptersTrack?.cues
|
||||
@@ -84,7 +78,11 @@ export const textTrackFeature = definePlayerFeature({
|
||||
// Listen for <track> load events on tracks that don't have cues yet.
|
||||
// `addtrack` fires before cues are parsed — we need the `load` event
|
||||
// on the <track> element to know when cues are ready.
|
||||
for (const trackEl of media.querySelectorAll?.('track') ?? []) {
|
||||
const tracks = media.querySelectorAll?.('track') ?? [];
|
||||
// For a CustomMediaElement, the functional tracks are in the shadow root.
|
||||
const shadowTracks = media.shadowRoot?.querySelectorAll?.('track') ?? [];
|
||||
|
||||
for (const trackEl of [...tracks, ...shadowTracks]) {
|
||||
if (!trackEl.track?.cues?.length) {
|
||||
listen(trackEl, 'load', sync, { signal: trackCleanup.signal });
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
kind="metadata"
|
||||
label="thumbnails"
|
||||
src="/docs/demos/thumbnail/basic.vtt"
|
||||
default
|
||||
/>
|
||||
</video>
|
||||
<media-thumbnail class="html-thumbnail-text-track__thumbnail" time="12"></media-thumbnail>
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function TextTrackUsage() {
|
||||
playsInline
|
||||
crossOrigin="anonymous"
|
||||
>
|
||||
<track kind="metadata" label="thumbnails" src="/docs/demos/thumbnail/basic.vtt" />
|
||||
<track kind="metadata" label="thumbnails" src="/docs/demos/thumbnail/basic.vtt" default />
|
||||
</Video>
|
||||
<Thumbnail className="react-thumbnail-text-track__thumbnail" time={12} />
|
||||
</Player.Container>
|
||||
|
||||
@@ -39,6 +39,7 @@ export default function HeroVideo({
|
||||
kind="metadata"
|
||||
label="thumbnails"
|
||||
src={`https://image.mux.com/${VJS10_DEMO_VIDEO.id}/storyboard.vtt`}
|
||||
default
|
||||
/>
|
||||
</HlsVideo>
|
||||
</SkinComponent>
|
||||
|
||||
@@ -46,6 +46,7 @@ import jsonSpriteHtmlTs from "@/components/docs/demos/thumbnail/html/css/JsonSpr
|
||||
kind="metadata"
|
||||
label="thumbnails"
|
||||
src="https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/storyboard.vtt"
|
||||
default
|
||||
/>
|
||||
</Video>
|
||||
<Thumbnail time={12} />
|
||||
@@ -59,6 +60,7 @@ import jsonSpriteHtmlTs from "@/components/docs/demos/thumbnail/html/css/JsonSpr
|
||||
kind="metadata"
|
||||
label="thumbnails"
|
||||
src="https://image.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM/storyboard.vtt"
|
||||
default
|
||||
/>
|
||||
</video>
|
||||
<media-thumbnail time="12"></media-thumbnail>
|
||||
|
||||
Reference in New Issue
Block a user