sanity-plugin-mux-input 2.13.0 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -24
- package/dist/index.d.mts +35 -2
- package/dist/index.d.ts +35 -2
- package/dist/index.js +2176 -461
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2178 -463
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/_exports/index.ts +1 -0
- package/src/actions/assets.ts +75 -0
- package/src/actions/secrets.ts +6 -1
- package/src/actions/upload.ts +1 -1
- package/src/components/AddCaptionDialog.tsx +421 -0
- package/src/components/CaptionsDialog.tsx +23 -0
- package/src/components/ConfigureApi.tsx +51 -5
- package/src/components/EditCaptionDialog.tsx +508 -0
- package/src/components/InputBrowser.tsx +8 -2
- package/src/components/Onboard.tsx +2 -2
- package/src/components/PageSelector.tsx +54 -0
- package/src/components/Player.styled.tsx +7 -2
- package/src/components/PlayerActionsMenu.tsx +14 -6
- package/src/components/SelectAsset.tsx +9 -3
- package/src/components/StudioTool.tsx +2 -2
- package/src/components/TextTracksManager.tsx +781 -0
- package/src/components/UploadConfiguration.tsx +104 -343
- package/src/components/Uploader.styled.tsx +8 -15
- package/src/components/Uploader.tsx +25 -7
- package/src/components/VideoDetails/VideoDetails.tsx +43 -7
- package/src/components/VideoInBrowser.tsx +53 -6
- package/src/components/VideoPlayer.tsx +122 -47
- package/src/components/VideoThumbnail.tsx +84 -72
- package/src/components/VideosBrowser.tsx +15 -5
- package/src/components/uploadConfiguration/PlaybackPolicy.tsx +95 -6
- package/src/components/uploadConfiguration/PlaybackPolicyOption.tsx +26 -10
- package/src/components/uploadConfiguration/ResolutionTierSelector.tsx +71 -0
- package/src/components/uploadConfiguration/StaticRenditionSelector.tsx +179 -0
- package/src/context/DrmPlaybackWarningContext.tsx +93 -0
- package/src/hooks/useAccessControl.ts +1 -0
- package/src/hooks/useDialogState.ts +1 -1
- package/src/hooks/useFetchFileSize.ts +54 -0
- package/src/hooks/useMediaMetadata.ts +100 -0
- package/src/hooks/useSaveSecrets.ts +10 -3
- package/src/hooks/useSecretsDocumentValues.ts +9 -1
- package/src/hooks/useSecretsFormState.ts +6 -3
- package/src/util/asserters.ts +14 -0
- package/src/util/createUrlParamsObject.ts +7 -3
- package/src/util/generateJwt.ts +11 -2
- package/src/util/getPlaybackPolicy.ts +63 -4
- package/src/util/getStoryboardSrc.ts +7 -3
- package/src/util/getVideoMetadata.ts +4 -1
- package/src/util/getVideoSrc.ts +9 -9
- package/src/util/readSecrets.ts +3 -1
- package/src/util/textTracks.ts +222 -0
- package/src/util/tryWithSuspend.ts +22 -0
- package/src/util/types.ts +39 -6
- package/src/util/getPlaybackId.ts +0 -9
package/package.json
CHANGED
package/src/_exports/index.ts
CHANGED
package/src/actions/assets.ts
CHANGED
|
@@ -69,3 +69,78 @@ export function listAssets(
|
|
|
69
69
|
query,
|
|
70
70
|
})
|
|
71
71
|
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Adds a new text track to an existing asset using a VTT file URL
|
|
75
|
+
*/
|
|
76
|
+
export function addTextTrackFromUrl(
|
|
77
|
+
client: SanityClient,
|
|
78
|
+
assetId: string,
|
|
79
|
+
vttUrl: string,
|
|
80
|
+
options: {
|
|
81
|
+
language_code: string
|
|
82
|
+
name: string
|
|
83
|
+
text_type?: 'subtitles'
|
|
84
|
+
}
|
|
85
|
+
) {
|
|
86
|
+
const {dataset} = client.config()
|
|
87
|
+
|
|
88
|
+
return client.request<{data: MuxAsset}>({
|
|
89
|
+
url: `/addons/mux/assets/${dataset}/${assetId}/tracks`,
|
|
90
|
+
withCredentials: true,
|
|
91
|
+
method: 'POST',
|
|
92
|
+
body: {
|
|
93
|
+
url: vttUrl,
|
|
94
|
+
type: 'text',
|
|
95
|
+
language_code: options.language_code,
|
|
96
|
+
name: options.name,
|
|
97
|
+
text_type: options.text_type || 'subtitles',
|
|
98
|
+
},
|
|
99
|
+
headers: {
|
|
100
|
+
'Content-Type': 'application/json',
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generates subtitles automatically for an audio track
|
|
107
|
+
*/
|
|
108
|
+
export function generateSubtitles(
|
|
109
|
+
client: SanityClient,
|
|
110
|
+
assetId: string,
|
|
111
|
+
audioTrackId: string,
|
|
112
|
+
options: {
|
|
113
|
+
language_code: string
|
|
114
|
+
name: string
|
|
115
|
+
}
|
|
116
|
+
) {
|
|
117
|
+
const {dataset} = client.config()
|
|
118
|
+
return client.request<{data: MuxAsset}>({
|
|
119
|
+
url: `/addons/mux/assets/${dataset}/${assetId}/tracks/${audioTrackId}/generate-subtitles`,
|
|
120
|
+
withCredentials: true,
|
|
121
|
+
method: 'POST',
|
|
122
|
+
body: {
|
|
123
|
+
generated_subtitles: [
|
|
124
|
+
{
|
|
125
|
+
language_code: options.language_code,
|
|
126
|
+
name: options.name,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
headers: {
|
|
131
|
+
'Content-Type': 'application/json',
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Deletes a text track from an asset
|
|
138
|
+
*/
|
|
139
|
+
export function deleteTextTrack(client: SanityClient, assetId: string, trackId: string) {
|
|
140
|
+
const {dataset} = client.config()
|
|
141
|
+
return client.request<{data: MuxAsset}>({
|
|
142
|
+
url: `/addons/mux/assets/${dataset}/${assetId}/tracks/${trackId}`,
|
|
143
|
+
withCredentials: true,
|
|
144
|
+
method: 'DELETE',
|
|
145
|
+
})
|
|
146
|
+
}
|
package/src/actions/secrets.ts
CHANGED
|
@@ -9,6 +9,7 @@ interface SecretsDocument {
|
|
|
9
9
|
enableSignedUrls: boolean
|
|
10
10
|
signingKeyId: string
|
|
11
11
|
signingKeyPrivate: string
|
|
12
|
+
drmConfigId: string
|
|
12
13
|
}
|
|
13
14
|
// eslint-disable-next-line max-params
|
|
14
15
|
export function saveSecrets(
|
|
@@ -17,7 +18,8 @@ export function saveSecrets(
|
|
|
17
18
|
secretKey: string,
|
|
18
19
|
enableSignedUrls: boolean,
|
|
19
20
|
signingKeyId: string,
|
|
20
|
-
signingKeyPrivate: string
|
|
21
|
+
signingKeyPrivate: string,
|
|
22
|
+
drmConfigId: string
|
|
21
23
|
): Promise<SecretsDocument> {
|
|
22
24
|
const doc: SecretsDocument = {
|
|
23
25
|
_id: 'secrets.mux',
|
|
@@ -27,7 +29,10 @@ export function saveSecrets(
|
|
|
27
29
|
enableSignedUrls,
|
|
28
30
|
signingKeyId,
|
|
29
31
|
signingKeyPrivate,
|
|
32
|
+
drmConfigId,
|
|
30
33
|
}
|
|
34
|
+
doc.signingKeyId = enableSignedUrls ? signingKeyId : ''
|
|
35
|
+
doc.signingKeyPrivate = enableSignedUrls ? signingKeyPrivate : ''
|
|
31
36
|
|
|
32
37
|
return client.createOrReplace(doc)
|
|
33
38
|
}
|
package/src/actions/upload.ts
CHANGED
|
@@ -158,7 +158,7 @@ type UploadResponse = {
|
|
|
158
158
|
new_asset_settings: {
|
|
159
159
|
static_renditions?: {resolution: string}[]
|
|
160
160
|
passthrough: string
|
|
161
|
-
playback_policies: ['public' | 'signed']
|
|
161
|
+
playback_policies: ['public' | 'signed' | 'drm']
|
|
162
162
|
}
|
|
163
163
|
status: string
|
|
164
164
|
timeout: number
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import {TranslateIcon, UploadIcon} from '@sanity/icons'
|
|
2
|
+
import {
|
|
3
|
+
Autocomplete,
|
|
4
|
+
Button,
|
|
5
|
+
Card,
|
|
6
|
+
Checkbox,
|
|
7
|
+
Dialog,
|
|
8
|
+
Flex,
|
|
9
|
+
Label,
|
|
10
|
+
Spinner,
|
|
11
|
+
Stack,
|
|
12
|
+
Text,
|
|
13
|
+
TextInput,
|
|
14
|
+
useToast,
|
|
15
|
+
} from '@sanity/ui'
|
|
16
|
+
import LanguagesList from 'iso-639-1'
|
|
17
|
+
import {useId, useRef, useState} from 'react'
|
|
18
|
+
|
|
19
|
+
import {addTextTrackFromUrl, generateSubtitles, getAsset} from '../actions/assets'
|
|
20
|
+
import {useClient} from '../hooks/useClient'
|
|
21
|
+
import {extractErrorMessage, pollTrackStatus} from '../util/textTracks'
|
|
22
|
+
import {type MuxTextTrack, SUPPORTED_MUX_LANGUAGES, type VideoAssetDocument} from '../util/types'
|
|
23
|
+
|
|
24
|
+
const LANGUAGE_OPTIONS = LanguagesList.getAllCodes().map((code) => ({
|
|
25
|
+
value: code,
|
|
26
|
+
label: LanguagesList.getNativeName(code),
|
|
27
|
+
}))
|
|
28
|
+
|
|
29
|
+
const MUX_LANGUAGE_OPTIONS = SUPPORTED_MUX_LANGUAGES.map((lang) => ({
|
|
30
|
+
value: lang.code,
|
|
31
|
+
label: lang.label,
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
export interface Props {
|
|
35
|
+
asset: VideoAssetDocument
|
|
36
|
+
onAdd: (track: MuxTextTrack) => void
|
|
37
|
+
onClose: () => void
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
41
|
+
const client = useClient()
|
|
42
|
+
const toast = useToast()
|
|
43
|
+
const dialogId = `AddCaptionDialog${useId()}`
|
|
44
|
+
|
|
45
|
+
const [isAutogenerated, setIsAutogenerated] = useState(false)
|
|
46
|
+
const [vttUrl, setVttUrl] = useState('')
|
|
47
|
+
const [languageCode, setLanguageCode] = useState('')
|
|
48
|
+
const [selectedLanguage, setSelectedLanguage] = useState<{value: string; label: string} | null>(
|
|
49
|
+
null
|
|
50
|
+
)
|
|
51
|
+
const [name, setName] = useState('')
|
|
52
|
+
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
53
|
+
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
|
54
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
55
|
+
|
|
56
|
+
const uploadVttFile = async (file: File): Promise<string> => {
|
|
57
|
+
const assetDocument = await client.assets.upload('file', file, {
|
|
58
|
+
filename: file.name,
|
|
59
|
+
})
|
|
60
|
+
return assetDocument.url
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const handleAddTrackFromUrl = async () => {
|
|
64
|
+
if (!asset.assetId) {
|
|
65
|
+
throw new Error('Asset ID is required')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const trimmedName = name.trim()
|
|
69
|
+
const trimmedLanguageCode = languageCode.trim()
|
|
70
|
+
|
|
71
|
+
let vttUrlToUse = vttUrl.trim()
|
|
72
|
+
|
|
73
|
+
if (selectedFile) {
|
|
74
|
+
try {
|
|
75
|
+
vttUrlToUse = await uploadVttFile(selectedFile)
|
|
76
|
+
} catch (uploadError) {
|
|
77
|
+
toast.push({
|
|
78
|
+
title: 'Failed to upload VTT file',
|
|
79
|
+
status: 'error',
|
|
80
|
+
description: 'Could not upload the VTT file to Sanity. Please try again.',
|
|
81
|
+
})
|
|
82
|
+
setIsSubmitting(false)
|
|
83
|
+
throw uploadError
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await addTextTrackFromUrl(client, asset.assetId, vttUrlToUse, {
|
|
88
|
+
language_code: trimmedLanguageCode,
|
|
89
|
+
name: trimmedName,
|
|
90
|
+
text_type: 'subtitles',
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const result = await pollTrackStatus({
|
|
94
|
+
client,
|
|
95
|
+
assetId: asset.assetId,
|
|
96
|
+
trackName: trimmedName,
|
|
97
|
+
trackLanguageCode: trimmedLanguageCode,
|
|
98
|
+
onTrackErrored: (track) => {
|
|
99
|
+
const errorMessage =
|
|
100
|
+
track.error?.messages?.[0] ||
|
|
101
|
+
track.error?.type ||
|
|
102
|
+
'The track failed to download from the provided URL'
|
|
103
|
+
toast.push({
|
|
104
|
+
title: 'Caption track failed',
|
|
105
|
+
status: 'error',
|
|
106
|
+
description: errorMessage,
|
|
107
|
+
})
|
|
108
|
+
onAdd(track)
|
|
109
|
+
onClose()
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
if (!result.found || !result.track) {
|
|
114
|
+
toast.push({
|
|
115
|
+
title: 'Caption track may have been added',
|
|
116
|
+
status: 'warning',
|
|
117
|
+
description:
|
|
118
|
+
'The track was created but its status could not be determined. It may still be processing. Please refresh the page to see if it appears.',
|
|
119
|
+
})
|
|
120
|
+
onClose()
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (result.status === 'errored') {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (result.status === 'preparing') {
|
|
129
|
+
toast.push({
|
|
130
|
+
title: 'Caption track is processing',
|
|
131
|
+
status: 'info',
|
|
132
|
+
description:
|
|
133
|
+
'The track was created and is being processed. It will appear in the list shortly.',
|
|
134
|
+
})
|
|
135
|
+
onAdd(result.track)
|
|
136
|
+
onClose()
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
toast.push({
|
|
141
|
+
title: 'Caption track added',
|
|
142
|
+
status: 'success',
|
|
143
|
+
description: 'Caption track added successfully',
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
onAdd(result.track)
|
|
147
|
+
onClose()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const handleGenerateSubtitles = async () => {
|
|
151
|
+
if (!asset.assetId) {
|
|
152
|
+
throw new Error('Asset ID is required')
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const assetData = await getAsset(client, asset.assetId)
|
|
156
|
+
const audioTrack = assetData.data.tracks?.find((track) => track.type === 'audio')
|
|
157
|
+
|
|
158
|
+
if (!audioTrack || !audioTrack.id) {
|
|
159
|
+
toast.push({
|
|
160
|
+
title: 'No audio track found',
|
|
161
|
+
status: 'error',
|
|
162
|
+
description:
|
|
163
|
+
'The asset does not have an audio track. Auto-generated subtitles require an audio track.',
|
|
164
|
+
})
|
|
165
|
+
throw new Error('No audio track found')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await generateSubtitles(client, asset.assetId, audioTrack.id, {
|
|
169
|
+
language_code: languageCode.trim(),
|
|
170
|
+
name: name.trim(),
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const mockTrackId = `generating-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`
|
|
174
|
+
const mockTrack: MuxTextTrack = {
|
|
175
|
+
type: 'text',
|
|
176
|
+
id: mockTrackId,
|
|
177
|
+
text_type: 'subtitles',
|
|
178
|
+
text_source: 'generated_live',
|
|
179
|
+
language_code: languageCode.trim(),
|
|
180
|
+
name: name.trim(),
|
|
181
|
+
status: 'preparing',
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
toast.push({
|
|
185
|
+
title: 'Generating subtitles',
|
|
186
|
+
status: 'success',
|
|
187
|
+
description: 'This may take a few minutes',
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
onAdd(mockTrack)
|
|
191
|
+
onClose()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const handleSubmit = async () => {
|
|
195
|
+
if (!isAutogenerated) {
|
|
196
|
+
if (!selectedFile && !vttUrl.trim()) {
|
|
197
|
+
toast.push({
|
|
198
|
+
title: 'VTT file or URL required',
|
|
199
|
+
status: 'error',
|
|
200
|
+
description: 'Please select a VTT file or enter a VTT file URL',
|
|
201
|
+
})
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (vttUrl.trim() && !selectedFile) {
|
|
206
|
+
try {
|
|
207
|
+
void new URL(vttUrl.trim())
|
|
208
|
+
} catch {
|
|
209
|
+
toast.push({
|
|
210
|
+
title: 'Invalid URL',
|
|
211
|
+
status: 'error',
|
|
212
|
+
description: 'Please enter a valid URL (e.g., https://example.com/subtitles.vtt)',
|
|
213
|
+
})
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!name.trim()) {
|
|
220
|
+
toast.push({
|
|
221
|
+
title: 'Audio name required',
|
|
222
|
+
status: 'error',
|
|
223
|
+
description: 'Please enter an audio name for this caption track',
|
|
224
|
+
})
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!languageCode.trim()) {
|
|
229
|
+
toast.push({
|
|
230
|
+
title: 'Language code required',
|
|
231
|
+
status: 'error',
|
|
232
|
+
description: 'Please enter a language code (e.g., en, es, fr)',
|
|
233
|
+
})
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
setIsSubmitting(true)
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
if (isAutogenerated) {
|
|
241
|
+
await handleGenerateSubtitles()
|
|
242
|
+
} else {
|
|
243
|
+
await handleAddTrackFromUrl()
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
toast.push({
|
|
247
|
+
title: 'Failed to add caption track',
|
|
248
|
+
status: 'error',
|
|
249
|
+
description: extractErrorMessage(error, 'Failed to add caption track'),
|
|
250
|
+
})
|
|
251
|
+
} finally {
|
|
252
|
+
setIsSubmitting(false)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<Dialog
|
|
258
|
+
id={dialogId}
|
|
259
|
+
header="Add Caption Track"
|
|
260
|
+
onClose={onClose}
|
|
261
|
+
width={1}
|
|
262
|
+
onClickOutside={onClose}
|
|
263
|
+
>
|
|
264
|
+
<Stack padding={4} space={4}>
|
|
265
|
+
<Stack space={2}>
|
|
266
|
+
<Flex align="center" marginBottom={3}>
|
|
267
|
+
<Checkbox
|
|
268
|
+
id="autogenerated-checkbox"
|
|
269
|
+
style={{display: 'block'}}
|
|
270
|
+
checked={isAutogenerated}
|
|
271
|
+
onChange={(e) => {
|
|
272
|
+
setIsAutogenerated(e.currentTarget.checked)
|
|
273
|
+
if (e.currentTarget.checked) {
|
|
274
|
+
setVttUrl('')
|
|
275
|
+
}
|
|
276
|
+
}}
|
|
277
|
+
disabled={isSubmitting}
|
|
278
|
+
/>
|
|
279
|
+
<Flex flex={1} paddingLeft={2}>
|
|
280
|
+
<Text>
|
|
281
|
+
<label htmlFor="autogenerated-checkbox">Generate captions</label>
|
|
282
|
+
</Text>
|
|
283
|
+
</Flex>
|
|
284
|
+
</Flex>
|
|
285
|
+
{!isAutogenerated && (
|
|
286
|
+
<Stack space={2}>
|
|
287
|
+
<Card padding={3} marginBottom={2} tone="transparent" border radius={2}>
|
|
288
|
+
<Flex align="center" justify="space-between">
|
|
289
|
+
<Text size={1} muted>
|
|
290
|
+
{selectedFile ? `Selected: ${selectedFile.name}` : 'No file selected'}
|
|
291
|
+
</Text>
|
|
292
|
+
<Button
|
|
293
|
+
icon={UploadIcon}
|
|
294
|
+
text="Select File"
|
|
295
|
+
mode="ghost"
|
|
296
|
+
tone="primary"
|
|
297
|
+
fontSize={1}
|
|
298
|
+
padding={2}
|
|
299
|
+
onClick={() => fileInputRef.current?.click()}
|
|
300
|
+
disabled={isSubmitting}
|
|
301
|
+
/>
|
|
302
|
+
</Flex>
|
|
303
|
+
<input
|
|
304
|
+
ref={fileInputRef}
|
|
305
|
+
type="file"
|
|
306
|
+
accept=".vtt,text/vtt"
|
|
307
|
+
style={{display: 'none'}}
|
|
308
|
+
onChange={(e) => {
|
|
309
|
+
if (e.target.files && e.target.files.length > 0 && !isSubmitting) {
|
|
310
|
+
setSelectedFile(e.target.files[0])
|
|
311
|
+
setVttUrl('')
|
|
312
|
+
}
|
|
313
|
+
}}
|
|
314
|
+
/>
|
|
315
|
+
</Card>
|
|
316
|
+
<Text size={1} muted style={{textAlign: 'center'}}>
|
|
317
|
+
Or enter the VTT file URL
|
|
318
|
+
</Text>
|
|
319
|
+
<Stack space={2}>
|
|
320
|
+
<Label htmlFor="vtt-url">VTT File URL</Label>
|
|
321
|
+
<TextInput
|
|
322
|
+
id="vtt-url"
|
|
323
|
+
placeholder="https://example.com/subtitles.vtt"
|
|
324
|
+
value={vttUrl}
|
|
325
|
+
onChange={(e) => {
|
|
326
|
+
setVttUrl(e.currentTarget.value)
|
|
327
|
+
setSelectedFile(null)
|
|
328
|
+
}}
|
|
329
|
+
disabled={isSubmitting}
|
|
330
|
+
/>
|
|
331
|
+
</Stack>
|
|
332
|
+
</Stack>
|
|
333
|
+
)}
|
|
334
|
+
</Stack>
|
|
335
|
+
|
|
336
|
+
<Stack space={2}>
|
|
337
|
+
<Label htmlFor="caption-name">Audio name</Label>
|
|
338
|
+
<Autocomplete
|
|
339
|
+
id="caption-name"
|
|
340
|
+
value={selectedLanguage?.value || ''}
|
|
341
|
+
onChange={(newValue) => {
|
|
342
|
+
const options = isAutogenerated ? MUX_LANGUAGE_OPTIONS : LANGUAGE_OPTIONS
|
|
343
|
+
const selected = options.find((opt) => opt.value === newValue)
|
|
344
|
+
if (selected) {
|
|
345
|
+
setSelectedLanguage(selected)
|
|
346
|
+
setLanguageCode(selected.value)
|
|
347
|
+
setName(selected.label)
|
|
348
|
+
}
|
|
349
|
+
}}
|
|
350
|
+
options={isAutogenerated ? MUX_LANGUAGE_OPTIONS : LANGUAGE_OPTIONS}
|
|
351
|
+
icon={TranslateIcon}
|
|
352
|
+
placeholder="Select language"
|
|
353
|
+
filterOption={(query, option) =>
|
|
354
|
+
option.label.toLowerCase().indexOf(query.toLowerCase()) > -1 ||
|
|
355
|
+
option.value.toLowerCase().indexOf(query.toLowerCase()) > -1
|
|
356
|
+
}
|
|
357
|
+
openButton
|
|
358
|
+
renderValue={(value) =>
|
|
359
|
+
(isAutogenerated ? MUX_LANGUAGE_OPTIONS : LANGUAGE_OPTIONS).find(
|
|
360
|
+
(l) => l.value === value
|
|
361
|
+
)?.label || value
|
|
362
|
+
}
|
|
363
|
+
renderOption={(option) => (
|
|
364
|
+
<Card data-as="button" padding={3} radius={2} tone="inherit">
|
|
365
|
+
<Text size={2} textOverflow="ellipsis">
|
|
366
|
+
{option.label} ({option.value})
|
|
367
|
+
</Text>
|
|
368
|
+
</Card>
|
|
369
|
+
)}
|
|
370
|
+
disabled={isSubmitting}
|
|
371
|
+
/>
|
|
372
|
+
</Stack>
|
|
373
|
+
|
|
374
|
+
<Stack space={2}>
|
|
375
|
+
<Label htmlFor="caption-language">Language Code</Label>
|
|
376
|
+
<TextInput
|
|
377
|
+
id="caption-language"
|
|
378
|
+
placeholder="en-US"
|
|
379
|
+
value={languageCode}
|
|
380
|
+
onChange={(e) => {
|
|
381
|
+
setLanguageCode(e.currentTarget.value)
|
|
382
|
+
if (selectedLanguage && selectedLanguage.value !== e.currentTarget.value) {
|
|
383
|
+
setSelectedLanguage(null)
|
|
384
|
+
if (!name || name === selectedLanguage.label) {
|
|
385
|
+
setName('')
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}}
|
|
389
|
+
disabled={isSubmitting}
|
|
390
|
+
/>
|
|
391
|
+
</Stack>
|
|
392
|
+
|
|
393
|
+
<Flex gap={2} justify="flex-end" marginTop={2}>
|
|
394
|
+
<Button text="Cancel" mode="ghost" onClick={onClose} disabled={isSubmitting} />
|
|
395
|
+
<Button
|
|
396
|
+
text="Add Caption Track"
|
|
397
|
+
tone="primary"
|
|
398
|
+
icon={
|
|
399
|
+
isSubmitting ? (
|
|
400
|
+
<Spinner
|
|
401
|
+
style={{
|
|
402
|
+
verticalAlign: 'middle',
|
|
403
|
+
display: 'inline-block',
|
|
404
|
+
marginBottom: '-3px',
|
|
405
|
+
width: '1em',
|
|
406
|
+
height: '1em',
|
|
407
|
+
marginRight: '-6px',
|
|
408
|
+
}}
|
|
409
|
+
/>
|
|
410
|
+
) : (
|
|
411
|
+
<UploadIcon />
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
onClick={handleSubmit}
|
|
415
|
+
disabled={isSubmitting}
|
|
416
|
+
/>
|
|
417
|
+
</Flex>
|
|
418
|
+
</Stack>
|
|
419
|
+
</Dialog>
|
|
420
|
+
)
|
|
421
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {Dialog, Stack} from '@sanity/ui'
|
|
2
|
+
import {useId} from 'react'
|
|
3
|
+
|
|
4
|
+
import {useDialogStateContext} from '../context/DialogStateContext'
|
|
5
|
+
import type {VideoAssetDocument} from '../util/types'
|
|
6
|
+
import TextTracksManager from './TextTracksManager'
|
|
7
|
+
|
|
8
|
+
export interface Props {
|
|
9
|
+
asset: VideoAssetDocument
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function CaptionsDialog({asset}: Props) {
|
|
13
|
+
const {setDialogState} = useDialogStateContext()
|
|
14
|
+
const dialogId = `CaptionsDialog${useId()}`
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Dialog id={dialogId} header="Edit Captions" onClose={() => setDialogState(false)} width={1}>
|
|
18
|
+
<Stack padding={4}>
|
|
19
|
+
<TextTracksManager asset={asset} />
|
|
20
|
+
</Stack>
|
|
21
|
+
</Dialog>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -32,7 +32,7 @@ export interface ConfigureApiDialogProps {
|
|
|
32
32
|
secrets: Secrets
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const fieldNames = ['token', 'secretKey', 'enableSignedUrls'] as const
|
|
35
|
+
const fieldNames = ['token', 'secretKey', 'enableSignedUrls', 'drmConfigId'] as const
|
|
36
36
|
|
|
37
37
|
// Internal dialog component that can be used with external state
|
|
38
38
|
export function ConfigureApiDialog({secrets, setDialogState}: ConfigureApiDialogProps) {
|
|
@@ -44,11 +44,12 @@ export function ConfigureApiDialog({secrets, setDialogState}: ConfigureApiDialog
|
|
|
44
44
|
() =>
|
|
45
45
|
secrets.token !== state.token ||
|
|
46
46
|
secrets.secretKey !== state.secretKey ||
|
|
47
|
-
secrets.enableSignedUrls !== state.enableSignedUrls
|
|
47
|
+
secrets.enableSignedUrls !== state.enableSignedUrls ||
|
|
48
|
+
secrets.drmConfigId !== state.drmConfigId,
|
|
48
49
|
[secrets, state]
|
|
49
50
|
)
|
|
50
51
|
const id = `ConfigureApi${useId()}`
|
|
51
|
-
const [tokenId, secretKeyId, enableSignedUrlsId] = useMemo<typeof fieldNames>(
|
|
52
|
+
const [tokenId, secretKeyId, enableSignedUrlsId, drmConfigIdId] = useMemo<typeof fieldNames>(
|
|
52
53
|
() => fieldNames.map((field) => `${id}-${field}`) as unknown as typeof fieldNames,
|
|
53
54
|
[id]
|
|
54
55
|
)
|
|
@@ -63,8 +64,8 @@ export function ConfigureApiDialog({secrets, setDialogState}: ConfigureApiDialog
|
|
|
63
64
|
if (!saving.current && event.currentTarget.reportValidity()) {
|
|
64
65
|
saving.current = true
|
|
65
66
|
dispatch({type: 'submit'})
|
|
66
|
-
const {token, secretKey, enableSignedUrls} = state
|
|
67
|
-
handleSaveSecrets({token, secretKey, enableSignedUrls})
|
|
67
|
+
const {token, secretKey, enableSignedUrls, drmConfigId} = state
|
|
68
|
+
handleSaveSecrets({token, secretKey, enableSignedUrls, drmConfigId})
|
|
68
69
|
.then((savedSecrets) => {
|
|
69
70
|
const {projectId, dataset} = client.config()
|
|
70
71
|
clear([cacheNs, secretsId, projectId, dataset])
|
|
@@ -106,6 +107,15 @@ export function ConfigureApiDialog({secrets, setDialogState}: ConfigureApiDialog
|
|
|
106
107
|
},
|
|
107
108
|
[dispatch]
|
|
108
109
|
)
|
|
110
|
+
const handleChangeDrmConfigId = useCallback(
|
|
111
|
+
(event: React.FormEvent<HTMLInputElement>) => {
|
|
112
|
+
dispatch({
|
|
113
|
+
type: 'change',
|
|
114
|
+
payload: {name: 'drmConfigId', value: event.currentTarget.value},
|
|
115
|
+
})
|
|
116
|
+
},
|
|
117
|
+
[dispatch]
|
|
118
|
+
)
|
|
109
119
|
|
|
110
120
|
useEffect(() => {
|
|
111
121
|
if (firstField.current) {
|
|
@@ -202,6 +212,42 @@ export function ConfigureApiDialog({secrets, setDialogState}: ConfigureApiDialog
|
|
|
202
212
|
) : null}
|
|
203
213
|
</Stack>
|
|
204
214
|
|
|
215
|
+
<FormField title="DRM Configuration ID" inputId={drmConfigIdId}>
|
|
216
|
+
<TextInput
|
|
217
|
+
id={drmConfigIdId}
|
|
218
|
+
onChange={handleChangeDrmConfigId}
|
|
219
|
+
type="text"
|
|
220
|
+
value={state.drmConfigId ?? ''}
|
|
221
|
+
required={false}
|
|
222
|
+
/>
|
|
223
|
+
</FormField>
|
|
224
|
+
<Card padding={[3, 3, 3]} radius={2} shadow={1} tone="neutral">
|
|
225
|
+
<Stack space={3}>
|
|
226
|
+
<Text size={1}>
|
|
227
|
+
DRM (Digital Rights Management) provides an extra layer of content security for
|
|
228
|
+
video content streamed from Mux. For additional information check out our{' '}
|
|
229
|
+
<a
|
|
230
|
+
href="https://www.mux.com/docs/guides/protect-videos-with-drm#play-drm-protected-videos"
|
|
231
|
+
target="_blank"
|
|
232
|
+
rel="noopener noreferrer"
|
|
233
|
+
>
|
|
234
|
+
DRM Guide
|
|
235
|
+
</a>
|
|
236
|
+
.
|
|
237
|
+
</Text>
|
|
238
|
+
<Text size={1}>
|
|
239
|
+
<a
|
|
240
|
+
href="https://www.mux.com/support/human"
|
|
241
|
+
target="_blank"
|
|
242
|
+
rel="noopener noreferrer"
|
|
243
|
+
>
|
|
244
|
+
Contact us
|
|
245
|
+
</a>{' '}
|
|
246
|
+
to get started using DRM.
|
|
247
|
+
</Text>
|
|
248
|
+
</Stack>
|
|
249
|
+
</Card>
|
|
250
|
+
|
|
205
251
|
<Inline space={2}>
|
|
206
252
|
<Button
|
|
207
253
|
text="Save"
|