sanity-plugin-r2-video 0.1.2 → 0.1.3

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 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** one MP4 per height you configured, plus the first frame as a poster:
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 so
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 is generated.
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 the Worker source expects them, so
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 the
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 the Studio ships the same value to browsers
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
  `;
@@ -28,17 +28,17 @@ type R2VideoPoster = {
28
28
  asset: R2VideoReference;
29
29
  };
30
30
  /**
31
- * An `r2Video.asset` document. Metadata only every MP4 lives in R2, and the
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 nothing in storage depends on it. */
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 the same documents the
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 a reference to an `r2Video.asset`. */
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 nothing is ever upscaled.
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 h264 included this maps to a **quantizer**,
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 you are asking for a
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 grainy footage produces far bigger files than
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 so
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
- * a 60 Mbps master would be stored at 60 Mbps. That tier is rarely the one
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 pair it
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 an orphaned object is invisible and costs pennies,
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 a folder made in either place shows up in
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<"filename" | "media" | "folder", any>): {
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<"filename" | "media", any>): {
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 in the Studio the
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;
@@ -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, Button, Flex, Box, TextInput, Spinner, Text, Card, Grid, Dialog, Select, Switch } from '@sanity/ui';
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';
@@ -92,7 +93,7 @@ var deleteVideoAsset = async (client, config, asset) => {
92
93
  try {
93
94
  await client.delete(asset.poster.asset._ref);
94
95
  } catch (error) {
95
- console.info(`Kept poster for '${asset.filename}' \u2014 still in use.`, error);
96
+ console.info(`Kept poster for '${asset.filename}' - still in use.`, error);
96
97
  }
97
98
  await deleteObjects(
98
99
  config,
@@ -198,7 +199,7 @@ var formatDuration = (seconds) => {
198
199
  };
199
200
  var formatBitrate = (bytes, seconds) => {
200
201
  if (!seconds) {
201
- return "\u2014";
202
+ return "-";
202
203
  }
203
204
  return `${(bytes * 8 / seconds / 1e6).toFixed(2)} Mbps`;
204
205
  };
@@ -218,12 +219,42 @@ var OVERLAY_STYLE = {
218
219
  var DragOverlay = () => {
219
220
  return /* @__PURE__ */ jsx(Flex, { style: OVERLAY_STYLE, children: /* @__PURE__ */ jsx(Text, { size: 3, style: { color: "inherit" }, children: "Drop files to upload" }) });
220
221
  };
222
+ var hasVideoFile = (transfer) => {
223
+ return Array.from(transfer.items).some((item) => {
224
+ if (item.kind !== "file") {
225
+ return false;
226
+ }
227
+ return item.type === "" || item.type.startsWith("video/");
228
+ });
229
+ };
230
+ var DropToUpload = ({ isRejected }) => {
231
+ return /* @__PURE__ */ jsx(
232
+ Card,
233
+ {
234
+ radius: 2,
235
+ tone: isRejected ? "critical" : "primary",
236
+ style: {
237
+ position: "absolute",
238
+ inset: -4,
239
+ zIndex: 3,
240
+ opacity: 0.9,
241
+ pointerEvents: "none"
242
+ },
243
+ children: /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 2, justify: "center", style: { height: "100%" }, children: [
244
+ /* @__PURE__ */ jsx(Text, { size: 2, children: isRejected ? /* @__PURE__ */ jsx(AccessDeniedIcon, {}) : /* @__PURE__ */ jsx(UploadIcon, {}) }),
245
+ /* @__PURE__ */ jsx(Text, { size: 2, children: isRejected ? "Can't upload this file here" : "Drop to upload" })
246
+ ] })
247
+ }
248
+ );
249
+ };
221
250
  var useFileDrop = ({ onDrop, isEnabled }) => {
222
251
  const [isDragging, setIsDragging] = useState(false);
252
+ const [isRejected, setIsRejected] = useState(false);
223
253
  const depth = useRef(0);
224
254
  const reset = () => {
225
255
  depth.current = 0;
226
256
  setIsDragging(false);
257
+ setIsRejected(false);
227
258
  };
228
259
  useEffect(() => {
229
260
  if (!isEnabled) {
@@ -238,6 +269,7 @@ var useFileDrop = ({ onDrop, isEnabled }) => {
238
269
  event.preventDefault();
239
270
  depth.current += 1;
240
271
  setIsDragging(true);
272
+ setIsRejected(!hasVideoFile(event.dataTransfer));
241
273
  };
242
274
  const dragLeft = (event) => {
243
275
  if (!isEnabled) {
@@ -269,6 +301,7 @@ var useFileDrop = ({ onDrop, isEnabled }) => {
269
301
  };
270
302
  return {
271
303
  isDragging: isDragging && isEnabled,
304
+ isRejected,
272
305
  dropProps: {
273
306
  onDragEnter: dragEntered,
274
307
  onDragLeave: dragLeft,
@@ -373,7 +406,7 @@ var PreviewEncode = ({ file, keepAudio, encoding }) => {
373
406
  isEncoding && /* @__PURE__ */ jsxs(Flex, { align: "center", gap: 3, children: [
374
407
  /* @__PURE__ */ jsx(Spinner, { muted: true }),
375
408
  /* @__PURE__ */ jsxs(Text, { muted: true, size: 1, children: [
376
- "Encoding top tier \u2014 ",
409
+ "Encoding top tier - ",
377
410
  Math.round(progress * 100),
378
411
  "%"
379
412
  ] })
@@ -474,7 +507,7 @@ var UploadSettings = ({
474
507
  /* @__PURE__ */ jsx(
475
508
  Field,
476
509
  {
477
- description: "Shared with the image library \u2014 create folders there and they appear here.",
510
+ description: "Shared with the image library - create folders there and they appear here.",
478
511
  id: folderInputId,
479
512
  label: "Folder",
480
513
  children: /* @__PURE__ */ jsxs(
@@ -843,7 +876,7 @@ var DialogUpload = ({
843
876
  /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
844
877
  /* @__PURE__ */ jsx(Flex, { justify: "center", children: /* @__PURE__ */ jsx(Text, { muted: true, size: 3, children: /* @__PURE__ */ jsx(UploadIcon, {}) }) }),
845
878
  /* @__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 \u2014 encoded to every size the site needs" })
879
+ /* @__PURE__ */ jsx(Text, { align: "center", muted: true, size: 0, children: "MP4, MOV or WebM - encoded to every size the site needs" })
847
880
  ] }),
848
881
  /* @__PURE__ */ jsx(Flex, { justify: "center", children: /* @__PURE__ */ jsx(
849
882
  Button,
@@ -923,7 +956,24 @@ var readFolder = (options) => {
923
956
  var InputVideo = (props) => {
924
957
  const { onChange, readOnly, renderDefault, schemaType } = props;
925
958
  const [isUploadOpen, setIsUploadOpen] = useState(false);
926
- const closeUpload = () => setIsUploadOpen(false);
959
+ const [droppedFiles, setDroppedFiles] = useState([]);
960
+ const isDisabled = readOnly === true;
961
+ const closeUpload = () => {
962
+ setIsUploadOpen(false);
963
+ setDroppedFiles([]);
964
+ };
965
+ const openUpload = () => {
966
+ setDroppedFiles([]);
967
+ setIsUploadOpen(true);
968
+ };
969
+ const dropped = (files) => {
970
+ setDroppedFiles(files);
971
+ setIsUploadOpen(true);
972
+ };
973
+ const { isDragging, isRejected, dropProps } = useFileDrop({
974
+ onDrop: dropped,
975
+ isEnabled: !isDisabled && !isUploadOpen
976
+ });
927
977
  const uploaded = (asset) => {
928
978
  onChange(
929
979
  set({
@@ -933,22 +983,29 @@ var InputVideo = (props) => {
933
983
  );
934
984
  closeUpload();
935
985
  };
936
- return /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
937
- renderDefault(props),
938
- /* @__PURE__ */ jsx(
939
- Button,
940
- {
941
- disabled: readOnly === true,
942
- icon: UploadIcon,
943
- mode: "ghost",
944
- text: "Upload video",
945
- onClick: () => setIsUploadOpen(true)
946
- }
947
- ),
986
+ return /* @__PURE__ */ jsxs(Stack, { gap: 3, style: { position: "relative" }, ...dropProps, children: [
987
+ isDragging && /* @__PURE__ */ jsx(DropToUpload, { isRejected }),
988
+ /* @__PURE__ */ jsxs(Flex, { gap: 0, children: [
989
+ /* @__PURE__ */ jsx(Box, { flex: 1, children: renderDefault({
990
+ ...props,
991
+ renderField: (field) => field.children
992
+ }) }),
993
+ /* @__PURE__ */ jsx(
994
+ Button,
995
+ {
996
+ disabled: isDisabled,
997
+ icon: UploadIcon,
998
+ mode: "ghost",
999
+ text: "Upload",
1000
+ onClick: openUpload
1001
+ }
1002
+ )
1003
+ ] }),
948
1004
  isUploadOpen && /* @__PURE__ */ jsx(
949
1005
  DialogUpload,
950
1006
  {
951
1007
  folderId: readFolder(schemaType.options),
1008
+ initialFiles: droppedFiles,
952
1009
  onClose: closeUpload,
953
1010
  onUploaded: uploaded
954
1011
  }
@@ -983,7 +1040,7 @@ var VideoPreview = ({ renditions, posterUrl }) => {
983
1040
  style: {
984
1041
  display: "block",
985
1042
  // Sized by height, not width, so the element is exactly the shape
986
- // of the video a width-driven box capped by max-height would
1043
+ // of the video - a width-driven box capped by max-height would
987
1044
  // letterbox instead, painting bars around anything tall
988
1045
  maxHeight: MAX_PLAYER_HEIGHT,
989
1046
  maxWidth: "100%",
@@ -1208,7 +1265,11 @@ var SCHEMA_R2_VIDEO = defineType({
1208
1265
  title: "Asset",
1209
1266
  name: "asset",
1210
1267
  type: "reference",
1211
- to: [{ type: "r2Video.asset" }]
1268
+ to: [{ type: "r2Video.asset" }],
1269
+ // New videos come from the upload button beside this input, which
1270
+ // encodes and stores before writing the document. Sanity's own create
1271
+ // would make an empty one the pipeline never filled in
1272
+ options: { disableNew: true }
1212
1273
  })
1213
1274
  ],
1214
1275
  preview: {
@@ -1282,7 +1343,7 @@ var DialogDelete = ({ asset, onDeleted, onClose }) => {
1282
1343
  children: /* @__PURE__ */ jsx(Card, { padding: 4, children: /* @__PURE__ */ jsxs(Stack, { gap: 4, children: [
1283
1344
  /* @__PURE__ */ jsxs(Text, { size: 1, children: [
1284
1345
  asset.filename,
1285
- " \u2014 ",
1346
+ " - ",
1286
1347
  asset.renditions.length,
1287
1348
  " renditions,",
1288
1349
  " ",
@@ -1499,7 +1560,7 @@ var removeOrphans = async (client, config, orphans) => {
1499
1560
  try {
1500
1561
  await client.delete(posterId);
1501
1562
  } catch (error) {
1502
- console.warn(`Kept poster '${posterId}' \u2014 still referenced.`, error);
1563
+ console.warn(`Kept poster '${posterId}' - still referenced.`, error);
1503
1564
  }
1504
1565
  }
1505
1566
  };
@@ -1569,7 +1630,7 @@ var DialogOrphans = ({ onCleaned, onClose }) => {
1569
1630
  /* @__PURE__ */ jsx(Text, { muted: true, size: 1, children: "Comparing storage against the library\u2026" })
1570
1631
  ] }),
1571
1632
  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 \u2014 every stored file belongs to a video." }),
1633
+ orphans && !isDone && total === 0 && /* @__PURE__ */ jsx(Text, { muted: true, size: 1, children: "Nothing to clean up - every stored file belongs to a video." }),
1573
1634
  orphans && !isDone && total > 0 && /* @__PURE__ */ jsxs(Stack, { gap: 3, children: [
1574
1635
  /* @__PURE__ */ jsxs(Text, { size: 1, children: [
1575
1636
  "Found ",
@@ -1859,7 +1920,7 @@ var ToolVideoLibrary = () => {
1859
1920
  var r2Video = definePlugin((options) => {
1860
1921
  const resolvedConfig = resolveConfig(options);
1861
1922
  return {
1862
- // The plugin's own identity, not the tool's route 'tool.name' below is
1923
+ // The plugin's own identity, not the tool's route - 'tool.name' below is
1863
1924
  // configurable and moves the tool's URL, this never changes
1864
1925
  name: "r2-video",
1865
1926
  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 the top
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanity-plugin-r2-video",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Video for Sanity Studio, encoded in the browser and stored in Cloudflare R2 as a ladder of plain MP4s.",
5
5
  "keywords": [
6
6
  "sanity",
package/worker/index.ts CHANGED
@@ -87,7 +87,7 @@ const storeObject = async (
87
87
  },
88
88
  });
89
89
 
90
- // Only the key and size the Studio builds URLs from its own configured
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
  };