sanity-plugin-mux-input 3.0.5 → 4.0.1
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/dist/index.js +20 -92
- package/dist/index.js.map +1 -1
- package/package.json +5 -15
- package/dist/index.cjs +0 -5746
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -288
- package/dist/index.d.cts.map +0 -1
- package/sanity.json +0 -8
- package/src/_exports/index.ts +0 -73
- package/src/actions/assets.ts +0 -152
- package/src/actions/secrets.ts +0 -110
- package/src/actions/upload.ts +0 -308
- package/src/clients/upChunkObservable.ts +0 -54
- package/src/components/AddCaptionDialog.tsx +0 -440
- package/src/components/CaptionsDialog.tsx +0 -23
- package/src/components/ConfigureApi.styled.tsx +0 -19
- package/src/components/ConfigureApi.tsx +0 -296
- package/src/components/DraggableWatermark.tsx +0 -885
- package/src/components/EditCaptionDialog.tsx +0 -511
- package/src/components/EditThumbnailDialog.tsx +0 -121
- package/src/components/ErrorBoundaryCard.tsx +0 -97
- package/src/components/FileInputButton.tsx +0 -54
- package/src/components/FileInputMenuItem.styled.tsx +0 -36
- package/src/components/FileInputMenuItem.tsx +0 -85
- package/src/components/FormField.tsx +0 -38
- package/src/components/IconInfo.tsx +0 -22
- package/src/components/ImportVideosFromMux.tsx +0 -339
- package/src/components/Input.styled.tsx +0 -22
- package/src/components/Input.tsx +0 -78
- package/src/components/InputBrowser.tsx +0 -41
- package/src/components/MuxLogo.tsx +0 -42
- package/src/components/Onboard.tsx +0 -65
- package/src/components/PageSelector.tsx +0 -54
- package/src/components/Player.styled.tsx +0 -11
- package/src/components/Player.tsx +0 -117
- package/src/components/PlayerActionsMenu.tsx +0 -191
- package/src/components/ResyncMetadata.tsx +0 -278
- package/src/components/SelectAsset.tsx +0 -39
- package/src/components/SelectSortOptions.tsx +0 -45
- package/src/components/SpinnerBox.tsx +0 -16
- package/src/components/StudioTool.tsx +0 -24
- package/src/components/TextTracksEditor.tsx +0 -117
- package/src/components/TextTracksManager.tsx +0 -738
- package/src/components/UploadConfiguration.tsx +0 -696
- package/src/components/UploadPlaceholder.tsx +0 -88
- package/src/components/UploadProgress.tsx +0 -80
- package/src/components/Uploader.styled.tsx +0 -65
- package/src/components/Uploader.tsx +0 -499
- package/src/components/VideoDetails/DeleteDialog.tsx +0 -148
- package/src/components/VideoDetails/VideoDetails.tsx +0 -358
- package/src/components/VideoDetails/VideoReferences.tsx +0 -63
- package/src/components/VideoDetails/useVideoDetails.ts +0 -103
- package/src/components/VideoInBrowser.tsx +0 -245
- package/src/components/VideoMetadata.tsx +0 -45
- package/src/components/VideoPlayer.tsx +0 -241
- package/src/components/VideoThumbnail.tsx +0 -139
- package/src/components/VideosBrowser.tsx +0 -100
- package/src/components/documentPreview/DocumentPreview.tsx +0 -84
- package/src/components/documentPreview/DraftStatus.tsx +0 -34
- package/src/components/documentPreview/MissingSchemaType.tsx +0 -32
- package/src/components/documentPreview/PaneItemPreview.tsx +0 -67
- package/src/components/documentPreview/PublishedStatus.tsx +0 -35
- package/src/components/documentPreview/TimeAgo.tsx +0 -12
- package/src/components/icons/Audio.tsx +0 -13
- package/src/components/icons/Resolution.tsx +0 -12
- package/src/components/icons/StopWatch.tsx +0 -20
- package/src/components/icons/ToolIcon.tsx +0 -19
- package/src/components/uploadConfiguration/PlaybackPolicy.tsx +0 -133
- package/src/components/uploadConfiguration/PlaybackPolicyOption.tsx +0 -76
- package/src/components/uploadConfiguration/PlaybackPolicyWarning.tsx +0 -29
- package/src/components/uploadConfiguration/ResolutionTierSelector.tsx +0 -72
- package/src/components/uploadConfiguration/StaticRenditionSelector.tsx +0 -180
- package/src/components/withFocusRing/helpers.ts +0 -24
- package/src/components/withFocusRing/index.ts +0 -1
- package/src/components/withFocusRing/withFocusRing.ts +0 -30
- package/src/context/DialogStateContext.tsx +0 -33
- package/src/context/DrmPlaybackWarningContext.tsx +0 -97
- package/src/hooks/useAccessControl.ts +0 -13
- package/src/hooks/useAssetDocumentValues.ts +0 -11
- package/src/hooks/useAssets.ts +0 -73
- package/src/hooks/useCancelUpload.ts +0 -22
- package/src/hooks/useClient.ts +0 -8
- package/src/hooks/useDialogState.ts +0 -11
- package/src/hooks/useDocReferences.ts +0 -21
- package/src/hooks/useFetchFileSize.ts +0 -55
- package/src/hooks/useImportMuxAssets.ts +0 -132
- package/src/hooks/useInView.ts +0 -41
- package/src/hooks/useMediaMetadata.ts +0 -104
- package/src/hooks/useMuxAssets.ts +0 -179
- package/src/hooks/useMuxPolling.ts +0 -52
- package/src/hooks/useResyncAsset.ts +0 -110
- package/src/hooks/useResyncMuxMetadata.ts +0 -169
- package/src/hooks/useSaveSecrets.ts +0 -78
- package/src/hooks/useSecretsDocumentValues.ts +0 -38
- package/src/hooks/useSecretsFormState.ts +0 -47
- package/src/plugin.tsx +0 -31
- package/src/sanity-ui.d.ts +0 -5
- package/src/schema.ts +0 -196
- package/src/util/addKeysToMuxData.ts +0 -30
- package/src/util/asserters.ts +0 -23
- package/src/util/assetTitlePlaceholder.ts +0 -31
- package/src/util/constants.ts +0 -15
- package/src/util/convertWatermarkToMux.ts +0 -160
- package/src/util/createSearchFilter.ts +0 -76
- package/src/util/createUrlParamsObject.ts +0 -29
- package/src/util/extractFiles.ts +0 -67
- package/src/util/formatBytes.ts +0 -31
- package/src/util/formatDriveShareLink.ts +0 -64
- package/src/util/formatSeconds.ts +0 -48
- package/src/util/generateJwt.ts +0 -57
- package/src/util/getAnimatedPosterSrc.ts +0 -26
- package/src/util/getPlaybackPolicy.ts +0 -69
- package/src/util/getPosterSrc.ts +0 -28
- package/src/util/getVideoMetadata.ts +0 -23
- package/src/util/getVideoSrc.ts +0 -23
- package/src/util/parsers.ts +0 -5
- package/src/util/pluginVersion.ts +0 -5
- package/src/util/readSecrets.ts +0 -38
- package/src/util/roundPxString.ts +0 -16
- package/src/util/textTracks.ts +0 -222
- package/src/util/tryWithSuspend.ts +0 -22
- package/src/util/types.ts +0 -566
- 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'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
|
-
})
|