Skip to content

Fork patch: Discover video window from GSAP opacity - #9

Draft
valeriangalliat wants to merge 5 commits into
mainfrom
patch/discover-video-opacity
Draft

valeriangalliat wants to merge 5 commits into
mainfrom
patch/discover-video-opacity

Conversation

@valeriangalliat

Copy link
Copy Markdown
Collaborator

🤖 AI agent: Arcade fork patch. Do not merge.

This draft shows patch/discover-video-opacity against main, the upstream mirror, so the patch has one page for its diff, its revisions and its reason. Arcade's /hyperframes-update rebases the branch onto main and cherry-picks it onto arcade; this PR is not part of that flow. When a rebuild changes the patch, its git range-diff against the previous sync/<date>/discover-video-opacity tag is posted as a comment.

Why

Visibility discovery sampled the opacity of .scene, which is usually 1, so the probe injected an auto-start video for its host's whole window. The patch reads the window from the video's own GSAP opacity instead:

  • It seeks through __hf.seek so nested GSAP updates, and binds mediaStart to the first visible frame. It never moves the start earlier than the host mapping.
  • When visibility moves the start later on a clip with a static src and no in-point, it adds the delta to mediaStart so encode keeps the host clock.
  • It stamps the opacity out-point onto the compiled video duration, so an in-flow overlay no longer goes display: none at source EOF.
  • It writes the slot ends back through the timing compiler (extendVideoSlotEnds).

Known follow-up, unverified: the mediaStart += delta bump ignores playback rate.

Upstream

Kept local. The upstream check errors with media_missing_data_start, this fallback is undocumented, and RFC heygen-com#2057 was not planned.

Drop when

Arcade's generator emits timed video (ARC-13573).

Arcade

ARC-14877 · corpus scenes ArcadeHQ/arcade#10036

@valeriangalliat
valeriangalliat force-pushed the patch/discover-video-opacity branch from 061a8ae to 9f4d89b Compare October 24, 2026 23:23
@valeriangalliat

valeriangalliat commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

🤖 AI agent: sync/2026-10-24 (tags moved the same day): rebased onto upstream 2e397dba5 (v0.8.194). Import conflict in probeStage.ts with upstream #5528: merged both import lists. New commit: heygen-com#5528 keeps playback rates on discovered media, so when opacity raises an auto-start clip's start, mediaStart now advances by the source time the clip played (sourceTimeAt) and a rate lane shifts to the new start, instead of assuming 1x. Two new probe tests fail without it.

First pass on this date, onto 722481214 (0.8.193): extendVideoSlotEnds rewritten on upstream replaceOpeningTags: upstream heygen-com#5156 removed maskInertRegions, so the old commit replayed without a conflict and threw a ReferenceError (caught by 5 probe tests). Probe log line follows upstream heygen-com#5414 wording. screenshotService.ts takes upstream; the patch net change there was nothing.

The range-diff below is against sync/2026-10-05.

git range-diff sync/2026-10-05/base..sync/2026-10-05/discover-video-opacity sync/2026-10-24/base..sync/2026-10-24/discover-video-opacity
1:  31e00ab63 = 1:  47872e64f fix(producer): window auto-start video to GSAP opacity, not .scene
2:  64af6d3e6 ! 2:  37fbe2858 fix(producer): keep auto-start file clock when opacity raises start
    @@ packages/producer/src/services/render/stages/probeStage.ts: export async functio
     +              video.mediaStart += raised;
     +            }
                  log.info(
    --              `[Probe] Runtime video discovery: ${video.id} visible ${win.visibleStart.toFixed(2)}s–${win.visibleEnd.toFixed(2)}s`,
    -+              `[Probe] Runtime video discovery: ${video.id} visible ${win.visibleStart.toFixed(2)}s–${win.visibleEnd.toFixed(2)}s → ${video.start.toFixed(2)}s–${video.end.toFixed(2)}s`,
    +-              `[Probe] Runtime video discovery: ${video.id} visible ${win.visibleStart.toFixed(2)}s to ${win.visibleEnd.toFixed(2)}s`,
    ++              `[Probe] Runtime video discovery: ${video.id} visible ${win.visibleStart.toFixed(2)}s to ${win.visibleEnd.toFixed(2)}s, placed ${video.start.toFixed(2)}s to ${video.end.toFixed(2)}s`,
                  );
                }
              }
