Skip to content

web-sdui: add frame-by-frame VideoEncoder export using WebCodecs#45

Merged
tejpratap46 merged 5 commits into
mainfrom
codex/add-video-generation-feature-with-videoencoder
May 13, 2026
Merged

web-sdui: add frame-by-frame VideoEncoder export using WebCodecs#45
tejpratap46 merged 5 commits into
mainfrom
codex/add-video-generation-feature-with-videoencoder

Conversation

@tejpratap46

@tejpratap46 tejpratap46 commented May 13, 2026

Copy link
Copy Markdown
Owner

Motivation

  • Provide an in-browser way to export the SDUI preview into a video by encoding each rendered frame with the WebCodecs VideoEncoder API.
  • Reuse the existing React preview rendering without reimplementing rendering logic by rasterizing the DOM per-frame.

Description

  • Added a previewRef prop to MotionCanvas and attached it to the preview container so the exporter can capture the rendered DOM (web-sdui/src/components/MotionCanvas.tsx).
  • Implemented a frame export pipeline in App.tsx including waitForNextPaint, DOM-to-canvas rasterization via SVG foreignObject (renderNodeToCanvas), and per-frame VideoFrame creation and encoding with VideoEncoder configured for vp8 (web-sdui/src/App.tsx).
  • Collected encoded chunks and packaged them into an IVF (VP8) file via a small IVF writer (downloadIvf) and triggered an automatic download after encoder.flush().
  • Added UI: Generate Video (VideoEncoder) button, encode status messages, and disabled-state while encoding; added local state flags isEncoding and encodeStatus.

Testing

  • Built the web app with cd web-sdui && npm run build, and the TypeScript + Vite build completed successfully.

Codex Task

Summary by CodeRabbit

  • New Features

    • Generate and download VP8-encoded videos of the live preview via a new "Generate Video" button; shows progress and status while running.
  • Improvements

    • Frame capture is synchronized with preview rendering for more accurate exports.
    • Preview area wired to support export operations and temporarily blocks frame controls during encoding.
  • Bug Fixes

    • Album art image requests adjusted to improve inlining and export reliability.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds browser-side video export using the WebCodecs VideoEncoder: the app captures preview frames via a ref, encodes them as VP8 into EncodedVideoChunks, assembles an IVF file, and provides a UI button to trigger download with encoding status.

Changes

VideoEncoder Integration

Layer / File(s) Summary
Component ref interface and wiring
web-sdui/src/components/MotionCanvas.tsx, web-sdui/src/App.tsx
MotionCanvasProps gains optional previewRef; the preview container attaches it via ref; App passes previewRef into MotionCanvas so the preview DOM can be captured.
Encoding state and refs
web-sdui/src/App.tsx
Import useRef and add isEncoding, encodeStatus, previewRef, and frameWaiterRef to track encoding progress and coordinate per-frame render readiness.
Video encoding pipeline
web-sdui/src/App.tsx
Adds helpers to wait for a frame render, serialize the preview DOM into a canvas (clone + inline images + SVG foreignObject), configure VideoEncoder (VP8), encode frames with periodic keyframes, collect chunks, finalize an IVF blob with per-chunk timestamps, and download it.
UI controls and export action
web-sdui/src/App.tsx
Disables seek/transport controls during encoding and adds a “Generate Video” button that triggers handleGenerateVideo, shows frame progress in the label, and renders encodeStatus.
Album art CORS attributes
web-sdui/src/views/MultiLyricsContainer.tsx
Album art <img> updated with crossOrigin="anonymous" and referrerPolicy="no-referrer" to enable inlining during DOM serialization.

Sequence Diagram

sequenceDiagram
  participant UI_Button as "Generate Video Button"
  participant App as "App.handleGenerateVideo"
  participant FrameWaiter as "frameWaiterRef"
  participant Preview as "previewRef.current (DOM)"
  participant CanvasRenderer as "renderNodeToCanvas"
  participant VideoEncoder as "VideoEncoder (VP8)"
  participant IVFBuilder as "IVF assembler"
  participant Downloader as "downloadIVF"

  UI_Button->>App: click -> handleGenerateVideo()
  App->>FrameWaiter: setFrameAndWaitForRender(frame)
  FrameWaiter->>Preview: wait until currentFrame rendered
  App->>CanvasRenderer: renderNodeToCanvas(previewRef.current)
  CanvasRenderer-->>App: canvas frame
  App->>VideoEncoder: encode(canvas frame) (keyframe cadence)
  VideoEncoder-->>App: encoded chunks (EncodedVideoChunk)
  App->>IVFBuilder: appendEncodedChunk(chunk, timestamp)
  App->>VideoEncoder: flush()
  App->>IVFBuilder: finalize IVF
  App->>Downloader: downloadIVF(ivfBlob)
  Downloader-->>UI_Button: saved video.ivf
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hop through frames with nimble paws,

Cloning nodes and banishing flaws.
VP8 hums with each keyframe cheer,
IVF bundled, download near.
Rabbit claps — the export's here!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding a frame-by-frame VideoEncoder export feature using WebCodecs API in the web-sdui module.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/add-video-generation-feature-with-videoencoder

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@amazon-q-developer amazon-q-developer Bot 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.

Summary

This PR adds WebCodecs-based video export functionality to the SDUI preview. The implementation successfully introduces frame-by-frame encoding using VideoEncoder with VP8 codec and IVF container format.

Critical Issues Identified (5)

