Skip to content

feat: Implement core video export functionality using WebCodecs#460

Draft
alamshafil wants to merge 1 commit into
OpenCut-app:stagingfrom
alamshafil:feature/export-webcodec
Draft

feat: Implement core video export functionality using WebCodecs#460
alamshafil wants to merge 1 commit into
OpenCut-app:stagingfrom
alamshafil:feature/export-webcodec

Conversation

@alamshafil
Copy link
Copy Markdown

@alamshafil alamshafil commented Jul 26, 2025

Description

This pull request introduces a video export feature, using WebCodecs and other modern browser APIs.

Integration of Video Export Feature:

  • Integrated the VideoExportDialog component into the editor header

Implementation of Video Export Core:

  • Created a new @opencut/video-export package with initial setup in package.json, including dependencies like mediabunny and type definitions for WebCodecs.
  • Developed the VideoExporter class in packages/video-export/src/api/video-exporter.ts, providing methods for exporting timelines, aborting exports, and estimating export times. It supports configurations like resolution, bitrate, framerate, and hardware acceleration.

Read the README.md in packages/video-export for more info.

Type of change

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)

Screenshots (if applicable)

image

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have added screenshots if ui has been changed
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

Additional context

The performance of this PR can be improved.

Summary by CodeRabbit

  • New Features

    • Introduced a full-featured video export engine with WebCodecs support, enabling users to export timelines as MP4 files directly from the editor.
    • Added an interactive export dialog with real-time progress, customizable settings (resolution, frame rate, bitrate, hardware acceleration), and debug log viewer.
    • Integrated support for export presets and custom configurations.
    • Provided export error handling, progress tracking, and user notifications.
    • Enhanced documentation for the video export engine.
  • Bug Fixes

    • Improved export workflow by replacing placeholder export logic with a robust, user-friendly export dialog.
  • Documentation

    • Added comprehensive documentation for the new video export engine, including usage examples and API references.

- Add main API exports for video exporting including VideoExporter and related utilities.
- Introduce RenderPipeline for processing frames with various processors.
- Create base classes for effects and transitions, including Blur, Brightness, Fade, Slide, and Wipe effects.
- Define types for export settings, progress, and timeline data.
- Implement utility functions for audio processing and time formatting.
- Develop a TimelineDataTransformer for converting OpenCut timeline data to export format.
- Add a debug logger for improved logging and error tracking.
@vercel
Copy link
Copy Markdown

vercel Bot commented Jul 26, 2025

@alamshafil is attempting to deploy a commit to the OpenCut OSS Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jul 26, 2025

Walkthrough

A comprehensive video export system was implemented for the OpenCut application. This includes a new @opencut/video-export package featuring a WebCodecs-based export engine, modular rendering pipeline, effect and transition support, and full TypeScript type coverage. The web app now integrates a dedicated export dialog, replacing the previous placeholder export logic.

Changes

File(s) / Path(s) Change Summary
apps/web/package.json Added @opencut/video-export as a local workspace dependency.
apps/web/src/components/editor-header.tsx Removed placeholder export logic; integrated new VideoExportDialog component for export functionality.
apps/web/src/components/export/VideoExportDialog.tsx Added a full-featured React export dialog UI, managing export state, progress, errors, and interaction with the video export engine.
packages/video-export/package.json Introduced new package manifest for @opencut/video-export with dependencies, entry points, and metadata.
packages/video-export/README.md Added comprehensive documentation covering features, usage, API, architecture, extensibility, and integration details.
packages/video-export/src/index.ts Created index file re-exporting all major modules, types, and pipeline elements for the package's public API.
packages/video-export/src/api/video-exporter.ts Implemented main export API class (VideoExporter), supporting timeline export, abort, presets, and support checks.
packages/video-export/src/core/video-export-engine.ts Added core export engine handling WebCodecs encoding, frame rendering, audio processing, output saving, and event management.
packages/video-export/src/core/frame-renderer.ts Implemented frame rendering system with modular renderers for media, text, overlays, subtitles, and effects pipeline.
packages/video-export/src/core/audio-processor.ts Added audio processor for mixing, resampling, and encoding timeline audio tracks.
packages/video-export/src/core/config-generator.ts Added configuration generator for validating and producing video/audio encoder settings based on export options.
packages/video-export/src/core/stream-target.ts Implemented output stream abstraction supporting File System Access API and download fallback for saving exported files.
packages/video-export/src/core/renderers/base-renderer.ts,
media-renderer.ts,
text-renderer.ts,
overlay-renderer.ts,
subtitle-renderer.ts,
index.ts
Introduced base and specialized element renderers for media, text, overlays, and subtitles; provided central export index.
packages/video-export/src/types/index.ts Defined comprehensive TypeScript types for export settings, timeline data, elements, progress, and events.
packages/video-export/src/pipeline/pipeline.ts Added render pipeline system supporting sequential effect and transition processors.
packages/video-export/src/pipeline/effects/base-effect.ts Implemented base effect class and built-in effects: blur, brightness, and color filter.
packages/video-export/src/pipeline/transitions/base-transition.ts Implemented base transition class and built-in transitions: fade, slide, and wipe.
packages/video-export/src/utils/audio-utils.ts,
debug-logger.ts,
time-utils.ts,
timeline-transformer.ts
Added audio conversion utilities, debug logger, time formatting helpers, and timeline data transformer for OpenCut integration.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant EditorHeader
    participant VideoExportDialog
    participant VideoExporter
    participant VideoExportEngine
    participant StreamTarget

    User->>EditorHeader: Clicks Export
    EditorHeader->>VideoExportDialog: Opens dialog
    User->>VideoExportDialog: Configures settings, starts export
    VideoExportDialog->>VideoExporter: exportTimeline(...)
    VideoExporter->>VideoExportEngine: export()
    VideoExportEngine->>StreamTarget: create()
    VideoExportEngine->>VideoExportEngine: Render frames, encode video/audio
    VideoExportEngine->>StreamTarget: saveBuffer()
    VideoExportEngine-->>VideoExporter: Emit progress/completion
    VideoExporter-->>VideoExportDialog: Notify progress/completion
    VideoExportDialog-->>User: Show progress, allow download
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

Poem

In the warren of code, a new path appears,
With WebCodecs and timelines, we leap past old fears.
Effects, transitions, and exports galore—
A dialog now guides you to render and more!
With every frame drawn and each bunny hop,
OpenCut’s exports reach the very top.
🐇✨

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@alamshafil
Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jul 26, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 27

♻️ Duplicate comments (1)
packages/video-export/src/pipeline/transitions/base-transition.ts (1)

