mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
fix(spf): implement preload IDL attribute on SpfMedia (#1069)
This commit is contained in:
@@ -21,5 +21,9 @@ export class SimpleHlsVideo extends MediaAttachMixin(SimpleHlsCustomMedia) {
|
||||
if (attrName === 'src' && oldValue !== newValue) {
|
||||
this.src = newValue ?? '';
|
||||
}
|
||||
|
||||
if (attrName === 'preload' && oldValue !== newValue) {
|
||||
this.preload = (newValue ?? '') as '' | 'none' | 'metadata' | 'auto';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import { attachMediaElement } from '../../utils/attach-media-element';
|
||||
import { mediaProps } from '../../utils/media-props';
|
||||
import { useComposedRefs } from '../../utils/use-composed-refs';
|
||||
|
||||
// TODO: mediaProps uses media.preload which SimpleHlsMedia does not yet expose.
|
||||
// Add preload getter/setter to SimpleHlsMedia as a follow-up.
|
||||
export type SimpleHlsVideoProps = PropsWithChildren<VideoHTMLAttributes<HTMLVideoElement>>;
|
||||
|
||||
export const SimpleHlsVideo = forwardRef<HTMLVideoElement, SimpleHlsVideoProps>(({ children, ...props }, ref) => {
|
||||
|
||||
@@ -77,6 +77,9 @@ export function syncPreloadAttribute(
|
||||
owners: WritableState<PlatformOwners>
|
||||
): () => void {
|
||||
return owners.subscribe((current) => {
|
||||
// Only infer preload from the element when no explicit value has been set.
|
||||
// An explicit value (set via SpfMedia.preload) always wins.
|
||||
if (state.current.preload !== undefined) return;
|
||||
const preload = current.mediaElement?.preload || undefined;
|
||||
state.patch({ preload: preload as 'auto' | 'metadata' | 'none' | undefined });
|
||||
});
|
||||
|
||||
@@ -492,7 +492,7 @@ variant1.m3u8`)
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('updates preload when mediaElement preload changes', () => {
|
||||
it('does not override preload when mediaElement changes and preload is already set', () => {
|
||||
interface State {
|
||||
presentation?: AddressableObject | Presentation | undefined;
|
||||
preload?: 'auto' | 'metadata' | 'none' | undefined;
|
||||
@@ -512,23 +512,24 @@ variant1.m3u8`)
|
||||
mediaElement: video,
|
||||
});
|
||||
|
||||
// Start syncing
|
||||
// Start syncing — initial inference from element
|
||||
const cleanup = syncPreloadAttribute(state, owners);
|
||||
|
||||
expect(state.current.preload).toBe('auto');
|
||||
|
||||
// Change to different mediaElement with different preload
|
||||
// Swap to a different mediaElement with a different preload value.
|
||||
// Since preload is already set, the new element's value is ignored.
|
||||
const updatedVideo = { preload: 'metadata' } as PlatformOwners['mediaElement'];
|
||||
owners.patch({ mediaElement: updatedVideo });
|
||||
owners.flush(); // Flush owners to trigger subscription
|
||||
state.flush(); // Flush state to apply preload update
|
||||
owners.flush();
|
||||
state.flush();
|
||||
|
||||
expect(state.current.preload).toBe('metadata');
|
||||
expect(state.current.preload).toBe('auto');
|
||||
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('sets preload to undefined when mediaElement is removed', () => {
|
||||
it('does not clear preload when mediaElement is removed and preload is already set', () => {
|
||||
interface State {
|
||||
presentation?: AddressableObject | Presentation | undefined;
|
||||
preload?: 'auto' | 'metadata' | 'none' | undefined;
|
||||
@@ -549,10 +550,10 @@ variant1.m3u8`)
|
||||
|
||||
const cleanup = syncPreloadAttribute(state, owners);
|
||||
|
||||
// Remove media element
|
||||
owners.patch({ mediaElement: undefined });
|
||||
|
||||
expect(state.current.preload).toBeUndefined();
|
||||
// Preload was already set — removing the element does not clear it.
|
||||
expect(state.current.preload).toBe('auto');
|
||||
|
||||
cleanup();
|
||||
});
|
||||
|
||||
@@ -26,6 +26,7 @@ import { createPlaybackEngine, type PlaybackEngine } from './engine';
|
||||
export class SpfMedia {
|
||||
#engine: PlaybackEngine;
|
||||
#config: PlaybackEngineConfig;
|
||||
#preload: '' | 'none' | 'metadata' | 'auto' = '';
|
||||
|
||||
/** Pending loadstart listener from a deferred play() retry, if any. */
|
||||
#loadstartListener: (() => void) | null = null;
|
||||
@@ -57,6 +58,24 @@ export class SpfMedia {
|
||||
this.#engine.destroy();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// preload — synchronous IDL attribute (WHATWG §4.8.11.2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
get preload(): '' | 'none' | 'metadata' | 'auto' {
|
||||
return this.#preload;
|
||||
}
|
||||
|
||||
set preload(value: '' | 'none' | 'metadata' | 'auto') {
|
||||
this.#preload = value;
|
||||
if (value) {
|
||||
this.#engine.state.patch({ preload: value });
|
||||
}
|
||||
// value = '' clears #preload (so the next engine recreation won't re-apply
|
||||
// an explicit value) but does not patch current state — the existing preload
|
||||
// stays in effect until the next src change creates a fresh engine.
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// src — synchronous IDL attribute (WHATWG §4.8.11.2)
|
||||
// Each assignment destroys the current engine and starts a fresh one, exactly
|
||||
@@ -74,6 +93,12 @@ export class SpfMedia {
|
||||
this.#engine.destroy();
|
||||
this.#engine = createPlaybackEngine(this.#config);
|
||||
|
||||
// Apply explicit preload before owners.patch so syncPreloadAttribute skips
|
||||
// element inference and the explicit value is preserved across src changes.
|
||||
if (this.#preload) {
|
||||
this.#engine.state.patch({ preload: this.#preload });
|
||||
}
|
||||
|
||||
if (prevMediaElement) {
|
||||
this.#engine.owners.patch({ mediaElement: prevMediaElement });
|
||||
}
|
||||
|
||||
@@ -284,6 +284,56 @@ describe('SpfMedia', () => {
|
||||
// Expected: play() resolves after the media element fires 'playing'.
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// preload — synchronous IDL attribute (WHATWG §4.8.11.2)
|
||||
// ---------------------------------------------------------------------------
|
||||
describe('preload', () => {
|
||||
it('returns empty string before any preload is set', () => {
|
||||
const media = new SpfMedia();
|
||||
expect(media.preload).toBe('');
|
||||
});
|
||||
|
||||
it('reflects the set value synchronously', () => {
|
||||
const media = new SpfMedia();
|
||||
media.preload = 'auto';
|
||||
expect(media.preload).toBe('auto');
|
||||
});
|
||||
|
||||
it('updates engine state immediately when set', () => {
|
||||
const media = new SpfMedia();
|
||||
media.preload = 'none';
|
||||
expect(media.engine.state.current.preload).toBe('none');
|
||||
});
|
||||
|
||||
it('setting preload to empty string resets the stored value but does not clear current engine state', () => {
|
||||
const media = new SpfMedia();
|
||||
media.preload = 'auto';
|
||||
media.preload = '';
|
||||
// '' only clears #preload so the next engine recreation won't re-apply
|
||||
// an explicit value — it does not patch the current engine state.
|
||||
expect(media.engine.state.current.preload).toBe('auto');
|
||||
});
|
||||
|
||||
it('survives src reassignment — explicit preload is preserved across engine recreation', () => {
|
||||
const media = new SpfMedia();
|
||||
media.preload = 'none';
|
||||
media.src = 'https://example.com/v.m3u8';
|
||||
expect(media.preload).toBe('none');
|
||||
expect(media.engine.state.current.preload).toBe('none');
|
||||
});
|
||||
|
||||
it('explicit preload is re-applied before owners.patch on src change so syncPreloadAttribute skips inference', () => {
|
||||
const media = new SpfMedia();
|
||||
const el = document.createElement('video');
|
||||
media.attach(el);
|
||||
media.preload = 'none';
|
||||
media.src = 'https://example.com/v.m3u8';
|
||||
// syncPreloadAttribute fires when owners.patch re-attaches the element,
|
||||
// but since preload was already patched into the new engine's state, it skips.
|
||||
expect(media.engine.state.current.preload).toBe('none');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// destroy() — explicit teardown (separate from detach)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user