Memory Leaks & Resource Management:

  • Blob URLs not properly released in error paths of renderNodeToCanvas
  • Missing cleanup when canvas context creation fails

Crash Prevention:

  • Encoder error callback doesn't halt the encoding loop, causing continued operations on failed encoder
  • encoder.close() in finally block may throw if encoder is in error state
  • Missing error state checks before encode operations

All identified issues have been marked with specific code suggestions to fix the defects. Please address these critical issues before merging to prevent memory leaks and runtime crashes during video generation.


You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.

Comment thread web-sdui/src/App.tsx
Comment on lines +321 to +324
const encoder = new VideoEncoder({
output: (chunk) => chunks.push(chunk),
error: (error) => setEncodeStatus(`Encoding error: ${error.message}`)
});

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.

🛑 Crash Risk: The encoder error callback only updates UI state but doesn't halt the encoding loop. When an encoding error occurs, the for-loop at line 327 continues encoding frames with a failed encoder, which will throw exceptions. Add error tracking and break the loop when encoder errors occur.

Suggested change
const encoder = new VideoEncoder({
output: (chunk) => chunks.push(chunk),
error: (error) => setEncodeStatus(`Encoding error: ${error.message}`)
});
let encodingError: Error | null = null;
const encoder = new VideoEncoder({
output: (chunk) => chunks.push(chunk),
error: (error) => {
encodingError = error;
setEncodeStatus(`Encoding error: ${error.message}`);
}
});

Comment thread web-sdui/src/App.tsx
Comment on lines +255 to +259
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(new Error('Failed to render frame.'));
img.src = url;
});

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.

🛑 Memory Leak: The blob URL is not revoked when the image fails to load, causing a memory leak. If img.onerror triggers at line 257, execution jumps to the rejection path without calling URL.revokeObjectURL(url), leaving the blob URL unreleased in memory.

Suggested change
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(new Error('Failed to render frame.'));
img.src = url;
});
await new Promise<void>((resolve, reject) => {
img.onload = () => {
URL.revokeObjectURL(url);
resolve();
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('Failed to render frame.'));
};
img.src = url;
});

Comment thread web-sdui/src/App.tsx
Comment on lines +260 to +267
const ctx = canvas.getContext('2d');
if (!ctx) {
URL.revokeObjectURL(url);
throw new Error('Could not create canvas context.');
}
ctx.clearRect(0, 0, width, height);
ctx.drawImage(img, 0, 0, width, height);
URL.revokeObjectURL(url);

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.

🛑 Logic Error: The URL revocation at line 267 executes before the canvas drawing completes when ctx is null, but the error is thrown after. Move URL revocation into the finally block or after the drawImage call to ensure it always executes, preventing the memory leak when context creation fails.

Suggested change
const ctx = canvas.getContext('2d');
if (!ctx) {
URL.revokeObjectURL(url);
throw new Error('Could not create canvas context.');
}
ctx.clearRect(0, 0, width, height);
ctx.drawImage(img, 0, 0, width, height);
URL.revokeObjectURL(url);
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Could not create canvas context.');
}
ctx.clearRect(0, 0, width, height);
ctx.drawImage(img, 0, 0, width, height);

Comment thread web-sdui/src/App.tsx
Comment on lines +327 to +335
for (let frame = 0; frame <= maxFrames; frame += 1) {
setCurrentFrame(frame);
setEncodeStatus(`Encoding frame ${frame + 1}/${maxFrames + 1}...`);
await waitForNextPaint();
await renderNodeToCanvas(previewRef.current, exportCanvas, width, height);
const videoFrame = new VideoFrame(exportCanvas, { timestamp: Math.round((frame / fps) * 1_000_000) });
encoder.encode(videoFrame, { keyFrame: frame % fps === 0 });
videoFrame.close();
}

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.

Check encodingError before each encode operation to prevent encoding with a failed encoder. Without this check, the loop continues after encoder failures, causing crashes.

Suggested change
for (let frame = 0; frame <= maxFrames; frame += 1) {
setCurrentFrame(frame);
setEncodeStatus(`Encoding frame ${frame + 1}/${maxFrames + 1}...`);
await waitForNextPaint();
await renderNodeToCanvas(previewRef.current, exportCanvas, width, height);
const videoFrame = new VideoFrame(exportCanvas, { timestamp: Math.round((frame / fps) * 1_000_000) });
encoder.encode(videoFrame, { keyFrame: frame % fps === 0 });
videoFrame.close();
}
for (let frame = 0; frame <= maxFrames; frame += 1) {
if (encodingError) {
throw encodingError;
}
setCurrentFrame(frame);
setEncodeStatus(`Encoding frame ${frame + 1}/${maxFrames + 1}...`);
await waitForNextPaint();
await renderNodeToCanvas(previewRef.current, exportCanvas, width, height);
const videoFrame = new VideoFrame(exportCanvas, { timestamp: Math.round((frame / fps) * 1_000_000) });
encoder.encode(videoFrame, { keyFrame: frame % fps === 0 });
videoFrame.close();
}

Comment thread web-sdui/src/App.tsx
Comment on lines +341 to +344
} finally {
encoder.close();
setIsEncoding(false);
}

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.

🛑 Crash Risk: Calling encoder.close() may throw if the encoder is already in an error state or closed. Wrap in a try-catch to prevent unhandled exceptions during cleanup.

