Merge pull request #2 from siddharthvaddem/v0.1.1

V0.1.1
This commit is contained in:
Sid
2025-11-23 17:04:49 -07:00
committed by GitHub
29 changed files with 2646 additions and 210 deletions
+11 -1
View File
@@ -6,12 +6,19 @@
"productName": "Openscreen",
"npmRebuild": true,
"buildDependenciesFromSource": true,
"compression": "maximum",
"directories": {
"output": "release/${version}"
},
"files": [
"dist",
"dist-electron"
"dist-electron",
"!*.png",
"!preview*.png",
"!*.md",
"!README.md",
"!CONTRIBUTING.md",
"!LICENSE"
],
"extraResources": [
{
@@ -19,6 +26,9 @@
"to": "assets/wallpapers"
}
],
"asarUnpack": [
"**/node_modules/uiohook-napi/**/*"
],
"mac": {
"target": [
"dmg"
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 798 KiB

After

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 660 B

After

Width:  |  Height:  |  Size: 630 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 353 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 713 KiB

After

Width:  |  Height:  |  Size: 853 KiB

+2419 -26
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -24,7 +24,7 @@
"clsx": "^2.1.1",
"dnd-timeline": "^2.2.0",
"lucide-react": "^0.545.0",
"mp4-muxer": "^5.2.2",
"mediabunny": "^1.25.1",
"mp4box": "^2.2.0",
"pixi.js": "^8.14.0",
"react": "^18.2.0",
@@ -44,16 +44,18 @@
"autoprefixer": "^10.4.21",
"electron": "^30.0.1",
"electron-builder": "^24.13.3",
"electron-icon-builder": "^2.0.1",
"electron-rebuild": "^3.2.9",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.18",
"terser": "^5.44.1",
"typescript": "^5.2.2",
"vite": "^5.1.6",
"vite-plugin-electron": "^0.28.6",
"vite-plugin-electron-renderer": "^0.14.5"
},
"main": "dist-electron/main.js"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 MiB

After

Width:  |  Height:  |  Size: 682 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 MiB

After

Width:  |  Height:  |  Size: 827 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 MiB

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 MiB

After

Width:  |  Height:  |  Size: 538 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 MiB

After

Width:  |  Height:  |  Size: 897 KiB

+19 -19
View File
@@ -1,7 +1,7 @@
import type React from "react";
import { useEffect, useRef, useImperativeHandle, forwardRef, useState, useMemo, useCallback } from "react";
import { getAssetPath } from "@/lib/assetPath";
import * as PIXI from 'pixi.js';
import { Application, Container, Sprite, Graphics, BlurFilter, Texture, VideoSource } from 'pixi.js';
import { ZOOM_DEPTH_SCALES, type ZoomRegion, type ZoomFocus, type ZoomDepth } from "./types";
import { DEFAULT_FOCUS, SMOOTHING_FACTOR, MIN_DELTA } from "./videoPlayback/constants";
import { clamp01 } from "./videoPlayback/mathUtils";
@@ -31,9 +31,9 @@ interface VideoPlaybackProps {
export interface VideoPlaybackRef {
video: HTMLVideoElement | null;
app: PIXI.Application | null;
videoSprite: PIXI.Sprite | null;
videoContainer: PIXI.Container | null;
app: Application | null;
videoSprite: Sprite | null;
videoContainer: Container | null;
play: () => Promise<void>;
pause: () => void;
}
@@ -56,10 +56,10 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
}, ref) => {
const videoRef = useRef<HTMLVideoElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const appRef = useRef<PIXI.Application | null>(null);
const videoSpriteRef = useRef<PIXI.Sprite | null>(null);
const videoContainerRef = useRef<PIXI.Container | null>(null);
const cameraContainerRef = useRef<PIXI.Container | null>(null);
const appRef = useRef<Application | null>(null);
const videoSpriteRef = useRef<Sprite | null>(null);
const videoContainerRef = useRef<Container | null>(null);
const cameraContainerRef = useRef<Container | null>(null);
const timeUpdateAnimationRef = useRef<number | null>(null);
const [pixiReady, setPixiReady] = useState(false);
const [videoReady, setVideoReady] = useState(false);
@@ -69,7 +69,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
const zoomRegionsRef = useRef<ZoomRegion[]>([]);
const selectedZoomIdRef = useRef<string | null>(null);
const animationStateRef = useRef({ scale: 1, focusX: DEFAULT_FOCUS.cx, focusY: DEFAULT_FOCUS.cy });
const blurFilterRef = useRef<PIXI.BlurFilter | null>(null);
const blurFilterRef = useRef<BlurFilter | null>(null);
const isDraggingFocusRef = useRef(false);
const stageSizeRef = useRef({ width: 0, height: 0 });
const videoSizeRef = useRef({ width: 0, height: 0 });
@@ -77,7 +77,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
const baseOffsetRef = useRef({ x: 0, y: 0 });
const baseMaskRef = useRef({ x: 0, y: 0, width: 0, height: 0 });
const cropBoundsRef = useRef({ startX: 0, endX: 0, startY: 0, endY: 0 });
const maskGraphicsRef = useRef<PIXI.Graphics | null>(null);
const maskGraphicsRef = useRef<Graphics | null>(null);
const isPlayingRef = useRef(isPlaying);
const isSeekingRef = useRef(false);
const allowPlaybackRef = useRef(false);
@@ -398,10 +398,10 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
if (!container) return;
let mounted = true;
let app: PIXI.Application | null = null;
let app: Application | null = null;
(async () => {
app = new PIXI.Application();
app = new Application();
await app.init({
width: container.clientWidth,
@@ -423,12 +423,12 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
container.appendChild(app.canvas);
// Camera container - this will be scaled/positioned for zoom
const cameraContainer = new PIXI.Container();
const cameraContainer = new Container();
cameraContainerRef.current = cameraContainer;
app.stage.addChild(cameraContainer);
// Video container - holds the masked video sprite
const videoContainer = new PIXI.Container();
const videoContainer = new Container();
videoContainerRef.current = videoContainer;
cameraContainer.addChild(videoContainer);
@@ -468,19 +468,19 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
if (!video || !app || !videoContainer) return;
if (video.videoWidth === 0 || video.videoHeight === 0) return;
const source = PIXI.VideoSource.from(video);
const source = VideoSource.from(video);
if ('autoPlay' in source) {
(source as { autoPlay?: boolean }).autoPlay = false;
}
if ('autoUpdate' in source) {
(source as { autoUpdate?: boolean }).autoUpdate = true;
}
const videoTexture = PIXI.Texture.from(source);
const videoTexture = Texture.from(source);
const videoSprite = new PIXI.Sprite(videoTexture);
const videoSprite = new Sprite(videoTexture);
videoSpriteRef.current = videoSprite;
const maskGraphics = new PIXI.Graphics();
const maskGraphics = new Graphics();
videoContainer.addChild(videoSprite);
videoContainer.addChild(maskGraphics);
videoContainer.mask = maskGraphics;
@@ -492,7 +492,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(({
focusY: DEFAULT_FOCUS.cy,
};
const blurFilter = new PIXI.BlurFilter();
const blurFilter = new BlurFilter();
blurFilter.quality = 3;
blurFilter.resolution = app.renderer.resolution;
blurFilter.blur = 0;
@@ -1,12 +1,12 @@
import * as PIXI from 'pixi.js';
import { Application, Sprite, Graphics } from 'pixi.js';
import { VIEWPORT_SCALE } from "./constants";
import type { CropRegion } from '../types';
interface LayoutParams {
container: HTMLDivElement;
app: PIXI.Application;
videoSprite: PIXI.Sprite;
maskGraphics: PIXI.Graphics;
app: Application;
videoSprite: Sprite;
maskGraphics: Graphics;
videoElement: HTMLVideoElement;
cropRegion?: CropRegion;
lockedVideoDimensions?: { width: number; height: number } | null;
@@ -1,8 +1,8 @@
import * as PIXI from 'pixi.js';
import { Container, BlurFilter } from 'pixi.js';
interface TransformParams {
cameraContainer: PIXI.Container;
blurFilter: PIXI.BlurFilter | null;
cameraContainer: Container;
blurFilter: BlurFilter | null;
stageSize: { width: number; height: number };
baseMask: { x: number; y: number; width: number; height: number };
zoomScale: number;
+16 -16
View File
@@ -1,4 +1,4 @@
import * as PIXI from 'pixi.js';
import { Application, Container, Sprite, Graphics, BlurFilter, Texture } from 'pixi.js';
import type { ZoomRegion, CropRegion } from '@/components/video-editor/types';
import { ZOOM_DEPTH_SCALES } from '@/components/video-editor/types';
import { findDominantRegion } from '@/components/video-editor/videoPlayback/zoomRegionUtils';
@@ -27,13 +27,13 @@ interface AnimationState {
// Renders video frames with all effects (background, zoom, crop, blur, shadow) to an offscreen canvas for export.
export class FrameRenderer {
private app: PIXI.Application | null = null;
private cameraContainer: PIXI.Container | null = null;
private videoContainer: PIXI.Container | null = null;
private videoSprite: PIXI.Sprite | null = null;
private backgroundSprite: PIXI.Sprite | null = null;
private maskGraphics: PIXI.Graphics | null = null;
private blurFilter: PIXI.BlurFilter | null = null;
private app: Application | null = null;
private cameraContainer: Container | null = null;
private videoContainer: Container | null = null;
private videoSprite: Sprite | null = null;
private backgroundSprite: Sprite | null = null;
private maskGraphics: Graphics | null = null;
private blurFilter: BlurFilter | null = null;
private shadowCanvas: HTMLCanvasElement | null = null;
private shadowCtx: CanvasRenderingContext2D | null = null;
private compositeCanvas: HTMLCanvasElement | null = null;
@@ -70,7 +70,7 @@ export class FrameRenderer {
}
// Initialize PixiJS with optimized settings for export performance
this.app = new PIXI.Application();
this.app = new Application();
await this.app.init({
canvas,
width: this.config.width,
@@ -82,8 +82,8 @@ export class FrameRenderer {
});
// Setup containers
this.cameraContainer = new PIXI.Container();
this.videoContainer = new PIXI.Container();
this.cameraContainer = new Container();
this.videoContainer = new Container();
this.app.stage.addChild(this.cameraContainer);
this.cameraContainer.addChild(this.videoContainer);
@@ -91,7 +91,7 @@ export class FrameRenderer {
await this.setupBackground();
// Setup blur filter for video container
this.blurFilter = new PIXI.BlurFilter();
this.blurFilter = new BlurFilter();
this.blurFilter.quality = 3;
this.blurFilter.resolution = this.app.renderer.resolution;
this.blurFilter.blur = 0;
@@ -120,7 +120,7 @@ export class FrameRenderer {
}
// Setup mask
this.maskGraphics = new PIXI.Graphics();
this.maskGraphics = new Graphics();
this.videoContainer.addChild(this.maskGraphics);
this.videoContainer.mask = this.maskGraphics;
}
@@ -251,13 +251,13 @@ export class FrameRenderer {
// Create or update video sprite from VideoFrame
if (!this.videoSprite) {
const texture = PIXI.Texture.from(videoFrame as any);
this.videoSprite = new PIXI.Sprite(texture);
const texture = Texture.from(videoFrame as any);
this.videoSprite = new Sprite(texture);
this.videoContainer.addChild(this.videoSprite);
} else {
// Destroy old texture to avoid memory leaks, then create new one
const oldTexture = this.videoSprite.texture;
const newTexture = PIXI.Texture.from(videoFrame as any);
const newTexture = Texture.from(videoFrame as any);
this.videoSprite.texture = newTexture;
oldTexture.destroy(true);
}
+57 -57
View File
@@ -1,31 +1,20 @@
import type { ExportConfig } from './types';
interface MP4MuxerOptions {
target: any; // ArrayBufferTarget instance
video: {
codec: string;
width: number;
height: number;
};
audio?: {
codec: string;
numberOfChannels: number;
sampleRate: number;
};
fastStart?: 'in-memory' | 'fragmented';
}
interface Muxer {
addVideoChunk(chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata): void;
addAudioChunk(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata): void;
finalize(): void;
target?: any;
}
import {
Output,
Mp4OutputFormat,
BufferTarget,
EncodedVideoPacketSource,
EncodedAudioPacketSource,
EncodedPacket
} from 'mediabunny';
export class VideoMuxer {
private muxer: Muxer | null = null;
private config: ExportConfig;
private output: Output | null = null;
private videoSource: EncodedVideoPacketSource | null = null;
private audioSource: EncodedAudioPacketSource | null = null;
private hasAudio: boolean;
private target: BufferTarget | null = null;
private config: ExportConfig;
constructor(config: ExportConfig, hasAudio = false) {
this.config = config;
@@ -33,57 +22,68 @@ export class VideoMuxer {
}
async initialize(): Promise<void> {
const MP4MuxerModule = await import('mp4-muxer');
const MP4MuxerClass = (MP4MuxerModule as any).Muxer || MP4MuxerModule.default;
const ArrayBufferTarget = (MP4MuxerModule as any).ArrayBufferTarget;
// Create the buffer target
this.target = new BufferTarget();
const target = new ArrayBufferTarget();
const options: MP4MuxerOptions = {
target,
video: {
codec: 'avc',
width: this.config.width,
height: this.config.height,
},
fastStart: 'in-memory',
};
this.output = new Output({
format: new Mp4OutputFormat({
fastStart: 'in-memory',
}),
target: this.target,
});
// Create video source - codec will be deduced from metadata
this.videoSource = new EncodedVideoPacketSource('avc');
this.output.addVideoTrack(this.videoSource, {
frameRate: this.config.frameRate,
});
// Create audio source if needed
if (this.hasAudio) {
options.audio = {
codec: 'opus',
numberOfChannels: 2,
sampleRate: 48000,
};
this.audioSource = new EncodedAudioPacketSource('opus');
this.output.addAudioTrack(this.audioSource);
}
this.muxer = new MP4MuxerClass(options) as Muxer;
// Start the output to begin accepting media data
await this.output.start();
}
addVideoChunk(chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata): void {
if (!this.muxer) {
async addVideoChunk(chunk: EncodedVideoChunk, meta?: EncodedVideoChunkMetadata): Promise<void> {
if (!this.videoSource) {
throw new Error('Muxer not initialized');
}
this.muxer.addVideoChunk(chunk, meta);
// Convert WebCodecs chunk to Mediabunny packet
const packet = EncodedPacket.fromEncodedChunk(chunk);
// Add metadata with the first chunk
await this.videoSource.add(packet, meta);
}
addAudioChunk(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata): void {
if (!this.muxer) {
throw new Error('Muxer not initialized');
}
if (!this.hasAudio) {
async addAudioChunk(chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata): Promise<void> {
if (!this.audioSource) {
throw new Error('Audio not configured for this muxer');
}
this.muxer.addAudioChunk(chunk, meta);
// Convert WebCodecs chunk to Mediabunny packet
const packet = EncodedPacket.fromEncodedChunk(chunk);
// Add metadata with the first chunk
await this.audioSource.add(packet, meta);
}
finalize(): Blob {
if (!this.muxer) {
async finalize(): Promise<Blob> {
if (!this.output || !this.target) {
throw new Error('Muxer not initialized');
}
this.muxer.finalize();
const buffer = this.muxer.target.buffer;
await this.output.finalize();
const buffer = this.target.buffer;
if (!buffer) {
throw new Error('Failed to finalize output');
}
return new Blob([buffer], { type: 'video/mp4' });
}
}
+92 -82
View File
@@ -21,12 +21,14 @@ export class VideoExporter {
private encoder: VideoEncoder | null = null;
private muxer: VideoMuxer | null = null;
private cancelled = false;
private encodedChunks: EncodedVideoChunk[] = [];
private encodeQueue = 0;
// Increased queue size for better throughput with hardware encoding
private readonly MAX_ENCODE_QUEUE = 120;
private videoDescription: Uint8Array | undefined;
private videoColorSpace: VideoColorSpaceInit | undefined;
// Track muxing promises for parallel processing
private muxingPromises: Promise<void>[] = [];
private chunkCount = 0;
constructor(config: VideoExporterConfig) {
this.config = config;
@@ -69,72 +71,67 @@ export class VideoExporter {
throw new Error('Video element not available');
}
// Process frames with optimized seeking
// Process frames continuously without batching delays
const frameDuration = 1_000_000 / this.config.frameRate; // in microseconds
let frameIndex = 0;
const timeStep = 1 / this.config.frameRate;
const BATCH_SIZE = 5; // Process frames in batches for better throughput
while (frameIndex < totalFrames && !this.cancelled) {
// Process a batch of frames
const batchEnd = Math.min(frameIndex + BATCH_SIZE, totalFrames);
for (let i = frameIndex; i < batchEnd && !this.cancelled; i++) {
const timestamp = i * frameDuration;
const videoTime = i * timeStep;
const i = frameIndex;
const timestamp = i * frameDuration;
const videoTime = i * timeStep;
// Seek if needed or wait for first frame to be ready
const needsSeek = Math.abs(videoElement.currentTime - videoTime) > 0.001;
if (needsSeek || i === 0) {
if (needsSeek) {
videoElement.currentTime = videoTime;
}
// Wait for video frame to be ready
await new Promise<void>(resolve => {
videoElement.requestVideoFrameCallback(() => resolve());
});
// Seek if needed or wait for first frame to be ready
const needsSeek = Math.abs(videoElement.currentTime - videoTime) > 0.001;
if (needsSeek || i === 0) {
if (needsSeek) {
videoElement.currentTime = videoTime;
}
// Create a VideoFrame from the video element (on GPU!)
const videoFrame = new VideoFrame(videoElement, {
timestamp,
// Wait for video frame to be ready
await new Promise<void>(resolve => {
videoElement.requestVideoFrameCallback(() => resolve());
});
// Render the frame with all effects
await this.renderer!.renderFrame(videoFrame, timestamp);
videoFrame.close();
const canvas = this.renderer!.getCanvas();
// Create VideoFrame from canvas on GPU without reading pixels
// @ts-ignore - colorSpace not in TypeScript definitions but works at runtime
const exportFrame = new VideoFrame(canvas, {
timestamp,
duration: frameDuration,
colorSpace: {
primaries: 'bt709',
transfer: 'iec61966-2-1',
matrix: 'rgb',
fullRange: true,
},
});
if (this.encoder && this.encoder.state === 'configured') {
this.encodeQueue++;
this.encoder.encode(exportFrame, { keyFrame: i % 150 === 0 });
}
exportFrame.close();
}
// Create a VideoFrame from the video element (on GPU!)
const videoFrame = new VideoFrame(videoElement, {
timestamp,
});
// Render the frame with all effects
await this.renderer!.renderFrame(videoFrame, timestamp);
// Wait for encoder queue once per batch
videoFrame.close();
const canvas = this.renderer!.getCanvas();
// Create VideoFrame from canvas on GPU without reading pixels
// @ts-ignore - colorSpace not in TypeScript definitions but works at runtime
const exportFrame = new VideoFrame(canvas, {
timestamp,
duration: frameDuration,
colorSpace: {
primaries: 'bt709',
transfer: 'iec61966-2-1',
matrix: 'rgb',
fullRange: true,
},
});
// Check encoder queue before encoding to keep it full
while (this.encodeQueue >= this.MAX_ENCODE_QUEUE && !this.cancelled) {
await new Promise(resolve => setTimeout(resolve, 0));
}
frameIndex = batchEnd;
if (this.encoder && this.encoder.state === 'configured') {
this.encodeQueue++;
this.encoder.encode(exportFrame, { keyFrame: i % 150 === 0 });
}
exportFrame.close();
// Batch progress updates to reduce callback overhead
frameIndex++;
// Update progress
if (this.config.onProgress) {
this.config.onProgress({
currentFrame: frameIndex,
@@ -154,35 +151,11 @@ export class VideoExporter {
await this.encoder.flush();
}
// Add all chunks to muxer with metadata
for (let i = 0; i < this.encodedChunks.length; i++) {
const chunk = this.encodedChunks[i];
const meta: EncodedVideoChunkMetadata = {};
// Add decoder config for the first chunk
if (i === 0 && this.videoDescription) {
// Use captured colorSpace from encoder or fallback to default sRGB colorspace
const colorSpace = this.videoColorSpace || {
primaries: 'bt709',
transfer: 'iec61966-2-1',
matrix: 'rgb',
fullRange: true,
};
meta.decoderConfig = {
codec: this.config.codec || 'avc1.640033',
codedWidth: this.config.width,
codedHeight: this.config.height,
description: this.videoDescription,
colorSpace,
};
}
this.muxer!.addVideoChunk(chunk, meta);
}
// Wait for all muxing operations to complete
await Promise.all(this.muxingPromises);
// Finalize muxer and get output blob
const blob = this.muxer!.finalize();
const blob = await this.muxer!.finalize();
return { success: true, blob };
} catch (error) {
@@ -197,8 +170,9 @@ export class VideoExporter {
}
private async initializeEncoder(): Promise<void> {
this.encodedChunks = [];
this.encodeQueue = 0;
this.muxingPromises = [];
this.chunkCount = 0;
let videoDescription: Uint8Array | undefined;
this.encoder = new VideoEncoder({
@@ -213,7 +187,42 @@ export class VideoExporter {
if (meta?.decoderConfig?.colorSpace && !this.videoColorSpace) {
this.videoColorSpace = meta.decoderConfig.colorSpace;
}
this.encodedChunks.push(chunk);
// Stream chunk to muxer immediately (parallel processing)
const isFirstChunk = this.chunkCount === 0;
this.chunkCount++;
const muxingPromise = (async () => {
try {
if (isFirstChunk && this.videoDescription) {
// Add decoder config for the first chunk
const colorSpace = this.videoColorSpace || {
primaries: 'bt709',
transfer: 'iec61966-2-1',
matrix: 'rgb',
fullRange: true,
};
const metadata: EncodedVideoChunkMetadata = {
decoderConfig: {
codec: this.config.codec || 'avc1.640033',
codedWidth: this.config.width,
codedHeight: this.config.height,
description: this.videoDescription,
colorSpace,
},
};
await this.muxer!.addVideoChunk(chunk, metadata);
} else {
await this.muxer!.addVideoChunk(chunk, meta);
}
} catch (error) {
console.error('Muxing error:', error);
}
})();
this.muxingPromises.push(muxingPromise);
this.encodeQueue--;
},
error: (error) => {
@@ -271,8 +280,9 @@ export class VideoExporter {
}
this.muxer = null;
this.encodedChunks = [];
this.encodeQueue = 0;
this.muxingPromises = [];
this.chunkCount = 0;
this.videoDescription = undefined;
this.videoColorSpace = undefined;
}
+21
View File
@@ -39,4 +39,25 @@ export default defineConfig({
'@': path.resolve(__dirname, 'src'),
},
},
build: {
target: 'esnext',
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.debug']
}
},
rollupOptions: {
output: {
manualChunks: {
'pixi': ['pixi.js'],
'react-vendor': ['react', 'react-dom'],
'video-processing': ['mediabunny', 'mp4box', '@fix-webm-duration/fix']
}
}
},
chunkSizeWarningLimit: 1000
}
})