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.
Files changed (56) hide show
  1. package/README.md +25 -24
  2. package/dist/index.d.mts +35 -2
  3. package/dist/index.d.ts +35 -2
  4. package/dist/index.js +2176 -461
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +2178 -463
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +1 -1
  9. package/src/_exports/index.ts +1 -0
  10. package/src/actions/assets.ts +75 -0
  11. package/src/actions/secrets.ts +6 -1
  12. package/src/actions/upload.ts +1 -1
  13. package/src/components/AddCaptionDialog.tsx +421 -0
  14. package/src/components/CaptionsDialog.tsx +23 -0
  15. package/src/components/ConfigureApi.tsx +51 -5
  16. package/src/components/EditCaptionDialog.tsx +508 -0
  17. package/src/components/InputBrowser.tsx +8 -2
  18. package/src/components/Onboard.tsx +2 -2
  19. package/src/components/PageSelector.tsx +54 -0
  20. package/src/components/Player.styled.tsx +7 -2
  21. package/src/components/PlayerActionsMenu.tsx +14 -6
  22. package/src/components/SelectAsset.tsx +9 -3
  23. package/src/components/StudioTool.tsx +2 -2
  24. package/src/components/TextTracksManager.tsx +781 -0
  25. package/src/components/UploadConfiguration.tsx +104 -343
  26. package/src/components/Uploader.styled.tsx +8 -15
  27. package/src/components/Uploader.tsx +25 -7
  28. package/src/components/VideoDetails/VideoDetails.tsx +43 -7
  29. package/src/components/VideoInBrowser.tsx +53 -6
  30. package/src/components/VideoPlayer.tsx +122 -47
  31. package/src/components/VideoThumbnail.tsx +84 -72
  32. package/src/components/VideosBrowser.tsx +15 -5
  33. package/src/components/uploadConfiguration/PlaybackPolicy.tsx +95 -6
  34. package/src/components/uploadConfiguration/PlaybackPolicyOption.tsx +26 -10
  35. package/src/components/uploadConfiguration/ResolutionTierSelector.tsx +71 -0
  36. package/src/components/uploadConfiguration/StaticRenditionSelector.tsx +179 -0
  37. package/src/context/DrmPlaybackWarningContext.tsx +93 -0
  38. package/src/hooks/useAccessControl.ts +1 -0
  39. package/src/hooks/useDialogState.ts +1 -1
  40. package/src/hooks/useFetchFileSize.ts +54 -0
  41. package/src/hooks/useMediaMetadata.ts +100 -0
  42. package/src/hooks/useSaveSecrets.ts +10 -3
  43. package/src/hooks/useSecretsDocumentValues.ts +9 -1
  44. package/src/hooks/useSecretsFormState.ts +6 -3
  45. package/src/util/asserters.ts +14 -0
  46. package/src/util/createUrlParamsObject.ts +7 -3
  47. package/src/util/generateJwt.ts +11 -2
  48. package/src/util/getPlaybackPolicy.ts +63 -4
  49. package/src/util/getStoryboardSrc.ts +7 -3
  50. package/src/util/getVideoMetadata.ts +4 -1
  51. package/src/util/getVideoSrc.ts +9 -9
  52. package/src/util/readSecrets.ts +3 -1
  53. package/src/util/textTracks.ts +222 -0
  54. package/src/util/tryWithSuspend.ts +22 -0
  55. package/src/util/types.ts +39 -6
  56. package/src/util/getPlaybackId.ts +0 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanity-plugin-mux-input",
3
- "version": "2.13.0",
3
+ "version": "2.15.0",
4
4
  "description": "An input component that integrates Sanity Studio with Mux video encoding/hosting service.",
5
5
  "keywords": [
6
6
  "sanity",
@@ -14,6 +14,7 @@ export const defaultConfig: PluginConfig = {
14
14
  normalize_audio: false,
15
15
  defaultPublic: true,
16
16
  defaultSigned: false,
17
+ defaultDrm: false,
17
18
  tool: DEFAULT_TOOL_CONFIG,
18
19
  allowedRolesForConfiguration: [],
19
20
  acceptedMimeTypes: ['video/*', 'audio/*'],
@@ -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
+ }
@@ -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
  }
@@ -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"