Suggested change
} finally {
encoder.close();
setIsEncoding(false);
}
} finally {
try {
encoder.close();
} catch (closeError) {
// Ignore close errors as encoder may already be closed or in error state
}
setIsEncoding(false);
}

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces video export functionality using the Web VideoEncoder API, allowing users to generate and download IVF video files from the UI preview. The implementation includes a frame-by-frame rendering pipeline from HTML to canvas and a custom IVF packager. Feedback identifies significant limitations in the current rasterization method regarding CSS and external resource capture, a bug in the IVF timestamp calculation that affects playback timing, and a suggestion to dynamically scale the video bitrate based on configuration.

Comment thread web-sdui/src/App.tsx Outdated
Comment on lines +245 to +250
const serialized = new XMLSerializer().serializeToString(node);
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
<foreignObject x="0" y="0" width="100%" height="100%">${serialized}</foreignObject>
</svg>
`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The current rasterization method using XMLSerializer and foreignObject has significant limitations that will likely result in the exported video looking different from the preview:

  1. Styles: External stylesheets and <style> tags in the document head are not captured by serializeToString(node). Only inline styles or styles within the node are preserved. This will cause the rendered frames to lose most of their CSS.
  2. External Resources: When an SVG is loaded into an Image object (as done on line 258), it is strictly sandboxed. It cannot load external resources like remote images (e.g., the YouTube avatar in defaultSDUI) or web fonts. These will be missing in the output.

To improve this, you should inject the document's styles into the SVG and ensure all images are converted to Data URLs before serialization.

Comment thread web-sdui/src/App.tsx
const frameHeader = new ArrayBuffer(12);
const fh = new DataView(frameHeader);
fh.setUint32(0, chunk.byteLength, true);
const timestamp = BigInt(Math.round(chunk.timestamp / 1000));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The IVF frame timestamp calculation is inconsistent with the file header. The header (lines 279-280) defines the timebase as 1 / fps seconds per unit. Therefore, the timestamp for each frame should be its integer index (0, 1, 2...). Currently, it is calculated as milliseconds, which will cause players to interpret the video timing incorrectly.

Suggested change
const timestamp = BigInt(Math.round(chunk.timestamp / 1000));
const timestamp = BigInt(Math.round((chunk.timestamp * fps) / 1000000));

Comment thread web-sdui/src/App.tsx
error: (error) => setEncodeStatus(`Encoding error: ${error.message}`)
});
try {
encoder.configure({ codec: 'vp8', width, height, bitrate: 4_000_000, framerate: fps });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The bitrate is currently hardcoded to 4Mbps. For varying resolutions and frame rates, it would be better to scale this based on the outputQuality configuration or the video dimensions to ensure consistent visual quality.

Suggested change
encoder.configure({ codec: 'vp8', width, height, bitrate: 4_000_000, framerate: fps });
encoder.configure({ codec: 'vp8', width, height, bitrate: (sdui?.config?.outputQuality || 100) * 50000, framerate: fps });

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
sdui c02873a Commit Preview URL

Branch Preview URL
May 13 2026, 09:49 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
lyrics c02873a Commit Preview URL

Branch Preview URL
May 13 2026, 09:50 AM

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web-sdui/src/App.tsx`:
- Around line 270-301: The downloadIvf helper writes IVF frame pts using
chunk.timestamp/1000 (ms) which mismatches the header timebase (1/fps), so
change pts to the frame index (e.g., use an incrementing frame counter or
compute Math.round(frameIndex) where header fps is used) so pts counts frames in
units of 1/fps; also ensure you write pts as a 64-bit little-endian split into
low/high via fh.setUint32(4, low, true) and fh.setUint32(8, high, true) as done
now. For the flaky download, append the created anchor element to document.body
before calling a.click(), and defer URL.revokeObjectURL(a.href) (e.g., via
setTimeout or queueMicrotask) after the click and then remove the anchor from
the DOM; update symbols: downloadIvf, header/fh/frameHeader, a.href, a.click(),
URL.revokeObjectURL to implement these changes.
- Around line 303-345: handleGenerateVideo currently ignores encoder runtime
errors and overwrites them during the frame loop; add a boolean flag (e.g.,
encoderError) scoped near the VideoEncoder construction and set it inside the
encoder.error callback (use error: unknown and narrow with instanceof Error) so
the loop checks that flag after each encode and breaks early if set, preventing
further encoder.encode calls; before calling encoder.configure call await
VideoEncoder.isConfigSupported({ codec: 'vp8', width, height, framerate: fps })
and throw/set status if unsupported; also replace catch(error: any) with
catch(error: unknown) and narrow to extract a safe message for setEncodeStatus.
- Around line 244-268: renderNodeToCanvas currently serializes a DOM into an SVG
<foreignObject> and draws it via an <img>, which taints the canvas (causing
VideoFrame construction to throw SecurityError) and also misses external
styles/fonts/images; fix by inlining external resources before serialization:
convert all <img> srcs (e.g., yt3.googleusercontent.com) to data: URLs or remove
them, copy computed styles into the cloned DOM's style attributes, embed
webfonts/stylesheets as <style> blocks, and set img.crossOrigin='anonymous'
where appropriate; alternatively replace renderNodeToCanvas with a proven
library (e.g., html-to-image or dom-to-image-more) that handles resource
inlining, or change the export path to render directly to a
canvas/OffscreenCanvas/captureStream to avoid SVG foreignObject—ensure the
VideoFrame creation uses a guaranteed origin-clean canvas.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 002360fa-6ab0-46d2-98bf-5b0b6d11566d

📥 Commits