3:  1f7e83af4 ! 3:  472398cdf fix(producer): stamp opacity out-point onto compiled video duration
    @@ Commit message
         Capture still used the file-bounded data-duration, so an in-flow overlay
         went display:none at source EOF and the underlay showed through.
     
    - ## packages/engine/src/services/screenshotService.ts ##
    -@@ packages/engine/src/services/screenshotService.ts: export async function injectVideoFramesBatch(
    -           img.style.top = `${offsetTop}px`;
    -           img.style.right = "auto";
    -           img.style.bottom = "auto";
    --          img.style.width = `${offsetWidth}px`;
    --          img.style.height = `${offsetHeight}px`;
    -+          // display:none (runtime hid a file-bounded leaf) reports a 0 box.
    -+          // Keep the last painted overlay size so a short clip can hold.
    -+          if (offsetWidth > 0 && offsetHeight > 0) {
    -+            img.style.width = `${offsetWidth}px`;
    -+            img.style.height = `${offsetHeight}px`;
    -+          }
    -         }
    -         img.style.objectFit = computedStyle.objectFit;
    -         img.style.objectPosition = computedStyle.objectPosition;
    -
      ## packages/producer/src/services/render/stages/probeStage.test.ts ##
     @@ packages/producer/src/services/render/stages/probeStage.test.ts: function makeProbeInput(overrides: {
        };
    @@ packages/producer/src/services/render/stages/probeStage.test.ts: describe("runPr
        it("does not let visibility pull start earlier than the host-mapped start", async () => {
     
      ## packages/producer/src/services/render/stages/probeStage.ts ##
    -@@ packages/producer/src/services/render/stages/probeStage.ts: import {
    -   isTransientBrowserError,
    -   probeBeginFrameLiveness,
    - } from "@hyperframes/engine";
    --import { fpsToNumber } from "@hyperframes/core";
    -+import { fpsToNumber, MEDIA_RENDER_ID_ATTR } from "@hyperframes/core";
    - import { extractMediaSrcMutations } from "@hyperframes/parsers";
    - import type { CompiledComposition } from "../../htmlCompiler.js";
    - import {
     @@ packages/producer/src/services/render/stages/probeStage.ts: export function hasAutoStartVideos(html: string): boolean {
        return document.querySelector("video[data-hf-auto-start]") !== null;
      }
    @@ packages/producer/src/services/render/stages/probeStage.ts: export async functio
     +              mediaStart: video.mediaStart,
     +            });
                  log.info(
    -               `[Probe] Runtime video discovery: ${video.id} visible ${win.visibleStart.toFixed(2)}s–${win.visibleEnd.toFixed(2)}s → ${video.start.toFixed(2)}s–${video.end.toFixed(2)}s`,
    +               `[Probe] Runtime video discovery: ${video.id} visible ${win.visibleStart.toFixed(2)}s to ${win.visibleEnd.toFixed(2)}s, placed ${video.start.toFixed(2)}s to ${video.end.toFixed(2)}s`,
                  );
                }
              }
4:  061a8aeef ! 4:  f87a27157 refactor(core): stamp opacity slot ends through the timing compiler
    @@ packages/core/src/compiler/timingCompiler.ts: import {
      
      export interface UnresolvedElement {
     @@ packages/core/src/compiler/timingCompiler.ts: export function extractResolvedMedia(html: string): ResolvedMediaElement[] {
    -  * For each resolution, replaces the declared duration with the clamped value
    -  * and recomputes data-end accordingly.
    -  */
    +   return resolved;
    + }
    + 
     +export type VideoSlotEnd = {
     +  id: string;
     +  end: number;
    @@ packages/core/src/compiler/timingCompiler.ts: export function extractResolvedMed
     + */
     +export function extendVideoSlotEnds(html: string, slots: readonly VideoSlotEnd[]): string {
     +  if (slots.length === 0) return html;
    -+  const { masked, restore } = maskInertRegions(html);
     +  const byId = new Map(slots.map((slot) => [slot.id, slot]));
    -+  const next = masked.replace(/<video\b[^>]*>/gi, (tag) => {
    ++  return replaceOpeningTags(html, ["video"], (tag) => {
     +    const id = getAttr(tag, MEDIA_RENDER_ID_ATTR) ?? getAttr(tag, "id");
     +    if (!id) return tag;
     +    const slot = byId.get(id);
    @@ packages/core/src/compiler/timingCompiler.ts: export function extractResolvedMed
     +      String(slot.duration),
     +    );
     +  });
    -+  return restore(next);
     +}
     +
    - export function clampDurations(html: string, clamps: ResolvedDuration[]): string {
    -   for (const { id, duration } of clamps) {
    -     html = replaceIdTags(html, id, (tag) => {
    + /**
    +  * Clamp existing data-duration and data-end on media elements.
    +  * For each resolution, replaces the declared duration with the clamped value
     
      ## packages/core/src/index.ts ##
     @@ packages/core/src/index.ts: export {
    @@ packages/core/src/index.ts: export {
      
      export {
     
    - ## packages/engine/src/services/screenshotService.ts ##
    -@@ packages/engine/src/services/screenshotService.ts: export async function injectVideoFramesBatch(
    -           img.style.top = `${offsetTop}px`;
    -           img.style.right = "auto";
    -           img.style.bottom = "auto";
    --          // display:none (runtime hid a file-bounded leaf) reports a 0 box.
    --          // Keep the last painted overlay size so a short clip can hold.
    --          if (offsetWidth > 0 && offsetHeight > 0) {
    --            img.style.width = `${offsetWidth}px`;
    --            img.style.height = `${offsetHeight}px`;
    --          }
    -+          img.style.width = `${offsetWidth}px`;
    -+          img.style.height = `${offsetHeight}px`;
    -         }
    -         img.style.objectFit = computedStyle.objectFit;
    -         img.style.objectPosition = computedStyle.objectPosition;
    -
      ## packages/producer/src/services/render/stages/probeStage.test.ts ##
     @@ packages/producer/src/services/render/stages/probeStage.test.ts: function makeProbeInput(overrides: {
        };
    @@ packages/producer/src/services/render/stages/probeStage.test.ts: function makePr
     
      ## packages/producer/src/services/render/stages/probeStage.ts ##
     @@ packages/producer/src/services/render/stages/probeStage.ts: import {
    -   isTransientBrowserError,
    -   probeBeginFrameLiveness,
      } from "@hyperframes/engine";
    --import { fpsToNumber, MEDIA_RENDER_ID_ATTR } from "@hyperframes/core";
    -+import {
    -+  MEDIA_RENDER_ID_ATTR,
    + import {
    +   MEDIA_RENDER_ID_ATTR,
     +  extendVideoSlotEnds,
    -+  fpsToNumber,
    +   fpsToNumber,
    +   normalizeRateSpec,
    +   readMediaStart,
    +   shiftRateLane,
    +   sourceTimeAt,
    +   timeAtSourceTime,
     +  type VideoSlotEnd,
    -+} from "@hyperframes/core";
    + } from "@hyperframes/core";
      import { extractMediaSrcMutations } from "@hyperframes/parsers";
    - import type { CompiledComposition } from "../../htmlCompiler.js";
    - import {
    + import type { BrowserMediaElement, CompiledComposition } from "../../htmlCompiler.js";
     @@ packages/producer/src/services/render/stages/probeStage.ts: export function hasAutoStartVideos(html: string): boolean {
        return document.querySelector("video[data-hf-auto-start]") !== null;
      }
    @@ packages/producer/src/services/render/stages/probeStage.ts: export async functio
     +              slotEnds.push({ id: video.id, end: video.end, duration });
     +            }
                  log.info(
    -               `[Probe] Runtime video discovery: ${video.id} visible ${win.visibleStart.toFixed(2)}s–${win.visibleEnd.toFixed(2)}s → ${video.start.toFixed(2)}s–${video.end.toFixed(2)}s`,
    +               `[Probe] Runtime video discovery: ${video.id} visible ${win.visibleStart.toFixed(2)}s to ${win.visibleEnd.toFixed(2)}s, placed ${video.start.toFixed(2)}s to ${video.end.toFixed(2)}s`,
                  );
                }
              }
-:  --------- > 5:  dc420a073 fix(producer): advance a sped-up clip's file clock by the source time it played

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Edit accuracy: accurate 271 (base branch 271), smooth 168 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (1)

  • seqplainkeys-none-pct-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true

Keep-local: visibility discovery sampled .scene (usually 1), so probe
injected the file for the whole host window. Seek via __hf.seek so
nested GSAP updates, and bind mediaStart to the first visible frame.
Never pull start earlier than host-map.
Do not upstream (wizard-emit untimed video).
When visibility moves start later and the clip has a static src and no
in-point, add the delta to mediaStart so encode keeps the host clock.
Empty-src stays bound to the reveal. Host-mapped start is a floor;
visibility owns the out-point. Authored mediaStart stays on the reveal.
Capture still used the file-bounded data-duration, so an in-flow overlay
went display:none at source EOF and the underlay showed through.
Move the hold write-back onto extendVideoSlotEnds and drop the injector
0-size special case so probe only applies one {id,end,duration} row.
… it played

When opacity raises an auto-start clip's start, the encoder's in-point moved
by the timeline delta, as if the clip plays at 1x. Integrate the clip's rate
over that delta instead, and shift a rate lane to the new start.
@valeriangalliat
valeriangalliat force-pushed the patch/discover-video-opacity branch from 9f4d89b to dc420a0 Compare October 25, 2026 02:43

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant