feat(player): SSR via declarative shadow DOM (stacked on #2758) - #2793
Draft
tombeckenham wants to merge 3 commits into
Draft
feat(player): SSR via declarative shadow DOM (stacked on #2758)#2793tombeckenham wants to merge 3 commits into
tombeckenham wants to merge 3 commits into
Conversation
…le app - New ./react subpath on @hyperframes/player: typed <HyperframesPlayer> component with camelCase props for every player attribute, callback props for every player event (CustomEvent detail unwrapped), and an imperative ref handle (play/pause/seek/stopMedia, color grading, currentTime/duration/paused/ready/scenes/element/iframeElement). - SSR-safe: the custom element registers via a dynamic package self-reference on mount, so the player module never evaluates during server rendering; ensurePlayerDefined() allows eager registration. - react is an optional peer dependency (^18 || ^19); the react entry builds separately (esm+cjs, react + player self-reference external) so consumers code-split the element bundle. - New examples/react-player workspace (@hyperframes/react-player-example): vite demo app driving props, events, and the ref handle against a bundled copy of the motion-blur registry composition; examples/* added to workspaces, CI paths filter, and the tracked-examples gitignore negations. - Player README documents the bindings; package-subpaths.json carries the ./react contract.
- Bind event listeners in a layout effect declared before attribute sync, so events the element dispatches synchronously during the initial attribute application (ratechange, volumechange, fast-srcdoc ready) are captured; stub now mirrors that dispatch behavior. - Swap useLayoutEffect for an isomorphic variant (useEffect on the server) to avoid the React 18 SSR warning, and add a node-environment renderToString suite proving no player-module evaluation, no console warnings, and hydration-stable bare markup. - Add the three missing event callbacks: onRuntimeProtocolError, onAudioOwnershipChange, onPlaybackError. - Add an event-surface drift test: scans player sources for dispatchEvent sites and fails when PLAYER_EVENT_CALLBACKS misses one (or claims a phantom event). - Add elementProps passthrough for host attributes (id, role, tabIndex, aria-*, data-*, DOM handlers). - Update the latest-callback ref in a layout effect instead of during render (concurrent-rendering safety). Addresses review feedback from Magi and Jerrai on heygen-com#2758.
Prototype of SSR for <hyperframes-player>: the shadow DOM — styles, container, composition iframe, optional poster — serializes into a <template shadowrootmode="open"> that the browser parses during HTML streaming, so the composition starts loading and paints its first frame before any client JavaScript runs. - ssr.ts (new): DOM-free serializer (renderPlayerShadowDomHtml / renderPlayerTagHtml). Pre-JS fit uses container-query CSS (scale(min(100cqw/w, 100cqh/h))); the client's scaleIframeToFit overrides it inline after upgrade. Attribute values escaped, dimensions coerced numeric before CSS interpolation. - setupPlayerShadowDom (iframe-dom.ts): the element constructor adopts a server-rendered declarative root — same iframe node, same poster — instead of rebuilding; foreign/malformed roots are cleared and rebuilt. - _applySrc/_applySrcdoc guards: the upgrade-time attribute replay skips the assignment when the iframe already carries the identical prepared URL, so an in-flight SSR load is never restarted. prepareSrc() extracted from prepareSrcForElement so serializer and element produce byte-identical srcs. - connectedCallback kicks the probe for an adopted iframe that finished loading before upgrade (its load event pre-dates the listener). - React: new ssr prop renders the template server-side only — the browser consumes it during parsing, client renders no child, so hydration matches without suppression. String attributes ride on the host tag for crawlers and the upgrade replay. Verified in Chrome: a zero-JS page paints the composition's first frame at correct scale; with the player script loading late, the upgrade adopts the mid-flight iframe (same node, no reload — document mark survives) and reaches ready with the probed duration. Known gaps before productionizing: no public ./ssr export or package-subpaths entry, cross-origin iframes fall back to the load event (no readiness handshake), no SSR-framework CI fixture.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Stacked on #2758 — review only the last commit (
a48708e74); the first commits are the React bindings PR. Will rebase onto main once #2758 merges. (A formal stacked base isn't possible cross-fork.)What this is
Prototype answering "can the player load with SSR?" — yes, via Declarative Shadow DOM. The server serializes the player's shadow DOM into a
<template shadowrootmode="open">; the browser parses it during HTML streaming, so the composition iframe starts loading and paints its first frame before any client JavaScript runs. When@hyperframes/playerregisters, the element upgrade adopts the server-rendered DOM instead of rebuilding it — the in-flight iframe is never reloaded.How
src/ssr.ts(new) — DOM-free serializer:renderPlayerShadowDomHtml()(template inner HTML) andrenderPlayerTagHtml()(full tag, for non-React SSR). Pre-JS fit is pure CSS:container-type: size+scale(min(calc(100cqw/w), calc(100cqh/h))); the client'sscaleIframeToFitoverrides inline after upgrade. Attribute values are escaped; dimensions are numerically coerced before CSS interpolation.setupPlayerShadowDom(iframe-dom.ts) — constructor adopts an existing declarative root (same iframe node, same poster); foreign/malformed roots are cleared and rebuilt imperatively._applySrc/_applySrcdocskip assignment when the iframe already carries the identical prepared URL, so the upgrade-time attribute replay can't restart an in-flight SSR load.prepareSrc()extracted so serializer and element produce byte-identical srcs.connectedCallbackinvokes the probe manually for an adopted iframe that finished loading before upgrade (its load event pre-dates the listener); about:blank filtered, cross-origin falls back to the load event.ssrprop emits the template during server rendering only. The browser consumes the template while parsing (removed from light DOM), the client renders no child, so hydration matches with no suppression. String attributes additionally ride on the host tag.Evidence (manual, Chrome)
player.iframeElementis the same node, and the player reachedreadywithduration: 4.ssrrenderToString test. The element's 127 pre-existing tests are untouched and green.Open items before this leaves draft
./ssrexport +package-subpaths.jsoncontract (serializer currently internal)ssrshould be default-on for the React binding once hardened