fix(react): thumbnails broken when using hls media (#1210)

This commit is contained in:
rahim
2026-04-03 00:11:27 -07:00
committed by GitHub
parent 831ddf4ac7
commit 8b571f5f05
6 changed files with 56 additions and 28 deletions
+7 -6
View File
@@ -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 = <T extends EventTarget>(
PrimaryClass: AnyConstructor<T>,
...AdditionalClasses: AnyConstructor<EventTarget>[]
) => {
export const ProxyMixin = <T extends EventTarget>(BaseClass: AnyConstructor<T>) => {
class MediaProxyImpl extends EventTarget {
#target: EventTarget | null = null;
#types = new Set<string>();
@@ -81,8 +78,12 @@ export const ProxyMixin = <T extends EventTarget>(
};
}
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<T & MediaProxy>;
@@ -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();
@@ -372,7 +372,6 @@ 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) => {
@@ -388,25 +387,11 @@ 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 {
// 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
+1 -5
View File
@@ -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 {});
@@ -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
@@ -55,8 +55,9 @@ export const Thumbnail = forwardRef<HTMLDivElement, ThumbnailProps>(function Thu
// Resolve thumbnails: external prop takes priority over auto <track> 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]);