@shotstack/shotstack-studio 2.0.0-beta.9 → 2.0.0-rc.1

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 CHANGED
@@ -5,7 +5,7 @@
5
5
  "cpuccino",
6
6
  "dazzatron"
7
7
  ],
8
- "version": "2.0.0-beta.9",
8
+ "version": "2.0.0-rc.1",
9
9
  "description": "A video editing library for creating and editing videos with Shotstack",
10
10
  "type": "module",
11
11
  "main": "dist/shotstack-studio.umd.js",
@@ -18,17 +18,18 @@
18
18
  "require": "./dist/shotstack-studio.umd.js",
19
19
  "default": "./dist/shotstack-studio.es.js"
20
20
  },
21
- "./schema": {
22
- "types": "./dist/schema/index.d.ts",
23
- "import": "./dist/schema/index.mjs",
24
- "require": "./dist/schema/index.cjs",
25
- "default": "./dist/schema/index.mjs"
21
+ "./internal": {
22
+ "types": "./dist/internal.d.ts",
23
+ "import": "./dist/internal.es.js",
24
+ "require": "./dist/internal.umd.js",
25
+ "default": "./dist/internal.es.js"
26
26
  }
27
27
  },
28
28
  "files": [
29
29
  "dist/shotstack-studio.umd.js",
30
30
  "dist/shotstack-studio.es.js",
31
- "dist/schema/**",
31
+ "dist/internal.umd.js",
32
+ "dist/internal.es.js",
32
33
  "dist/**/*.d.ts"
33
34
  ],
34
35
  "keywords": [
@@ -38,16 +39,20 @@
38
39
  "ffmpeg"
39
40
  ],
40
41
  "license": "PolyForm Shield License 1.0.0",
42
+ "engines": {
43
+ "node": ">=22 <23"
44
+ },
41
45
  "repository": {
42
46
  "type": "git",
43
47
  "url": "https://github.com/shotstack/shotstack-studio-sdk"
44
48
  },
45
49
  "scripts": {
46
50
  "dev": "vite",
51
+ "dev:shotstack": "vite --open /shotstack.html",
47
52
  "start": "npm run build && vite preview",
48
- "build": "npm run build:main && npm run build:schema",
53
+ "build": "npm run build:main && npm run build:internal",
49
54
  "build:main": "vite build",
50
- "build:schema": "vite build --config vite.config.schema.ts",
55
+ "build:internal": "node scripts/build-internal.mjs",
51
56
  "test": "jest",
52
57
  "test:watch": "jest --watch",
53
58
  "test:package": "node test-package.js",
@@ -55,11 +60,12 @@
55
60
  "lint": "eslint --ignore-path .gitignore .",
56
61
  "lint:fix": "eslint --ignore-path .gitignore --fix .",
57
62
  "format": "prettier --ignore-path .gitignore --write .",
58
- "prepublishOnly": "npm run build && npm run test && npm run test:package",
63
+ "verify:ci": "npm run lint && npm run typecheck && npm run test && npm run build && npm run test:package",
64
+ "release:check": "npm run verify:ci && npm pack --dry-run --json",
65
+ "prepublishOnly": "npm run release:check",
59
66
  "generate:fonts": "npx tsx scripts/fetch-google-fonts.ts"
60
67
  },
61
68
  "devDependencies": {
62
- "@jest/globals": "^30.2.0",
63
69
  "@types/howler": "^2.2.12",
64
70
  "@types/jest": "^30.0.0",
65
71
  "@types/node": "^22.9.0",
@@ -80,16 +86,13 @@
80
86
  "vite-plugin-dts": "^4.5.4"
81
87
  },
82
88
  "dependencies": {
83
- "@huggingface/transformers": "^3.0.0",
84
- "@shotstack/schemas": "^1.3.9",
85
- "@shotstack/shotstack-canvas": "^1.8.0",
86
- "fast-deep-equal": "^3.1.3",
89
+ "@shotstack/schemas": "1.7.0",
90
+ "@shotstack/shotstack-canvas": "^1.9.6",
87
91
  "howler": "^2.2.4",
88
92
  "mediabunny": "^1.11.2",
89
- "opentype": "^0.1.2",
90
93
  "opentype.js": "^1.3.4",
91
94
  "pixi-filters": "^6.0.5",
92
- "pixi.js": "^8.5.2",
95
+ "pixi.js": "^8.15.0",
93
96
  "zod": "^4.0.0"
94
97
  }
