@hyperframes/engine 0.8.204 → 0.8.205

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3337,8 +3337,8 @@ async function captureAccumulatedFrame(session, frameIndex, absFrameIndex, plan)
3337
3337
  await seekPageTimeline(session.page, frameTime, undefined);
3338
3338
  const totals = { seekMs: Date.now() - eventfulSeekStart, beforeCaptureMs: 0, screenshotMs: 0 };
3339
3339
  if (session.onBeforeCapture) {
3340
- // The injector copies a video's own style only when its frame changes: inject at frameTime
3341
- // first so the held frame carries frameTime's style, not the first sample's.
3340
+ // Sub-samples leave a video's style alone: inject at frameTime first so the held frame
3341
+ // carries frameTime's style, not the first sample's.
3342
3342
  const injectStart = Date.now();
3343
3343
  session.captureStep = "injecting video frames";
3344
3344
  await session.onBeforeCapture(session.page, frameTime, frameTime);
@@ -194,9 +194,11 @@ export declare function ensureRenderFrameSiblings(page: Page): Promise<void>;
194
194
  * cache entry for a frame that never reached the page, which would otherwise
195
195
  * short-circuit the next inject at the same frameIndex and leave the host's
196
196
  * first visible frame blank.
197
+ *
198
+ * `restyleIds` keep their current picture and only re-copy the video's style.
197
199
  */
198
200
  export declare function injectVideoFramesBatch(page: Page, updates: Array<{
199
201
  videoId: string;
200
202
  dataUri: string;
201
- }>): Promise<string[]>;
203
+ }>, restyleIds?: readonly string[]): Promise<string[]>;
202
204
  export declare function syncVideoFrameVisibility(page: Page, activeVideoIds: string[]): Promise<void>;
