sanity-plugin-mux-input 2.14.0 → 2.16.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 (55) 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 +1057 -470
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +1059 -472
  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 +15 -1
  18. package/src/components/ResyncMetadata.tsx +152 -73
  19. package/src/components/SelectAsset.tsx +9 -3
  20. package/src/components/StudioTool.tsx +2 -2
  21. package/src/components/TextTracksManager.tsx +11 -55
  22. package/src/components/UploadConfiguration.tsx +104 -343
  23. package/src/components/Uploader.tsx +18 -7
  24. package/src/components/VideoDetails/VideoDetails.tsx +55 -19
  25. package/src/components/VideoDetails/useVideoDetails.ts +15 -1
  26. package/src/components/VideoInBrowser.tsx +53 -6
  27. package/src/components/VideoPlayer.tsx +120 -47
  28. package/src/components/VideoThumbnail.tsx +84 -72
  29. package/src/components/VideosBrowser.tsx +7 -5
  30. package/src/components/uploadConfiguration/PlaybackPolicy.tsx +95 -6
  31. package/src/components/uploadConfiguration/PlaybackPolicyOption.tsx +26 -10
  32. package/src/components/uploadConfiguration/ResolutionTierSelector.tsx +71 -0
  33. package/src/components/uploadConfiguration/StaticRenditionSelector.tsx +179 -0
  34. package/src/context/DrmPlaybackWarningContext.tsx +93 -0
  35. package/src/hooks/useFetchFileSize.ts +54 -0
  36. package/src/hooks/useMediaMetadata.ts +100 -0
  37. package/src/hooks/useResyncAsset.ts +110 -0
  38. package/src/hooks/useResyncMuxMetadata.ts +33 -0
  39. package/src/hooks/useSaveSecrets.ts +10 -3
  40. package/src/hooks/useSecretsDocumentValues.ts +9 -1
  41. package/src/hooks/useSecretsFormState.ts +6 -3
  42. package/src/schema.ts +5 -0
  43. package/src/util/addKeysToMuxData.ts +30 -0
  44. package/src/util/asserters.ts +14 -0
  45. package/src/util/createUrlParamsObject.ts +7 -3
  46. package/src/util/generateJwt.ts +11 -2
  47. package/src/util/getPlaybackPolicy.ts +63 -4
  48. package/src/util/getStoryboardSrc.ts +7 -3
  49. package/src/util/getVideoMetadata.ts +1 -0
  50. package/src/util/getVideoSrc.ts +9 -9
  51. package/src/util/readSecrets.ts +3 -1
  52. package/src/util/textTracks.ts +6 -3
  53. package/src/util/tryWithSuspend.ts +22 -0
  54. package/src/util/types.ts +27 -2
  55. package/src/util/getPlaybackId.ts +0 -9
@@ -2,8 +2,9 @@ import {SearchIcon} from '@sanity/icons'
2
2
  import {Card, Flex, Grid, Inline, Label, Stack, Text, TextInput} from '@sanity/ui'
3
3
  import {useMemo, useState} from 'react'
4
4
 
5
+ import {DrmPlaybackWarningContextProvider} from '../context/DrmPlaybackWarningContext'
5
6
  import useAssets from '../hooks/useAssets'
6
- import type {VideoAssetDocument} from '../util/types'
7
+ import type {PluginConfig, VideoAssetDocument} from '../util/types'
7
8
  import ConfigureApi from './ConfigureApi'
8
9
  import ImportVideosFromMux from './ImportVideosFromMux'
9
10
  import PageSelector from './PageSelector'
@@ -15,10 +16,11 @@ import VideoDetails from './VideoDetails/VideoDetails'
15
16
  import VideoInBrowser from './VideoInBrowser'
16
17
 
17
18
  export interface VideosBrowserProps {
19
+ config: PluginConfig
18
20
  onSelect?: (asset: VideoAssetDocument) => void
19
21
  }
20
22
 
