sanity-plugin-mux-input 3.0.5 → 4.0.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.
Files changed (123) hide show
  1. package/dist/index.js +28 -28
  2. package/dist/index.js.map +1 -1
  3. package/package.json +5 -15
  4. package/dist/index.cjs +0 -5746
  5. package/dist/index.cjs.map +0 -1
  6. package/dist/index.d.cts +0 -288
  7. package/dist/index.d.cts.map +0 -1
  8. package/sanity.json +0 -8
  9. package/src/_exports/index.ts +0 -73
  10. package/src/actions/assets.ts +0 -152
  11. package/src/actions/secrets.ts +0 -110
  12. package/src/actions/upload.ts +0 -308
  13. package/src/clients/upChunkObservable.ts +0 -54
  14. package/src/components/AddCaptionDialog.tsx +0 -440
  15. package/src/components/CaptionsDialog.tsx +0 -23
  16. package/src/components/ConfigureApi.styled.tsx +0 -19
  17. package/src/components/ConfigureApi.tsx +0 -296
  18. package/src/components/DraggableWatermark.tsx +0 -885
  19. package/src/components/EditCaptionDialog.tsx +0 -511
  20. package/src/components/EditThumbnailDialog.tsx +0 -121
  21. package/src/components/ErrorBoundaryCard.tsx +0 -97
  22. package/src/components/FileInputButton.tsx +0 -54
  23. package/src/components/FileInputMenuItem.styled.tsx +0 -36
  24. package/src/components/FileInputMenuItem.tsx +0 -85
  25. package/src/components/FormField.tsx +0 -38
  26. package/src/components/IconInfo.tsx +0 -22
  27. package/src/components/ImportVideosFromMux.tsx +0 -339
  28. package/src/components/Input.styled.tsx +0 -22
  29. package/src/components/Input.tsx +0 -78
  30. package/src/components/InputBrowser.tsx +0 -41
  31. package/src/components/MuxLogo.tsx +0 -42
  32. package/src/components/Onboard.tsx +0 -65
  33. package/src/components/PageSelector.tsx +0 -54
  34. package/src/components/Player.styled.tsx +0 -11
  35. package/src/components/Player.tsx +0 -117
  36. package/src/components/PlayerActionsMenu.tsx +0 -191
  37. package/src/components/ResyncMetadata.tsx +0 -278
  38. package/src/components/SelectAsset.tsx +0 -39
  39. package/src/components/SelectSortOptions.tsx +0 -45
  40. package/src/components/SpinnerBox.tsx +0 -16
  41. package/src/components/StudioTool.tsx +0 -24
  42. package/src/components/TextTracksEditor.tsx +0 -117
  43. package/src/components/TextTracksManager.tsx +0 -738
  44. package/src/components/UploadConfiguration.tsx +0 -696
  45. package/src/components/UploadPlaceholder.tsx +0 -88
  46. package/src/components/UploadProgress.tsx +0 -80
  47. package/src/components/Uploader.styled.tsx +0 -65
  48. package/src/components/Uploader.tsx +0 -499
  49. package/src/components/VideoDetails/DeleteDialog.tsx +0 -148
  50. package/src/components/VideoDetails/VideoDetails.tsx +0 -358
  51. package/src/components/VideoDetails/VideoReferences.tsx +0 -63
  52. package/src/components/VideoDetails/useVideoDetails.ts +0 -103
  53. package/src/components/VideoInBrowser.tsx +0 -245
  54. package/src/components/VideoMetadata.tsx +0 -45
  55. package/src/components/VideoPlayer.tsx +0 -241
  56. package/src/components/VideoThumbnail.tsx +0 -139
  57. package/src/components/VideosBrowser.tsx +0 -100
  58. package/src/components/documentPreview/DocumentPreview.tsx +0 -84
  59. package/src/components/documentPreview/DraftStatus.tsx +0 -34
  60. package/src/components/documentPreview/MissingSchemaType.tsx +0 -32
  61. package/src/components/documentPreview/PaneItemPreview.tsx +0 -67
  62. package/src/components/documentPreview/PublishedStatus.tsx +0 -35
  63. package/src/components/documentPreview/TimeAgo.tsx +0 -12
  64. package/src/components/icons/Audio.tsx +0 -13
  65. package/src/components/icons/Resolution.tsx +0 -12
  66. package/src/components/icons/StopWatch.tsx +0 -20
  67. package/src/components/icons/ToolIcon.tsx +0 -19
  68. package/src/components/uploadConfiguration/PlaybackPolicy.tsx +0 -133
  69. package/src/components/uploadConfiguration/PlaybackPolicyOption.tsx +0 -76
  70. package/src/components/uploadConfiguration/PlaybackPolicyWarning.tsx +0 -29
  71. package/src/components/uploadConfiguration/ResolutionTierSelector.tsx +0 -72
  72. package/src/components/uploadConfiguration/StaticRenditionSelector.tsx +0 -180
  73. package/src/components/withFocusRing/helpers.ts +0 -24
  74. package/src/components/withFocusRing/index.ts +0 -1
  75. package/src/components/withFocusRing/withFocusRing.ts +0 -30
  76. package/src/context/DialogStateContext.tsx +0 -33
  77. package/src/context/DrmPlaybackWarningContext.tsx +0 -97
  78. package/src/hooks/useAccessControl.ts +0 -13
  79. package/src/hooks/useAssetDocumentValues.ts +0 -11
  80. package/src/hooks/useAssets.ts +0 -73
  81. package/src/hooks/useCancelUpload.ts +0 -22
  82. package/src/hooks/useClient.ts +0 -8
  83. package/src/hooks/useDialogState.ts +0 -11
  84. package/src/hooks/useDocReferences.ts +0 -21
  85. package/src/hooks/useFetchFileSize.ts +0 -55
  86. package/src/hooks/useImportMuxAssets.ts +0 -132
  87. package/src/hooks/useInView.ts +0 -41
  88. package/src/hooks/useMediaMetadata.ts +0 -104
  89. package/src/hooks/useMuxAssets.ts +0 -179
  90. package/src/hooks/useMuxPolling.ts +0 -52
  91. package/src/hooks/useResyncAsset.ts +0 -110
  92. package/src/hooks/useResyncMuxMetadata.ts +0 -169
  93. package/src/hooks/useSaveSecrets.ts +0 -78
  94. package/src/hooks/useSecretsDocumentValues.ts +0 -38
  95. package/src/hooks/useSecretsFormState.ts +0 -47
  96. package/src/plugin.tsx +0 -31
  97. package/src/sanity-ui.d.ts +0 -5
  98. package/src/schema.ts +0 -196
  99. package/src/util/addKeysToMuxData.ts +0 -30
  100. package/src/util/asserters.ts +0 -23
  101. package/src/util/assetTitlePlaceholder.ts +0 -31
  102. package/src/util/constants.ts +0 -15
  103. package/src/util/convertWatermarkToMux.ts +0 -160
  104. package/src/util/createSearchFilter.ts +0 -76
  105. package/src/util/createUrlParamsObject.ts +0 -29
  106. package/src/util/extractFiles.ts +0 -67
  107. package/src/util/formatBytes.ts +0 -31
  108. package/src/util/formatDriveShareLink.ts +0 -64
  109. package/src/util/formatSeconds.ts +0 -48
  110. package/src/util/generateJwt.ts +0 -57
  111. package/src/util/getAnimatedPosterSrc.ts +0 -26
  112. package/src/util/getPlaybackPolicy.ts +0 -69
  113. package/src/util/getPosterSrc.ts +0 -28
  114. package/src/util/getVideoMetadata.ts +0 -23
  115. package/src/util/getVideoSrc.ts +0 -23
  116. package/src/util/parsers.ts +0 -5
  117. package/src/util/pluginVersion.ts +0 -5
  118. package/src/util/readSecrets.ts +0 -38
  119. package/src/util/roundPxString.ts +0 -16
  120. package/src/util/textTracks.ts +0 -222
  121. package/src/util/tryWithSuspend.ts +0 -22
  122. package/src/util/types.ts +0 -566
  123. package/v2-incompatible.js +0 -11
