sanity-plugin-mux-input 2.11.1 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -5
- package/dist/index.d.mts +36 -2
- package/dist/index.d.ts +36 -2
- package/dist/index.js +186 -50
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +186 -50
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -8
- package/src/_exports/index.ts +26 -2
- package/src/actions/upload.ts +4 -3
- package/src/components/Player.tsx +14 -5
- package/src/components/UploadConfiguration.tsx +190 -30
- package/src/components/Uploader.tsx +1 -1
- package/src/components/documentPreview/PaneItemPreview.tsx +8 -8
- package/src/hooks/useMuxPolling.ts +20 -4
- package/src/sanity-ui.d.ts +5 -0
- package/src/schema.ts +9 -3
- package/src/util/types.ts +47 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sanity-plugin-mux-input",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"description": "An input component that integrates Sanity Studio with Mux video encoding/hosting service.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -70,11 +70,11 @@
|
|
|
70
70
|
"use-error-boundary": "^2.0.6"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
|
-
"@sanity/client": "^
|
|
74
|
-
"@sanity/pkg-utils": "^
|
|
75
|
-
"@sanity/plugin-kit": "4.0.
|
|
73
|
+
"@sanity/client": "^7.12.1",
|
|
74
|
+
"@sanity/pkg-utils": "^8.1.29",
|
|
75
|
+
"@sanity/plugin-kit": "^4.0.20",
|
|
76
76
|
"@sanity/semantic-release-preset": "^5.0.0",
|
|
77
|
-
"@sanity/vision": "^3.
|
|
77
|
+
"@sanity/vision": "^3.99.0",
|
|
78
78
|
"@types/lodash": "^4.17.15",
|
|
79
79
|
"@types/react": "^19.0.10",
|
|
80
80
|
"@types/react-is": "^19.0.0",
|
|
@@ -90,16 +90,16 @@
|
|
|
90
90
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
91
91
|
"husky": "^9.0.11",
|
|
92
92
|
"lint-staged": "^15.2.2",
|
|
93
|
-
"npm-run-all2": "^
|
|
93
|
+
"npm-run-all2": "^6.2.6",
|
|
94
94
|
"prettier": "^3.5.2",
|
|
95
95
|
"prettier-plugin-packagejson": "^2.5.9",
|
|
96
96
|
"react": "^19.0.0",
|
|
97
97
|
"react-dom": "^19.0.0",
|
|
98
98
|
"react-is": "^19.0.0",
|
|
99
|
-
"sanity": "^3.
|
|
99
|
+
"sanity": "^3.99.0",
|
|
100
100
|
"semantic-release": "^24.2.3",
|
|
101
101
|
"styled-components": "^6.1.15",
|
|
102
|
-
"typescript": "5.
|
|
102
|
+
"typescript": "5.9.3"
|
|
103
103
|
},
|
|
104
104
|
"peerDependencies": {
|
|
105
105
|
"react": "^18.3 || ^19",
|
package/src/_exports/index.ts
CHANGED
|
@@ -3,10 +3,11 @@ import {definePlugin} from 'sanity'
|
|
|
3
3
|
import createStudioTool, {DEFAULT_TOOL_CONFIG} from '../components/StudioTool'
|
|
4
4
|
import {muxVideoCustomRendering} from '../plugin'
|
|
5
5
|
import {muxVideoSchema, schemaTypes} from '../schema'
|
|
6
|
-
import type {PluginConfig} from '../util/types'
|
|
6
|
+
import type {PluginConfig, StaticRenditionResolution} from '../util/types'
|
|
7
7
|
export type {VideoAssetDocument} from '../util/types'
|
|
8
8
|
|
|
9
9
|
export const defaultConfig: PluginConfig = {
|
|
10
|
+
static_renditions: [],
|
|
10
11
|
mp4_support: 'none',
|
|
11
12
|
video_quality: 'plus',
|
|
12
13
|
max_resolution_tier: '1080p',
|
|
@@ -17,6 +18,25 @@ export const defaultConfig: PluginConfig = {
|
|
|
17
18
|
allowedRolesForConfiguration: [],
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Converts legacy mp4_support configuration to static_renditions format
|
|
23
|
+
*/
|
|
24
|
+
function convertLegacyConfig(config: Partial<PluginConfig>): {
|
|
25
|
+
static_renditions: StaticRenditionResolution[]
|
|
26
|
+
} {
|
|
27
|
+
// If static_renditions is already provided, use it
|
|
28
|
+
if (config.static_renditions && config.static_renditions.length > 0) {
|
|
29
|
+
return {static_renditions: config.static_renditions}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Convert legacy mp4_support to static_renditions
|
|
33
|
+
if (config.mp4_support === 'standard') {
|
|
34
|
+
return {static_renditions: ['highest']}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {static_renditions: []}
|
|
38
|
+
}
|
|
39
|
+
|
|
20
40
|
export const muxInput = definePlugin<Partial<PluginConfig> | void>((userConfig) => {
|
|
21
41
|
// TODO: Remove this on next major version when we end support for encoding_tier
|
|
22
42
|
if (typeof userConfig === 'object' && 'encoding_tier' in userConfig) {
|
|
@@ -30,7 +50,11 @@ export const muxInput = definePlugin<Partial<PluginConfig> | void>((userConfig)
|
|
|
30
50
|
}
|
|
31
51
|
}
|
|
32
52
|
}
|
|
33
|
-
const config: PluginConfig = {
|
|
53
|
+
const config: PluginConfig = {
|
|
54
|
+
...defaultConfig,
|
|
55
|
+
...(userConfig || {}),
|
|
56
|
+
...convertLegacyConfig(userConfig || {}),
|
|
57
|
+
}
|
|
34
58
|
return {
|
|
35
59
|
name: 'mux-input',
|
|
36
60
|
schema: {
|
package/src/actions/upload.ts
CHANGED
|
@@ -156,7 +156,7 @@ type UploadResponse = {
|
|
|
156
156
|
cors_origin: string
|
|
157
157
|
id: string
|
|
158
158
|
new_asset_settings: {
|
|
159
|
-
|
|
159
|
+
static_renditions?: {resolution: string}[]
|
|
160
160
|
passthrough: string
|
|
161
161
|
playback_policies: ['public' | 'signed']
|
|
162
162
|
}
|
|
@@ -242,16 +242,17 @@ export function testUrl(url: string): Observable<string> {
|
|
|
242
242
|
if (typeof url !== 'string') {
|
|
243
243
|
return throwError(error)
|
|
244
244
|
}
|
|
245
|
+
const trimmedUrl = url.trim()
|
|
245
246
|
let parsed
|
|
246
247
|
try {
|
|
247
|
-
parsed = new URL(
|
|
248
|
+
parsed = new URL(trimmedUrl)
|
|
248
249
|
} catch (err) {
|
|
249
250
|
return throwError(error)
|
|
250
251
|
}
|
|
251
252
|
if (parsed && !parsed.protocol.match(/http:|https:/)) {
|
|
252
253
|
return throwError(error)
|
|
253
254
|
}
|
|
254
|
-
return of(
|
|
255
|
+
return of(trimmedUrl)
|
|
255
256
|
}
|
|
256
257
|
|
|
257
258
|
function optionsFromFile(opts: {preserveFilename?: boolean}, file: File) {
|
|
@@ -33,14 +33,23 @@ const Player = ({asset, buttons, readOnly, onChange}: Props) => {
|
|
|
33
33
|
return true
|
|
34
34
|
}, [asset])
|
|
35
35
|
const isPreparingStaticRenditions = useMemo<boolean>(() => {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// Legacy: If static_renditions has a status field, it was created with mp4_support (deprecated)
|
|
37
|
+
// We don't process this old format, just return false
|
|
38
|
+
// Note: 'disabled' status is valid in the new format when no renditions were requested
|
|
39
|
+
if (
|
|
40
|
+
asset?.data?.static_renditions?.status &&
|
|
41
|
+
asset?.data?.static_renditions?.status !== 'disabled'
|
|
42
|
+
) {
|
|
43
|
+
return false
|
|
38
44
|
}
|
|
39
|
-
|
|
45
|
+
|
|
46
|
+
// Check if any file in static_renditions is still preparing
|
|
47
|
+
const files = asset?.data?.static_renditions?.files
|
|
48
|
+
if (!files || files.length === 0) {
|
|
40
49
|
return false
|
|
41
50
|
}
|
|
42
|
-
return
|
|
43
|
-
}, [asset?.data?.static_renditions?.status])
|
|
51
|
+
return files.some((file) => file.status === 'preparing')
|
|
52
|
+
}, [asset?.data?.static_renditions?.status, asset?.data?.static_renditions?.files])
|
|
44
53
|
const playRef = useRef<HTMLDivElement>(null)
|
|
45
54
|
const muteRef = useRef<HTMLDivElement>(null)
|
|
46
55
|
const handleCancelUpload = useCancelUpload(asset, onChange)
|
|
@@ -2,7 +2,7 @@ import {DocumentVideoIcon, UploadIcon} from '@sanity/icons'
|
|
|
2
2
|
import {Box, Button, Card, Checkbox, Dialog, Flex, Label, Radio, Stack, Text} from '@sanity/ui'
|
|
3
3
|
import {uuid} from '@sanity/uuid'
|
|
4
4
|
import LanguagesList from 'iso-639-1'
|
|
5
|
-
import {useEffect, useId, useReducer, useRef} from 'react'
|
|
5
|
+
import {useEffect, useId, useMemo, useReducer, useRef, useState} from 'react'
|
|
6
6
|
import {FormField} from 'sanity'
|
|
7
7
|
|
|
8
8
|
import formatBytes from '../util/formatBytes'
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type MuxNewAssetSettings,
|
|
15
15
|
type PluginConfig,
|
|
16
16
|
type Secrets,
|
|
17
|
+
type StaticRenditionResolution,
|
|
17
18
|
type SupportedMuxLanguage,
|
|
18
19
|
type UploadConfig,
|
|
19
20
|
type UploadTextTrack,
|
|
@@ -25,7 +26,7 @@ import type {StagedUpload} from './Uploader'
|
|
|
25
26
|
export type UploadConfigurationStateAction =
|
|
26
27
|
| {action: 'video_quality'; value: UploadConfig['video_quality']}
|
|
27
28
|
| {action: 'max_resolution_tier'; value: UploadConfig['max_resolution_tier']}
|
|
28
|
-
| {action: '
|
|
29
|
+
| {action: 'static_renditions'; value: UploadConfig['static_renditions']}
|
|
29
30
|
| {action: 'normalize_audio'; value: UploadConfig['normalize_audio']}
|
|
30
31
|
| {action: 'signed_policy'; value: UploadConfig['signed_policy']}
|
|
31
32
|
| {action: 'public_policy'; value: UploadConfig['public_policy']}
|
|
@@ -43,6 +44,34 @@ const RESOLUTION_TIERS = [
|
|
|
43
44
|
{value: '2160p', label: '2160p (4k)'},
|
|
44
45
|
] as const satisfies {value: UploadConfig['max_resolution_tier']; label: string}[]
|
|
45
46
|
|
|
47
|
+
const ADVANCED_RESOLUTIONS: {value: StaticRenditionResolution; label: string}[] = [
|
|
48
|
+
{value: '270p', label: '270p'},
|
|
49
|
+
{value: '360p', label: '360p'},
|
|
50
|
+
{value: '480p', label: '480p'},
|
|
51
|
+
{value: '540p', label: '540p'},
|
|
52
|
+
{value: '720p', label: '720p'},
|
|
53
|
+
{value: '1080p', label: '1080p'},
|
|
54
|
+
{value: '1440p', label: '1440p'},
|
|
55
|
+
{value: '2160p', label: '2160p'},
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sanitizes static renditions configuration to ensure 'highest' is not mixed with specific resolutions.
|
|
60
|
+
* If both are present, only 'highest' (and 'audio-only' if present) will be kept.
|
|
61
|
+
*/
|
|
62
|
+
function sanitizeStaticRenditions(
|
|
63
|
+
renditions: StaticRenditionResolution[]
|
|
64
|
+
): StaticRenditionResolution[] {
|
|
65
|
+
const hasHighest = renditions.includes('highest')
|
|
66
|
+
const hasSpecificResolutions = renditions.some((r) => r !== 'highest' && r !== 'audio-only')
|
|
67
|
+
|
|
68
|
+
if (hasHighest && hasSpecificResolutions) {
|
|
69
|
+
return renditions.filter((r) => r === 'highest' || r === 'audio-only')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return renditions
|
|
73
|
+
}
|
|
74
|
+
|
|
46
75
|
/**
|
|
47
76
|
* The modal for configuring a staged upload. Handles triggering of the asset
|
|
48
77
|
* upload, even if no modal needs to be shown.
|
|
@@ -84,7 +113,7 @@ export default function UploadConfiguration({
|
|
|
84
113
|
if (action.value === 'basic') {
|
|
85
114
|
return Object.assign({}, prev, {
|
|
86
115
|
video_quality: action.value,
|
|
87
|
-
|
|
116
|
+
static_renditions: [],
|
|
88
117
|
max_resolution_tier: '1080p',
|
|
89
118
|
text_tracks: prev.text_tracks?.filter(({type}) => type !== 'autogenerated'),
|
|
90
119
|
public_policy: true,
|
|
@@ -94,12 +123,12 @@ export default function UploadConfiguration({
|
|
|
94
123
|
}
|
|
95
124
|
return Object.assign({}, prev, {
|
|
96
125
|
video_quality: action.value,
|
|
97
|
-
|
|
126
|
+
static_renditions: sanitizeStaticRenditions(pluginConfig.static_renditions || []),
|
|
98
127
|
max_resolution_tier: pluginConfig.max_resolution_tier,
|
|
99
128
|
text_tracks: [...autoTextTracks, ...(prev.text_tracks || [])],
|
|
100
129
|
})
|
|
101
130
|
|
|
102
|
-
case '
|
|
131
|
+
case 'static_renditions':
|
|
103
132
|
case 'max_resolution_tier':
|
|
104
133
|
case 'normalize_audio':
|
|
105
134
|
case 'signed_policy':
|
|
@@ -141,7 +170,7 @@ export default function UploadConfiguration({
|
|
|
141
170
|
{
|
|
142
171
|
video_quality: pluginConfig.video_quality,
|
|
143
172
|
max_resolution_tier: pluginConfig.max_resolution_tier,
|
|
144
|
-
|
|
173
|
+
static_renditions: sanitizeStaticRenditions(pluginConfig.static_renditions || []),
|
|
145
174
|
signed_policy: secrets.enableSignedUrls && pluginConfig.defaultSigned,
|
|
146
175
|
public_policy: pluginConfig.defaultPublic,
|
|
147
176
|
normalize_audio: pluginConfig.normalize_audio,
|
|
@@ -149,6 +178,54 @@ export default function UploadConfiguration({
|
|
|
149
178
|
} as UploadConfig
|
|
150
179
|
)
|
|
151
180
|
|
|
181
|
+
// Determine if user is in advanced mode based on selected renditions
|
|
182
|
+
const isAdvancedMode = useMemo(() => {
|
|
183
|
+
const specificResolutions = config.static_renditions.filter(
|
|
184
|
+
(r) => r !== 'highest' && r !== 'audio-only'
|
|
185
|
+
)
|
|
186
|
+
return specificResolutions.length > 0
|
|
187
|
+
}, [config.static_renditions])
|
|
188
|
+
|
|
189
|
+
const [renditionMode, setRenditionMode] = useState<'standard' | 'advanced'>(
|
|
190
|
+
isAdvancedMode ? 'advanced' : 'standard'
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
// Helper to toggle a rendition
|
|
194
|
+
const toggleRendition = (rendition: StaticRenditionResolution) => {
|
|
195
|
+
const current = config.static_renditions
|
|
196
|
+
const hasRendition = current.includes(rendition)
|
|
197
|
+
|
|
198
|
+
if (hasRendition) {
|
|
199
|
+
dispatch({
|
|
200
|
+
action: 'static_renditions',
|
|
201
|
+
value: current.filter((r) => r !== rendition),
|
|
202
|
+
})
|
|
203
|
+
} else {
|
|
204
|
+
dispatch({
|
|
205
|
+
action: 'static_renditions',
|
|
206
|
+
value: [...current, rendition],
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// When switching modes, clear renditions that don't apply
|
|
212
|
+
const handleModeChange = (mode: 'standard' | 'advanced') => {
|
|
213
|
+
setRenditionMode(mode)
|
|
214
|
+
if (mode === 'standard') {
|
|
215
|
+
// Remove specific resolutions, keep only highest and audio-only
|
|
216
|
+
dispatch({
|
|
217
|
+
action: 'static_renditions',
|
|
218
|
+
value: config.static_renditions.filter((r) => r === 'highest' || r === 'audio-only'),
|
|
219
|
+
})
|
|
220
|
+
} else {
|
|
221
|
+
// Remove highest, keep specific resolutions and audio-only
|
|
222
|
+
dispatch({
|
|
223
|
+
action: 'static_renditions',
|
|
224
|
+
value: config.static_renditions.filter((r) => r !== 'highest'),
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
152
229
|
// If user-provided config is disabled, begin the upload immediately with
|
|
153
230
|
// the developer-specified values from the schema or config or defaults.
|
|
154
231
|
// This can include auto-generated subtitles!
|
|
@@ -290,31 +367,111 @@ export default function UploadConfiguration({
|
|
|
290
367
|
|
|
291
368
|
{!basicConfig && (
|
|
292
369
|
<FormField title="Additional Configuration">
|
|
293
|
-
<Stack space={
|
|
370
|
+
<Stack space={3}>
|
|
294
371
|
<PlaybackPolicy id={id} config={config} secrets={secrets} dispatch={dispatch} />
|
|
295
372
|
|
|
296
|
-
{
|
|
297
|
-
<
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
373
|
+
<Stack space={3}>
|
|
374
|
+
<FormField
|
|
375
|
+
title="Static Renditions"
|
|
376
|
+
description="Generate downloadable MP4 or M4A files. Note: Mux will not upscale to produce MP4 renditions - renditions that would cause upscaling are skipped."
|
|
377
|
+
>
|
|
378
|
+
<Stack space={3}>
|
|
379
|
+
{/* Mode Selector */}
|
|
380
|
+
<Flex gap={3}>
|
|
381
|
+
<Flex align="center" gap={2}>
|
|
382
|
+
<Radio
|
|
383
|
+
checked={renditionMode === 'standard'}
|
|
384
|
+
name="rendition-mode"
|
|
385
|
+
onChange={() => handleModeChange('standard')}
|
|
386
|
+
value="standard"
|
|
387
|
+
id={`${id}--mode-standard`}
|
|
388
|
+
/>
|
|
389
|
+
<Text as="label" htmlFor={`${id}--mode-standard`}>
|
|
390
|
+
Standard
|
|
391
|
+
</Text>
|
|
392
|
+
</Flex>
|
|
393
|
+
<Flex align="center" gap={2}>
|
|
394
|
+
<Radio
|
|
395
|
+
checked={renditionMode === 'advanced'}
|
|
396
|
+
name="rendition-mode"
|
|
397
|
+
onChange={() => handleModeChange('advanced')}
|
|
398
|
+
value="advanced"
|
|
399
|
+
id={`${id}--mode-advanced`}
|
|
400
|
+
/>
|
|
401
|
+
<Text as="label" htmlFor={`${id}--mode-advanced`}>
|
|
402
|
+
Advanced
|
|
403
|
+
</Text>
|
|
404
|
+
</Flex>
|
|
405
|
+
</Flex>
|
|
406
|
+
|
|
407
|
+
{/* Standard Mode Options */}
|
|
408
|
+
{renditionMode === 'standard' && (
|
|
409
|
+
<Stack space={2}>
|
|
410
|
+
<Flex align="center" gap={2} padding={[0, 2]}>
|
|
411
|
+
<Checkbox
|
|
412
|
+
id={`${id}--highest`}
|
|
413
|
+
style={{display: 'block'}}
|
|
414
|
+
checked={config.static_renditions.includes('highest')}
|
|
415
|
+
onChange={() => toggleRendition('highest')}
|
|
416
|
+
/>
|
|
417
|
+
<Text as="label" htmlFor={`${id}--highest`}>
|
|
418
|
+
Highest Resolution (up to 4K)
|
|
419
|
+
</Text>
|
|
420
|
+
</Flex>
|
|
421
|
+
<Flex align="center" gap={2} padding={[0, 2]}>
|
|
422
|
+
<Checkbox
|
|
423
|
+
id={`${id}--audio-only-standard`}
|
|
424
|
+
style={{display: 'block'}}
|
|
425
|
+
checked={config.static_renditions.includes('audio-only')}
|
|
426
|
+
onChange={() => toggleRendition('audio-only')}
|
|
427
|
+
/>
|
|
428
|
+
<Text as="label" htmlFor={`${id}--audio-only-standard`}>
|
|
429
|
+
Audio Only (M4A)
|
|
430
|
+
</Text>
|
|
431
|
+
</Flex>
|
|
432
|
+
</Stack>
|
|
433
|
+
)}
|
|
434
|
+
|
|
435
|
+
{/* Advanced Mode Options */}
|
|
436
|
+
{renditionMode === 'advanced' && (
|
|
437
|
+
<Stack space={2}>
|
|
438
|
+
<Label size={1} muted>
|
|
439
|
+
Select specific resolutions:
|
|
440
|
+
</Label>
|
|
441
|
+
<Flex gap={2} wrap="wrap">
|
|
442
|
+
{ADVANCED_RESOLUTIONS.map(({value, label}) => {
|
|
443
|
+
const inputId = `${id}--resolution-${value}`
|
|
444
|
+
return (
|
|
445
|
+
<Flex key={value} align="center" gap={2}>
|
|
446
|
+
<Checkbox
|
|
447
|
+
id={inputId}
|
|
448
|
+
style={{display: 'block'}}
|
|
449
|
+
checked={config.static_renditions.includes(value)}
|
|
450
|
+
onChange={() => toggleRendition(value)}
|
|
451
|
+
/>
|
|
452
|
+
<Text as="label" htmlFor={inputId} size={1}>
|
|
453
|
+
{label}
|
|
454
|
+
</Text>
|
|
455
|
+
</Flex>
|
|
456
|
+
)
|
|
457
|
+
})}
|
|
458
|
+
</Flex>
|
|
459
|
+
<Flex align="center" gap={2} padding={[2, 2, 0, 2]}>
|
|
460
|
+
<Checkbox
|
|
461
|
+
id={`${id}--audio-only-advanced`}
|
|
462
|
+
style={{display: 'block'}}
|
|
463
|
+
checked={config.static_renditions.includes('audio-only')}
|
|
464
|
+
onChange={() => toggleRendition('audio-only')}
|
|
465
|
+
/>
|
|
466
|
+
<Text as="label" htmlFor={`${id}--audio-only-advanced`}>
|
|
467
|
+
Audio Only (M4A)
|
|
468
|
+
</Text>
|
|
469
|
+
</Flex>
|
|
470
|
+
</Stack>
|
|
471
|
+
)}
|
|
472
|
+
</Stack>
|
|
473
|
+
</FormField>
|
|
474
|
+
</Stack>
|
|
318
475
|
</Stack>
|
|
319
476
|
</FormField>
|
|
320
477
|
)}
|
|
@@ -385,7 +542,10 @@ function formatUploadConfig(config: UploadConfig): MuxNewAssetSettings {
|
|
|
385
542
|
[] as NonNullable<MuxNewAssetSettings['input']>
|
|
386
543
|
),
|
|
387
544
|
],
|
|
388
|
-
|
|
545
|
+
static_renditions:
|
|
546
|
+
config.static_renditions.length > 0
|
|
547
|
+
? config.static_renditions.map((resolution) => ({resolution}))
|
|
548
|
+
: undefined,
|
|
389
549
|
playback_policy: setPlaybackPolicy(config),
|
|
390
550
|
max_resolution_tier: config.max_resolution_tier,
|
|
391
551
|
video_quality: config.video_quality,
|
|
@@ -274,7 +274,7 @@ export default function Uploader(props: Props) {
|
|
|
274
274
|
event.preventDefault()
|
|
275
275
|
event.stopPropagation()
|
|
276
276
|
const clipboardData = event.clipboardData || (window as any).clipboardData
|
|
277
|
-
const url = clipboardData.getData('text')
|
|
277
|
+
const url = clipboardData.getData('text')?.trim()
|
|
278
278
|
if (!isValidUrl(url)) {
|
|
279
279
|
toast.push({status: 'error', title: 'Invalid URL for Mux video input.'})
|
|
280
280
|
return
|
|
@@ -45,26 +45,26 @@ export function PaneItemPreview(props: PaneItemPreviewProps) {
|
|
|
45
45
|
: null
|
|
46
46
|
|
|
47
47
|
const observable = useMemo(
|
|
48
|
-
() => getPreviewStateObservable(props.documentPreviewStore, schemaType, value._id
|
|
49
|
-
[props.documentPreviewStore, schemaType,
|
|
48
|
+
() => getPreviewStateObservable(props.documentPreviewStore, schemaType, value._id),
|
|
49
|
+
[props.documentPreviewStore, schemaType, value._id]
|
|
50
50
|
)
|
|
51
|
-
const {
|
|
52
|
-
draft: null,
|
|
53
|
-
published: null,
|
|
51
|
+
const {snapshot, original, isLoading} = useObservable(observable, {
|
|
54
52
|
isLoading: true,
|
|
53
|
+
snapshot: null,
|
|
54
|
+
original: null,
|
|
55
55
|
})
|
|
56
56
|
|
|
57
57
|
const status = isLoading ? null : (
|
|
58
58
|
<Inline space={4}>
|
|
59
59
|
{presence && presence.length > 0 && <DocumentPreviewPresence presence={presence} />}
|
|
60
|
-
<PublishedStatus document={
|
|
61
|
-
<DraftStatus document={
|
|
60
|
+
<PublishedStatus document={original} />
|
|
61
|
+
<DraftStatus document={snapshot} />
|
|
62
62
|
</Inline>
|
|
63
63
|
)
|
|
64
64
|
|
|
65
65
|
return (
|
|
66
66
|
<SanityDefaultPreview
|
|
67
|
-
{...(getPreviewValueWithFallback({
|
|
67
|
+
{...(getPreviewValueWithFallback({snapshot, original, fallback: {title}}) as any)}
|
|
68
68
|
isPlaceholder={isLoading}
|
|
69
69
|
icon={icon}
|
|
70
70
|
layout={layout}
|
|
@@ -10,11 +10,27 @@ export const useMuxPolling = (asset?: VideoAssetDocument) => {
|
|
|
10
10
|
const client = useClient()
|
|
11
11
|
const projectId = useProjectId()
|
|
12
12
|
const dataset = useDataset()
|
|
13
|
+
const isPreparingStaticRenditions = useMemo(() => {
|
|
14
|
+
// Legacy: If static_renditions has a status field, it was created with mp4_support (deprecated)
|
|
15
|
+
// We don't process this old format, just return false
|
|
16
|
+
// Note: 'disabled' status is valid in the new format when no renditions were requested
|
|
17
|
+
if (
|
|
18
|
+
asset?.data?.static_renditions?.status &&
|
|
19
|
+
asset?.data?.static_renditions?.status !== 'disabled'
|
|
20
|
+
) {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const files = asset?.data?.static_renditions?.files
|
|
25
|
+
if (!files || files.length === 0) {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
return files.some((file) => file.status === 'preparing')
|
|
29
|
+
}, [asset?.data?.static_renditions?.status, asset?.data?.static_renditions?.files])
|
|
30
|
+
|
|
13
31
|
const shouldFetch = useMemo(
|
|
14
|
-
() =>
|
|
15
|
-
|
|
16
|
-
(asset?.status === 'preparing' || asset?.data?.static_renditions?.status === 'preparing'),
|
|
17
|
-
[asset?.assetId, asset?.data?.static_renditions?.status, asset?.status]
|
|
32
|
+
() => !!asset?.assetId && (asset?.status === 'preparing' || isPreparingStaticRenditions),
|
|
33
|
+
[asset?.assetId, asset?.status, isPreparingStaticRenditions]
|
|
18
34
|
)
|
|
19
35
|
return useSWR(
|
|
20
36
|
shouldFetch ? `/${projectId}/addons/mux/assets/${dataset}/data/${asset?.assetId}` : null,
|
package/src/schema.ts
CHANGED
|
@@ -39,12 +39,18 @@ const muxStaticRenditionFile = {
|
|
|
39
39
|
name: 'mux.staticRenditionFile',
|
|
40
40
|
type: 'object',
|
|
41
41
|
fields: [
|
|
42
|
-
{type: 'string', name: 'ext'},
|
|
43
42
|
{type: 'string', name: 'name'},
|
|
43
|
+
{type: 'string', name: 'ext'},
|
|
44
|
+
{type: 'number', name: 'height'},
|
|
44
45
|
{type: 'number', name: 'width'},
|
|
45
46
|
{type: 'number', name: 'bitrate'},
|
|
46
|
-
{type: '
|
|
47
|
-
{type: '
|
|
47
|
+
{type: 'string', name: 'filesize'},
|
|
48
|
+
{type: 'string', name: 'type'},
|
|
49
|
+
{type: 'string', name: 'status'},
|
|
50
|
+
{type: 'string', name: 'resolution_tier'},
|
|
51
|
+
{type: 'string', name: 'resolution'},
|
|
52
|
+
{type: 'string', name: 'id'},
|
|
53
|
+
{type: 'string', name: 'passthrough'},
|
|
48
54
|
],
|
|
49
55
|
}
|
|
50
56
|
|
package/src/util/types.ts
CHANGED
|
@@ -1,8 +1,42 @@
|
|
|
1
1
|
import type {ObjectInputProps, PreviewLayoutKey, PreviewProps, SchemaType} from 'sanity'
|
|
2
2
|
import type {PartialDeep} from 'type-fest'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Standard static rendition options available for plugin configuration defaults
|
|
6
|
+
*/
|
|
7
|
+
export type StandardRendition = 'highest' | 'audio-only'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* All static rendition resolution options supported by Mux
|
|
11
|
+
*/
|
|
12
|
+
export type StaticRenditionResolution =
|
|
13
|
+
| 'highest'
|
|
14
|
+
| 'audio-only'
|
|
15
|
+
| '270p'
|
|
16
|
+
| '360p'
|
|
17
|
+
| '480p'
|
|
18
|
+
| '540p'
|
|
19
|
+
| '720p'
|
|
20
|
+
| '1080p'
|
|
21
|
+
| '1440p'
|
|
22
|
+
| '2160p'
|
|
23
|
+
|
|
4
24
|
export interface MuxInputConfig {
|
|
5
25
|
/**
|
|
26
|
+
* Enable static renditions by default. Can be overwritten on a per-asset basis.
|
|
27
|
+
* Supports:
|
|
28
|
+
* - Standard mode: 'highest' (up to 4K MP4) and/or 'audio-only' (M4A)
|
|
29
|
+
* - Advanced mode: Specific resolutions ('270p', '360p', '480p', '540p', '720p', '1080p', '1440p', '2160p') and/or 'audio-only'
|
|
30
|
+
*
|
|
31
|
+
* **Important**: 'highest' cannot be mixed with specific resolutions. If both are provided, only 'highest' will be used.
|
|
32
|
+
*
|
|
33
|
+
* @see {@link https://docs.mux.com/guides/video/enable-static-mp4-renditions}
|
|
34
|
+
* @defaultValue []
|
|
35
|
+
*/
|
|
36
|
+
static_renditions: StaticRenditionResolution[]
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @deprecated Use `static_renditions` instead. This field is kept for backward compatibility.
|
|
6
40
|
* Enable static renditions by setting this to 'standard'. Can be overwritten on a per-asset basis.
|
|
7
41
|
* Requires `"video_quality": "plus"`
|
|
8
42
|
* @see {@link https://docs.mux.com/guides/video/enable-static-mp4-renditions#why-enable-mp4-support}
|
|
@@ -182,10 +216,8 @@ export function isAutogeneratedTrack(
|
|
|
182
216
|
export type UploadTextTrack = AutogeneratedTextTrack | CustomTextTrack
|
|
183
217
|
|
|
184
218
|
export interface UploadConfig
|
|
185
|
-
extends Pick<
|
|
186
|
-
|
|
187
|
-
'max_resolution_tier' | 'mp4_support' | 'normalize_audio' | 'video_quality'
|
|
188
|
-
> {
|
|
219
|
+
extends Pick<MuxInputConfig, 'max_resolution_tier' | 'normalize_audio' | 'video_quality'> {
|
|
220
|
+
static_renditions: StaticRenditionResolution[]
|
|
189
221
|
text_tracks: UploadTextTrack[]
|
|
190
222
|
signed_policy: boolean
|
|
191
223
|
public_policy: boolean
|
|
@@ -196,10 +228,9 @@ export interface UploadConfig
|
|
|
196
228
|
* @docs {@link https://docs.mux.com/api-reference#video/operation/create-direct-upload}
|
|
197
229
|
*/
|
|
198
230
|
export interface MuxNewAssetSettings
|
|
199
|
-
extends Pick<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
> {
|
|
231
|
+
extends Pick<MuxInputConfig, 'max_resolution_tier' | 'normalize_audio' | 'video_quality'> {
|
|
232
|
+
/** Static renditions configuration for downloadable files */
|
|
233
|
+
static_renditions?: {resolution: StaticRenditionResolution}[]
|
|
203
234
|
/** An array of objects that each describe an input file to be used to create the asset.*/
|
|
204
235
|
input?: {
|
|
205
236
|
/** The URL of the file that Mux should download and use. */
|
|
@@ -371,12 +402,18 @@ export interface MuxAsset {
|
|
|
371
402
|
static_renditions?: {
|
|
372
403
|
status: 'ready' | 'preparing' | 'disabled' | 'errored'
|
|
373
404
|
files: {
|
|
374
|
-
name:
|
|
405
|
+
name: string
|
|
375
406
|
ext: 'mp4' | 'm4a'
|
|
376
407
|
height: number
|
|
377
408
|
width: number
|
|
378
409
|
bitrate: number
|
|
379
|
-
filesize:
|
|
410
|
+
filesize: string
|
|
411
|
+
type: 'standard' | 'advanced'
|
|
412
|
+
status: 'ready' | 'preparing' | 'skipped' | 'errored'
|
|
413
|
+
resolution_tier?: string
|
|
414
|
+
resolution?: string
|
|
415
|
+
id: string
|
|
416
|
+
passthrough?: string
|
|
380
417
|
}[]
|
|
381
418
|
}
|
|
382
419
|
recording_times?: {
|