Reviewing files that changed from the base of the PR and between f51c6b3 and 26aa8b2.

📒 Files selected for processing (2)
  • web-sdui/src/App.tsx
  • web-sdui/src/components/MotionCanvas.tsx

Comment thread web-sdui/src/App.tsx
Comment thread web-sdui/src/App.tsx
Comment on lines +270 to +301
const downloadIvf = (chunks: EncodedVideoChunk[], width: number, height: number, fps: number) => {
const fourcc = 'VP80';
const frameCount = chunks.length;
const header = new ArrayBuffer(32);
const dv = new DataView(header);
dv.setUint8(0, 'D'.charCodeAt(0)); dv.setUint8(1, 'K'.charCodeAt(0)); dv.setUint8(2, 'I'.charCodeAt(0)); dv.setUint8(3, 'F'.charCodeAt(0));
dv.setUint16(4, 0, true); dv.setUint16(6, 32, true);
for (let i = 0; i < 4; i += 1) dv.setUint8(8 + i, fourcc.charCodeAt(i));
dv.setUint16(12, width, true); dv.setUint16(14, height, true);
dv.setUint32(16, fps, true); dv.setUint32(20, 1, true);
dv.setUint32(24, frameCount, true); dv.setUint32(28, 0, true);

const parts: BlobPart[] = [header];
for (const chunk of chunks) {
const frameHeader = new ArrayBuffer(12);
const fh = new DataView(frameHeader);
fh.setUint32(0, chunk.byteLength, true);
const timestamp = BigInt(Math.round(chunk.timestamp / 1000));
fh.setUint32(4, Number(timestamp & BigInt(0xffffffff)), true);
fh.setUint32(8, Number((timestamp >> BigInt(32)) & BigInt(0xffffffff)), true);
const chunkData = new Uint8Array(chunk.byteLength);
chunk.copyTo(chunkData);
parts.push(frameHeader, chunkData);
}

const blob = new Blob(parts, { type: 'video/x-ivf' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `motion-${Date.now()}.ivf`;
a.click();
URL.revokeObjectURL(a.href);
};

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

IVF frame pts doesn't match the declared timebase; download trigger can also be flaky.

Two issues in this helper:

  1. Timebase/pts mismatch. The header declares timebase = framerate_den/framerate_num = 1/fps, so each frame's pts is measured in units of 1/fps seconds. For a 24 fps stream, frame N should have pts = N (decodes to N/24 s). The code instead writes round(chunk.timestamp / 1000) — i.e. milliseconds — so frame N gets pts ≈ N * 1000 / fps. With timebase 1/24 that decodes to ≈ N * 41.7 / 24 ≈ N * 1.74 seconds per frame, ~42× too slow in any player that respects pts (ffmpeg, pion's ivfreader, etc.). Some players ignore pts and fall back to the header framerate, which is why a playable file may still come out — but the file is not a valid 24 fps stream.

    Pick one consistent scheme, e.g. keep timebase = 1/fps and write the frame index as pts:

    ♻️ Suggested fix
    -    const parts: BlobPart[] = [header];
    -    for (const chunk of chunks) {
    +    const parts: BlobPart[] = [header];
    +    chunks.forEach((chunk, index) => {
           const frameHeader = new ArrayBuffer(12);
           const fh = new DataView(frameHeader);
           fh.setUint32(0, chunk.byteLength, true);
    -      const timestamp = BigInt(Math.round(chunk.timestamp / 1000));
    +      // pts is in units of timebase (den/num = 1/fps) → frame index.
    +      const timestamp = BigInt(index);
           fh.setUint32(4, Number(timestamp & BigInt(0xffffffff)), true);
           fh.setUint32(8, Number((timestamp >> BigInt(32)) & BigInt(0xffffffff)), true);
           const chunkData = new Uint8Array(chunk.byteLength);
           chunk.copyTo(chunkData);
           parts.push(frameHeader, chunkData);
    -    }
    +    });
  2. Download anchor isn't in the DOM and URL is revoked too eagerly. a.click() on a detached anchor is not reliable across browsers (notably Firefox), and calling URL.revokeObjectURL(a.href) synchronously after click() can interrupt the download before the browser has read the blob. Append the anchor, click it, then revoke on a microtask/timeout:

    ♻️ Suggested fix
    -    const blob = new Blob(parts, { type: 'video/x-ivf' });
    -    const a = document.createElement('a');
    -    a.href = URL.createObjectURL(blob);
    -    a.download = `motion-${Date.now()}.ivf`;
    -    a.click();
    -    URL.revokeObjectURL(a.href);
    +    const blob = new Blob(parts, { type: 'video/x-ivf' });
    +    const url = URL.createObjectURL(blob);
    +    const a = document.createElement('a');
    +    a.href = url;
    +    a.download = `motion-${Date.now()}.ivf`;
    +    document.body.appendChild(a);
    +    a.click();
    +    a.remove();
    +    setTimeout(() => URL.revokeObjectURL(url), 0);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-sdui/src/App.tsx` around lines 270 - 301, The downloadIvf helper writes
IVF frame pts using chunk.timestamp/1000 (ms) which mismatches the header
timebase (1/fps), so change pts to the frame index (e.g., use an incrementing
frame counter or compute Math.round(frameIndex) where header fps is used) so pts
counts frames in units of 1/fps; also ensure you write pts as a 64-bit
little-endian split into low/high via fh.setUint32(4, low, true) and
fh.setUint32(8, high, true) as done now. For the flaky download, append the
created anchor element to document.body before calling a.click(), and defer
URL.revokeObjectURL(a.href) (e.g., via setTimeout or queueMicrotask) after the
click and then remove the anchor from the DOM; update symbols: downloadIvf,
header/fh/frameHeader, a.href, a.click(), URL.revokeObjectURL to implement these
changes.

Comment thread web-sdui/src/App.tsx
Comment on lines +303 to +345
const handleGenerateVideo = async () => {
if (!('VideoEncoder' in window)) {
setEncodeStatus('VideoEncoder is not supported in this browser.');
return;
}
if (!previewRef.current) {
setEncodeStatus('Preview element is not available.');
return;
}
setIsEncoding(true);
setEncodeStatus('Starting encoding...');
const fps = sdui?.config?.fps || 24;
const width = sdui?.config?.aspectRatio?.width || 480;
const height = sdui?.config?.aspectRatio?.height || 854;
const exportCanvas = document.createElement('canvas');
exportCanvas.width = width;
exportCanvas.height = height;
const chunks: EncodedVideoChunk[] = [];
const encoder = new VideoEncoder({
output: (chunk) => chunks.push(chunk),
error: (error) => setEncodeStatus(`Encoding error: ${error.message}`)
});
try {
encoder.configure({ codec: 'vp8', width, height, bitrate: 4_000_000, framerate: fps });
for (let frame = 0; frame <= maxFrames; frame += 1) {
setCurrentFrame(frame);
setEncodeStatus(`Encoding frame ${frame + 1}/${maxFrames + 1}...`);
await waitForNextPaint();
await renderNodeToCanvas(previewRef.current, exportCanvas, width, height);
const videoFrame = new VideoFrame(exportCanvas, { timestamp: Math.round((frame / fps) * 1_000_000) });
encoder.encode(videoFrame, { keyFrame: frame % fps === 0 });
videoFrame.close();
}
await encoder.flush();
downloadIvf(chunks, width, height, fps);
setEncodeStatus(`Done. Downloaded ${chunks.length} encoded frames as IVF.`);
} catch (error: any) {
setEncodeStatus(`Failed: ${error?.message || 'Unknown error'}`);
} finally {
encoder.close();
setIsEncoding(false);
}
};

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

WebCodecs VideoEncoder isConfigSupported vp8 error callback state

💡 Result:

WebCodecs VideoEncoder + VP8 + “isConfigSupported” error callback state: 1) What “isConfigSupported” does (and what it doesn’t) - VideoEncoder.isConfigSupported(config) checks whether the browser can successfully configure the encoder with the given config, and it resolves to { supported, config } when the config is structurally valid; it throws TypeError only if the provided config is invalid (e.g., empty codec, negative width). [1][2] - If you get supported === false, this indicates the config is valid but not supported; in that case you should not proceed to configure/encode with it. (The API is intended as a best-effort pre-check.) [1][3] 2) How the “error callback” and encoder state are related - The error callback is invoked when an encoding error occurs, and when an error occurs the encoder transitions permanently to the "closed" state; you must create a new VideoEncoder instance afterward. [4] - MDN’s WebCodecs usage guide also notes that configure failures are reported via the error callback provided to the VideoEncoder constructor when the encoder can’t be initialized with the specified parameters. [5] 3) Interpreting “vp8 isConfigSupported error callback state” (the likely meaning) - If your flow is: a) await VideoEncoder.isConfigSupported({ codec: "vp8",... }) b) supported is false, or you still call configure anyway c) you observe your encoder error callback firing and/or encoder.state becoming "closed" then the expected behavior is: - For structurally valid but unsupported configs, you should see supported === false from isConfigSupported. [1][2] - If you proceed anyway (or if you hit an implementation-specific failure after configure), the encoder can report an error through the error callback and transition to "closed" permanently. [5][4] 4) Code pattern to avoid ending up in "closed" - Pre-check with isConfigSupported; only construct/configure the encoder if supported is true. [6][1] Example: const config = { codec: "vp8", width: 640, height: 480, bitrate: 2_000_000, framerate: 30 }; const { supported } = await VideoEncoder.isConfigSupported(config); if (!supported) { // fall back / choose another codec or parameters return; } const encoder = new VideoEncoder({ output: (chunk, meta) => { /* mux/store chunk */ }, error: (e) => { // When called, the encoder transitions permanently to "closed" console.error(e); }, }); encoder.configure(config); (That “error => encoder is closed” rule is per WebCodecs behavior described by MDN.) [4][5] 5) Edge case: “closed” can also happen even if isConfigSupported looked okay - Even with a supported config, WebCodecs can still error during encoding (e.g., resource reclamation when tab is switched), and on error the encoder transitions permanently to "closed". [4] If you paste your exact config object (codec string + width/height + framerate/bitrate + any hardwareAcceleration value) and the console error text, I can map it to the likely class of failure (unsupported config vs runtime encoder failure) and what to change (e.g., fall back to prefer-software vs lower resolution).

Citations:


Encoder errors don't abort the loop and the original message is overwritten; also pre-check codec support and avoid any.

Problems in order of impact:

  1. Loop doesn't honor encoder errors. The error callback at line 323 only sets encodeStatus. When the encoder encounters an error and transitions to the closed state, the loop continues calling encoder.encode(...) for remaining frames and keeps overwriting encodeStatus with Encoding frame N/M…, so the user never sees the actual error. Eventually encoder.flush() rejects and the catch block replaces the message with Failed: <flush error>, hiding the real cause. Use a flag to track encoder errors and break early.

  2. No isConfigSupported pre-check. Although VP8 is widely supported, you should query VideoEncoder.isConfigSupported() before configuring. This gives you a synchronous signal to fail fast with an actionable error instead of relying on the async error callback, which may fire mid-loop and get buried by subsequent status updates.

  3. error: any (line 339) — flagged by @typescript-eslint/no-explicit-any. Use unknown and narrow with instanceof Error.

♻️ Suggested fix
     setIsEncoding(true);
     setEncodeStatus('Starting encoding...');
     const fps = sdui?.config?.fps || 24;
     const width = sdui?.config?.aspectRatio?.width || 480;
     const height = sdui?.config?.aspectRatio?.height || 854;
+    const config: VideoEncoderConfig = { codec: 'vp8', width, height, bitrate: 4_000_000, framerate: fps };
+
+    const support = await VideoEncoder.isConfigSupported(config);
+    if (!support.supported) {
+      setEncodeStatus('VP8 encoding is not supported in this browser.');
+      setIsEncoding(false);
+      return;
+    }
+
     const exportCanvas = document.createElement('canvas');
     exportCanvas.width = width;
     exportCanvas.height = height;
     const chunks: EncodedVideoChunk[] = [];
+    let encoderError: Error | null = null;
     const encoder = new VideoEncoder({
       output: (chunk) => chunks.push(chunk),
-      error: (error) => setEncodeStatus(`Encoding error: ${error.message}`)
+      error: (error) => { encoderError = error; setEncodeStatus(`Encoding error: ${error.message}`); },
     });
     try {
-      encoder.configure({ codec: 'vp8', width, height, bitrate: 4_000_000, framerate: fps });
+      encoder.configure(config);
       for (let frame = 0; frame <= maxFrames; frame += 1) {
+        if (encoderError) throw encoderError;
         setCurrentFrame(frame);
         setEncodeStatus(`Encoding frame ${frame + 1}/${maxFrames + 1}...`);
         await waitForNextPaint();
-        await renderNodeToCanvas(previewRef.current, exportCanvas, width, height);
+        await renderNodeToCanvas(previewRef.current!, exportCanvas, width, height);
         const videoFrame = new VideoFrame(exportCanvas, { timestamp: Math.round((frame / fps) * 1_000_000) });
         encoder.encode(videoFrame, { keyFrame: frame % fps === 0 });
         videoFrame.close();
       }
       await encoder.flush();
+      if (encoderError) throw encoderError;
       downloadIvf(chunks, width, height, fps);
       setEncodeStatus(`Done. Downloaded ${chunks.length} encoded frames as IVF.`);
-    } catch (error: any) {
-      setEncodeStatus(`Failed: ${error?.message || 'Unknown error'}`);
+    } catch (error: unknown) {
+      const message = error instanceof Error ? error.message : 'Unknown error';
+      setEncodeStatus(`Failed: ${message}`);
     } finally {
+      if (encoder.state !== 'closed') encoder.close();
       setIsEncoding(false);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleGenerateVideo = async () => {
if (!('VideoEncoder' in window)) {
setEncodeStatus('VideoEncoder is not supported in this browser.');
return;
}
if (!previewRef.current) {
setEncodeStatus('Preview element is not available.');
return;
}
setIsEncoding(true);
setEncodeStatus('Starting encoding...');
const fps = sdui?.config?.fps || 24;
const width = sdui?.config?.aspectRatio?.width || 480;
const height = sdui?.config?.aspectRatio?.height || 854;
const exportCanvas = document.createElement('canvas');
exportCanvas.width = width;
exportCanvas.height = height;
const chunks: EncodedVideoChunk[] = [];
const encoder = new VideoEncoder({
output: (chunk) => chunks.push(chunk),
error: (error) => setEncodeStatus(`Encoding error: ${error.message}`)
});
try {
encoder.configure({ codec: 'vp8', width, height, bitrate: 4_000_000, framerate: fps });
for (let frame = 0; frame <= maxFrames; frame += 1) {
setCurrentFrame(frame);
setEncodeStatus(`Encoding frame ${frame + 1}/${maxFrames + 1}...`);
await waitForNextPaint();
await renderNodeToCanvas(previewRef.current, exportCanvas, width, height);
const videoFrame = new VideoFrame(exportCanvas, { timestamp: Math.round((frame / fps) * 1_000_000) });
encoder.encode(videoFrame, { keyFrame: frame % fps === 0 });
videoFrame.close();
}
await encoder.flush();
downloadIvf(chunks, width, height, fps);
setEncodeStatus(`Done. Downloaded ${chunks.length} encoded frames as IVF.`);
} catch (error: any) {
setEncodeStatus(`Failed: ${error?.message || 'Unknown error'}`);
} finally {
encoder.close();
setIsEncoding(false);
}
};
const handleGenerateVideo = async () => {
if (!('VideoEncoder' in window)) {
setEncodeStatus('VideoEncoder is not supported in this browser.');
return;
}
if (!previewRef.current) {
setEncodeStatus('Preview element is not available.');
return;
}
setIsEncoding(true);
setEncodeStatus('Starting encoding...');
const fps = sdui?.config?.fps || 24;
const width = sdui?.config?.aspectRatio?.width || 480;
const height = sdui?.config?.aspectRatio?.height || 854;
const config: VideoEncoderConfig = { codec: 'vp8', width, height, bitrate: 4_000_000, framerate: fps };
const support = await VideoEncoder.isConfigSupported(config);
if (!support.supported) {
setEncodeStatus('VP8 encoding is not supported in this browser.');
setIsEncoding(false);
return;
}
const exportCanvas = document.createElement('canvas');
exportCanvas.width = width;
exportCanvas.height = height;
const chunks: EncodedVideoChunk[] = [];
let encoderError: Error | null = null;
const encoder = new VideoEncoder({
output: (chunk) => chunks.push(chunk),
error: (error) => { encoderError = error; setEncodeStatus(`Encoding error: ${error.message}`); },
});
try {
encoder.configure(config);
for (let frame = 0; frame <= maxFrames; frame += 1) {
if (encoderError) throw encoderError;
setCurrentFrame(frame);
setEncodeStatus(`Encoding frame ${frame + 1}/${maxFrames + 1}...`);
await waitForNextPaint();
await renderNodeToCanvas(previewRef.current!, exportCanvas, width, height);
const videoFrame = new VideoFrame(exportCanvas, { timestamp: Math.round((frame / fps) * 1_000_000) });
encoder.encode(videoFrame, { keyFrame: frame % fps === 0 });
videoFrame.close();
}
await encoder.flush();
if (encoderError) throw encoderError;
downloadIvf(chunks, width, height, fps);
setEncodeStatus(`Done. Downloaded ${chunks.length} encoded frames as IVF.`);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error';
setEncodeStatus(`Failed: ${message}`);
} finally {
if (encoder.state !== 'closed') encoder.close();
setIsEncoding(false);
}
};
🧰 Tools
🪛 GitHub Check: CodeFactor

[warning] 339-339: web-sdui/src/App.tsx#L339
Unexpected any. Specify a different type. (@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-sdui/src/App.tsx` around lines 303 - 345, handleGenerateVideo currently
ignores encoder runtime errors and overwrites them during the frame loop; add a
boolean flag (e.g., encoderError) scoped near the VideoEncoder construction and
set it inside the encoder.error callback (use error: unknown and narrow with
instanceof Error) so the loop checks that flag after each encode and breaks
early if set, preventing further encoder.encode calls; before calling
encoder.configure call await VideoEncoder.isConfigSupported({ codec: 'vp8',
width, height, framerate: fps }) and throw/set status if unsupported; also
replace catch(error: any) with catch(error: unknown) and narrow to extract a
safe message for setEncodeStatus.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web-sdui/src/App.tsx`:
- Around line 242-253: There is a race because user-driven calls to
setCurrentFrame (from the play/pause handler and seek bar) can resolve
frameWaiterRef while encoding; add an isEncoding boolean state (e.g.,
useState(false)) and set it true/false around the encoding loop, then: 1)
disable the play/pause button and seek bar UI when isEncoding is true so they
cannot trigger setCurrentFrame during encoding, and 2) guard any external
setters (the play/pause handler and the seek change handler) to be no-ops when
isEncoding is true so setCurrentFrame and setFrameAndWaitForRender only come
from the encoder while encoding. Ensure you update references to frameWaiterRef,
setFrameAndWaitForRender, currentFrame, and the play/seek handlers accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 94520ec2-1452-4ddc-a675-c0b313a06473

📥 Commits

Reviewing files that changed from the base of the PR and between 001332c and fbf2392.

📒 Files selected for processing (1)
  • web-sdui/src/App.tsx

Comment thread web-sdui/src/App.tsx

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
web-sdui/src/App.tsx (1)

395-412: 💤 Low value

previewRef.current may be null inside the loop per TypeScript.

The early-return at line 360 narrows previewRef.current for that block, but the narrowing is lost across the awaits inside the loop, so line 403 passes HTMLDivElement | null into renderNodeToCanvas's HTMLElement parameter. Capture the ref once before the loop (e.g. const previewNode = previewRef.current!; after the guard) and pass that local to keep types honest without repeated non-null assertions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-sdui/src/App.tsx` around lines 395 - 412, The loop uses
previewRef.current across awaits so TypeScript cannot guarantee it’s non-null;
before the for loop (after the existing early-return guard) capture the ref into
a local constant (e.g. const previewNode = previewRef.current!) and then use
that local (previewNode) in setFrameAndWaitForRender/renderNodeToCanvas calls
and any logging instead of previewRef.current so the type stays narrowed across
awaits and you avoid repeated non-null assertions in functions like
renderNodeToCanvas, the VideoFrame creation, and encoder.encode.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web-sdui/src/App.tsx`:
- Around line 276-289: renderNodeToCanvas currently re-fetches and re-inlines
every <img> src on every frame causing repeated network calls; create a
per-export cache (e.g. const imageCache = new Map<string, string | null>()) in
handleGenerateVideo and pass it into renderNodeToCanvas, then change
renderNodeToCanvas to consult imageCache before calling imageToDataUrl(src) and
store the resolved dataURL (or null for failures) in the cache so subsequent
frames reuse the inlined result instead of refetching.

---

Nitpick comments:
In `@web-sdui/src/App.tsx`:
- Around line 395-412: The loop uses previewRef.current across awaits so
TypeScript cannot guarantee it’s non-null; before the for loop (after the
existing early-return guard) capture the ref into a local constant (e.g. const
previewNode = previewRef.current!) and then use that local (previewNode) in
setFrameAndWaitForRender/renderNodeToCanvas calls and any logging instead of
previewRef.current so the type stays narrowed across awaits and you avoid
repeated non-null assertions in functions like renderNodeToCanvas, the
VideoFrame creation, and encoder.encode.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4a4bbd36-68fb-48cb-af28-123478eeb4ad

📥 Commits

Reviewing files that changed from the base of the PR and between fbf2392 and 6245273.

📒 Files selected for processing (2)
  • web-sdui/src/App.tsx
  • web-sdui/src/views/MultiLyricsContainer.tsx
✅ Files skipped from review due to trivial changes (1)
  • web-sdui/src/views/MultiLyricsContainer.tsx

Comment thread web-sdui/src/App.tsx
Comment on lines +276 to +289
const renderNodeToCanvas = async (node: HTMLElement, canvas: HTMLCanvasElement, width: number, height: number) => {
const clonedNode = node.cloneNode(true) as HTMLElement;
const clonedImages = Array.from(clonedNode.querySelectorAll('img'));
for (const image of clonedImages) {
const originalSrc = image.getAttribute('src');
if (!originalSrc) continue;
const inlined = await imageToDataUrl(originalSrc);
if (inlined) {
image.setAttribute('src', inlined);
} else {
image.style.visibility = 'hidden';
console.warn('[VideoEncoder] Hiding non-CORS image during export', { src: originalSrc });
}
}

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cache inlined image data URLs across frames.

renderNodeToCanvas runs once per frame and re-fetches every <img> src inside the cloned DOM on each call. For the default 873-frame export with the album art image, that's ~873 network round-trips for the same URL, which is wasteful, slows export dramatically, and risks remote rate-limiting/failures partway through (causing the image to flip to visibility: hidden mid-video). Resolve and cache each unique src to a data URL once before the encoding loop (or memoize across calls), and reuse the inlined value per frame.

♻️ Sketch: hoist a per-export src→dataURL cache
-  const renderNodeToCanvas = async (node: HTMLElement, canvas: HTMLCanvasElement, width: number, height: number) => {
+  const renderNodeToCanvas = async (
+    node: HTMLElement,
+    canvas: HTMLCanvasElement,
+    width: number,
+    height: number,
+    imageCache: Map<string, string | null>,
+  ) => {
     const clonedNode = node.cloneNode(true) as HTMLElement;
     const clonedImages = Array.from(clonedNode.querySelectorAll('img'));
     for (const image of clonedImages) {
       const originalSrc = image.getAttribute('src');
       if (!originalSrc) continue;
-      const inlined = await imageToDataUrl(originalSrc);
+      let inlined = imageCache.get(originalSrc);
+      if (inlined === undefined) {
+        inlined = await imageToDataUrl(originalSrc);
+        imageCache.set(originalSrc, inlined);
+      }
       if (inlined) {
         image.setAttribute('src', inlined);
       } else {
         image.style.visibility = 'hidden';
-        console.warn('[VideoEncoder] Hiding non-CORS image during export', { src: originalSrc });
       }
     }

Then create const imageCache = new Map<string, string | null>(); in handleGenerateVideo and pass it into each renderNodeToCanvas call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-sdui/src/App.tsx` around lines 276 - 289, renderNodeToCanvas currently
re-fetches and re-inlines every <img> src on every frame causing repeated
network calls; create a per-export cache (e.g. const imageCache = new
Map<string, string | null>()) in handleGenerateVideo and pass it into
renderNodeToCanvas, then change renderNodeToCanvas to consult imageCache before
calling imageToDataUrl(src) and store the resolved dataURL (or null for
failures) in the cache so subsequent frames reuse the inlined result instead of
refetching.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
web-sdui/src/App.tsx (1)

419-421: ⚡ Quick win

Replace error: any with unknown and narrow appropriately.

Flagged by @typescript-eslint/no-explicit-any. Use unknown and narrow with instanceof Error for type safety.

♻️ Proposed fix
-    } catch (error: any) {
+    } catch (error: unknown) {
       console.error('[VideoEncoder] Export failed', error);
-      setEncodeStatus(`Failed: ${error?.message || 'Unknown error'}`);
+      const message = error instanceof Error ? error.message : 'Unknown error';
+      setEncodeStatus(`Failed: ${message}`);
     } finally {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-sdui/src/App.tsx` around lines 419 - 421, The catch block in App.tsx uses
an explicit any type for the caught error; change the catch parameter from
error: any to error: unknown and then narrow it with an if (error instanceof
Error) check before accessing error.message (falling back to a safe default
otherwise). Update the console.error and setEncodeStatus calls inside the catch
to use the narrowed error message (or a generic message) so we no longer rely on
any, referencing the existing catch block that logs '[VideoEncoder] Export
failed' and calls setEncodeStatus.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@web-sdui/src/App.tsx`:
- Around line 419-421: The catch block in App.tsx uses an explicit any type for
the caught error; change the catch parameter from error: any to error: unknown
and then narrow it with an if (error instanceof Error) check before accessing
error.message (falling back to a safe default otherwise). Update the
console.error and setEncodeStatus calls inside the catch to use the narrowed
error message (or a generic message) so we no longer rely on any, referencing
the existing catch block that logs '[VideoEncoder] Export failed' and calls
setEncodeStatus.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 794c441a-2e25-4902-bbf4-830af8687a24

📥 Commits

Reviewing files that changed from the base of the PR and between 6245273 and c02873a.

📒 Files selected for processing (1)
  • web-sdui/src/App.tsx

@tejpratap46 tejpratap46 merged commit c52f8b4 into main May 13, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant