diff --git a/packages/core/src/core/media/proxy.ts b/packages/core/src/core/media/proxy.ts index f932ef77..042f2279 100644 --- a/packages/core/src/core/media/proxy.ts +++ b/packages/core/src/core/media/proxy.ts @@ -21,10 +21,7 @@ export interface MediaProxy { * * The `get`, `set`, and `call` methods can be overridden to provide catch-all custom behavior. */ -export const ProxyMixin = ( - PrimaryClass: AnyConstructor, - ...AdditionalClasses: AnyConstructor[] -) => { +export const ProxyMixin = (BaseClass: AnyConstructor) => { class MediaProxyImpl extends EventTarget { #target: EventTarget | null = null; #types = new Set(); @@ -81,8 +78,12 @@ export const ProxyMixin = ( }; } - for (const Class of [PrimaryClass, ...AdditionalClasses]) { - defineClassPropHooks(MediaProxyImpl, Class.prototype); + for ( + let proto = BaseClass.prototype; + proto && !Object.prototype.isPrototypeOf.call(proto, MediaProxyImpl.prototype); + proto = Object.getPrototypeOf(proto) + ) { + defineClassPropHooks(MediaProxyImpl, proto); } return MediaProxyImpl as unknown as Constructor; diff --git a/packages/core/src/core/media/tests/proxy.test.ts b/packages/core/src/core/media/tests/proxy.test.ts index 2236a57c..16cdd9b2 100644 --- a/packages/core/src/core/media/tests/proxy.test.ts +++ b/packages/core/src/core/media/tests/proxy.test.ts @@ -155,6 +155,46 @@ describe('ProxyMixin', () => { }); }); + describe('prototype chain walking', () => { + it('proxies methods inherited from ancestor prototypes', () => { + class Grandparent extends EventTarget { + inherited() { + return 'grandparent'; + } + } + + class Parent extends Grandparent { + direct() { + return 'parent'; + } + } + + const ParentProxy = ProxyMixin(Parent); + const proxy = new ParentProxy(); + const target = new Parent(); + proxy.attach(target); + + expect(proxy.direct()).toBe('parent'); + expect(proxy.inherited()).toBe('grandparent'); + }); + + it('stops before prototypes the proxy already extends', () => { + class Child extends EventTarget { + custom() { + return 'custom'; + } + } + + const ChildProxy = ProxyMixin(Child); + + // addEventListener is defined by the proxy itself — the walk + // should not overwrite it with a forwarding hook. + expect(Object.getOwnPropertyDescriptor(ChildProxy.prototype, 'addEventListener')?.value).toBe( + ChildProxy.prototype.addEventListener + ); + }); + }); + describe('EventListenerObject support', () => { it('invokes handleEvent on an object listener', () => { const { proxy, target } = setup(); diff --git a/packages/core/src/dom/media/custom-media-element/index.ts b/packages/core/src/dom/media/custom-media-element/index.ts index 60b75949..7a73b080 100644 --- a/packages/core/src/dom/media/custom-media-element/index.ts +++ b/packages/core/src/dom/media/custom-media-element/index.ts @@ -372,7 +372,6 @@ export function CustomMediaMixin>( this.#childObserver?.observe(el, { attributes: true }); } this.target?.append(clone); - this.#enableDefaultTrack(clone as HTMLTrackElement); }); removeNativeChildren.forEach((clone, el) => { @@ -388,25 +387,11 @@ export function CustomMediaMixin>( 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 { - // Enable default text 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 diff --git a/packages/core/src/dom/media/proxy.ts b/packages/core/src/dom/media/proxy.ts index d8f34639..08e7e79d 100644 --- a/packages/core/src/dom/media/proxy.ts +++ b/packages/core/src/dom/media/proxy.ts @@ -1,7 +1,3 @@ import { ProxyMixin } from '../../core/media/proxy'; -export const VideoProxy = ProxyMixin( - globalThis.HTMLVideoElement ?? class {}, - globalThis.HTMLMediaElement ?? class {}, - globalThis.EventTarget ?? class {} -); +export const VideoProxy = ProxyMixin(globalThis.HTMLVideoElement ?? class {}); diff --git a/packages/core/src/dom/store/features/text-track.ts b/packages/core/src/dom/store/features/text-track.ts index 1a0a5312..4b7d7353 100644 --- a/packages/core/src/dom/store/features/text-track.ts +++ b/packages/core/src/dom/store/features/text-track.ts @@ -60,6 +60,11 @@ 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 diff --git a/packages/react/src/ui/thumbnail/thumbnail.tsx b/packages/react/src/ui/thumbnail/thumbnail.tsx index 983967a5..71bb9e64 100644 --- a/packages/react/src/ui/thumbnail/thumbnail.tsx +++ b/packages/react/src/ui/thumbnail/thumbnail.tsx @@ -55,8 +55,9 @@ export const Thumbnail = forwardRef(function Thu // Resolve thumbnails: external prop takes priority over auto path. const thumbnails = useMemo(() => { if (externalThumbnails && externalThumbnails.length > 0) return externalThumbnails; - if (!textTrack?.thumbnailCues.length) return []; - return mapCuesToThumbnails(textTrack.thumbnailCues, textTrack.thumbnailTrackSrc ?? undefined); + return textTrack && textTrack.thumbnailCues.length > 0 + ? mapCuesToThumbnails(textTrack.thumbnailCues, textTrack.thumbnailTrackSrc ?? undefined) + : []; }, [externalThumbnails, textTrack]); const thumbnail = useMemo(() => core.findActiveThumbnail(thumbnails, time), [core, thumbnails, time]);