95
98
  }
package/readme.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![License](https://img.shields.io/badge/license-PolyForm_Shield-blue.svg)](https://polyformproject.org/licenses/shield/1.0.0/)
5
5
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.6-blue.svg)](https://www.typescriptlang.org/)
6
6
 
7
- A JavaScript library for creating and editing videos in the browser.
7
+ A JavaScript SDK for browser-based video editing with timeline, canvas preview, and export.
8
8
 
9
9
  ## Interactive Examples
10
10
 
@@ -18,11 +18,11 @@ Try Shotstack Studio in your preferred framework:
18
18
 
19
19
  ## Features
20
20
 
21
- - Create video compositions with multiple tracks and clips
22
- - Visual timeline interface
23
- - Multi-track, drag-and-drop clip manipulation with snap-to-grid
24
- - Use in conjunction with the [Shotstack Edit API](https://shotstack.io/docs/guide/getting-started/hello-world-using-curl/) to render video
25
- - Export to video via the browser
21
+ - Template-driven editing with undo/redo command model
22
+ - Canvas preview rendering
23
+ - Visual timeline with drag, resize, selection, and snapping
24
+ - Extensible UI via `UIController` button API
25
+ - Browser export pipeline via `VideoExporter`
26
26
 
27
27
  ## Installation
28
28
 
@@ -37,33 +37,57 @@ yarn add @shotstack/shotstack-studio
37
37
  ## Quick Start
38
38
 
39
39
  ```typescript
40
- import { Edit, Canvas, Controls, Timeline } from "@shotstack/shotstack-studio";
40
+ import { Edit, Canvas, Controls, Timeline, UIController } from "@shotstack/shotstack-studio";
41
41
 
42
- // 1. Load a template
42
+ // 1) Load a template
43
43
  const response = await fetch("https://shotstack-assets.s3.amazonaws.com/templates/hello-world/hello.json");
44
44
  const template = await response.json();
45
45
 
46
- // 2. Create Edit from template and load it
46
+ // 2) Create core components
47
47
  const edit = new Edit(template);
48
+ const canvas = new Canvas(edit);
49
+ const ui = UIController.create(edit, canvas);
50
+
51
+ // 3) Load runtime
52
+ await canvas.load();
48
53
  await edit.load();
49
54
 
50
- // 3. Create a canvas to display the edit
51
- const canvas = new Canvas(edit);
52
- await canvas.load(); // Renders to [data-shotstack-studio] element
55
+ // 4) Add one custom UI button
56
+ ui.registerButton({
57
+ id: "text",
58
+ icon: `<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3H13"/><path d="M8 3V13"/><path d="M5 13H11"/></svg>`,
59
+ tooltip: "Add Text"
60
+ });
53
61
 
