sanity-plugin-mux-input 2.14.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 (48) hide show
  1. package/README.md +25 -24
  2. package/dist/index.d.mts +13 -1
  3. package/dist/index.d.ts +13 -1
  4. package/dist/index.js +771 -351
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +773 -353
  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/secrets.ts +6 -1
  11. package/src/actions/upload.ts +1 -1
  12. package/src/components/ConfigureApi.tsx +51 -5
  13. package/src/components/EditCaptionDialog.tsx +2 -2
  14. package/src/components/InputBrowser.tsx +8 -2
  15. package/src/components/PageSelector.tsx +4 -7
  16. package/src/components/Player.styled.tsx +7 -2
  17. package/src/components/PlayerActionsMenu.tsx +1 -1
  18. package/src/components/SelectAsset.tsx +9 -3
  19. package/src/components/StudioTool.tsx +2 -2
  20. package/src/components/UploadConfiguration.tsx +104 -343
  21. package/src/components/Uploader.tsx +18 -7
  22. package/src/components/VideoDetails/VideoDetails.tsx +28 -8
  23. package/src/components/VideoInBrowser.tsx +53 -6
  24. package/src/components/VideoPlayer.tsx +120 -47
  25. package/src/components/VideoThumbnail.tsx +84 -72
  26. package/src/components/VideosBrowser.tsx +7 -5
  27. package/src/components/uploadConfiguration/PlaybackPolicy.tsx +95 -6
  28. package/src/components/uploadConfiguration/PlaybackPolicyOption.tsx +26 -10
  29. package/src/components/uploadConfiguration/ResolutionTierSelector.tsx +71 -0
  30. package/src/components/uploadConfiguration/StaticRenditionSelector.tsx +179 -0
  31. package/src/context/DrmPlaybackWarningContext.tsx +93 -0
  32. package/src/hooks/useFetchFileSize.ts +54 -0
  33. package/src/hooks/useMediaMetadata.ts +100 -0
  34. package/src/hooks/useSaveSecrets.ts +10 -3
  35. package/src/hooks/useSecretsDocumentValues.ts +9 -1
  36. package/src/hooks/useSecretsFormState.ts +6 -3
  37. package/src/util/asserters.ts +14 -0
  38. package/src/util/createUrlParamsObject.ts +7 -3
  39. package/src/util/generateJwt.ts +11 -2
  40. package/src/util/getPlaybackPolicy.ts +63 -4
  41. package/src/util/getStoryboardSrc.ts +7 -3
  42. package/src/util/getVideoMetadata.ts +1 -0
  43. package/src/util/getVideoSrc.ts +9 -9
  44. package/src/util/readSecrets.ts +3 -1
  45. package/src/util/textTracks.ts +6 -3
  46. package/src/util/tryWithSuspend.ts +22 -0
  47. package/src/util/types.ts +27 -2
  48. 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.14.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/*'],
@@ -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
@@ -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"
@@ -18,7 +18,7 @@ import {useEffect, useId, useRef, useState} from 'react'
18
18
  import {addTextTrackFromUrl, deleteTextTrack, getAsset} from '../actions/assets'
19
19
  import {useClient} from '../hooks/useClient'
20
20
  import {generateJwt} from '../util/generateJwt'
21
- import {getPlaybackId} from '../util/getPlaybackId'
21
+ import {getPlaybackId} from '../util/getPlaybackPolicy'
22
22
  import {getPlaybackPolicy} from '../util/getPlaybackPolicy'
23
23
  import {downloadVttFile, extractErrorMessage, pollTrackStatus} from '../util/textTracks'
24
24
  import type {MuxTextTrack, VideoAssetDocument} from '../util/types'
@@ -276,7 +276,7 @@ export default function EditCaptionDialog({asset, track, onUpdate, onClose}: Pro
276
276
  const playbackId = getPlaybackId(asset)
277
277
  if (!playbackId) return ''
278
278
  let url = `https://stream.mux.com/${playbackId}/text/${track.id}.vtt`
279
- if (getPlaybackPolicy(asset) === 'signed') {
279
+ if (getPlaybackPolicy(asset)?.policy === 'signed') {
280
280
  const token = generateJwt(client, playbackId, 'v')
281
281
  url += `?token=${token}`
282
282
  }
@@ -16,7 +16,8 @@ export default function InputBrowser({
16
16
  setDialogState,
17
17
  asset,
18
18
  onChange,
19
- }: Pick<SelectAssetProps, 'onChange' | 'asset'> & {
19
+ config,
20
+ }: Pick<SelectAssetProps, 'onChange' | 'asset' | 'config'> & {
20
21
  setDialogState: SetDialogState
21
22
  }) {
22
23
  const id = `InputBrowser${useId()}`
@@ -29,7 +30,12 @@ export default function InputBrowser({
29
30
  onClose={handleClose}
30
31
  width={2}
31
32
  >
32
- <SelectAsset asset={asset} onChange={onChange} setDialogState={setDialogState} />
33
+ <SelectAsset
34
+ config={config}
35
+ asset={asset}
36
+ onChange={onChange}
37
+ setDialogState={setDialogState}
38
+ />
33
39
  </StyledDialog>
34
40
  )
35
41
  }
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-shadow */
2
1
  import {ChevronLeftIcon, ChevronRightIcon} from '@sanity/icons'