55-56: Add cleanup for temporary canvases to prevent memory leaks.

All transition classes create temporary canvases without proper cleanup, similar to the effects.

Apply the same pattern suggested for effects - wrap canvas operations in try/finally blocks and reset canvas dimensions to help garbage collection.

Also applies to: 109-111, 139-141

🧹 Nitpick comments (24)
packages/video-export/src/utils/time-utils.ts (2)

11-11: Consider using stricter input validation.

The condition !timeInSeconds will treat 0 as falsy, but 0 is a valid time value that should format to "0s". This works correctly due to the fallback logic, but the condition could be more explicit.

-  if (!timeInSeconds || timeInSeconds < 0) {
+  if (timeInSeconds <= 0) {

42-42: Same input validation consideration applies here.

Similar to formatTime, the condition treats 0 as falsy when it's a valid input.

-  if (!timeInSeconds || timeInSeconds < 0) {
+  if (timeInSeconds <= 0) {
packages/video-export/package.json (1)

6-7: Consider build output for main/module fields.

The main and module fields point to TypeScript source files. While this works for development in a monorepo, consider pointing to compiled JavaScript output for production builds.

For production builds, these should typically point to:

"main": "./dist/index.js",
"module": "./dist/index.js"
packages/video-export/src/core/renderers/media-renderer.ts (2)

111-117: Fix typo in comment.

-    // Simple direct rendering`
+    // Simple direct rendering

265-273: Improve video cleanup in dispose method.

Setting src = '' and calling load() may not properly release video resources.

   dispose(): void {
     for (const media of this.mediaCache.values()) {
       if (media instanceof HTMLVideoElement) {
-        media.src = '';
-        media.load();
+        media.pause();
+        media.removeAttribute('src');
+        media.load();
       }
     }
     this.mediaCache.clear();
   }
packages/video-export/src/core/renderers/subtitle-renderer.ts (2)

135-143: Implement actual font loading for web fonts.

The current implementation assumes fonts are already loaded, which may cause text to render with fallback fonts.

Would you like me to implement proper web font loading using the Font Loading API? I can open a new issue to track this enhancement.


103-106: Consider more accurate text height measurement.

Using fontSize as text height is an approximation that may not work well for all fonts.

Consider using font metrics for more accurate height calculation:

     const metrics = this.context.measureText(element.content);
     const textWidth = metrics.width;
-    const textHeight = element.fontSize;
+    // Use actual font metrics if available
+    const textHeight = (metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent) || element.fontSize;
packages/video-export/src/core/frame-renderer.ts (1)

152-163: Complete the effect processor creation logic.

The method checks for processor existence but doesn't create one when missing.

The current implementation only logs but doesn't create effect processors. Would you like me to implement the processor creation logic based on effectType and parameters? I can open a new issue to track this.

packages/video-export/src/core/stream-target.ts (1)

55-63: Improve download reliability and cleanup timing

The current implementation has potential issues:

  1. The 1-second timeout might be too short for large files
  2. The anchor element should be clicked after being added to DOM for better browser compatibility
 const a = document.createElement('a');
 a.href = url;
 a.download = this.fileName;
+a.style.display = 'none';
 document.body.appendChild(a);
 a.click();
 document.body.removeChild(a);
 
 // Clean up
-setTimeout(() => URL.revokeObjectURL(url), 1000);
+// Wait longer for large files and ensure download starts
+setTimeout(() => URL.revokeObjectURL(url), 60000); // 60 seconds
packages/video-export/src/core/config-generator.ts (1)

19-29: Consider supporting additional video codecs for better performance

Currently only H.264 is attempted. Modern browsers support VP9 and AV1 which can provide better compression.

 const configOptions: VideoEncoderConfig[] = [
   {
     codec: 'avc1.64002a', // H.264 High Profile Level 4.2 - Best codec for compatibility across all browsers, devices, phones, and web platforms
     width,
     height,
     bitrate: settings.videoBitrate,
     framerate: settings.fps,
     hardwareAcceleration: settings.hardwareAcceleration ? 'prefer-hardware' : 'prefer-software',
   },
+  {
+    codec: 'vp09.00.41.08', // VP9 Profile 0 Level 4.1 - Better compression, widely supported
+    width,
+    height,
+    bitrate: Math.floor(settings.videoBitrate * 0.7), // VP9 is more efficient
+    framerate: settings.fps,
+    hardwareAcceleration: settings.hardwareAcceleration ? 'prefer-hardware' : 'prefer-software',
+  },
+  {
+    codec: 'av01.0.08M.10', // AV1 Main Profile - Best compression, growing support
+    width,
+    height,
+    bitrate: Math.floor(settings.videoBitrate * 0.5), // AV1 is very efficient
+    framerate: settings.fps,
+    hardwareAcceleration: settings.hardwareAcceleration ? 'prefer-hardware' : 'prefer-software',
+  },
 ];
packages/video-export/src/core/renderers/overlay-renderer.ts (2)

135-191: Consider making pattern parameters configurable

The pattern rendering methods use hardcoded values for sizes and spacing. Consider making these configurable through element properties or configuration objects for better flexibility.

Example refactor for the dot pattern method:

-  private renderDotPattern(element: OverlayExportElement): void {
-    const dotSize = 5;
-    const spacing = 15;
+  private renderDotPattern(element: OverlayExportElement): void {
+    // Allow customization through element properties or use defaults
+    const dotSize = element.patternConfig?.dotSize ?? 5;
+    const spacing = element.patternConfig?.spacing ?? 15;

This approach could be applied to all pattern methods to make them more flexible and reusable.


203-204: Consider flexible CORS handling for images

Setting crossOrigin = 'anonymous' might cause issues with images from servers that don't support CORS. Consider making this configurable or implementing a fallback mechanism.

-    image.crossOrigin = 'anonymous';
+    // Only set crossOrigin if the URL is cross-origin
+    try {
+      const url = new URL(source);
+      const currentOrigin = window.location.origin;
+      if (url.origin !== currentOrigin) {
+        image.crossOrigin = 'anonymous';
+      }
+    } catch {
+      // Invalid URL or relative path, no CORS needed
+    }
packages/video-export/src/pipeline/pipeline.ts (1)

78-80: Improve type safety in getProcessorsByType

The current implementation uses unsafe type assertion. Consider a more type-safe approach.

-  getProcessorsByType(type: string): PipelineProcessor[] {
-    return this.processors.filter(p => (p as any).type === type);
-  }
+  getProcessorsByType<T extends 'effect' | 'transition'>(
+    type: T
+  ): T extends 'effect' ? EffectProcessor[] : TransitionProcessor[] {
+    return this.processors.filter(p => 
+      'type' in p && p.type === type
+    ) as any;
+  }

This provides better type safety and IntelliSense support while avoiding the as any assertion.

apps/web/src/components/export/VideoExportDialog.tsx (2)

106-127: Simplify time formatting logic

The formatTime function has complex nested conditions that could be simplified for better readability.

 function formatTime(seconds: number): string {
-  if (seconds < 60) {
-    return `${Math.round(seconds)}s`;
-  } else if (seconds < 3600) {
-    const minutes = Math.floor(seconds / 60);
-    const remainingSeconds = Math.round(seconds % 60);
-    return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
-  } else {
-    const hours = Math.floor(seconds / 3600);
-    const minutes = Math.floor((seconds % 3600) / 60);
-    const remainingSeconds = Math.round(seconds % 60);
-    
-    if (minutes === 0 && remainingSeconds === 0) {
-      return `${hours}h`;
-    } else if (remainingSeconds === 0) {
-      return `${hours}h ${minutes}m`;
-    } else {
-      return `${hours}h ${minutes}m ${remainingSeconds}s`;
-    }
-  }
+  const hours = Math.floor(seconds / 3600);
+  const minutes = Math.floor((seconds % 3600) / 60);
+  const secs = Math.round(seconds % 60);
+  
+  const parts = [];
+  if (hours > 0) parts.push(`${hours}h`);
+  if (minutes > 0) parts.push(`${minutes}m`);
+  if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);
+  
+  return parts.join(' ');
 }

247-258: Consolidate error handling logic

The error handling logic is duplicated between the onError callback and the catch block. Consider extracting this into a shared function.

+    const handleExportError = (error: Error | unknown) => {
+      console.error('Export error:', error);
+      setCurrentExporter(null);
+      const errorMessage = error instanceof Error ? error.message : "Unknown error";
+      setExportState({
+        isExporting: false,
+        progress: null,
+        error: errorMessage,
+        completed: false
+      });
+      toast.error(`Export failed: ${errorMessage}`);
+    };

     try {
       // ... export logic ...
     } catch (error) {
-      console.error('Export catch block error:', error);
-      setCurrentExporter(null);
-      const errorMessage = error instanceof Error ? error.message : "Unknown error";
-      setExportState({
-        isExporting: false,
-        progress: null,
-        error: errorMessage,
-        completed: false
-      });
-      toast.error(`Export failed: ${errorMessage}`);
+      handleExportError(error);
     }

Then use handleExportError in the onError callback as well.

packages/video-export/src/core/audio-processor.ts (1)

20-24: Improve type safety for audio track filtering

The current implementation uses type assertions (as any) which reduces type safety. Consider defining proper types for media elements.

     const audioTracks = timelineData.tracks.filter(track => 
       track.type === 'audio' || track.elements.some(el => 
-        el.type === 'media' && ((el as any).mediaType === 'audio' || (el as any).mediaType === 'video')
+        el.type === 'media' && 'mediaType' in el && (el.mediaType === 'audio' || el.mediaType === 'video')
       )
     );
packages/video-export/src/api/video-exporter.ts (2)

153-153: Document the resolution multiplier calculation for 720p.

The value 0.67 for 720p resolution is a magic number that should be documented or defined as a constant.

-      '720p_high': {
-        resolution: 0.67, // 720p
+      '720p_high': {
+        resolution: 0.67, // 720p = 1280x720, which is 0.67 of 1920x1080

Or better yet, define resolution constants:

const RESOLUTION_MULTIPLIERS = {
  '720p': 720 / 1080, // 0.667
  '1080p': 1,
  '4k': 2
} as const;

Also applies to: 159-159


210-221: Extract magic numbers to named constants for clarity.

The complexity calculation uses hardcoded thresholds that should be defined as constants for better maintainability.

+      // Complexity thresholds
+      const COMPLEXITY_THRESHOLDS = {
+        high: { elements: 20, tracks: 10, resolution: 2 },
+        medium: { elements: 10, tracks: 5, resolution: 1 }
+      };
+      
+      // Base render time per frame in seconds
+      const BASE_RENDER_TIMES = {
+        low: 0.1,
+        medium: 0.2,
+        high: 0.5
+      };
+
       let complexity: 'low' | 'medium' | 'high' = 'low';
-      let baseRenderTime = 0.1; // Base seconds per frame
+      let baseRenderTime = BASE_RENDER_TIMES.low;
       
       // Estimate complexity
-      if (elementCount > 20 || trackCount > 10 || resolution >= 2) {
+      if (elementCount > COMPLEXITY_THRESHOLDS.high.elements || 
+          trackCount > COMPLEXITY_THRESHOLDS.high.tracks || 
+          resolution >= COMPLEXITY_THRESHOLDS.high.resolution) {
         complexity = 'high';
-        baseRenderTime = 0.5; // Higher for complex projects
+        baseRenderTime = BASE_RENDER_TIMES.high;
packages/video-export/src/utils/timeline-transformer.ts (1)

9-61: Consider importing shared types instead of redefining them.

These OpenCut interfaces are redefined here but should ideally be imported from a shared types module to ensure consistency across the codebase.

Consider creating a shared types package or module that both the main app and this export package can import from. This would prevent type drift and ensure consistency.

packages/video-export/README.md (3)

5-5: Consider removing or rephrasing the LLM authorship note.

While transparency is good, mentioning LLM authorship might reduce confidence in the documentation. Consider rephrasing or moving this to a contributors section.

-Note: Document written by LLM to help agents understand OpenCut's video export system.
+Note: This document provides a comprehensive overview of OpenCut's video export system architecture and usage.

609-618: Improve error type checking in the error handling example.

The error handling example could be more robust by checking error types more specifically.

 } catch (error) {
-  if (error.message.includes('WebCodecs')) {
+  if (error instanceof Error && error.message.includes('WebCodecs')) {
     // Browser doesn't support WebCodecs
-  } else if (error.message.includes('validation')) {
+  } else if (error instanceof Error && error.message.includes('validation')) {
     // Timeline data validation failed
   } else {
     // Other export error
+    console.error('Unexpected export error:', error);
   }
 }

259-259: Track TODO items for UI integration.

The README mentions TODOs for adding effects and transitions to the Editor UI. These should be tracked as issues.

Would you like me to create GitHub issues to track these UI integration tasks for effects and transitions?

Also applies to: 284-284

packages/video-export/src/types/index.ts (2)

167-194: Consider default-optional fields & safer trimming semantics

trimStart and trimEnd are required numbers. A caller that hasn’t trimmed must still supply explicit 0, leading to noisy object literals and potential NaN bugs if a field is forgotten.

Recommend:

-  trimStart: number;
-  trimEnd: number;
+  trimStart?: number; // default 0
+  trimEnd?: number;   // default 0

and let the engine coerce undefined to 0. This mirrors common NLE data models and simplifies authoring.


351-356: Event payloads could be richer & type-safe

'complete': string only returns a path/URL. Consumers usually need stats (duration, size) and the ExportSettings used. Consider:

interface ExportCompletePayload {
  fileUrl: string;
  settings: ExportSettings;
  totalElapsed: number;
  sizeBytes?: number;
}

and update the map to 'complete': ExportCompletePayload. This prevents the inevitable follow-up “can we also return …?” change.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 642f51d and 53dbb96.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (26)
  • apps/web/package.json (1 hunks)
  • apps/web/src/components/editor-header.tsx (2 hunks)
  • apps/web/src/components/export/VideoExportDialog.tsx (1 hunks)
  • packages/video-export/README.md (1 hunks)
  • packages/video-export/package.json (1 hunks)
  • packages/video-export/src/api/video-exporter.ts (1 hunks)
  • packages/video-export/src/core/audio-processor.ts (1 hunks)
  • packages/video-export/src/core/config-generator.ts (1 hunks)
  • packages/video-export/src/core/frame-renderer.ts (1 hunks)
  • packages/video-export/src/core/renderers/base-renderer.ts (1 hunks)
  • packages/video-export/src/core/renderers/index.ts (1 hunks)
  • packages/video-export/src/core/renderers/media-renderer.ts (1 hunks)
  • packages/video-export/src/core/renderers/overlay-renderer.ts (1 hunks)
  • packages/video-export/src/core/renderers/subtitle-renderer.ts (1 hunks)
  • packages/video-export/src/core/renderers/text-renderer.ts (1 hunks)
  • packages/video-export/src/core/stream-target.ts (1 hunks)
  • packages/video-export/src/core/video-export-engine.ts (1 hunks)
  • packages/video-export/src/index.ts (1 hunks)
  • packages/video-export/src/pipeline/effects/base-effect.ts (1 hunks)
  • packages/video-export/src/pipeline/pipeline.ts (1 hunks)
  • packages/video-export/src/pipeline/transitions/base-transition.ts (1 hunks)
  • packages/video-export/src/types/index.ts (1 hunks)
  • packages/video-export/src/utils/audio-utils.ts (1 hunks)
  • packages/video-export/src/utils/debug-logger.ts (1 hunks)
  • packages/video-export/src/utils/time-utils.ts (1 hunks)
  • packages/video-export/src/utils/timeline-transformer.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: alamshafil
PR: OpenCut-app/OpenCut#325
File: apps/web/src/components/ui/font-picker.tsx:27-27
Timestamp: 2025-07-18T15:22:59.093Z
Learning: In the OpenCut video export system, the FontPicker component uses getGoogleFonts() instead of FONT_OPTIONS to limit font selection to Google Fonts only. This is because the video export functionality requires actual font files to be loaded into FFmpeg's virtual filesystem through the FontManager, rather than CSS-based fonts. System fonts like Arial and Helvetica are not available as downloadable files in the browser environment, making Google Fonts the appropriate choice for video export compatibility.
packages/video-export/src/api/video-exporter.ts (1)

Learnt from: alamshafil
PR: #325
File: apps/web/src/components/ui/font-picker.tsx:27-27
Timestamp: 2025-07-18T15:22:59.093Z
Learning: In the OpenCut video export system, the FontPicker component uses getGoogleFonts() instead of FONT_OPTIONS to limit font selection to Google Fonts only. This is because the video export functionality requires actual font files to be loaded into FFmpeg's virtual filesystem through the FontManager, rather than CSS-based fonts. System fonts like Arial and Helvetica are not available as downloadable files in the browser environment, making Google Fonts the appropriate choice for video export compatibility.

packages/video-export/README.md (1)

Learnt from: alamshafil
PR: #325
File: apps/web/src/components/ui/font-picker.tsx:27-27
Timestamp: 2025-07-18T15:22:59.093Z
Learning: In the OpenCut video export system, the FontPicker component uses getGoogleFonts() instead of FONT_OPTIONS to limit font selection to Google Fonts only. This is because the video export functionality requires actual font files to be loaded into FFmpeg's virtual filesystem through the FontManager, rather than CSS-based fonts. System fonts like Arial and Helvetica are not available as downloadable files in the browser environment, making Google Fonts the appropriate choice for video export compatibility.

🧬 Code Graph Analysis (6)
packages/video-export/src/core/renderers/subtitle-renderer.ts (2)
packages/video-export/src/core/renderers/index.ts (2)
  • SubtitleElementRenderer (6-6)
  • BaseElementRenderer (2-2)
packages/video-export/src/types/index.ts (1)
  • SubtitleExportElement (301-330)
packages/video-export/src/core/frame-renderer.ts (7)
packages/video-export/src/types/index.ts (4)
  • ExportSettings (55-88)
  • TimelineExportData (124-137)
  • RenderFrame (334-344)
  • ExportElement (332-332)
packages/video-export/src/pipeline/pipeline.ts (1)
  • RenderPipeline (21-91)
packages/video-export/src/core/renderers/media-renderer.ts (1)
  • MediaElementRenderer (8-274)
packages/video-export/src/core/renderers/text-renderer.ts (1)
  • TextElementRenderer (8-171)
packages/video-export/src/core/renderers/overlay-renderer.ts (1)
  • OverlayElementRenderer (8-230)
packages/video-export/src/core/renderers/subtitle-renderer.ts (1)
  • SubtitleElementRenderer (8-151)
packages/video-export/src/utils/debug-logger.ts (2)
  • error (26-40)
  • debugLogger (80-80)
packages/video-export/src/core/config-generator.ts (1)
packages/video-export/src/types/index.ts (2)
  • ExportSettings (55-88)
  • EncoderConfig (346-349)
packages/video-export/src/pipeline/pipeline.ts (2)
packages/video-export/src/index.ts (7)
  • PipelineProcessor (33-33)
  • RenderFrame (24-24)
  • ExportSettings (17-17)
  • EffectProcessor (33-33)
  • TransitionProcessor (33-33)
  • RenderPipeline (32-32)
  • TimelineExportData (19-19)
packages/video-export/src/types/index.ts (3)
  • RenderFrame (334-344)
  • ExportSettings (55-88)
  • TimelineExportData (124-137)
packages/video-export/src/pipeline/effects/base-effect.ts (3)
packages/video-export/src/index.ts (7)
  • BaseEffect (34-34)
  • EffectProcessor (33-33)
  • RenderFrame (24-24)
  • ExportSettings (17-17)
  • BlurEffect (34-34)
  • BrightnessEffect (34-34)
  • ColorFilterEffect (34-34)
packages/video-export/src/pipeline/pipeline.ts (1)
  • EffectProcessor (10-13)
packages/video-export/src/types/index.ts (2)
  • RenderFrame (334-344)
  • ExportSettings (55-88)
packages/video-export/src/utils/timeline-transformer.ts (2)
packages/video-export/src/index.ts (6)
  • TimelineDataTransformer (12-12)
  • TimelineExportData (19-19)
  • ExportTrack (20-20)
  • ExportElement (21-21)
  • MediaExportElement (22-22)
  • TextExportElement (23-23)
packages/video-export/src/types/index.ts (5)
  • TimelineExportData (124-137)
  • ExportTrack (140-164)
  • ExportElement (332-332)
  • MediaExportElement (197-217)
  • TextExportElement (220-261)
🔇 Additional comments (16)
packages/video-export/src/utils/time-utils.ts (2)

65-68: Functions look well-implemented.

Both formatProgress and formatRenderSpeed handle edge cases appropriately and provide clean, readable output formatting.

Also applies to: 76-79


51-51: Seconds padding is correct and requires no change.

Tests for various edge cases confirm that seconds is always computed as timeInSeconds % 60 (i.e., in [0, 60)), so seconds.toFixed(3).padStart(6, '0') consistently yields the expected six-character output:

  • 65.123 → “1:05.123”
  • 3599.999 → “59:59.999”
  • 0.001 → “0.001s”

No updates needed.

apps/web/package.json (1)

26-26: Dependency addition looks correct.

The workspace dependency for the new video export package is properly added and will enable the web app to use the export functionality.

packages/video-export/src/core/renderers/index.ts (1)

1-6: Clean barrel export implementation.

The index file properly exports all renderer classes, providing a clean API surface for the rendering system.

apps/web/src/components/editor-header.tsx (2)

11-11: Import statement is correct.

The VideoExportDialog component is properly imported from the export directory.


102-111: Clean integration of the video export dialog.

The existing export button is now properly wrapped with the VideoExportDialog component, maintaining the same UI while adding the new export functionality. The component composition pattern is well-executed.

packages/video-export/package.json (1)

17-18: TypeScript type dependencies look appropriate.

The WebCodecs and File System Access API type definitions are correctly placed in devDependencies, and the peer dependency on @opencut/db makes sense for the export functionality.

Also applies to: 21-21

packages/video-export/src/core/renderers/text-renderer.ts (1)

57-67: Well-structured text style setup

The method correctly composes the font string and aligns text properties with the center-origin coordinate system.

packages/video-export/src/core/video-export-engine.ts (2)

210-264: Verify frame counting logic for accurate progress reporting

The loop processes totalFrames + 1 frames (0 to totalFrames inclusive), but progress calculation uses frame / totalFrames which could exceed 100% on the last frame.

Either adjust the loop condition or the progress calculation:

-for (let frame = 0; frame <= totalFrames; frame++) {
+for (let frame = 0; frame < totalFrames; frame++) {

Or update progress calculation to account for the extra frame:

-percentage: (frame / totalFrames) * 100,
+percentage: (frame / (totalFrames + 1)) * 100,

329-340: Good fault tolerance with fallback canvas

The fallback canvas implementation ensures export continues even if individual frames fail to render, providing a solid background instead of corrupting the video.

packages/video-export/src/core/config-generator.ts (1)

106-124: Well-designed settings validation

The validation method properly constrains all parameters to reasonable ranges and handles edge cases well. Good future-proofing with support for up to 8K resolution.

packages/video-export/src/core/renderers/base-renderer.ts (1)

1-76: Well-structured base renderer implementation

The abstract base class provides a solid foundation for element renderers with proper context management, timing calculations, and property application. The save/restore pattern ensures clean state management between renders.

packages/video-export/src/core/audio-processor.ts (1)

126-128: Good audio clipping prevention!

The clamping logic properly prevents audio clipping by keeping values within the valid range of -1 to 1. This is a best practice for audio processing.

packages/video-export/src/index.ts (1)

1-35: Well-organized package exports!

The index file is well-structured with clear categorization of exports (API, Core engine, Utilities, Types, Pipeline system). This makes the package API clear and easy to use.

packages/video-export/src/utils/timeline-transformer.ts (1)

181-183: Duration Calculation Verified
The trimEnd field represents the amount trimmed from the end (not an absolute timestamp). Across the export and rendering code—including the audio-processor (where trimEnd is converted to samples) and the base/frame renderers—the math consistently subtracts trimStart and trimEnd from duration. Therefore,

const actualDuration = element.duration - element.trimStart - element.trimEnd;

is correct and does not need changing.

packages/video-export/src/pipeline/transitions/base-transition.ts (1)

44-47: Fix incorrect cross-fade alpha calculation.

The cross-fade alpha calculation doesn't make sense. It goes from 1→0 in the first half, then 0→0.5 in the second half, which would make the content disappear and partially reappear.

For a proper cross-fade between two sources, you need:

       case 'cross':
-        // For cross-fade, this would need two frames to blend
-        alpha = this.progress < 0.5 ? (1 - this.progress * 2) : (this.progress - 0.5) * 2;
+        // Cross-fade requires blending two frames, not just alpha
+        // This should either:
+        // 1. Throw an error indicating cross-fade needs special handling
+        // 2. Default to fade-out behavior with a comment
+        throw new Error('Cross-fade requires two frames to blend and should be handled differently');

Likely an incorrect or invalid review comment.

Comment on lines +517 to +531
<Input
id="video-bitrate"
type="number"
min="1"
max="100"
step="0.5"
value={customSettings.videoBitrate || 10}
onChange={(e) =>
setCustomSettings(prev => ({
...prev,
videoBitrate: parseFloat(e.target.value)
}))
}
/>
</div>
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.

🛠️ Refactor suggestion

Add input validation for bitrate values

The numeric inputs rely only on HTML5 constraints which can be bypassed. Consider adding validation to ensure values stay within acceptable ranges.

   onChange={(e) => 
     setCustomSettings(prev => ({ 
       ...prev, 
-      videoBitrate: parseFloat(e.target.value) 
+      videoBitrate: Math.max(1, Math.min(100, parseFloat(e.target.value) || 10))
     }))
   }

Apply similar validation to the audio bitrate input as well.

📝 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
<Input
id="video-bitrate"
type="number"
min="1"
max="100"
step="0.5"
value={customSettings.videoBitrate || 10}
onChange={(e) =>
setCustomSettings(prev => ({
...prev,
videoBitrate: parseFloat(e.target.value)
}))
}
/>
</div>
<Input
id="video-bitrate"
type="number"
min="1"
max="100"
step="0.5"
value={customSettings.videoBitrate || 10}
onChange={(e) =>
setCustomSettings(prev => ({
...prev,
videoBitrate: Math.max(
1,
Math.min(
100,
parseFloat(e.target.value) || 10
)
)
}))
}
/>
🤖 Prompt for AI Agents
In apps/web/src/components/export/VideoExportDialog.tsx between lines 517 and
531, the video bitrate input lacks proper validation beyond HTML5 attributes,
which can be bypassed. Add explicit validation in the onChange handler to clamp
the videoBitrate value within the allowed range (e.g., 1 to 100) before updating
the state. Similarly, locate the audio bitrate input and apply the same
validation logic to ensure its value also stays within acceptable bounds.

}
},
"dependencies": {
"mediabunny": "^1.3.1"
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.

💡 Verification agent

🧩 Analysis chain

Verify mediabunny version and security.

Ensure that mediabunny ^1.3.1 is the latest stable version and check for any known security vulnerabilities.


🌐 Web query:

What is the latest version of the mediabunny npm package and are there any known security vulnerabilities in version 1.3.1?

💡 Result:

The latest version of the mediabunny npm package is 1.1.1, published two days ago. (socket.dev) Version 1.3.1 does not exist; the most recent release is 1.1.1.

Regarding security vulnerabilities, there are no known issues reported for version 1.1.1. The package has been analyzed by Socket, which monitors for malicious and high-risk dependencies, and no alerts have been issued. (socket.dev)

For more information or to report any concerns, you can visit the mediabunny GitHub repository.

Citations:


Invalid mediabunny version—please update to an existing release

The dependency in packages/video-export/package.json is set to

"mediabunny": "^1.3.1"

but version 1.3.1 does not exist on npm. The latest published version is 1.1.1, and there are no known security vulnerabilities in that release.

• Update line 14 of packages/video-export/package.json to:

- "mediabunny": "^1.3.1"
+ "mediabunny": "^1.1.1"
🤖 Prompt for AI Agents
In packages/video-export/package.json at line 14, the mediabunny dependency
version is set to 1.3.1, which does not exist on npm. Update the version to the
latest published release, 1.1.1, by changing the version string to "^1.1.1" to
ensure the dependency is valid and installable.

Comment on lines +46 to +47
timelineTracks: any[],
mediaItems: any[],
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.

🛠️ Refactor suggestion

⚠️ Potential issue

Replace any[] with proper types for better type safety.

Using any[] bypasses TypeScript's type checking and could lead to runtime errors. Define proper interfaces for the timeline data structures.

-    timelineTracks: any[],
-    mediaItems: any[],
+    timelineTracks: OpenCutTimelineTrack[],
+    mediaItems: OpenCutMediaItem[],

Consider importing these types from a shared types module or defining them in this package's types file.

📝 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
timelineTracks: any[],
mediaItems: any[],
timelineTracks: OpenCutTimelineTrack[],
mediaItems: OpenCutMediaItem[],
🤖 Prompt for AI Agents
In packages/video-export/src/api/video-exporter.ts at lines 46 to 47, replace
the use of `any[]` for `timelineTracks` and `mediaItems` with specific,
well-defined TypeScript interfaces to improve type safety. Define or import
appropriate interfaces that describe the structure of timeline tracks and media
items, either from a shared types module or by creating new type definitions
within this package. Update the variable declarations to use these interfaces
instead of `any[]`.

Comment on lines +53 to +58
console.log('Starting export with:', {
timelineTracks: timelineTracks.length,
mediaItems: mediaItems.length,
projectSettings
});

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.

🛠️ Refactor suggestion

Use debugLogger instead of console.log for consistency.

The file imports debugLogger but uses console.log directly. This inconsistency could make debugging harder in production.

-      console.log('Starting export with:', {
+      debugLogger.log('🎬 Starting export with:', {
         timelineTracks: timelineTracks.length,
         mediaItems: mediaItems.length,
         projectSettings
       });

-      console.log('Transformed timeline data:', timelineData);
+      debugLogger.log('📊 Transformed timeline data:', timelineData);

Also applies to: 66-66

🤖 Prompt for AI Agents
In packages/video-export/src/api/video-exporter.ts at lines 53 to 58 and line
66, replace the console.log calls with debugLogger calls to maintain logging
consistency. Use debugLogger to output the same messages instead of console.log,
ensuring all debug information is handled uniformly across the file.

Comment on lines +256 to +260
const hasAudioElements = timelineData.tracks.some(track =>
track.elements.some(el =>
el.type === 'media' && ((el as any).mediaType === 'audio' || (el as any).mediaType === 'video')
)
);
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.

🛠️ Refactor suggestion

Avoid type casting by properly typing the element.

The use of (el as any) indicates missing type information in the element definition.

-      track.elements.some(el => 
-        el.type === 'media' && ((el as any).mediaType === 'audio' || (el as any).mediaType === 'video')
+      track.elements.some(el => 
+        el.type === 'media' && (el as MediaExportElement).mediaType === 'audio' || (el as MediaExportElement).mediaType === 'video')

This requires importing MediaExportElement from the types module.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/video-export/src/api/video-exporter.ts around lines 256 to 260, the
code uses (el as any) to access mediaType, indicating missing type information.
To fix this, import MediaExportElement from the types module and properly type
the elements in the tracks so that el is recognized as MediaExportElement. This
will eliminate the need for type casting and improve type safety.

Comment on lines +334 to +344
export interface RenderFrame {
/** Frame number */
frameNumber: number;

/** Timestamp in seconds */
timestamp: number;

/** Canvas context for this frame */
canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D;
}
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.

🛠️ Refactor suggestion

Use OffscreenCanvas or generic context to support worker rendering

HTMLCanvasElement & CanvasRenderingContext2D tie RenderFrame to the main thread DOM. The encoder pipeline likely runs in a worker where only OffscreenCanvas exists. Suggest loosening the contract:

-export interface RenderFrame {
-  /** Frame number */
-  frameNumber: number;
-  /** Timestamp in seconds */
-  timestamp: number;
-  /** Canvas context for this frame */
-  canvas: HTMLCanvasElement | OffscreenCanvas;
-  context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
-}
+export interface RenderFrame<C = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D> {
+  frameNumber: number;
+  timestamp: number;
+  canvas: OffscreenCanvas | HTMLCanvasElement;
+  context: C;
+}

This keeps the type safe while unlocking worker-based rendering and WebCodecs’ VideoTrackGenerator.

📝 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
export interface RenderFrame {
/** Frame number */
frameNumber: number;
/** Timestamp in seconds */
timestamp: number;
/** Canvas context for this frame */
canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D;
}
export interface RenderFrame<C = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D> {
frameNumber: number;
timestamp: number;
canvas: OffscreenCanvas | HTMLCanvasElement;
context: C;
}
🤖 Prompt for AI Agents
In packages/video-export/src/types/index.ts around lines 334 to 344, the
RenderFrame interface currently uses HTMLCanvasElement and
CanvasRenderingContext2D, which restricts it to main thread DOM usage. To
support worker rendering and WebCodecs VideoTrackGenerator, update the types to
use OffscreenCanvas or a more generic canvas and context type that works in both
main thread and worker environments, ensuring type safety while enabling broader
compatibility.

Comment on lines +33 to +40
let sample = audioBuffer.getChannelData(channel)[i] * 32767; // Convert float [-1,1] to 16-bit PCM

// Clamp values to the Int16 range
if (sample > 32767) sample = 32767;
if (sample < -32767) sample = -32767;

interleaved[i * numberOfChannels + channel] = sample;
}
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

Fix Int16 clamping range.

The minimum value for Int16 should be -32768, not -32767.

       let sample = audioBuffer.getChannelData(channel)[i] * 32767; // Convert float [-1,1] to 16-bit PCM
       
       // Clamp values to the Int16 range
       if (sample > 32767) sample = 32767;
-      if (sample < -32767) sample = -32767;
+      if (sample < -32768) sample = -32768;
📝 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
let sample = audioBuffer.getChannelData(channel)[i] * 32767; // Convert float [-1,1] to 16-bit PCM
// Clamp values to the Int16 range
if (sample > 32767) sample = 32767;
if (sample < -32767) sample = -32767;
interleaved[i * numberOfChannels + channel] = sample;
}
let sample = audioBuffer.getChannelData(channel)[i] * 32767; // Convert float [-1,1] to 16-bit PCM
// Clamp values to the Int16 range
if (sample > 32767) sample = 32767;
if (sample < -32768) sample = -32768;
interleaved[i * numberOfChannels + channel] = sample;
}
🤖 Prompt for AI Agents
In packages/video-export/src/utils/audio-utils.ts around lines 33 to 40, the
clamping for the Int16 minimum value is incorrect; it should be -32768 instead
of -32767. Update the condition that clamps the sample value to use -32768 as
the lower bound to correctly represent the Int16 range.

Comment on lines +6 to +56
log(message: string, ...args: any[]) {
const timestamp = new Date().toLocaleTimeString();
const logEntry = `[${timestamp}] ${message}${args.length > 0 ? ' ' + args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ') : ''}`;

this.logs.push(logEntry);

// Keep only the last maxLogs entries
if (this.logs.length > this.maxLogs) {
this.logs = this.logs.slice(-this.maxLogs);
}

// Also log to console
console.log(message, ...args);

// Notify listeners
this.notifyListeners();
}

error(message: string, ...args: any[]) {
const timestamp = new Date().toLocaleTimeString();
const logEntry = `[${timestamp}] ERROR: ${message}${args.length > 0 ? ' ' + args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ') : ''}`;

this.logs.push(logEntry);

if (this.logs.length > this.maxLogs) {
this.logs = this.logs.slice(-this.maxLogs);
}

console.error(message, ...args);
this.notifyListeners();
}

warn(message: string, ...args: any[]) {
const timestamp = new Date().toLocaleTimeString();
const logEntry = `[${timestamp}] WARN: ${message}${args.length > 0 ? ' ' + args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ') : ''}`;

this.logs.push(logEntry);

if (this.logs.length > this.maxLogs) {
this.logs = this.logs.slice(-this.maxLogs);
}

console.warn(message, ...args);
this.notifyListeners();
}
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.

🛠️ Refactor suggestion

Extract duplicated logging logic into a private method.

The log formatting and array trimming logic is duplicated across log, error, and warn methods. This violates the DRY principle.

Apply this refactor to reduce duplication:

+  private formatLogEntry(level: string, message: string, args: any[]): string {
+    const timestamp = new Date().toLocaleTimeString();
+    const prefix = level ? `${level}: ` : '';
+    const formattedArgs = args.length > 0 ? ' ' + args.map(arg => 
+      typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
+    ).join(' ') : '';
+    return `[${timestamp}] ${prefix}${message}${formattedArgs}`;
+  }
+
+  private addLogEntry(entry: string): void {
+    this.logs.push(entry);
+    if (this.logs.length > this.maxLogs) {
+      this.logs = this.logs.slice(-this.maxLogs);
+    }
+    this.notifyListeners();
+  }

   log(message: string, ...args: any[]) {
-    const timestamp = new Date().toLocaleTimeString();
-    const logEntry = `[${timestamp}] ${message}${args.length > 0 ? ' ' + args.map(arg => 
-      typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
-    ).join(' ') : ''}`;
-    
-    this.logs.push(logEntry);
-    
-    // Keep only the last maxLogs entries
-    if (this.logs.length > this.maxLogs) {
-      this.logs = this.logs.slice(-this.maxLogs);
-    }
-    
+    const logEntry = this.formatLogEntry('', message, args);
+    this.addLogEntry(logEntry);
     // Also log to console
     console.log(message, ...args);
-    
-    // Notify listeners
-    this.notifyListeners();
   }

   error(message: string, ...args: any[]) {
-    const timestamp = new Date().toLocaleTimeString();
-    const logEntry = `[${timestamp}] ERROR: ${message}${args.length > 0 ? ' ' + args.map(arg => 
-      typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
-    ).join(' ') : ''}`;
-    
-    this.logs.push(logEntry);
-    
-    if (this.logs.length > this.maxLogs) {
-      this.logs = this.logs.slice(-this.maxLogs);
-    }
-    
+    const logEntry = this.formatLogEntry('ERROR', message, args);
+    this.addLogEntry(logEntry);
     console.error(message, ...args);
-    this.notifyListeners();
   }

   warn(message: string, ...args: any[]) {
-    const timestamp = new Date().toLocaleTimeString();
-    const logEntry = `[${timestamp}] WARN: ${message}${args.length > 0 ? ' ' + args.map(arg => 
-      typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
-    ).join(' ') : ''}`;
-    
-    this.logs.push(logEntry);
-    
-    if (this.logs.length > this.maxLogs) {
-      this.logs = this.logs.slice(-this.maxLogs);
-    }
-    
+    const logEntry = this.formatLogEntry('WARN', message, args);
+    this.addLogEntry(logEntry);
     console.warn(message, ...args);
-    this.notifyListeners();
   }
📝 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
log(message: string, ...args: any[]) {
const timestamp = new Date().toLocaleTimeString();
const logEntry = `[${timestamp}] ${message}${args.length > 0 ? ' ' + args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ') : ''}`;
this.logs.push(logEntry);
// Keep only the last maxLogs entries
if (this.logs.length > this.maxLogs) {
this.logs = this.logs.slice(-this.maxLogs);
}
// Also log to console
console.log(message, ...args);
// Notify listeners
this.notifyListeners();
}
error(message: string, ...args: any[]) {
const timestamp = new Date().toLocaleTimeString();
const logEntry = `[${timestamp}] ERROR: ${message}${args.length > 0 ? ' ' + args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ') : ''}`;
this.logs.push(logEntry);
if (this.logs.length > this.maxLogs) {
this.logs = this.logs.slice(-this.maxLogs);
}
console.error(message, ...args);
this.notifyListeners();
}
warn(message: string, ...args: any[]) {
const timestamp = new Date().toLocaleTimeString();
const logEntry = `[${timestamp}] WARN: ${message}${args.length > 0 ? ' ' + args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ') : ''}`;
this.logs.push(logEntry);
if (this.logs.length > this.maxLogs) {
this.logs = this.logs.slice(-this.maxLogs);
}
console.warn(message, ...args);
this.notifyListeners();
}
private formatLogEntry(level: string, message: string, args: any[]): string {
const timestamp = new Date().toLocaleTimeString();
const prefix = level ? `${level}: ` : '';
const formattedArgs = args.length > 0
? ' ' + args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ')
: '';
return `[${timestamp}] ${prefix}${message}${formattedArgs}`;
}
private addLogEntry(entry: string): void {
this.logs.push(entry);
if (this.logs.length > this.maxLogs) {
this.logs = this.logs.slice(-this.maxLogs);
}
this.notifyListeners();
}
log(message: string, ...args: any[]) {
const logEntry = this.formatLogEntry('', message, args);
this.addLogEntry(logEntry);
// Also log to console
console.log(message, ...args);
}
error(message: string, ...args: any[]) {
const logEntry = this.formatLogEntry('ERROR', message, args);
this.addLogEntry(logEntry);
console.error(message, ...args);
}
warn(message: string, ...args: any[]) {
const logEntry = this.formatLogEntry('WARN', message, args);
this.addLogEntry(logEntry);
console.warn(message, ...args);
}
🤖 Prompt for AI Agents
In packages/video-export/src/utils/debug-logger.ts between lines 6 and 56, the
log, error, and warn methods duplicate code for formatting log entries and
trimming the logs array. Refactor by extracting this common logic into a private
helper method that takes the log level and message with arguments, formats the
log entry, trims the logs array if needed, and pushes the entry. Then update the
log, error, and warn methods to call this helper and handle console output and
listener notification accordingly.

Comment on lines +8 to +10
const logEntry = `[${timestamp}] ${message}${args.length > 0 ? ' ' + args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ') : ''}`;
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

Add error handling for JSON.stringify to prevent logging failures.

JSON.stringify can throw errors on circular references or unsupported types like BigInt, which would cause the entire logging operation to fail.

Wrap JSON.stringify in a try-catch block:

-      typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
+      typeof arg === 'object' ? (() => {
+        try {
+          return JSON.stringify(arg);
+        } catch (e) {
+          return '[Circular or unserializable object]';
+        }
+      })() : String(arg)

Also applies to: 28-30, 44-46

🤖 Prompt for AI Agents
In packages/video-export/src/utils/debug-logger.ts around lines 8 to 10, 28 to
30, and 44 to 46, the use of JSON.stringify can throw errors on circular
references or unsupported types, causing logging to fail. Wrap each
JSON.stringify call in a try-catch block to safely handle errors by returning a
fallback string like "[Unable to stringify]" when an exception occurs, ensuring
the logging operation continues without interruption.

Comment on lines +219 to +222
for (const element of track.elements) {
if (!element.id || element.startTime < 0 || element.duration <= 0) {
errors.push(`Invalid element: ${element.id || 'unknown'} in track ${track.name}`);
}
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.

🛠️ Refactor suggestion

Add validation for trim values and calculated duration.

The validation should check if trim values are valid and if the calculated duration after trimming is positive.

       for (const element of track.elements) {
         if (!element.id || element.startTime < 0 || element.duration <= 0) {
           errors.push(`Invalid element: ${element.id || 'unknown'} in track ${track.name}`);
         }
+        
+        // Validate trim values
+        if (element.trimStart < 0 || element.trimEnd < 0) {
+          errors.push(`Invalid trim values for element: ${element.id}`);
+        }
+        
+        if (element.trimStart + element.trimEnd >= element.duration) {
+          errors.push(`Trim values exceed duration for element: ${element.id}`);
+        }
📝 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
for (const element of track.elements) {
if (!element.id || element.startTime < 0 || element.duration <= 0) {
errors.push(`Invalid element: ${element.id || 'unknown'} in track ${track.name}`);
}
for (const element of track.elements) {
if (!element.id || element.startTime < 0 || element.duration <= 0) {
errors.push(`Invalid element: ${element.id || 'unknown'} in track ${track.name}`);
}
// Validate trim values
if (element.trimStart < 0 || element.trimEnd < 0) {
errors.push(`Invalid trim values for element: ${element.id}`);
}
if (element.trimStart + element.trimEnd >= element.duration) {
errors.push(`Trim values exceed duration for element: ${element.id}`);
}
🤖 Prompt for AI Agents
In packages/video-export/src/utils/timeline-transformer.ts around lines 219 to
222, the current validation only checks element id, startTime, and duration but
does not validate trim values or the duration after trimming. Add checks to
ensure trim start and end values are valid (e.g., non-negative and within
element duration) and verify that the duration after applying trims is positive.
If these conditions fail, push an appropriate error message to the errors array.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant