feat: add Mux video component (#1036)

This commit is contained in:
Wesley Luyten
2026-03-24 18:19:50 -05:00
committed by GitHub
parent 833210a270
commit 271a8c8502
45 changed files with 941 additions and 182 deletions
+1
View File
@@ -0,0 +1 @@
import '../../define/media/mux-video';
@@ -0,0 +1,14 @@
import { MuxVideo } from '../../media/mux-video';
import { safeDefine } from '../safe-define';
export class MuxVideoElement extends MuxVideo {
static readonly tagName = 'mux-video';
}
safeDefine(MuxVideoElement);
declare global {
interface HTMLElementTagNameMap {
[MuxVideoElement.tagName]: MuxVideoElement;
}
}
+3 -18
View File
@@ -1,28 +1,13 @@
import { DashCustomMedia } from '@videojs/core/dom/media/dash';
import { DashCustomMedia, DashMediaDelegate } from '@videojs/core/dom/media/dash';
import { MediaAttachMixin } from '../../store/media-attach-mixin';
import { MediaPropsMixin } from '../../utils/media-props-mixin';
export class DashVideo extends MediaAttachMixin(DashCustomMedia) {
static getTemplateHTML(attrs: Record<string, string>): string {
const { src, ...rest } = attrs;
// biome-ignore lint/complexity/noThisInStatic: intentional use of super
return super.getTemplateHTML(rest);
}
export class DashVideo extends MediaPropsMixin(MediaAttachMixin(DashCustomMedia), DashMediaDelegate) {
constructor() {
super();
this.attach(this.target);
}
attributeChangedCallback(attrName: string, oldValue: string | null, newValue: string | null): void {
if (attrName !== 'src') {
super.attributeChangedCallback(attrName, oldValue, newValue);
}
if (attrName === 'src' && oldValue !== newValue) {
this.src = newValue ?? '';
}
}
disconnectedCallback(): void {
super.disconnectedCallback?.();
+3 -18
View File
@@ -1,13 +1,8 @@
import { HlsCustomMedia } from '@videojs/core/dom/media/hls';
import { HlsCustomMedia, HlsMediaDelegate } from '@videojs/core/dom/media/hls';
import { MediaAttachMixin } from '../../store/media-attach-mixin';
import { MediaPropsMixin } from '../../utils/media-props-mixin';
export class HlsVideo extends MediaAttachMixin(HlsCustomMedia) {
static getTemplateHTML(attrs: Record<string, string>): string {
const { src, ...rest } = attrs;
// biome-ignore lint/complexity/noThisInStatic: intentional use of super
return super.getTemplateHTML(rest);
}
export class HlsVideo extends MediaPropsMixin(MediaAttachMixin(HlsCustomMedia), HlsMediaDelegate) {
constructor() {
super();
// TODO: If we like to support native media elements that
@@ -17,16 +12,6 @@ export class HlsVideo extends MediaAttachMixin(HlsCustomMedia) {
this.attach(this.target);
}
attributeChangedCallback(attrName: string, oldValue: string | null, newValue: string | null): void {
if (attrName !== 'src') {
super.attributeChangedCallback(attrName, oldValue, newValue);
}
if (attrName === 'src' && oldValue !== newValue) {
this.src = newValue ?? '';
}
}
disconnectedCallback(): void {
super.disconnectedCallback?.();
@@ -0,0 +1,18 @@
import { MuxCustomVideo, MuxMediaDelegate } from '@videojs/core/dom/media/mux';
import { MediaAttachMixin } from '../../store/media-attach-mixin';
import { MediaPropsMixin } from '../../utils/media-props-mixin';
export class MuxVideo extends MediaPropsMixin(MediaAttachMixin(MuxCustomVideo), MuxMediaDelegate) {
constructor() {
super();
this.attach(this.target);
}
disconnectedCallback(): void {
super.disconnectedCallback?.();
if (!this.hasAttribute('keep-alive')) {
this.destroy();
}
}
}
@@ -1,29 +1,11 @@
import { SimpleHlsCustomMedia } from '@videojs/core/dom/media/simple-hls';
import { SpfMedia } from '@videojs/spf/dom';
import { MediaAttachMixin } from '../../store/media-attach-mixin';
import { MediaPropsMixin } from '../../utils/media-props-mixin';
export class SimpleHlsVideo extends MediaAttachMixin(SimpleHlsCustomMedia) {
static getTemplateHTML(attrs: Record<string, string>): string {
const { src, ...rest } = attrs;
// biome-ignore lint/complexity/noThisInStatic: intentional use of super
return super.getTemplateHTML(rest);
}
export class SimpleHlsVideo extends MediaPropsMixin(MediaAttachMixin(SimpleHlsCustomMedia), SpfMedia) {
constructor() {
super();
this.attach(this.target);
}
attributeChangedCallback(attrName: string, oldValue: string | null, newValue: string | null): void {
if (attrName !== 'src') {
super.attributeChangedCallback(attrName, oldValue, newValue);
}
if (attrName === 'src' && oldValue !== newValue) {
this.src = newValue ?? '';
}
if (attrName === 'preload' && oldValue !== newValue) {
this.preload = (newValue ?? '') as '' | 'none' | 'metadata' | 'auto';
}
}
}
@@ -0,0 +1,61 @@
import type { AnyConstructor } from '@videojs/utils/types';
type AnyClass = abstract new (...args: any[]) => any;
function camelToKebab(str: string): string {
return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
}
function buildAttrPropMap(DelegateClass: AnyClass): Map<string, string> {
const map = new Map<string, string>();
for (let proto = DelegateClass.prototype; proto && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) {
for (const key of Object.getOwnPropertyNames(proto)) {
const desc = Object.getOwnPropertyDescriptor(proto, key);
if (desc?.set) map.set(camelToKebab(key), key);
}
}
return map;
}
/**
* Mixin that intercepts `getTemplateHTML` and `attributeChangedCallback` to
* handle delegate-owned properties. Delegate props are stripped from template
* HTML attrs and routed through the property setter in `attributeChangedCallback`
* instead of being forwarded to the inner native element.
*/
export function MediaPropsMixin<Base extends AnyConstructor<HTMLElement>>(
BaseClass: Base,
DelegateClass: AnyClass
): Base {
const attrToProp = buildAttrPropMap(DelegateClass);
class MediaPropsElement extends (BaseClass as any) {
static get observedAttributes(): string[] {
// biome-ignore lint/complexity/noThisInStatic: intentional use of super
return [...new Set([...(super.observedAttributes ?? []), ...attrToProp.keys()])];
}
static getTemplateHTML(attrs: Record<string, string>): string {
const filtered = { ...attrs };
for (const attr of attrToProp.keys()) {
delete filtered[attr];
}
// biome-ignore lint/complexity/noThisInStatic: intentional use of super
return super.getTemplateHTML(filtered);
}
attributeChangedCallback(attrName: string, oldValue: string | null, newValue: string | null): void {
const prop = attrToProp.get(attrName);
if (prop) {
if (oldValue !== newValue) {
(this as any)[prop] = typeof (this as any)[prop] === 'boolean' ? newValue !== null : (newValue ?? '');
}
return;
}
super.attributeChangedCallback?.(attrName, oldValue, newValue);
}
}
return MediaPropsElement as unknown as Base;
}