@@ -1,696 +0,0 @@
1
- import {DocumentVideoIcon, ErrorOutlineIcon, UploadIcon} from '@sanity/icons'
2
- import {Box, Button, Card, Dialog, Flex, Label, Radio, Stack, Text} from '@sanity/ui'
3
- import {uuid} from '@sanity/uuid'
4
- import LanguagesList from 'iso-639-1'
5
- import {memo, useEffect, useId, useReducer, useRef, useState} from 'react'
6
- import {FormField} from 'sanity'
7
-
8
- import {useFetchFileSize} from '../hooks/useFetchFileSize'
9
- import {useMediaMetadata, type VideoAssetMetadata} from '../hooks/useMediaMetadata'
10
- import {convertWatermarkToMuxOverlay} from '../util/convertWatermarkToMux'
11
- import formatBytes from '../util/formatBytes'
12
- import {formatSeconds} from '../util/formatSeconds'
13
- import {
14
- type AutogeneratedTextTrack,
15
- type CustomTextTrack,
16
- isAutogeneratedTrack,
17
- isCustomTextTrack,
18
- type MuxNewAssetSettings,
19
- type PluginConfig,
20
- type Secrets,
21
- type StaticRenditionResolution,
22
- type SupportedMuxLanguage,
23
- type UploadConfig,
24
- type UploadTextTrack,
25
- type WatermarkConfig,
26
- } from '../util/types'
27
- import DraggableWatermark, {WatermarkControls} from './DraggableWatermark'
28
- import TextTracksEditor, {type TrackAction} from './TextTracksEditor'
29
- import PlaybackPolicy from './uploadConfiguration/PlaybackPolicy'
30
- import {
31
- RESOLUTION_TIERS,
32
- ResolutionTierSelector,
33
- } from './uploadConfiguration/ResolutionTierSelector'
34
- import {StaticRenditionSelector} from './uploadConfiguration/StaticRenditionSelector'
35
- import type {StagedUpload} from './Uploader'
36
-
37
- export type UploadConfigurationStateAction =
38
- | {action: 'video_quality'; value: UploadConfig['video_quality']}
39
- | {action: 'max_resolution_tier'; value: UploadConfig['max_resolution_tier']}
40
- | {action: 'static_renditions'; value: UploadConfig['static_renditions']}
41
- | {action: 'normalize_audio'; value: UploadConfig['normalize_audio']}
42
- | {action: 'signed_policy'; value: UploadConfig['signed_policy']}
43
- | {action: 'public_policy'; value: UploadConfig['public_policy']}
44
- | {action: 'drm_policy'; value: UploadConfig['drm_policy']}
45
- | {action: 'watermark'; value: WatermarkConfig}
46
- | TrackAction
47
-
48
- const VIDEO_QUALITY_LEVELS = [
49
- {value: 'basic', label: 'Basic'},
50
- {value: 'plus', label: 'Plus'},
51
- {value: 'premium', label: 'Premium'},
52
- ] as const satisfies {value: UploadConfig['video_quality']; label: string}[]
53
-
54
- /**
55
- * Sanitizes static renditions configuration to ensure 'highest' is not mixed with specific resolutions.
56
- * If both are present, only 'highest' (and 'audio-only' if present) will be kept.
57
- */
58
- function sanitizeStaticRenditions(
59
- renditions: StaticRenditionResolution[],
60
- ): StaticRenditionResolution[] {
61
- const hasHighest = renditions.includes('highest')
62
- const hasSpecificResolutions = renditions.some((r) => r !== 'highest' && r !== 'audio-only')
63
-
64
- if (hasHighest && hasSpecificResolutions) {
65
- return renditions.filter((r) => r === 'highest' || r === 'audio-only')
66
- }
67
-
68
- return renditions
69
- }
70
-
71
- /**
72
- * The modal for configuring a staged upload. Handles triggering of the asset
73
- * upload, even if no modal needs to be shown.
74
- *
75
- * @returns
76
- */
77
- export default function UploadConfiguration({
78
- stagedUpload,
79
- secrets,
80
- pluginConfig,
81
- startUpload,
82
- onClose,
83
- }: {
84
- stagedUpload: StagedUpload
85
- secrets: Secrets
86
- pluginConfig: PluginConfig
87
- startUpload: (settings: MuxNewAssetSettings, watermark: WatermarkConfig | undefined) => void
88
- onClose: () => void
89
- }) {
90
- const id = useId()
91
- const [watermarkValidationError, setWatermarkValidationError] = useState<string | null>(null)
92
- const watermarkPreviewContainerRef = useRef<HTMLDivElement>(null)
93
- const watermarkPreviewVideoRef = useRef<HTMLVideoElement>(null)
94
- // oxlint-disable-next-line react/react-compiler
95
- const autoTextTracks = useRef<NonNullable<UploadConfig['text_tracks']>>(
96
- pluginConfig.video_quality === 'plus' && pluginConfig.defaultAutogeneratedSubtitleLang
97
- ? [
98
- {
99
- _id: uuid(),
100
- type: 'autogenerated',
101
- language_code: pluginConfig.defaultAutogeneratedSubtitleLang,
102
- name: LanguagesList.getNativeName(pluginConfig.defaultAutogeneratedSubtitleLang),
103
- } satisfies AutogeneratedTextTrack,
104
- ]
105
- : [],
106
- ).current
107
-
108
- const [config, dispatch] = useReducer(
109
- // oxlint-disable-next-line react/react-compiler
110
- (prev: UploadConfig, action: UploadConfigurationStateAction) => {
111
- switch (action.action) {
112
- case 'video_quality':
113
- // If video quality level switches to basic, remove plus-only features
114
- if (action.value === 'basic') {
115
- return Object.assign({}, prev, {
116
- video_quality: action.value,
117
- static_renditions: [],
118
- max_resolution_tier: '1080p',
119
- text_tracks: prev.text_tracks?.filter(({type}) => type !== 'autogenerated'),
120
- public_policy: true,
121
- signed_policy: false,
122
- drm_policy: false,
123
- })
124
- // If video quality level switches to plus, add back in default plus features
125
- }
126
- return Object.assign({}, prev, {
127
- video_quality: action.value,
128
- static_renditions: sanitizeStaticRenditions(pluginConfig.static_renditions || []),
129
- max_resolution_tier: pluginConfig.max_resolution_tier,
130
- text_tracks: [...autoTextTracks, ...(prev.text_tracks || [])],
131
- })
132
-
133
- case 'static_renditions':
134
- case 'max_resolution_tier':
135
- case 'normalize_audio':
136
- case 'signed_policy':
137
- return Object.assign({}, prev, {[action.action]: action.value})
138
- case 'public_policy':
139
- return Object.assign({}, prev, {[action.action]: action.value})
140
- case 'drm_policy':
141
- return Object.assign({}, prev, {[action.action]: action.value})
142
- case 'watermark':
143
- return Object.assign({}, prev, {watermark: action.value})
144
- // Updating individual tracks
145
- case 'track': {
146
- const text_tracks = [...prev.text_tracks]
147
- const target_track_i = text_tracks.findIndex(({_id}) => _id === action.id)
148
- switch (action.subAction) {
149
- case 'add':
150
- // Exit early if track already exists
151
- if (target_track_i !== -1) break
152
- text_tracks.push({
153
- _id: action.id,
154
- ...action.value,
155
- } as AutogeneratedTextTrack)
156
- break
157
- case 'update':
158
- if (target_track_i === -1) break
159
- text_tracks[target_track_i] = {
160
- ...text_tracks[target_track_i],
161
- ...action.value,
162
- } as UploadTextTrack
163
- break
164
- case 'delete':
165
- if (target_track_i === -1) break
166
- text_tracks.splice(target_track_i, 1)
167
- break
168
- }
169
- return Object.assign({}, prev, {text_tracks})
170
- }
171
- default:
172
- return prev
173
- }
174
- },
175
- {
176
- video_quality: pluginConfig.video_quality,
177
- max_resolution_tier: pluginConfig.max_resolution_tier,
178
- static_renditions: sanitizeStaticRenditions(pluginConfig.static_renditions || []),
179
- signed_policy: secrets.enableSignedUrls && pluginConfig.defaultSigned,
180
- public_policy: pluginConfig.defaultPublic,
181
- drm_policy: pluginConfig.defaultDrm && !!secrets.drmConfigId,
182
- normalize_audio: pluginConfig.normalize_audio,
183
- text_tracks: autoTextTracks,
184
- } as UploadConfig,
185
- )
186
-
187
- // Video validations
188
- const [validationError, setValidationError] = useState<string | null>(null)
189
- const MAX_FILE_SIZE = pluginConfig.maxAssetFileSize
190
- const MAX_DURATION_SECONDS = pluginConfig.maxAssetDuration
191
-
192
- const {fileSize, isLoadingFileSize, canSkipFileSizeValidation} = useFetchFileSize(
193
- stagedUpload,
194
- MAX_FILE_SIZE,
195
- )
196
- const {videoAssetMetadata, setVideoAssetMetadata, isLoadingMetadata} =
197
- useMediaMetadata(stagedUpload)
198
-
199
- useEffect(() => {
200
- if (fileSize) {
201
- setVideoAssetMetadata((old) => ({...old, size: fileSize}))
202
- }
203
- }, [fileSize, setVideoAssetMetadata])
204
-
205
- useEffect(() => {
206
- const validateDuration = (duration: number) => {
207
- if (MAX_DURATION_SECONDS && duration > MAX_DURATION_SECONDS) {
208
- setValidationError(
209
- `Video duration (${formatSeconds(duration)}) exceeds maximum allowed duration of ${formatSeconds(MAX_DURATION_SECONDS)}`,
210
- )
211
- return false
212
- }
213
- return true
214
- }
215
-
216
- const validateFileSize = (size: number): boolean => {
217
- if (MAX_FILE_SIZE === undefined || size <= MAX_FILE_SIZE) {
218
- return true
219
- }
220
-
221
- setValidationError(
222
- `File size (${formatBytes(size)}) exceeds maximum allowed size of ${formatBytes(MAX_FILE_SIZE)}`,
223
- )
224
- return false
225
- }
226
-
227
- const validateDrmAvailability = (isAudioOnly: boolean) => {
228
- if (config.drm_policy && isAudioOnly) {
229
- setValidationError('Audio-only asset cannot be DRM protected')
230
- return false
231
- }
232
- return true
233
- }
234
-
235
- let valid = true
236
- if (videoAssetMetadata?.size) {
237
- valid = valid && (canSkipFileSizeValidation || validateFileSize(videoAssetMetadata.size))
238
- }
239
- if (videoAssetMetadata?.duration) {
240
- valid = valid && validateDuration(videoAssetMetadata.duration)
241
- }
242
- if (videoAssetMetadata?.isAudioOnly != undefined) {
243
- valid = valid && validateDrmAvailability(videoAssetMetadata.isAudioOnly)
244
- }
245
- if (valid) {
246
- // oxlint-disable-next-line react/react-compiler
247
- setValidationError(null)
248
- }
249
- }, [
250
- MAX_FILE_SIZE,
251
- MAX_DURATION_SECONDS,
252
- canSkipFileSizeValidation,
253
- videoAssetMetadata?.duration,
254
- videoAssetMetadata?.size,
255
- videoAssetMetadata?.height,
256
- videoAssetMetadata?.width,
257
- videoAssetMetadata,
258
- config.drm_policy,
259
- validationError,
260
- ])
261
-
262
- // If user-provided config is disabled, begin the upload immediately with
263
- // the developer-specified values from the schema or config or defaults.
264
- // This can include auto-generated subtitles!
265
- const {disableTextTrackConfig, disableUploadConfig} = pluginConfig
266
- const skipConfig = disableTextTrackConfig && disableUploadConfig
267
- useEffect(() => {
268
- if (skipConfig) {
269
- const {settings, watermark} = formatUploadConfig(config, secrets, {
270
- videoAspectRatio: videoAssetMetadata?.aspectRatio,
271
- })
272
- startUpload(settings, watermark)
273
- }
274
- // eslint-disable-next-line react-hooks/exhaustive-deps
275
- }, [])
276
- if (skipConfig) return null
277
-
278
- const basicConfig = config.video_quality !== 'plus' && config.video_quality !== 'premium'
279
- const playbackPolicySelected = config.public_policy || config.signed_policy || config.drm_policy
280
- const maxSupportedResolution = RESOLUTION_TIERS.findIndex(
281
- (rt) => rt.value === pluginConfig.max_resolution_tier,
282
- )
283
- return (
284
- <Dialog
285
- animate
286
- open
287
- id="upload-configuration"
288
- zOffset={1000}
289
- width={1}
290
- header="Configure Mux Upload"
291
- onClose={onClose}
292
- >
293
- <Stack padding={4} space={2}>
294
- {(validationError || watermarkValidationError) && (
295
- <Card padding={3} tone="critical" radius={2} marginBottom={2}>
296
- <Flex gap={2} align="flex-start">
297
- <ErrorOutlineIcon width={20} height={20} />
298
- <Stack space={2}>
299
- <Text size={1} weight="semibold">
300
- Validation Error
301
- </Text>
302
- <Text size={1}>{validationError || watermarkValidationError}</Text>
303
- </Stack>
304
- </Flex>
305
- </Card>
306
- )}
307
- <Label size={3}>FILE TO UPLOAD</Label>
308
- <Card
309
- tone="transparent"
310
- border
311
- padding={3}
312
- paddingY={4}
313
- style={{borderRadius: '0.1865rem'}}
314
- >
315
- <Flex gap={2}>
316
- <DocumentVideoIcon fontSize="2em" />
317
- <Stack space={2}>
318
- <Text textOverflow="ellipsis" as="h2" size={3}>
319
- {stagedUpload.type === 'file' ? stagedUpload.files[0]!.name : stagedUpload.url}
320
- </Text>
321
- <Text as="p" size={1} muted>
322
- {stagedUpload.type === 'file'
323
- ? `Direct File Upload (${formatBytes(stagedUpload.files[0]!.size)})`
324
- : (() => {
325
- if (videoAssetMetadata?.size) {
326
- return `File From URL (${formatBytes(videoAssetMetadata.size)})`
327
- }
328
- if (isLoadingFileSize) {
329
- return 'File From URL (Loading size...)'
330
- }
331
- return 'File From URL (Unknown size)'
332
- })()}
333
- </Text>
334
- {stagedUpload.type === 'file' && (
335
- <Stack space={1}>
336
- {isLoadingMetadata && (
337
- <Text as="p" size={1} muted>
338
- Reading video metadata...
339
- </Text>
340
- )}
341
- {videoAssetMetadata?.duration && !validationError && (
342
- <Text as="p" size={1} muted>
343
- Duration: {formatSeconds(videoAssetMetadata.duration)}
344
- </Text>
345
- )}
346
- </Stack>
347
- )}
348
- </Stack>
349
- </Flex>
350
- </Card>
351
- {!disableUploadConfig && (
352
- <Stack space={3} paddingBottom={2}>
353
- <FormField
354
- path={[]}
355
- title="Video Quality Level"
356
- description={
357
- <>
358
- The video quality level informs the cost, quality, and available platform features
359
- for the asset.{' '}
360
- <a
361
- href="https://docs.mux.com/guides/use-encoding-tiers"
362
- target="_blank"
363
- rel="noopener noreferrer"
364
- >
365
- See the Mux guide for more details.
366
- </a>
367
- </>
368
- }
369
- >
370
- <Flex gap={3}>
371
- {VIDEO_QUALITY_LEVELS.map(({value, label}) => {
372
- const inputId = `${id}--encodingtier-${value}`
373
- return (
374
- <Flex key={value} align="center" gap={2}>
375
- <Radio
376
- checked={config.video_quality === value}
377
- name="asset-encodingtier"
378
- onChange={(e) =>
379
- dispatch({
380
- action: 'video_quality' as const,
381
- value: e.currentTarget.value as UploadConfig['video_quality'],
382
- })
383
- }
384
- value={value}
385
- id={inputId}
386
- />
387
- <Text as="label" htmlFor={inputId}>
388
- {label}
389
- </Text>
390
- </Flex>
391
- )
392
- })}
393
- </Flex>
394
- </FormField>
395
-
396
- {!basicConfig && (
397
- <>
398
- <FormField title="Additional Configuration" path={[]}>
399
- <Stack space={3}>
400
- <PlaybackPolicy id={id} config={config} secrets={secrets} dispatch={dispatch} />
401
- {maxSupportedResolution > 0 && (
402
- <ResolutionTierSelector
403
- id={id}
404
- config={config}
405
- dispatch={dispatch}
406
- maxSupportedResolution={maxSupportedResolution}
407
- />
408
- )}
409
- <StaticRenditionSelector id={id} config={config} dispatch={dispatch} />
410
- {!disableTextTrackConfig && (
411
- <TextTracksEditor
412
- tracks={config.text_tracks}
413
- dispatch={dispatch}
414
- defaultLang={pluginConfig.defaultAutogeneratedSubtitleLang}
415
- />
416
- )}
417
- </Stack>
418
- </FormField>
419
- <WatermarkSection
420
- config={config}
421
- dispatch={dispatch}
422
- stagedUpload={stagedUpload}
423
- videoAssetMetadata={videoAssetMetadata}
424
- watermarkPreviewContainerRef={watermarkPreviewContainerRef}
425
- watermarkPreviewVideoRef={watermarkPreviewVideoRef}
426
- onValidationChange={setWatermarkValidationError}
427
- />
428
- </>
429
- )}
430
- </Stack>
431
- )}
432
-
433
- <Box marginTop={4}>
434
- <Button
435
- disabled={
436
- (!basicConfig && !playbackPolicySelected) ||
437
- validationError !== null ||
438
- isLoadingMetadata ||
439
- (isLoadingFileSize && !canSkipFileSizeValidation)
440
- }
441
- icon={UploadIcon}
442
- text="Upload"
443
- tone="positive"
444
- onClick={() => {
445
- if (!validationError) {
446
- const {settings, watermark} = formatUploadConfig(config, secrets, {
447
- videoAspectRatio: videoAssetMetadata?.aspectRatio,
448
- })
449
- startUpload(settings, watermark)
450
- }
451
- }}
452
- />
453
- </Box>
454
- </Stack>
455
- </Dialog>
456
- )
457
- }
458
-
459
- function setAdvancedPlaybackPolicy(
460
- config: UploadConfig,
461
- secrets: Secrets,
462
- ): MuxNewAssetSettings['advanced_playback_policies'] {
463
- const advanced_playback_policies: MuxNewAssetSettings['advanced_playback_policies'] = []
464
- if (config.public_policy) {
465
- advanced_playback_policies.push({policy: 'public'})
466
- }
467
- if (config.signed_policy) {
468
- advanced_playback_policies.push({policy: 'signed'})
469
- }
470
- if (config.drm_policy) {
471
- if (secrets.drmConfigId)
472
- advanced_playback_policies.push({
473
- policy: 'drm',
474
- drm_configuration_id: secrets.drmConfigId ?? undefined,
475
- })
476
- else {
477
- console.error('Selected DRM Policy but missing DRM Configuration Id')
478
- }
479
- }
480
- return advanced_playback_policies
481
- }
482
-
483
- function formatUploadConfig(
484
- config: UploadConfig,
485
- secrets: Secrets,
486
- options?: {videoAspectRatio?: number | null},
487
- ): {
488
- settings: MuxNewAssetSettings
489
- watermark?: WatermarkConfig
490
- } {
491
- const generated_subtitles = config.text_tracks
492
- .filter<AutogeneratedTextTrack>(isAutogeneratedTrack)
493
- .map<{name: string; language_code: SupportedMuxLanguage}>((track) => ({
494
- name: track.name,
495
- language_code: track.language_code,
496
- }))
497
-
498
- const inputs: NonNullable<MuxNewAssetSettings['input']> = [
499
- {
500
- type: 'video',
501
- generated_subtitles: generated_subtitles.length > 0 ? generated_subtitles : undefined,
502
- },
503
- ...config.text_tracks.filter<CustomTextTrack>(isCustomTextTrack).reduce(
504
- (acc, track) => {
505
- if (track.language_code && track.file && track.name) {
506
- acc.push({
507
- url: track.file.contents,
508
- type: 'text',
509
- text_type: track.type === 'subtitles' ? 'subtitles' : undefined,
510
- language_code: track.language_code,
511
- name: track.name,
512
- closed_captions: track.type === 'captions',
513
- })
514
- }
515
- return acc
516
- },
517
- [] as NonNullable<MuxNewAssetSettings['input']>,
518
- ),
519
- ]
520
-
521
- if (config.watermark?.imageUrl) {
522
- const watermarkForMux: WatermarkConfig = {...config.watermark, enabled: true}
523
- const overlaySettings = convertWatermarkToMuxOverlay(watermarkForMux, {
524
- videoAspectRatio: options?.videoAspectRatio ?? undefined,
525
- units: 'px',
526
- })
527
- if (overlaySettings) {
528
- inputs.push({
529
- url: config.watermark.imageUrl,
530
- overlay_settings: overlaySettings,
531
- } as NonNullable<MuxNewAssetSettings['input']>[number])
532
- }
533
- }
534
-
535
- return {
536
- settings: {
537
- input: inputs,
538
- static_renditions:
539
- config.static_renditions.length > 0
540
- ? config.static_renditions.map((resolution) => ({resolution}))
541
- : undefined,
542
- advanced_playback_policies: setAdvancedPlaybackPolicy(config, secrets),
543
- max_resolution_tier: config.max_resolution_tier,
544
- video_quality: config.video_quality,
545
- normalize_audio: config.normalize_audio,
546
- },
547
- watermark: config.watermark?.imageUrl
548
- ? ({...config.watermark, enabled: true} as WatermarkConfig)
549
- : undefined,
550
- }
551
- }
552
-
553
- function WatermarkSection({
554
- config,
555
- dispatch,
556
- stagedUpload,
557
- videoAssetMetadata,
558
- watermarkPreviewContainerRef,
559
- watermarkPreviewVideoRef,
560
- onValidationChange,
561
- }: {
562
- config: UploadConfig
563
- dispatch: (action: UploadConfigurationStateAction) => void
564
- stagedUpload: StagedUpload
565
- videoAssetMetadata: VideoAssetMetadata | null
566
- watermarkPreviewContainerRef: React.RefObject<HTMLDivElement | null>
567
- watermarkPreviewVideoRef: React.RefObject<HTMLVideoElement | null>
568
- onValidationChange: (error: string | null) => void
569
- }) {
570
- if (videoAssetMetadata?.isAudioOnly !== false) return null
571
- return (
572
- <FormField
573
- path={[]}
574
- title="Watermark"
575
- description={
576
- <>
577
- Add a watermark overlay to your video using Mux&apos;s native watermark support.{' '}
578
- <a
579
- href="https://www.mux.com/docs/guides/add-watermarks-to-your-videos"
580
- target="_blank"
581
- rel="noopener noreferrer"
582
- >
583
- Learn more about Mux watermarks.
584
- </a>
585
- </>
586
- }
587
- >
588
- <Stack space={3}>
589
- <WatermarkControls
590
- watermark={config.watermark || {enabled: false}}
591
- onChange={(watermark) => {
592
- dispatch({action: 'watermark', value: watermark})
593
- }}
594
- onValidationChange={onValidationChange}
595
- previewContainerRef={watermarkPreviewContainerRef}
596
- previewVideoRef={watermarkPreviewVideoRef}
597
- />
598
- {config.watermark?.imageUrl &&
599
- stagedUpload.type === 'file' &&
600
- // Canvas preview is only shown in "Canvas" mode (no explicit overlay_settings)
601
- !config.watermark.overlay_settings && (
602
- <WatermarkPreview
603
- stagedUpload={stagedUpload}
604
- watermark={config.watermark}
605
- videoAspectRatio={videoAssetMetadata.aspectRatio}
606
- onWatermarkChange={(watermark) => {
607
- dispatch({action: 'watermark', value: watermark})
608
- }}
609
- previewContainerRef={watermarkPreviewContainerRef}
610
- videoRef={watermarkPreviewVideoRef}
611
- />
612
- )}
613
- </Stack>
614
- </FormField>
615
- )
616
- }
617
-
618
- // Memoized preview component to prevent unnecessary re-renders
619
- const WatermarkPreview = memo(function WatermarkPreview({
620
- stagedUpload,
621
- watermark,
622
- onWatermarkChange,
623
- videoAspectRatio,
624
- previewContainerRef,
625
- videoRef,
626
- }: {
627
- stagedUpload: StagedUpload
628
- watermark: WatermarkConfig
629
- onWatermarkChange: (watermark: WatermarkConfig) => void
630
- videoAspectRatio?: number | null
631
- previewContainerRef: React.RefObject<HTMLDivElement | null>
632
- videoRef: React.RefObject<HTMLVideoElement | null>
633
- }) {
634
- // Initialize video source only once
635
- useEffect(() => {
636
- if (videoRef.current && stagedUpload.type === 'file') {
637
- const file = stagedUpload.files[0]
638
- const url = URL.createObjectURL(file!)
639
- videoRef.current.src = url
640
-
641
- return () => {
642
- URL.revokeObjectURL(url)
643
- }
644
- }
645
- return undefined
646
- }, [stagedUpload, videoRef])
647
-
648
- const isVertical =
649
- videoAspectRatio !== null && videoAspectRatio !== undefined && videoAspectRatio < 1
650
-
651
- return (
652
- <Card
653
- tone="transparent"
654
- border
655
- style={{
656
- overflow: 'hidden',
657
- // For vertical videos, center the preview and limit its width
658
- display: 'flex',
659
- justifyContent: 'center',
660
- }}
661
- >
662
- {/* Inner container that exactly matches the video aspect ratio - no padding, no letterbox */}
663
- <div
664
- ref={previewContainerRef}
665
- style={{
666
- position: 'relative',
667
- // For vertical videos: limit width so the preview doesn't get too tall
668
- // For horizontal videos: use full width
669
- width: isVertical ? 'auto' : '100%',
670
- aspectRatio: videoAspectRatio ? String(videoAspectRatio) : '16/9',
671
- ...(isVertical ? {height: '400px', maxHeight: '50vh'} : {minHeight: '200px'}),
672
- overflow: 'hidden',
673
- }}
674
- >
675
- <video
676
- ref={videoRef}
677
- style={{
678
- position: 'absolute',
679
- top: 0,
680
- left: 0,
681
- width: '100%',
682
- height: '100%',
683
- objectFit: 'fill',
684
- display: 'block',
685
- }}
686
- />
687
- <DraggableWatermark
688
- watermark={watermark}
689
- onChange={onWatermarkChange}
690
- containerRef={previewContainerRef as React.RefObject<HTMLDivElement>}
691
- videoElementRef={videoRef as React.RefObject<HTMLVideoElement>}
692
- />
693
- </div>
694
- </Card>
695
- )
696
- })