sanity-plugin-mux-input 2.14.0 → 2.16.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 +25 -24
- package/dist/index.d.mts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +1057 -470
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1059 -472
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/_exports/index.ts +1 -0
- package/src/actions/secrets.ts +6 -1
- package/src/actions/upload.ts +1 -1
- package/src/components/ConfigureApi.tsx +51 -5
- package/src/components/EditCaptionDialog.tsx +2 -2
- package/src/components/InputBrowser.tsx +8 -2
- package/src/components/PageSelector.tsx +4 -7
- package/src/components/Player.styled.tsx +7 -2
- package/src/components/PlayerActionsMenu.tsx +15 -1
- package/src/components/ResyncMetadata.tsx +152 -73
- package/src/components/SelectAsset.tsx +9 -3
- package/src/components/StudioTool.tsx +2 -2
- package/src/components/TextTracksManager.tsx +11 -55
- package/src/components/UploadConfiguration.tsx +104 -343
- package/src/components/Uploader.tsx +18 -7
- package/src/components/VideoDetails/VideoDetails.tsx +55 -19
- package/src/components/VideoDetails/useVideoDetails.ts +15 -1
- package/src/components/VideoInBrowser.tsx +53 -6
- package/src/components/VideoPlayer.tsx +120 -47
- package/src/components/VideoThumbnail.tsx +84 -72
- package/src/components/VideosBrowser.tsx +7 -5
- package/src/components/uploadConfiguration/PlaybackPolicy.tsx +95 -6
- package/src/components/uploadConfiguration/PlaybackPolicyOption.tsx +26 -10
- package/src/components/uploadConfiguration/ResolutionTierSelector.tsx +71 -0
- package/src/components/uploadConfiguration/StaticRenditionSelector.tsx +179 -0
- package/src/context/DrmPlaybackWarningContext.tsx +93 -0
- package/src/hooks/useFetchFileSize.ts +54 -0
- package/src/hooks/useMediaMetadata.ts +100 -0
- package/src/hooks/useResyncAsset.ts +110 -0
- package/src/hooks/useResyncMuxMetadata.ts +33 -0
- package/src/hooks/useSaveSecrets.ts +10 -3
- package/src/hooks/useSecretsDocumentValues.ts +9 -1
- package/src/hooks/useSecretsFormState.ts +6 -3
- package/src/schema.ts +5 -0
- package/src/util/addKeysToMuxData.ts +30 -0
- package/src/util/asserters.ts +14 -0
- package/src/util/createUrlParamsObject.ts +7 -3
- package/src/util/generateJwt.ts +11 -2
- package/src/util/getPlaybackPolicy.ts +63 -4
- package/src/util/getStoryboardSrc.ts +7 -3
- package/src/util/getVideoMetadata.ts +1 -0
- package/src/util/getVideoSrc.ts +9 -9
- package/src/util/readSecrets.ts +3 -1
- package/src/util/textTracks.ts +6 -3
- package/src/util/tryWithSuspend.ts +22 -0
- package/src/util/types.ts +27 -2
- package/src/util/getPlaybackId.ts +0 -9
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {DocumentVideoIcon, ErrorOutlineIcon, UploadIcon} from '@sanity/icons'
|
|
2
|
-
import {Box, Button, Card,
|
|
2
|
+
import {Box, Button, Card, 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,
|
|
5
|
+
import {useEffect, useId, useReducer, useRef, useState} from 'react'
|
|
6
6
|
import {FormField} from 'sanity'
|
|
7
7
|
|
|
8
|
+
import {useFetchFileSize} from '../hooks/useFetchFileSize'
|
|
9
|
+
import {useMediaMetadata} from '../hooks/useMediaMetadata'
|
|
8
10
|
import formatBytes from '../util/formatBytes'
|
|
9
11
|
import {formatSeconds} from '../util/formatSeconds'
|
|
10
12
|
import {
|
|
@@ -22,6 +24,11 @@ import {
|
|
|
22
24
|
} from '../util/types'
|
|
23
25
|
import TextTracksEditor, {type TrackAction} from './TextTracksEditor'
|
|
24
26
|
import PlaybackPolicy from './uploadConfiguration/PlaybackPolicy'
|
|
27
|
+
import {
|
|
28
|
+
RESOLUTION_TIERS,
|
|
29
|
+
ResolutionTierSelector,
|
|
30
|
+
} from './uploadConfiguration/ResolutionTierSelector'
|
|
31
|
+
import {StaticRenditionSelector} from './uploadConfiguration/StaticRenditionSelector'
|
|
25
32
|
import type {StagedUpload} from './Uploader'
|
|
26
33
|
|
|
27
34
|
export type UploadConfigurationStateAction =
|
|
@@ -31,6 +38,7 @@ export type UploadConfigurationStateAction =
|
|
|
31
38
|
| {action: 'normalize_audio'; value: UploadConfig['normalize_audio']}
|
|
32
39
|
| {action: 'signed_policy'; value: UploadConfig['signed_policy']}
|
|
33
40
|
| {action: 'public_policy'; value: UploadConfig['public_policy']}
|
|
41
|
+
| {action: 'drm_policy'; value: UploadConfig['drm_policy']}
|
|
34
42
|
| TrackAction
|
|
35
43
|
|
|
36
44
|
const VIDEO_QUALITY_LEVELS = [
|
|
@@ -39,23 +47,6 @@ const VIDEO_QUALITY_LEVELS = [
|
|
|
39
47
|
{value: 'premium', label: 'Premium'},
|
|
40
48
|
] as const satisfies {value: UploadConfig['video_quality']; label: string}[]
|
|
41
49
|
|
|
42
|
-
const RESOLUTION_TIERS = [
|
|
43
|
-
{value: '1080p', label: '1080p'},
|
|
44
|
-
{value: '1440p', label: '1440p (2k)'},
|
|
45
|
-
{value: '2160p', label: '2160p (4k)'},
|
|
46
|
-
] as const satisfies {value: UploadConfig['max_resolution_tier']; label: string}[]
|
|
47
|
-
|
|
48
|
-
const ADVANCED_RESOLUTIONS: {value: StaticRenditionResolution; label: string}[] = [
|
|
49
|
-
{value: '270p', label: '270p'},
|
|
50
|
-
{value: '360p', label: '360p'},
|
|
51
|
-
{value: '480p', label: '480p'},
|
|
52
|
-
{value: '540p', label: '540p'},
|
|
53
|
-
{value: '720p', label: '720p'},
|
|
54
|
-
{value: '1080p', label: '1080p'},
|
|
55
|
-
{value: '1440p', label: '1440p'},
|
|
56
|
-
{value: '2160p', label: '2160p'},
|
|
57
|
-
]
|
|
58
|
-
|
|
59
50
|
/**
|
|
60
51
|
* Sanitizes static renditions configuration to ensure 'highest' is not mixed with specific resolutions.
|
|
61
52
|
* If both are present, only 'highest' (and 'audio-only' if present) will be kept.
|
|
@@ -119,6 +110,7 @@ export default function UploadConfiguration({
|
|
|
119
110
|
text_tracks: prev.text_tracks?.filter(({type}) => type !== 'autogenerated'),
|
|
120
111
|
public_policy: true,
|
|
121
112
|
signed_policy: false,
|
|
113
|
+
drm_policy: false,
|
|
122
114
|
})
|
|
123
115
|
// If video quality level switches to plus, add back in default plus features
|
|
124
116
|
}
|
|
@@ -136,6 +128,8 @@ export default function UploadConfiguration({
|
|
|
136
128
|
return Object.assign({}, prev, {[action.action]: action.value})
|
|
137
129
|
case 'public_policy':
|
|
138
130
|
return Object.assign({}, prev, {[action.action]: action.value})
|
|
131
|
+
case 'drm_policy':
|
|
132
|
+
return Object.assign({}, prev, {[action.action]: action.value})
|
|
139
133
|
// Updating individual tracks
|
|
140
134
|
case 'track': {
|
|
141
135
|
const text_tracks = [...prev.text_tracks]
|
|
@@ -174,88 +168,39 @@ export default function UploadConfiguration({
|
|
|
174
168
|
static_renditions: sanitizeStaticRenditions(pluginConfig.static_renditions || []),
|
|
175
169
|
signed_policy: secrets.enableSignedUrls && pluginConfig.defaultSigned,
|
|
176
170
|
public_policy: pluginConfig.defaultPublic,
|
|
171
|
+
drm_policy: pluginConfig.defaultDrm && !!secrets.drmConfigId,
|
|
177
172
|
normalize_audio: pluginConfig.normalize_audio,
|
|
178
173
|
text_tracks: autoTextTracks,
|
|
179
174
|
} as UploadConfig
|
|
180
175
|
)
|
|
181
176
|
|
|
182
|
-
// Determine if user is in advanced mode based on selected renditions
|
|
183
|
-
const isAdvancedMode = useMemo(() => {
|
|
184
|
-
const specificResolutions = config.static_renditions.filter(
|
|
185
|
-
(r) => r !== 'highest' && r !== 'audio-only'
|
|
186
|
-
)
|
|
187
|
-
return specificResolutions.length > 0
|
|
188
|
-
}, [config.static_renditions])
|
|
189
|
-
|
|
190
|
-
const [renditionMode, setRenditionMode] = useState<'standard' | 'advanced'>(
|
|
191
|
-
isAdvancedMode ? 'advanced' : 'standard'
|
|
192
|
-
)
|
|
193
|
-
|
|
194
177
|
// Video validations
|
|
195
|
-
const [videoDuration, setVideoDuration] = useState<number | null>(null)
|
|
196
|
-
const [urlFileSize, setUrlFileSize] = useState<number | null>(null)
|
|
197
|
-
const [isLoadingDuration, setIsLoadingDuration] = useState(false)
|
|
198
|
-
const [isLoadingFileSize, setIsLoadingFileSize] = useState(false)
|
|
199
178
|
const [validationError, setValidationError] = useState<string | null>(null)
|
|
200
|
-
const [canSkipFileSizeValidation, setCanSkipFileSizeValidation] = useState(false)
|
|
201
|
-
|
|
202
179
|
const MAX_FILE_SIZE = pluginConfig.maxAssetFileSize
|
|
203
180
|
const MAX_DURATION_SECONDS = pluginConfig.maxAssetDuration
|
|
204
181
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
setCanSkipFileSizeValidation(false)
|
|
212
|
-
|
|
213
|
-
let videoElement: HTMLVideoElement | null = null
|
|
214
|
-
let currentVideoSrc: string | null = null
|
|
182
|
+
const {fileSize, isLoadingFileSize, canSkipFileSizeValidation} = useFetchFileSize(
|
|
183
|
+
stagedUpload,
|
|
184
|
+
MAX_FILE_SIZE
|
|
185
|
+
)
|
|
186
|
+
const {videoAssetMetadata, setVideoAssetMetadata, isLoadingMetadata} =
|
|
187
|
+
useMediaMetadata(stagedUpload)
|
|
215
188
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
videoElement.onerror = null
|
|
220
|
-
videoElement.src = ''
|
|
221
|
-
videoElement.load()
|
|
222
|
-
videoElement = null
|
|
223
|
-
}
|
|
224
|
-
if (shouldRevokeUrl && currentVideoSrc?.startsWith('blob:')) {
|
|
225
|
-
URL.revokeObjectURL(currentVideoSrc)
|
|
226
|
-
}
|
|
227
|
-
currentVideoSrc = null
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (fileSize) {
|
|
191
|
+
setVideoAssetMetadata((old) => ({...old, size: fileSize}))
|
|
228
192
|
}
|
|
193
|
+
}, [fileSize, setVideoAssetMetadata])
|
|
229
194
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
videoElement.onloadedmetadata = () => {
|
|
239
|
-
const duration = videoElement!.duration
|
|
240
|
-
setVideoDuration(duration)
|
|
241
|
-
setIsLoadingDuration(false)
|
|
242
|
-
|
|
243
|
-
if (duration > MAX_DURATION_SECONDS) {
|
|
244
|
-
setValidationError(
|
|
245
|
-
`Video duration (${formatSeconds(duration)}) exceeds maximum allowed duration of ${formatSeconds(MAX_DURATION_SECONDS)}`
|
|
246
|
-
)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
cleanupVideo(shouldRevokeUrl)
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
videoElement.onerror = () => {
|
|
253
|
-
setIsLoadingDuration(false)
|
|
254
|
-
console.warn('Could not read video metadata for validation')
|
|
255
|
-
cleanupVideo(shouldRevokeUrl)
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
const validateDuration = (duration: number) => {
|
|
197
|
+
if (MAX_DURATION_SECONDS && duration > MAX_DURATION_SECONDS) {
|
|
198
|
+
setValidationError(
|
|
199
|
+
`Video duration (${formatSeconds(duration)}) exceeds maximum allowed duration of ${formatSeconds(MAX_DURATION_SECONDS)}`
|
|
200
|
+
)
|
|
201
|
+
return false
|
|
256
202
|
}
|
|
257
|
-
|
|
258
|
-
videoElement.src = videoSrc
|
|
203
|
+
return true
|
|
259
204
|
}
|
|
260
205
|
|
|
261
206
|
const validateFileSize = (size: number): boolean => {
|
|
@@ -269,95 +214,39 @@ export default function UploadConfiguration({
|
|
|
269
214
|
return false
|
|
270
215
|
}
|
|
271
216
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
validateDuration(URL.createObjectURL(file), true)
|
|
217
|
+
const validateDrmAvailability = (isAudioOnly: boolean) => {
|
|
218
|
+
if (config.drm_policy && isAudioOnly) {
|
|
219
|
+
setValidationError('Audio-only asset cannot be DRM protected')
|
|
220
|
+
return false
|
|
277
221
|
}
|
|
222
|
+
return true
|
|
278
223
|
}
|
|
279
224
|
|
|
280
|
-
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
// Get file size from URL
|
|
285
|
-
const fetchFileSize = async () => {
|
|
286
|
-
setIsLoadingFileSize(true)
|
|
287
|
-
try {
|
|
288
|
-
const response = await fetch(url, {method: 'HEAD'})
|
|
289
|
-
const contentLength = response.headers.get('content-length')
|
|
290
|
-
const fileSize = contentLength ? parseInt(contentLength, 10) : null
|
|
291
|
-
|
|
292
|
-
setIsLoadingFileSize(false)
|
|
293
|
-
if (fileSize) {
|
|
294
|
-
setUrlFileSize(fileSize)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Validate file size if limit is configured and size is available
|
|
298
|
-
const shouldValidateDuration =
|
|
299
|
-
MAX_FILE_SIZE === undefined || fileSize === null || validateFileSize(fileSize)
|
|
300
|
-
|
|
301
|
-
if (fileSize === null && MAX_FILE_SIZE !== undefined) {
|
|
302
|
-
// Size unknown but size limit is configured - skip file size validation
|
|
303
|
-
setCanSkipFileSizeValidation(true)
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (shouldValidateDuration) {
|
|
307
|
-
validateDuration(url)
|
|
308
|
-
}
|
|
309
|
-
} catch {
|
|
310
|
-
setIsLoadingFileSize(false)
|
|
311
|
-
console.warn('Could not validate file size from URL')
|
|
312
|
-
// Skip validation of file size, but still validate duration
|
|
313
|
-
setCanSkipFileSizeValidation(true)
|
|
314
|
-
validateDuration(url)
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
fetchFileSize()
|
|
225
|
+
let valid = true
|
|
226
|
+
if (videoAssetMetadata?.size) {
|
|
227
|
+
valid = valid && (canSkipFileSizeValidation || validateFileSize(videoAssetMetadata.size))
|
|
319
228
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
cleanupVideo(true)
|
|
229
|
+
if (videoAssetMetadata?.duration) {
|
|
230
|
+
valid = valid && validateDuration(videoAssetMetadata.duration)
|
|
323
231
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
// Helper to toggle a rendition
|
|
327
|
-
const toggleRendition = (rendition: StaticRenditionResolution) => {
|
|
328
|
-
const current = config.static_renditions
|
|
329
|
-
const hasRendition = current.includes(rendition)
|
|
330
|
-
|
|
331
|
-
if (hasRendition) {
|
|
332
|
-
dispatch({
|
|
333
|
-
action: 'static_renditions',
|
|
334
|
-
value: current.filter((r) => r !== rendition),
|
|
335
|
-
})
|
|
336
|
-
} else {
|
|
337
|
-
dispatch({
|
|
338
|
-
action: 'static_renditions',
|
|
339
|
-
value: [...current, rendition],
|
|
340
|
-
})
|
|
232
|
+
if (videoAssetMetadata?.isAudioOnly != undefined) {
|
|
233
|
+
valid = valid && validateDrmAvailability(videoAssetMetadata.isAudioOnly)
|
|
341
234
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
// When switching modes, clear renditions that don't apply
|
|
345
|
-
const handleModeChange = (mode: 'standard' | 'advanced') => {
|
|
346
|
-
setRenditionMode(mode)
|
|
347
|
-
if (mode === 'standard') {
|
|
348
|
-
// Remove specific resolutions, keep only highest and audio-only
|
|
349
|
-
dispatch({
|
|
350
|
-
action: 'static_renditions',
|
|
351
|
-
value: config.static_renditions.filter((r) => r === 'highest' || r === 'audio-only'),
|
|
352
|
-
})
|
|
353
|
-
} else {
|
|
354
|
-
// Remove highest, keep specific resolutions and audio-only
|
|
355
|
-
dispatch({
|
|
356
|
-
action: 'static_renditions',
|
|
357
|
-
value: config.static_renditions.filter((r) => r !== 'highest'),
|
|
358
|
-
})
|
|
235
|
+
if (valid) {
|
|
236
|
+
setValidationError(null)
|
|
359
237
|
}
|
|
360
|
-
}
|
|
238
|
+
}, [
|
|
239
|
+
MAX_FILE_SIZE,
|
|
240
|
+
MAX_DURATION_SECONDS,
|
|
241
|
+
canSkipFileSizeValidation,
|
|
242
|
+
videoAssetMetadata?.duration,
|
|
243
|
+
videoAssetMetadata?.size,
|
|
244
|
+
videoAssetMetadata?.height,
|
|
245
|
+
videoAssetMetadata?.width,
|
|
246
|
+
videoAssetMetadata,
|
|
247
|
+
config.drm_policy,
|
|
248
|
+
validationError,
|
|
249
|
+
])
|
|
361
250
|
|
|
362
251
|
// If user-provided config is disabled, begin the upload immediately with
|
|
363
252
|
// the developer-specified values from the schema or config or defaults.
|
|
@@ -365,12 +254,13 @@ export default function UploadConfiguration({
|
|
|
365
254
|
const {disableTextTrackConfig, disableUploadConfig} = pluginConfig
|
|
366
255
|
const skipConfig = disableTextTrackConfig && disableUploadConfig
|
|
367
256
|
useEffect(() => {
|
|
368
|
-
if (skipConfig) startUpload(formatUploadConfig(config))
|
|
257
|
+
if (skipConfig) startUpload(formatUploadConfig(config, secrets))
|
|
369
258
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
370
259
|
}, [])
|
|
371
260
|
if (skipConfig) return null
|
|
372
261
|
|
|
373
262
|
const basicConfig = config.video_quality !== 'plus' && config.video_quality !== 'premium'
|
|
263
|
+
const playbackPolicySelected = config.public_policy || config.signed_policy || config.drm_policy
|
|
374
264
|
const maxSupportedResolution = RESOLUTION_TIERS.findIndex(
|
|
375
265
|
(rt) => rt.value === pluginConfig.max_resolution_tier
|
|
376
266
|
)
|
|
@@ -416,8 +306,8 @@ export default function UploadConfiguration({
|
|
|
416
306
|
{stagedUpload.type === 'file'
|
|
417
307
|
? `Direct File Upload (${formatBytes(stagedUpload.files[0].size)})`
|
|
418
308
|
: (() => {
|
|
419
|
-
if (
|
|
420
|
-
return `File From URL (${formatBytes(
|
|
309
|
+
if (videoAssetMetadata?.size) {
|
|
310
|
+
return `File From URL (${formatBytes(videoAssetMetadata.size)})`
|
|
421
311
|
}
|
|
422
312
|
if (isLoadingFileSize) {
|
|
423
313
|
return 'File From URL (Loading size...)'
|
|
@@ -427,14 +317,14 @@ export default function UploadConfiguration({
|
|
|
427
317
|
</Text>
|
|
428
318
|
{stagedUpload.type === 'file' && (
|
|
429
319
|
<Stack space={1}>
|
|
430
|
-
{
|
|
320
|
+
{isLoadingMetadata && (
|
|
431
321
|
<Text as="p" size={1} muted>
|
|
432
322
|
Reading video metadata...
|
|
433
323
|
</Text>
|
|
434
324
|
)}
|
|
435
|
-
{
|
|
325
|
+
{videoAssetMetadata?.duration && !validationError && (
|
|
436
326
|
<Text as="p" size={1} muted>
|
|
437
|
-
Duration: {formatSeconds(
|
|
327
|
+
Duration: {formatSeconds(videoAssetMetadata.duration)}
|
|
438
328
|
</Text>
|
|
439
329
|
)}
|
|
440
330
|
</Stack>
|
|
@@ -486,180 +376,38 @@ export default function UploadConfiguration({
|
|
|
486
376
|
</Flex>
|
|
487
377
|
</FormField>
|
|
488
378
|
|
|
489
|
-
{!basicConfig && maxSupportedResolution > 0 && (
|
|
490
|
-
<FormField
|
|
491
|
-
title="Resolution Tier"
|
|
492
|
-
description={
|
|
493
|
-
<>
|
|
494
|
-
The maximum{' '}
|
|
495
|
-
<a
|
|
496
|
-
href="https://docs.mux.com/api-reference#video/operation/create-direct-upload"
|
|
497
|
-
target="_blank"
|
|
498
|
-
rel="noopener noreferrer"
|
|
499
|
-
>
|
|
500
|
-
resolution_tier
|
|
501
|
-
</a>{' '}
|
|
502
|
-
your asset is encoded, stored, and streamed at.
|
|
503
|
-
</>
|
|
504
|
-
}
|
|
505
|
-
>
|
|
506
|
-
<Flex gap={3} wrap={'wrap'}>
|
|
507
|
-
{RESOLUTION_TIERS.map(({value, label}, index) => {
|
|
508
|
-
const inputId = `${id}--type-${value}`
|
|
509
|
-
|
|
510
|
-
if (index > maxSupportedResolution) return null
|
|
511
|
-
|
|
512
|
-
return (
|
|
513
|
-
<Flex key={value} align="center" gap={2}>
|
|
514
|
-
<Radio
|
|
515
|
-
checked={config.max_resolution_tier === value}
|
|
516
|
-
name="asset-resolutiontier"
|
|
517
|
-
onChange={(e) =>
|
|
518
|
-
dispatch({
|
|
519
|
-
action: 'max_resolution_tier',
|
|
520
|
-
value: e.currentTarget.value as UploadConfig['max_resolution_tier'],
|
|
521
|
-
})
|
|
522
|
-
}
|
|
523
|
-
value={value}
|
|
524
|
-
id={inputId}
|
|
525
|
-
/>
|
|
526
|
-
<Text as="label" htmlFor={inputId}>
|
|
527
|
-
{label}
|
|
528
|
-
</Text>
|
|
529
|
-
</Flex>
|
|
530
|
-
)
|
|
531
|
-
})}
|
|
532
|
-
</Flex>
|
|
533
|
-
</FormField>
|
|
534
|
-
)}
|
|
535
|
-
|
|
536
379
|
{!basicConfig && (
|
|
537
380
|
<FormField title="Additional Configuration">
|
|
538
381
|
<Stack space={3}>
|
|
539
382
|
<PlaybackPolicy id={id} config={config} secrets={secrets} dispatch={dispatch} />
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
/>
|
|
557
|
-
<Text as="label" htmlFor={`${id}--mode-standard`}>
|
|
558
|
-
Standard
|
|
559
|
-
</Text>
|
|
560
|
-
</Flex>
|
|
561
|
-
<Flex align="center" gap={2}>
|
|
562
|
-
<Radio
|
|
563
|
-
checked={renditionMode === 'advanced'}
|
|
564
|
-
name="rendition-mode"
|
|
565
|
-
onChange={() => handleModeChange('advanced')}
|
|
566
|
-
value="advanced"
|
|
567
|
-
id={`${id}--mode-advanced`}
|
|
568
|
-
/>
|
|
569
|
-
<Text as="label" htmlFor={`${id}--mode-advanced`}>
|
|
570
|
-
Advanced
|
|
571
|
-
</Text>
|
|
572
|
-
</Flex>
|
|
573
|
-
</Flex>
|
|
574
|
-
|
|
575
|
-
{/* Standard Mode Options */}
|
|
576
|
-
{renditionMode === 'standard' && (
|
|
577
|
-
<Stack space={2}>
|
|
578
|
-
<Flex align="center" gap={2} padding={[0, 2]}>
|
|
579
|
-
<Checkbox
|
|
580
|
-
id={`${id}--highest`}
|
|
581
|
-
style={{display: 'block'}}
|
|
582
|
-
checked={config.static_renditions.includes('highest')}
|
|
583
|
-
onChange={() => toggleRendition('highest')}
|
|
584
|
-
/>
|
|
585
|
-
<Text as="label" htmlFor={`${id}--highest`}>
|
|
586
|
-
Highest Resolution (up to 4K)
|
|
587
|
-
</Text>
|
|
588
|
-
</Flex>
|
|
589
|
-
<Flex align="center" gap={2} padding={[0, 2]}>
|
|
590
|
-
<Checkbox
|
|
591
|
-
id={`${id}--audio-only-standard`}
|
|
592
|
-
style={{display: 'block'}}
|
|
593
|
-
checked={config.static_renditions.includes('audio-only')}
|
|
594
|
-
onChange={() => toggleRendition('audio-only')}
|
|
595
|
-
/>
|
|
596
|
-
<Text as="label" htmlFor={`${id}--audio-only-standard`}>
|
|
597
|
-
Audio Only (M4A)
|
|
598
|
-
</Text>
|
|
599
|
-
</Flex>
|
|
600
|
-
</Stack>
|
|
601
|
-
)}
|
|
602
|
-
|
|
603
|
-
{/* Advanced Mode Options */}
|
|
604
|
-
{renditionMode === 'advanced' && (
|
|
605
|
-
<Stack space={2}>
|
|
606
|
-
<Label size={1} muted>
|
|
607
|
-
Select specific resolutions:
|
|
608
|
-
</Label>
|
|
609
|
-
<Flex gap={2} wrap="wrap">
|
|
610
|
-
{ADVANCED_RESOLUTIONS.map(({value, label}) => {
|
|
611
|
-
const inputId = `${id}--resolution-${value}`
|
|
612
|
-
return (
|
|
613
|
-
<Flex key={value} align="center" gap={2}>
|
|
614
|
-
<Checkbox
|
|
615
|
-
id={inputId}
|
|
616
|
-
style={{display: 'block'}}
|
|
617
|
-
checked={config.static_renditions.includes(value)}
|
|
618
|
-
onChange={() => toggleRendition(value)}
|
|
619
|
-
/>
|
|
620
|
-
<Text as="label" htmlFor={inputId} size={1}>
|
|
621
|
-
{label}
|
|
622
|
-
</Text>
|
|
623
|
-
</Flex>
|
|
624
|
-
)
|
|
625
|
-
})}
|
|
626
|
-
</Flex>
|
|
627
|
-
<Flex align="center" gap={2} padding={[2, 2, 0, 2]}>
|
|
628
|
-
<Checkbox
|
|
629
|
-
id={`${id}--audio-only-advanced`}
|
|
630
|
-
style={{display: 'block'}}
|
|
631
|
-
checked={config.static_renditions.includes('audio-only')}
|
|
632
|
-
onChange={() => toggleRendition('audio-only')}
|
|
633
|
-
/>
|
|
634
|
-
<Text as="label" htmlFor={`${id}--audio-only-advanced`}>
|
|
635
|
-
Audio Only (M4A)
|
|
636
|
-
</Text>
|
|
637
|
-
</Flex>
|
|
638
|
-
</Stack>
|
|
639
|
-
)}
|
|
640
|
-
</Stack>
|
|
641
|
-
</FormField>
|
|
642
|
-
</Stack>
|
|
383
|
+
{maxSupportedResolution > 0 && (
|
|
384
|
+
<ResolutionTierSelector
|
|
385
|
+
id={id}
|
|
386
|
+
config={config}
|
|
387
|
+
dispatch={dispatch}
|
|
388
|
+
maxSupportedResolution={maxSupportedResolution}
|
|
389
|
+
/>
|
|
390
|
+
)}
|
|
391
|
+
<StaticRenditionSelector id={id} config={config} dispatch={dispatch} />
|
|
392
|
+
{!disableTextTrackConfig && (
|
|
393
|
+
<TextTracksEditor
|
|
394
|
+
tracks={config.text_tracks}
|
|
395
|
+
dispatch={dispatch}
|
|
396
|
+
defaultLang={pluginConfig.defaultAutogeneratedSubtitleLang}
|
|
397
|
+
/>
|
|
398
|
+
)}
|
|
643
399
|
</Stack>
|
|
644
400
|
</FormField>
|
|
645
401
|
)}
|
|
646
402
|
</Stack>
|
|
647
403
|
)}
|
|
648
404
|
|
|
649
|
-
{!disableTextTrackConfig && !basicConfig && (
|
|
650
|
-
<TextTracksEditor
|
|
651
|
-
tracks={config.text_tracks}
|
|
652
|
-
dispatch={dispatch}
|
|
653
|
-
defaultLang={pluginConfig.defaultAutogeneratedSubtitleLang}
|
|
654
|
-
/>
|
|
655
|
-
)}
|
|
656
|
-
|
|
657
405
|
<Box marginTop={4}>
|
|
658
406
|
<Button
|
|
659
407
|
disabled={
|
|
660
|
-
(!basicConfig && !
|
|
408
|
+
(!basicConfig && !playbackPolicySelected) ||
|
|
661
409
|
validationError !== null ||
|
|
662
|
-
|
|
410
|
+
isLoadingMetadata ||
|
|
663
411
|
(isLoadingFileSize && !canSkipFileSizeValidation)
|
|
664
412
|
}
|
|
665
413
|
icon={UploadIcon}
|
|
@@ -667,7 +415,7 @@ export default function UploadConfiguration({
|
|
|
667
415
|
tone="positive"
|
|
668
416
|
onClick={() => {
|
|
669
417
|
if (!validationError) {
|
|
670
|
-
startUpload(formatUploadConfig(config))
|
|
418
|
+
startUpload(formatUploadConfig(config, secrets))
|
|
671
419
|
}
|
|
672
420
|
}}
|
|
673
421
|
/>
|
|
@@ -677,18 +425,31 @@ export default function UploadConfiguration({
|
|
|
677
425
|
)
|
|
678
426
|
}
|
|
679
427
|
|
|
680
|
-
function
|
|
681
|
-
|
|
428
|
+
function setAdvancedPlaybackPolicy(
|
|
429
|
+
config: UploadConfig,
|
|
430
|
+
secrets: Secrets
|
|
431
|
+
): MuxNewAssetSettings['advanced_playback_policies'] {
|
|
432
|
+
const advanced_playback_policies: MuxNewAssetSettings['advanced_playback_policies'] = []
|
|
682
433
|
if (config.public_policy) {
|
|
683
|
-
|
|
434
|
+
advanced_playback_policies.push({policy: 'public'})
|
|
684
435
|
}
|
|
685
436
|
if (config.signed_policy) {
|
|
686
|
-
|
|
437
|
+
advanced_playback_policies.push({policy: 'signed'})
|
|
438
|
+
}
|
|
439
|
+
if (config.drm_policy) {
|
|
440
|
+
if (secrets.drmConfigId)
|
|
441
|
+
advanced_playback_policies.push({
|
|
442
|
+
policy: 'drm',
|
|
443
|
+
drm_configuration_id: secrets.drmConfigId ?? undefined,
|
|
444
|
+
})
|
|
445
|
+
else {
|
|
446
|
+
console.error('Selected DRM Policy but missing DRM Configuration Id')
|
|
447
|
+
}
|
|
687
448
|
}
|
|
688
|
-
return
|
|
449
|
+
return advanced_playback_policies
|
|
689
450
|
}
|
|
690
451
|
|
|
691
|
-
function formatUploadConfig(config: UploadConfig): MuxNewAssetSettings {
|
|
452
|
+
function formatUploadConfig(config: UploadConfig, secrets: Secrets): MuxNewAssetSettings {
|
|
692
453
|
const generated_subtitles = config.text_tracks
|
|
693
454
|
.filter<AutogeneratedTextTrack>(isAutogeneratedTrack)
|
|
694
455
|
.map<{name: string; language_code: SupportedMuxLanguage}>((track) => ({
|
|
@@ -723,7 +484,7 @@ function formatUploadConfig(config: UploadConfig): MuxNewAssetSettings {
|
|
|
723
484
|
config.static_renditions.length > 0
|
|
724
485
|
? config.static_renditions.map((resolution) => ({resolution}))
|
|
725
486
|
: undefined,
|
|
726
|
-
|
|
487
|
+
advanced_playback_policies: setAdvancedPlaybackPolicy(config, secrets),
|
|
727
488
|
max_resolution_tier: config.max_resolution_tier,
|
|
728
489
|
video_quality: config.video_quality,
|
|
729
490
|
normalize_audio: config.normalize_audio,
|
|
@@ -9,8 +9,9 @@ import {PatchEvent, set, setIfMissing} from 'sanity'
|
|
|
9
9
|
import {uploadFile, uploadUrl} from '../actions/upload'
|
|
10
10
|
import {DialogStateProvider} from '../context/DialogStateContext'
|
|
11
11
|
import {type DialogState, type SetDialogState} from '../hooks/useDialogState'
|
|
12
|
-
import {isValidUrl} from '../util/asserters'
|
|
12
|
+
import {isServerError, isValidUrl} from '../util/asserters'
|
|
13
13
|
import {extractDroppedFiles} from '../util/extractFiles'
|
|
14
|
+
import {hasPlaybackPolicy} from '../util/getPlaybackPolicy'
|
|
14
15
|
import type {
|
|
15
16
|
MuxInputProps,
|
|
16
17
|
MuxNewAssetSettings,
|
|
@@ -66,7 +67,7 @@ type UploaderStateAction =
|
|
|
66
67
|
| Extract<UploadUrlEvent, {type: 'url'}>
|
|
67
68
|
))
|
|
68
69
|
| {action: 'progress'; percent: number}
|
|
69
|
-
| {action: 'error'; error: Error}
|
|
70
|
+
| {action: 'error'; error: Error; settings: MuxNewAssetSettings}
|
|
70
71
|
| {action: 'complete' | 'reset'}
|
|
71
72
|
|
|
72
73
|
/**
|
|
@@ -125,12 +126,21 @@ export default function Uploader(props: Props) {
|
|
|
125
126
|
uploadRef.current = null
|
|
126
127
|
uploadingDocumentId.current = null
|
|
127
128
|
return INITIAL_STATE
|
|
128
|
-
case 'error':
|
|
129
|
+
case 'error': {
|
|
129
130
|
// Clear upload observable on error
|
|
130
131
|
uploadRef.current?.unsubscribe()
|
|
131
132
|
uploadRef.current = null
|
|
132
133
|
uploadingDocumentId.current = null
|
|
133
|
-
|
|
134
|
+
|
|
135
|
+
let error = action.error
|
|
136
|
+
if (isServerError(action.error) && hasPlaybackPolicy(action.settings, 'drm')) {
|
|
137
|
+
error = new Error(
|
|
138
|
+
'Unknown Error while uploading DRM protected content. Make sure your DRM configuration ID is valid and set correctly'
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return Object.assign({}, INITIAL_STATE, {error: error})
|
|
143
|
+
}
|
|
134
144
|
default:
|
|
135
145
|
return prev
|
|
136
146
|
}
|
|
@@ -254,7 +264,7 @@ export default function Uploader(props: Props) {
|
|
|
254
264
|
}
|
|
255
265
|
},
|
|
256
266
|
complete: () => dispatch({action: 'complete'}),
|
|
257
|
-
error: (error) => dispatch({action: 'error', error}),
|
|
267
|
+
error: (error) => dispatch({action: 'error', error, settings}),
|
|
258
268
|
})
|
|
259
269
|
}
|
|
260
270
|
|
|
@@ -372,7 +382,7 @@ export default function Uploader(props: Props) {
|
|
|
372
382
|
|
|
373
383
|
// Upload has errored
|
|
374
384
|
if (state.error !== null) {
|
|
375
|
-
const error =
|
|
385
|
+
const error = state.error
|
|
376
386
|
return (
|
|
377
387
|
<Flex gap={3} direction="column" justify="center" align="center">
|
|
378
388
|
<Text size={5} muted>
|
|
@@ -380,7 +390,7 @@ export default function Uploader(props: Props) {
|
|
|
380
390
|
</Text>
|
|
381
391
|
<Text>Something went wrong</Text>
|
|
382
392
|
{error instanceof Error && error.message && (
|
|
383
|
-
<Text size={1} muted>
|
|
393
|
+
<Text size={1} muted weight="semibold" style={{textAlign: 'center'}}>
|
|
384
394
|
{error.message}
|
|
385
395
|
</Text>
|
|
386
396
|
)}
|
|
@@ -471,6 +481,7 @@ export default function Uploader(props: Props) {
|
|
|
471
481
|
</UploadCard>
|
|
472
482
|
{props.dialogState === 'select-video' && (
|
|
473
483
|
<InputBrowser
|
|
484
|
+
config={props.config}
|
|
474
485
|
asset={props.asset}
|
|
475
486
|
onChange={props.onChange}
|
|
476
487
|
setDialogState={props.setDialogState}
|