sanity-plugin-mux-input 2.9.1 → 2.10.1
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 +8 -8
- package/dist/index.d.mts +13 -5
- package/dist/index.d.ts +13 -5
- package/dist/index.js +2175 -1933
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2179 -1937
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/_exports/index.ts +13 -1
- package/src/actions/assets.ts +22 -0
- package/src/components/ConfigureApi.tsx +32 -9
- package/src/components/ImportVideosFromMux.tsx +26 -2
- package/src/components/Input.tsx +3 -3
- package/src/components/ResyncMetadata.tsx +201 -0
- package/src/components/UploadConfiguration.tsx +28 -28
- package/src/components/VideosBrowser.tsx +10 -2
- package/src/hooks/useImportMuxAssets.ts +3 -3
- package/src/hooks/useMuxAssets.ts +64 -53
- package/src/hooks/useResyncMuxMetadata.ts +143 -0
- package/src/schema.ts +4 -0
- package/src/util/assetTitlePlaceholder.ts +31 -0
- package/src/util/types.ts +20 -10
package/package.json
CHANGED
package/src/_exports/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type {VideoAssetDocument} from '../util/types'
|
|
|
8
8
|
|
|
9
9
|
export const defaultConfig: PluginConfig = {
|
|
10
10
|
mp4_support: 'none',
|
|
11
|
-
|
|
11
|
+
video_quality: 'plus',
|
|
12
12
|
max_resolution_tier: '1080p',
|
|
13
13
|
normalize_audio: false,
|
|
14
14
|
defaultSigned: false,
|
|
@@ -17,6 +17,18 @@ export const defaultConfig: PluginConfig = {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const muxInput = definePlugin<Partial<PluginConfig> | void>((userConfig) => {
|
|
20
|
+
// TODO: Remove this on next major version when we end support for encoding_tier
|
|
21
|
+
if (typeof userConfig === 'object' && 'encoding_tier' in userConfig) {
|
|
22
|
+
const deprecated_encoding_tier = userConfig.encoding_tier
|
|
23
|
+
if (!userConfig.video_quality) {
|
|
24
|
+
if (deprecated_encoding_tier === 'baseline') {
|
|
25
|
+
userConfig.video_quality = 'basic'
|
|
26
|
+
}
|
|
27
|
+
if (deprecated_encoding_tier === 'smart') {
|
|
28
|
+
userConfig.video_quality = 'plus'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
20
32
|
const config: PluginConfig = {...defaultConfig, ...(userConfig || {})}
|
|
21
33
|
return {
|
|
22
34
|
name: 'mux-input',
|
package/src/actions/assets.ts
CHANGED
|
@@ -47,3 +47,25 @@ export function getAsset(client: SanityClient, assetId: string) {
|
|
|
47
47
|
method: 'GET',
|
|
48
48
|
})
|
|
49
49
|
}
|
|
50
|
+
|
|
51
|
+
export function listAssets(
|
|
52
|
+
client: SanityClient,
|
|
53
|
+
options: {limit?: number; cursor?: string | null}
|
|
54
|
+
) {
|
|
55
|
+
const {dataset} = client.config()
|
|
56
|
+
const query: {limit?: string; cursor?: string} = {}
|
|
57
|
+
|
|
58
|
+
if (options.limit) {
|
|
59
|
+
query.limit = options.limit.toString()
|
|
60
|
+
}
|
|
61
|
+
if (options.cursor) {
|
|
62
|
+
query.cursor = options.cursor
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return client.request<{data: MuxAsset[]; next_cursor?: string | null}>({
|
|
66
|
+
url: `/addons/mux/assets/${dataset}/data/list`,
|
|
67
|
+
withCredentials: true,
|
|
68
|
+
method: 'GET',
|
|
69
|
+
query,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
@@ -11,25 +11,31 @@ import {
|
|
|
11
11
|
Text,
|
|
12
12
|
TextInput,
|
|
13
13
|
} from '@sanity/ui'
|
|
14
|
-
import
|
|
14
|
+
import {useCallback, useEffect, useId, useMemo, useRef} from 'react'
|
|
15
15
|
import {clear, preload} from 'suspend-react'
|
|
16
16
|
|
|
17
17
|
import {useClient} from '../hooks/useClient'
|
|
18
18
|
import type {SetDialogState} from '../hooks/useDialogState'
|
|
19
|
+
import {useDialogState} from '../hooks/useDialogState'
|
|
19
20
|
import {useSaveSecrets} from '../hooks/useSaveSecrets'
|
|
21
|
+
import {useSecretsDocumentValues} from '../hooks/useSecretsDocumentValues'
|
|
20
22
|
import {useSecretsFormState} from '../hooks/useSecretsFormState'
|
|
21
|
-
import {cacheNs} from '../util/constants'
|
|
23
|
+
import {cacheNs, DIALOGS_Z_INDEX} from '../util/constants'
|
|
22
24
|
import {_id as secretsId} from '../util/readSecrets'
|
|
23
25
|
import type {Secrets} from '../util/types'
|
|
24
26
|
import {Header} from './ConfigureApi.styled'
|
|
25
27
|
import FormField from './FormField'
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
// Props for the dialog component when used with external state management
|
|
30
|
+
export interface ConfigureApiDialogProps {
|
|
28
31
|
setDialogState: SetDialogState
|
|
29
32
|
secrets: Secrets
|
|
30
33
|
}
|
|
34
|
+
|
|
31
35
|
const fieldNames = ['token', 'secretKey', 'enableSignedUrls'] as const
|
|
32
|
-
|
|
36
|
+
|
|
37
|
+
// Internal dialog component that can be used with external state
|
|
38
|
+
export function ConfigureApiDialog({secrets, setDialogState}: ConfigureApiDialogProps) {
|
|
33
39
|
const client = useClient()
|
|
34
40
|
const [state, dispatch] = useSecretsFormState(secrets)
|
|
35
41
|
const hasSecretsInitially = useMemo(() => secrets.token && secrets.secretKey, [secrets])
|
|
@@ -112,13 +118,13 @@ function ConfigureApi({secrets, setDialogState}: Props) {
|
|
|
112
118
|
animate
|
|
113
119
|
id={id}
|
|
114
120
|
onClose={handleClose}
|
|
121
|
+
onClickOutside={handleClose}
|
|
115
122
|
header={<Header />}
|
|
123
|
+
zOffset={DIALOGS_Z_INDEX}
|
|
124
|
+
position="fixed"
|
|
116
125
|
width={1}
|
|
117
|
-
style={{
|
|
118
|
-
maxWidth: '550px',
|
|
119
|
-
}}
|
|
120
126
|
>
|
|
121
|
-
<Box padding={
|
|
127
|
+
<Box padding={3}>
|
|
122
128
|
<form onSubmit={handleSubmit} noValidate>
|
|
123
129
|
<Stack space={4}>
|
|
124
130
|
{!hasSecretsInitially && (
|
|
@@ -224,4 +230,21 @@ function ConfigureApi({secrets, setDialogState}: Props) {
|
|
|
224
230
|
)
|
|
225
231
|
}
|
|
226
232
|
|
|
227
|
-
|
|
233
|
+
// Wrapper component that manages its own dialog state (used in VideosBrowser)
|
|
234
|
+
export default function ConfigureApi() {
|
|
235
|
+
const [dialogOpen, setDialogOpen] = useDialogState()
|
|
236
|
+
const secretDocumentValues = useSecretsDocumentValues()
|
|
237
|
+
|
|
238
|
+
const openDialog = useCallback(() => setDialogOpen('secrets'), [setDialogOpen])
|
|
239
|
+
|
|
240
|
+
if (dialogOpen === 'secrets') {
|
|
241
|
+
return (
|
|
242
|
+
<ConfigureApiDialog
|
|
243
|
+
secrets={secretDocumentValues.value.secrets}
|
|
244
|
+
setDialogState={setDialogOpen}
|
|
245
|
+
/>
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return <Button mode="bleed" text="Configure plugin" onClick={openDialog} />
|
|
250
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
CheckmarkCircleIcon,
|
|
3
|
+
ErrorOutlineIcon,
|
|
4
|
+
InfoOutlineIcon,
|
|
5
|
+
RetrieveIcon,
|
|
6
|
+
RetryIcon,
|
|
7
|
+
} from '@sanity/icons'
|
|
2
8
|
import {
|
|
3
9
|
Box,
|
|
4
10
|
Button,
|
|
@@ -120,7 +126,7 @@ function ImportVideosDialog(props: ReturnType<typeof useImportMuxAssets>) {
|
|
|
120
126
|
<Button
|
|
121
127
|
fontSize={2}
|
|
122
128
|
padding={3}
|
|
123
|
-
mode="
|
|
129
|
+
mode="ghost"
|
|
124
130
|
text="Cancel"
|
|
125
131
|
tone="critical"
|
|
126
132
|
onClick={props.closeDialog}
|
|
@@ -149,6 +155,24 @@ function ImportVideosDialog(props: ReturnType<typeof useImportMuxAssets>) {
|
|
|
149
155
|
}
|
|
150
156
|
>
|
|
151
157
|
<Box padding={3}>
|
|
158
|
+
{/* WARNING: SKIPPED ASSETS WITHOUT PLAYBACK */}
|
|
159
|
+
{props.muxAssets.hasSkippedAssetsWithoutPlayback && (
|
|
160
|
+
<Card tone="caution" marginBottom={5} padding={3} border>
|
|
161
|
+
<Flex align="center" gap={2}>
|
|
162
|
+
<InfoOutlineIcon fontSize={36} />
|
|
163
|
+
<Stack space={2}>
|
|
164
|
+
<Text size={2} weight="semibold">
|
|
165
|
+
Some videos were skipped
|
|
166
|
+
</Text>
|
|
167
|
+
<Text size={1}>
|
|
168
|
+
Videos without playback IDs cannot be imported and have been excluded from the
|
|
169
|
+
list.
|
|
170
|
+
</Text>
|
|
171
|
+
</Stack>
|
|
172
|
+
</Flex>
|
|
173
|
+
</Card>
|
|
174
|
+
)}
|
|
175
|
+
|
|
152
176
|
{/* LOADING ASSETS STATE */}
|
|
153
177
|
{(props.muxAssets.loading || props.assetsInSanityLoading) && (
|
|
154
178
|
<Card tone="primary" marginBottom={5} padding={3} border>
|
package/src/components/Input.tsx
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import {Card} from '@sanity/ui'
|
|
2
2
|
import {memo, Suspense} from 'react'
|
|
3
3
|
|
|
4
|
+
import {useAccessControl} from '../hooks/useAccessControl'
|
|
4
5
|
import {useAssetDocumentValues} from '../hooks/useAssetDocumentValues'
|
|
5
6
|
import {useClient} from '../hooks/useClient'
|
|
6
7
|
import {useDialogState} from '../hooks/useDialogState'
|
|
7
8
|
import {useMuxPolling} from '../hooks/useMuxPolling'
|
|
8
9
|
import {useSecretsDocumentValues} from '../hooks/useSecretsDocumentValues'
|
|
9
10
|
import type {MuxInputProps, PluginConfig} from '../util/types'
|
|
10
|
-
import
|
|
11
|
+
import {ConfigureApiDialog} from './ConfigureApi'
|
|
11
12
|
import ErrorBoundaryCard from './ErrorBoundaryCard'
|
|
12
13
|
import {InputFallback} from './Input.styled'
|
|
13
14
|
import Onboard from './Onboard'
|
|
14
15
|
import Uploader from './Uploader'
|
|
15
|
-
import {useAccessControl} from '../hooks/useAccessControl'
|
|
16
16
|
|
|
17
17
|
export interface InputProps extends MuxInputProps {
|
|
18
18
|
config: PluginConfig
|
|
@@ -62,7 +62,7 @@ const Input = (props: InputProps) => {
|
|
|
62
62
|
)}
|
|
63
63
|
|
|
64
64
|
{dialogState === 'secrets' && hasConfigAccess && (
|
|
65
|
-
<
|
|
65
|
+
<ConfigureApiDialog
|
|
66
66
|
setDialogState={setDialogState}
|
|
67
67
|
secrets={secretDocumentValues.value.secrets}
|
|
68
68
|
/>
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import {CheckmarkCircleIcon, ErrorOutlineIcon, SyncIcon} from '@sanity/icons'
|
|
2
|
+
import {Box, Button, Card, Dialog, Flex, Heading, Spinner, Stack, Text} from '@sanity/ui'
|
|
3
|
+
|
|
4
|
+
import useResyncMuxMetadata from '../hooks/useResyncMuxMetadata'
|
|
5
|
+
import {isEmptyOrPlaceholderTitle} from '../util/assetTitlePlaceholder'
|
|
6
|
+
import {DIALOGS_Z_INDEX} from '../util/constants'
|
|
7
|
+
|
|
8
|
+
// eslint-disable-next-line complexity
|
|
9
|
+
function ResyncMetadataDialog(props: ReturnType<typeof useResyncMuxMetadata>) {
|
|
10
|
+
const {resyncState} = props
|
|
11
|
+
|
|
12
|
+
const canTriggerResync = resyncState === 'idle' || resyncState === 'error'
|
|
13
|
+
const isResyncing = resyncState === 'syncing'
|
|
14
|
+
const isDone = resyncState === 'done'
|
|
15
|
+
|
|
16
|
+
const videosToUpdate = props.matchedAssets?.filter((m) => m.muxAsset).length || 0
|
|
17
|
+
const videosWithEmptyOrPlaceholder =
|
|
18
|
+
props.matchedAssets?.filter(
|
|
19
|
+
(m) => m.muxAsset && m.muxTitle && isEmptyOrPlaceholderTitle(m.currentTitle, m.muxAsset.id)
|
|
20
|
+
).length || 0
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Dialog
|
|
24
|
+
animate
|
|
25
|
+
header={'Resync Metadata from Mux'}
|
|
26
|
+
zOffset={DIALOGS_Z_INDEX}
|
|
27
|
+
id="resync-metadata-dialog"
|
|
28
|
+
onClose={props.closeDialog}
|
|
29
|
+
onClickOutside={props.closeDialog}
|
|
30
|
+
width={1}
|
|
31
|
+
position="fixed"
|
|
32
|
+
footer={
|
|
33
|
+
!isDone && (
|
|
34
|
+
<Card padding={3}>
|
|
35
|
+
<Flex justify="space-between" align="center">
|
|
36
|
+
<Button
|
|
37
|
+
fontSize={2}
|
|
38
|
+
padding={3}
|
|
39
|
+
mode="ghost"
|
|
40
|
+
text="Cancel"
|
|
41
|
+
tone="critical"
|
|
42
|
+
onClick={props.closeDialog}
|
|
43
|
+
disabled={isResyncing}
|
|
44
|
+
/>
|
|
45
|
+
<Flex gap={2}>
|
|
46
|
+
{videosWithEmptyOrPlaceholder > 0 && (
|
|
47
|
+
<Button
|
|
48
|
+
fontSize={2}
|
|
49
|
+
padding={3}
|
|
50
|
+
mode="ghost"
|
|
51
|
+
text={`Update empty (${videosWithEmptyOrPlaceholder})`}
|
|
52
|
+
tone="caution"
|
|
53
|
+
onClick={props.syncOnlyEmpty}
|
|
54
|
+
disabled={isResyncing || !canTriggerResync}
|
|
55
|
+
/>
|
|
56
|
+
)}
|
|
57
|
+
<Button
|
|
58
|
+
icon={SyncIcon}
|
|
59
|
+
fontSize={2}
|
|
60
|
+
padding={3}
|
|
61
|
+
mode="ghost"
|
|
62
|
+
text={`Update all (${videosToUpdate})`}
|
|
63
|
+
tone="positive"
|
|
64
|
+
onClick={props.syncAllVideos}
|
|
65
|
+
iconRight={isResyncing && Spinner}
|
|
66
|
+
disabled={!canTriggerResync}
|
|
67
|
+
/>
|
|
68
|
+
</Flex>
|
|
69
|
+
</Flex>
|
|
70
|
+
</Card>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
>
|
|
74
|
+
<Box padding={4}>
|
|
75
|
+
{/* LOADING ASSETS STATE */}
|
|
76
|
+
{(props.muxAssets.loading || props.sanityAssetsLoading) && (
|
|
77
|
+
<Card tone="primary" marginBottom={5} padding={3} border>
|
|
78
|
+
<Flex align="center" gap={4}>
|
|
79
|
+
<Spinner muted size={4} />
|
|
80
|
+
<Stack space={2}>
|
|
81
|
+
<Text size={2} weight="semibold">
|
|
82
|
+
Loading assets from Mux
|
|
83
|
+
</Text>
|
|
84
|
+
<Text size={1}>This may take a while.</Text>
|
|
85
|
+
</Stack>
|
|
86
|
+
</Flex>
|
|
87
|
+
</Card>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{/* ERROR LOADING MUX */}
|
|
91
|
+
{props.muxAssets.error && (
|
|
92
|
+
<Card tone="critical" marginBottom={5} padding={3} border>
|
|
93
|
+
<Flex align="center" gap={2}>
|
|
94
|
+
<ErrorOutlineIcon fontSize={36} />
|
|
95
|
+
<Stack space={2}>
|
|
96
|
+
<Text size={2} weight="semibold">
|
|
97
|
+
There was an error getting data from Mux
|
|
98
|
+
</Text>
|
|
99
|
+
<Text size={1}>Please try again or contact a developer for help.</Text>
|
|
100
|
+
</Stack>
|
|
101
|
+
</Flex>
|
|
102
|
+
</Card>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
{/* SYNCING STATE */}
|
|
106
|
+
{resyncState === 'syncing' && (
|
|
107
|
+
<Card tone="primary" marginBottom={5} padding={3} border>
|
|
108
|
+
<Flex align="center" gap={4}>
|
|
109
|
+
<Spinner muted size={4} />
|
|
110
|
+
<Stack space={2}>
|
|
111
|
+
<Text size={2} weight="semibold">
|
|
112
|
+
Updating video metadata
|
|
113
|
+
</Text>
|
|
114
|
+
<Text size={1}>Syncing titles from Mux...</Text>
|
|
115
|
+
</Stack>
|
|
116
|
+
</Flex>
|
|
117
|
+
</Card>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{/* ERROR SYNCING */}
|
|
121
|
+
{resyncState === 'error' && (
|
|
122
|
+
<Card tone="critical" marginBottom={5} padding={3} border>
|
|
123
|
+
<Flex align="center" gap={2}>
|
|
124
|
+
<ErrorOutlineIcon fontSize={36} />
|
|
125
|
+
<Stack space={2}>
|
|
126
|
+
<Text size={2} weight="semibold">
|
|
127
|
+
There was an error syncing metadata
|
|
128
|
+
</Text>
|
|
129
|
+
<Text size={1}>
|
|
130
|
+
{props.resyncError
|
|
131
|
+
? `Error: ${props.resyncError}`
|
|
132
|
+
: 'Please try again or contact a developer for help.'}
|
|
133
|
+
</Text>
|
|
134
|
+
</Stack>
|
|
135
|
+
</Flex>
|
|
136
|
+
</Card>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{/* SUCCESS STATE */}
|
|
140
|
+
{resyncState === 'done' && (
|
|
141
|
+
<Stack paddingY={5} marginBottom={4} space={3} style={{textAlign: 'center'}}>
|
|
142
|
+
<Box>
|
|
143
|
+
<CheckmarkCircleIcon fontSize={48} />
|
|
144
|
+
</Box>
|
|
145
|
+
<Heading size={2}>Metadata synced successfully</Heading>
|
|
146
|
+
<Text size={2}>All video titles have been updated from Mux.</Text>
|
|
147
|
+
</Stack>
|
|
148
|
+
)}
|
|
149
|
+
|
|
150
|
+
{/* CONFIRMATION MESSAGE */}
|
|
151
|
+
{resyncState === 'idle' && !props.muxAssets.loading && !props.sanityAssetsLoading && (
|
|
152
|
+
<Stack space={4}>
|
|
153
|
+
<Heading size={1}>
|
|
154
|
+
There {videosToUpdate === 1 ? 'is' : 'are'} {videosToUpdate} video
|
|
155
|
+
{videosToUpdate === 1 ? '' : 's'} with Mux metadata
|
|
156
|
+
</Heading>
|
|
157
|
+
<Text size={2}>
|
|
158
|
+
This will update video titles in Sanity to match those in Mux. No new videos will be
|
|
159
|
+
created.
|
|
160
|
+
</Text>
|
|
161
|
+
{videosWithEmptyOrPlaceholder > 0 && (
|
|
162
|
+
<Card padding={3} tone="caution" border>
|
|
163
|
+
<Flex align="flex-start" gap={2}>
|
|
164
|
+
<Box>
|
|
165
|
+
<ErrorOutlineIcon />
|
|
166
|
+
</Box>
|
|
167
|
+
<Stack space={2}>
|
|
168
|
+
<Text size={2} weight="semibold">
|
|
169
|
+
Videos with empty or placeholder titles
|
|
170
|
+
</Text>
|
|
171
|
+
<Text size={1} muted>
|
|
172
|
+
{videosWithEmptyOrPlaceholder} video
|
|
173
|
+
{videosWithEmptyOrPlaceholder === 1 ? '' : 's'} without titles or with
|
|
174
|
+
placeholder titles (e.g., "Asset #123") can be updated selectively.
|
|
175
|
+
</Text>
|
|
176
|
+
</Stack>
|
|
177
|
+
</Flex>
|
|
178
|
+
</Card>
|
|
179
|
+
)}
|
|
180
|
+
</Stack>
|
|
181
|
+
)}
|
|
182
|
+
</Box>
|
|
183
|
+
</Dialog>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export default function ResyncMetadata() {
|
|
188
|
+
const resyncMetadata = useResyncMuxMetadata()
|
|
189
|
+
|
|
190
|
+
if (!resyncMetadata.hasSecrets) {
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (resyncMetadata.dialogOpen) {
|
|
195
|
+
// eslint-disable-next-line consistent-return
|
|
196
|
+
return <ResyncMetadataDialog {...resyncMetadata} />
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// eslint-disable-next-line consistent-return
|
|
200
|
+
return <Button mode="bleed" text="Resync Metadata" onClick={resyncMetadata.openDialog} />
|
|
201
|
+
}
|
|
@@ -23,7 +23,7 @@ import PlaybackPolicy from './uploadConfiguration/PlaybackPolicy'
|
|
|
23
23
|
import type {StagedUpload} from './Uploader'
|
|
24
24
|
|
|
25
25
|
export type UploadConfigurationStateAction =
|
|
26
|
-
| {action: '
|
|
26
|
+
| {action: 'video_quality'; value: UploadConfig['video_quality']}
|
|
27
27
|
| {action: 'max_resolution_tier'; value: UploadConfig['max_resolution_tier']}
|
|
28
28
|
| {action: 'mp4_support'; value: UploadConfig['mp4_support']}
|
|
29
29
|
| {action: 'normalize_audio'; value: UploadConfig['normalize_audio']}
|
|
@@ -31,10 +31,11 @@ export type UploadConfigurationStateAction =
|
|
|
31
31
|
| {action: 'public_policy'; value: UploadConfig['public_policy']}
|
|
32
32
|
| TrackAction
|
|
33
33
|
|
|
34
|
-
const
|
|
35
|
-
{value: '
|
|
36
|
-
{value: '
|
|
37
|
-
|
|
34
|
+
const VIDEO_QUALITY_LEVELS = [
|
|
35
|
+
{value: 'basic', label: 'Basic'},
|
|
36
|
+
{value: 'plus', label: 'Plus'},
|
|
37
|
+
{value: 'premium', label: 'Premium'},
|
|
38
|
+
] as const satisfies {value: UploadConfig['video_quality']; label: string}[]
|
|
38
39
|
|
|
39
40
|
const RESOLUTION_TIERS = [
|
|
40
41
|
{value: '1080p', label: '1080p'},
|
|
@@ -63,7 +64,7 @@ export default function UploadConfiguration({
|
|
|
63
64
|
}) {
|
|
64
65
|
const id = useId()
|
|
65
66
|
const autoTextTracks = useRef<NonNullable<UploadConfig['text_tracks']>>(
|
|
66
|
-
pluginConfig.
|
|
67
|
+
pluginConfig.video_quality === 'plus' && pluginConfig.defaultAutogeneratedSubtitleLang
|
|
67
68
|
? [
|
|
68
69
|
{
|
|
69
70
|
_id: uuid(),
|
|
@@ -78,21 +79,21 @@ export default function UploadConfiguration({
|
|
|
78
79
|
const [config, dispatch] = useReducer(
|
|
79
80
|
(prev: UploadConfig, action: UploadConfigurationStateAction) => {
|
|
80
81
|
switch (action.action) {
|
|
81
|
-
case '
|
|
82
|
-
// If
|
|
83
|
-
if (action.value === '
|
|
82
|
+
case 'video_quality':
|
|
83
|
+
// If video quality level switches to basic, remove plus-only features
|
|
84
|
+
if (action.value === 'basic') {
|
|
84
85
|
return Object.assign({}, prev, {
|
|
85
|
-
|
|
86
|
+
video_quality: action.value,
|
|
86
87
|
mp4_support: 'none',
|
|
87
88
|
max_resolution_tier: '1080p',
|
|
88
89
|
text_tracks: prev.text_tracks?.filter(({type}) => type !== 'autogenerated'),
|
|
89
90
|
public_policy: true,
|
|
90
91
|
signed_policy: false,
|
|
91
92
|
})
|
|
92
|
-
// If
|
|
93
|
+
// If video quality level switches to plus, add back in default plus features
|
|
93
94
|
}
|
|
94
95
|
return Object.assign({}, prev, {
|
|
95
|
-
|
|
96
|
+
video_quality: action.value,
|
|
96
97
|
mp4_support: pluginConfig.mp4_support,
|
|
97
98
|
max_resolution_tier: pluginConfig.max_resolution_tier,
|
|
98
99
|
text_tracks: [...autoTextTracks, ...(prev.text_tracks || [])],
|
|
@@ -138,7 +139,7 @@ export default function UploadConfiguration({
|
|
|
138
139
|
}
|
|
139
140
|
},
|
|
140
141
|
{
|
|
141
|
-
|
|
142
|
+
video_quality: pluginConfig.video_quality,
|
|
142
143
|
max_resolution_tier: pluginConfig.max_resolution_tier,
|
|
143
144
|
mp4_support: pluginConfig.mp4_support,
|
|
144
145
|
signed_policy: secrets.enableSignedUrls && pluginConfig.defaultSigned,
|
|
@@ -159,6 +160,7 @@ export default function UploadConfiguration({
|
|
|
159
160
|
}, [])
|
|
160
161
|
if (skipConfig) return null
|
|
161
162
|
|
|
163
|
+
const basicConfig = config.video_quality !== 'plus' && config.video_quality !== 'premium'
|
|
162
164
|
const maxSupportedResolution = RESOLUTION_TIERS.findIndex(
|
|
163
165
|
(rt) => rt.value === pluginConfig.max_resolution_tier
|
|
164
166
|
)
|
|
@@ -198,11 +200,11 @@ export default function UploadConfiguration({
|
|
|
198
200
|
{!disableUploadConfig && (
|
|
199
201
|
<Stack space={3} paddingBottom={2}>
|
|
200
202
|
<FormField
|
|
201
|
-
title="
|
|
203
|
+
title="Video Quality Level"
|
|
202
204
|
description={
|
|
203
205
|
<>
|
|
204
|
-
The
|
|
205
|
-
the asset.{' '}
|
|
206
|
+
The video quality level informs the cost, quality, and available platform features
|
|
207
|
+
for the asset.{' '}
|
|
206
208
|
<a
|
|
207
209
|
href="https://docs.mux.com/guides/use-encoding-tiers"
|
|
208
210
|
target="_blank"
|
|
@@ -214,17 +216,17 @@ export default function UploadConfiguration({
|
|
|
214
216
|
}
|
|
215
217
|
>
|
|
216
218
|
<Flex gap={3}>
|
|
217
|
-
{
|
|
219
|
+
{VIDEO_QUALITY_LEVELS.map(({value, label}) => {
|
|
218
220
|
const inputId = `${id}--encodingtier-${value}`
|
|
219
221
|
return (
|
|
220
222
|
<Flex key={value} align="center" gap={2}>
|
|
221
223
|
<Radio
|
|
222
|
-
checked={config.
|
|
224
|
+
checked={config.video_quality === value}
|
|
223
225
|
name="asset-encodingtier"
|
|
224
226
|
onChange={(e) =>
|
|
225
227
|
dispatch({
|
|
226
|
-
action: '
|
|
227
|
-
value: e.currentTarget.value as UploadConfig['
|
|
228
|
+
action: 'video_quality' as const,
|
|
229
|
+
value: e.currentTarget.value as UploadConfig['video_quality'],
|
|
228
230
|
})
|
|
229
231
|
}
|
|
230
232
|
value={value}
|
|
@@ -239,7 +241,7 @@ export default function UploadConfiguration({
|
|
|
239
241
|
</Flex>
|
|
240
242
|
</FormField>
|
|
241
243
|
|
|
242
|
-
{
|
|
244
|
+
{!basicConfig && maxSupportedResolution > 0 && (
|
|
243
245
|
<FormField
|
|
244
246
|
title="Resolution Tier"
|
|
245
247
|
description={
|
|
@@ -286,12 +288,12 @@ export default function UploadConfiguration({
|
|
|
286
288
|
</FormField>
|
|
287
289
|
)}
|
|
288
290
|
|
|
289
|
-
{
|
|
291
|
+
{!basicConfig && (
|
|
290
292
|
<FormField title="Additional Configuration">
|
|
291
293
|
<Stack space={2}>
|
|
292
294
|
<PlaybackPolicy id={id} config={config} secrets={secrets} dispatch={dispatch} />
|
|
293
295
|
|
|
294
|
-
{
|
|
296
|
+
{!basicConfig && (
|
|
295
297
|
<Flex align="center" gap={2} padding={[0, 2]}>
|
|
296
298
|
<Checkbox
|
|
297
299
|
id={`${id}--mp4_support`}
|
|
@@ -319,7 +321,7 @@ export default function UploadConfiguration({
|
|
|
319
321
|
</Stack>
|
|
320
322
|
)}
|
|
321
323
|
|
|
322
|
-
{!disableTextTrackConfig &&
|
|
324
|
+
{!disableTextTrackConfig && !basicConfig && (
|
|
323
325
|
<TextTracksEditor
|
|
324
326
|
tracks={config.text_tracks}
|
|
325
327
|
dispatch={dispatch}
|
|
@@ -329,9 +331,7 @@ export default function UploadConfiguration({
|
|
|
329
331
|
|
|
330
332
|
<Box marginTop={4}>
|
|
331
333
|
<Button
|
|
332
|
-
disabled={
|
|
333
|
-
config.encoding_tier === 'smart' && !config.public_policy && !config.signed_policy
|
|
334
|
-
}
|
|
334
|
+
disabled={!basicConfig && !config.public_policy && !config.signed_policy}
|
|
335
335
|
icon={UploadIcon}
|
|
336
336
|
text="Upload"
|
|
337
337
|
tone="positive"
|
|
@@ -388,7 +388,7 @@ function formatUploadConfig(config: UploadConfig): MuxNewAssetSettings {
|
|
|
388
388
|
mp4_support: config.mp4_support,
|
|
389
389
|
playback_policy: setPlaybackPolicy(config),
|
|
390
390
|
max_resolution_tier: config.max_resolution_tier,
|
|
391
|
-
|
|
391
|
+
video_quality: config.video_quality,
|
|
392
392
|
normalize_audio: config.normalize_audio,
|
|
393
393
|
}
|
|
394
394
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {SearchIcon} from '@sanity/icons'
|
|
2
|
-
import {Card, Flex, Grid, Label, Stack, Text, TextInput} from '@sanity/ui'
|
|
2
|
+
import {Card, Flex, Grid, Inline, Label, Stack, Text, TextInput} from '@sanity/ui'
|
|
3
3
|
import {useMemo, useState} from 'react'
|
|
4
4
|
|
|
5
5
|
import useAssets from '../hooks/useAssets'
|
|
6
6
|
import type {VideoAssetDocument} from '../util/types'
|
|
7
|
+
import ConfigureApi from './ConfigureApi'
|
|
7
8
|
import ImportVideosFromMux from './ImportVideosFromMux'
|
|
9
|
+
import ResyncMetadata from './ResyncMetadata'
|
|
8
10
|
import {SelectSortOptions} from './SelectSortOptions'
|
|
9
11
|
import SpinnerBox from './SpinnerBox'
|
|
10
12
|
import type {VideoDetailsProps} from './VideoDetails/useVideoDetails'
|
|
@@ -39,7 +41,13 @@ export default function VideosBrowser({onSelect}: VideosBrowserProps) {
|
|
|
39
41
|
/>
|
|
40
42
|
<SelectSortOptions setSort={setSort} sort={sort} />
|
|
41
43
|
</Flex>
|
|
42
|
-
{placement === 'tool' &&
|
|
44
|
+
{placement === 'tool' && (
|
|
45
|
+
<Inline space={2}>
|
|
46
|
+
<ImportVideosFromMux />
|
|
47
|
+
<ResyncMetadata />
|
|
48
|
+
<ConfigureApi />
|
|
49
|
+
</Inline>
|
|
50
|
+
)}
|
|
43
51
|
</Flex>
|
|
44
52
|
<Stack space={3}>
|
|
45
53
|
{assets?.length > 0 && (
|
|
@@ -3,11 +3,11 @@ import {useMemo, useState} from 'react'
|
|
|
3
3
|
import {
|
|
4
4
|
createHookFromObservableFactory,
|
|
5
5
|
type DocumentStore,
|
|
6
|
-
truncateString,
|
|
7
6
|
useClient,
|
|
8
7
|
useDocumentStore,
|
|
9
8
|
} from 'sanity'
|
|
10
9
|
|
|
10
|
+
import {generateAssetPlaceholder} from '../util/assetTitlePlaceholder'
|
|
11
11
|
import {parseMuxDate} from '../util/parsers'
|
|
12
12
|
import type {MuxAsset, VideoAssetDocument} from '../util/types'
|
|
13
13
|
import {SANITY_API_VERSION} from './useClient'
|
|
@@ -37,7 +37,7 @@ export default function useImportMuxAssets() {
|
|
|
37
37
|
const dialogOpen = importState !== 'closed'
|
|
38
38
|
|
|
39
39
|
const muxAssets = useMuxAssets({
|
|
40
|
-
|
|
40
|
+
client,
|
|
41
41
|
enabled: hasSecrets && dialogOpen,
|
|
42
42
|
})
|
|
43
43
|
|
|
@@ -101,7 +101,7 @@ function muxAssetToSanityDocument(asset: MuxAsset): VideoAssetDocument | undefin
|
|
|
101
101
|
_createdAt: parseMuxDate(asset.created_at).toISOString(),
|
|
102
102
|
assetId: asset.id,
|
|
103
103
|
playbackId,
|
|
104
|
-
filename: asset.meta?.title ??
|
|
104
|
+
filename: asset.meta?.title ?? generateAssetPlaceholder(asset.id),
|
|
105
105
|
status: asset.status,
|
|
106
106
|
data: asset,
|
|
107
107
|
}
|