@@ -548,13 +548,21 @@ export async function ensureRenderFrameSiblings(page) {
548
548
  * cache entry for a frame that never reached the page, which would otherwise
549
549
  * short-circuit the next inject at the same frameIndex and leave the host's
550
550
  * first visible frame blank.
551
+ *
552
+ * `restyleIds` keep their current picture and only re-copy the video's style.
551
553
  */
552
- export async function injectVideoFramesBatch(page, updates) {
553
- if (updates.length === 0)
554
+ export async function injectVideoFramesBatch(page, updates, restyleIds = []) {
555
+ if (updates.length === 0 && restyleIds.length === 0)
554
556
  return [];
555
- return withVideoFrameSources(page, updates, (resolved) => injectResolvedVideoFramesBatch(page, resolved));
557
+ return withVideoFrameSources(page, updates, (resolved) => injectResolvedVideoFramesBatch(page, resolved, restyleIds));
556
558
  }
557
- async function injectResolvedVideoFramesBatch(page, updates) {
559
+ async function injectResolvedVideoFramesBatch(page, updates, restyleIds) {
560
+ // Build the sibling id with core's function rather than a template here,
561
+ // so the id the readers look up has exactly one definition.
562
+ const items = [
563
+ ...updates,
564
+ ...restyleIds.map((videoId) => ({ videoId })),
565
+ ].map((item) => ({ ...item, frameId: renderFrameIdForRenderId(item.videoId) }));
558
566
  return await page.evaluate(
559
567
  // fallow-ignore-next-line complexity
560
568
  async (items, visualProperties, colorGradingSourceHiddenAttr) => {
@@ -615,6 +623,9 @@ async function injectResolvedVideoFramesBatch(page, updates) {
615
623
  continue;
616
624
  let img = video.nextElementSibling;
617
625
  const hasImg = img !== null && img.classList.contains("__render_frame__");
626
+ const restyleOnly = item.dataUri === undefined;
627
+ if (restyleOnly && !hasImg)
628
+ continue;
618
629
  if (isVisualAncestorHidden(video)) {
619
630
  // Don't paint a frame over a hidden host — if an existing replacement
620
631
  // <img> is still around from when the host was visible, hide it so it
@@ -700,7 +711,7 @@ async function injectResolvedVideoFramesBatch(page, updates) {
700
711
  // Publish after full decode so raster cannot race it with a reduced JPEG decode.
701
712
  img.style.setProperty("visibility", "hidden", "important");
702
713
  img.decoding = "sync";
703
- if (img.getAttribute("src") !== item.dataUri) {
714
+ if (item.dataUri !== undefined && img.getAttribute("src") !== item.dataUri) {
704
715
  if (item.crossOrigin)
705
716
  img.crossOrigin = item.crossOrigin;
706
717
  else
@@ -718,23 +729,21 @@ async function injectResolvedVideoFramesBatch(page, updates) {
718
729
  // GSAP-controlled value.
719
730
  video.style.setProperty("visibility", "hidden", "important");
720
731
  video.style.setProperty("pointer-events", "none", "important");
721
- injectedIds.push(item.videoId);
732
+ if (!restyleOnly)
733
+ injectedIds.push(item.videoId);
722
734
  }
723
735
  if (pendingDecodes.length > 0) {
724
736
  await Promise.all(pendingDecodes);
725
737
  }
726
738
  for (const img of imagesToPublish)
727
739
  img.style.visibility = "visible";
728
- if (injectedIds.length > 0) {
740
+ if (imagesToPublish.length > 0) {
729
741
  const redraw = window
730
742
  .__hf?.colorGrading?.redraw;
731
743
  redraw?.();
732
744
  }
733
745
  return injectedIds;
734
- },
735
- // Build the sibling id with core's function rather than a template here,
736
- // so the id the readers look up has exactly one definition.
737
- updates.map((update) => ({ ...update, frameId: renderFrameIdForRenderId(update.videoId) })), [...MEDIA_VISUAL_STYLE_PROPERTIES], COLOR_GRADING_SOURCE_HIDDEN_ATTR);
746
+ }, items, [...MEDIA_VISUAL_STYLE_PROPERTIES], COLOR_GRADING_SOURCE_HIDDEN_ATTR);
738
747
  }
739
748
  export async function syncVideoFrameVisibility(page, activeVideoIds) {
740
749
  await page.evaluate(
@@ -159,14 +159,19 @@ export function createVideoFrameInjector(frameLookup, config) {
159
159
  }
160
160
  }
161
161
  const updates = [];
162
+ const restyleIds = [];
162
163
  const activeIds = new Set();
164
+ const isBlurSubSample = heldVideoTime !== undefined && heldVideoTime !== time;
163
165
  if (activePayloads.size > 0) {
164
166
  const pendingReads = [];
165
167
  for (const [videoId, payload] of activePayloads) {
166
168
  activeIds.add(videoId);
167
169
  const lastFrameIndex = lastInjectedFrameByVideo.get(videoId);
168
- if (lastFrameIndex === payload.frameIndex)
170
+ if (lastFrameIndex === payload.frameIndex) {
171
+ if (!isBlurSubSample)
172
+ restyleIds.push(videoId);
169
173
  continue;
174
+ }
170
175
  pendingReads.push(frameCache
171
176
  .get(payload.framePath)
172
177
  .then((dataUri) => ({ videoId, dataUri, frameIndex: payload.frameIndex })));
@@ -179,14 +184,14 @@ export function createVideoFrameInjector(frameLookup, config) {
179
184
  }
180
185
  }
181
186
  await syncVideoFrameVisibility(page, Array.from(activeIds));
182
- if (updates.length > 0) {
187
+ if (updates.length > 0 || restyleIds.length > 0) {
183
188
  // Only record cache entries for videos the page actually painted.
184
189
  // injectVideoFramesBatch skips any video whose visual ancestor is
185
190
  // hidden (sub-comp host out-of-window) and returns the subset of ids
186
191
  // it really wrote — recording the rest would short-circuit the next
187
192
  // call at the same frameIndex and leave the host's first visible
188
193
  // frame blank.
189
- const injectedIds = new Set(await injectVideoFramesBatch(page, updates.map((u) => ({ videoId: u.videoId, dataUri: u.dataUri }))));
194
+ const injectedIds = new Set(await injectVideoFramesBatch(page, updates.map((u) => ({ videoId: u.videoId, dataUri: u.dataUri })), restyleIds));
190
195
  for (const update of updates) {
191
196
  if (injectedIds.has(update.videoId)) {
192
197
  lastInjectedFrameByVideo.set(update.videoId, update.frameIndex);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/engine",
3
- "version": "0.8.204",
3
+ "version": "0.8.205",
4
4
  "description": "Seekable web page to video rendering engine (Puppeteer + FFmpeg)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -50,8 +50,8 @@
50
50
  "linkedom": "^0.18.12",
51
51
  "puppeteer": "^25.8.0",
52
52
  "puppeteer-core": "^25.8.0",
53
- "@hyperframes/parsers": "^0.8.204",
54
- "@hyperframes/core": "^0.8.204"
53
+ "@hyperframes/parsers": "^0.8.205",
54
+ "@hyperframes/core": "^0.8.205"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/node": "^25.0.10",