diff --git a/packages/mux-player/src/index.ts b/packages/mux-player/src/index.ts index 842828a8e..50d254b94 100644 --- a/packages/mux-player/src/index.ts +++ b/packages/mux-player/src/index.ts @@ -21,7 +21,7 @@ import type { MinResolutionValue, RenditionOrderValue, } from '@mux/playback-core'; -import VideoApiElement, { initVideoApi } from './video-api'; +import VideoApiElement from './video-api'; import { getPlayerVersion, toPropName, @@ -110,7 +110,7 @@ function getProps(el: MuxPlayerElement, state?: any): MuxTemplateProps { // Give priority to playbackId derrived asset URL's if playbackId is set. src: !el.playbackId && el.src, playbackId: el.playbackId, - hasSrc: !!el.playbackId || !!el.src, + hasSrc: !!el.playbackId || !!el.src || !!el.currentSrc, poster: el.poster, storyboard: el.storyboard, storyboardSrc: el.getAttribute(PlayerAttributes.STORYBOARD_SRC), @@ -341,18 +341,20 @@ class MuxPlayerElement extends VideoApiElement implements MuxPlayerElement { logger.error(` failed to upgrade!`); } - initVideoApi(this); + this.init(); this.#setUpThemeAttributes(); this.#setUpErrors(); this.#setUpCaptionsButton(); this.#userInactive = this.mediaController?.hasAttribute(MediaControllerAttributes.USER_INACTIVE) ?? true; this.#setUpCaptionsMovement(); + // NOTE: Make sure we re-render when stream type changes to ensure other props-driven // template details get updated appropriately (e.g. thumbnails track) (CJP) - this.media?.addEventListener('streamtypechange', () => { - this.#render(); - }); + this.media?.addEventListener('streamtypechange', () => this.#render()); + + // NOTE: Make sure we re-render when tags are appended so hasSrc is updated. + this.media?.addEventListener('loadstart', () => this.#render()); } #setupCSSProperties() { diff --git a/packages/mux-player/src/video-api.ts b/packages/mux-player/src/video-api.ts index a2783684a..ab5001279 100644 --- a/packages/mux-player/src/video-api.ts +++ b/packages/mux-player/src/video-api.ts @@ -61,24 +61,6 @@ const AllowedVideoAttributeNames = Object.values(AllowedVideoAttributes).filter( ); const CustomVideoAttributesNames = Object.values(CustomVideoAttributes); -/** - * Gets called from mux-player when mux-video is rendered and upgraded. - * We might just merge VideoApiElement in MuxPlayerElement and remove this? - */ -export function initVideoApi(el: VideoApiElement) { - el.querySelectorAll(':scope > track').forEach((track) => { - el.media?.append(track.cloneNode()); - }); - - // The video events are dispatched on the VideoApiElement instance. - // This makes it possible to add event listeners before the element is upgraded. - AllowedVideoEvents.forEach((type) => { - el.media?.addEventListener(type, (evt) => { - el.dispatchEvent(new Event(evt.type)); - }); - }); -} - // NOTE: Some of these are defined in MuxPlayerElement. We may want to apply a // `Pick<>` on these to also enforce consistency (CJP). type PartialHTMLVideoElement = Omit< @@ -96,7 +78,6 @@ type PartialHTMLVideoElement = Omit< | 'requestPictureInPicture' | 'requestVideoFrameCallback' | 'controls' - | 'currentSrc' | 'disableRemotePlayback' | 'mediaKeys' | 'networkState' @@ -148,6 +129,8 @@ interface VideoApiElement extends PartialHTMLVideoElement, HTMLElement { } class VideoApiElement extends globalThis.HTMLElement implements VideoApiElement { + #mediaChildrenMap = new WeakMap(); + static get observedAttributes() { return [...AllowedVideoAttributeNames, ...CustomVideoAttributesNames]; } @@ -160,23 +143,19 @@ class VideoApiElement extends globalThis.HTMLElement implements VideoApiElement constructor() { super(); - this.querySelectorAll(':scope > track').forEach((track) => { - this.media?.append(track.cloneNode()); - }); - // Watch for child adds/removes and update the native element if necessary - /** @type {(mutationList: MutationRecord[]) => void} */ const mutationCallback = (mutationsList: MutationRecord[]) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { - // Child being removed mutation.removedNodes.forEach((node) => { - const track = this.media?.querySelector(`track[src="${(node as HTMLTrackElement).src}"]`); - if (track) this.media?.removeChild(track); + this.#mediaChildrenMap.get(node)?.remove(); }); mutation.addedNodes.forEach((node) => { - this.media?.append(node.cloneNode()); + const element = node as HTMLElement; + if (!element?.slot) { + this.media?.append(getOrInsertNodeClone(this.#mediaChildrenMap, node)); + } }); } } @@ -184,6 +163,24 @@ class VideoApiElement extends globalThis.HTMLElement implements VideoApiElement const observer = new MutationObserver(mutationCallback); observer.observe(this, { childList: true, subtree: true }); + + // The video events are dispatched on the VideoApiElement instance. + // This makes it possible to add event listeners before the element is upgraded. + AllowedVideoEvents.forEach((type) => { + this.media?.addEventListener(type, (evt) => { + this.dispatchEvent(new Event(evt.type)); + }); + }); + } + + /** + * Gets called from mux-player when mux-video is rendered and upgraded. + * We might just merge VideoApiElement in MuxPlayerElement and remove this? + */ + init() { + this.querySelectorAll(':scope > :not([slot])').forEach((child) => { + this.media?.append(getOrInsertNodeClone(this.#mediaChildrenMap, child)); + }); } attributeChangedCallback(attrName: string, oldValue: string | null, newValue: string) { @@ -221,6 +218,10 @@ class VideoApiElement extends globalThis.HTMLElement implements VideoApiElement this.media?.pause(); } + load() { + this.media?.load(); + } + requestCast(options: CastOptions) { return this.media?.requestCast(options); } @@ -277,6 +278,10 @@ class VideoApiElement extends globalThis.HTMLElement implements VideoApiElement return this.media?.videoHeight ?? 0; } + get currentSrc() { + return this.media?.currentSrc ?? ''; + } + get currentTime() { return this.media?.currentTime ?? 0; } @@ -410,4 +415,13 @@ function getVideoAttribute(el: VideoApiElement, name: string) { return el.media ? el.media.getAttribute(name) : el.getAttribute(name); } +function getOrInsertNodeClone(map: WeakMap, node: Node) { + let clone = map.get(node); + if (!clone) { + clone = node.cloneNode(); + map.set(node, clone); + } + return clone; +} + export default VideoApiElement;