clipwise 0.2.1 → 0.4.0

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.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { EventEmitter } from 'events';
2
3
 
3
4
  declare const StepActionSchema: z.ZodDiscriminatedUnion<"action", [z.ZodObject<{
4
5
  action: z.ZodLiteral<"navigate">;
@@ -492,6 +493,7 @@ declare const OutputConfigSchema: z.ZodObject<{
492
493
  height: z.ZodDefault<z.ZodNumber>;
493
494
  fps: z.ZodDefault<z.ZodNumber>;
494
495
  quality: z.ZodDefault<z.ZodNumber>;
496
+ preset: z.ZodOptional<z.ZodEnum<["social", "balanced", "archive"]>>;
495
497
  outputDir: z.ZodDefault<z.ZodString>;
496
498
  filename: z.ZodDefault<z.ZodString>;
497
499
  }, "strip", z.ZodTypeAny, {
@@ -502,12 +504,14 @@ declare const OutputConfigSchema: z.ZodObject<{
502
504
  quality: number;
503
505
  outputDir: string;
504
506
  filename: string;
507
+ preset?: "social" | "balanced" | "archive" | undefined;
505
508
  }, {
506
509
  format?: "gif" | "mp4" | "webm" | "png-sequence" | undefined;
507
510
  width?: number | undefined;
508
511
  height?: number | undefined;
509
512
  fps?: number | undefined;
510
513
  quality?: number | undefined;
514
+ preset?: "social" | "balanced" | "archive" | undefined;
511
515
  outputDir?: string | undefined;
512
516
  filename?: string | undefined;
513
517
  }>;
@@ -1146,6 +1150,7 @@ declare const ScenarioSchema: z.ZodObject<{
1146
1150
  height: z.ZodDefault<z.ZodNumber>;
1147
1151
  fps: z.ZodDefault<z.ZodNumber>;
1148
1152
  quality: z.ZodDefault<z.ZodNumber>;
1153
+ preset: z.ZodOptional<z.ZodEnum<["social", "balanced", "archive"]>>;
1149
1154
  outputDir: z.ZodDefault<z.ZodString>;
1150
1155
  filename: z.ZodDefault<z.ZodString>;
1151
1156
  }, "strip", z.ZodTypeAny, {
@@ -1156,12 +1161,14 @@ declare const ScenarioSchema: z.ZodObject<{
1156
1161
  quality: number;
1157
1162
  outputDir: string;
1158
1163
  filename: string;
1164
+ preset?: "social" | "balanced" | "archive" | undefined;
1159
1165
  }, {
1160
1166
  format?: "gif" | "mp4" | "webm" | "png-sequence" | undefined;
1161
1167
  width?: number | undefined;
1162
1168
  height?: number | undefined;
1163
1169
  fps?: number | undefined;
1164
1170
  quality?: number | undefined;
1171
+ preset?: "social" | "balanced" | "archive" | undefined;
1165
1172
  outputDir?: string | undefined;
1166
1173
  filename?: string | undefined;
1167
1174
  }>>;
@@ -1543,6 +1550,7 @@ declare const ScenarioSchema: z.ZodObject<{
1543
1550
  quality: number;
1544
1551
  outputDir: string;
1545
1552
  filename: string;
1553
+ preset?: "social" | "balanced" | "archive" | undefined;
1546
1554
  };
1547
1555
  steps: {
1548
1556
  actions: ({
@@ -1749,6 +1757,7 @@ declare const ScenarioSchema: z.ZodObject<{
1749
1757
  height?: number | undefined;
1750
1758
  fps?: number | undefined;
1751
1759
  quality?: number | undefined;
1760
+ preset?: "social" | "balanced" | "archive" | undefined;
1752
1761
  outputDir?: string | undefined;
1753
1762
  filename?: string | undefined;
1754
1763
  } | undefined;
@@ -1775,6 +1784,8 @@ interface CapturedFrame {
1775
1784
  width: number;
1776
1785
  height: number;
1777
1786
  };
1787
+ /** Device pixel ratio used during capture (1 = normal, 2 = Retina/HiDPI). */
1788
+ deviceScaleFactor?: number;
1778
1789
  stepName?: string;
1779
1790
  stepIndex?: number;
1780
1791
  actionType?: string;
@@ -1784,12 +1795,45 @@ interface ComposedFrame {
1784
1795
  index: number;
1785
1796
  buffer: Buffer;
1786
1797
  timestamp: number;
1798
+ /**
1799
+ * Present when buffer contains raw RGBA pixels (not PNG).
1800
+ * Allows the encoder to skip the PNG-decode step and consume pixels directly.
1801
+ */
1802
+ rawInfo?: {
1803
+ width: number;
1804
+ height: number;
1805
+ channels: 4;
1806
+ };
1807
+ }
1808
+ interface DedupStats {
1809
+ /** CDP로부터 수신한 원본 프레임 수 */
1810
+ received: number;
1811
+ /** 중복 제거 후 실제 저장된 고유 프레임 수 */
1812
+ stored: number;
1813
+ /** 중복으로 판단해 건너뛴 프레임 수 */
1814
+ skipped: number;
1787
1815
  }
1788
1816
  interface RecordingSession {
1789
1817
  scenario: Scenario;
1790
1818
  frames: CapturedFrame[];
1791
1819
  startTime: number;
1792
1820
  endTime?: number;
1821
+ /** 정적 프레임 중복 제거 통계 */
1822
+ dedupStats?: DedupStats;
1823
+ }
1824
+ /**
1825
+ * Handle returned by ClipwiseRecorder.recordToChannel().
1826
+ *
1827
+ * frameStream: async iterable of CapturedFrames as they are captured
1828
+ * (post-dedup, no FPS resampling — frames arrive at CDP capture rate).
1829
+ * Closes automatically when recording ends.
1830
+ *
1831
+ * done: resolves with the full RecordingSession (including FPS-resampled
1832
+ * frames) when recording has completely finished.
1833
+ */
1834
+ interface RecordingHandle {
1835
+ frameStream: AsyncIterable<CapturedFrame>;
1836
+ done: Promise<RecordingSession>;
1793
1837
  }
1794
1838
 
1795
1839
  declare class ClipwiseRecorder {
@@ -1804,11 +1848,16 @@ declare class ClipwiseRecorder {
1804
1848
  private currentStepIndex;
1805
1849
  private cursorPosition;
1806
1850
  private viewport;
1851
+ private deviceScaleFactor;
1807
1852
  private isCapturing;
1808
1853
  private targetFps;
1809
1854
  private cursorSpeed;
1810
1855
  private firstContentTimestamp;
1811
1856
  private pendingResponsePromises;
1857
+ private lastFrameSignature;
1858
+ private dedupStats;
1859
+ private frameChannel;
1860
+ private channelIndex;
1812
1861
  /**
1813
1862
  * Launch the browser and create a page with the scenario viewport.
1814
1863
  */
@@ -1826,6 +1875,26 @@ declare class ClipwiseRecorder {
1826
1875
  * Execute the full scenario with continuous capture and return a RecordingSession.
1827
1876
  */
1828
1877
  record(scenario: Scenario): Promise<RecordingSession>;
1878
+ /**
1879
+ * Start recording concurrently and return a RecordingHandle immediately.
1880
+ *
1881
+ * frameStream: yields CapturedFrames as each unique frame arrives from CDP
1882
+ * (post-dedup, sequential indices starting at 0, NO FPS resampling).
1883
+ * Closes when recording ends.
1884
+ *
1885
+ * done: resolves with the full RecordingSession (FPS-resampled) once
1886
+ * all steps have completed and the browser has been cleaned up.
1887
+ *
1888
+ * Use this with CanvasRenderer.composeStreamOnline() to overlap recording
1889
+ * time with composition time — total wall-clock ≈ max(recordingMs, composeMs).
1890
+ */
1891
+ recordToChannel(scenario: Scenario): RecordingHandle;
1892
+ /**
1893
+ * Build a single CapturedFrame from a RawFrame in real-time.
1894
+ * Used by recordToChannel() to emit frames as they arrive.
1895
+ * Cursor/click data reflects the timeline up to this moment.
1896
+ */
1897
+ private buildFrameOnline;
1829
1898
  /**
1830
1899
  * Wait for a given duration while forcing periodic repaints
1831
1900
  * so CDP screencast keeps sending frames even on static pages.
@@ -1868,12 +1937,41 @@ declare class ClipwiseRecorder {
1868
1937
  * Interpolate cursor position at a given timestamp using the cursor timeline.
1869
1938
  */
1870
1939
  private interpolateCursorAt;
1940
+ /**
1941
+ * Binary search: returns the index of the last entry whose timestamp <= target.
1942
+ * Assumes the array is sorted by timestamp in ascending order.
1943
+ */
1944
+ private binarySearchTimeline;
1871
1945
  /**
1872
1946
  * Clean up browser resources. Always called after recording.
1873
1947
  */
1874
1948
  cleanup(): Promise<void>;
1875
1949
  }
1876
1950
 
1951
+ /**
1952
+ * Pre-computed static layers that are identical for every frame in a session.
1953
+ *
1954
+ * backdropRaw: background gradient + shadow + watermark composited together at
1955
+ * output dimensions, stored as raw RGBA. Workers composite the per-frame
1956
+ * screenshot onto this buffer instead of re-generating the background SVG
1957
+ * and shadow SVG on every frame — eliminating ~3 PNG encode/decode cycles.
1958
+ *
1959
+ * browserChromePng: pre-rasterized browser chrome bar PNG. Applied via
1960
+ * Sharp's .extend() + .composite() in a single pipeline pass instead of the
1961
+ * current two-pass create-blank-canvas + composite pattern.
1962
+ *
1963
+ * Both are computed once per worker (first frame), then cached in memory for
1964
+ * all subsequent frames handled by that worker.
1965
+ */
1966
+ interface StaticLayers {
1967
+ backdropRaw: Buffer;
1968
+ backdropWidth: number;
1969
+ backdropHeight: number;
1970
+ /** Pre-rasterized browser chrome bar PNG. Null when device frame is disabled or not "browser". */
1971
+ browserChromePng: Buffer | null;
1972
+ /** Pixel height of the chrome bar (0 when browserChromePng is null). */
1973
+ browserChromeHeight: number;
1974
+ }
1877
1975
  interface FrameContext {
1878
1976
  zoomScale: number;
1879
1977
  clickProgress: number | null;
@@ -1881,26 +1979,18 @@ interface FrameContext {
1881
1979
  x: number;
1882
1980
  y: number;
1883
1981
  }>;
1982
+ /** When present, skip redundant per-frame SVG generation for background/watermark/device-frame. */
1983
+ staticLayers?: StaticLayers;
1884
1984
  }
1985
+
1885
1986
  declare class CanvasRenderer {
1886
1987
  private effects;
1887
1988
  private output;
1888
1989
  private steps;
1889
1990
  constructor(effects: EffectsConfig, output: OutputConfig, steps?: Step[]);
1890
1991
  /**
1891
- * Apply the full effects pipeline to a single captured frame.
1892
- *
1893
- * Pipeline order:
1894
- * 1. Device frame (browser chrome / mobile mockup)
1895
- * 2. Cursor highlight (Screen Studio glow)
1896
- * 3. Cursor trail
1897
- * 4. Cursor rendering
1898
- * 5. Click ripple effect (animated progress)
1899
- * 6. Keystroke HUD
1900
- * 7. Zoom (adaptive, cursor-following)
1901
- * 8. Background (padding, gradient, rounded corners)
1902
- * 9. Watermark overlay
1903
- * 10. Final resize
1992
+ * Apply the full effects pipeline to a single frame.
1993
+ * Delegates to the standalone composeFrame function.
1904
1994
  */
1905
1995
  composeFrame(frame: CapturedFrame, context?: Partial<FrameContext>): Promise<ComposedFrame>;
1906
1996
  /**
@@ -1909,22 +1999,96 @@ declare class CanvasRenderer {
1909
1999
  * Multi-pass approach:
1910
2000
  * Pass 1: Speed ramping (adjust frame set).
1911
2001
  * Pass 2: Calculate per-frame contexts (zoom, click, trail).
1912
- * Pass 3: Render each frame with effects.
2002
+ * Pass 3: Render frames in parallel using worker threads.
1913
2003
  * Pass 4: Apply scene transitions at step boundaries.
1914
2004
  */
1915
2005
  composeAll(frames: CapturedFrame[]): Promise<ComposedFrame[]>;
1916
2006
  /**
1917
- * Calculate per-frame rendering context (zoom, click progress, cursor trail, tilt).
2007
+ * Distribute frame composition across a pool of worker threads.
2008
+ * Workers process frames concurrently; results are collected in order.
2009
+ */
2010
+ private processWithWorkers;
2011
+ /**
2012
+ * Calculate per-frame rendering context (zoom scale, click progress, cursor trail).
1918
2013
  */
1919
2014
  private calculateFrameContexts;
1920
2015
  /**
1921
2016
  * Apply speed ramping: slow down near actions, speed up during idle.
1922
- * Returns a new frame array with frames duplicated or skipped.
1923
2017
  */
1924
2018
  private applySpeedRamp;
2019
+ /**
2020
+ * Returns true when no effect requires the full frame array upfront.
2021
+ *
2022
+ * When true, composeStreamOnline() can be used: frames are composited as they
2023
+ * arrive (no need to wait for all frames to be collected first).
2024
+ *
2025
+ * Currently the only blocking effect is speed ramp, which needs to scan all
2026
+ * frames to compute action-proximity indices. Zoom uses the window-based
2027
+ * calculateAdaptiveZoomInWindow() so it works with a rolling lookahead buffer.
2028
+ */
2029
+ canStreamOnline(): boolean;
2030
+ /**
2031
+ * Online streaming compose — accepts an AsyncIterable of frames (e.g. from
2032
+ * ClipwiseRecorder.recordToChannel()) and begins compositing immediately,
2033
+ * without waiting for all frames to be collected.
2034
+ *
2035
+ * Each frame is dispatched to the worker pool as soon as its zoom lookahead
2036
+ * window is satisfied (i.e. when frame i + transitionFrames has arrived).
2037
+ * This creates a natural pipeline: recording produces frames while workers
2038
+ * consume them in parallel.
2039
+ *
2040
+ * Requires canStreamOnline() === true (speedRamp must be disabled).
2041
+ * Transitions (step boundaries with transition: fade) are applied inline
2042
+ * using the same applyTransitionsToStream() logic as composeStream().
2043
+ */
2044
+ composeStreamOnline(source: AsyncIterable<CapturedFrame>): AsyncGenerator<ComposedFrame>;
2045
+ /**
2046
+ * Worker-pool online streaming: dispatches frame i to a worker as soon as
2047
+ * frame i + transitionFrames has arrived from the source.
2048
+ *
2049
+ * Uses a notify-on-progress pattern (same as streamWithWorkers) extended
2050
+ * with an intake coroutine that feeds the growing frames[] buffer.
2051
+ */
2052
+ private streamOnlineWithWorkers;
2053
+ /**
2054
+ * Stream frame composition — yields ComposedFrames as workers finish,
2055
+ * in display order, so the encoder can start before all frames are composed.
2056
+ *
2057
+ * Same 4-pass structure as composeAll():
2058
+ * Pass 1 & 2 run upfront (need the full frame set).
2059
+ * Pass 3 streams via the worker pool (ordered yield).
2060
+ * Pass 4 transitions are buffered inline and applied at step boundaries.
2061
+ */
2062
+ composeStream(frames: CapturedFrame[]): AsyncGenerator<ComposedFrame>;
2063
+ /**
2064
+ * Worker-pool streaming: dispatches frames to workers and yields results
2065
+ * in display order as soon as each frame is ready.
2066
+ *
2067
+ * Uses a notify-on-progress pattern to bridge event-driven workers
2068
+ * to an ordered AsyncGenerator without busy-polling.
2069
+ */
2070
+ private streamWithWorkers;
2071
+ /**
2072
+ * Sequential streaming fallback for small frame counts where worker
2073
+ * thread overhead would exceed the parallelism benefit.
2074
+ */
2075
+ private streamSequential;
2076
+ /**
2077
+ * Pre-compute [startIdx, endIdx] windows for every fade transition so that
2078
+ * applyTransitionsToStream can buffer only those frames.
2079
+ */
2080
+ private getTransitionWindows;
2081
+ /**
2082
+ * Wrap a ComposedFrame stream with inline transition buffering.
2083
+ *
2084
+ * Non-transition frames are yielded immediately.
2085
+ * Frames inside a fade window are held until both endpoints are available,
2086
+ * then the crossfade is applied and all window frames are flushed in order.
2087
+ * A pending map maintains global display order across window boundaries.
2088
+ */
2089
+ private applyTransitionsToStream;
1925
2090
  /**
1926
2091
  * Apply crossfade transitions at step boundaries where configured.
1927
- * Modifies the composed array in-place.
1928
2092
  */
1929
2093
  private applyTransitions;
1930
2094
  }
@@ -1935,26 +2099,130 @@ declare class CanvasRenderer {
1935
2099
  */
1936
2100
  declare function encodeGif(frames: ComposedFrame[], config: OutputConfig): Promise<Buffer>;
1937
2101
  /**
1938
- * Encode a sequence of composed frames into an MP4 file using ffmpeg.
1939
- * Writes frames as PNG sequence to a temp directory, then runs ffmpeg.
2102
+ * Encode a sequence of composed frames into an MP4 buffer.
2103
+ *
2104
+ * Uses FFmpeg stdin piping (raw video) to eliminate disk I/O overhead.
2105
+ * Encoder priority: hevc_videotoolbox (macOS HEVC HW) → h264_videotoolbox
2106
+ * (macOS H.264 HW) → libx264 (software fallback).
2107
+ *
2108
+ * Encoding quality is controlled by the output.preset field:
2109
+ * social — screen-recording quality, ~2–4 MB / 30s
2110
+ * balanced — high fidelity, ~4–8 MB / 30s
2111
+ * archive — near-lossless, uncapped bitrate
1940
2112
  *
1941
2113
  * Requires ffmpeg to be installed and available in PATH.
1942
2114
  */
1943
2115
  declare function encodeMp4(frames: ComposedFrame[], config: OutputConfig): Promise<Buffer>;
2116
+ /**
2117
+ * Streaming variant of encodeMp4 — accepts an AsyncIterable so frames can
2118
+ * be piped to FFmpeg as they arrive from the composition pipeline,
2119
+ * overlapping composition and encoding rather than waiting for all frames.
2120
+ */
2121
+ declare function encodeMp4Stream(frames: AsyncIterable<ComposedFrame>, config: OutputConfig): Promise<Buffer>;
1944
2122
  /**
1945
2123
  * Save a sequence of composed frames as individual PNG files.
1946
- * Returns an array of file paths for the saved images.
1947
2124
  */
1948
2125
  declare function savePngSequence(frames: ComposedFrame[], config: OutputConfig): Promise<string[]>;
1949
2126
 
2127
+ /**
2128
+ * Emitted by StreamingSession after each frame is composed.
2129
+ */
2130
+ interface PipelineProgress {
2131
+ /** Number of frames composed so far */
2132
+ composed: number;
2133
+ /** Total frames in the session */
2134
+ total: number;
2135
+ /** Completion percentage (0–100) */
2136
+ pct: number;
2137
+ }
2138
+ interface ConcurrentResult {
2139
+ /** Fully-encoded MP4 buffer. */
2140
+ buffer: Buffer;
2141
+ /** Full RecordingSession (FPS-resampled) returned when recording completed. */
2142
+ session: RecordingSession;
2143
+ }
2144
+ /**
2145
+ * ConcurrentSession overlaps recording and composition in the same process.
2146
+ *
2147
+ * While the recorder captures CDP screencast frames, the compositor begins
2148
+ * applying effects immediately — each frame is dispatched to the worker pool
2149
+ * as soon as its zoom lookahead window is satisfied.
2150
+ *
2151
+ * Total wall-clock time ≈ max(recordingMs, composeMs) instead of the sum.
2152
+ * Requires renderer.canStreamOnline() === true (speedRamp must be disabled).
2153
+ *
2154
+ * Emits 'progress' after each composed frame.
2155
+ * During recording the total is unknown (pct = -1); after recording ends and
2156
+ * composition finishes, a final event with pct = 100 is emitted.
2157
+ *
2158
+ * Usage:
2159
+ * const pipeline = new ConcurrentSession(recorder, scenario, renderer);
2160
+ * pipeline.on("progress", ({ composed, total, pct }) => {
2161
+ * spinner.text = total > 0
2162
+ * ? `Processing... ${composed}/${total} (${pct}%)`
2163
+ * : `Recording & composing... ${composed} frames`;
2164
+ * });
2165
+ * const { buffer, session } = await pipeline.run();
2166
+ */
2167
+ declare class ConcurrentSession extends EventEmitter {
2168
+ private readonly recorder;
2169
+ private readonly scenario;
2170
+ private readonly renderer;
2171
+ constructor(recorder: ClipwiseRecorder, scenario: Scenario, renderer: CanvasRenderer);
2172
+ /**
2173
+ * Start recording and compositing concurrently.
2174
+ * Returns when both recording and encoding are complete.
2175
+ */
2176
+ run(): Promise<ConcurrentResult>;
2177
+ }
2178
+ /**
2179
+ * StreamingSession bridges a recorded session to the compose+encode streaming
2180
+ * pipeline and emits fine-grained progress events.
2181
+ *
2182
+ * Emits:
2183
+ * 'progress' — after each frame is composed, with a PipelineProgress payload
2184
+ *
2185
+ * Usage:
2186
+ * const pipeline = new StreamingSession(session, renderer);
2187
+ * pipeline.on("progress", ({ composed, total, pct }) => {
2188
+ * spinner.text = `Composing & encoding... ${composed}/${total} (${pct}%)`;
2189
+ * });
2190
+ * const mp4Buffer = await pipeline.run();
2191
+ */
2192
+ declare class StreamingSession extends EventEmitter {
2193
+ private readonly session;
2194
+ private readonly renderer;
2195
+ constructor(session: RecordingSession, renderer: CanvasRenderer);
2196
+ /** Total frames in the underlying recording session. */
2197
+ get totalFrames(): number;
2198
+ /**
2199
+ * Run the compose → encode pipeline.
2200
+ *
2201
+ * Composes frames via the worker pool (Phase 1-B streaming, ordered yield),
2202
+ * forwarding each to FFmpeg as it completes. Emits a 'progress' event after
2203
+ * every composed frame so callers can update a spinner or progress bar.
2204
+ *
2205
+ * @returns The fully-encoded MP4 as a Buffer.
2206
+ */
2207
+ run(): Promise<Buffer>;
2208
+ }
2209
+
1950
2210
  /**
1951
2211
  * Calculate adaptive zoom scale based on proximity to click/action frames.
1952
2212
  * Zooms in smoothly near important actions, stays at 1.0 during idle.
1953
2213
  *
2214
+ * Scans only the ±transitionFrames influence window — frames outside that
2215
+ * range always produce 1.0 anyway, so scanning the full array is wasted work.
2216
+ * This reduces per-call cost from O(n) to O(transitionFrames).
2217
+ *
2218
+ * For bulk context calculation over many frames, prefer the lookup-based API:
2219
+ * buildZoomClickLookup() once → calculateAdaptiveZoomFromLookup() per frame
2220
+ * which achieves O(n + n·log k) instead of O(n·transitionFrames).
2221
+ *
1954
2222
  * @param frames - Array of frames with optional clickPosition
1955
2223
  * @param currentIndex - Index of the current frame
1956
2224
  * @param maxScale - Peak zoom scale
1957
- * @param transitionFrames - Number of frames for zoom-in/zoom-out transition
2225
+ * @param transitionFrames - Half-width of the zoom influence window (frames)
1958
2226
  * @returns Scale value for the current frame (1.0 = no zoom)
1959
2227
  */
1960
2228
  declare function calculateAdaptiveZoom(frames: Array<{
@@ -1963,6 +2231,42 @@ declare function calculateAdaptiveZoom(frames: Array<{
1963
2231
  y: number;
1964
2232
  } | null;
1965
2233
  }>, currentIndex: number, maxScale: number, transitionFrames: number): number;
2234
+ /**
2235
+ * Pre-extract the indices of all click frames in a single O(n) pass.
2236
+ * Pass the result to calculateAdaptiveZoomFromLookup() for O(log k) per-frame
2237
+ * zoom computation, instead of O(transitionFrames) per frame.
2238
+ *
2239
+ * @returns Sorted array of frame indices that carry a clickPosition.
2240
+ */
2241
+ declare function buildZoomClickLookup(frames: ReadonlyArray<{
2242
+ clickPosition: unknown;
2243
+ }>): number[];
2244
+ /**
2245
+ * Calculate adaptive zoom scale using a pre-built click index lookup.
2246
+ * Binary-searches the lookup for the nearest click — O(log k) per call.
2247
+ *
2248
+ * Use buildZoomClickLookup() once before iterating, then call this per frame.
2249
+ *
2250
+ * @param clickLookup - Sorted array of click frame indices from buildZoomClickLookup()
2251
+ * @param currentIndex - Index of the frame being evaluated
2252
+ */
2253
+ declare function calculateAdaptiveZoomFromLookup(clickLookup: readonly number[], currentIndex: number, maxScale: number, transitionFrames: number): number;
2254
+ /**
2255
+ * Calculate adaptive zoom scale using only a local window of frames.
2256
+ * Does NOT need the full frame array — only frames within
2257
+ * [currentIndex - transitionFrames, currentIndex + transitionFrames].
2258
+ *
2259
+ * This is the Phase 3-A compatible API: when composition runs concurrently
2260
+ * with recording, only the ±transitionFrames lookahead buffer needs to be
2261
+ * available before frame i can be composed.
2262
+ *
2263
+ * @param windowFrames - Slice of frames around currentIndex
2264
+ * @param windowStart - Absolute timeline index of windowFrames[0]
2265
+ * @param currentIndex - Absolute timeline index of the frame being composed
2266
+ */
2267
+ declare function calculateAdaptiveZoomInWindow(windowFrames: ReadonlyArray<{
2268
+ clickPosition: unknown;
2269
+ }>, windowStart: number, currentIndex: number, maxScale: number, transitionFrames: number): number;
1966
2270
  /**
1967
2271
  * Calculate pan offset to keep a focus point centered when zoomed in.
1968
2272
  * The offset defines the top-left corner of the visible crop region.
@@ -1982,38 +2286,50 @@ declare function lerpZoom(current: number, target: number, factor: number): numb
1982
2286
  type CursorEffect = EffectsConfig["cursor"];
1983
2287
  /**
1984
2288
  * Render a glowing highlight circle around the cursor (Screen Studio style).
2289
+ * @param dpr - Device pixel ratio (default 1). Scales position and size for HiDPI.
1985
2290
  */
1986
2291
  declare function renderCursorHighlight(frameBuffer: Buffer, position: {
1987
2292
  x: number;
1988
2293
  y: number;
1989
- }, config: CursorEffect, frameWidth: number, frameHeight: number): Promise<Buffer>;
2294
+ }, config: CursorEffect, frameWidth: number, frameHeight: number, dpr?: number): Promise<Buffer>;
1990
2295
  /**
1991
2296
  * Render a cursor trail (fading line segments following cursor path).
1992
2297
  * Each segment fades from transparent (oldest) to opaque (newest).
2298
+ * @param dpr - Device pixel ratio (default 1). Scales positions for HiDPI.
1993
2299
  */
1994
2300
  declare function renderCursorTrail(frameBuffer: Buffer, positions: Array<{
1995
2301
  x: number;
1996
2302
  y: number;
1997
- }>, config: CursorEffect, frameWidth: number, frameHeight: number): Promise<Buffer>;
2303
+ }>, config: CursorEffect, frameWidth: number, frameHeight: number, dpr?: number): Promise<Buffer>;
1998
2304
 
1999
2305
  type KeystrokeConfig = EffectsConfig["keystroke"];
2000
2306
  /**
2001
2307
  * Render a keystroke HUD overlay showing recently typed keys.
2002
2308
  * Inspired by KeyCastr / CursorClip.
2003
2309
  */
2004
- declare function renderKeystrokeHud(frameBuffer: Buffer, keystrokes: KeystrokeEvent[], frameTimestamp: number, config: KeystrokeConfig, frameWidth: number, frameHeight: number): Promise<Buffer>;
2310
+ declare function renderKeystrokeHud(frameBuffer: Buffer, keystrokes: KeystrokeEvent[], frameTimestamp: number, config: KeystrokeConfig, frameWidth: number, frameHeight: number, dpr?: number): Promise<Buffer>;
2005
2311
 
2312
+ type RawInfo = {
2313
+ width: number;
2314
+ height: number;
2315
+ channels: 4;
2316
+ };
2006
2317
  /**
2007
2318
  * Apply a crossfade transition between two frame buffers.
2008
2319
  * Uses raw pixel weighted averaging for accurate blending.
2009
2320
  *
2010
- * @param fromBuffer - The outgoing frame
2011
- * @param toBuffer - The incoming frame
2012
- * @param progress - 0 = fully "from", 1 = fully "to"
2013
- * @param width - Frame width
2014
- * @param height - Frame height
2321
+ * @param fromBuffer - The outgoing frame (PNG or raw RGBA)
2322
+ * @param toBuffer - The incoming frame (PNG or raw RGBA)
2323
+ * @param progress - 0 = fully "from", 1 = fully "to"
2324
+ * @param width - Frame width
2325
+ * @param height - Frame height
2326
+ * @param fromRawInfo - Pass when fromBuffer is raw RGBA
2327
+ * @param toRawInfo - Pass when toBuffer is raw RGBA
2015
2328
  */
2016
- declare function applyCrossfade(fromBuffer: Buffer, toBuffer: Buffer, progress: number, width: number, height: number): Promise<Buffer>;
2329
+ declare function applyCrossfade(fromBuffer: Buffer, toBuffer: Buffer, progress: number, width: number, height: number, fromRawInfo?: RawInfo, toRawInfo?: RawInfo): Promise<{
2330
+ buffer: Buffer;
2331
+ rawInfo: RawInfo;
2332
+ }>;
2017
2333
 
2018
2334
  type WatermarkConfig = EffectsConfig["watermark"];
2019
2335
  /**
@@ -2042,4 +2358,4 @@ interface ValidationResult {
2042
2358
  */
2043
2359
  declare function validateScenario(scenario: Scenario): ValidationResult;
2044
2360
 
2045
- export { CanvasRenderer, type CapturedFrame, ClipwiseRecorder, type ComposedFrame, type EffectsConfig, type FrameContext, type KeystrokeEvent, type OutputConfig, type RecordingSession, type Scenario, type Step, type StepAction, applyCrossfade, calculateAdaptiveZoom, calculatePanOffset, encodeGif, encodeMp4, lerpZoom, loadScenario, parseScenario, renderCursorHighlight, renderCursorTrail, renderKeystrokeHud, renderWatermark, savePngSequence, validateScenario };
2361
+ export { CanvasRenderer, type CapturedFrame, ClipwiseRecorder, type ComposedFrame, type ConcurrentResult, ConcurrentSession, type EffectsConfig, type FrameContext, type KeystrokeEvent, type OutputConfig, type PipelineProgress, type RecordingHandle, type RecordingSession, type Scenario, type Step, type StepAction, StreamingSession, applyCrossfade, buildZoomClickLookup, calculateAdaptiveZoom, calculateAdaptiveZoomFromLookup, calculateAdaptiveZoomInWindow, calculatePanOffset, encodeGif, encodeMp4, encodeMp4Stream, lerpZoom, loadScenario, parseScenario, renderCursorHighlight, renderCursorTrail, renderKeystrokeHud, renderWatermark, savePngSequence, validateScenario };