sanity-plugin-r2-video 0.1.1 → 0.1.2

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.
Files changed (2) hide show
  1. package/README.md +158 -191
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,26 +1,88 @@
1
1
  # sanity-plugin-r2-video
2
2
 
3
- Video for Sanity Studio. Encodes in the browser, stores plain MP4s in Cloudflare
4
- R2.
3
+ [![NPM version][npm-image]][npm-url]
4
+ [![NPM downloads][npm-downloads-image]][npm-downloads-url]
5
5
 
6
- Encoding runs on the editor's machine through [mediabunny](https://mediabunny.dev),
7
- using WebCodecs in a Web Worker. One MP4 comes out per height you ask for. The
8
- first frame is uploaded to Sanity as an ordinary image, so posters get the Sanity
9
- CDN, `srcset`, `auto=format` and LQIP without any extra work. Players pick one
10
- rendition when they load and keep it.
6
+ A video plugin for Sanity Studio that encodes in the browser and stores plain MP4s in Cloudflare R2.
11
7
 
12
- ## Installing
8
+ ## Introduction
9
+
10
+ An editor drops in one file. Out come **renditions** — one MP4 per height you configured, plus the first frame as a poster:
11
+
12
+ ```
13
+ j6w3wy2bd0jq/270.mp4 840 KB
14
+ j6w3wy2bd0jq/360.mp4 1.4 MB
15
+ j6w3wy2bd0jq/480.mp4 2.3 MB
16
+ j6w3wy2bd0jq/720.mp4 4.9 MB
17
+ j6w3wy2bd0jq/1080.mp4 9.7 MB
18
+ ```
19
+
20
+ Your site picks whichever one fits: the 360 behind a thumbnail, the 1080 in a hero. Each document keeps the whole list, with the height, key and size of every file, so choosing one takes a line of code. Upload a 480p clip and you get three files instead of five, as nothing is ever upscaled.
21
+
22
+ Encoding runs on the editor's machine with [mediabunny](https://mediabunny.dev), using WebCodecs in a Web Worker. A Cloudflare Worker you own writes the MP4s to your bucket. The poster is saved as an ordinary Sanity image, so it gets the Sanity CDN, `srcset`, `auto=format` and LQIP for free.
23
+
24
+ That means no encoding service, no per-minute bill, and no R2 credentials outside Cloudflare.
25
+
26
+ **Note**: These are plain MP4s, not adaptive streams. A player picks one rendition when it loads and keeps it.
27
+
28
+ ## Requirements
29
+
30
+ - Sanity Studio v6, React 19, `@sanity/ui` v4 or v5, `@sanity/icons` v5 and `styled-components` v6
31
+ - Node 20 or later
32
+ - A Cloudflare account with R2 enabled
33
+ - Chrome to upload. Encoding needs WebCodecs h264, which Safari doesn't reliably provide, so the Studio checks for it and says so before an editor picks a file. Playback works everywhere.
34
+
35
+ ## Installation
36
+
37
+ Install this package with `npm`.
13
38
 
14
39
  ```bash
15
- pnpm add sanity-plugin-r2-video
40
+ npm i sanity-plugin-r2-video
16
41
  ```
17
42
 
18
- `sanity`, `react`, `react-dom`, `@sanity/ui`, `@sanity/icons` and
19
- `styled-components` are peer dependencies. `mediabunny` is the only real
20
- dependency.
43
+ ## Setup
21
44
 
