@umituz/react-native-video-editor 1.1.54 → 1.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/infrastructure/services/layer-operations/layer-duplicate.service.ts +4 -2
- package/src/infrastructure/services/scene-operations.service.ts +5 -8
- package/src/infrastructure/utils/data-clone.utils.ts +141 -0
- package/src/infrastructure/utils/position-calculations.utils.ts +182 -0
- package/src/infrastructure/utils/srt.utils.ts +4 -20
- package/src/infrastructure/utils/time-calculations.utils.ts +107 -0
- package/src/infrastructure/utils/video-calculations.utils.ts +169 -0
- package/src/presentation/components/DraggableLayer.tsx +36 -16
- package/src/presentation/components/EditorPreviewArea.tsx +92 -57
- package/src/presentation/components/EditorTimeline.tsx +105 -47
- package/src/presentation/components/EditorToolPanel.tsx +141 -115
- package/src/presentation/hooks/useEditorHistory.ts +20 -9
- package/src/presentation/hooks/useEditorPlayback.ts +62 -25
- package/src/presentation/hooks/useExportForm.ts +10 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-video-editor",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.55",
|
|
4
4
|
"description": "Professional video editor with layer-based timeline, text/image/shape/audio/animation layers, and export functionality",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
import { generateUUID } from "@umituz/react-native-design-system/uuid";
|
|
7
7
|
import type { Scene } from "../../../domain/entities/video-project.types";
|
|
8
8
|
import type { LayerOperationResult } from "../../../domain/entities/video-project.types";
|
|
9
|
+
import { cloneLayerWithNewId } from "../../../infrastructure/utils/data-clone.utils";
|
|
9
10
|
|
|
10
11
|
class LayerDuplicateService {
|
|
11
12
|
/**
|
|
12
13
|
* Duplicate layer
|
|
14
|
+
* Optimized using clone utility for better performance
|
|
13
15
|
*/
|
|
14
16
|
duplicateLayer(
|
|
15
17
|
scenes: Scene[],
|
|
@@ -37,9 +39,9 @@ class LayerDuplicateService {
|
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
// Use clone utility for consistent duplication
|
|
40
43
|
const duplicatedLayer = {
|
|
41
|
-
...
|
|
42
|
-
id: generateUUID(),
|
|
44
|
+
...cloneLayerWithNewId(layerToDuplicate, generateUUID),
|
|
43
45
|
position: {
|
|
44
46
|
x: layerToDuplicate.position.x + 5,
|
|
45
47
|
y: layerToDuplicate.position.y + 5,
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { generateUUID } from "@umituz/react-native-design-system/uuid";
|
|
7
7
|
import type { Scene, Audio } from "../../domain/entities/video-project.types";
|
|
8
8
|
import type { SceneOperationResult } from "../../domain/entities/video-project.types";
|
|
9
|
+
import { cloneSceneWithNewId } from "../utils/data-clone.utils";
|
|
9
10
|
|
|
10
11
|
class SceneOperationsService {
|
|
11
12
|
/**
|
|
@@ -38,6 +39,7 @@ class SceneOperationsService {
|
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Duplicate scene
|
|
42
|
+
* Optimized using clone utility for better performance
|
|
41
43
|
*/
|
|
42
44
|
duplicateScene(scenes: Scene[], sceneIndex: number): SceneOperationResult {
|
|
43
45
|
try {
|
|
@@ -50,14 +52,9 @@ class SceneOperationsService {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
const sceneToDuplicate = scenes[sceneIndex];
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
layers: sceneToDuplicate.layers.map((layer) => ({
|
|
57
|
-
...layer,
|
|
58
|
-
id: generateUUID(),
|
|
59
|
-
})),
|
|
60
|
-
};
|
|
55
|
+
|
|
56
|
+
// Use clone utility for consistent duplication
|
|
57
|
+
const duplicatedScene = cloneSceneWithNewId(sceneToDuplicate, generateUUID);
|
|
61
58
|
|
|
62
59
|
const updatedScenes = [...scenes];
|
|
63
60
|
updatedScenes.splice(sceneIndex + 1, 0, duplicatedScene);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Clone Utilities
|
|
3
|
+
* Optimized deep cloning functions for video editor data structures
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Scene, VideoProject, Layer } from "../../domain/entities/video-project.types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Optimized deep clone for VideoProject
|
|
10
|
+
* Uses structured clone if available, falls back to manual clone
|
|
11
|
+
*/
|
|
12
|
+
export function cloneVideoProject(project: VideoProject): VideoProject {
|
|
13
|
+
// Use structured clone for better performance if available
|
|
14
|
+
if (typeof structuredClone !== 'undefined') {
|
|
15
|
+
return structuredClone(project);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Manual clone fallback - only clone what's necessary
|
|
19
|
+
return {
|
|
20
|
+
...project,
|
|
21
|
+
scenes: project.scenes.map((scene: Scene) => ({
|
|
22
|
+
...scene,
|
|
23
|
+
layers: scene.layers.map((layer: Layer) => ({ ...layer })),
|
|
24
|
+
})),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Optimized deep clone for Scene
|
|
30
|
+
*/
|
|
31
|
+
export function cloneScene(scene: Scene): Scene {
|
|
32
|
+
if (typeof structuredClone !== 'undefined') {
|
|
33
|
+
return structuredClone(scene);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
...scene,
|
|
38
|
+
layers: scene.layers.map((layer: Layer) => ({ ...layer })),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Optimized shallow clone for Layer
|
|
44
|
+
* Layer objects are already immutable in our architecture
|
|
45
|
+
*/
|
|
46
|
+
export function cloneLayer(layer: Layer): Layer {
|
|
47
|
+
return { ...layer };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Clone multiple layers
|
|
52
|
+
*/
|
|
53
|
+
export function cloneLayers(layers: Layer[]): Layer[] {
|
|
54
|
+
return layers.map((layer: Layer) => ({ ...layer }));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Clone scene with new ID (for duplication)
|
|
59
|
+
*/
|
|
60
|
+
export function cloneSceneWithNewId(scene: Scene, generateId: () => string): Scene {
|
|
61
|
+
return {
|
|
62
|
+
...scene,
|
|
63
|
+
id: generateId(),
|
|
64
|
+
layers: scene.layers.map((layer: Layer) => ({
|
|
65
|
+
...layer,
|
|
66
|
+
id: generateId(),
|
|
67
|
+
})),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Clone layer with new ID (for duplication)
|
|
73
|
+
*/
|
|
74
|
+
export function cloneLayerWithNewId(layer: Layer, generateId: () => string): Layer {
|
|
75
|
+
return {
|
|
76
|
+
...layer,
|
|
77
|
+
id: generateId(),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Optimized clone for Audio configuration
|
|
83
|
+
*/
|
|
84
|
+
export function cloneAudio(audio: Scene["audio"]): Scene["audio"] {
|
|
85
|
+
if (!audio) return audio;
|
|
86
|
+
return { ...audio };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Optimized clone for Background configuration
|
|
91
|
+
*/
|
|
92
|
+
export function cloneBackground(background: Scene["background"]): Scene["background"] {
|
|
93
|
+
return { ...background };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Optimized clone for Transition configuration
|
|
98
|
+
*/
|
|
99
|
+
export function cloneTransition(transition: Scene["transition"]): Scene["transition"] {
|
|
100
|
+
return { ...transition };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Create a safe copy of an array (shallow clone)
|
|
105
|
+
*/
|
|
106
|
+
export function cloneArray<T>(array: T[]): T[] {
|
|
107
|
+
return [...array];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Safe array slice that handles edge cases
|
|
112
|
+
*/
|
|
113
|
+
export function safeSlice<T>(
|
|
114
|
+
array: T[],
|
|
115
|
+
start?: number,
|
|
116
|
+
end?: number,
|
|
117
|
+
): T[] {
|
|
118
|
+
return array.slice(start, end);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check if two values are shallow equal
|
|
123
|
+
*/
|
|
124
|
+
export function isShallowEqual<T>(a: T, b: T): boolean {
|
|
125
|
+
if (a === b) return true;
|
|
126
|
+
if (a == null || b == null) return false;
|
|
127
|
+
if (typeof a !== 'object' || typeof b !== 'object') return false;
|
|
128
|
+
|
|
129
|
+
const keysA = Object.keys(a as object);
|
|
130
|
+
const keysB = Object.keys(b as object);
|
|
131
|
+
|
|
132
|
+
if (keysA.length !== keysB.length) return false;
|
|
133
|
+
|
|
134
|
+
for (const key of keysA) {
|
|
135
|
+
if ((a as Record<string, unknown>)[key] !== (b as Record<string, unknown>)[key]) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Position & Size Calculation Utilities
|
|
3
|
+
* Centralized position and size calculations for video editor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface Position {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface Size {
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert percentage position to canvas pixels
|
|
18
|
+
*/
|
|
19
|
+
export function percentageToPixels(
|
|
20
|
+
percentage: number,
|
|
21
|
+
canvasSize: number,
|
|
22
|
+
): number {
|
|
23
|
+
if (canvasSize <= 0) return 0;
|
|
24
|
+
return (percentage / 100) * canvasSize;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Convert canvas pixels to percentage
|
|
29
|
+
*/
|
|
30
|
+
export function pixelsToPercentage(
|
|
31
|
+
pixels: number,
|
|
32
|
+
canvasSize: number,
|
|
33
|
+
): number {
|
|
34
|
+
if (canvasSize <= 0) return 0;
|
|
35
|
+
return (pixels / canvasSize) * 100;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Convert position from percentage to pixels
|
|
40
|
+
*/
|
|
41
|
+
export function positionToPixels(
|
|
42
|
+
position: Position,
|
|
43
|
+
canvasWidth: number,
|
|
44
|
+
canvasHeight: number,
|
|
45
|
+
): Position {
|
|
46
|
+
return {
|
|
47
|
+
x: percentageToPixels(position.x, canvasWidth),
|
|
48
|
+
y: percentageToPixels(position.y, canvasHeight),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Convert position from pixels to percentage
|
|
54
|
+
*/
|
|
55
|
+
export function positionToPercentage(
|
|
56
|
+
position: Position,
|
|
57
|
+
canvasWidth: number,
|
|
58
|
+
canvasHeight: number,
|
|
59
|
+
): Position {
|
|
60
|
+
return {
|
|
61
|
+
x: pixelsToPercentage(position.x, canvasWidth),
|
|
62
|
+
y: pixelsToPercentage(position.y, canvasHeight),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Convert size from percentage to pixels
|
|
68
|
+
*/
|
|
69
|
+
export function sizeToPixels(
|
|
70
|
+
size: Size,
|
|
71
|
+
canvasWidth: number,
|
|
72
|
+
canvasHeight: number,
|
|
73
|
+
): Size {
|
|
74
|
+
return {
|
|
75
|
+
width: percentageToPixels(size.width, canvasWidth),
|
|
76
|
+
height: percentageToPixels(size.height, canvasHeight),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Convert size from pixels to percentage
|
|
82
|
+
*/
|
|
83
|
+
export function sizeToPercentage(
|
|
84
|
+
size: Size,
|
|
85
|
+
canvasWidth: number,
|
|
86
|
+
canvasHeight: number,
|
|
87
|
+
): Size {
|
|
88
|
+
return {
|
|
89
|
+
width: pixelsToPercentage(size.width, canvasWidth),
|
|
90
|
+
height: pixelsToPercentage(size.height, canvasHeight),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Clamp value between min and max
|
|
96
|
+
*/
|
|
97
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
98
|
+
return Math.max(min, Math.min(max, value));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Clamp position to canvas bounds (in pixels)
|
|
103
|
+
*/
|
|
104
|
+
export function clampPositionToCanvas(
|
|
105
|
+
position: Position,
|
|
106
|
+
canvasWidth: number,
|
|
107
|
+
canvasHeight: number,
|
|
108
|
+
elementWidth: number,
|
|
109
|
+
elementHeight: number,
|
|
110
|
+
): Position {
|
|
111
|
+
return {
|
|
112
|
+
x: clamp(position.x, 0, canvasWidth - elementWidth),
|
|
113
|
+
y: clamp(position.y, 0, canvasHeight - elementHeight),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Clamp position to percentage bounds (0-100)
|
|
119
|
+
*/
|
|
120
|
+
export function clampPositionPercentage(position: Position): Position {
|
|
121
|
+
return {
|
|
122
|
+
x: clamp(position.x, 0, 100),
|
|
123
|
+
y: clamp(position.y, 0, 100),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Clamp size to minimum and maximum percentage values
|
|
129
|
+
*/
|
|
130
|
+
export function clampSizePercentage(
|
|
131
|
+
size: Size,
|
|
132
|
+
minSize: number = 1,
|
|
133
|
+
maxSize: number = 100,
|
|
134
|
+
): Size {
|
|
135
|
+
return {
|
|
136
|
+
width: clamp(size.width, minSize, maxSize),
|
|
137
|
+
height: clamp(size.height, minSize, maxSize),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Calculate center position for an element
|
|
143
|
+
*/
|
|
144
|
+
export function calculateCenterPosition(
|
|
145
|
+
elementWidth: number,
|
|
146
|
+
elementHeight: number,
|
|
147
|
+
canvasWidth: number,
|
|
148
|
+
canvasHeight: number,
|
|
149
|
+
): Position {
|
|
150
|
+
return {
|
|
151
|
+
x: (canvasWidth - elementWidth) / 2,
|
|
152
|
+
y: (canvasHeight - elementHeight) / 2,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Offset position by delta
|
|
158
|
+
*/
|
|
159
|
+
export function offsetPosition(position: Position, deltaX: number, deltaY: number): Position {
|
|
160
|
+
return {
|
|
161
|
+
x: position.x + deltaX,
|
|
162
|
+
y: position.y + deltaY,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if position is within canvas bounds
|
|
168
|
+
*/
|
|
169
|
+
export function isPositionInBounds(
|
|
170
|
+
position: Position,
|
|
171
|
+
canvasWidth: number,
|
|
172
|
+
canvasHeight: number,
|
|
173
|
+
elementWidth: number,
|
|
174
|
+
elementHeight: number,
|
|
175
|
+
): boolean {
|
|
176
|
+
return (
|
|
177
|
+
position.x >= 0 &&
|
|
178
|
+
position.y >= 0 &&
|
|
179
|
+
position.x + elementWidth <= canvasWidth &&
|
|
180
|
+
position.y + elementHeight <= canvasHeight
|
|
181
|
+
);
|
|
182
|
+
}
|
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SRT Subtitle Utilities
|
|
3
|
+
* Re-exports time utilities for backward compatibility
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import type { Subtitle } from "../../domain/entities/video-project.types";
|
|
6
|
-
|
|
7
|
-
function toSrtTime(seconds: number): string {
|
|
8
|
-
const h = Math.floor(seconds / 3600);
|
|
9
|
-
const m = Math.floor((seconds % 3600) / 60);
|
|
10
|
-
const s = Math.floor(seconds % 60);
|
|
11
|
-
const ms = Math.floor((seconds % 1) * 1000);
|
|
12
|
-
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")},${String(ms).padStart(3, "0")}`;
|
|
13
|
-
}
|
|
7
|
+
import { toSrtTime } from "./time-calculations.utils";
|
|
14
8
|
|
|
15
9
|
export function generateSRT(subtitles: Subtitle[]): string {
|
|
16
10
|
const validSubtitles = subtitles.filter((sub) => (
|
|
@@ -26,15 +20,5 @@ export function generateSRT(subtitles: Subtitle[]): string {
|
|
|
26
20
|
.join("\n");
|
|
27
21
|
}
|
|
28
22
|
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
const s = Math.floor(seconds % 60);
|
|
32
|
-
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function formatTimeDetailed(seconds: number): string {
|
|
36
|
-
const m = Math.floor(seconds / 60);
|
|
37
|
-
const s = Math.floor(seconds % 60);
|
|
38
|
-
const t = Math.floor((seconds % 1) * 10);
|
|
39
|
-
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${t}`;
|
|
40
|
-
}
|
|
23
|
+
// Re-export time utilities for backward compatibility
|
|
24
|
+
export { formatTimeDisplay, formatTimeDetailed, toSrtTime } from "./time-calculations.utils";
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time Calculation Utilities
|
|
3
|
+
* Centralized time-related calculations for video editor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Convert milliseconds to seconds
|
|
8
|
+
*/
|
|
9
|
+
export function msToSeconds(ms: number): number {
|
|
10
|
+
return ms / 1000;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convert seconds to milliseconds
|
|
15
|
+
*/
|
|
16
|
+
export function secondsToMs(seconds: number): number {
|
|
17
|
+
return seconds * 1000;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Format milliseconds as display time (e.g., "5s" or "1:23")
|
|
22
|
+
*/
|
|
23
|
+
export function formatTimeDisplay(ms: number, showMinutes: boolean = false): string {
|
|
24
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
25
|
+
|
|
26
|
+
if (!showMinutes || totalSeconds < 60) {
|
|
27
|
+
return `${totalSeconds}s`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
31
|
+
const seconds = totalSeconds % 60;
|
|
32
|
+
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Format milliseconds as detailed time (e.g., "1:23.4")
|
|
37
|
+
*/
|
|
38
|
+
export function formatTimeDetailed(ms: number): string {
|
|
39
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
40
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
41
|
+
const seconds = totalSeconds % 60;
|
|
42
|
+
const tenths = Math.floor((ms % 1000) / 100);
|
|
43
|
+
|
|
44
|
+
return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}.${tenths}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Calculate progress percentage (0-1)
|
|
49
|
+
*/
|
|
50
|
+
export function calculateProgress(current: number, total: number): number {
|
|
51
|
+
if (total <= 0) return 0;
|
|
52
|
+
return Math.min(1, Math.max(0, current / total));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Calculate progress percentage (0-100)
|
|
57
|
+
*/
|
|
58
|
+
export function calculateProgressPercent(current: number, total: number): number {
|
|
59
|
+
return calculateProgress(current, total) * 100;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Calculate delta time between two timestamps
|
|
64
|
+
*/
|
|
65
|
+
export function calculateDelta(current: number, previous: number): number {
|
|
66
|
+
return current - previous;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Add delta time to base time
|
|
71
|
+
*/
|
|
72
|
+
export function addDeltaTime(baseTime: number, deltaTime: number): number {
|
|
73
|
+
return baseTime + deltaTime;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Clamp time to duration bounds
|
|
78
|
+
*/
|
|
79
|
+
export function clampTime(time: number, duration: number): number {
|
|
80
|
+
return Math.max(0, Math.min(duration, time));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if time is at or past duration
|
|
85
|
+
*/
|
|
86
|
+
export function isTimeAtEnd(time: number, duration: number): boolean {
|
|
87
|
+
return time >= duration;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Convert seconds to SRT time format (HH:MM:SS,mmm)
|
|
92
|
+
*/
|
|
93
|
+
export function toSrtTime(seconds: number): string {
|
|
94
|
+
const h = Math.floor(seconds / 3600);
|
|
95
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
96
|
+
const s = Math.floor(seconds % 60);
|
|
97
|
+
const ms = Math.floor((seconds % 1) * 1000);
|
|
98
|
+
|
|
99
|
+
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")},${String(ms).padStart(3, "0")}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Calculate total duration from scenes (in milliseconds)
|
|
104
|
+
*/
|
|
105
|
+
export function calculateTotalDuration(sceneDurations: number[]): number {
|
|
106
|
+
return sceneDurations.reduce((total, duration) => total + duration, 0);
|
|
107
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Calculation Utilities
|
|
3
|
+
* Centralized video-related calculations for export and file size estimation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Scene, VideoProject } from "../../domain/entities/video-project.types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolution multipliers for file size estimation
|
|
10
|
+
*/
|
|
11
|
+
export const RESOLUTION_MULTIPLIERS: Record<string, number> = {
|
|
12
|
+
"720p": 0.5,
|
|
13
|
+
"1080p": 1.0,
|
|
14
|
+
"4k": 3.0,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Quality multipliers for file size estimation
|
|
19
|
+
*/
|
|
20
|
+
export const QUALITY_MULTIPLIERS: Record<string, number> = {
|
|
21
|
+
low: 0.6,
|
|
22
|
+
medium: 1.0,
|
|
23
|
+
high: 1.4,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Base file size per second of video (in MB)
|
|
28
|
+
*/
|
|
29
|
+
export const BASE_SIZE_PER_SECOND = 2.5;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Calculate estimated file size for video export
|
|
33
|
+
*/
|
|
34
|
+
export function calculateEstimatedFileSize(
|
|
35
|
+
durationSeconds: number,
|
|
36
|
+
resolution: string,
|
|
37
|
+
quality: string,
|
|
38
|
+
): number {
|
|
39
|
+
const baseSize = durationSeconds * BASE_SIZE_PER_SECOND;
|
|
40
|
+
const resolutionMultiplier = RESOLUTION_MULTIPLIERS[resolution] || 1.0;
|
|
41
|
+
const qualityMultiplier = QUALITY_MULTIPLIERS[quality] || 1.0;
|
|
42
|
+
|
|
43
|
+
return baseSize * resolutionMultiplier * qualityMultiplier;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Calculate estimated file size and return formatted string
|
|
48
|
+
*/
|
|
49
|
+
export function formatEstimatedFileSize(
|
|
50
|
+
durationSeconds: number,
|
|
51
|
+
resolution: string,
|
|
52
|
+
quality: string,
|
|
53
|
+
decimals: number = 1,
|
|
54
|
+
): string {
|
|
55
|
+
const sizeInMB = calculateEstimatedFileSize(durationSeconds, resolution, quality);
|
|
56
|
+
return sizeInMB.toFixed(decimals);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Calculate total project duration from scenes (in seconds)
|
|
61
|
+
*/
|
|
62
|
+
export function calculateProjectDuration(scenes: Scene[]): number {
|
|
63
|
+
return scenes.reduce((total, scene) => total + scene.duration, 0) / 1000;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Calculate total project duration from VideoProject (in seconds)
|
|
68
|
+
*/
|
|
69
|
+
export function calculateVideoProjectDuration(project: VideoProject): number {
|
|
70
|
+
return calculateProjectDuration(project.scenes);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Calculate aspect ratio as decimal
|
|
75
|
+
*/
|
|
76
|
+
export function getAspectRatioValue(aspectRatio: string): number {
|
|
77
|
+
const ratioMap: Record<string, number> = {
|
|
78
|
+
"16:9": 16 / 9,
|
|
79
|
+
"9:16": 9 / 16,
|
|
80
|
+
"1:1": 1,
|
|
81
|
+
"4:5": 4 / 5,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return ratioMap[aspectRatio] || 16 / 9;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Calculate height from width and aspect ratio
|
|
89
|
+
*/
|
|
90
|
+
export function calculateHeightFromAspectRatio(
|
|
91
|
+
width: number,
|
|
92
|
+
aspectRatio: string,
|
|
93
|
+
): number {
|
|
94
|
+
const ratio = getAspectRatioValue(aspectRatio);
|
|
95
|
+
return width / ratio;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Calculate width from height and aspect ratio
|
|
100
|
+
*/
|
|
101
|
+
export function calculateWidthFromAspectRatio(
|
|
102
|
+
height: number,
|
|
103
|
+
aspectRatio: string,
|
|
104
|
+
): number {
|
|
105
|
+
const ratio = getAspectRatioValue(aspectRatio);
|
|
106
|
+
return height * ratio;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Calculate resolution dimensions
|
|
111
|
+
*/
|
|
112
|
+
export interface ResolutionDimensions {
|
|
113
|
+
width: number;
|
|
114
|
+
height: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function calculateResolutionDimensions(
|
|
118
|
+
resolution: string,
|
|
119
|
+
aspectRatio: string,
|
|
120
|
+
): ResolutionDimensions {
|
|
121
|
+
const baseHeights: Record<string, number> = {
|
|
122
|
+
"720p": 720,
|
|
123
|
+
"1080p": 1080,
|
|
124
|
+
"4k": 2160,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const height = baseHeights[resolution] || 1080;
|
|
128
|
+
const width = calculateWidthFromAspectRatio(height, aspectRatio);
|
|
129
|
+
|
|
130
|
+
return { width, height };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Calculate bitrate based on resolution and quality (in kbps)
|
|
135
|
+
*/
|
|
136
|
+
export function calculateBitrate(
|
|
137
|
+
resolution: string,
|
|
138
|
+
quality: string,
|
|
139
|
+
): number {
|
|
140
|
+
const baseBitrates: Record<string, number> = {
|
|
141
|
+
"720p": 5000,
|
|
142
|
+
"1080p": 10000,
|
|
143
|
+
"4k": 40000,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const qualityMultipliers: Record<string, number> = {
|
|
147
|
+
low: 0.6,
|
|
148
|
+
medium: 1.0,
|
|
149
|
+
high: 1.5,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const baseBitrate = baseBitrates[resolution] || 10000;
|
|
153
|
+
const qualityMultiplier = qualityMultipliers[quality] || 1.0;
|
|
154
|
+
|
|
155
|
+
return Math.round(baseBitrate * qualityMultiplier);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Estimate file size based on bitrate and duration (in MB)
|
|
160
|
+
*/
|
|
161
|
+
export function estimateFileSizeFromBitrate(
|
|
162
|
+
bitrateKbps: number,
|
|
163
|
+
durationSeconds: number,
|
|
164
|
+
): number {
|
|
165
|
+
// Formula: (bitrate in kbps * duration) / 8 = kilobytes
|
|
166
|
+
// Then divide by 1024 to get MB
|
|
167
|
+
const kilobytes = (bitrateKbps * durationSeconds) / 8;
|
|
168
|
+
return kilobytes / 1024;
|
|
169
|
+
}
|