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.
- package/README.md +25 -24
- package/dist/index.d.mts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +1057 -470
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1059 -472
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/_exports/index.ts +1 -0
- package/src/actions/secrets.ts +6 -1
- package/src/actions/upload.ts +1 -1
- package/src/components/ConfigureApi.tsx +51 -5
- package/src/components/EditCaptionDialog.tsx +2 -2
- package/src/components/InputBrowser.tsx +8 -2
- package/src/components/PageSelector.tsx +4 -7
- package/src/components/Player.styled.tsx +7 -2
- package/src/components/PlayerActionsMenu.tsx +15 -1
- package/src/components/ResyncMetadata.tsx +152 -73
- package/src/components/SelectAsset.tsx +9 -3
- package/src/components/StudioTool.tsx +2 -2
- package/src/components/TextTracksManager.tsx +11 -55
- package/src/components/UploadConfiguration.tsx +104 -343
- package/src/components/Uploader.tsx +18 -7
- package/src/components/VideoDetails/VideoDetails.tsx +55 -19
- package/src/components/VideoDetails/useVideoDetails.ts +15 -1
- package/src/components/VideoInBrowser.tsx +53 -6
- package/src/components/VideoPlayer.tsx +120 -47
- package/src/components/VideoThumbnail.tsx +84 -72
- package/src/components/VideosBrowser.tsx +7 -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/useFetchFileSize.ts +54 -0
- package/src/hooks/useMediaMetadata.ts +100 -0
- package/src/hooks/useResyncAsset.ts +110 -0
- package/src/hooks/useResyncMuxMetadata.ts +33 -0
- package/src/hooks/useSaveSecrets.ts +10 -3
- package/src/hooks/useSecretsDocumentValues.ts +9 -1
- package/src/hooks/useSecretsFormState.ts +6 -3
- package/src/schema.ts +5 -0
- package/src/util/addKeysToMuxData.ts +30 -0
- 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 +1 -0
- package/src/util/getVideoSrc.ts +9 -9
- package/src/util/readSecrets.ts +3 -1
- package/src/util/textTracks.ts +6 -3
- package/src/util/tryWithSuspend.ts +22 -0
- package/src/util/types.ts +27 -2
- 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}
|
|
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:
|
|
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=
|
|
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=
|
|
36
|
-
|
|
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 {
|
|
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:
|
|
19
|
-
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
|
|
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
|
-
|
|
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
|
+
}
|