mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
Merge pull request #2535 from heygen-com/fix/runtime-partial-timeline-pause-guard
fix(runtime): tolerate registry timelines without pause() in interactive transport
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
// AUTO-GENERATED by scripts/build-position-edits-render.ts - do not edit
|
||||
const POSITION_EDITS_RENDER_IIFE: string =
|
||||
'"use strict";(()=>{function A(){return globalThis}function P(e,t){if(typeof window>"u")return;let o=A(),r=o.__hf?.onSwallowed;if(r)try{r({label:e,error:t})}catch(i){}(o.__hfDebug||o.__HYPERFRAMES_DEBUG)&&console.debug(`[hyperframes] ${e} swallowed:`,t)}var T=null;function R(e,t){if(T)try{T({source:"hf-preview",type:"analytics",event:e,properties:t??{}})}catch(o){P("runtime.analytics.site1",o)}}var _="data-hf-edit-base-x",m="data-hf-edit-base-y",w="data-hf-edit-original-translate",k=e=>{let t=parseFloat(e??"");return Number.isFinite(t)?t:0},H=e=>{let t=[],o=0,r="";for(let i of e.trim())i==="("&&(o+=1),i===")"&&(o=Math.max(0,o-1)),/\\s/.test(i)&&o===0?(r&&t.push(r),r=""):r+=i;return r&&t.push(r),t},$=/^-?(?:\\d+(?:\\.\\d+)?|\\.\\d+)px$/,E=(e,t)=>$.test(e)&&$.test(t)?`${parseFloat(e)+parseFloat(t)}px`:`calc(${e} + ${t})`,I=(e,t,o)=>{if(!e||e==="none")return`${t} ${o}`;let[r,i,l]=H(e);if(r===void 0)return`${t} ${o}`;if(i===void 0)return`${E(r,t)} ${o}`;let d=l===void 0?"":` ${l}`;return`${E(r,t)} ${E(i,o)}${d}`},O=e=>{try{e.ownerDocument.defaultView?.gsap?.getProperty?.(e,"x")}catch{}},j=e=>{let t=e.style.getPropertyValue("translate").trim();if(t)return t==="none"?"":t;try{let o=e.ownerDocument.defaultView,r=o?o.getComputedStyle(e).getPropertyValue("translate").trim():"";return r==="none"?"":r}catch{return""}},S=new WeakMap;function V(e,t){let o=S.get(e);if(!t?.force&&o!==void 0&&e.style.getPropertyValue("translate")!==o){R("position_edit_fold_skipped",{hfId:e.getAttribute("data-hf-id")});return}let r=k(e.getAttribute("data-x"))-k(e.getAttribute(_)),i=k(e.getAttribute("data-y"))-k(e.getAttribute(m));e.hasAttribute(w)||e.setAttribute(w,j(e)),o===void 0&&O(e);let l=e.getAttribute(w)??"",d=I(l,`${r}px`,`${i}px`);e.style.setProperty("translate",d),S.set(e,e.style.getPropertyValue("translate"))}function h(e,t){let o=e.defaultView?.HTMLElement,r=u=>o?u instanceof o:typeof u.style?.setProperty=="function",i=e.querySelectorAll(`[${w}]:not([${_}]):not([${m}])`);for(let u=0;u<i.length;u++){let a=i[u];if(a===void 0||!r(a))continue;let b=a.getAttribute(w)??"";b===""?a.style.removeProperty("translate"):a.style.setProperty("translate",b),a.removeAttribute(w),S.delete(a)}let l=e.querySelectorAll(`[${_}], [${m}]`),d=0;for(let u=0;u<l.length;u++){let a=l[u];a===void 0||!r(a)||(V(a,t),d+=1)}return d}var x="__hfPositionEditsSeekReapplyWrapped",D=new WeakSet,F=new WeakMap,L=new WeakMap;function M(e){let t=e,o=()=>{try{h(t.document)}catch{}},r=n=>typeof n=="function"&&(D.has(n)||!!n[x]),i=n=>{D.add(n);try{Object.defineProperty(n,x,{value:!0})}catch{}},l=n=>{if(typeof n!="function"||r(n))return n;let s=function(...c){let f=n.apply(this,c);return o(),f};return i(s),s},d=(n,s)=>{let c=F.get(n);if(c?.has(s))return!0;let f=Object.getOwnPropertyDescriptor(n,s);if(f?.configurable===!1){let p=n[s];return typeof p=="function"&&(n[s]=l(p),o()),!1}let g=n[s],y=f?.set;return Object.defineProperty(n,s,{configurable:!0,enumerable:f?.enumerable??!0,get:()=>g,set:p=>{g=l(p),y?.call(n,p)}}),g=l(g),c??(c=new Set),c.add(s),F.set(n,c),o(),!0},u=(n,s)=>{let c=L.get(t),f=Object.getOwnPropertyDescriptor(t,n);if(!c?.has(n)){if(f?.configurable===!1){let p=t[n];return p?d(p,s):!1}let y=t[n];Object.defineProperty(t,n,{configurable:!0,enumerable:f?.enumerable??!0,get:()=>y,set:p=>{y=p,y&&d(y,s)}}),c??(c=new Set),c.add(n),L.set(t,c)}let g=t[n];return g?d(g,s):!1},a=()=>{let n=u("__hf","seek"),s=u("__player","renderSeek");return n&&s};if(a())return;let b=120,v=t.setInterval(()=>{if(a()){t.clearInterval(v);return}b-=1,b<=0&&t.clearInterval(v)},50)}function W(){document.querySelector(`[${_}], [${m}]`)&&(h(document),M(window))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",W,{once:!0}):W();})();\n';
|
||||
'"use strict";(()=>{function A(){return globalThis}function P(e,t){if(typeof window>"u")return;let o=A(),r=o.__hf?.onSwallowed;if(r)try{r({label:e,error:t})}catch(i){}(o.__hfDebug||o.__HYPERFRAMES_DEBUG)&&console.debug(`[hyperframes] ${e} swallowed:`,t)}var T=null;function R(e,t){if(T)try{T({source:"hf-preview",type:"analytics",event:e,properties:t??{}})}catch(o){P("runtime.analytics.site1",o)}}var m="data-hf-edit-base-x",b="data-hf-edit-base-y",w="data-hf-edit-original-translate",k=e=>{let t=parseFloat(e??"");return Number.isFinite(t)?t:0},H=e=>{let t=[],o=0,r="";for(let i of e.trim())i==="("&&(o+=1),i===")"&&(o=Math.max(0,o-1)),/\\s/.test(i)&&o===0?(r&&t.push(r),r=""):r+=i;return r&&t.push(r),t},$=/^-?(?:\\d+(?:\\.\\d+)?|\\.\\d+)px$/,E=(e,t)=>$.test(e)&&$.test(t)?`${parseFloat(e)+parseFloat(t)}px`:`calc(${e} + ${t})`,I=(e,t,o)=>{if(!e||e==="none")return`${t} ${o}`;let[r,i,l]=H(e);if(r===void 0)return`${t} ${o}`;if(i===void 0)return`${E(r,t)} ${o}`;let d=l===void 0?"":` ${l}`;return`${E(r,t)} ${E(i,o)}${d}`},O=e=>{try{e.ownerDocument.defaultView?.gsap?.getProperty?.(e,"x")}catch{}},j=e=>{let t=e.style.getPropertyValue("translate").trim();if(t)return t==="none"?"":t;try{let o=e.ownerDocument.defaultView,r=o?o.getComputedStyle(e).getPropertyValue("translate").trim():"";return r==="none"?"":r}catch{return""}},S=new WeakMap;function V(e,t){let o=S.get(e);if(!t?.force&&o!==void 0&&e.style.getPropertyValue("translate")!==o){R("position_edit_fold_skipped",{hfId:e.getAttribute("data-hf-id")});return}let r=k(e.getAttribute("data-x"))-k(e.getAttribute(m)),i=k(e.getAttribute("data-y"))-k(e.getAttribute(b));e.hasAttribute(w)||e.setAttribute(w,j(e)),o===void 0&&O(e);let l=e.getAttribute(w)??"",d=I(l,`${r}px`,`${i}px`);e.style.setProperty("translate",d),S.set(e,e.style.getPropertyValue("translate"))}function h(e,t){let o=e.defaultView?.HTMLElement,r=u=>o?u instanceof o:typeof u.style?.setProperty=="function",i=e.querySelectorAll(`[${w}]:not([${m}]):not([${b}])`);for(let u=0;u<i.length;u++){let a=i[u];if(a===void 0||!r(a))continue;let _=a.getAttribute(w)??"";_===""?a.style.removeProperty("translate"):a.style.setProperty("translate",_),a.removeAttribute(w),S.delete(a)}let l=e.querySelectorAll(`[${m}], [${b}]`),d=0;for(let u=0;u<l.length;u++){let a=l[u];a===void 0||!r(a)||(V(a,t),d+=1)}return d}var x="__hfPositionEditsSeekReapplyWrapped",D=new WeakSet,F=new WeakMap,L=new WeakMap;function M(e){let t=e,o=()=>{try{h(t.document)}catch{}},r=n=>typeof n=="function"&&(D.has(n)||!!n[x]),i=n=>{D.add(n);try{Object.defineProperty(n,x,{value:!0})}catch{}},l=n=>{if(typeof n!="function"||r(n))return n;let s=function(...c){let f=n.apply(this,c);return o(),f};return i(s),s},d=(n,s)=>{let c=F.get(n);if(c?.has(s))return!0;let f=Object.getOwnPropertyDescriptor(n,s);if(f?.configurable===!1){let p=n[s];return typeof p=="function"&&(n[s]=l(p),o()),!1}let g=n[s],y=f?.set;return Object.defineProperty(n,s,{configurable:!0,enumerable:f?.enumerable??!0,get:()=>g,set:p=>{g=l(p),y?.call(n,p)}}),g=l(g),c??(c=new Set),c.add(s),F.set(n,c),o(),!0},u=(n,s)=>{let c=L.get(t),f=Object.getOwnPropertyDescriptor(t,n);if(!c?.has(n)){if(f?.configurable===!1){let p=t[n];return p?d(p,s):!1}let y=t[n];Object.defineProperty(t,n,{configurable:!0,enumerable:f?.enumerable??!0,get:()=>y,set:p=>{y=p,y&&d(y,s)}}),c??(c=new Set),c.add(n),L.set(t,c)}let g=t[n];return g?d(g,s):!1},a=()=>{let n=u("__hf","seek"),s=u("__player","renderSeek");return n&&s};if(a())return;let _=120,v=t.setInterval(()=>{if(a()){t.clearInterval(v);return}_-=1,_<=0&&t.clearInterval(v)},50)}function W(){document.querySelector(`[${m}], [${b}]`)&&(h(document),M(window))}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",W,{once:!0}):W();})();\n';
|
||||
|
||||
/** Returns the pre-built position-edits render IIFE as a string constant. */
|
||||
export function getPositionEditsRenderScript(): string {
|
||||
|
||||
@@ -13,7 +13,8 @@ export type RuntimeAnalyticsEvent =
|
||||
| "composition_seeked"
|
||||
| "composition_ended"
|
||||
| "element_picked"
|
||||
| "position_edit_fold_skipped";
|
||||
| "position_edit_fold_skipped"
|
||||
| "timeline_missing_pause";
|
||||
|
||||
export type RuntimeAnalyticsProperties = Record<string, string | number | boolean | null>;
|
||||
|
||||
|
||||
@@ -2035,4 +2035,35 @@ describe("initSandboxRuntimeModular", () => {
|
||||
expect(footer.style.left).toBe("");
|
||||
});
|
||||
});
|
||||
describe("partial registry timelines", () => {
|
||||
it("survives play/pause/seek when the sole registered timeline lacks pause()", () => {
|
||||
const root = document.createElement("div");
|
||||
root.setAttribute("data-composition-id", "main");
|
||||
root.setAttribute("data-root", "true");
|
||||
root.setAttribute("data-start", "0");
|
||||
root.setAttribute("data-duration", "10");
|
||||
root.setAttribute("data-width", "1920");
|
||||
root.setAttribute("data-height", "1080");
|
||||
document.body.appendChild(root);
|
||||
|
||||
// An authored composition can register a PARTIAL timeline — duration/seek
|
||||
// only. It renders fine (the render path never pauses), so the interactive
|
||||
// transport must tolerate the missing pause() instead of throwing
|
||||
// "tl.pause is not a function" (top recurring studio unhandled error).
|
||||
const partial = createMockTimeline(10) as RuntimeTimelineLike & { pause?: unknown };
|
||||
delete partial.pause;
|
||||
window.__timelines = { main: partial as RuntimeTimelineLike };
|
||||
|
||||
initSandboxRuntimeModular();
|
||||
const player = window.__player;
|
||||
expect(player).toBeDefined();
|
||||
|
||||
expect(() => {
|
||||
player?.play();
|
||||
player?.pause();
|
||||
player?.seek(1);
|
||||
player?.renderSeek(2);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,6 +53,32 @@ import { shouldAttemptPeriodicTimelineBind } from "./timelineRebindPolicy";
|
||||
const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
|
||||
const AUTHORED_END_ATTR = "data-hf-authored-end";
|
||||
|
||||
/**
|
||||
* A `window.__timelines` entry is authored content and may be a PARTIAL
|
||||
* RuntimeTimelineLike — e.g. duration/seek only, no `pause()`. Such
|
||||
* compositions render fine (the render path only seeks and never pauses), so
|
||||
* timeline resolution stays permissive by design; the interactive transport
|
||||
* must not crash on the missing method (top recurring studio:unhandled_error:
|
||||
* "E.pause is not a function"). One analytics event per page so the
|
||||
* composition author can find the partial timeline.
|
||||
*/
|
||||
let warnedTimelineMissingPause = false;
|
||||
function pauseTimelineIfPossible(tl: RuntimeTimelineLike | null | undefined): void {
|
||||
if (!tl) return;
|
||||
if (typeof tl.pause !== "function") {
|
||||
if (!warnedTimelineMissingPause) {
|
||||
warnedTimelineMissingPause = true;
|
||||
emitAnalyticsEvent("timeline_missing_pause", {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
tl.pause();
|
||||
} catch (err) {
|
||||
swallow("runtime.timeline.pause", err);
|
||||
}
|
||||
}
|
||||
|
||||
type ExportRenderFpsResolution = {
|
||||
fps: number | null;
|
||||
source: "render-options" | "default" | "unknown";
|
||||
@@ -1210,7 +1236,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
if (typeof state.capturedTimeline.progress === "function") {
|
||||
state.capturedTimeline.progress(1, true);
|
||||
state.capturedTimeline.progress(0, false);
|
||||
state.capturedTimeline.pause();
|
||||
pauseTimelineIfPossible(state.capturedTimeline);
|
||||
}
|
||||
}
|
||||
if (boundDuration > 0) {
|
||||
@@ -1232,7 +1258,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
}
|
||||
const seekTime = Math.max(0, state.currentTime || 0);
|
||||
state.capturedTimeline.totalTime(seekTime, false);
|
||||
state.capturedTimeline.pause();
|
||||
pauseTimelineIfPossible(state.capturedTimeline);
|
||||
}
|
||||
|
||||
// GSAP bakes the CSS `translate` into style.transform on seek.
|
||||
@@ -1536,9 +1562,13 @@ export function initSandboxRuntimeModular(): void {
|
||||
state.capturedTimeline.timeScale(state.playbackRate);
|
||||
}
|
||||
try {
|
||||
state.capturedTimeline.pause();
|
||||
state.capturedTimeline.seek(previousTime, false);
|
||||
if (wasPlaying) {
|
||||
// pause guarded separately: a PARTIAL timeline without pause() must not
|
||||
// abort the seek/play restore below (the catch would swallow them too).
|
||||
pauseTimelineIfPossible(state.capturedTimeline);
|
||||
if (typeof state.capturedTimeline.seek === "function") {
|
||||
state.capturedTimeline.seek(previousTime, false);
|
||||
}
|
||||
if (wasPlaying && typeof state.capturedTimeline.play === "function") {
|
||||
state.capturedTimeline.play();
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -2135,7 +2165,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
const declaredDur = Number(rootEl?.getAttribute("data-duration") ?? 0);
|
||||
if (declaredDur > 0) clock.setDuration(declaredDur);
|
||||
}
|
||||
if (tl) tl.pause();
|
||||
pauseTimelineIfPossible(tl);
|
||||
if (!clock.play()) return;
|
||||
state.isPlaying = true;
|
||||
state.mediaForceSyncNextTick = true;
|
||||
@@ -2161,7 +2191,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
state.mediaForceSyncNextTick = true;
|
||||
hardSyncAllMedia(state.currentTime);
|
||||
const tl = state.capturedTimeline;
|
||||
if (tl) tl.pause();
|
||||
pauseTimelineIfPossible(tl);
|
||||
runAdapters("pause");
|
||||
syncMediaForCurrentState();
|
||||
colorGrading.redraw();
|
||||
@@ -2181,7 +2211,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
state.isPlaying = false;
|
||||
state.mediaForceSyncNextTick = true;
|
||||
const tl = state.capturedTimeline;
|
||||
if (tl) tl.pause();
|
||||
pauseTimelineIfPossible(tl);
|
||||
seekTimelineAndAdapters(state.currentTime);
|
||||
runAdapters("pause");
|
||||
if (options?.keepPlaying && wasPlaying) {
|
||||
@@ -2549,7 +2579,8 @@ export function initSandboxRuntimeModular(): void {
|
||||
) => {
|
||||
try {
|
||||
const suppressEvents = options?.suppressEvents === true;
|
||||
timeline.pause();
|
||||
// Guarded: a partial timeline without pause() must still get its seek.
|
||||
pauseTimelineIfPossible(timeline);
|
||||
if (typeof timeline.totalTime === "function") {
|
||||
timeline.totalTime(timeSeconds, suppressEvents);
|
||||
} else {
|
||||
@@ -2783,7 +2814,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
player._timeline = state.capturedTimeline;
|
||||
}
|
||||
if (state.capturedTimeline && state.capturedTimeline !== prevTimeline) {
|
||||
state.capturedTimeline.pause();
|
||||
pauseTimelineIfPossible(state.capturedTimeline);
|
||||
}
|
||||
const dur = getSafeTimelineDurationSeconds(state.capturedTimeline, 0);
|
||||
if (dur > 0) clock.setDuration(dur);
|
||||
@@ -2996,7 +3027,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
if (state.capturedTimeline) {
|
||||
const dur = getSafeTimelineDurationSeconds(state.capturedTimeline, 0);
|
||||
if (dur > 0) clock.setDuration(dur);
|
||||
state.capturedTimeline.pause();
|
||||
pauseTimelineIfPossible(state.capturedTimeline);
|
||||
}
|
||||
|
||||
installPositionEditsSeekReapply(window as Window & typeof globalThis);
|
||||
|
||||
Reference in New Issue
Block a user