Skip to content

feat(lint,player): fast-capture lint rule + player media sync#1921

Open
vanceingalls wants to merge 1 commit into
de2-05-producerfrom
de2-06-lint-player
Open

feat(lint,player): fast-capture lint rule + player media sync#1921
vanceingalls wants to merge 1 commit into
de2-05-producerfrom
de2-06-lint-player

Conversation

@vanceingalls

@vanceingalls vanceingalls commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Lint rule + player media sync (stack 6/6 — stack tip)

Final layer. This is the complete feature — the tip is tsc-clean on engine + producer with 231 tests passing (the intermediate PRs 1–5 are package-split for review and don't each compile in isolation).

packages/lint/src/rules/composition.ts (+36)

Lint rule for fast-capture composition requirements (e.g. warn when a composition is missing data-duration — the zero-duration case that fails capture on both paths). Test: composition.test.ts.

packages/player/ (+ ~85)

  • hyperframes-player.ts, controls.ts — player-side sync alignment with the fast-capture seek protocol.
  • parent-media.ts — parent-frame media coordination; test added.

Verification (whole feature, at this tip)

  • tsc --noEmit: 0 errors (engine + producer)
  • engine drawElementService + frameCapture + config + static-dedup: 145 pass, 3 skip
  • producer htmlCompiler (incl. bg-image localizer): 86 pass, 0 fail

Context

Fresh stack rebased onto current main, superseding the stale #1295 (drawElement fast-capture) + #1444 (worker-encode). During the rebase, main was found to have independently shipped static-frame dedup; this stack unifies onto main's implementation and extends it to the drawElement/worker path (see #1919 for the reviewer note). All corpus "damaged" comps were traced to unavailable/corrupt source assets, not drawElement regressions.


Stack: #1916#1917#1918#1919#1920#1921 (here, tip).

🤖 Generated with Claude Code

@miga-heygen miga-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — feat(lint,player): fast-capture lint rule + player media sync

VERDICT: APPROVE (comment-only stamp per process)

Summary

Two well-scoped additions: a lint rule that saves authors 45 seconds per render by detecting missing data-no-timeline, and audible-scrub support in the player so users hear audio as they drag the scrubber. Both are cleanly implemented with good test coverage and careful attention to edge cases.


Lint rule: missing_data_no_timeline

The rule catches compositions that will stall 45s waiting for a GSAP timeline that never registers. Solid design choices:

  1. Boolean attribute detection: readAttr only handles valued attrs (attr="val"), so the rule correctly strips quoted values from the tag then uses a custom regex (?:^|\s)data-no-timeline(?=[\s>=/]|$) to match the boolean form. The lookahead correctly rejects hyphenated variants like data-no-timeline-start (test cases cover both regressions).

  2. External script bail-out: Can't statically scan <script src="..."> for window.__timelines registration, so the rule exits early. Right call — false positives in a lint rule erode trust fast.

  3. Sub-composition skip: Sub-compositions don't own the timeline poll, so the isSubComposition guard is correct.

  4. window.__timelines[ detection: The scripts.some(s => s.content.includes("window.__timelines[")) heuristic is simple. It would miss aliased patterns (const tl = window.__timelines; tl[id] = ...) but the comment rightly notes this is a static heuristic and the data-no-timeline escape hatch exists for outliers. Acceptable trade-off for a warning-severity rule.

One minor observation — the regex test is case-insensitive (/i) which is technically correct for HTML attribute names, though data-no-timeline will always be lowercase in practice. No issue, just belt-and-suspenders.

Player media sync: audible scrub

Clean three-layer implementation:

  1. Controls (controls.ts): onScrubStart/onScrubEnd callbacks fire on mouse/touch events. The if (scrubbing) guard on mouseup/touchend prevents double-fire — good defensive pattern. Both mouse and touch paths are covered symmetrically.

  2. Player (hyperframes-player.ts): _scrubbing flag gates seek() between scrubAll (audible, playing) and pauseAll + seekAll (silent). The onScrubEnd handler calls this.seek(this._currentTime) which re-enters the non-scrubbing path and settles the audio to silence. Clean state transition.

  3. ParentMediaManager (parent-media.ts): scrubAll mirrors seekAll structurally but calls _playEntry for in-window proxies instead of just positioning. Out-of-window proxies are paused. _refreshEntryBounds is called per entry (matching seekAll's pattern), so live trim/move edits are respected.

The comment about muted proxies ("play() is a no-op for output") is accurate — muted elements still play but produce no audio, which is the correct behavior.

Test coverage

  • Lint: 8 test cases covering the happy path, boolean attribute, script registration, no-root-id, attribute-value false positive regression, hyphenated-variant regression, sub-composition, and external scripts. The two regression tests (attribute-value substring, hyphenated variant) are particularly valuable — they catch real regex pitfalls.

  • Player: scrubAll test verifies in-window proxy plays at the correct relative time and out-of-window proxy is paused. The test uses makeFakeAudio(true) for the in-window proxy (initially paused) to confirm scrub starts playback, and makeFakeAudio(false) for out-of-window (initially playing) to confirm it gets paused. Directionally correct.

Ponytail

No issues. Both features are self-contained additions. The lint rule defaults to warning severity (won't break CI). The player changes are additive — the scrub callbacks are optional (?. invocations), and the _scrubbing flag only affects the audioOwner === "parent" path, so compositions without parent-owned audio are unaffected.

Findings

No blocking issues found. Ship it.


Diff: +185/-9 across 6 files

Review by Miga

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stamped current head eb7ffe1f after checking existing reviews/comments and current checks. Miga's review is approve-intent, and Tai's PR-body wording note is non-blocking.

No required checks are reported for this branch. There is an optional perf matrix failure visible on the PR, but the concrete lint/player changes remain review-clean and the required gate is not red.

Verdict: APPROVE
Reasoning: The lint rule and player media-sync changes are scoped, tested, and have no open blocker in the current review record.

  • Magi

@terencecho terencecho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM per my per-PR review + re-review on the Slack thread. Non-blocking nits I raised (where present) are fair as post-merge follow-ups.

— Review by tai (pr-review)

vanceingalls added a commit that referenced this pull request Jul 6, 2026
## drawElement fast-capture — config + CLI flag (stack 1/6)

Foundation layer for the drawElement fast-capture feature: the config surface and CLI/Docker plumbing that the rest of the stack builds on.

### What this adds
- **`packages/engine/src/config.ts`** — new config fields for fast capture: `useDrawElement` / `enableDrawElementWorkerEncode` (macOS-GPU `drawElementImage` capture + worker-offloaded JPEG encode), resolved from env in `resolveConfig` (env `HF_DE_WORKER_ENCODE`). Wired alongside main's existing `staticFrameDedup` (unified downstream in 4/6).
- **`packages/cli/src/commands/render.ts`** — `--experimental-fast-capture` flag → sets `experimentalFastCapture`; `--debug` passthrough.
- **`packages/cli/src/utils/dockerRunArgs.ts`** — pass the fast-capture env through to the container.
- **`.github/workflows/fast-video-validation.yml`** — CI job validating fast-capture renders.
- `.oxlintrc.json` / `.fallowrc.jsonc` — ignore-pattern housekeeping for the new paths.

### Notes
- Config-only + entrypoint; no capture behavior yet (that's 2/6–4/6).
- Tests: `config.test.ts`, `dockerRunArgs.test.ts` added.

---
**Stack (drawElement fast-capture, rebased onto current `main`, supersedes #1295 + #1444):**
1. **#1916 config + CLI** ← you are here
2. #1917 drawElementImage capture service
3. #1918 3D projection + compositor-effect risk gate
4. #1919 frame-capture core (routing, worker-encode, static-dedup unification)
5. #1920 producer render stages + remote bg-image localizer
6. #1921 lint rule + player media sync

⚠️ Intermediate PRs (1–5) are split by package boundary for review and **do not each compile independently** (cross-file deps); the complete feature is green at the stack tip (#1921) — tsc-clean on engine + producer, 231 tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.

4 participants