feat(lint,player): fast-capture lint rule + player media sync#1921
feat(lint,player): fast-capture lint rule + player media sync#1921vanceingalls wants to merge 1 commit into
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
251add2 to
2b1624a
Compare
e30816f to
07e6ca3
Compare
miga-heygen
left a comment
There was a problem hiding this comment.
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:
-
Boolean attribute detection:
readAttronly 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 likedata-no-timeline-start(test cases cover both regressions). -
External script bail-out: Can't statically scan
<script src="...">forwindow.__timelinesregistration, so the rule exits early. Right call — false positives in a lint rule erode trust fast. -
Sub-composition skip: Sub-compositions don't own the timeline poll, so the
isSubCompositionguard is correct. -
window.__timelines[detection: Thescripts.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 thedata-no-timelineescape 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:
-
Controls (
controls.ts):onScrubStart/onScrubEndcallbacks fire on mouse/touch events. Theif (scrubbing)guard on mouseup/touchend prevents double-fire — good defensive pattern. Both mouse and touch paths are covered symmetrically. -
Player (
hyperframes-player.ts):_scrubbingflag gatesseek()betweenscrubAll(audible, playing) andpauseAll+seekAll(silent). TheonScrubEndhandler callsthis.seek(this._currentTime)which re-enters the non-scrubbing path and settles the audio to silence. Clean state transition. -
ParentMediaManager (
parent-media.ts):scrubAllmirrorsseekAllstructurally but calls_playEntryfor in-window proxies instead of just positioning. Out-of-window proxies are paused._refreshEntryBoundsis called per entry (matchingseekAll'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:
scrubAlltest verifies in-window proxy plays at the correct relative time and out-of-window proxy is paused. The test usesmakeFakeAudio(true)for the in-window proxy (initially paused) to confirm scrub starts playback, andmakeFakeAudio(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
07e6ca3 to
6894682
Compare
2b1624a to
74ec999
Compare
6894682 to
eb7ffe1
Compare
74ec999 to
09adbe8
Compare
miguel-heygen
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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)
## 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)
09adbe8 to
2b4e206
Compare
eb7ffe1 to
ac9ca09
Compare

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)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