@pixerate/editor 0.75.0 → 0.77.0
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/dist/agent/index.cjs +28 -28
- package/dist/agent/index.js +3 -3
- package/dist/animation-editor/index.cjs +148 -218
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.js +8 -78
- package/dist/animation-editor/index.js.map +1 -1
- package/dist/{chunk-OK7T2LHK.js → chunk-GNO5PEMH.js} +2 -2
- package/dist/{chunk-EIXXBKUB.js → chunk-IMSX4IVG.js} +7 -7
- package/dist/chunk-INZJALPY.js +87 -0
- package/dist/chunk-INZJALPY.js.map +1 -0
- package/dist/{chunk-PL3MPFPA.cjs → chunk-MXF4DP2C.cjs} +77 -77
- package/dist/{chunk-PL3MPFPA.cjs.map → chunk-MXF4DP2C.cjs.map} +1 -1
- package/dist/{chunk-T4XWVPJK.cjs → chunk-RKJDBXTY.cjs} +3 -3
- package/dist/{chunk-T4XWVPJK.cjs.map → chunk-RKJDBXTY.cjs.map} +1 -1
- package/dist/{chunk-HNDSPZ5T.cjs → chunk-ROXFP2LQ.cjs} +4 -4
- package/dist/{chunk-HNDSPZ5T.cjs.map → chunk-ROXFP2LQ.cjs.map} +1 -1
- package/dist/{chunk-WON34HKM.js → chunk-SVW2UNUC.js} +2 -2
- package/dist/{chunk-66IMF6OD.cjs → chunk-TYOAJDFD.cjs} +3 -3
- package/dist/{chunk-66IMF6OD.cjs.map → chunk-TYOAJDFD.cjs.map} +1 -1
- package/dist/chunk-WAJQKWQU.cjs +87 -0
- package/dist/chunk-WAJQKWQU.cjs.map +1 -0
- package/dist/{chunk-UKI64KUD.js → chunk-ZQNDSUZO.js} +6 -6
- package/dist/extensions/index.cjs +7 -7
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.js +6 -6
- package/dist/index.cjs +35 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +17 -17
- package/dist/serializers/index.cjs +4 -4
- package/dist/serializers/index.cjs.map +1 -1
- package/dist/serializers/index.js +3 -3
- package/dist/video-editor/index.cjs +2234 -0
- package/dist/video-editor/index.cjs.map +1 -0
- package/dist/video-editor/index.d.cts +1003 -0
- package/dist/video-editor/index.d.ts +1003 -0
- package/dist/video-editor/index.js +2234 -0
- package/dist/video-editor/index.js.map +1 -0
- package/package.json +6 -1
- /package/dist/{chunk-OK7T2LHK.js.map → chunk-GNO5PEMH.js.map} +0 -0
- /package/dist/{chunk-EIXXBKUB.js.map → chunk-IMSX4IVG.js.map} +0 -0
- /package/dist/{chunk-WON34HKM.js.map → chunk-SVW2UNUC.js.map} +0 -0
- /package/dist/{chunk-UKI64KUD.js.map → chunk-ZQNDSUZO.js.map} +0 -0
|
@@ -0,0 +1,1003 @@
|
|
|
1
|
+
import { c as AnimationPlayer, C as Clock, b as AnimationLoopMode } from '../player-DrcP2xqj.cjs';
|
|
2
|
+
import { H as HistoryManager } from '../HistoryManager-DeQQMBCj.cjs';
|
|
3
|
+
import { A as AgentToolProvider } from '../registry-DIpIaCkH.cjs';
|
|
4
|
+
import '../easing-lM0Xo9Ni.cjs';
|
|
5
|
+
import '../types-DsiXGmjo.cjs';
|
|
6
|
+
import '../schema-Di0CS6Pe.cjs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A video project is plain JSON: the media it uses (assets), tracks of clips placed on a timeline,
|
|
10
|
+
* and markers. Every time is in milliseconds; `fps` sets the frame grid edits snap to and exports use.
|
|
11
|
+
*
|
|
12
|
+
* Tracks are listed top to bottom as a timeline shows them: on screen, a clip on an earlier video
|
|
13
|
+
* track draws over one on a later track. Clips on one track never overlap.
|
|
14
|
+
*/
|
|
15
|
+
type VideoAssetKind = 'video' | 'audio' | 'image';
|
|
16
|
+
/** A piece of source media. The project refers to it by id; `src` is whatever the host can load. */
|
|
17
|
+
interface VideoAsset {
|
|
18
|
+
id: string;
|
|
19
|
+
kind: VideoAssetKind;
|
|
20
|
+
/** URL, blob URL, path or data URL (whatever the playback engine and exporter can open). */
|
|
21
|
+
src: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
/** Length of the source (ms). Required for video and audio; images have none. */
|
|
24
|
+
duration?: number;
|
|
25
|
+
/** Intrinsic size in pixels (video and image). Placement assumes the project size when missing. */
|
|
26
|
+
width?: number;
|
|
27
|
+
height?: number;
|
|
28
|
+
/** Whether a video asset has sound (default true). */
|
|
29
|
+
hasAudio?: boolean;
|
|
30
|
+
/** A still to show for a video where its frames cannot be drawn (SVG renders, thumbnails). */
|
|
31
|
+
poster?: string;
|
|
32
|
+
}
|
|
33
|
+
type VideoTrackKind = 'video' | 'audio';
|
|
34
|
+
interface VideoTrack {
|
|
35
|
+
id: string;
|
|
36
|
+
kind: VideoTrackKind;
|
|
37
|
+
name?: string;
|
|
38
|
+
/** Silences every clip on the track. */
|
|
39
|
+
muted?: boolean;
|
|
40
|
+
/** Leaves the track's pictures out of the frame. */
|
|
41
|
+
hidden?: boolean;
|
|
42
|
+
/** Refuses edits to the track's clips. */
|
|
43
|
+
locked?: boolean;
|
|
44
|
+
/** Gain applied to every clip on the track (1 = unchanged, default 1). */
|
|
45
|
+
volume?: number;
|
|
46
|
+
clips: VideoClip[];
|
|
47
|
+
}
|
|
48
|
+
/** Where a picture sits in the frame, after it has been fitted. */
|
|
49
|
+
interface VideoTransform {
|
|
50
|
+
/** Offset of the centre from the frame's centre, in pixels (default 0). */
|
|
51
|
+
x?: number;
|
|
52
|
+
y?: number;
|
|
53
|
+
/** 1 = fitted size (default 1). */
|
|
54
|
+
scale?: number;
|
|
55
|
+
/** Degrees clockwise (default 0). */
|
|
56
|
+
rotation?: number;
|
|
57
|
+
}
|
|
58
|
+
/** How a picture is fitted to the frame before its transform: like CSS `object-fit`. */
|
|
59
|
+
type VideoFit = 'contain' | 'cover' | 'fill' | 'none';
|
|
60
|
+
declare const VIDEO_FITS: readonly ["contain", "cover", "fill", "none"];
|
|
61
|
+
type VideoTransitionType = 'crossfade' | 'dip' | 'wipe';
|
|
62
|
+
declare const VIDEO_TRANSITION_TYPES: readonly ["crossfade", "dip", "wipe"];
|
|
63
|
+
type VideoWipeDirection = 'left' | 'right' | 'up' | 'down';
|
|
64
|
+
declare const VIDEO_WIPE_DIRECTIONS: readonly ["left", "right", "up", "down"];
|
|
65
|
+
/**
|
|
66
|
+
* A transition into a clip from the clip that ends where it starts on the same track. It plays over
|
|
67
|
+
* the cut, half before and half after, so both clips run past the cut into their handles (media
|
|
68
|
+
* beyond the out point of the one before, before the in point of this one).
|
|
69
|
+
*/
|
|
70
|
+
interface VideoTransition {
|
|
71
|
+
type: VideoTransitionType;
|
|
72
|
+
/** Length (ms), centred on the cut. */
|
|
73
|
+
duration: number;
|
|
74
|
+
/** For dip: the colour passed through (default black). */
|
|
75
|
+
color?: string;
|
|
76
|
+
/** For wipe: the way the edge travels (default right, revealing from the left). */
|
|
77
|
+
direction?: VideoWipeDirection;
|
|
78
|
+
}
|
|
79
|
+
interface VideoClipBase {
|
|
80
|
+
id: string;
|
|
81
|
+
name?: string;
|
|
82
|
+
/** Where it starts on the timeline (ms). */
|
|
83
|
+
start: number;
|
|
84
|
+
/** How long it lasts on the timeline (ms). */
|
|
85
|
+
duration: number;
|
|
86
|
+
/** 0–1 (default 1). */
|
|
87
|
+
opacity?: number;
|
|
88
|
+
transform?: VideoTransform;
|
|
89
|
+
/** Default contain. */
|
|
90
|
+
fit?: VideoFit;
|
|
91
|
+
/** Fade in from nothing over this long (ms), pictures and sound. */
|
|
92
|
+
fadeIn?: number;
|
|
93
|
+
/** Fade out over this long (ms). */
|
|
94
|
+
fadeOut?: number;
|
|
95
|
+
/** Transition from the clip before (see `VideoTransition`). */
|
|
96
|
+
transition?: VideoTransition;
|
|
97
|
+
}
|
|
98
|
+
/** A stretch of an asset. */
|
|
99
|
+
interface VideoMediaClip extends VideoClipBase {
|
|
100
|
+
type: 'media';
|
|
101
|
+
asset: string;
|
|
102
|
+
/** Where in the source it starts (ms, default 0). */
|
|
103
|
+
in?: number;
|
|
104
|
+
/** Playback speed (default 1): it uses `duration × speed` of the source. */
|
|
105
|
+
speed?: number;
|
|
106
|
+
/** Gain (1 = unchanged, default 1). */
|
|
107
|
+
volume?: number;
|
|
108
|
+
/** Silences this clip. */
|
|
109
|
+
muted?: boolean;
|
|
110
|
+
}
|
|
111
|
+
/** A title. */
|
|
112
|
+
interface VideoTextClip extends VideoClipBase {
|
|
113
|
+
type: 'text';
|
|
114
|
+
text: string;
|
|
115
|
+
/** CSS colour (default white). */
|
|
116
|
+
color?: string;
|
|
117
|
+
/** Pixels (default 64). */
|
|
118
|
+
fontSize?: number;
|
|
119
|
+
/** Default sans-serif. */
|
|
120
|
+
fontFamily?: string;
|
|
121
|
+
/** Default 700. */
|
|
122
|
+
fontWeight?: number;
|
|
123
|
+
/** Box behind the text (CSS colour); none by default. */
|
|
124
|
+
background?: string;
|
|
125
|
+
}
|
|
126
|
+
/** A solid colour filling the frame. */
|
|
127
|
+
interface VideoColorClip extends VideoClipBase {
|
|
128
|
+
type: 'color';
|
|
129
|
+
color: string;
|
|
130
|
+
}
|
|
131
|
+
type VideoClip = VideoMediaClip | VideoTextClip | VideoColorClip;
|
|
132
|
+
type VideoClipType = VideoClip['type'];
|
|
133
|
+
declare const VIDEO_CLIP_TYPES: readonly ["media", "text", "color"];
|
|
134
|
+
interface VideoMarker {
|
|
135
|
+
id: string;
|
|
136
|
+
time: number;
|
|
137
|
+
label?: string;
|
|
138
|
+
/** CSS colour. */
|
|
139
|
+
color?: string;
|
|
140
|
+
}
|
|
141
|
+
interface VideoProject {
|
|
142
|
+
version: 1;
|
|
143
|
+
name?: string;
|
|
144
|
+
/** Frame size in pixels. */
|
|
145
|
+
width: number;
|
|
146
|
+
height: number;
|
|
147
|
+
/** Frames per second: the grid edits snap to and exports use. */
|
|
148
|
+
fps: number;
|
|
149
|
+
/** What shows where no clip covers the frame (default black). */
|
|
150
|
+
background?: string;
|
|
151
|
+
assets: VideoAsset[];
|
|
152
|
+
tracks: VideoTrack[];
|
|
153
|
+
markers?: VideoMarker[];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
declare function parseVideoTransform(v: unknown, path: string): VideoTransform | undefined;
|
|
157
|
+
declare function parseVideoTransition(v: unknown, path: string): VideoTransition | undefined;
|
|
158
|
+
declare function parseVideoAsset(v: unknown, path?: string): VideoAsset;
|
|
159
|
+
declare function parseVideoClip(v: unknown, path?: string): VideoClip;
|
|
160
|
+
declare function parseVideoTrack(v: unknown, path?: string): VideoTrack;
|
|
161
|
+
declare function parseVideoMarker(v: unknown, path?: string): VideoMarker;
|
|
162
|
+
/**
|
|
163
|
+
* Reads a project from JSON text or an object, checking every field. Throws an error naming the
|
|
164
|
+
* first bad field (`tracks[0].clips[2].duration must be greater than 0.`). Ids must be unique
|
|
165
|
+
* within assets, tracks, clips and markers. Overlaps and missing media are left to
|
|
166
|
+
* `checkVideoProject`, so a project with problems can still be opened and fixed.
|
|
167
|
+
*/
|
|
168
|
+
declare function parseVideoProject(input: unknown): VideoProject;
|
|
169
|
+
interface CreateVideoProjectOptions {
|
|
170
|
+
name?: string;
|
|
171
|
+
/** Default 1920. */
|
|
172
|
+
width?: number;
|
|
173
|
+
/** Default 1080. */
|
|
174
|
+
height?: number;
|
|
175
|
+
/** Default 30. */
|
|
176
|
+
fps?: number;
|
|
177
|
+
background?: string;
|
|
178
|
+
}
|
|
179
|
+
/** An empty project with one video track (`v1`) and one audio track (`a1`). */
|
|
180
|
+
declare function createVideoProject(options?: CreateVideoProjectOptions): VideoProject;
|
|
181
|
+
|
|
182
|
+
/** Clip edges closer than this (ms) count as touching: a cut, not a gap. */
|
|
183
|
+
declare const VIDEO_TIME_EPSILON = 0.5;
|
|
184
|
+
declare const videoClipEnd: (clip: VideoClip) => number;
|
|
185
|
+
/** Playback speed of a clip (1 for titles and colours). */
|
|
186
|
+
declare const videoClipSpeed: (clip: VideoClip) => number;
|
|
187
|
+
/** The stretch of source a media clip plays (ms), or `null` for titles and colours. */
|
|
188
|
+
declare function videoClipSourceRange(clip: VideoClip): {
|
|
189
|
+
in: number;
|
|
190
|
+
out: number;
|
|
191
|
+
} | null;
|
|
192
|
+
/** Length of one frame (ms). */
|
|
193
|
+
declare const videoFrameDuration: (fps: number) => number;
|
|
194
|
+
/** `time` moved to the nearest frame boundary. */
|
|
195
|
+
declare function snapToVideoFrame(time: number, fps: number): number;
|
|
196
|
+
/** Whole frames in `time` at `fps` (rounded). */
|
|
197
|
+
declare const videoFrames: (time: number, fps: number) => number;
|
|
198
|
+
/** Non-drop SMPTE timecode `HH:MM:SS:FF` for a time (ms). */
|
|
199
|
+
declare function formatVideoTimecode(time: number, fps: number): string;
|
|
200
|
+
/** Readable time for messages: `2.50 s`. */
|
|
201
|
+
declare const formatVideoSeconds: (time: number) => string;
|
|
202
|
+
declare function videoAssetMap(project: VideoProject): Map<string, VideoAsset>;
|
|
203
|
+
/** A track's clips in timeline order. */
|
|
204
|
+
declare function sortedVideoClips(track: VideoTrack): VideoClip[];
|
|
205
|
+
interface VideoClipLocation {
|
|
206
|
+
track: VideoTrack;
|
|
207
|
+
trackIndex: number;
|
|
208
|
+
clip: VideoClip;
|
|
209
|
+
/** Index in `track.clips`. */
|
|
210
|
+
index: number;
|
|
211
|
+
}
|
|
212
|
+
declare function findVideoClip(project: VideoProject, id: string): VideoClipLocation | null;
|
|
213
|
+
declare function allVideoClipIds(project: VideoProject): string[];
|
|
214
|
+
/** Total length: the end of the last clip (ms). */
|
|
215
|
+
declare function videoProjectDuration(project: VideoProject): number;
|
|
216
|
+
/** The clip that ends where `clip` starts on its track (a cut), if any. */
|
|
217
|
+
declare function videoClipBefore(track: VideoTrack, clip: VideoClip): VideoClip | null;
|
|
218
|
+
/** The clip that starts where `clip` ends on its track (a cut), if any. */
|
|
219
|
+
declare function videoClipAfter(track: VideoTrack, clip: VideoClip): VideoClip | null;
|
|
220
|
+
/**
|
|
221
|
+
* Timeline time (ms) a clip can run past its edges into source media: before its in point
|
|
222
|
+
* (`before`) and after its out point (`after`). Unlimited for stills, titles, colours and media
|
|
223
|
+
* whose length is unknown.
|
|
224
|
+
*/
|
|
225
|
+
declare function videoClipHandles(clip: VideoClip, assets: Map<string, VideoAsset>): {
|
|
226
|
+
before: number;
|
|
227
|
+
after: number;
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* How far a clip plays past each edge because of transitions: `lead` before its start (its own
|
|
231
|
+
* transition in) and `tail` after its end (the next clip's transition in). Dips need neither.
|
|
232
|
+
*/
|
|
233
|
+
declare function videoClipOverhang(track: VideoTrack, clip: VideoClip): {
|
|
234
|
+
lead: number;
|
|
235
|
+
tail: number;
|
|
236
|
+
};
|
|
237
|
+
interface VideoSnapOptions {
|
|
238
|
+
/** Clip ids whose edges are not snap targets (the ones being dragged). */
|
|
239
|
+
exclude?: Iterable<string>;
|
|
240
|
+
/** Also snap to this time (usually the playhead). */
|
|
241
|
+
playhead?: number;
|
|
242
|
+
}
|
|
243
|
+
/** Times worth snapping to: 0, every clip edge, every marker and the playhead, ascending. */
|
|
244
|
+
declare function videoSnapPoints(project: VideoProject, options?: VideoSnapOptions): number[];
|
|
245
|
+
/** The snap point nearest `time` if it is within `threshold` (ms), else `time`. */
|
|
246
|
+
declare function snapVideoTime(time: number, points: readonly number[], threshold: number): {
|
|
247
|
+
time: number;
|
|
248
|
+
snapped: number | null;
|
|
249
|
+
};
|
|
250
|
+
interface VideoGap {
|
|
251
|
+
track: string;
|
|
252
|
+
start: number;
|
|
253
|
+
end: number;
|
|
254
|
+
}
|
|
255
|
+
/** Empty stretches between clips (and before the first) on each track, or one track. */
|
|
256
|
+
declare function findVideoGaps(project: VideoProject, trackId?: string): VideoGap[];
|
|
257
|
+
/** The first clip on `track` (other than `ignore`) that overlaps `[start, end)`, if any. */
|
|
258
|
+
declare function videoOverlap(track: VideoTrack, start: number, end: number, ignore?: ReadonlySet<string>): VideoClip | null;
|
|
259
|
+
/** The first free stretch on `track` at or after `from` that fits `duration`. */
|
|
260
|
+
declare function nextFreeVideoSlot(track: VideoTrack, from: number, duration: number, ignore?: ReadonlySet<string>): number;
|
|
261
|
+
/** Clips under `time` (start inclusive, end exclusive), top track first. */
|
|
262
|
+
declare function videoClipsAt(project: VideoProject, time: number): {
|
|
263
|
+
track: string;
|
|
264
|
+
clip: VideoClip;
|
|
265
|
+
}[];
|
|
266
|
+
|
|
267
|
+
type VideoIssueSeverity = 'error' | 'warning' | 'note';
|
|
268
|
+
declare const VIDEO_ISSUE_CODES: readonly ["missing-asset", "wrong-track", "overlap", "source-overrun", "transition-without-cut", "transition-too-long", "transition-handles", "fades-too-long", "unknown-duration", "unused-asset", "marker-past-end", "empty"];
|
|
269
|
+
type VideoIssueCode = (typeof VIDEO_ISSUE_CODES)[number];
|
|
270
|
+
interface VideoIssue {
|
|
271
|
+
code: VideoIssueCode;
|
|
272
|
+
severity: VideoIssueSeverity;
|
|
273
|
+
/** One sentence saying what is wrong and how to fix it. */
|
|
274
|
+
message: string;
|
|
275
|
+
track?: string;
|
|
276
|
+
clip?: string;
|
|
277
|
+
asset?: string;
|
|
278
|
+
marker?: string;
|
|
279
|
+
}
|
|
280
|
+
/** A stable key for an issue (code and what it is about), to tell new problems from old ones. */
|
|
281
|
+
declare const videoIssueKey: (i: VideoIssue) => string;
|
|
282
|
+
/**
|
|
283
|
+
* Everything wrong with a project, errors first: clips pointing at missing media or sitting on the
|
|
284
|
+
* wrong kind of track, overlaps, clips running past the end of their source, transitions with no
|
|
285
|
+
* cut to play over or not enough media either side of it, and softer notes (unused assets, markers
|
|
286
|
+
* past the end). Edits never add errors (see `applyVideoEdits`), so errors only come from a loaded file.
|
|
287
|
+
*/
|
|
288
|
+
declare function checkVideoProject(project: VideoProject): VideoIssue[];
|
|
289
|
+
|
|
290
|
+
type Settings = {
|
|
291
|
+
name?: string;
|
|
292
|
+
width?: number;
|
|
293
|
+
height?: number;
|
|
294
|
+
fps?: number;
|
|
295
|
+
background?: string;
|
|
296
|
+
};
|
|
297
|
+
type TrackFields = {
|
|
298
|
+
name?: string;
|
|
299
|
+
muted?: boolean;
|
|
300
|
+
hidden?: boolean;
|
|
301
|
+
locked?: boolean;
|
|
302
|
+
volume?: number;
|
|
303
|
+
};
|
|
304
|
+
/** A clip to add: `id` and `start` are optional (default: a fresh id, the end of the track). */
|
|
305
|
+
type NewVideoClip = {
|
|
306
|
+
[K in VideoClip['type']]: Omit<Extract<VideoClip, {
|
|
307
|
+
type: K;
|
|
308
|
+
}>, 'id' | 'start' | 'duration'> & {
|
|
309
|
+
id?: string;
|
|
310
|
+
start?: number;
|
|
311
|
+
duration?: number;
|
|
312
|
+
};
|
|
313
|
+
}[VideoClip['type']];
|
|
314
|
+
/** One change to a project. `applyVideoEdits` applies a list of them all or nothing. */
|
|
315
|
+
type VideoEdit = ({
|
|
316
|
+
op: 'set_settings';
|
|
317
|
+
} & Settings) | {
|
|
318
|
+
op: 'add_asset';
|
|
319
|
+
asset: Omit<VideoAsset, 'id'> & {
|
|
320
|
+
id?: string;
|
|
321
|
+
};
|
|
322
|
+
} | {
|
|
323
|
+
op: 'update_asset';
|
|
324
|
+
asset: string;
|
|
325
|
+
set?: Partial<Omit<VideoAsset, 'id'>>;
|
|
326
|
+
remove?: string[];
|
|
327
|
+
} | {
|
|
328
|
+
op: 'remove_asset';
|
|
329
|
+
asset: string;
|
|
330
|
+
} | {
|
|
331
|
+
op: 'add_track';
|
|
332
|
+
track: TrackFields & {
|
|
333
|
+
id?: string;
|
|
334
|
+
kind: VideoTrackKind;
|
|
335
|
+
};
|
|
336
|
+
at?: number;
|
|
337
|
+
} | {
|
|
338
|
+
op: 'update_track';
|
|
339
|
+
track: string;
|
|
340
|
+
set?: TrackFields;
|
|
341
|
+
remove?: string[];
|
|
342
|
+
} | {
|
|
343
|
+
op: 'remove_track';
|
|
344
|
+
track: string;
|
|
345
|
+
} | {
|
|
346
|
+
op: 'move_track';
|
|
347
|
+
track: string;
|
|
348
|
+
to: number;
|
|
349
|
+
} | {
|
|
350
|
+
op: 'add_clip';
|
|
351
|
+
track: string;
|
|
352
|
+
clip: NewVideoClip;
|
|
353
|
+
ripple?: boolean;
|
|
354
|
+
} | {
|
|
355
|
+
op: 'move_clip';
|
|
356
|
+
clip: string;
|
|
357
|
+
start?: number;
|
|
358
|
+
track?: string;
|
|
359
|
+
} | {
|
|
360
|
+
op: 'trim_clip';
|
|
361
|
+
clip: string;
|
|
362
|
+
edge: 'start' | 'end';
|
|
363
|
+
to: number;
|
|
364
|
+
ripple?: boolean;
|
|
365
|
+
} | {
|
|
366
|
+
op: 'split_clip';
|
|
367
|
+
clip: string;
|
|
368
|
+
at: number;
|
|
369
|
+
id?: string;
|
|
370
|
+
} | {
|
|
371
|
+
op: 'delete_clips';
|
|
372
|
+
clips: string[];
|
|
373
|
+
ripple?: boolean;
|
|
374
|
+
} | {
|
|
375
|
+
op: 'update_clip';
|
|
376
|
+
clip: string;
|
|
377
|
+
set?: Record<string, unknown>;
|
|
378
|
+
remove?: string[];
|
|
379
|
+
} | {
|
|
380
|
+
op: 'set_transition';
|
|
381
|
+
clip: string;
|
|
382
|
+
transition: VideoTransition | null;
|
|
383
|
+
} | {
|
|
384
|
+
op: 'close_gaps';
|
|
385
|
+
track: string;
|
|
386
|
+
from?: number;
|
|
387
|
+
} | {
|
|
388
|
+
op: 'add_marker';
|
|
389
|
+
marker: Omit<VideoMarker, 'id'> & {
|
|
390
|
+
id?: string;
|
|
391
|
+
};
|
|
392
|
+
} | {
|
|
393
|
+
op: 'update_marker';
|
|
394
|
+
marker: string;
|
|
395
|
+
set?: Partial<Omit<VideoMarker, 'id'>>;
|
|
396
|
+
remove?: string[];
|
|
397
|
+
} | {
|
|
398
|
+
op: 'remove_marker';
|
|
399
|
+
marker: string;
|
|
400
|
+
} | {
|
|
401
|
+
op: 'replace_project';
|
|
402
|
+
project: VideoProject;
|
|
403
|
+
};
|
|
404
|
+
type VideoEditOp = VideoEdit['op'];
|
|
405
|
+
declare const VIDEO_EDIT_OPS: readonly VideoEditOp[];
|
|
406
|
+
/** What one op made or changed, so the next step can refer to it. */
|
|
407
|
+
interface VideoEditOutcome {
|
|
408
|
+
op: VideoEditOp;
|
|
409
|
+
clip?: string;
|
|
410
|
+
clips?: string[];
|
|
411
|
+
track?: string;
|
|
412
|
+
asset?: string;
|
|
413
|
+
marker?: string;
|
|
414
|
+
}
|
|
415
|
+
type VideoEditResult = {
|
|
416
|
+
ok: true;
|
|
417
|
+
project: VideoProject;
|
|
418
|
+
issues: VideoIssue[];
|
|
419
|
+
outcomes: VideoEditOutcome[];
|
|
420
|
+
} | {
|
|
421
|
+
ok: false;
|
|
422
|
+
error: string;
|
|
423
|
+
};
|
|
424
|
+
/** Default lengths (ms) for clips added without one. */
|
|
425
|
+
declare const VIDEO_DEFAULT_STILL_DURATION = 5000;
|
|
426
|
+
declare const VIDEO_DEFAULT_TITLE_DURATION = 3000;
|
|
427
|
+
/**
|
|
428
|
+
* Applies edits in order, all or nothing: the first that fails (an unknown id, an overlap, a trim
|
|
429
|
+
* past the end of the source, a locked track) stops the lot and the project is unchanged. Edits
|
|
430
|
+
* may not add errors (`checkVideoProject`); ones already there (from a loaded file) do not block.
|
|
431
|
+
* With several edits, errors name the op (`op 2 (trim_clip): …`).
|
|
432
|
+
*/
|
|
433
|
+
declare function applyVideoEdits(project: VideoProject, edits: VideoEdit | readonly VideoEdit[]): VideoEditResult;
|
|
434
|
+
|
|
435
|
+
/** A rectangle in frame pixels. */
|
|
436
|
+
interface VideoRect {
|
|
437
|
+
x: number;
|
|
438
|
+
y: number;
|
|
439
|
+
width: number;
|
|
440
|
+
height: number;
|
|
441
|
+
}
|
|
442
|
+
/** One picture in a frame, in draw order (first is at the back). */
|
|
443
|
+
interface VideoLayer {
|
|
444
|
+
/** The clip it comes from, or `transition:<clip>` for a dip's colour. */
|
|
445
|
+
clip: string;
|
|
446
|
+
track: string;
|
|
447
|
+
type: VideoClip['type'];
|
|
448
|
+
/** For media: the asset, its kind and `src`, and the moment of the source to show (ms). */
|
|
449
|
+
asset?: string;
|
|
450
|
+
assetKind?: VideoAsset['kind'];
|
|
451
|
+
src?: string;
|
|
452
|
+
sourceTime?: number;
|
|
453
|
+
/** 0–1, after fades and transitions. */
|
|
454
|
+
opacity: number;
|
|
455
|
+
/** Where the fitted picture sits before rotation (its centre is the rotation centre). */
|
|
456
|
+
rect: VideoRect;
|
|
457
|
+
/** Degrees clockwise about the rect's centre. */
|
|
458
|
+
rotation: number;
|
|
459
|
+
/** For a wipe: the part shown is the `progress` (0–1) of the rect from the edge opposite `direction`. */
|
|
460
|
+
wipe?: {
|
|
461
|
+
direction: VideoWipeDirection;
|
|
462
|
+
progress: number;
|
|
463
|
+
};
|
|
464
|
+
/** For text: what to draw. */
|
|
465
|
+
text?: {
|
|
466
|
+
text: string;
|
|
467
|
+
color: string;
|
|
468
|
+
fontSize: number;
|
|
469
|
+
fontFamily: string;
|
|
470
|
+
fontWeight: number;
|
|
471
|
+
background?: string;
|
|
472
|
+
};
|
|
473
|
+
/** For colour clips and dips. */
|
|
474
|
+
color?: string;
|
|
475
|
+
}
|
|
476
|
+
/** One sound playing at a moment. */
|
|
477
|
+
interface VideoAudioSource {
|
|
478
|
+
clip: string;
|
|
479
|
+
track: string;
|
|
480
|
+
asset: string;
|
|
481
|
+
src: string;
|
|
482
|
+
/** The moment of the source playing (ms). */
|
|
483
|
+
sourceTime: number;
|
|
484
|
+
/** Gain after clip and track volume, fades and transitions (0 = silent). */
|
|
485
|
+
gain: number;
|
|
486
|
+
speed: number;
|
|
487
|
+
}
|
|
488
|
+
interface VideoFrame {
|
|
489
|
+
time: number;
|
|
490
|
+
width: number;
|
|
491
|
+
height: number;
|
|
492
|
+
background: string;
|
|
493
|
+
layers: VideoLayer[];
|
|
494
|
+
audio: VideoAudioSource[];
|
|
495
|
+
}
|
|
496
|
+
/** Places a picture of `size` in the frame by `fit`, then the clip's transform. */
|
|
497
|
+
declare function placeVideoRect(frame: {
|
|
498
|
+
width: number;
|
|
499
|
+
height: number;
|
|
500
|
+
}, size: {
|
|
501
|
+
width: number;
|
|
502
|
+
height: number;
|
|
503
|
+
}, fit?: VideoFit, transform?: VideoClip['transform']): VideoRect;
|
|
504
|
+
/** A title's text and style with the defaults filled in. */
|
|
505
|
+
declare function videoTextStyle(clip: VideoTextClip): NonNullable<VideoLayer['text']>;
|
|
506
|
+
/**
|
|
507
|
+
* A frame holding only one title, fully opaque on a transparent background, placed as the preview
|
|
508
|
+
* places it: draw it with `drawVideoFrame` and save the canvas as a PNG to give the ffmpeg export
|
|
509
|
+
* titles without `drawtext` (`VideoFfmpegOptions.titleImage`). Fades and opacity are left to the export.
|
|
510
|
+
*/
|
|
511
|
+
declare function videoTitleFrame(project: VideoProject, clip: VideoTextClip): VideoFrame;
|
|
512
|
+
/**
|
|
513
|
+
* What plays at `time` (ms): the pictures to draw, back to front, with their placement, opacity
|
|
514
|
+
* and the moment of source each shows, and the sounds with their gain. A pure function of the
|
|
515
|
+
* project and the time, so previews, exporters and agents all agree on every frame.
|
|
516
|
+
*/
|
|
517
|
+
declare function videoFrameAt(project: VideoProject, time: number): VideoFrame;
|
|
518
|
+
|
|
519
|
+
/** The part of a rect a wipe shows. */
|
|
520
|
+
declare function videoWipeRect(rect: VideoRect, wipe: NonNullable<VideoLayer['wipe']>): VideoRect;
|
|
521
|
+
interface VideoSvgOptions {
|
|
522
|
+
/**
|
|
523
|
+
* The picture for a media layer: return an href (URL or data URL) to draw, or `null` for a
|
|
524
|
+
* labelled placeholder. Default: images use `src`, videos their `poster`, else a placeholder.
|
|
525
|
+
*/
|
|
526
|
+
imageFor?: (layer: VideoLayer, frame: VideoFrame) => string | null;
|
|
527
|
+
/** Frame rate for the timecode on placeholders (default 30). */
|
|
528
|
+
fps?: number;
|
|
529
|
+
}
|
|
530
|
+
/** A frame as standalone SVG: images and posters as `<image>`, video without one as a labelled box. */
|
|
531
|
+
declare function videoFrameToSvg(frame: VideoFrame, options?: VideoSvgOptions, assets?: VideoProject['assets']): string;
|
|
532
|
+
/** The project at `time` as SVG. */
|
|
533
|
+
declare function videoProjectSvg(project: VideoProject, time: number, options?: VideoSvgOptions): string;
|
|
534
|
+
/** Several moments in one labelled picture, `columns` a row, each frame scaled to `width` pixels. */
|
|
535
|
+
declare function videoContactSheetSvg(project: VideoProject, times: readonly number[], options?: VideoSvgOptions & {
|
|
536
|
+
columns?: number;
|
|
537
|
+
width?: number;
|
|
538
|
+
}): string;
|
|
539
|
+
/** The subset of `CanvasRenderingContext2D` `drawVideoFrame` uses (so tests and workers can pass their own). */
|
|
540
|
+
type VideoCanvasContext = Pick<CanvasRenderingContext2D, 'save' | 'restore' | 'fillRect' | 'drawImage' | 'translate' | 'rotate' | 'beginPath' | 'rect' | 'clip' | 'fillText' | 'measureText' | 'setTransform'> & {
|
|
541
|
+
globalAlpha: number;
|
|
542
|
+
fillStyle: string | CanvasGradient | CanvasPattern;
|
|
543
|
+
font: string;
|
|
544
|
+
textAlign: CanvasTextAlign;
|
|
545
|
+
textBaseline: CanvasTextBaseline;
|
|
546
|
+
strokeStyle: string | CanvasGradient | CanvasPattern;
|
|
547
|
+
lineWidth: number;
|
|
548
|
+
strokeRect(x: number, y: number, w: number, h: number): void;
|
|
549
|
+
};
|
|
550
|
+
/**
|
|
551
|
+
* Draws a frame on a 2D canvas scaled by `scale` (canvas pixels per frame pixel). `sourceFor`
|
|
552
|
+
* returns what to draw for a media layer (a `<video>` seeked to `layer.sourceTime`, an `<img>`,
|
|
553
|
+
* an `ImageBitmap`…) or `null` for a placeholder box.
|
|
554
|
+
*/
|
|
555
|
+
declare function drawVideoFrame(ctx: VideoCanvasContext, frame: VideoFrame, sourceFor: (layer: VideoLayer) => CanvasImageSource | null, scale?: number): void;
|
|
556
|
+
|
|
557
|
+
interface VideoEdlOptions {
|
|
558
|
+
/** Track to export (default: the bottom video track, V1 in most NLEs). */
|
|
559
|
+
track?: string;
|
|
560
|
+
/** Record timecode of the timeline's start (ms, default 1 hour, the usual 01:00:00:00). */
|
|
561
|
+
recordStart?: number;
|
|
562
|
+
/** Title line (default: the project's name). */
|
|
563
|
+
title?: string;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* One track as a CMX 3600 edit decision list, for conforming in another NLE: an event per clip with
|
|
567
|
+
* source and record timecode at the project's frame rate, dissolves (`D`) for crossfades, dips and
|
|
568
|
+
* wipes (`W001`), `M2` lines for speed changes, and `* FROM CLIP NAME` comments. Titles and colour
|
|
569
|
+
* clips become black (`BL`) events.
|
|
570
|
+
*/
|
|
571
|
+
declare function toVideoEdl(project: VideoProject, options?: VideoEdlOptions): string;
|
|
572
|
+
interface VideoFfmpegOptions {
|
|
573
|
+
/** Output file (default `output.mp4`). */
|
|
574
|
+
output?: string;
|
|
575
|
+
/** The path or URL ffmpeg should open for an asset (default its `src`). */
|
|
576
|
+
resolve?: (asset: VideoAsset) => string;
|
|
577
|
+
/** Default `libx264`. */
|
|
578
|
+
videoCodec?: string;
|
|
579
|
+
/** Default `aac`. */
|
|
580
|
+
audioCodec?: string;
|
|
581
|
+
/** x264 quality (default 18). */
|
|
582
|
+
crf?: number;
|
|
583
|
+
/** A font file for titles (drawtext's `fontfile`); default the system's font for the family. */
|
|
584
|
+
fontFile?: string;
|
|
585
|
+
/**
|
|
586
|
+
* Titles as images instead of `drawtext` (which needs an ffmpeg built with libfreetype; Homebrew's
|
|
587
|
+
* is not): return the path or URL of a transparent PNG the size of the frame showing the title as
|
|
588
|
+
* the preview draws it (`videoTitleFrame` + `drawVideoFrame`). It is overlaid like a still, with
|
|
589
|
+
* the title's opacity and fades.
|
|
590
|
+
*/
|
|
591
|
+
titleImage?: (clip: VideoTextClip, size: {
|
|
592
|
+
width: number;
|
|
593
|
+
height: number;
|
|
594
|
+
}) => string;
|
|
595
|
+
}
|
|
596
|
+
interface VideoFfmpegExport {
|
|
597
|
+
/** Arguments after `ffmpeg` (pass to `spawn('ffmpeg', args)`; no shell quoting needed). */
|
|
598
|
+
args: string[];
|
|
599
|
+
/** The same as one shell command line, quoted for a POSIX shell. */
|
|
600
|
+
command: string;
|
|
601
|
+
/** The `-filter_complex` graph on its own. */
|
|
602
|
+
filterComplex: string;
|
|
603
|
+
/** Length rendered (ms). */
|
|
604
|
+
duration: number;
|
|
605
|
+
/** Anything the render only approximates. */
|
|
606
|
+
notes: string[];
|
|
607
|
+
}
|
|
608
|
+
/** Escapes a value for a filter option inside a filtergraph (option level, then graph level). */
|
|
609
|
+
declare const ffmpegFilterValue: (s: string) => string;
|
|
610
|
+
/**
|
|
611
|
+
* The project as an ffmpeg command: each clip an input trimmed to what it plays (plus transition
|
|
612
|
+
* overlap), scaled and placed as `videoFrameAt` places it, faded, and overlaid on a background in
|
|
613
|
+
* track order between its start and end; titles with `drawtext`; sound delayed into place, faded,
|
|
614
|
+
* mixed and trimmed to the project's length. Render on a server or with ffmpeg.wasm.
|
|
615
|
+
*/
|
|
616
|
+
declare function toVideoFfmpeg(project: VideoProject, options?: VideoFfmpegOptions): VideoFfmpegExport;
|
|
617
|
+
|
|
618
|
+
/** What a preview follows: a `VideoEditorController` fits. */
|
|
619
|
+
interface VideoPreviewSource {
|
|
620
|
+
getState(): {
|
|
621
|
+
project: VideoProject;
|
|
622
|
+
time: number;
|
|
623
|
+
playing: boolean;
|
|
624
|
+
};
|
|
625
|
+
subscribe(listener: () => void): () => void;
|
|
626
|
+
}
|
|
627
|
+
/** The parts of a media element the preview drives (so tests can pass fakes). */
|
|
628
|
+
interface VideoPreviewMedia {
|
|
629
|
+
src: string;
|
|
630
|
+
currentTime: number;
|
|
631
|
+
playbackRate: number;
|
|
632
|
+
volume: number;
|
|
633
|
+
muted: boolean;
|
|
634
|
+
paused: boolean;
|
|
635
|
+
readyState: number;
|
|
636
|
+
preload: string;
|
|
637
|
+
play(): Promise<void> | void;
|
|
638
|
+
pause(): void;
|
|
639
|
+
addEventListener(type: string, listener: () => void): void;
|
|
640
|
+
removeEventListener(type: string, listener: () => void): void;
|
|
641
|
+
removeAttribute?(name: string): void;
|
|
642
|
+
load?(): void;
|
|
643
|
+
}
|
|
644
|
+
interface VideoPreviewImage {
|
|
645
|
+
src: string;
|
|
646
|
+
complete: boolean;
|
|
647
|
+
addEventListener(type: string, listener: () => void): void;
|
|
648
|
+
}
|
|
649
|
+
interface VideoPreviewOptions {
|
|
650
|
+
/** Canvas pixels per frame pixel (default: fits the canvas's displayed width at the device pixel ratio, at most 1). */
|
|
651
|
+
resolution?: number;
|
|
652
|
+
/** How far (ms) a playing element may drift from the timeline before it is re-seeked (default 200). */
|
|
653
|
+
tolerance?: number;
|
|
654
|
+
/** Makes media and image elements (default `document.createElement`). */
|
|
655
|
+
createMedia?: (kind: 'video' | 'audio') => VideoPreviewMedia;
|
|
656
|
+
createImage?: () => VideoPreviewImage;
|
|
657
|
+
/** The 2D context to draw on (default `canvas.getContext('2d')`). */
|
|
658
|
+
context?: VideoCanvasContext | null;
|
|
659
|
+
/** Called after each frame is drawn. */
|
|
660
|
+
onFrame?: (frame: VideoFrame) => void;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Plays a project in the browser: keeps one `<video>` or `<audio>` element per playing clip in step
|
|
664
|
+
* with the timeline (seeking when it drifts, matching speed and gain, pausing with the editor), and
|
|
665
|
+
* draws each frame (`videoFrameAt` + `drawVideoFrame`) on a canvas. Framework-free: React and Svelte
|
|
666
|
+
* wrap it in `<VideoPreview>`. Elements for clips that stop playing are paused and released.
|
|
667
|
+
*/
|
|
668
|
+
declare class VideoPreviewEngine {
|
|
669
|
+
private readonly source;
|
|
670
|
+
private readonly options;
|
|
671
|
+
private readonly canvas;
|
|
672
|
+
private readonly ctx;
|
|
673
|
+
private readonly media;
|
|
674
|
+
private readonly images;
|
|
675
|
+
private readonly unsubscribe;
|
|
676
|
+
private scheduled;
|
|
677
|
+
private destroyed;
|
|
678
|
+
constructor(source: VideoPreviewSource, canvas: {
|
|
679
|
+
width: number;
|
|
680
|
+
height: number;
|
|
681
|
+
clientWidth?: number;
|
|
682
|
+
getContext?: (id: '2d') => unknown;
|
|
683
|
+
}, options?: VideoPreviewOptions);
|
|
684
|
+
private createMedia;
|
|
685
|
+
private image;
|
|
686
|
+
private entry;
|
|
687
|
+
private release;
|
|
688
|
+
/** Brings the media elements in step with the editor and draws the frame. */
|
|
689
|
+
update(): void;
|
|
690
|
+
private redraw;
|
|
691
|
+
private sourceFor;
|
|
692
|
+
private draw;
|
|
693
|
+
/** Stops following the editor and releases every media element. */
|
|
694
|
+
destroy(): void;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
interface VideoEditorOptions {
|
|
698
|
+
/** The project to open (default: an empty 1920 × 1080, 30 fps project with tracks v1 and a1). */
|
|
699
|
+
project?: VideoProject;
|
|
700
|
+
/** Shared undo history (default: a new one). */
|
|
701
|
+
history?: HistoryManager;
|
|
702
|
+
/** Time source for playback (default: requestAnimationFrame). Pass a `ManualClock` to step frames. */
|
|
703
|
+
clock?: Clock;
|
|
704
|
+
readOnly?: boolean;
|
|
705
|
+
/** Called after every change to the project (not on playback or selection). */
|
|
706
|
+
onChange?: (project: VideoProject) => void;
|
|
707
|
+
}
|
|
708
|
+
/** A snapshot of the editor. The same object is returned until something changes. */
|
|
709
|
+
interface VideoEditorState {
|
|
710
|
+
project: VideoProject;
|
|
711
|
+
/** Total length (ms): the end of the last clip. */
|
|
712
|
+
duration: number;
|
|
713
|
+
/** Playhead (ms). */
|
|
714
|
+
time: number;
|
|
715
|
+
playing: boolean;
|
|
716
|
+
loop: AnimationLoopMode;
|
|
717
|
+
/** Selected clip ids, in the order they were selected. */
|
|
718
|
+
selection: readonly string[];
|
|
719
|
+
/** Clips under the playhead, top track first. */
|
|
720
|
+
current: readonly string[];
|
|
721
|
+
/** The project's problems (see `checkVideoProject`). */
|
|
722
|
+
issues: readonly VideoIssue[];
|
|
723
|
+
readOnly: boolean;
|
|
724
|
+
/** Why the last edit was refused, if it was. */
|
|
725
|
+
error: string | null;
|
|
726
|
+
canUndo: boolean;
|
|
727
|
+
canRedo: boolean;
|
|
728
|
+
undoLabel: string;
|
|
729
|
+
redoLabel: string;
|
|
730
|
+
/** Bumped on every change. */
|
|
731
|
+
revision: number;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Headless non-linear video editor: a `VideoProject` of assets and tracks of clips, edited with
|
|
735
|
+
* undoable, all-or-nothing edits (insert, ripple, trim, split, move, transitions), played on an
|
|
736
|
+
* injectable clock, with a clip selection, snapping and frames at any time. UI-agnostic: React and
|
|
737
|
+
* Svelte bind it with `useVideoEditor` / `createVideoEditor`, `VideoPreviewEngine` plays it in a
|
|
738
|
+
* browser and agents drive it through `createVideoEditorToolProvider`. `getState()` returns the same
|
|
739
|
+
* object until something changes, so it works with `useSyncExternalStore` and Svelte runes.
|
|
740
|
+
*/
|
|
741
|
+
declare class VideoEditorController {
|
|
742
|
+
readonly history: HistoryManager;
|
|
743
|
+
readonly player: AnimationPlayer;
|
|
744
|
+
private project;
|
|
745
|
+
private issues;
|
|
746
|
+
private selection;
|
|
747
|
+
private readOnly;
|
|
748
|
+
private error;
|
|
749
|
+
private revision;
|
|
750
|
+
private applying;
|
|
751
|
+
private readonly onChange?;
|
|
752
|
+
private readonly listeners;
|
|
753
|
+
private readonly unsubscribers;
|
|
754
|
+
private state;
|
|
755
|
+
constructor(options?: VideoEditorOptions);
|
|
756
|
+
private connect;
|
|
757
|
+
/** Pauses and stops listening to the player and history. Subscribing again reconnects (React StrictMode). */
|
|
758
|
+
destroy(): void;
|
|
759
|
+
getState: () => VideoEditorState;
|
|
760
|
+
subscribe: (listener: (state: VideoEditorState) => void) => (() => void);
|
|
761
|
+
/** The current project (immutable; edit through the controller). */
|
|
762
|
+
getProject(): VideoProject;
|
|
763
|
+
private computeState;
|
|
764
|
+
private notify;
|
|
765
|
+
private setProject;
|
|
766
|
+
private commit;
|
|
767
|
+
private refuse;
|
|
768
|
+
/** Why edits are refused now, or `null` when they are allowed. */
|
|
769
|
+
readOnlyReason(): string | null;
|
|
770
|
+
/**
|
|
771
|
+
* Applies edits as one undo step, all or nothing (see `applyVideoEdits`). With `dryRun` it
|
|
772
|
+
* reports what would happen and changes nothing.
|
|
773
|
+
*/
|
|
774
|
+
apply(edits: VideoEdit | readonly VideoEdit[], label?: string, options?: {
|
|
775
|
+
dryRun?: boolean;
|
|
776
|
+
}): VideoEditResult;
|
|
777
|
+
/** Opens a project (JSON or object). `clearHistory` drops the undo stack (opening a file); otherwise it is one undo step. */
|
|
778
|
+
load(project: VideoProject | string, options?: {
|
|
779
|
+
clearHistory?: boolean;
|
|
780
|
+
}): VideoEditResult;
|
|
781
|
+
setReadOnly(readOnly: boolean): void;
|
|
782
|
+
/** Selects clips (unknown ids are dropped). `add` toggles them into the current selection. */
|
|
783
|
+
select(clips: readonly string[] | null, options?: {
|
|
784
|
+
add?: boolean;
|
|
785
|
+
}): void;
|
|
786
|
+
play(): void;
|
|
787
|
+
pause(): void;
|
|
788
|
+
toggle(): void;
|
|
789
|
+
/** Moves the playhead (ms). */
|
|
790
|
+
seek(time: number): void;
|
|
791
|
+
/** Moves the playhead by whole frames (negative steps back), pausing. */
|
|
792
|
+
step(frames: number): void;
|
|
793
|
+
setLoop(loop: AnimationLoopMode): void;
|
|
794
|
+
undo(): boolean;
|
|
795
|
+
redo(): boolean;
|
|
796
|
+
/**
|
|
797
|
+
* `time` moved to the nearest clip edge, marker or the playhead within `threshold` ms (default:
|
|
798
|
+
* 8 frames), else to the nearest frame. Clips in `exclude` (the ones being dragged) are ignored.
|
|
799
|
+
*/
|
|
800
|
+
snap(time: number, options?: {
|
|
801
|
+
exclude?: Iterable<string>;
|
|
802
|
+
threshold?: number;
|
|
803
|
+
playhead?: boolean;
|
|
804
|
+
}): {
|
|
805
|
+
time: number;
|
|
806
|
+
snapped: number | null;
|
|
807
|
+
};
|
|
808
|
+
/** Splits the selected clips (or every clip) under the playhead there, as one undo step. */
|
|
809
|
+
splitAtPlayhead(): VideoEditResult;
|
|
810
|
+
/** Deletes the selected clips; `ripple` closes the gaps they leave. */
|
|
811
|
+
deleteSelection(options?: {
|
|
812
|
+
ripple?: boolean;
|
|
813
|
+
}): VideoEditResult;
|
|
814
|
+
/** What plays at `time` (default: the playhead). */
|
|
815
|
+
frame(time?: number): VideoFrame;
|
|
816
|
+
/** The frame at `time` (default: the playhead) as SVG. */
|
|
817
|
+
toSvg(time?: number, options?: VideoSvgOptions): string;
|
|
818
|
+
/** The project as an ffmpeg command. */
|
|
819
|
+
toFfmpeg(options?: VideoFfmpegOptions): VideoFfmpegExport;
|
|
820
|
+
/** One track as a CMX 3600 EDL. */
|
|
821
|
+
toEdl(options?: VideoEdlOptions): string;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
interface VideoEditorToolProviderOptions {
|
|
825
|
+
/** Provider id (unique per registry). Default `video`. */
|
|
826
|
+
id?: string;
|
|
827
|
+
/** Always prefix tool names with `<namespace>__`. */
|
|
828
|
+
namespace?: string;
|
|
829
|
+
/** The project cannot be edited: write tools are hidden and `get_video_project` says why. A string is the reason. */
|
|
830
|
+
readOnly?: boolean | string | (() => boolean | string);
|
|
831
|
+
/** Turns SVG into PNG (e.g. `rasterizeSvg`), so `render_video_frame` can return an image. */
|
|
832
|
+
rasterize?: (svg: string, size: {
|
|
833
|
+
width: number;
|
|
834
|
+
height: number;
|
|
835
|
+
}) => Promise<{
|
|
836
|
+
data: string;
|
|
837
|
+
mimeType: string;
|
|
838
|
+
}> | {
|
|
839
|
+
data: string;
|
|
840
|
+
mimeType: string;
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
/** A project as agents see it: settings, assets, and each track's clips in order with their times. */
|
|
844
|
+
declare function describeVideoProject(project: VideoProject): {
|
|
845
|
+
assets: VideoAsset[];
|
|
846
|
+
tracks: {
|
|
847
|
+
clips: ({
|
|
848
|
+
source?: {
|
|
849
|
+
in: number;
|
|
850
|
+
out: number;
|
|
851
|
+
} | null | undefined;
|
|
852
|
+
end: number;
|
|
853
|
+
type: "media";
|
|
854
|
+
asset: string;
|
|
855
|
+
in?: number;
|
|
856
|
+
speed?: number;
|
|
857
|
+
volume?: number;
|
|
858
|
+
muted?: boolean;
|
|
859
|
+
id: string;
|
|
860
|
+
name?: string;
|
|
861
|
+
start: number;
|
|
862
|
+
duration: number;
|
|
863
|
+
opacity?: number;
|
|
864
|
+
transform?: VideoTransform;
|
|
865
|
+
fit?: VideoFit;
|
|
866
|
+
fadeIn?: number;
|
|
867
|
+
fadeOut?: number;
|
|
868
|
+
transition?: VideoTransition;
|
|
869
|
+
} | {
|
|
870
|
+
source?: {
|
|
871
|
+
in: number;
|
|
872
|
+
out: number;
|
|
873
|
+
} | null | undefined;
|
|
874
|
+
end: number;
|
|
875
|
+
type: "text";
|
|
876
|
+
text: string;
|
|
877
|
+
color?: string;
|
|
878
|
+
fontSize?: number;
|
|
879
|
+
fontFamily?: string;
|
|
880
|
+
fontWeight?: number;
|
|
881
|
+
background?: string;
|
|
882
|
+
id: string;
|
|
883
|
+
name?: string;
|
|
884
|
+
start: number;
|
|
885
|
+
duration: number;
|
|
886
|
+
opacity?: number;
|
|
887
|
+
transform?: VideoTransform;
|
|
888
|
+
fit?: VideoFit;
|
|
889
|
+
fadeIn?: number;
|
|
890
|
+
fadeOut?: number;
|
|
891
|
+
transition?: VideoTransition;
|
|
892
|
+
} | {
|
|
893
|
+
source?: {
|
|
894
|
+
in: number;
|
|
895
|
+
out: number;
|
|
896
|
+
} | null | undefined;
|
|
897
|
+
end: number;
|
|
898
|
+
type: "color";
|
|
899
|
+
color: string;
|
|
900
|
+
id: string;
|
|
901
|
+
name?: string;
|
|
902
|
+
start: number;
|
|
903
|
+
duration: number;
|
|
904
|
+
opacity?: number;
|
|
905
|
+
transform?: VideoTransform;
|
|
906
|
+
fit?: VideoFit;
|
|
907
|
+
fadeIn?: number;
|
|
908
|
+
fadeOut?: number;
|
|
909
|
+
transition?: VideoTransition;
|
|
910
|
+
})[];
|
|
911
|
+
volume?: number | undefined;
|
|
912
|
+
locked?: boolean | undefined;
|
|
913
|
+
hidden?: boolean | undefined;
|
|
914
|
+
muted?: boolean | undefined;
|
|
915
|
+
name?: string | undefined;
|
|
916
|
+
id: string;
|
|
917
|
+
kind: VideoTrackKind;
|
|
918
|
+
index: number;
|
|
919
|
+
}[];
|
|
920
|
+
markers: VideoMarker[];
|
|
921
|
+
background?: string | undefined;
|
|
922
|
+
width: number;
|
|
923
|
+
height: number;
|
|
924
|
+
fps: number;
|
|
925
|
+
duration: number;
|
|
926
|
+
name?: string | undefined;
|
|
927
|
+
};
|
|
928
|
+
/**
|
|
929
|
+
* Agent tools for a `VideoEditorController`: read the project (assets, tracks with clips and their
|
|
930
|
+
* times, gaps, what plays at a moment), check it, render frames, export to ffmpeg or an EDL, and
|
|
931
|
+
* edit it a step at a time (add assets and tracks, add / move / trim / split / delete clips, ripple,
|
|
932
|
+
* transitions, markers). Every write is checked first (unknown ids with "did you mean", overlaps,
|
|
933
|
+
* trims past the source, locked tracks), applies as one undo step, and returns what it made and
|
|
934
|
+
* the resulting length and issues. Write tools are hidden while the project is read-only.
|
|
935
|
+
*/
|
|
936
|
+
declare function createVideoEditorToolProvider(controller: VideoEditorController, options?: VideoEditorToolProviderOptions): AgentToolProvider;
|
|
937
|
+
|
|
938
|
+
interface VideoRulerTick {
|
|
939
|
+
time: number;
|
|
940
|
+
/** Pixels from the timeline's left edge. */
|
|
941
|
+
x: number;
|
|
942
|
+
/** Major ticks carry a label. */
|
|
943
|
+
major: boolean;
|
|
944
|
+
label?: string;
|
|
945
|
+
}
|
|
946
|
+
/** Ruler ticks for `[0, duration]` at `pxPerSecond`: a major tick at least every ~80 px, labelled with timecode. */
|
|
947
|
+
declare function videoRulerTicks(duration: number, pxPerSecond: number, fps: number): VideoRulerTick[];
|
|
948
|
+
/** Fill colour a timeline uses for a clip, by what it is. */
|
|
949
|
+
declare function videoClipTint(clip: VideoClip, asset?: VideoAsset): string;
|
|
950
|
+
/** A short label for a clip on the timeline. */
|
|
951
|
+
declare function videoClipLabel(clip: VideoClip, asset?: VideoAsset): string;
|
|
952
|
+
type VideoClipDragMode = 'move' | 'trim-start' | 'trim-end';
|
|
953
|
+
interface VideoClipDragInput {
|
|
954
|
+
clip: string;
|
|
955
|
+
mode: VideoClipDragMode;
|
|
956
|
+
/** How far the pointer moved along the timeline (ms). */
|
|
957
|
+
delta: number;
|
|
958
|
+
/** For move: the track under the pointer (default the clip's own). */
|
|
959
|
+
track?: string;
|
|
960
|
+
/** Push later clips along instead of refusing overlaps (trims) — hold a modifier for it. */
|
|
961
|
+
ripple?: boolean;
|
|
962
|
+
/** Snaps a time (e.g. `controller.snap(t, { exclude: [clip] }).time`); default: none. */
|
|
963
|
+
snap?: (time: number) => number;
|
|
964
|
+
}
|
|
965
|
+
interface VideoClipDragPlan {
|
|
966
|
+
/** Where the clip would be: draw this while dragging. */
|
|
967
|
+
start: number;
|
|
968
|
+
duration: number;
|
|
969
|
+
track: string;
|
|
970
|
+
/** The edit to apply on release (`null` when nothing changed). */
|
|
971
|
+
edit: VideoEdit | null;
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* What dragging a clip (or one of its edges) by `delta` would do: the clip's new place for drawing
|
|
975
|
+
* while the pointer moves, and the edit to apply on release. Moves snap the edge nearer a snap
|
|
976
|
+
* point; trims snap the edge being dragged. The edit is checked when it is applied, so an overlap
|
|
977
|
+
* or a trim past the source is refused then (with the reason in the editor's state).
|
|
978
|
+
*/
|
|
979
|
+
declare function planVideoClipDrag(project: VideoProject, input: VideoClipDragInput): VideoClipDragPlan | null;
|
|
980
|
+
/** Tracks a clip may be dragged onto: the same kind as its own, unlocked. */
|
|
981
|
+
declare function videoDropTracks(project: VideoProject, clipId: string): string[];
|
|
982
|
+
/** The edit that adds an asset as a clip where the person dropped it (or at the end of the first fitting track). */
|
|
983
|
+
declare function videoAssetDropEdit(project: VideoProject, assetId: string, options?: {
|
|
984
|
+
track?: string;
|
|
985
|
+
start?: number;
|
|
986
|
+
ripple?: boolean;
|
|
987
|
+
}): VideoEdit | null;
|
|
988
|
+
/** What `probeVideoMedia` found out about a file. */
|
|
989
|
+
interface VideoMediaProbe {
|
|
990
|
+
kind: VideoAssetKind;
|
|
991
|
+
duration?: number;
|
|
992
|
+
width?: number;
|
|
993
|
+
height?: number;
|
|
994
|
+
}
|
|
995
|
+
/** The asset kind for a MIME type or file name, or `null` when it is not media. */
|
|
996
|
+
declare function videoAssetKindOf(typeOrName: string): VideoAssetKind | null;
|
|
997
|
+
/**
|
|
998
|
+
* Reads a media file's length and size in the browser by loading its metadata into a detached
|
|
999
|
+
* `<video>`, `<audio>` or `<img>`. Rejects with a readable error when it cannot be loaded.
|
|
1000
|
+
*/
|
|
1001
|
+
declare function probeVideoMedia(src: string, kind: VideoAssetKind, timeout?: number): Promise<VideoMediaProbe>;
|
|
1002
|
+
|
|
1003
|
+
export { type CreateVideoProjectOptions, type NewVideoClip, VIDEO_CLIP_TYPES, VIDEO_DEFAULT_STILL_DURATION, VIDEO_DEFAULT_TITLE_DURATION, VIDEO_EDIT_OPS, VIDEO_FITS, VIDEO_ISSUE_CODES, VIDEO_TIME_EPSILON, VIDEO_TRANSITION_TYPES, VIDEO_WIPE_DIRECTIONS, type VideoAsset, type VideoAssetKind, type VideoAudioSource, type VideoCanvasContext, type VideoClip, type VideoClipDragInput, type VideoClipDragMode, type VideoClipDragPlan, type VideoClipLocation, type VideoClipType, type VideoColorClip, type VideoEdit, type VideoEditOp, type VideoEditOutcome, type VideoEditResult, VideoEditorController, type VideoEditorOptions, type VideoEditorState, type VideoEditorToolProviderOptions, type VideoEdlOptions, type VideoFfmpegExport, type VideoFfmpegOptions, type VideoFit, type VideoFrame, type VideoGap, type VideoIssue, type VideoIssueCode, type VideoIssueSeverity, type VideoLayer, type VideoMarker, type VideoMediaClip, type VideoMediaProbe, VideoPreviewEngine, type VideoPreviewImage, type VideoPreviewMedia, type VideoPreviewOptions, type VideoPreviewSource, type VideoProject, type VideoRect, type VideoRulerTick, type VideoSnapOptions, type VideoSvgOptions, type VideoTextClip, type VideoTrack, type VideoTrackKind, type VideoTransform, type VideoTransition, type VideoTransitionType, type VideoWipeDirection, allVideoClipIds, applyVideoEdits, checkVideoProject, createVideoEditorToolProvider, createVideoProject, describeVideoProject, drawVideoFrame, ffmpegFilterValue, findVideoClip, findVideoGaps, formatVideoSeconds, formatVideoTimecode, nextFreeVideoSlot, parseVideoAsset, parseVideoClip, parseVideoMarker, parseVideoProject, parseVideoTrack, parseVideoTransform, parseVideoTransition, placeVideoRect, planVideoClipDrag, probeVideoMedia, snapToVideoFrame, snapVideoTime, sortedVideoClips, toVideoEdl, toVideoFfmpeg, videoAssetDropEdit, videoAssetKindOf, videoAssetMap, videoClipAfter, videoClipBefore, videoClipEnd, videoClipHandles, videoClipLabel, videoClipOverhang, videoClipSourceRange, videoClipSpeed, videoClipTint, videoClipsAt, videoContactSheetSvg, videoDropTracks, videoFrameAt, videoFrameDuration, videoFrameToSvg, videoFrames, videoIssueKey, videoOverlap, videoProjectDuration, videoProjectSvg, videoRulerTicks, videoSnapPoints, videoTextStyle, videoTitleFrame, videoWipeRect };
|