sanity-plugin-r2-video 0.1.2 → 0.1.4
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/README.md +21 -1
- package/bin/cli.js +6 -6
- package/dist/studio/index.d.ts +19 -16
- package/dist/studio/index.js +104 -32
- package/dist/studio/transcode.worker.d.ts +1 -1
- package/package.json +1 -1
- package/worker/index.ts +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ A video plugin for Sanity Studio that encodes in the browser and stores plain MP
|
|
|
7
7
|
|
|
8
8
|
## Introduction
|
|
9
9
|
|
|
10
|
-
An editor drops in one file. Out come **renditions**
|
|
10
|
+
An editor drops in one file. Out come **renditions** - one MP4 per height you configured, plus the first frame as a poster:
|
|
11
11
|
|
|
12
12
|
```
|
|
13
13
|
j6w3wy2bd0jq/270.mp4 840 KB
|
|
@@ -269,6 +269,26 @@ pnpm run build # tsup: ESM + types into dist
|
|
|
269
269
|
|
|
270
270
|
`./worker` ships as TypeScript. Wrangler compiles it, and the generated Worker extends `worker/tsconfig.json` for the compiler options it was written against.
|
|
271
271
|
|
|
272
|
+
### Linking it into a Studio
|
|
273
|
+
|
|
274
|
+
Point a Studio at a checkout with a `link:` override, then allow this directory in Vite. The encoder is loaded as a worker by URL rather than imported, so it never enters the module graph Vite serves by default, and every upload fails on a 403 without this:
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
// sanity.cli.ts
|
|
278
|
+
vite: (config) => ({
|
|
279
|
+
...config,
|
|
280
|
+
server: {
|
|
281
|
+
...config.server,
|
|
282
|
+
fs: {
|
|
283
|
+
...config.server?.fs,
|
|
284
|
+
allow: [...(config.server?.fs?.allow ?? []), "/path/to/sanity-plugin-r2-video"],
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Vite doesn't watch a linked package, so a rebuild needs a hard reload rather than arriving over HMR.
|
|
291
|
+
|
|
272
292
|
## License
|
|
273
293
|
|
|
274
294
|
MIT
|
package/bin/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ const USAGE = `
|
|
|
9
9
|
sanity-plugin-r2-video setup worker [directory]
|
|
10
10
|
|
|
11
11
|
Writes a deployable Cloudflare Worker: A wrangler config filled in from your
|
|
12
|
-
answers, and an entry point that re-exports this package's endpoint
|
|
12
|
+
answers, and an entry point that re-exports this package's endpoint - so
|
|
13
13
|
upgrading the package upgrades the Worker.
|
|
14
14
|
|
|
15
15
|
directory Where to write it. Defaults to ./r2-video-worker
|
|
@@ -40,8 +40,8 @@ const parseFlags = (argv) => {
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* The Worker re-exports the package's handler rather than copying it, so
|
|
43
|
-
* upgrading the package upgrades the deployed endpoint. Only identity
|
|
44
|
-
* name, account, bucket, origins
|
|
43
|
+
* upgrading the package upgrades the deployed endpoint. Only identity -
|
|
44
|
+
* name, account, bucket, origins - is generated.
|
|
45
45
|
*/
|
|
46
46
|
const ENTRY = `// The endpoint itself lives in the plugin, so upgrading the package upgrades
|
|
47
47
|
// this Worker. Only deployment identity belongs here, in \`wrangler.jsonc\`.
|
|
@@ -67,7 +67,7 @@ const ask = async (rl, question, fallback) => {
|
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Only the values that differ per deployment. The binding name, the entry point
|
|
70
|
-
* and the compatibility date are fixed
|
|
70
|
+
* and the compatibility date are fixed - the Worker source expects them, so
|
|
71
71
|
* asking would only create a way to get them wrong.
|
|
72
72
|
*/
|
|
73
73
|
const createConfig = ({ name, accountId, bucket, origins }) => {
|
|
@@ -78,7 +78,7 @@ const createConfig = ({ name, accountId, bucket, origins }) => {
|
|
|
78
78
|
"main": "src/index.ts",
|
|
79
79
|
"compatibility_date": "2026-08-01",
|
|
80
80
|
|
|
81
|
-
// The binding is what keeps R2 credentials out of your repo entirely
|
|
81
|
+
// The binding is what keeps R2 credentials out of your repo entirely - the
|
|
82
82
|
// Worker reaches the bucket directly, so nothing has to be signed or stored
|
|
83
83
|
"r2_buckets": [
|
|
84
84
|
{
|
|
@@ -93,7 +93,7 @@ const createConfig = ({ name, accountId, bucket, origins }) => {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
// UPLOAD_TOKEN is set separately, with: wrangler secret put UPLOAD_TOKEN
|
|
96
|
-
// Not because it's secret
|
|
96
|
+
// Not because it's secret - the Studio ships the same value to browsers -
|
|
97
97
|
// but so it stays out of this file, out of git, and out of deploy logs
|
|
98
98
|
}
|
|
99
99
|
`;
|
package/dist/studio/index.d.ts
CHANGED
|
@@ -28,17 +28,17 @@ type R2VideoPoster = {
|
|
|
28
28
|
asset: R2VideoReference;
|
|
29
29
|
};
|
|
30
30
|
/**
|
|
31
|
-
* An `r2Video.asset` document. Metadata only
|
|
31
|
+
* An `r2Video.asset` document. Metadata only - every MP4 lives in R2, and the
|
|
32
32
|
* poster lives in Sanity's own image pipeline so it inherits the CDN, the
|
|
33
33
|
* `srcset` helpers and the native preview branch.
|
|
34
34
|
*/
|
|
35
35
|
type R2VideoAsset = {
|
|
36
36
|
_id: string;
|
|
37
37
|
_type: "r2Video.asset";
|
|
38
|
-
/** Display name. Editable
|
|
38
|
+
/** Display name. Editable - nothing in storage depends on it. */
|
|
39
39
|
filename: string;
|
|
40
40
|
/**
|
|
41
|
-
* The media library folder this video belongs to
|
|
41
|
+
* The media library folder this video belongs to - the same documents the
|
|
42
42
|
* image browser uses, so folders are shared rather than mirrored. Object
|
|
43
43
|
* keys keep the prefix they were uploaded under, so renaming a folder moves
|
|
44
44
|
* the video in the Studio without invalidating anything already in R2.
|
|
@@ -50,7 +50,7 @@ type R2VideoAsset = {
|
|
|
50
50
|
renditions: R2VideoRendition[];
|
|
51
51
|
uploadedAt: string;
|
|
52
52
|
};
|
|
53
|
-
/** The value an `r2Video` field holds
|
|
53
|
+
/** The value an `r2Video` field holds - a reference to an `r2Video.asset`. */
|
|
54
54
|
type R2VideoValue = {
|
|
55
55
|
_type?: "r2Video";
|
|
56
56
|
asset?: R2VideoReference;
|
|
@@ -68,7 +68,7 @@ type R2VideoFieldOptions = {
|
|
|
68
68
|
type R2VideoEncodingConfig = {
|
|
69
69
|
/**
|
|
70
70
|
* Rendition heights to produce, in any order. A source shorter than a tier
|
|
71
|
-
* skips it
|
|
71
|
+
* skips it - nothing is ever upscaled.
|
|
72
72
|
*/
|
|
73
73
|
heights?: number[];
|
|
74
74
|
/**
|
|
@@ -81,7 +81,7 @@ type R2VideoEncodingConfig = {
|
|
|
81
81
|
/**
|
|
82
82
|
* Compression level passed to both encoders, from 0 (worst) to 1 (best).
|
|
83
83
|
*
|
|
84
|
-
* For codecs that support it
|
|
84
|
+
* For codecs that support it - h264 included - this maps to a **quantizer**,
|
|
85
85
|
* not a bitrate. That means constant quality and *variable file size*: the
|
|
86
86
|
* output is as large as the footage needs to hit that quality, so detailed
|
|
87
87
|
* or grainy material produces much bigger files than flat material.
|
|
@@ -93,7 +93,7 @@ type R2VideoEncodingConfig = {
|
|
|
93
93
|
*
|
|
94
94
|
* QP 22 is h264's practical transparency threshold, which is why 0.75 is the
|
|
95
95
|
* default. Going to 1 can produce a file **larger than the source** when the
|
|
96
|
-
* source was exported at a normal quantizer
|
|
96
|
+
* source was exported at a normal quantizer - you are asking for a
|
|
97
97
|
* higher-fidelity encode than the original, and encoding can't add back
|
|
98
98
|
* detail that was never captured.
|
|
99
99
|
*/
|
|
@@ -102,12 +102,12 @@ type R2VideoEncodingConfig = {
|
|
|
102
102
|
* Encode to a target **bitrate** instead of a quantizer.
|
|
103
103
|
*
|
|
104
104
|
* Flips the trade-off `quality` makes. The quantizer default is constant
|
|
105
|
-
* quality with variable size
|
|
105
|
+
* quality with variable size - grainy footage produces far bigger files than
|
|
106
106
|
* flat footage. With this on, size becomes predictable and quality varies
|
|
107
107
|
* instead: a tier lands at roughly the same weight whatever you feed it.
|
|
108
108
|
*
|
|
109
109
|
* The target is derived from frame size and `quality`, using 3 Mbps at
|
|
110
|
-
* 1920×1080 as the reference and a multiplier from the quality curve
|
|
110
|
+
* 1920×1080 as the reference and a multiplier from the quality curve - so
|
|
111
111
|
* `0.75` is about 6.1 Mbps at 1080p, and `0.5` about 3.2 Mbps.
|
|
112
112
|
*
|
|
113
113
|
* Worth turning on when knowing what lands in the bucket matters more than
|
|
@@ -120,7 +120,7 @@ type R2VideoEncodingConfig = {
|
|
|
120
120
|
* than transcodes: instant, and bit-identical to the upload.
|
|
121
121
|
*
|
|
122
122
|
* Off by default because it hands size control to whoever exported the file
|
|
123
|
-
*
|
|
123
|
+
* - a 60 Mbps master would be stored at 60 Mbps. That tier is rarely the one
|
|
124
124
|
* served, since playback picks by element size, but the bytes are real.
|
|
125
125
|
*/
|
|
126
126
|
nativeTopTier?: boolean;
|
|
@@ -139,7 +139,7 @@ type R2VideoPluginConfig = {
|
|
|
139
139
|
endpointUrl: string;
|
|
140
140
|
/**
|
|
141
141
|
* Shared secret the Worker checks. This ships inside the Studio bundle, so
|
|
142
|
-
* it gates casual access rather than providing real authentication
|
|
142
|
+
* it gates casual access rather than providing real authentication - pair it
|
|
143
143
|
* with the Worker's origin allowlist.
|
|
144
144
|
*/
|
|
145
145
|
token: string;
|
|
@@ -191,14 +191,14 @@ declare const findReferencingDocuments: (client: SanityClient, id: string) => Pr
|
|
|
191
191
|
*
|
|
192
192
|
* The document goes before the poster, because it holds the strong reference
|
|
193
193
|
* that would otherwise 409. And Sanity goes before R2, because the two failure
|
|
194
|
-
* modes are not symmetric
|
|
194
|
+
* modes are not symmetric - an orphaned object is invisible and costs pennies,
|
|
195
195
|
* whereas a document pointing at deleted media breaks the site.
|
|
196
196
|
*/
|
|
197
197
|
declare const deleteVideoAsset: (client: SanityClient, config: R2VideoPluginConfig, asset: R2VideoAsset) => Promise<void>;
|
|
198
198
|
|
|
199
199
|
/**
|
|
200
200
|
* A folder from the media library. These are the *same* documents the image
|
|
201
|
-
* browser uses, not a parallel set
|
|
201
|
+
* browser uses, not a parallel set - a folder made in either place shows up in
|
|
202
202
|
* both, so there is one place folders are defined.
|
|
203
203
|
*/
|
|
204
204
|
type MediaFolder = {
|
|
@@ -310,7 +310,7 @@ declare const createVideoAssetSchema: (config: ResolvedR2VideoConfig) => {
|
|
|
310
310
|
folder: string;
|
|
311
311
|
media: string;
|
|
312
312
|
};
|
|
313
|
-
prepare({ filename, folder, media }: Record<"
|
|
313
|
+
prepare({ filename, folder, media }: Record<"media" | "filename" | "folder", any>): {
|
|
314
314
|
title: any;
|
|
315
315
|
subtitle: any;
|
|
316
316
|
media: any;
|
|
@@ -338,13 +338,16 @@ declare const SCHEMA_R2_VIDEO: {
|
|
|
338
338
|
to: {
|
|
339
339
|
type: string;
|
|
340
340
|
}[];
|
|
341
|
+
options: {
|
|
342
|
+
disableNew: true;
|
|
343
|
+
};
|
|
341
344
|
}[];
|
|
342
345
|
preview: {
|
|
343
346
|
select: {
|
|
344
347
|
filename: string;
|
|
345
348
|
media: string;
|
|
346
349
|
};
|
|
347
|
-
prepare({ filename, media }: Record<"
|
|
350
|
+
prepare({ filename, media }: Record<"media" | "filename", any>): {
|
|
348
351
|
title: any;
|
|
349
352
|
media: any;
|
|
350
353
|
};
|
|
@@ -392,7 +395,7 @@ type Props = {
|
|
|
392
395
|
};
|
|
393
396
|
/**
|
|
394
397
|
* Plays the video rather than showing its first frame. Muted and looping to
|
|
395
|
-
* match how these are used on the site, but with controls
|
|
398
|
+
* match how these are used on the site, but with controls - in the Studio the
|
|
396
399
|
* point is to check the footage, so scrubbing has to be possible.
|
|
397
400
|
*/
|
|
398
401
|
declare const VideoPreview: ({ renditions, posterUrl }: Props) => react.JSX.Element;
|
package/dist/studio/index.js
CHANGED
|
@@ -5,7 +5,8 @@ import { defineType, defineField, definePlugin, useClient, set } from 'sanity';
|
|
|
5
5
|
import { createContext, useState, useCallback, useEffect, useRef, useMemo, useContext, useId } from 'react';
|
|
6
6
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
7
7
|
import { UploadIcon } from '@sanity/icons/Upload';
|
|
8
|
-
import { Stack,
|
|
8
|
+
import { Stack, Flex, Box, Button, TextInput, Spinner, Text, Card, Grid, Dialog, Select, Switch } from '@sanity/ui';
|
|
9
|
+
import { AccessDeniedIcon } from '@sanity/icons/AccessDenied';
|
|
9
10
|
import { canEncodeVideo } from 'mediabunny';
|
|
10
11
|
import { WarningOutlineIcon } from '@sanity/icons/WarningOutline';
|
|
11
12
|
import { ChevronDownIcon } from '@sanity/icons/ChevronDown';
|
|
@@ -89,14 +90,21 @@ var findReferencingDocuments = (client, id) => {
|
|
|
89
90
|
};
|
|
90
91
|
var deleteVideoAsset = async (client, config, asset) => {
|
|
91
92
|
await client.delete(asset._id);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
const posterId = asset.poster?.asset?._ref;
|
|
94
|
+
const renditions = asset.renditions ?? [];
|
|
95
|
+
if (posterId) {
|
|
96
|
+
try {
|
|
97
|
+
await client.delete(posterId);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.info(
|
|
100
|
+
`Kept poster for '${asset.filename}' - still in use.`,
|
|
101
|
+
error
|
|
102
|
+
);
|
|
103
|
+
}
|
|
96
104
|
}
|
|
97
105
|
await deleteObjects(
|
|
98
106
|
config,
|
|
99
|
-
|
|
107
|
+
renditions.map((rendition) => rendition.key)
|
|
100
108
|
);
|
|
101
109
|
};
|
|
102
110
|
|
|
@@ -198,7 +206,7 @@ var formatDuration = (seconds) => {
|
|
|
198
206
|
};
|
|
199
207
|
var formatBitrate = (bytes, seconds) => {
|
|
200
208
|
if (!seconds) {
|
|
201
|
-
return "
|
|
209
|
+
return "-";
|
|
202
210
|
}
|
|
203
211
|
return `${(bytes * 8 / seconds / 1e6).toFixed(2)} Mbps`;
|
|
204
212
|
};
|
|
@@ -218,12 +226,42 @@ var OVERLAY_STYLE = {
|
|
|
218
226
|
var DragOverlay = () => {
|
|
219
227
|
return /* @__PURE__ */ jsx(Flex, { style: OVERLAY_STYLE, children: /* @__PURE__ */ jsx(Text, { size: 3, style: { color: "inherit" }, children: "Drop files to upload" }) });
|
|
220
228
|
};
|
|
229
|
+
var hasVideoFile = (transfer) => {
|
|
230
|
+
return Array.from(transfer.items).some((item) => {
|
|
231
|
+
if (item.kind !== "file") {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
return item.type === "" || item.type.startsWith("video/");
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
var DropToUpload = ({ isRejected }) => {
|
|
238
|
+
return /* @__PURE__ */ jsx(
|
|
239
|
+
Card,
|
|
240
|
+
{
|
|
241
|
+
radius: 2,
|
|
242
|
+
tone: isRejected ? "critical" : "primary",
|
|
243
|
+
style: {
|
|
244
|
+
position: "absolute",
|
|
245
|
+
inset: -4,
|
|
246
|
+
zIndex: 3,
|
|
247
|
+
opacity: 0.9,
|
|
248
|
+
pointerEvents: "none"
|
|
249
|
+
},
|
|
250
|
+
children: /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, justify: "center", style: { height: "100%" }, children: [
|
|
251
|
+
/* @__PURE__ */ jsx(Text, { size: 2, children: isRejected ? /* @__PURE__ */ jsx(AccessDeniedIcon, {}) : /* @__PURE__ */ jsx(UploadIcon, {}) }),
|
|
252
|
+
/* @__PURE__ */ jsx(Text, { size: 2, children: isRejected ? "Can't upload this file here" : "Drop to upload" })
|
|
253
|
+
] })
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
};
|
|
221
257
|
var useFileDrop = ({ onDrop, isEnabled }) => {
|
|
222
258
|
const [isDragging, setIsDragging] = useState(false);
|
|
259
|
+
const [isRejected, setIsRejected] = useState(false);
|
|
223
260
|
const depth = useRef(0);
|
|
224
261
|
const reset = () => {
|
|
225
262
|
depth.current = 0;
|
|
226
263
|
setIsDragging(false);
|
|
264
|
+
setIsRejected(false);
|
|
227
265
|
};
|
|
228
266
|
useEffect(() => {
|
|
229
267
|
if (!isEnabled) {
|
|
@@ -238,6 +276,7 @@ var useFileDrop = ({ onDrop, isEnabled }) => {
|
|
|
238
276
|
event.preventDefault();
|
|
239
277
|
depth.current += 1;
|
|
240
278
|
setIsDragging(true);
|
|
279
|
+
setIsRejected(!hasVideoFile(event.dataTransfer));
|
|
241
280
|
};
|
|
242
281
|
const dragLeft = (event) => {
|
|
243
282
|
if (!isEnabled) {
|
|
@@ -269,6 +308,7 @@ var useFileDrop = ({ onDrop, isEnabled }) => {
|
|
|
269
308
|
};
|
|
270
309
|
return {
|
|
271
310
|
isDragging: isDragging && isEnabled,
|
|
311
|
+
isRejected,
|
|
272
312
|
dropProps: {
|
|
273
313
|
onDragEnter: dragEntered,
|
|
274
314
|
onDragLeave: dragLeft,
|
|
@@ -373,7 +413,7 @@ var PreviewEncode = ({ file, keepAudio, encoding }) => {
|
|
|
373
413
|
isEncoding && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, children: [
|
|
374
414
|
/* @__PURE__ */ jsx(Spinner, { muted: true }),
|
|
375
415
|
/* @__PURE__ */ jsxs(Text, { muted: true, size: 1, children: [
|
|
376
|
-
"Encoding top tier
|
|
416
|
+
"Encoding top tier - ",
|
|
377
417
|
Math.round(progress * 100),
|
|
378
418
|
"%"
|
|
379
419
|
] })
|
|
@@ -474,7 +514,7 @@ var UploadSettings = ({
|
|
|
474
514
|
/* @__PURE__ */ jsx(
|
|
475
515
|
Field,
|
|
476
516
|
{
|
|
477
|
-
description: "Shared with the image library
|
|
517
|
+
description: "Shared with the image library - create folders there and they appear here.",
|
|
478
518
|
id: folderInputId,
|
|
479
519
|
label: "Folder",
|
|
480
520
|
children: /* @__PURE__ */ jsxs(
|
|
@@ -843,7 +883,7 @@ var DialogUpload = ({
|
|
|
843
883
|
/* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
|
|
844
884
|
/* @__PURE__ */ jsx(Flex, { justify: "center", children: /* @__PURE__ */ jsx(Text, { muted: true, size: 3, children: /* @__PURE__ */ jsx(UploadIcon, {}) }) }),
|
|
845
885
|
/* @__PURE__ */ jsx(Text, { align: "center", muted: true, size: 1, children: "Drop videos here" }),
|
|
846
|
-
/* @__PURE__ */ jsx(Text, { align: "center", muted: true, size: 0, children: "MP4, MOV or WebM
|
|
886
|
+
/* @__PURE__ */ jsx(Text, { align: "center", muted: true, size: 0, children: "MP4, MOV or WebM - encoded to every size the site needs" })
|
|
847
887
|
] }),
|
|
848
888
|
/* @__PURE__ */ jsx(Flex, { justify: "center", children: /* @__PURE__ */ jsx(
|
|
849
889
|
Button,
|
|
@@ -923,7 +963,24 @@ var readFolder = (options) => {
|
|
|
923
963
|
var InputVideo = (props) => {
|
|
924
964
|
const { onChange, readOnly, renderDefault, schemaType } = props;
|
|
925
965
|
const [isUploadOpen, setIsUploadOpen] = useState(false);
|
|
926
|
-
const
|
|
966
|
+
const [droppedFiles, setDroppedFiles] = useState([]);
|
|
967
|
+
const isDisabled = readOnly === true;
|
|
968
|
+
const closeUpload = () => {
|
|
969
|
+
setIsUploadOpen(false);
|
|
970
|
+
setDroppedFiles([]);
|
|
971
|
+
};
|
|
972
|
+
const openUpload = () => {
|
|
973
|
+
setDroppedFiles([]);
|
|
974
|
+
setIsUploadOpen(true);
|
|
975
|
+
};
|
|
976
|
+
const dropped = (files) => {
|
|
977
|
+
setDroppedFiles(files);
|
|
978
|
+
setIsUploadOpen(true);
|
|
979
|
+
};
|
|
980
|
+
const { isDragging, isRejected, dropProps } = useFileDrop({
|
|
981
|
+
onDrop: dropped,
|
|
982
|
+
isEnabled: !isDisabled && !isUploadOpen
|
|
983
|
+
});
|
|
927
984
|
const uploaded = (asset) => {
|
|
928
985
|
onChange(
|
|
929
986
|
set({
|
|
@@ -933,22 +990,29 @@ var InputVideo = (props) => {
|
|
|
933
990
|
);
|
|
934
991
|
closeUpload();
|
|
935
992
|
};
|
|
936
|
-
return /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
|
|
937
|
-
|
|
938
|
-
/* @__PURE__ */
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
993
|
+
return /* @__PURE__ */ jsxs(Stack, { gap: 3, style: { position: "relative" }, ...dropProps, children: [
|
|
994
|
+
isDragging && /* @__PURE__ */ jsx(DropToUpload, { isRejected }),
|
|
995
|
+
/* @__PURE__ */ jsxs(Flex, { gap: 0, children: [
|
|
996
|
+
/* @__PURE__ */ jsx(Box, { flex: 1, children: renderDefault({
|
|
997
|
+
...props,
|
|
998
|
+
renderField: (field) => field.children
|
|
999
|
+
}) }),
|
|
1000
|
+
/* @__PURE__ */ jsx(
|
|
1001
|
+
Button,
|
|
1002
|
+
{
|
|
1003
|
+
disabled: isDisabled,
|
|
1004
|
+
icon: UploadIcon,
|
|
1005
|
+
mode: "ghost",
|
|
1006
|
+
text: "Upload",
|
|
1007
|
+
onClick: openUpload
|
|
1008
|
+
}
|
|
1009
|
+
)
|
|
1010
|
+
] }),
|
|
948
1011
|
isUploadOpen && /* @__PURE__ */ jsx(
|
|
949
1012
|
DialogUpload,
|
|
950
1013
|
{
|
|
951
1014
|
folderId: readFolder(schemaType.options),
|
|
1015
|
+
initialFiles: droppedFiles,
|
|
952
1016
|
onClose: closeUpload,
|
|
953
1017
|
onUploaded: uploaded
|
|
954
1018
|
}
|
|
@@ -983,7 +1047,7 @@ var VideoPreview = ({ renditions, posterUrl }) => {
|
|
|
983
1047
|
style: {
|
|
984
1048
|
display: "block",
|
|
985
1049
|
// Sized by height, not width, so the element is exactly the shape
|
|
986
|
-
// of the video
|
|
1050
|
+
// of the video - a width-driven box capped by max-height would
|
|
987
1051
|
// letterbox instead, painting bars around anything tall
|
|
988
1052
|
maxHeight: MAX_PLAYER_HEIGHT,
|
|
989
1053
|
maxWidth: "100%",
|
|
@@ -1208,7 +1272,11 @@ var SCHEMA_R2_VIDEO = defineType({
|
|
|
1208
1272
|
title: "Asset",
|
|
1209
1273
|
name: "asset",
|
|
1210
1274
|
type: "reference",
|
|
1211
|
-
to: [{ type: "r2Video.asset" }]
|
|
1275
|
+
to: [{ type: "r2Video.asset" }],
|
|
1276
|
+
// New videos come from the upload button beside this input, which
|
|
1277
|
+
// encodes and stores before writing the document. Sanity's own create
|
|
1278
|
+
// would make an empty one the pipeline never filled in
|
|
1279
|
+
options: { disableNew: true }
|
|
1212
1280
|
})
|
|
1213
1281
|
],
|
|
1214
1282
|
preview: {
|
|
@@ -1282,7 +1350,7 @@ var DialogDelete = ({ asset, onDeleted, onClose }) => {
|
|
|
1282
1350
|
children: /* @__PURE__ */ jsx(Card, { padding: 4, children: /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
|
|
1283
1351
|
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
1284
1352
|
asset.filename,
|
|
1285
|
-
"
|
|
1353
|
+
" - ",
|
|
1286
1354
|
asset.renditions.length,
|
|
1287
1355
|
" renditions,",
|
|
1288
1356
|
" ",
|
|
@@ -1499,7 +1567,7 @@ var removeOrphans = async (client, config, orphans) => {
|
|
|
1499
1567
|
try {
|
|
1500
1568
|
await client.delete(posterId);
|
|
1501
1569
|
} catch (error) {
|
|
1502
|
-
console.warn(`Kept poster '${posterId}'
|
|
1570
|
+
console.warn(`Kept poster '${posterId}' - still referenced.`, error);
|
|
1503
1571
|
}
|
|
1504
1572
|
}
|
|
1505
1573
|
};
|
|
@@ -1569,7 +1637,7 @@ var DialogOrphans = ({ onCleaned, onClose }) => {
|
|
|
1569
1637
|
/* @__PURE__ */ jsx(Text, { muted: true, size: 1, children: "Comparing storage against the library\u2026" })
|
|
1570
1638
|
] }),
|
|
1571
1639
|
isDone && /* @__PURE__ */ jsx(Card, { padding: 3, radius: 2, tone: "positive", children: /* @__PURE__ */ jsx(Text, { size: 1, children: "Removed." }) }),
|
|
1572
|
-
orphans && !isDone && total === 0 && /* @__PURE__ */ jsx(Text, { muted: true, size: 1, children: "Nothing to clean up
|
|
1640
|
+
orphans && !isDone && total === 0 && /* @__PURE__ */ jsx(Text, { muted: true, size: 1, children: "Nothing to clean up - every stored file belongs to a video." }),
|
|
1573
1641
|
orphans && !isDone && total > 0 && /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
|
|
1574
1642
|
/* @__PURE__ */ jsxs(Text, { size: 1, children: [
|
|
1575
1643
|
"Found ",
|
|
@@ -1661,7 +1729,10 @@ var QUERY_ASSETS = `*[_type == "r2Video.asset"] | order(uploadedAt desc){
|
|
|
1661
1729
|
poster,
|
|
1662
1730
|
duration,
|
|
1663
1731
|
hasAudio,
|
|
1664
|
-
|
|
1732
|
+
|
|
1733
|
+
// A document the pipeline never finished - Sanity's own create button made
|
|
1734
|
+
// these before the reference field disabled it - has no renditions at all
|
|
1735
|
+
"renditions": coalesce(renditions, []),
|
|
1665
1736
|
uploadedAt,
|
|
1666
1737
|
"posterUrl": poster.asset->url,
|
|
1667
1738
|
"folderName": folder->name
|
|
@@ -1670,10 +1741,11 @@ var slugify = (name) => {
|
|
|
1670
1741
|
return name.toLowerCase().replace(/['\u2019]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1671
1742
|
};
|
|
1672
1743
|
var toTitle = (asset, isFiltered) => {
|
|
1744
|
+
const filename = asset.filename || "Untitled video";
|
|
1673
1745
|
if (isFiltered || !asset.folderName) {
|
|
1674
|
-
return
|
|
1746
|
+
return filename;
|
|
1675
1747
|
}
|
|
1676
|
-
return `${slugify(asset.folderName)}/${
|
|
1748
|
+
return `${slugify(asset.folderName)}/${filename}`;
|
|
1677
1749
|
};
|
|
1678
1750
|
var matches = (asset, search) => {
|
|
1679
1751
|
if (!search) {
|
|
@@ -1859,7 +1931,7 @@ var ToolVideoLibrary = () => {
|
|
|
1859
1931
|
var r2Video = definePlugin((options) => {
|
|
1860
1932
|
const resolvedConfig = resolveConfig(options);
|
|
1861
1933
|
return {
|
|
1862
|
-
// The plugin's own identity, not the tool's route
|
|
1934
|
+
// The plugin's own identity, not the tool's route - 'tool.name' below is
|
|
1863
1935
|
// configurable and moves the tool's URL, this never changes
|
|
1864
1936
|
name: "r2-video",
|
|
1865
1937
|
schema: {
|
|
@@ -16,7 +16,7 @@ type TranscodeRequest = {
|
|
|
16
16
|
options: TranscodeOptions;
|
|
17
17
|
/**
|
|
18
18
|
* Encode only the tallest tier and stop. Used to show what the current
|
|
19
|
-
* settings actually produce before committing to the whole ladder
|
|
19
|
+
* settings actually produce before committing to the whole ladder - the top
|
|
20
20
|
* tier is the one that varies most, and the one most likely to surprise.
|
|
21
21
|
*/
|
|
22
22
|
topTierOnly?: boolean;
|
package/package.json
CHANGED
package/worker/index.ts
CHANGED
|
@@ -87,7 +87,7 @@ const storeObject = async (
|
|
|
87
87
|
},
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
-
// Only the key and size
|
|
90
|
+
// Only the key and size - the Studio builds URLs from its own configured
|
|
91
91
|
// origin, so this Worker never needs to know one
|
|
92
92
|
return jsonResponse({ key, size: body.byteLength }, 200, headers);
|
|
93
93
|
};
|