3
2
  import {Button, Label} from '@sanity/ui'
4
3
  import {Dispatch, SetStateAction, useEffect} from 'react'
@@ -7,8 +6,6 @@ const PageSelector = (props: {
7
6
  page: number
8
7
  setPage: Dispatch<SetStateAction<number>>
9
8
  total: number
10
- // eslint-disable-next-line react/no-unused-prop-types
11
- limit: number
12
9
  }) => {
13
10
  const page = props.page
14
11
  const setPage = props.setPage
@@ -30,8 +27,8 @@ const PageSelector = (props: {
30
27
  style={{cursor: 'pointer'}}
31
28
  disabled={page <= 0}
32
29
  onClick={() => {
33
- setPage((page) => {
34
- return Math.min(props.total - 1, Math.max(0, page - 1))
30
+ setPage((p) => {
31
+ return Math.min(props.total - 1, Math.max(0, p - 1))
35
32
  })
36
33
  }}
37
34
  />
@@ -45,8 +42,8 @@ const PageSelector = (props: {
45
42
  style={{cursor: 'pointer'}}
46
43
  disabled={page >= props.total - 1}
47
44
  onClick={() => {
48
- setPage((page) => {
49
- return Math.min(props.total - 1, Math.max(0, page + 1))
45
+ setPage((p) => {
46
+ return Math.min(props.total - 1, Math.max(0, p + 1))
50
47
  })
51
48
  }}
52
49
  />
@@ -1,4 +1,4 @@
1
- import {useState} from 'react'
1
+ import {Suspense, useState} from 'react'
2
2
  import {styled} from 'styled-components'
3
3
 
4
4
  import {useClient} from '../hooks/useClient'
@@ -46,5 +46,10 @@ export function ThumbnailsMetadataTrack({asset}: ThumbnailsMetadataTrackProps) {
46
46
  // Why useState instead of useMemo? Because we really really only want to run it exactly once and useMemo doesn't make that guarantee
47
47
  const [src] = useState<string>(() => getStoryboardSrc({asset, client}))
48
48
 
49
- return <track label="thumbnails" default kind="metadata" src={src} />
49
+ return (
50
+ /* We use Suspense here because `getStoryboardSrc` uses suspend() under the hood */
51
+ <Suspense fallback={null}>
52
+ <track label="thumbnails" default kind="metadata" src={src} />
53
+ </Suspense>
54
+ )
50
55
  }
@@ -64,7 +64,7 @@ function PlayerActionsMenu(
64
64
  const {asset, readOnly, dialogState, setDialogState, onChange, onSelect, accept} = props
65
65
  const [open, setOpen] = useState(false)
66
66
  const [menuElement, setMenuRef] = useState<HTMLDivElement | null>(null)
67
- const isSigned = useMemo(() => getPlaybackPolicy(asset) === 'signed', [asset])
67
+ const isSigned = useMemo(() => getPlaybackPolicy(asset)?.policy === 'signed', [asset])
68
68
  const {hasConfigAccess} = useAccessControl(props.config)
69
69
 
70
70
  const onReset = useCallback(() => onChange(PatchEvent.from(unset([]))), [onChange])
@@ -2,15 +2,21 @@ import {useCallback} from 'react'
2
2
  import {PatchEvent, set, setIfMissing, unset} from 'sanity'
3
3
 
4
4
  import type {SetDialogState} from '../hooks/useDialogState'
5
- import type {MuxInputProps, VideoAssetDocument} from '../util/types'
5
+ import type {MuxInputProps, PluginConfig, VideoAssetDocument} from '../util/types'
6
6
  import VideosBrowser, {type VideosBrowserProps} from './VideosBrowser'
7
7
 
8
8
  export interface Props extends Pick<MuxInputProps, 'onChange'> {
9
9
  asset?: VideoAssetDocument | null | undefined
10
10
  setDialogState: SetDialogState
11
+ config: PluginConfig
11
12
  }
12
13
 
13
- export default function SelectAssets({asset: selectedAsset, onChange, setDialogState}: Props) {
14
+ export default function SelectAssets({
15
+ asset: selectedAsset,
16
+ onChange,
17
+ setDialogState,
18
+ config,
19
+ }: Props) {
14
20
  const handleSelect = useCallback<Required<VideosBrowserProps>['onSelect']>(
15
21
  (chosenAsset) => {
16
22
  if (!chosenAsset?._id) {
@@ -29,5 +35,5 @@ export default function SelectAssets({asset: selectedAsset, onChange, setDialogS
29
35
  [onChange, setDialogState, selectedAsset]
30
36
  )
31
37
 
32
- return <VideosBrowser onSelect={handleSelect} />
38
+ return <VideosBrowser onSelect={handleSelect} config={config} />
33
39
  }
@@ -4,8 +4,8 @@ import type {PluginConfig} from '../util/types'
4
4
  import ToolIcon from './icons/ToolIcon'
5
5
  import VideosBrowser from './VideosBrowser'
6
6
 
7
- const StudioTool: React.FC<PluginConfig> = () => {
8
- return <VideosBrowser />
7
+ const StudioTool: React.FC<PluginConfig> = (config) => {
8
+ return <VideosBrowser config={config} />
9
9
  }
10
10
 
11
11
  export const DEFAULT_TOOL_CONFIG = {