21
- export default function VideosBrowser({onSelect}: VideosBrowserProps) {
23
+ export default function VideosBrowser({onSelect, config}: VideosBrowserProps) {
22
24
  const {assets, isLoading, searchQuery, setSearchQuery, setSort, sort} = useAssets()
23
25
  const [page, setPage] = useState<number>(0)
24
26
  const pageLimit = 20
@@ -34,7 +36,7 @@ export default function VideosBrowser({onSelect}: VideosBrowserProps) {
34
36
 
35
37
  const placement = onSelect ? 'input' : 'tool'
36
38
  return (
37
- <>
39
+ <DrmPlaybackWarningContextProvider config={config}>
38
40
  <Stack padding={4} space={4} style={{minHeight: '50vh'}}>
39
41
  <Flex justify="space-between" align="center">
40
42
  <Flex align="center" gap={3}>
@@ -47,7 +49,7 @@ export default function VideosBrowser({onSelect}: VideosBrowserProps) {
47
49
  placeholder="Search videos"
48
50
  />
49
51
  <SelectSortOptions setSort={setSort} sort={sort} />
50
- <PageSelector page={page} setPage={setPage} total={pageTotal} limit={pageLimit} />
52
+ <PageSelector page={page} setPage={setPage} total={pageTotal} />
51
53
  </Flex>
52
54
  {placement === 'tool' && (
53
55
  <Inline space={2}>
@@ -93,6 +95,6 @@ export default function VideosBrowser({onSelect}: VideosBrowserProps) {
93
95
  {freshEditedAsset && (
94
96
  <VideoDetails closeDialog={() => setEditedAsset(null)} asset={freshEditedAsset} />
95
97
  )}
96
- </>
98
+ </DrmPlaybackWarningContextProvider>
97
99
  )
98
100
  }
@@ -1,6 +1,8 @@
1
- import {Grid, Text} from '@sanity/ui'
1
+ import {Code, Grid, Text} from '@sanity/ui'
2
+ import {ActionDispatch} from 'react'
2
3
 
3
4
  import {Secrets, UploadConfig} from '../../util/types'
5
+ import {UploadConfigurationStateAction} from '../UploadConfiguration'
4
6
  import PlaybackPolicyOption from './PlaybackPolicyOption'
5
7
  import PlaybackPolicyWarning from './PlaybackPolicyWarning'
6
8
 
@@ -13,9 +15,10 @@ export default function PlaybackPolicy({
13
15
  id: string
14
16
  config: UploadConfig
15
17
  secrets: Secrets
16
- dispatch: any
18
+ dispatch: ActionDispatch<[action: UploadConfigurationStateAction]>
17
19
  }) {
18
- const noPolicySelected = !(config.public_policy || config.signed_policy)
20
+ const noPolicySelected = !(config.public_policy || config.signed_policy || config.drm_policy)
21
+ const drmPolicyDisabled = !secrets.drmConfigId
19
22
  return (
20
23
  <Grid gap={3}>
21
24
  <Text weight="bold">Advanced Playback Policies</Text>
@@ -23,7 +26,14 @@ export default function PlaybackPolicy({
23
26
  id={`${id}--public`}
24
27
  checked={config.public_policy}
25
28
  optionName="Public"
26
- description="Playback IDs are accessible by constructing an HLS URL like https://stream.mux.com/{PLAYBACK_ID}"
29
+ description={
30
+ <>
31
+ <Text size={2} muted>
32
+ Playback IDs are accessible by constructing an HLS URL like
33
+ </Text>
34
+ <Code>{'https://stream.mux.com/{PLAYBACK_ID}'}</Code>
35
+ </>
36
+ }
27
37
  dispatch={dispatch}
28
38
  action="public_policy"
29
39
  />
@@ -32,12 +42,91 @@ export default function PlaybackPolicy({
32
42
  id={`${id}--signed`}
33
43
  checked={config.signed_policy}
34
44
  optionName="Signed"
35
- description="Playback IDs should be used with tokens https://stream.mux.com/{PLAYBACK_ID}?token={TOKEN}.
36
- // See Secure video playback for details about creating tokens."
45
+ description={
46
+ <>
47
+ <Text size={2} muted>
48
+ Playback IDs should be used with tokens
49
+ </Text>
50
+ <Code>{'https://stream.mux.com/{PLAYBACK_ID}?token={TOKEN}'}</Code>
51
+ <Text size={2} muted>
52
+ See{' '}
53
+ <a
54
+ href="https://www.mux.com/docs/guides/secure-video-playback"
55
+ target="_blank"
56
+ rel="noopener noreferrer"
57
+ >
58
+ Secure video playback
59
+ </a>{' '}
60
+ for details about creating tokens.
61
+ </Text>
62
+ </>
63
+ }
64
+ // See Secure video playback for details about creating tokens."
37
65
  dispatch={dispatch}
38
66
  action="signed_policy"
39
67
  />
40
68
  )}
69
+ {drmPolicyDisabled ? (
70
+ <PlaybackPolicyOption
71
+ id={`${id}--drm`}
72
+ checked={false}
73
+ optionName="DRM - Disabled"
74
+ description={
75
+ <>
76
+ <Text size={2} muted>
77
+ To enable DRM add your DRM Configuration Id to your plugin configuration in the API
78
+ Credentials view.{' '}
79
+ <a
80
+ href="https://www.mux.com/support/human"
81
+ target="_blank"
82
+ rel="noopener noreferrer"
83
+ >
84
+ Contact us
85
+ </a>{' '}
86
+ to get started using DRM.
87
+ </Text>
88
+ </>
89
+ }
90
+ dispatch={dispatch}
91
+ disabled
92
+ />
93
+ ) : (
94
+ <PlaybackPolicyOption
95
+ id={`${id}--drm`}
96
+ checked={config.drm_policy}
97
+ optionName="DRM"
98
+ description={
99
+ <>
100
+ <Text size={2} muted>
101
+ Playback IDs should be used with tokens as with Signed playback, but require extra
102
+ configuration.
103
+ </Text>
104
+ <Code>{'https://stream.mux.com/{PLAYBACK_ID}?token={TOKEN}'}</Code>
105
+ <Text size={2} muted>
106
+ See{' '}
107
+ <a
108
+ href="https://www.mux.com/docs/guides/protect-videos-with-drm#play-drm-protected-videos"
109
+ target="_blank"
110
+ rel="noopener noreferrer"
111
+ >
112
+ Protect videos with DRM
113
+ </a>{' '}
114
+ for details about configuring your player for DRM playback and{' '}
115
+ <a
116
+ href="https://www.mux.com/docs/guides/secure-video-playback"
117
+ target="_blank"
118
+ rel="noopener noreferrer"
119
+ >
120
+ Secure video playback
121
+ </a>{' '}
122
+ for details about creating tokens.
123
+ </Text>
124
+ </>
125
+ }
126
+ dispatch={dispatch}
127
+ action="drm_policy"
128
+ />
129
+ )}
41
130
  {noPolicySelected && <PlaybackPolicyWarning />}
42
131
  </Grid>
43
132
  )
@@ -1,5 +1,5 @@
1
- import {Box, Checkbox, Flex, Grid, Stack, Text} from '@sanity/ui'
2
- import {CSSProperties, useState} from 'react'
1
+ import {Checkbox, Flex, Grid, Text} from '@sanity/ui'
2
+ import {ActionDispatch, CSSProperties, ReactNode, useState} from 'react'
3
3
 
4
4
  import {UploadConfigurationStateAction} from '../UploadConfiguration'
5
5
 
@@ -10,13 +10,15 @@ export default function PlaybackPolicyOption({
10
10
  description,
11
11
  dispatch,
12
12
  action,
13
+ disabled,
13
14
  }: {
14
15
  id: string
15
16
  checked: boolean
16
17
  optionName: string
17
- description: string
18
- dispatch: any
19
- action: UploadConfigurationStateAction['action']
18
+ description: string | ReactNode
19
+ dispatch: ActionDispatch<[action: UploadConfigurationStateAction]>
20
+ action?: 'public_policy' | 'signed_policy' | 'drm_policy'
21
+ disabled?: boolean
20
22
  }) {
21
23
  const [scale, setScale] = useState(1)
22
24
 
@@ -24,7 +26,7 @@ export default function PlaybackPolicyOption({
24
26
  outline: '0.01rem solid grey',
25
27
  transform: `scale(${scale})`,
26
28
  transition: 'transform 0.1s ease-in-out',
27
- cursor: 'pointer',
29
+ cursor: disabled ? 'not-allowed' : 'pointer',
28
30
  borderRadius: '0.25rem',
29
31
  }
30
32
 
@@ -36,23 +38,37 @@ export default function PlaybackPolicyOption({
36
38
  }
37
39
 
38
40
  const handleBoxClick = () => {
41
+ if (!action) return
39
42
  triggerAnimation()
40
43
  dispatch({
41
44
  action,
42
45
  value: !checked,
43
46
  })
44
47
  }
48
+
49
+ const descriptionJsx =
50
+ typeof description === 'string' ? (
51
+ <Text size={2} muted>
52
+ {description}
53
+ </Text>
54
+ ) : (
55
+ description
56
+ )
45
57
  return (
46
58
  <label>
47
59
  <Flex gap={3} padding={3} style={boxStyle}>
48
- <Checkbox id={id} required checked={checked} onChange={handleBoxClick} />
60
+ <Checkbox
61
+ id={id}
62
+ required
63
+ checked={checked}
64
+ onChange={handleBoxClick}
65
+ disabled={disabled}
66
+ />
49
67
  <Grid gap={3}>
50
68
  <Text size={3} weight="bold">
51
69
  {optionName}
52
70
  </Text>
53
- <Text size={2} muted>
54
- {description}
55
- </Text>
71
+ {descriptionJsx}
56
72
  </Grid>
57
73
  </Flex>
58
74
  </label>
@@ -0,0 +1,71 @@
1
+ import {Flex, Radio, Text} from '@sanity/ui'
2
+ import {ActionDispatch} from 'react'
3
+ import {FormField} from 'sanity'
4
+
5
+ import {type UploadConfig} from '../../util/types'
6
+ import {UploadConfigurationStateAction} from '../UploadConfiguration'
7
+
8
+ export const RESOLUTION_TIERS = [
9
+ {value: '1080p', label: '1080p'},
10
+ {value: '1440p', label: '1440p (2k)'},
11
+ {value: '2160p', label: '2160p (4k)'},
12
+ ] as const satisfies {value: UploadConfig['max_resolution_tier']; label: string}[]
13
+
14
+ export const ResolutionTierSelector = ({
15
+ id,
16
+ config,
17
+ dispatch,
18
+ maxSupportedResolution,
19
+ }: {
20
+ id: string
21
+ config: UploadConfig
22
+ dispatch: ActionDispatch<[action: UploadConfigurationStateAction]>
23
+ maxSupportedResolution: number
24
+ }) => {
25
+ return (
26
+ <FormField
27
+ title="Resolution Tier"
28
+ description={
29
+ <>
30
+ The maximum{' '}
31
+ <a
32
+ href="https://docs.mux.com/api-reference#video/operation/create-direct-upload"
33
+ target="_blank"
34
+ rel="noopener noreferrer"
35
+ >
36
+ resolution_tier
37
+ </a>{' '}
38
+ your asset is encoded, stored, and streamed at.
39
+ </>
40
+ }
41
+ >
42
+ <Flex gap={3} wrap={'wrap'}>
43
+ {RESOLUTION_TIERS.map(({value, label}, index) => {
44
+ const inputId = `${id}--type-${value}`
45
+
46
+ if (index > maxSupportedResolution) return null
47
+
48
+ return (
49
+ <Flex key={value} align="center" gap={2}>
50
+ <Radio
51
+ checked={config.max_resolution_tier === value}
52
+ name="asset-resolutiontier"
53
+ onChange={(e) =>
54
+ dispatch({
55
+ action: 'max_resolution_tier',
56
+ value: e.currentTarget.value as UploadConfig['max_resolution_tier'],
57
+ })
58
+ }
59
+ value={value}
60
+ id={inputId}
61
+ />
62
+ <Text as="label" htmlFor={inputId}>
63
+ {label}
64
+ </Text>
65
+ </Flex>
66
+ )
67
+ })}
68
+ </Flex>
69
+ </FormField>
70
+ )
71
+ }
@@ -0,0 +1,179 @@
1
+ import {Checkbox, Flex, Label, Radio, Stack, Text} from '@sanity/ui'
2
+ import {ActionDispatch, useMemo, useState} from 'react'
3
+ import {FormField} from 'sanity'
4
+
5
+ import {type StaticRenditionResolution, type UploadConfig} from '../../util/types'
6
+ import {UploadConfigurationStateAction} from '../UploadConfiguration'
7
+
8
+ const ADVANCED_RESOLUTIONS: {value: StaticRenditionResolution; label: string}[] = [
9
+ {value: '270p', label: '270p'},
10
+ {value: '360p', label: '360p'},
11
+ {value: '480p', label: '480p'},
12
+ {value: '540p', label: '540p'},
13
+ {value: '720p', label: '720p'},
14
+ {value: '1080p', label: '1080p'},
15
+ {value: '1440p', label: '1440p'},
16
+ {value: '2160p', label: '2160p'},
17
+ ]
18
+
19
+ export const StaticRenditionSelector = ({
20
+ id,
21
+ config,
22
+ dispatch,
23
+ }: {
24
+ id: string
25
+ config: UploadConfig
26
+ dispatch: ActionDispatch<[action: UploadConfigurationStateAction]>
27
+ }) => {
28
+ // Determine if user is in advanced mode based on selected renditions
29
+ const isAdvancedMode = useMemo(() => {
30
+ const specificResolutions = config.static_renditions.filter(
31
+ (r) => r !== 'highest' && r !== 'audio-only'
32
+ )
33
+ return specificResolutions.length > 0
34
+ }, [config.static_renditions])
35
+
36
+ const [renditionMode, setRenditionMode] = useState<'standard' | 'advanced'>(
37
+ isAdvancedMode ? 'advanced' : 'standard'
38
+ )
39
+
40
+ // Helper to toggle a rendition
41
+ const toggleRendition = (rendition: StaticRenditionResolution) => {
42
+ const current = config.static_renditions
43
+ const hasRendition = current.includes(rendition)
44
+
45
+ if (hasRendition) {
46
+ dispatch({
47
+ action: 'static_renditions',
48
+ value: current.filter((r) => r !== rendition),
49
+ })
50
+ } else {
51
+ dispatch({
52
+ action: 'static_renditions',
53
+ value: [...current, rendition],
54
+ })
55
+ }
56
+ }
57
+
58
+ // When switching modes, clear renditions that don't apply
59
+ const handleModeChange = (mode: 'standard' | 'advanced') => {
60
+ setRenditionMode(mode)
61
+ if (mode === 'standard') {
62
+ // Remove specific resolutions, keep only highest and audio-only
63
+ dispatch({
64
+ action: 'static_renditions',
65
+ value: config.static_renditions.filter((r) => r === 'highest' || r === 'audio-only'),
66
+ })
67
+ } else {
68
+ // Remove highest, keep specific resolutions and audio-only
69
+ dispatch({
70
+ action: 'static_renditions',
71
+ value: config.static_renditions.filter((r) => r !== 'highest'),
72
+ })
73
+ }
74
+ }
75
+ return (
76
+ <Stack space={3}>
77
+ <FormField
78
+ title="Static Renditions"
79
+ description="Generate downloadable MP4 or M4A files. Note: Mux will not upscale to produce MP4 renditions - renditions that would cause upscaling are skipped."
80
+ >
81
+ <Stack space={3}>
82
+ {/* Mode Selector */}
83
+ <Flex gap={3}>
84
+ <Flex align="center" gap={2}>
85
+ <Radio
86
+ checked={renditionMode === 'standard'}
87
+ name="rendition-mode"
88
+ onChange={() => handleModeChange('standard')}
89
+ value="standard"
90
+ id={`${id}--mode-standard`}
91
+ />
92
+ <Text as="label" htmlFor={`${id}--mode-standard`}>
93
+ Standard
94
+ </Text>
95
+ </Flex>
96
+ <Flex align="center" gap={2}>
97
+ <Radio
98
+ checked={renditionMode === 'advanced'}
99
+ name="rendition-mode"
100
+ onChange={() => handleModeChange('advanced')}
101
+ value="advanced"
102
+ id={`${id}--mode-advanced`}
103
+ />
104
+ <Text as="label" htmlFor={`${id}--mode-advanced`}>
105
+ Advanced
106
+ </Text>
107
+ </Flex>
108
+ </Flex>
109
+
110
+ {/* Standard Mode Options */}
111
+ {renditionMode === 'standard' && (
112
+ <Stack space={2}>
113
+ <Flex align="center" gap={2} padding={[0, 2]}>
114
+ <Checkbox
115
+ id={`${id}--highest`}
116
+ style={{display: 'block'}}
117
+ checked={config.static_renditions.includes('highest')}
118
+ onChange={() => toggleRendition('highest')}
119
+ />
120
+ <Text as="label" htmlFor={`${id}--highest`}>
121
+ Highest Resolution (up to 4K)
122
+ </Text>
123
+ </Flex>
124
+ <Flex align="center" gap={2} padding={[0, 2]}>
125
+ <Checkbox
126
+ id={`${id}--audio-only-standard`}
127
+ style={{display: 'block'}}
128
+ checked={config.static_renditions.includes('audio-only')}
129
+ onChange={() => toggleRendition('audio-only')}
130
+ />
131
+ <Text as="label" htmlFor={`${id}--audio-only-standard`}>
132
+ Audio Only (M4A)
133
+ </Text>
134
+ </Flex>
135
+ </Stack>
136
+ )}
137
+
138
+ {/* Advanced Mode Options */}
139
+ {renditionMode === 'advanced' && (
140
+ <Stack space={2}>
141
+ <Label size={1} muted>
142
+ Select specific resolutions:
143
+ </Label>
144
+ <Flex gap={2} wrap="wrap">
145
+ {ADVANCED_RESOLUTIONS.map(({value, label}) => {
146
+ const inputId = `${id}--resolution-${value}`
147
+ return (
148
+ <Flex key={value} align="center" gap={2}>
149
+ <Checkbox
150
+ id={inputId}
151
+ style={{display: 'block'}}
152
+ checked={config.static_renditions.includes(value)}
153
+ onChange={() => toggleRendition(value)}
154
+ />
155
+ <Text as="label" htmlFor={inputId} size={1}>
156
+ {label}
157
+ </Text>
158
+ </Flex>
159
+ )
160
+ })}
161
+ </Flex>
162
+ <Flex align="center" gap={2} padding={[2, 2, 0, 2]}>
163
+ <Checkbox
164
+ id={`${id}--audio-only-advanced`}
165
+ style={{display: 'block'}}
166
+ checked={config.static_renditions.includes('audio-only')}
167
+ onChange={() => toggleRendition('audio-only')}
168
+ />
169
+ <Text as="label" htmlFor={`${id}--audio-only-advanced`}>
170
+ Audio Only (M4A)
171
+ </Text>
172
+ </Flex>
173
+ </Stack>
174
+ )}
175
+ </Stack>
176
+ </FormField>
177
+ </Stack>
178
+ )
179
+ }
@@ -0,0 +1,93 @@
1
+ import {Button, Card, Dialog, Stack, Text} from '@sanity/ui'
2
+ import React, {createContext, useContext, useState} from 'react'
3
+
4
+ import {PluginConfig} from '../util/types'
5
+
6
+ const LOCAL_STORAGE_HAS_SHOWN_WARNING_KEY = 'mux-plugin-has-shown-drm-playback-warning'
7
+
8
+ type DrmPlaybackWarningContextContextProps = {
9
+ hasShownWarning: boolean
10
+ setHasWarnedAboutDrmPlayback: (b: boolean) => void
11
+ }
12
+
13
+ const DrmPlaybackWarningContext = createContext<DrmPlaybackWarningContextContextProps>({
14
+ hasShownWarning: false,
15
+ setHasWarnedAboutDrmPlayback: () => {
16
+ return null
17
+ },
18
+ })
19
+
20
+ interface DrmPlaybackWarningContextProviderProps {
21
+ config?: PluginConfig
22
+ children: React.ReactNode
23
+ }
24
+
25
+ export const DrmPlaybackWarningContextProvider = ({
26
+ config,
27
+ children,
28
+ }: DrmPlaybackWarningContextProviderProps) => {
29
+ const warningDisabled = config?.disableDrmPlaybackWarning ?? false
30
+ const hasWarned: boolean =
31
+ warningDisabled || window.localStorage.getItem(LOCAL_STORAGE_HAS_SHOWN_WARNING_KEY) === 'true'
32
+ const [hasWarnedAboutDrmPlayback, setHasWarnedAboutDrmPlayback] = useState(hasWarned)
33
+
34
+ const setHasShownWarning = (b: boolean) => {
35
+ window.localStorage.setItem(LOCAL_STORAGE_HAS_SHOWN_WARNING_KEY, b.toString())
36
+ setHasWarnedAboutDrmPlayback(b)
37
+ }
38
+ return (
39
+ <DrmPlaybackWarningContext.Provider
40
+ value={{
41
+ hasShownWarning: hasWarnedAboutDrmPlayback,
42
+ setHasWarnedAboutDrmPlayback: setHasShownWarning,
43
+ }}
44
+ >
45
+ {children}
46
+ </DrmPlaybackWarningContext.Provider>
47
+ )
48
+ }
49
+
50
+ export const useDrmPlaybackWarningContext = () => {
51
+ const context = useContext(DrmPlaybackWarningContext)
52
+ return context
53
+ }
54
+
55
+ export const DRMWarningDialog = ({onClose}: {onClose: () => void}) => {
56
+ const {setHasWarnedAboutDrmPlayback} = useDrmPlaybackWarningContext()
57
+ const _onClose = () => {
58
+ setHasWarnedAboutDrmPlayback(true)
59
+ onClose()
60
+ }
61
+ return (
62
+ <Dialog
63
+ open
64
+ id="drm-playback-warn"
65
+ onClose={_onClose}
66
+ header="DRM Playback Warning"
67
+ footer={
68
+ <Stack padding={3}>
69
+ <Button mode="ghost" tone="primary" onClick={_onClose} text="Ok" />
70
+ </Stack>
71
+ }
72
+ >
73
+ <Stack space={3} padding={3}>
74
+ <Card padding={[3, 3, 3]} radius={2}>
75
+ <Stack space={3}>
76
+ <Text size={1} weight="semibold">
77
+ DRM-protected playback will generate a license with a small associated cost. The
78
+ plugin will attempt to play signed or public playback IDs instead whenever possible.
79
+ </Text>
80
+ </Stack>
81
+ </Card>
82
+ <Card padding={[3, 3, 3]} radius={2} tone="suggest">
83
+ <Stack space={3}>
84
+ <Text size={1} weight="semibold">
85
+ This is a one time warning. If it persists, you can disable it from your plugin
86
+ configuration.
87
+ </Text>
88
+ </Stack>
89
+ </Card>
90
+ </Stack>
91
+ </Dialog>
92
+ )
93
+ }
@@ -0,0 +1,54 @@
1
+ import {useEffect, useState} from 'react'
2
+
3
+ import {StagedUpload} from '../components/Uploader'
4
+
5
+ export function useFetchFileSize(stagedUpload: StagedUpload, maxFileSize?: number) {
6
+ const [fileSize, setFileSize] = useState<number | null>(null)
7
+ const [isLoadingFileSize, setIsLoadingFileSize] = useState(false)
8
+ const [canSkipFileSizeValidation, setCanSkipFileSizeValidation] = useState(false)
9
+
10
+ useEffect(() => {
11
+ // Fetch URL Upload file size
12
+ if (stagedUpload.type === 'url') {
13
+ setIsLoadingFileSize(false)
14
+ setCanSkipFileSizeValidation(false)
15
+ setFileSize(null)
16
+ const url = stagedUpload.url
17
+
18
+ // Get file size from URL
19
+ const fetchFileSize = async () => {
20
+ setIsLoadingFileSize(true)
21
+ try {
22
+ const response = await fetch(url, {method: 'HEAD'})
23
+ const contentLength = response.headers.get('content-length')
24
+ const newFileSize = contentLength ? parseInt(contentLength, 10) : null
25
+
26
+ setIsLoadingFileSize(false)
27
+ if (newFileSize) {
28
+ setFileSize(newFileSize)
29
+ }
30
+ if (newFileSize === null && maxFileSize !== undefined) {
31
+ // Size unknown but size limit is configured - skip file size validation
32
+ setCanSkipFileSizeValidation(true)
33
+ }
34
+ } catch {
35
+ console.warn('Could not validate file size from URL')
36
+ // Skip validation of file size, but still validate duration
37
+ setCanSkipFileSizeValidation(true)
38
+ setIsLoadingFileSize(false)
39
+ }
40
+ }
41
+
42
+ fetchFileSize()
43
+ }
44
+ if (stagedUpload.type === 'file') {
45
+ setFileSize(stagedUpload.files[0].size)
46
+ }
47
+ }, [maxFileSize, stagedUpload, stagedUpload.type])
48
+
49
+ return {
50
+ fileSize,
51
+ isLoadingFileSize,
52
+ canSkipFileSizeValidation,
53
+ }
54
+ }