summaryrefslogtreecommitdiff
path: root/apps/web/src/services/storage/migrations/transformers
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/services/storage/migrations/transformers')
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts313
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts136
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts218
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts3
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts10
-rw-r--r--apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts45
6 files changed, 357 insertions, 368 deletions
diff --git a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts
index c4b64e50..065a6b34 100644
--- a/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v1-to-v2.ts
@@ -1,17 +1,24 @@
-import { DEFAULT_BACKGROUND_BLUR_INTENSITY } from "@/background/blur";
-import { DEFAULT_BACKGROUND_COLOR } from "@/background/color";
-import { DEFAULT_CANVAS_SIZE } from "@/canvas/sizes";
-const DEFAULT_FPS = 30;
-import { IndexedDBAdapter } from "@/services/storage/indexeddb-adapter";
-import type { MediaAssetData } from "@/services/storage/types";
import type { MigrationResult, ProjectRecord } from "./types";
import { getProjectId, isRecord } from "./utils";
-interface LegacyTimelineData {
- tracks: unknown[];
- lastModified: string;
+// Frozen snapshots of v2-era defaults. See ./README.md.
+const DEFAULT_BACKGROUND_BLUR_INTENSITY = 10;
+const DEFAULT_BACKGROUND_COLOR = "#000000";
+const DEFAULT_CANVAS_SIZE = { width: 1920, height: 1080 };
+const DEFAULT_FPS = 30;
+
+type LegacyMediaType = "image" | "video" | "audio";
+
+export interface V1ToV2Context {
+ legacyTracksBySceneId: Record<string, unknown[]>;
+ mediaTypesById: Record<string, LegacyMediaType>;
}
+const EMPTY_V1_TO_V2_CONTEXT: V1ToV2Context = {
+ legacyTracksBySceneId: {},
+ mediaTypesById: {},
+};
+
interface LegacyMediaElement {
type: "media";
mediaId: string;
@@ -146,21 +153,13 @@ interface V2AudioTrack {
type V2TimelineTrack = V2VideoTrack | V2TextTrack | V2AudioTrack;
-export interface TransformV1ToV2Options {
- loadMediaAsset?: ({
- mediaId,
- }: {
- mediaId: string;
- }) => Promise<MediaAssetData | null>;
-}
-
-export async function transformProjectV1ToV2({
+export function transformProjectV1ToV2({
project,
- options = {},
+ context = EMPTY_V1_TO_V2_CONTEXT,
}: {
project: ProjectRecord;
- options?: TransformV1ToV2Options;
-}): Promise<MigrationResult<ProjectRecord>> {
+ context?: V1ToV2Context;
+}): MigrationResult<ProjectRecord> {
const projectId = getProjectId({ project });
if (!projectId) {
return { project, skipped: true, reason: "no project id" };
@@ -170,27 +169,23 @@ export async function transformProjectV1ToV2({
return { project, skipped: true, reason: "already v2" };
}
- const migratedProject = await migrateProject({
+ const migratedProject = migrateProject({
project,
projectId,
- loadMediaAsset: options.loadMediaAsset,
+ context,
});
return { project: migratedProject, skipped: false };
}
-async function migrateProject({
+function migrateProject({
project,
projectId,
- loadMediaAsset,
+ context,
}: {
project: ProjectRecord;
projectId: string;
- loadMediaAsset?: ({
- mediaId,
- }: {
- mediaId: string;
- }) => Promise<MediaAssetData | null>;
-}): Promise<ProjectRecord> {
+ context: V1ToV2Context;
+}): ProjectRecord {
const createdAt = normalizeDateString({ value: project.createdAt });
const updatedAt = normalizeDateString({ value: project.updatedAt });
const metadataValue = project.metadata;
@@ -217,42 +212,35 @@ async function migrateProject({
? project.bookmarks
: null;
- const migratedScenes = await Promise.all(
- scenes.map(async (scene) => {
- if (!isRecord(scene)) {
- return scene;
- }
-
- const sceneId = getStringValue({ value: scene.id });
- if (!sceneId) {
- return scene;
- }
+ const migratedScenes = scenes.map((scene) => {
+ if (!isRecord(scene)) {
+ return scene;
+ }
- const existingTracks = scene.tracks;
- const shouldLoadTracks =
- !Array.isArray(existingTracks) || existingTracks.length === 0;
+ const sceneId = getStringValue({ value: scene.id });
+ if (!sceneId) {
+ return scene;
+ }
- if (!shouldLoadTracks) {
- return scene;
- }
+ const existingTracks = scene.tracks;
+ const shouldLoadTracks =
+ !Array.isArray(existingTracks) || existingTracks.length === 0;
- const tracks = await loadTracksFromLegacyDB({
- projectId,
- sceneId,
- isMain: scene.isMain === true,
- });
+ if (!shouldLoadTracks) {
+ return scene;
+ }
- const transformedTracks = await transformTracks({
- tracks,
- loadMediaAsset,
- });
+ const tracks = context.legacyTracksBySceneId[sceneId] ?? [];
+ const transformedTracks = transformTracks({
+ tracks,
+ context,
+ });
- return {
- ...scene,
- tracks: transformedTracks,
- };
- }),
- );
+ return {
+ ...scene,
+ tracks: transformedTracks,
+ };
+ });
const normalizedScenes = applyLegacyBookmarks({
scenes: migratedScenes,
@@ -305,183 +293,126 @@ async function migrateProject({
};
}
-async function loadTracksFromLegacyDB({
- projectId,
- sceneId,
- isMain,
-}: {
- projectId: string;
- sceneId: string;
- isMain: boolean;
-}): Promise<unknown[]> {
- if (typeof indexedDB === "undefined") {
- return [];
- }
-
- const sceneDbName = `video-editor-timelines-${projectId}-${sceneId}`;
- const projectDbName = `video-editor-timelines-${projectId}`;
-
- const adapter = new IndexedDBAdapter<LegacyTimelineData>(
- sceneDbName,
- "timeline",
- 1,
- );
-
- let data = await adapter.get("timeline");
-
- if (!data && isMain) {
- const projectAdapter = new IndexedDBAdapter<LegacyTimelineData>(
- projectDbName,
- "timeline",
- 1,
- );
- data = await projectAdapter.get("timeline");
- }
-
- if (!data || !Array.isArray(data.tracks)) {
- return [];
- }
-
- return data.tracks;
-}
-
-async function transformTracks({
+function transformTracks({
tracks,
- loadMediaAsset,
+ context,
}: {
tracks: unknown[];
- loadMediaAsset?: ({
- mediaId,
- }: {
- mediaId: string;
- }) => Promise<MediaAssetData | null>;
-}): Promise<V2TimelineTrack[]> {
+ context: V1ToV2Context;
+}): V2TimelineTrack[] {
if (!Array.isArray(tracks)) {
return [];
}
let isFirstVideoTrackFound = false;
- const transformedTracks: (V2TimelineTrack | null)[] = [];
-
- for (const track of tracks) {
+ const transformedTracks = tracks.map((track): V2TimelineTrack | null => {
if (!isRecord(track)) {
- transformedTracks.push(null);
- continue;
+ return null;
}
const trackType = track.type;
if (trackType === "media") {
- const videoTrack = await transformMediaTrack({
+ const isMain = !isFirstVideoTrackFound;
+ isFirstVideoTrackFound = true;
+ const videoTrack = transformMediaTrack({
track: track as LegacyMediaTrack,
- loadMediaAsset,
- isMain: !isFirstVideoTrackFound,
+ context,
+ isMain,
});
- isFirstVideoTrackFound = true;
- transformedTracks.push(videoTrack);
- continue;
+ return videoTrack;
}
if (trackType === "text") {
- transformedTracks.push(transformTextTrack({ track }));
- continue;
+ return transformTextTrack({ track });
}
if (trackType === "audio") {
- transformedTracks.push(transformAudioTrack({ track }));
- continue;
+ return transformAudioTrack({ track });
}
- transformedTracks.push(null);
- }
+ return null;
+ });
return transformedTracks.filter(
(track): track is V2TimelineTrack => track !== null,
);
}
-async function transformMediaTrack({
+function transformMediaTrack({
track,
- loadMediaAsset,
+ context,
isMain,
}: {
track: LegacyMediaTrack;
- loadMediaAsset?: ({
- mediaId,
- }: {
- mediaId: string;
- }) => Promise<MediaAssetData | null>;
+ context: V1ToV2Context;
isMain: boolean;
-}): Promise<V2VideoTrack> {
+}): V2VideoTrack {
const elements = Array.isArray(track.elements) ? track.elements : [];
- const transformedElements = await Promise.all(
- elements.map(async (element) => {
- if (!isRecord(element) || element.type !== "media") {
- return null;
- }
-
- const mediaElement = element as LegacyMediaElement;
- const mediaId = getStringValue({ value: mediaElement.mediaId });
- if (!mediaId) {
- return null;
- }
+ const transformedElements = elements.map((element) => {
+ if (!isRecord(element) || element.type !== "media") {
+ return null;
+ }
- let mediaType: "video" | "image" = "video";
- if (loadMediaAsset) {
- const mediaAsset = await loadMediaAsset({ mediaId });
- if (mediaAsset) {
- mediaType = mediaAsset.type === "image" ? "image" : "video";
- }
- }
+ const mediaElement = element as LegacyMediaElement;
+ const mediaId = getStringValue({ value: mediaElement.mediaId });
+ if (!mediaId) {
+ return null;
+ }
- const defaultTransform: V2Transform = {
- scale: 1,
- position: { x: 0, y: 0 },
- rotate: 0,
- };
+ let mediaType: "video" | "image" = "video";
+ const storedMediaType = context.mediaTypesById[mediaId];
+ if (storedMediaType) {
+ mediaType = storedMediaType === "image" ? "image" : "video";
+ }
- const muted = mediaElement.muted === true;
+ const defaultTransform: V2Transform = {
+ scale: 1,
+ position: { x: 0, y: 0 },
+ rotate: 0,
+ };
- if (mediaType === "image") {
- const imageElement: V2ImageElement = {
- id: getStringValue({ value: element.id, fallback: "" }),
- name: getStringValue({ value: element.name, fallback: "" }),
- type: "image",
- mediaId,
- duration: getNumberValue({ value: element.duration, fallback: 0 }),
- startTime: getNumberValue({
- value: element.startTime,
- fallback: 0,
- }),
- trimStart: getNumberValue({
- value: element.trimStart,
- fallback: 0,
- }),
- trimEnd: getNumberValue({ value: element.trimEnd, fallback: 0 }),
- hidden: false,
- transform: defaultTransform,
- opacity: 1,
- };
- return imageElement;
- }
+ const muted = mediaElement.muted === true;
- const videoElement: V2VideoElement = {
+ if (mediaType === "image") {
+ const imageElement: V2ImageElement = {
id: getStringValue({ value: element.id, fallback: "" }),
name: getStringValue({ value: element.name, fallback: "" }),
- type: "video",
+ type: "image",
mediaId,
- muted,
+ duration: getNumberValue({ value: element.duration, fallback: 0 }),
+ startTime: getNumberValue({
+ value: element.startTime,
+ fallback: 0,
+ }),
+ trimStart: getNumberValue({
+ value: element.trimStart,
+ fallback: 0,
+ }),
+ trimEnd: getNumberValue({ value: element.trimEnd, fallback: 0 }),
hidden: false,
transform: defaultTransform,
opacity: 1,
- duration: getNumberValue({ value: element.duration, fallback: 0 }),
- startTime: getNumberValue({ value: element.startTime, fallback: 0 }),
- trimStart: getNumberValue({ value: element.trimStart, fallback: 0 }),
- trimEnd: getNumberValue({ value: element.trimEnd, fallback: 0 }),
};
- return videoElement;
- }),
- );
+ return imageElement;
+ }
+
+ const videoElement: V2VideoElement = {
+ id: getStringValue({ value: element.id, fallback: "" }),
+ name: getStringValue({ value: element.name, fallback: "" }),
+ type: "video",
+ mediaId,
+ muted,
+ hidden: false,
+ transform: defaultTransform,
+ opacity: 1,
+ duration: getNumberValue({ value: element.duration, fallback: 0 }),
+ startTime: getNumberValue({ value: element.startTime, fallback: 0 }),
+ trimStart: getNumberValue({ value: element.trimStart, fallback: 0 }),
+ trimEnd: getNumberValue({ value: element.trimEnd, fallback: 0 }),
+ };
+ return videoElement;
+ });
const validElements = transformedElements.filter(
(element): element is V2VideoElement | V2ImageElement => element !== null,
diff --git a/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts b/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts
index 5f3c01e9..84b2704a 100644
--- a/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v14-to-v15.ts
@@ -1,67 +1,69 @@
-import { STICKER_INTRINSIC_SIZE_FALLBACK } from "@/stickers/intrinsic-size";
-import type { MigrationResult, ProjectRecord } from "./types";
-import { getProjectId, isRecord } from "./utils";
-
-export function transformProjectV14ToV15({
- project,
-}: {
- project: ProjectRecord;
-}): MigrationResult<ProjectRecord> {
- if (!getProjectId({ project })) {
- return { project, skipped: true, reason: "no project id" };
- }
-
- if (typeof project.version === "number" && project.version >= 15) {
- return { project, skipped: true, reason: "already v15" };
- }
-
- const migratedProject = backfillStickerIntrinsicDimensions({ project });
-
- return {
- project: { ...migratedProject, version: 15 },
- skipped: false,
- };
-}
-
-function backfillStickerIntrinsicDimensions({
- project,
-}: {
- project: ProjectRecord;
-}): ProjectRecord {
- const scenesValue = project.scenes;
- if (!Array.isArray(scenesValue)) return project;
-
- const migratedScenes = scenesValue.map((scene) => {
- if (!isRecord(scene)) return scene;
- const tracksValue = scene.tracks;
- if (!Array.isArray(tracksValue)) return scene;
-
- const migratedTracks = tracksValue.map((track) => {
- if (!isRecord(track)) return track;
- const elementsValue = track.elements;
- if (!Array.isArray(elementsValue)) return track;
-
- const migratedElements = elementsValue.map((element) => {
- if (!isRecord(element)) return element;
- if (element.type !== "sticker") return element;
- if (
- typeof element.intrinsicWidth === "number" &&
- typeof element.intrinsicHeight === "number"
- ) {
- return element;
- }
- return {
- ...element,
- intrinsicWidth: STICKER_INTRINSIC_SIZE_FALLBACK,
- intrinsicHeight: STICKER_INTRINSIC_SIZE_FALLBACK,
- };
- });
-
- return { ...track, elements: migratedElements };
- });
-
- return { ...scene, tracks: migratedTracks };
- });
-
- return { ...project, scenes: migratedScenes };
-}
+import type { MigrationResult, ProjectRecord } from "./types";
+import { getProjectId, isRecord } from "./utils";
+
+// Frozen snapshot of the v15-era fallback. See ./README.md.
+const STICKER_INTRINSIC_SIZE_FALLBACK = 200;
+
+export function transformProjectV14ToV15({
+ project,
+}: {
+ project: ProjectRecord;
+}): MigrationResult<ProjectRecord> {
+ if (!getProjectId({ project })) {
+ return { project, skipped: true, reason: "no project id" };
+ }
+
+ if (typeof project.version === "number" && project.version >= 15) {
+ return { project, skipped: true, reason: "already v15" };
+ }
+
+ const migratedProject = backfillStickerIntrinsicDimensions({ project });
+
+ return {
+ project: { ...migratedProject, version: 15 },
+ skipped: false,
+ };
+}
+
+function backfillStickerIntrinsicDimensions({
+ project,
+}: {
+ project: ProjectRecord;
+}): ProjectRecord {
+ const scenesValue = project.scenes;
+ if (!Array.isArray(scenesValue)) return project;
+
+ const migratedScenes = scenesValue.map((scene) => {
+ if (!isRecord(scene)) return scene;
+ const tracksValue = scene.tracks;
+ if (!Array.isArray(tracksValue)) return scene;
+
+ const migratedTracks = tracksValue.map((track) => {
+ if (!isRecord(track)) return track;
+ const elementsValue = track.elements;
+ if (!Array.isArray(elementsValue)) return track;
+
+ const migratedElements = elementsValue.map((element) => {
+ if (!isRecord(element)) return element;
+ if (element.type !== "sticker") return element;
+ if (
+ typeof element.intrinsicWidth === "number" &&
+ typeof element.intrinsicHeight === "number"
+ ) {
+ return element;
+ }
+ return {
+ ...element,
+ intrinsicWidth: STICKER_INTRINSIC_SIZE_FALLBACK,
+ intrinsicHeight: STICKER_INTRINSIC_SIZE_FALLBACK,
+ };
+ });
+
+ return { ...track, elements: migratedElements };
+ });
+
+ return { ...scene, tracks: migratedTracks };
+ });
+
+ return { ...project, scenes: migratedScenes };
+}
diff --git a/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts b/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts
index 3ef6cde9..660d91ee 100644
--- a/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v17-to-v18.ts
@@ -1,105 +1,113 @@
-import type { MigrationResult, ProjectRecord } from "./types";
-import { VOLUME_DB_MIN } from "@/timeline/audio-constants";
-import { clampDb } from "@/timeline/audio-state";
-import { getProjectId, isRecord } from "./utils";
-
-export function transformProjectV17ToV18({
- project,
-}: {
- project: ProjectRecord;
-}): MigrationResult<ProjectRecord> {
- if (!getProjectId({ project })) {
- return { project, skipped: true, reason: "no project id" };
- }
-
- if (typeof project.version === "number" && project.version >= 18) {
- return { project, skipped: true, reason: "already v18" };
- }
-
- return {
- project: {
- ...migrateElementVolumes({ project }),
- version: 18,
- },
- skipped: false,
- };
-}
-
-function migrateElementVolumes({
- project,
-}: {
- project: ProjectRecord;
-}): ProjectRecord {
- const scenesValue = project.scenes;
- if (!Array.isArray(scenesValue)) {
- return project;
- }
-
- const migratedScenes = scenesValue.map((scene) => {
- if (!isRecord(scene)) {
- return scene;
- }
-
- const tracksValue = scene.tracks;
- if (!Array.isArray(tracksValue)) {
- return scene;
- }
-
- const migratedTracks = tracksValue.map((track) => {
- if (!isRecord(track)) {
- return track;
- }
-
- const elementsValue = track.elements;
- if (!Array.isArray(elementsValue)) {
- return track;
- }
-
- const migratedElements = elementsValue.map((element) => {
- if (!isRecord(element)) {
- return element;
- }
-
- if (element.type !== "audio" && element.type !== "video") {
- return element;
- }
-
- const legacyVolume = element.volume;
- return {
- ...element,
- volume:
- typeof legacyVolume === "number"
- ? linearGainToDb(legacyVolume)
- : 0,
- };
- });
-
- return {
- ...track,
- elements: migratedElements,
- };
- });
-
- return {
- ...scene,
- tracks: migratedTracks,
- };
- });
-
- return {
- ...project,
- scenes: migratedScenes,
- };
-}
-
-function linearGainToDb(gain: number): number {
- if (!Number.isFinite(gain)) {
- return 0;
- }
-
- if (gain <= 0) {
- return VOLUME_DB_MIN;
- }
-
- return clampDb(20 * Math.log10(gain));
-}
+import type { MigrationResult, ProjectRecord } from "./types";
+import { getProjectId, isRecord } from "./utils";
+
+// Frozen snapshots of v18-era volume bounds. See ./README.md.
+const VOLUME_DB_MIN = -60;
+const VOLUME_DB_MAX = 20;
+
+export function transformProjectV17ToV18({
+ project,
+}: {
+ project: ProjectRecord;
+}): MigrationResult<ProjectRecord> {
+ if (!getProjectId({ project })) {
+ return { project, skipped: true, reason: "no project id" };
+ }
+
+ if (typeof project.version === "number" && project.version >= 18) {
+ return { project, skipped: true, reason: "already v18" };
+ }
+
+ return {
+ project: {
+ ...migrateElementVolumes({ project }),
+ version: 18,
+ },
+ skipped: false,
+ };
+}
+
+function migrateElementVolumes({
+ project,
+}: {
+ project: ProjectRecord;
+}): ProjectRecord {
+ const scenesValue = project.scenes;
+ if (!Array.isArray(scenesValue)) {
+ return project;
+ }
+
+ const migratedScenes = scenesValue.map((scene) => {
+ if (!isRecord(scene)) {
+ return scene;
+ }
+
+ const tracksValue = scene.tracks;
+ if (!Array.isArray(tracksValue)) {
+ return scene;
+ }
+
+ const migratedTracks = tracksValue.map((track) => {
+ if (!isRecord(track)) {
+ return track;
+ }
+
+ const elementsValue = track.elements;
+ if (!Array.isArray(elementsValue)) {
+ return track;
+ }
+
+ const migratedElements = elementsValue.map((element) => {
+ if (!isRecord(element)) {
+ return element;
+ }
+
+ if (element.type !== "audio" && element.type !== "video") {
+ return element;
+ }
+
+ const legacyVolume = element.volume;
+ return {
+ ...element,
+ volume:
+ typeof legacyVolume === "number" ? linearGainToDb(legacyVolume) : 0,
+ };
+ });
+
+ return {
+ ...track,
+ elements: migratedElements,
+ };
+ });
+
+ return {
+ ...scene,
+ tracks: migratedTracks,
+ };
+ });
+
+ return {
+ ...project,
+ scenes: migratedScenes,
+ };
+}
+
+function linearGainToDb(gain: number): number {
+ if (!Number.isFinite(gain)) {
+ return 0;
+ }
+
+ if (gain <= 0) {
+ return VOLUME_DB_MIN;
+ }
+
+ return clampDb(20 * Math.log10(gain));
+}
+
+function clampDb(value: number): number {
+ if (!Number.isFinite(value)) {
+ return 0;
+ }
+
+ return Math.min(VOLUME_DB_MAX, Math.max(VOLUME_DB_MIN, value));
+}
diff --git a/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts b/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts
index 6c30e5c8..14b7a092 100644
--- a/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v20-to-v21.ts
@@ -1,7 +1,8 @@
-import { INTENSITY_TO_SIGMA_DIVISOR } from "@/effects/definitions/blur";
import type { MigrationResult, ProjectRecord } from "./types";
import { getProjectId, isRecord } from "./utils";
+// Frozen snapshot of the v21-era divisor. See ./README.md.
+const INTENSITY_TO_SIGMA_DIVISOR = 5;
const LEGACY_DEFAULT_BACKGROUND_BLUR_INTENSITY = 50;
export function transformProjectV20ToV21({
diff --git a/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts b/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts
index 5e3adc33..24cc86e2 100644
--- a/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v22-to-v23.ts
@@ -1,7 +1,8 @@
import type { MigrationResult, ProjectRecord } from "./types";
import { getProjectId, isRecord } from "./utils";
-import { TICKS_PER_SECOND } from "@/wasm";
+// Frozen snapshot of the v23-era tick rate. See ./README.md.
+const TICKS_PER_SECOND = 120_000;
const ARBITRARY_FPS_DENOMINATOR = 1_000_000;
const STANDARD_FRAME_RATES = [
{ value: 24_000 / 1_001, numerator: 24_000, denominator: 1_001 },
@@ -193,7 +194,9 @@ function migrateAnimationChannel({ channel }: { channel: unknown }): unknown {
return {
...channel,
- keys: channel.keys.map((keyframe) => migrateAnimationKeyframe({ keyframe })),
+ keys: channel.keys.map((keyframe) =>
+ migrateAnimationKeyframe({ keyframe }),
+ ),
};
}
@@ -290,7 +293,8 @@ function migrateFrameRate({ fps }: { fps: unknown }): unknown {
}
const standardFrameRate = STANDARD_FRAME_RATES.find(
- (candidate) => Math.abs(fps - candidate.value) <= STANDARD_FRAME_RATE_TOLERANCE,
+ (candidate) =>
+ Math.abs(fps - candidate.value) <= STANDARD_FRAME_RATE_TOLERANCE,
);
if (standardFrameRate) {
return {
diff --git a/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts b/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts
index 820b0087..3b118ee7 100644
--- a/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts
+++ b/apps/web/src/services/storage/migrations/transformers/v26-to-v27.ts
@@ -1,7 +1,50 @@
-import { parseCustomMaskPath } from "@/masks/custom-path";
import type { MigrationResult, ProjectRecord } from "./types";
import { getProjectId, isRecord } from "./utils";
+interface CustomMaskPathPoint {
+ id: string;
+ x: number;
+ y: number;
+ inX: number;
+ inY: number;
+ outX: number;
+ outY: number;
+}
+
+function isCustomMaskPathPoint(value: unknown): value is CustomMaskPathPoint {
+ if (!value || typeof value !== "object") {
+ return false;
+ }
+
+ const candidate = value as Record<string, unknown>;
+ return (
+ typeof candidate.id === "string" &&
+ typeof candidate.x === "number" &&
+ typeof candidate.y === "number" &&
+ typeof candidate.inX === "number" &&
+ typeof candidate.inY === "number" &&
+ typeof candidate.outX === "number" &&
+ typeof candidate.outY === "number"
+ );
+}
+
+function parseCustomMaskPath({
+ path,
+}: {
+ path: string;
+}): CustomMaskPathPoint[] {
+ if (!path) {
+ return [];
+ }
+
+ try {
+ const parsed = JSON.parse(path);
+ return Array.isArray(parsed) ? parsed.filter(isCustomMaskPathPoint) : [];
+ } catch {
+ return [];
+ }
+}
+
export function transformProjectV26ToV27({
project,
}: {