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