1
0
Files
VR-Web-Player/src/vr180player/media/media-adapter.ts
2026-06-10 12:48:36 +10:00

159 lines
3.9 KiB
TypeScript

export type MediaCapabilities = {
audio: boolean;
dynamicTexture: boolean;
playback: boolean;
timeline: boolean;
};
type MediaLoadCallbacks = {
onError: (event: Event) => void;
onReady: () => void;
};
export type MediaKind = 'image' | 'video';
export interface MediaAdapter<TElement extends HTMLElement = HTMLElement, TTextureSource = TElement> {
readonly capabilities: MediaCapabilities;
readonly element: TElement;
readonly kind: MediaKind;
readonly textureSource: TTextureSource;
bindLoadState(callbacks: MediaLoadCallbacks): void;
getTitle(): string;
hideElement(): void;
load(): void;
shouldUpdateTexture(): boolean;
showElement(): void;
}
export type SupportedMediaAdapter = ImageMediaAdapter | VideoMediaAdapter;
const VIDEO_CAPABILITIES: MediaCapabilities = {
audio: true,
dynamicTexture: true,
playback: true,
timeline: true
};
const IMAGE_CAPABILITIES: MediaCapabilities = {
audio: false,
dynamicTexture: false,
playback: false,
timeline: false
};
export class VideoMediaAdapter implements MediaAdapter<HTMLVideoElement, HTMLVideoElement> {
readonly capabilities = VIDEO_CAPABILITIES;
readonly kind = 'video' as const;
constructor(readonly element: HTMLVideoElement) {}
get textureSource(): HTMLVideoElement {
return this.element;
}
getTitle(): string {
return this.element.getAttribute('title') ||
this.element.querySelector('source')?.src.split('/').pop()?.split('.')[0].replace(/-/g, ' ') ||
'Video Title';
}
bindLoadState({ onError, onReady }: MediaLoadCallbacks): void {
if (this.element.readyState >= this.element.HAVE_METADATA) {
queueMicrotask(onReady);
}
this.element.addEventListener('loadedmetadata', onReady);
this.element.addEventListener('canplaythrough', onReady);
this.element.addEventListener('error', onError);
}
hideElement(): void {
this.element.style.display = 'none';
}
load(): void {
this.element.load();
}
shouldUpdateTexture(): boolean {
return !this.element.paused && !this.element.ended;
}
showElement(): void {
this.element.style.display = '';
}
}
export class ImageMediaAdapter implements MediaAdapter<HTMLImageElement, HTMLImageElement> {
readonly capabilities = IMAGE_CAPABILITIES;
readonly kind = 'image' as const;
constructor(readonly element: HTMLImageElement) {}
get textureSource(): HTMLImageElement {
return this.element;
}
getTitle(): string {
return this.element.getAttribute('title') ||
this.element.getAttribute('alt') ||
getFilenameTitle(this.element.currentSrc || this.element.src) ||
'Image Title';
}
bindLoadState({ onError, onReady }: MediaLoadCallbacks): void {
if (this.element.complete && this.element.naturalWidth > 0) {
queueMicrotask(onReady);
}
this.element.addEventListener('load', onReady);
this.element.addEventListener('error', onError);
}
hideElement(): void {
this.element.style.display = 'none';
}
load(): void {
// Images begin loading from markup. Kept for parity with video media.
}
shouldUpdateTexture(): boolean {
return false;
}
showElement(): void {
this.element.style.display = '';
}
}
export function createMediaAdapter(playerContainer: HTMLElement): SupportedMediaAdapter | null {
const mediaElements = Array.from(
playerContainer.querySelectorAll<HTMLVideoElement | HTMLImageElement>('video,img')
);
if (mediaElements.length !== 1) {
return null;
}
const mediaElement = mediaElements[0];
const tagName = mediaElement.tagName.toLowerCase();
mediaElement.classList.add('vrwp-media');
if (tagName === 'video') {
mediaElement.classList.add('vrwp-video');
return new VideoMediaAdapter(mediaElement as HTMLVideoElement);
}
if (tagName === 'img') {
mediaElement.classList.add('vrwp-image');
return new ImageMediaAdapter(mediaElement as HTMLImageElement);
}
return null;
}
function getFilenameTitle(source: string): string {
return source.split('/').pop()?.split('.')[0].replace(/-/g, ' ') || '';
}