22
- Add the plugin, pointing it at a Worker you've deployed (see [Setting up a
23
- Worker](#setting-up-a-worker)):
45
+ Five steps, in order. The plugin needs a deployed Worker before it can be configured.
46
+
47
+ ### 1. Create the bucket
48
+
49
+ ```bash
50
+ wrangler r2 bucket create my-bucket
51
+ ```
52
+
53
+ Then open the bucket's **Settings** in the Cloudflare dashboard and enable public access, either with the `r2.dev` development URL or a custom domain. That public origin is your `bucketUrl`, and without it nothing you upload is playable.
54
+
55
+ **Note**: Uploads go through the Worker rather than the browser, so the bucket needs no CORS policy.
56
+
57
+ ### 2. Scaffold the Worker
58
+
59
+ The Worker owns the R2 binding. To generate one:
60
+
61
+ ```bash
62
+ npx sanity-plugin-r2-video setup worker
63
+ ```
64
+
65
+ It asks for a Worker name, your Cloudflare account id, the bucket name, and the Studio origins allowed to call it. Pass `--name`, `--account`, `--bucket` and `--origins` to skip the prompts.
66
+
67
+ This writes `r2-video-worker/`, containing a `wrangler.jsonc` and an entry point that re-exports this package's handler. Upgrading the package upgrades the deployed Worker.
68
+
69
+ ### 3. Deploy the Worker
70
+
71
+ Install its dependencies, generate a shared secret, then deploy:
72
+
73
+ ```bash
74
+ npm i sanity-plugin-r2-video
75
+ npm i -D wrangler @cloudflare/workers-types
76
+ openssl rand -base64 32
77
+ wrangler secret put UPLOAD_TOKEN --config r2-video-worker/wrangler.jsonc
78
+ wrangler deploy --config r2-video-worker/wrangler.jsonc
79
+ ```
80
+
81
+ Keep the generated secret, as the Studio needs the same value in the next step.
82
+
83
+ **Note**: `--origins` defaults to `http://localhost:3333`. When you deploy the Studio, add its production origin to `ALLOWED_ORIGINS` in `wrangler.jsonc` and deploy the Worker again, or requests from it get a `403`.
84
+
85
+ ### 4. Add the plugin
24
86
 
25
87
  ```ts
26
88
  // sanity.config.ts
@@ -29,58 +91,60 @@ import { r2Video } from "sanity-plugin-r2-video/studio";
29
91
  export default defineConfig({
30
92
  plugins: [
31
93
  r2Video({
32
- endpointUrl: "https://….workers.dev", // deployed Worker
33
- token: "…", // matches the Worker's UPLOAD_TOKEN
34
- bucketUrl: "https://….r2.dev", // where renditions are served from
94
+ endpointUrl: "https://….workers.dev", // Worker you deployed
95
+ token: "…", // UPLOAD_TOKEN you generated
96
+ bucketUrl: "https://….r2.dev", // bucket's public origin
35
97
  }),
36
98
  ],
37
99
  });
38
100
  ```
39
101
 
40
- That registers two schema types and a tool. Add video to a document with the
41
- `r2Video` type:
102
+ This registers an `r2Video.asset` document type, an `r2Video` field type, and an **R2 Video** tool.
103
+
104
+ ### 5. Add a field
42
105
 
43
106
  ```ts
44
107
  defineField({ name: "video", title: "Video", type: "r2Video" })
45
108
  ```
46
109
 
47
- ## Using it
48
-
49
- Videos live in the **R2 Video** tool.
110
+ To file uploads made from this field under a fixed media library folder, pass its document id:
50
111
 
51
- **Upload** — drop files onto the library, or press Upload. Files stage in a list
52
- first and nothing encodes until you confirm.
112
+ ```ts
113
+ defineField({
114
+ name: "video",
115
+ type: "r2Video",
116
+ options: { folder: "<folder document id>" },
117
+ })
118
+ ```
53
119
 
54
- **Settings** — collapsed by default. Open it to change the folder, keep audio, or
55
- adjust quality for one batch. **Preview** encodes only the tallest tier, so you
56
- can check size and quality before running the whole ladder.
120
+ ## Usage
57
121
 
58
- **Details** click a card to play the video and see every rendition with its
59
- size. Rename it, move it to another folder, or delete it from here.
122
+ Videos live in the **R2 Video** tool.
60
123
 
61
- **Sync** next to Upload. Compares the bucket against the library and offers to
62
- remove anything no document points at.
124
+ | Action | Notes |
125
+ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
126
+ | **Upload** | Drop files on the library, or press Upload. Files stage in a list first and nothing encodes until you confirm. |
127
+ | **Settings** | Collapsed by default. Change the folder, keep audio, or adjust quality for one batch. **Preview** encodes only the tallest tier, so you can check size and quality before running the whole ladder. |
128
+ | **Details** | Click a card to play the video and see every rendition with its size. Rename it, move it to another folder, or delete it from here. |
129
+ | **Sync** | Lists objects in the bucket that no video document claims, and posters nothing references, then offers to delete them. |
63
130
 
64
- Inside a document, an `r2Video` field lets you pick from the library or upload
65
- without leaving the page.
131
+ Inside a document, an `r2Video` field picks from the library or uploads without leaving the page.
66
132
 
67
- ## What you get back
133
+ ### Playing video on your site
68
134
 
69
- An `r2Video` field stores a reference and nothing else, so the same video can be
70
- used on many documents without encoding it twice:
135
+ A field stores a reference and nothing else, so the same video can be used on many documents without encoding it twice:
71
136
 
72
137
  ```json
73
138
  { "_type": "r2Video", "asset": { "_type": "reference", "_ref": "hK3n…" } }
74
139
  ```
75
140
 
76
- Follow the reference and you get an `r2Video.asset` document. It holds metadata
77
- only. The MP4s are in R2 and the poster is a normal Sanity image asset:
141
+ Follow that reference for an `r2Video.asset` document. It holds metadata only, as the MP4s are in R2 and the poster is a normal Sanity image asset:
78
142
 
79
143
  ```ts
80
144
  type R2VideoAsset = {
81
145
  _id: string;
82
146
  _type: "r2Video.asset";
83
- filename: string; // display name, safe to change
147
+ filename: string; // display name, safe to change
84
148
  folder?: { _type: "reference"; _ref: string };
85
149
  poster: { _type: "image"; asset: { _type: "reference"; _ref: string } };
86
150
  duration: number; // seconds
@@ -89,28 +153,19 @@ type R2VideoAsset = {
89
153
  renditions: {
90
154
  width: number;
91
155
  height: number;
92
- key: string; // R2 object key, e.g. "j6w3wy2bd0jq/720.mp4"
156
+ key: string; // R2 object key, "j6w3wy2bd0jq/720.mp4"
93
157
  size: number; // bytes
94
158
  }[];
95
159
  };
96
160
  ```
97
161
 
98
- Every type here is exported from `sanity-plugin-r2-video/studio`.
99
-
100
- A rendition's `key` is the whole story for playback. Join it to your `bucketUrl`
101
- and you have a source URL:
162
+ A rendition's `key` joined to your `bucketUrl` is a playable URL:
102
163
 
103
164
  ```ts
104
165
  const src = `${bucketUrl}/${rendition.key}`;
105
166
  ```
106
167
 
107
- `renditions` only ever contains heights that were actually encoded. A 480p source
108
- never produces a 1080p entry, so you can pick from this array without checking
109
- whether a tier exists.
110
-
111
- If you'd rather store the video's id than a list of keys, take it off any key and
112
- rebuild the rest with `resolveRenditionPath`. Keys are `<id>/<height>.mp4`, so the
113
- id is the part before the slash:
168
+ To store the video's id instead of a list of keys, take it off any key and rebuild the rest. Keys are `<id>/<height>.mp4`, so the id is the part before the slash:
114
169
 
115
170
  ```ts
116
171
  import { resolveRenditionPath } from "sanity-plugin-r2-video/storage";
@@ -118,86 +173,35 @@ import { resolveRenditionPath } from "sanity-plugin-r2-video/storage";
118
173
  const src = `${bucketUrl}/${resolveRenditionPath(id, 720)}`;
119
174
  ```
120
175
 
121
- ```groq
122
- "id": string::split(renditions[0].key, "/")[0]
123
- ```
124
-
125
- Note that the id is not the document's `_id`. It's generated at upload time for
126
- the R2 key, so a video keeps its objects even if the document is recreated.
127
-
128
- ## Configuring
129
-
130
- Only `endpointUrl`, `token` and `bucketUrl` are required. Everything else falls
131
- back to [`studio/defaults.ts`](studio/defaults.ts), shown here with its defaults.
132
-
133
- ```ts
134
- r2Video({
135
- endpointUrl: "https://….workers.dev",
136
- token: "…",
137
- bucketUrl: "https://….r2.dev",
138
- apiVersion: "2024-01-01",
139
- tool: { name: "r2-video", title: "R2 Video" },
140
- folders: {
141
- type: "media.folder", // document type folders are read from
142
- poster: "_R2 Video Posters", // where generated posters are filed
143
- },
144
- encoding: {
145
- heights: [270, 360, 480, 720, 1080],
146
- videoCodec: "avc",
147
- audioCodec: "aac",
148
- quality: 0.75,
149
- preferBitrate: false,
150
- nativeTopTier: false,
151
- },
152
- });
153
- ```
154
-
155
- ### Encoding
156
-
157
- **`heights`** is the tier ladder. A source shorter than a tier skips it. Every
158
- tier it does reach is another full encode, so the list is what upload time costs.
159
-
160
- **`quality`** sets a quantizer for h264 rather than a bitrate, so quality stays
161
- constant and file size varies. `0.75` is QP 22, where h264 stops being
162
- distinguishable from the source. `1` is QP 16, near-lossless, and routinely
163
- produces files **larger than the source**.
164
-
165
- **`preferBitrate`** flips that trade for predictable size and variable quality. A
166
- 1080p tier lands near 6.1 Mbps at `0.75` whatever the footage.
176
+ **Note**: That id isn't the document's `_id`. It's generated at upload time for the R2 key, so a video keeps its objects even if the document is recreated.
167
177
 
168
- **`nativeTopTier`** copies the top rendition instead of re-encoding it, when its
169
- height and codec already match the source. That's instant and bit-identical, and
170
- its size is whatever the source was exported at.
178
+ `sanity-plugin-r2-video/storage` has no dependencies, so a web app can import it to build URLs without pulling the Studio, `sanity` or `react` into its bundle. Every type above is exported from `sanity-plugin-r2-video/studio`.
171
179
 
172
- ## Setting up a Worker
180
+ ## API
173
181
 
174
- The Worker holds the R2 binding. Scaffold one and the binding name, entry point
175
- and compatibility date come out right:
182
+ ### r2Video()
176
183
 
177
- ```bash
178
- pnpx sanity-plugin-r2-video setup worker
179
- ```
180
-
181
- Pass `--account`, `--bucket`, `--name` and `--origins` to skip the prompts.
182
-
183
- Then install its dependencies, create the bucket, set the shared secret, and
184
- deploy:
185
-
186
- ```bash
187
- pnpm add sanity-plugin-r2-video
188
- pnpm add -D wrangler @cloudflare/workers-types
189
- wrangler r2 bucket create my-bucket
190
- openssl rand -base64 32
191
- wrangler secret put UPLOAD_TOKEN --config r2-video-worker/wrangler.jsonc
192
- wrangler deploy --config r2-video-worker/wrangler.jsonc
193
- ```
184
+ | Property | Type | Default | Notes |
185
+ | ------------------ | --------- | -------------------- | ---------------------------------------------------------------------------------------------- |
186
+ | **endpointUrl** | `string` | undefined | **Required**. Origin of the deployed Worker. The Studio never touches the bucket directly. |
187
+ | **token** | `string` | undefined | **Required**. Shared secret, matching the Worker's `UPLOAD_TOKEN`. |
188
+ | **bucketUrl** | `string` | undefined | **Required**. Public origin renditions are served from. No document stores an origin. |
189
+ | **apiVersion** | `string` | `"2024-01-01"` | Sanity API version the plugin's own queries and mutations run against. |
190
+ | **tool** | `object` | `{ name: "r2-video", title: "R2 Video" }` | Name and title of the Studio tool. |
191
+ | **folders.type** | `string` | `"media.folder"` | Document type folders are read from, defaulting to `sanity-plugin-media`'s. The plugin never imports that package, it reads a type by name, so this can point anywhere. |
192
+ | **folders.poster** | `string` | `"_R2 Video Posters"` | Folder generated posters are filed under, created on first upload. |
193
+ | **encoding** | `object` | See below | Encoding options, applied to every rendition. |
194
194
 
195
- Give `r2Video()` the deployed URL as `endpointUrl`, the same secret as `token`,
196
- and the bucket's public URL as `bucketUrl`.
195
+ ### encoding
197
196
 
198
- The generated entry point re-exports this package's handler, so upgrading the
199
- package upgrades the deployed Worker. Only deployment identity gets written out:
200
- name, account, bucket and origins.
197
+ | Property | Type | Default | Notes |
198
+ | ----------------- | ---------- | ----------------------------- | ------------------------------------------------------------------------------------------- |
199
+ | **heights** | `number[]` | `[270, 360, 480, 720, 1080]` | The tier ladder. A source shorter than a tier skips it, and every tier it does reach is another full encode, so this list is what upload time costs. |
200
+ | **videoCodec** | `string` | `"avc"` | `avc` (h264) is the only codec every browser plays from a plain `<video src>`. |
201
+ | **audioCodec** | `string` | `"aac"` | Used only when an upload opts into keeping audio. |
202
+ | **quality** | `number` | `0.75` | A quantizer for h264 rather than a bitrate, so quality stays constant and file size varies. `0.75` is QP 22, where h264 stops being distinguishable from the source. `1` is QP 16, near-lossless, and routinely produces files **larger than the source**. |
203
+ | **preferBitrate** | `boolean` | `false` | Flips that trade for predictable size and variable quality. A 1080p tier lands near 6.1 Mbps at `0.75` whatever the footage. |
204
+ | **nativeTopTier** | `boolean` | `false` | Copies the top rendition instead of re-encoding it, when its height and codec already match the source. Instant and bit-identical, but its size is whatever the source was exported at. |
201
205
 
202
206
  ## How it works
203
207
 
@@ -207,61 +211,17 @@ Studio ──encoded renditions──▶ Worker ──binding──▶ R2 bucket
207
211
  └──poster──▶ Sanity image assets bucket URL ─┘
208
212
  ```
209
213
 
210
- The Worker holds the only R2 binding, so **no R2 credentials exist outside
211
- Cloudflare**. Uploads go through it as plain request bodies, which means nothing
212
- is signed and the bucket needs no CORS policy. A rendition has to fit in one
213
- request body, capping an upload at 100 MB.
214
+ ### Security
214
215
 
215
- `UPLOAD_TOKEN` ships inside the Studio bundle, because the browser is what sends
216
- the upload. **Anyone who can load the Studio can read it.** What actually
217
- restricts access is the Worker's origin allowlist. Put Cloudflare Access in front
218
- of the Worker if you need real authentication.
216
+ The Worker holds the only R2 binding, so **no R2 credentials exist outside Cloudflare**. Renditions are sent as plain request bodies, which means nothing is signed and the bucket needs no CORS policy.
219
217
 
220
- ### Entry points
221
-
222
- Three, each with its own tsconfig and its own type universe:
223
-
224
- ```
225
- ./studio the Sanity plugin react, react-dom, @sanity/ui
226
- ./worker the endpoint @cloudflare/workers-types
227
- ./storage where files live nothing
228
- ```
229
-
230
- `./storage` has no dependencies. A web app can import it to build source URLs
231
- without pulling the Studio UI, `sanity` or `react` into its bundle.
218
+ `UPLOAD_TOKEN` ships inside the Studio bundle, because the browser is what uploads. **Anyone who can load the Studio can read it.** What actually restricts access is the Worker's origin allowlist. Put Cloudflare Access in front of the Worker if you need real authentication.
232
219
 
233
220
  ### Keys
234
221
 
235
- One directory per video, one object per tier, and no folder segment:
236
-
237
- ```
238
- j6w3wy2bd0jq/270.mp4
239
- j6w3wy2bd0jq/360.mp4
240
- j6w3wy2bd0jq/480.mp4
241
- j6w3wy2bd0jq/720.mp4
242
- j6w3wy2bd0jq/1080.mp4
243
- ```
244
-
245
- Documents store keys and heights rather than URLs, and both the Studio and your
246
- front end build sources from `bucketUrl`. Moving the bucket behind a custom
247
- domain is a config change rather than a migration.
222
+ One directory per video, one object per tier, and no folder segment, as in `<id>/<height>.mp4` above.
248
223
 
249
- ### Folders
250
-
251
- Videos are filed in the same folder documents your image library uses, rather
252
- than a parallel set. Create a folder in the Media tool and it shows up in the
253
- video picker. Rename it there and every video moves with it.
254
-
255
- The type is `folders.type`, which defaults to `sanity-plugin-media`'s
256
- `media.folder`. The plugin never imports that package. It reads a document type
257
- by name, so `folders.type` can point anywhere.
258
-
259
- Keys carry no folder, so renaming or moving a video never touches the bucket. The
260
- tool's filter lists only folders holding video, while the upload dialog offers
261
- all of them.
262
-
263
- Generated posters go to their own folder, set by `folders.poster` and created on
264
- first upload, so they stay out of the folders holding real images.
224
+ Documents store keys and heights rather than URLs, and both the Studio and your front end build sources from `bucketUrl`. Moving the bucket behind a custom domain is a config change rather than a migration. Keys carry no folder either, so renaming or moving a video in the Studio never touches the bucket.
265
225
 
266
226
  ### Deleting
267
227
 
@@ -272,29 +232,32 @@ Order matters here, and `delete-video.ts` documents it:
272
232
  3. Delete the poster asset, now unreferenced, so no `409`.
273
233
  4. Delete the R2 objects, batched.
274
234
 
275
- Sanity goes before R2. An orphaned object is invisible and costs pennies, while a
276
- document pointing at deleted media breaks the site.
235
+ Sanity goes before R2. An orphaned object is invisible and costs pennies, while a document pointing at deleted media breaks the site.
277
236
 
278
237
  ### When an upload fails
279
238
 
280
- The document is written last, so a failure can't leave a video in the library
281
- pointing at files that aren't there.
239
+ The document is written last, so a failure can't leave a video in the library pointing at files that aren't there. Anything created before that point is rolled back, and keys are recorded before each upload rather than after, since a request that times out may still have stored the object. Rollback never throws, so the failure that started it is what you see.
282
240
 
283
- Anything created before that point is rolled back. Keys are recorded before each
284
- upload rather than after, since a request that times out may still have stored
285
- the object. Rollback never throws, so the failure that started it is what you
286
- see.
241
+ A closed tab or a crash skips rollback, and encoded renditions live only in memory, so there's no resume. Whatever either case leaves behind is unreferenced, and **Sync** in the tool finds and removes it.
287
242
 
288
- A closed tab or a crash skips rollback, and encoded renditions live only in
289
- memory, so there's no resume. Whatever either case leaves behind is unreferenced,
290
- and **Sync** in the tool finds and removes it.
243
+ ### Entry points
244
+
245
+ Three, each with its own tsconfig and its own type universe:
246
+
247
+ | Import | Contains | Depends on |
248
+ | ------------------------------------- | ------------------ | --------------------------------- |
249
+ | `sanity-plugin-r2-video/studio` | The Sanity plugin | `react`, `react-dom`, `@sanity/ui` |
250
+ | `sanity-plugin-r2-video/worker` | The endpoint | `@cloudflare/workers-types` |
251
+ | `sanity-plugin-r2-video/storage` | Where files live | Nothing |
291
252
 
292
253
  ## Limitations
293
254
 
294
- Uploading needs WebCodecs h264 encoding, which Safari doesn't reliably provide,
295
- so uploads are effectively Chrome-only today. The gate is a capability check
296
- using mediabunny's `canEncodeVideo('avc')` rather than user-agent sniffing, and
297
- unsupported browsers are told before they pick a file. Playback works everywhere.
255
+ - **Uploading is Chrome-only today.** WebCodecs h264 encoding is the requirement, and the gate is a `canEncodeVideo('avc')` capability check rather than user-agent sniffing.
256
+ - **100 MB per rendition.** Each MP4 is uploaded as a single request body. The source file can be much larger, but any one tier over 100 MB is rejected with a `413`, so lower `quality` or drop the tallest tier.
257
+
258
+ ## Contributing
259
+
260
+ Want to get involved, or found an issue? Please contribute using the GitHub Flow. Create a branch, add commits, and open a Pull Request or submit a new issue.
298
261
 
299
262
  ## Developing
300
263
 
@@ -304,9 +267,13 @@ pnpm run type-check
304
267
  pnpm run build # tsup: ESM + types into dist
305
268
  ```
306
269
 
307
- `./worker` ships as TypeScript. Wrangler compiles it, and the generated Worker
308
- extends `worker/tsconfig.json` for the compiler options it was written against.
270
+ `./worker` ships as TypeScript. Wrangler compiles it, and the generated Worker extends `worker/tsconfig.json` for the compiler options it was written against.
309
271
 
310
272
  ## License
311
273
 
312
274
  MIT
275
+
276
+ [npm-image]: https://img.shields.io/npm/v/sanity-plugin-r2-video.svg?style=flat-square
277
+ [npm-url]: https://npmjs.org/package/sanity-plugin-r2-video
278
+ [npm-downloads-image]: https://img.shields.io/npm/dm/sanity-plugin-r2-video.svg
279
+ [npm-downloads-url]: https://npmcharts.com/compare/sanity-plugin-r2-video?minimal=true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanity-plugin-r2-video",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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",