mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
holy cow
This commit is contained in:
@@ -15,7 +15,11 @@ import {
|
||||
} from "@/constants/project-constants";
|
||||
import { buildDefaultScene } from "@/lib/scene-utils";
|
||||
import { generateThumbnail } from "@/lib/media-processing-utils";
|
||||
import { CURRENT_VERSION, runMigrations } from "@/lib/migrations";
|
||||
import {
|
||||
CURRENT_STORAGE_VERSION,
|
||||
migrations,
|
||||
runStorageMigrations,
|
||||
} from "@/lib/migrations";
|
||||
|
||||
export interface MigrationState {
|
||||
isMigrating: boolean;
|
||||
@@ -30,6 +34,7 @@ export class ProjectManager {
|
||||
private isLoading = true;
|
||||
private isInitialized = false;
|
||||
private invalidProjectIds = new Set<string>();
|
||||
private storageMigrationPromise: Promise<void> | null = null;
|
||||
private listeners = new Set<() => void>();
|
||||
private migrationState: MigrationState = {
|
||||
isMigrating: false,
|
||||
@@ -40,6 +45,43 @@ export class ProjectManager {
|
||||
|
||||
constructor(private editor: EditorCore) {}
|
||||
|
||||
private async ensureStorageMigrations(): Promise<void> {
|
||||
if (this.storageMigrationPromise) {
|
||||
await this.storageMigrationPromise;
|
||||
return;
|
||||
}
|
||||
|
||||
this.storageMigrationPromise = (async () => {
|
||||
let hasShownState = false;
|
||||
|
||||
await runStorageMigrations({
|
||||
migrations,
|
||||
callbacks: {
|
||||
onMigrationStart: ({ fromVersion, toVersion }) => {
|
||||
hasShownState = true;
|
||||
this.setMigrationState({
|
||||
isMigrating: true,
|
||||
fromVersion,
|
||||
toVersion,
|
||||
projectName: null,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (hasShownState) {
|
||||
this.setMigrationState({
|
||||
isMigrating: false,
|
||||
fromVersion: null,
|
||||
toVersion: null,
|
||||
projectName: null,
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
await this.storageMigrationPromise;
|
||||
}
|
||||
|
||||
async createNewProject({ name }: { name: string }): Promise<string> {
|
||||
const mainScene = buildDefaultScene({ name: "Main scene", isMain: true });
|
||||
const newProject: TProject = {
|
||||
@@ -59,7 +101,7 @@ export class ProjectManager {
|
||||
color: DEFAULT_COLOR,
|
||||
},
|
||||
},
|
||||
version: CURRENT_VERSION,
|
||||
version: CURRENT_STORAGE_VERSION,
|
||||
};
|
||||
|
||||
this.active = newProject;
|
||||
@@ -88,6 +130,7 @@ export class ProjectManager {
|
||||
this.notify();
|
||||
}
|
||||
|
||||
await this.ensureStorageMigrations();
|
||||
this.editor.media.clearAllAssets();
|
||||
this.editor.scenes.clearScenes();
|
||||
|
||||
@@ -97,34 +140,7 @@ export class ProjectManager {
|
||||
throw new Error(`Project with id ${id} not found`);
|
||||
}
|
||||
|
||||
let project = result.project;
|
||||
const migrationResult = runMigrations({ project });
|
||||
|
||||
if (migrationResult.migrated) {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.setMigrationState({
|
||||
isMigrating: true,
|
||||
fromVersion: migrationResult.fromVersion ?? null,
|
||||
toVersion: migrationResult.toVersion ?? null,
|
||||
projectName: project.metadata.name,
|
||||
});
|
||||
|
||||
project = migrationResult.project;
|
||||
await storageService.saveProject({ project });
|
||||
|
||||
const elapsed = Date.now() - startTime;
|
||||
if (elapsed < 300) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 300 - elapsed));
|
||||
}
|
||||
|
||||
this.setMigrationState({
|
||||
isMigrating: false,
|
||||
fromVersion: null,
|
||||
toVersion: null,
|
||||
projectName: null,
|
||||
});
|
||||
}
|
||||
const project = result.project;
|
||||
|
||||
this.active = project;
|
||||
this.notify();
|
||||
@@ -173,6 +189,7 @@ export class ProjectManager {
|
||||
this.notify();
|
||||
}
|
||||
|
||||
await this.ensureStorageMigrations();
|
||||
try {
|
||||
const metadata = await storageService.loadAllProjectsMetadata();
|
||||
this.savedProjects = metadata;
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
import type { EditorCore } from "@/core";
|
||||
import type { RootNode } from "@/services/renderer/nodes/root-node";
|
||||
import type { ExportOptions, ExportResult } from "@/types/export";
|
||||
import type { TimelineTrack } from "@/types/timeline";
|
||||
import type { MediaAsset } from "@/types/assets";
|
||||
import { SceneExporter } from "@/services/renderer/scene-exporter";
|
||||
import { buildScene } from "@/services/renderer/scene-builder";
|
||||
|
||||
interface AudioElement {
|
||||
buffer: AudioBuffer;
|
||||
startTime: number;
|
||||
duration: number;
|
||||
trimStart: number;
|
||||
trimEnd: number;
|
||||
muted: boolean;
|
||||
}
|
||||
import { createTimelineAudioBuffer } from "@/lib/audio-utils";
|
||||
|
||||
export class RendererManager {
|
||||
private renderTree: RootNode | null = null;
|
||||
@@ -58,7 +48,7 @@ export class RendererManager {
|
||||
let audioBuffer: AudioBuffer | null = null;
|
||||
if (includeAudio) {
|
||||
onProgress?.({ progress: 0.05 });
|
||||
audioBuffer = await this.createTimelineAudioBuffer({
|
||||
audioBuffer = await createTimelineAudioBuffer({
|
||||
tracks,
|
||||
mediaAssets,
|
||||
duration,
|
||||
@@ -128,121 +118,6 @@ export class RendererManager {
|
||||
}
|
||||
}
|
||||
|
||||
private async createTimelineAudioBuffer({
|
||||
tracks,
|
||||
mediaAssets,
|
||||
duration,
|
||||
sampleRate = 44100,
|
||||
}: {
|
||||
tracks: TimelineTrack[];
|
||||
mediaAssets: MediaAsset[];
|
||||
duration: number;
|
||||
sampleRate?: number;
|
||||
}): Promise<AudioBuffer | null> {
|
||||
const AudioContextClass =
|
||||
window.AudioContext ||
|
||||
(window as typeof window & { webkitAudioContext: typeof AudioContext })
|
||||
.webkitAudioContext;
|
||||
const audioContext = new AudioContextClass();
|
||||
|
||||
const audioElements: AudioElement[] = [];
|
||||
const mediaMap = new Map<string, MediaAsset>(
|
||||
mediaAssets.map((m) => [m.id, m]),
|
||||
);
|
||||
|
||||
for (const track of tracks) {
|
||||
if (track.muted) continue;
|
||||
|
||||
for (const element of track.elements) {
|
||||
if (element.type !== "audio") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (element.duration <= 0) continue;
|
||||
|
||||
try {
|
||||
let audioBuffer: AudioBuffer;
|
||||
|
||||
if (element.sourceType === "upload") {
|
||||
const mediaAsset = mediaMap.get(element.mediaId);
|
||||
if (!mediaAsset || mediaAsset.type !== "audio") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const arrayBuffer = await mediaAsset.file.arrayBuffer();
|
||||
audioBuffer = await audioContext.decodeAudioData(
|
||||
arrayBuffer.slice(0),
|
||||
);
|
||||
} else {
|
||||
// library audio - already has decoded buffer
|
||||
audioBuffer = element.buffer;
|
||||
}
|
||||
|
||||
audioElements.push({
|
||||
buffer: audioBuffer,
|
||||
startTime: element.startTime,
|
||||
duration: element.duration,
|
||||
trimStart: element.trimStart,
|
||||
trimEnd: element.trimEnd,
|
||||
muted: element.muted || track.muted || false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("Failed to decode audio:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (audioElements.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const outputChannels = 2;
|
||||
const outputLength = Math.ceil(duration * sampleRate);
|
||||
const outputBuffer = audioContext.createBuffer(
|
||||
outputChannels,
|
||||
outputLength,
|
||||
sampleRate,
|
||||
);
|
||||
|
||||
for (const element of audioElements) {
|
||||
if (element.muted) continue;
|
||||
|
||||
const {
|
||||
buffer,
|
||||
startTime,
|
||||
trimStart,
|
||||
duration: elementDuration,
|
||||
} = element;
|
||||
|
||||
const sourceStartSample = Math.floor(trimStart * buffer.sampleRate);
|
||||
const sourceLengthSamples = Math.floor(
|
||||
elementDuration * buffer.sampleRate,
|
||||
);
|
||||
const outputStartSample = Math.floor(startTime * sampleRate);
|
||||
|
||||
const resampleRatio = sampleRate / buffer.sampleRate;
|
||||
const resampledLength = Math.floor(sourceLengthSamples * resampleRatio);
|
||||
|
||||
for (let channel = 0; channel < outputChannels; channel++) {
|
||||
const outputData = outputBuffer.getChannelData(channel);
|
||||
const sourceChannel = Math.min(channel, buffer.numberOfChannels - 1);
|
||||
const sourceData = buffer.getChannelData(sourceChannel);
|
||||
|
||||
for (let i = 0; i < resampledLength; i++) {
|
||||
const outputIndex = outputStartSample + i;
|
||||
if (outputIndex >= outputLength) break;
|
||||
|
||||
const sourceIndex = sourceStartSample + Math.floor(i / resampleRatio);
|
||||
if (sourceIndex >= sourceData.length) break;
|
||||
|
||||
outputData[outputIndex] += sourceData[sourceIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return outputBuffer;
|
||||
}
|
||||
|
||||
subscribe(listener: () => void): () => void {
|
||||
this.listeners.add(listener);
|
||||
return () => this.listeners.delete(listener);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
removeBookmarkFromArray,
|
||||
isBookmarkAtTime,
|
||||
} from "@/lib/timeline/bookmark-utils";
|
||||
import { ensureMainTrack } from "@/lib/timeline/track-utils";
|
||||
|
||||
export class ScenesManager {
|
||||
private active: TScene | null = null;
|
||||
@@ -220,14 +221,34 @@ export class ScenesManager {
|
||||
try {
|
||||
const result = await storageService.loadProject({ id: projectId });
|
||||
if (result?.project.scenes) {
|
||||
const { scenes: ensuredScenes, hasAddedMainTrack } =
|
||||
this.ensureScenesHaveMainTrack({
|
||||
scenes: result.project.scenes ?? [],
|
||||
});
|
||||
const currentScene = findCurrentScene({
|
||||
scenes: result.project.scenes,
|
||||
scenes: ensuredScenes,
|
||||
currentSceneId: result.project.currentSceneId,
|
||||
});
|
||||
|
||||
this.list = result.project.scenes;
|
||||
this.list = ensuredScenes;
|
||||
this.active = currentScene;
|
||||
this.notify();
|
||||
|
||||
if (hasAddedMainTrack) {
|
||||
const activeProject = this.editor.project.getActive();
|
||||
if (activeProject) {
|
||||
const updatedProject = {
|
||||
...activeProject,
|
||||
scenes: ensuredScenes,
|
||||
metadata: {
|
||||
...activeProject.metadata,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
};
|
||||
await storageService.saveProject({ project: updatedProject });
|
||||
this.editor.project.setActiveProject({ project: updatedProject });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load project scenes:", error);
|
||||
@@ -245,23 +266,26 @@ export class ScenesManager {
|
||||
currentSceneId?: string;
|
||||
}): void {
|
||||
const ensuredScenes = ensureMainScene({ scenes });
|
||||
const { scenes: scenesWithMainTracks, hasAddedMainTrack } =
|
||||
this.ensureScenesHaveMainTrack({ scenes: ensuredScenes });
|
||||
const currentScene = currentSceneId
|
||||
? ensuredScenes.find((s) => s.id === currentSceneId)
|
||||
? scenesWithMainTracks.find((s) => s.id === currentSceneId)
|
||||
: null;
|
||||
|
||||
const fallbackScene = getMainScene({ scenes: ensuredScenes });
|
||||
const fallbackScene = getMainScene({ scenes: scenesWithMainTracks });
|
||||
|
||||
this.list = ensuredScenes;
|
||||
this.list = scenesWithMainTracks;
|
||||
this.active = currentScene || fallbackScene;
|
||||
this.notify();
|
||||
|
||||
if (ensuredScenes.length > scenes.length) {
|
||||
const hasAddedMainScene = ensuredScenes.length > scenes.length;
|
||||
if (hasAddedMainScene || hasAddedMainTrack) {
|
||||
const activeProject = this.editor.project.getActive();
|
||||
|
||||
if (activeProject) {
|
||||
const updatedProject = {
|
||||
...activeProject,
|
||||
scenes: ensuredScenes,
|
||||
scenes: scenesWithMainTracks,
|
||||
metadata: {
|
||||
...activeProject.metadata,
|
||||
updatedAt: new Date(),
|
||||
@@ -372,6 +396,31 @@ export class ScenesManager {
|
||||
}
|
||||
}
|
||||
|
||||
private ensureScenesHaveMainTrack({ scenes }: { scenes: TScene[] }): {
|
||||
scenes: TScene[];
|
||||
hasAddedMainTrack: boolean;
|
||||
} {
|
||||
let hasAddedMainTrack = false;
|
||||
const ensuredScenes: TScene[] = [];
|
||||
|
||||
for (const scene of scenes) {
|
||||
const existingTracks = scene.tracks ?? [];
|
||||
const updatedTracks = ensureMainTrack({ tracks: existingTracks });
|
||||
if (updatedTracks !== existingTracks) {
|
||||
hasAddedMainTrack = true;
|
||||
ensuredScenes.push({
|
||||
...scene,
|
||||
tracks: updatedTracks,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
} else {
|
||||
ensuredScenes.push(scene);
|
||||
}
|
||||
}
|
||||
|
||||
return { scenes: ensuredScenes, hasAddedMainTrack };
|
||||
}
|
||||
|
||||
private async updateProjectWithScenes({
|
||||
updatedScenes,
|
||||
updatedSceneId,
|
||||
|
||||
Reference in New Issue
Block a user