54
- // 4. Initialize the Timeline
55
- const container = document.querySelector("[data-shotstack-timeline]");
56
- const timeline = new Timeline(edit, container, {
57
- features: { toolbar: true, ruler: true, playhead: true, snap: true }
62
+ ui.on("button:text", ({ position }) => {
63
+ edit.addTrack(0, {
64
+ clips: [
65
+ {
66
+ asset: {
67
+ type: "rich-text",
68
+ text: "Title",
69
+ font: { family: "Work Sans", size: 72, weight: 600, color: "#ffffff", opacity: 1 },
70
+ align: { horizontal: "center", vertical: "middle" }
71
+ },
72
+ start: position,
73
+ length: 5,
74
+ width: 500,
75
+ height: 200
76
+ }
77
+ ]
78
+ });
58
79
  });
80
+
81
+ // 5) Timeline + controls
82
+ const timelineContainer = document.querySelector("[data-shotstack-timeline]") as HTMLElement;
83
+ const timeline = new Timeline(edit, timelineContainer);
59
84
  await timeline.load();
60
85
 
61
- // 5. Add keyboard controls
62
86
  const controls = new Controls(edit);
63
87
  await controls.load();
64
88
  ```
65
89
 
66
- Your HTML should include containers for both the canvas and timeline:
90
+ Your HTML must include both containers:
67
91
 
68
92
  ```html
69
93
  <div data-shotstack-studio></div>
@@ -74,185 +98,159 @@ Your HTML should include containers for both the canvas and timeline:
74
98
 
75
99
  ### Edit
76
100
 
77
- The Edit class represents a video project with its timeline, clips, and properties.
101
+ `Edit` is the runtime editing session and source of truth for document mutations.
78
102
 
79
103
  ```typescript
80
104
  import { Edit } from "@shotstack/shotstack-studio";
81
105
 
82
- // Create an edit from a template
83
106
  const edit = new Edit(templateJson);
84
107
  await edit.load();
85
108
 
86
- // Or reload a different template later
87
- await edit.loadEdit(newTemplate);
109
+ await edit.loadEdit(nextTemplateJson);
88
110
 
89
- // Playback controls
111
+ // Playback (seconds)
90
112
  edit.play();
91
113
  edit.pause();
92
- edit.seek(2000); // Seek to 2 seconds (in milliseconds)
93
- edit.stop(); // Stop and return to beginning
94
-
95
- // Editing functions
96
- edit.addClip(0, {
97
- asset: {
98
- type: "image",
99
- src: "https://example.com/image.jpg"
100
- },
114
+ edit.seek(2);
115
+ edit.stop();
116
+
117
+ // Mutations
118
+ await edit.addTrack(0, { clips: [] });
119
+ await edit.addClip(0, {
120
+ asset: { type: "image", src: "https://example.com/image.jpg" },
101
121
  start: 0,
102
122
  length: 5
103
123
  });
124
+ await edit.updateClip(0, 0, { length: 6 });
125
+ await edit.deleteClip(0, 0);
104
126
 
105
- edit.addTrack(1, { clips: [] });
106
- edit.deleteClip(0, 0);
107
- edit.deleteTrack(1);
108
-
109
- // Undo/Redo
110
- edit.undo();
111
- edit.redo();
112
- edit.canUndo(); // Check if undo is available (useful for UI)
113
- edit.canRedo(); // Check if redo is available
127
+ // History
128
+ await edit.undo();
129
+ await edit.redo();
114
130
 
115
- // Get edit information
131
+ // Read state
116
132
  const clip = edit.getClip(0, 0);
117
133
  const track = edit.getTrack(0);
118
- const editJson = edit.getEdit();
119
- const duration = edit.totalDuration; // in milliseconds
134
+ const snapshot = edit.getEdit();
135
+ const durationSeconds = edit.totalDuration;
120
136
  ```
121
137
 
122
138
  #### Events
123
139
 
124
- The Edit class provides a typed event system to listen for specific actions:
140
+ Listen using string event names:
125
141
 
126
142
  ```typescript
127
- import { Edit, EditEvent } from "@shotstack/shotstack-studio";
143
+ const unsubscribeClipSelected = edit.events.on("clip:selected", data => {
144
+ console.log("Selected clip", data.trackIndex, data.clipIndex);
145
+ });
128
146
 
129
- // Listen for clip selection events
130
- edit.events.on(EditEvent.ClipSelected, data => {
131
- console.log("Clip selected:", data.clip);
132
- console.log("Track index:", data.trackIndex);
133
- console.log("Clip index:", data.clipIndex);
147
+ edit.events.on("clip:updated", data => {
148
+ console.log("Updated from", data.previous, "to", data.current);
134
149
  });
135
150
 
136
- // Listen for clip update events
137
- edit.events.on(EditEvent.ClipUpdated, data => {
138
- console.log("Previous state:", data.previous);
139
- console.log("Current state:", data.current);
151
+ edit.events.on("playback:play", () => {
152
+ console.log("Playback started");
140
153
  });
141
154
 
142
- // Listen for playback events
143
- edit.events.on(EditEvent.PlaybackPlay, () => console.log("Playing"));
144
- edit.events.on(EditEvent.PlaybackPause, () => console.log("Paused"));
155
+ // Unsubscribe when no longer needed
156
+ unsubscribeClipSelected();
145
157
  ```
146
158
 
147
- Available events:
148
-
149
- **Playback:** `PlaybackPlay`, `PlaybackPause`
150
-
151
- **Clips:** `ClipAdded`, `ClipDeleted`, `ClipSelected`, `ClipUpdated`, `ClipCopied`, `ClipSplit`, `ClipRestored`
152
-
153
- **Selection:** `SelectionCleared`
154
-
155
- **Edit State:** `EditChanged`, `EditUndo`, `EditRedo`
159
+ Available event names:
156
160
 
157
- **Tracks:** `TrackAdded`, `TrackRemoved`
158
-
159
- **Duration:** `DurationChanged`
160
-
161
- **Output:** `OutputResized`, `OutputFpsChanged`, `OutputFormatChanged`
162
-
163
- **Merge Fields:** `MergeFieldRegistered`, `MergeFieldUpdated`, `MergeFieldRemoved`, `MergeFieldChanged`
164
-
165
- **Transcription:** `TranscriptionProgress`, `TranscriptionCompleted`, `TranscriptionFailed`
161
+ - Playback: `playback:play`, `playback:pause`
162
+ - Timeline: `timeline:updated`, `timeline:backgroundChanged`
163
+ - Clip lifecycle: `clip:added`, `clip:split`, `clip:selected`, `clip:updated`, `clip:deleted`, `clip:restored`, `clip:copied`, `clip:loadFailed`, `clip:unresolved`
164
+ - Selection: `selection:cleared`
165
+ - Edit state: `edit:changed`, `edit:undo`, `edit:redo`
166
+ - Track: `track:added`, `track:removed`
167
+ - Duration: `duration:changed`
168
+ - Output: `output:resized`, `output:resolutionChanged`, `output:aspectRatioChanged`, `output:fpsChanged`, `output:formatChanged`, `output:destinationsChanged`
169
+ - Merge fields: `mergefield:changed`
170
+ - Luma masking: `luma:attached`, `luma:detached`
166
171
 
167
172
  ### Canvas
168
173
 
169
- The Canvas class provides the visual rendering of the edit.
174
+ `Canvas` renders the current edit.
170
175
 
171
176
  ```typescript
172
- // Create and load the canvas
177
+ import { Canvas } from "@shotstack/shotstack-studio";
178
+
173
179
  const canvas = new Canvas(edit);
174
180
  await canvas.load();
175
181
 
176
- // Zoom and positioning
177
182
  canvas.centerEdit();
178
183
  canvas.zoomToFit();
179
- canvas.setZoom(1.5); // 1.0 is 100%, 0.5 is 50%, etc.
180
- canvas.dispose(); // Clean up resources when done
184
+ canvas.setZoom(1.25);
185
+ canvas.resize();
186
+ canvas.dispose();
181
187
  ```
182
188
 
183
- ### Controls
189
+ ### UIController
184
190
 
185
- The Controls class adds keyboard controls for playback.
191
+ `UIController` manages built-in UI wiring and extensible button events.
186
192
 
187
193
  ```typescript
188
- const controls = new Controls(edit);
189
- await controls.load();
194
+ import { UIController } from "@shotstack/shotstack-studio";
195
+
196
+ const ui = UIController.create(edit, canvas, { mergeFields: true });
197
+
198
+ ui.registerButton({
199
+ id: "add-title",
200
+ icon: `<svg viewBox="0 0 16 16">...</svg>`,
201
+ tooltip: "Add Title"
202
+ });
203
+
204
+ const unsubscribe = ui.on("button:add-title", ({ position }) => {
205
+ console.log("Button clicked at", position, "seconds");
206
+ });
190
207
 
191
- // Available keyboard controls:
192
- // Space - Play/Pause
193
- // J - Stop
194
- // K - Pause
195
- // L - Play
196
- // Left Arrow - Seek backward
197
- // Right Arrow - Seek forward
198
- // Shift + Arrow - Seek larger amount
199
- // Comma - Step backward one frame
200
- // Period - Step forward one frame
201
- // Cmd/Ctrl + Z - Undo
202
- // Cmd/Ctrl + Shift + Z - Redo
203
- // Cmd/Ctrl + E - Export/download video
208
+ ui.unregisterButton("add-title");
209
+ unsubscribe();
210
+ ui.dispose();
204
211
  ```
205
212
 
206
213
  ### Timeline
207
214
 
208
- The Timeline class provides a visual timeline interface for editing.
215
+ `Timeline` provides visual clip editing.
209
216
 
210
217
  ```typescript
211
218
  import { Timeline } from "@shotstack/shotstack-studio";
212
219
 
213
- const container = document.querySelector("[data-shotstack-timeline]");
214
- const timeline = new Timeline(edit, container, {
215
- features: {
216
- toolbar: true, // Playback controls and editing buttons
217
- ruler: true, // Time ruler with markers
218
- playhead: true, // Draggable playhead
219
- snap: true, // Snap clips to grid and other clips
220
- badges: true // Asset type badges on clips
221
- }
222
- });
220
+ const container = document.querySelector("[data-shotstack-timeline]") as HTMLElement;
221
+ const timeline = new Timeline(edit, container);
222
+
223
223
  await timeline.load();
224
+ timeline.zoomIn();
225
+ timeline.zoomOut();
226
+ timeline.dispose();
224
227
  ```
225
228
 
226
- ### VideoExporter
229
+ ### Controls
227
230
 
228
- The VideoExporter class exports the Edit to a MP4 video file encoded in h264 and AAC.
231
+ `Controls` enables keyboard playback/edit shortcuts.
229
232
 
230
233
  ```typescript
231
- const exporter = new VideoExporter(edit, canvas);
232
- await exporter.export("my-video.mp4", 25); // filename, fps
234
+ import { Controls } from "@shotstack/shotstack-studio";
235
+
236
+ const controls = new Controls(edit);
237
+ await controls.load();
233
238
  ```
234
239
 
235
- ## Merge Fields
240
+ ### VideoExporter
236
241
 
237
- Merge fields allow dynamic content substitution using `{{ FIELD_NAME }}` syntax in your templates.
242
+ `VideoExporter` exports a timeline render from the browser runtime.
238
243
 
239
244
  ```typescript
240
- import { Edit, EditEvent } from "@shotstack/shotstack-studio";
245
+ import { VideoExporter } from "@shotstack/shotstack-studio";
241
246
 
242
- // Set a merge field value
243
- edit.setMergeField("TITLE", "My Video Title");
244
- edit.setMergeField("SUBTITLE", "A great subtitle");
245
-
246
- // Get all registered merge fields
247
- const fields = edit.getMergeFields();
248
-
249
- // Listen for merge field changes
250
- edit.events.on(EditEvent.MergeFieldUpdated, ({ field }) => {
251
- console.log(`Field ${field.name} updated to:`, field.value);
252
- });
247
+ const exporter = new VideoExporter(edit, canvas);
248
+ await exporter.export("my-video.mp4", 25);
253
249
  ```
254
250
 
255
- In templates, use placeholders that will be replaced with merge field values:
251
+ ## Merge Fields
252
+
253
+ Merge fields are template placeholders, typically in the form `{{ FIELD_NAME }}`.
256
254
 
257
255
  ```json
258
256
  {
@@ -263,34 +261,43 @@ In templates, use placeholders that will be replaced with merge field values:
263
261
  }
264
262
  ```
265
263
 
266
- ## Custom Toolbar Buttons
264
+ When merge-field-aware UI is required, enable it via `UIController` options:
265
+
266
+ ```typescript
267
+ const ui = UIController.create(edit, canvas, { mergeFields: true });
268
+ ```
269
+
270
+ You can also subscribe to merge field events when integrations update merge data:
267
271
 
268
- Register custom toolbar buttons to extend the canvas toolbar with your own actions:
272
+ ```typescript
273
+ edit.events.on("mergefield:changed", ({ fields }) => {
274
+ console.log("Merge fields updated:", fields.length);
275
+ });
276
+ ```
277
+
278
+ ## Custom UI Buttons
279
+
280
+ Use `UIController` to register and handle custom button actions.
269
281
 
270
282
  ```typescript
271
- // Register a custom button
272
- edit.registerToolbarButton({
273
- id: "add-text",
283
+ ui.registerButton({
284
+ id: "text",
274
285
  icon: `<svg viewBox="0 0 16 16">...</svg>`,
275
286
  tooltip: "Add Text",
276
- event: "text:requested",
277
- dividerBefore: true // Optional: add a divider before this button
287
+ dividerBefore: true
278
288
  });
279
289
 
280
- // Handle the custom event
281
- edit.events.on("text:requested", ({ position }) => {
282
- // position is the current playhead position in milliseconds
283
- edit.addClip(0, {
284
- asset: { type: "text", text: "New Text" },
285
- start: position / 1000,
286
- length: 5
287
- });
290
+ ui.on("button:text", ({ position, selectedClip }) => {
291
+ console.log("Current time (seconds):", position);
292
+ console.log("Current selection:", selectedClip);
288
293
  });
294
+
295
+ ui.unregisterButton("text");
289
296
  ```
290
297
 
291
298
  ## API Reference
292
299
 
293
- For complete schema and type definitions, see the [Shotstack API Reference](https://shotstack.io/docs/api/#tocs_edit).
300
+ For schema-level details and type definitions, see the [Shotstack API Reference](https://shotstack.io/docs/api/#tocs_edit).
294
301
 
295
302
  ## License
296
303
 
Binary file
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>
Binary file