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
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import {useEffect, useState} from 'react'
|
|
2
2
|
import {defer, of, timer} from 'rxjs'
|
|
3
3
|
import {concatMap, expand, tap} from 'rxjs/operators'
|
|
4
|
+
import type {SanityClient} from 'sanity'
|
|
4
5
|
|
|
5
|
-
import
|
|
6
|
+
import {listAssets} from '../actions/assets'
|
|
7
|
+
import type {MuxAsset} from '../util/types'
|
|
6
8
|
|
|
7
|
-
const FIRST_PAGE = 1
|
|
8
9
|
const ASSETS_PER_PAGE = 100
|
|
9
10
|
|
|
10
11
|
type MuxAssetsState = {
|
|
11
|
-
|
|
12
|
+
cursor: string | null
|
|
12
13
|
loading: boolean
|
|
13
14
|
data?: MuxAsset[]
|
|
14
15
|
error?: FetchError
|
|
16
|
+
hasSkippedAssetsWithoutPlayback?: boolean
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
type FetchError =
|
|
@@ -23,49 +25,36 @@ type FetchError =
|
|
|
23
25
|
type PageResult = (
|
|
24
26
|
| {
|
|
25
27
|
data: MuxAsset[]
|
|
28
|
+
next_cursor: string | null
|
|
26
29
|
}
|
|
27
30
|
| {
|
|
28
31
|
error: FetchError
|
|
29
32
|
}
|
|
30
33
|
) & {
|
|
31
|
-
|
|
34
|
+
cursor: string | null
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
38
|
* @docs {@link https://docs.mux.com/api-reference#video/operation/list-assets}
|
|
36
39
|
*/
|
|
37
40
|
async function fetchMuxAssetsPage(
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
client: SanityClient,
|
|
42
|
+
cursor: string | null
|
|
40
43
|
): Promise<PageResult> {
|
|
41
44
|
try {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
Authorization: `Basic ${btoa(`${token}:${secretKey}`)}`,
|
|
47
|
-
},
|
|
48
|
-
}
|
|
49
|
-
)
|
|
50
|
-
const json = await res.json()
|
|
51
|
-
|
|
52
|
-
if (json.error) {
|
|
53
|
-
return {
|
|
54
|
-
pageNum,
|
|
55
|
-
error: {
|
|
56
|
-
_tag: 'MuxError',
|
|
57
|
-
error: json.error,
|
|
58
|
-
},
|
|
59
|
-
}
|
|
60
|
-
}
|
|
45
|
+
const response = await listAssets(client, {
|
|
46
|
+
limit: ASSETS_PER_PAGE,
|
|
47
|
+
cursor,
|
|
48
|
+
})
|
|
61
49
|
|
|
62
50
|
return {
|
|
63
|
-
|
|
64
|
-
data:
|
|
51
|
+
cursor,
|
|
52
|
+
data: response.data as MuxAsset[],
|
|
53
|
+
next_cursor: response.next_cursor || null,
|
|
65
54
|
}
|
|
66
55
|
} catch (error) {
|
|
67
56
|
return {
|
|
68
|
-
|
|
57
|
+
cursor,
|
|
69
58
|
error: {_tag: 'FetchError'},
|
|
70
59
|
}
|
|
71
60
|
}
|
|
@@ -76,66 +65,88 @@ function accumulateIntermediateState(
|
|
|
76
65
|
pageResult: PageResult
|
|
77
66
|
): MuxAssetsState {
|
|
78
67
|
const currentData = ('data' in currentState && currentState.data) || []
|
|
68
|
+
const newAssets = ('data' in pageResult && pageResult.data) || []
|
|
69
|
+
|
|
70
|
+
// Filter assets and check for skipped items
|
|
71
|
+
const {validAssets, skippedInThisPage} = newAssets.reduce<{
|
|
72
|
+
validAssets: MuxAsset[]
|
|
73
|
+
skippedInThisPage: boolean
|
|
74
|
+
}>(
|
|
75
|
+
(acc, asset) => {
|
|
76
|
+
const hasPlaybackIds = asset.playback_ids && asset.playback_ids.length > 0
|
|
77
|
+
const isDuplicate = currentData.some((a) => a.id === asset.id)
|
|
78
|
+
|
|
79
|
+
if (!hasPlaybackIds) {
|
|
80
|
+
acc.skippedInThisPage = true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (hasPlaybackIds && !isDuplicate) {
|
|
84
|
+
acc.validAssets.push(asset)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return acc
|
|
88
|
+
},
|
|
89
|
+
{validAssets: [], skippedInThisPage: false}
|
|
90
|
+
)
|
|
91
|
+
|
|
79
92
|
return {
|
|
80
93
|
...currentState,
|
|
81
|
-
data: [
|
|
82
|
-
...currentData,
|
|
83
|
-
...(('data' in pageResult && pageResult.data) || []).filter(
|
|
84
|
-
// De-duplicate assets for safety
|
|
85
|
-
(asset) => !currentData.some((a) => a.id === asset.id)
|
|
86
|
-
),
|
|
87
|
-
],
|
|
94
|
+
data: [...currentData, ...validAssets],
|
|
88
95
|
error:
|
|
89
96
|
'error' in pageResult
|
|
90
97
|
? pageResult.error
|
|
91
98
|
: // Reset error if current page is successful
|
|
92
99
|
undefined,
|
|
93
|
-
|
|
100
|
+
cursor: 'next_cursor' in pageResult ? pageResult.next_cursor : pageResult.cursor,
|
|
94
101
|
loading: true,
|
|
102
|
+
hasSkippedAssetsWithoutPlayback:
|
|
103
|
+
currentState.hasSkippedAssetsWithoutPlayback || skippedInThisPage,
|
|
95
104
|
}
|
|
96
105
|
}
|
|
97
106
|
|
|
98
107
|
function hasMorePages(pageResult: PageResult) {
|
|
99
108
|
return (
|
|
100
|
-
typeof pageResult === 'object' &&
|
|
101
|
-
'data' in pageResult &&
|
|
102
|
-
Array.isArray(pageResult.data) &&
|
|
103
|
-
pageResult.data.length > 0
|
|
109
|
+
typeof pageResult === 'object' && 'next_cursor' in pageResult && pageResult.next_cursor !== null
|
|
104
110
|
)
|
|
105
111
|
}
|
|
106
112
|
|
|
107
113
|
/**
|
|
108
114
|
* Fetches all assets from a Mux environment. Rules:
|
|
109
115
|
* - One page at a time
|
|
110
|
-
* -
|
|
111
|
-
* - We've finished fetching
|
|
116
|
+
* - Uses cursor-based pagination
|
|
117
|
+
* - We've finished fetching when `next_cursor` is null
|
|
112
118
|
* - Rate limiting to one request per 2 seconds
|
|
113
119
|
* - Update state while still fetching to give feedback to users
|
|
114
120
|
*/
|
|
115
|
-
export default function useMuxAssets({
|
|
116
|
-
const [state, setState] = useState<MuxAssetsState>({loading: true,
|
|
121
|
+
export default function useMuxAssets({client, enabled}: {client: SanityClient; enabled: boolean}) {
|
|
122
|
+
const [state, setState] = useState<MuxAssetsState>({loading: true, cursor: null})
|
|
117
123
|
|
|
118
124
|
useEffect(() => {
|
|
119
125
|
if (!enabled) return
|
|
120
126
|
|
|
121
127
|
const subscription = defer(() =>
|
|
122
128
|
fetchMuxAssetsPage(
|
|
123
|
-
|
|
124
|
-
// When we've already successfully loaded before (fully or partially), we start from the
|
|
125
|
-
'data' in state && state.data && state.data.length > 0 && !state.error
|
|
126
|
-
? state.pageNum + 1
|
|
127
|
-
: state.pageNum
|
|
129
|
+
client,
|
|
130
|
+
// When we've already successfully loaded before (fully or partially), we start from the next cursor to avoid re-fetching
|
|
131
|
+
'data' in state && state.data && state.data.length > 0 && !state.error ? state.cursor : null
|
|
128
132
|
)
|
|
129
133
|
)
|
|
130
134
|
.pipe(
|
|
131
|
-
// Here we
|
|
135
|
+
// Here we use "expand" to recursively fetch next pages
|
|
132
136
|
expand((pageResult) => {
|
|
133
|
-
// if fetched page has
|
|
137
|
+
// if fetched page has next_cursor, we continue emitting, requesting the next page
|
|
134
138
|
// after 2s to avoid rate limiting
|
|
135
139
|
if (hasMorePages(pageResult)) {
|
|
136
140
|
return timer(2000).pipe(
|
|
137
|
-
|
|
138
|
-
|
|
141
|
+
concatMap(() =>
|
|
142
|
+
// eslint-disable-next-line max-nested-callbacks
|
|
143
|
+
defer(() =>
|
|
144
|
+
fetchMuxAssetsPage(
|
|
145
|
+
client,
|
|
146
|
+
'next_cursor' in pageResult ? pageResult.next_cursor : null
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
)
|
|
139
150
|
)
|
|
140
151
|
}
|
|
141
152
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {useMemo, useState} from 'react'
|
|
2
|
+
import {
|
|
3
|
+
createHookFromObservableFactory,
|
|
4
|
+
type DocumentStore,
|
|
5
|
+
useClient,
|
|
6
|
+
useDocumentStore,
|
|
7
|
+
} from 'sanity'
|
|
8
|
+
|
|
9
|
+
import {isEmptyOrPlaceholderTitle} from '../util/assetTitlePlaceholder'
|
|
10
|
+
import type {MuxAsset, VideoAssetDocument} from '../util/types'
|
|
11
|
+
import {SANITY_API_VERSION} from './useClient'
|
|
12
|
+
import useMuxAssets from './useMuxAssets'
|
|
13
|
+
import {useSecretsDocumentValues} from './useSecretsDocumentValues'
|
|
14
|
+
|
|
15
|
+
type ResyncState = 'closed' | 'idle' | 'syncing' | 'done' | 'error'
|
|
16
|
+
|
|
17
|
+
export type MatchedAsset = {
|
|
18
|
+
sanityDoc: VideoAssetDocument
|
|
19
|
+
muxAsset: MuxAsset | undefined
|
|
20
|
+
muxTitle: string | undefined
|
|
21
|
+
currentTitle: string | undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function useResyncMuxMetadata() {
|
|
25
|
+
const documentStore = useDocumentStore()
|
|
26
|
+
const client = useClient({
|
|
27
|
+
apiVersion: SANITY_API_VERSION,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const [sanityAssets, sanityAssetsLoading] = useSanityAssets(documentStore)
|
|
31
|
+
|
|
32
|
+
const secretDocumentValues = useSecretsDocumentValues()
|
|
33
|
+
const hasSecrets = !!secretDocumentValues.value.secrets?.secretKey
|
|
34
|
+
|
|
35
|
+
const [resyncError, setResyncError] = useState<unknown>()
|
|
36
|
+
const [resyncState, setResyncState] = useState<ResyncState>('closed')
|
|
37
|
+
const dialogOpen = resyncState !== 'closed'
|
|
38
|
+
|
|
39
|
+
const muxAssets = useMuxAssets({
|
|
40
|
+
client,
|
|
41
|
+
enabled: hasSecrets && dialogOpen,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const matchedAssets = useMemo(() => {
|
|
45
|
+
return sanityAssets && muxAssets.data
|
|
46
|
+
? sanityAssets.map((sanityDoc) => {
|
|
47
|
+
const muxAsset = muxAssets.data?.find(
|
|
48
|
+
(m) => m.id === sanityDoc.assetId || m.id === sanityDoc.data?.id
|
|
49
|
+
)
|
|
50
|
+
return {
|
|
51
|
+
sanityDoc,
|
|
52
|
+
muxAsset,
|
|
53
|
+
muxTitle: muxAsset?.meta?.title,
|
|
54
|
+
currentTitle: sanityDoc.filename,
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
: undefined
|
|
58
|
+
}, [sanityAssets, muxAssets.data])
|
|
59
|
+
|
|
60
|
+
const closeDialog = () => {
|
|
61
|
+
if (resyncState !== 'syncing') setResyncState('closed')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const openDialog = () => {
|
|
65
|
+
if (resyncState === 'closed') setResyncState('idle')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function syncAllVideos() {
|
|
69
|
+
if (!matchedAssets) return
|
|
70
|
+
|
|
71
|
+
setResyncState('syncing')
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const tx = client.transaction()
|
|
75
|
+
|
|
76
|
+
matchedAssets.forEach((matched) => {
|
|
77
|
+
// Update all videos with the Mux title, even if it's undefined/empty
|
|
78
|
+
const newTitle = matched.muxTitle || ''
|
|
79
|
+
tx.patch(matched.sanityDoc._id, {set: {filename: newTitle}})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
await tx.commit({returnDocuments: false})
|
|
83
|
+
setResyncState('done')
|
|
84
|
+
} catch (error) {
|
|
85
|
+
setResyncState('error')
|
|
86
|
+
setResyncError(error)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function syncOnlyEmpty() {
|
|
91
|
+
if (!matchedAssets) return
|
|
92
|
+
|
|
93
|
+
setResyncState('syncing')
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const tx = client.transaction()
|
|
97
|
+
|
|
98
|
+
matchedAssets.forEach((matched) => {
|
|
99
|
+
// Only update if the current title is empty or has the placeholder format
|
|
100
|
+
// AND there's a new title available from Mux
|
|
101
|
+
if (
|
|
102
|
+
matched.muxAsset &&
|
|
103
|
+
matched.muxTitle &&
|
|
104
|
+
isEmptyOrPlaceholderTitle(matched.currentTitle, matched.muxAsset.id)
|
|
105
|
+
) {
|
|
106
|
+
tx.patch(matched.sanityDoc._id, {set: {filename: matched.muxTitle}})
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
await tx.commit({returnDocuments: false})
|
|
111
|
+
setResyncState('done')
|
|
112
|
+
} catch (error) {
|
|
113
|
+
setResyncState('error')
|
|
114
|
+
setResyncError(error)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
sanityAssetsLoading,
|
|
120
|
+
closeDialog,
|
|
121
|
+
dialogOpen,
|
|
122
|
+
resyncState,
|
|
123
|
+
resyncError,
|
|
124
|
+
hasSecrets,
|
|
125
|
+
syncAllVideos,
|
|
126
|
+
syncOnlyEmpty,
|
|
127
|
+
matchedAssets,
|
|
128
|
+
muxAssets,
|
|
129
|
+
openDialog,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const useSanityAssets = createHookFromObservableFactory<VideoAssetDocument[], DocumentStore>(
|
|
134
|
+
(documentStore) => {
|
|
135
|
+
return documentStore.listenQuery(
|
|
136
|
+
/* groq */ `*[_type == "mux.videoAsset"]`,
|
|
137
|
+
{},
|
|
138
|
+
{
|
|
139
|
+
apiVersion: SANITY_API_VERSION,
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
)
|
package/src/schema.ts
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {truncateString} from 'sanity'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generates a placeholder title for a Mux asset when no title is available.
|
|
5
|
+
* This format is used when importing assets that don't have a title in Mux.
|
|
6
|
+
*
|
|
7
|
+
* @param assetId - The Mux asset ID
|
|
8
|
+
* @returns A placeholder title in the format "Asset #[truncated-id]"
|
|
9
|
+
*/
|
|
10
|
+
export function generateAssetPlaceholder(assetId: string): string {
|
|
11
|
+
return `Asset #${truncateString(assetId, 15)}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Checks if a filename is empty or has the placeholder format.
|
|
16
|
+
* This is used to determine if a video title should be updated during metadata sync.
|
|
17
|
+
*
|
|
18
|
+
* @param filename - The current filename/title of the video
|
|
19
|
+
* @param assetId - The Mux asset ID to check against
|
|
20
|
+
* @returns true if the filename is empty or matches the placeholder format
|
|
21
|
+
*/
|
|
22
|
+
export function isEmptyOrPlaceholderTitle(filename: string | undefined, assetId: string): boolean {
|
|
23
|
+
// Check if filename is empty/undefined
|
|
24
|
+
if (!filename || filename.trim() === '') {
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check if filename matches the placeholder format for this asset
|
|
29
|
+
const placeholder = generateAssetPlaceholder(assetId)
|
|
30
|
+
return filename === placeholder
|
|
31
|
+
}
|
package/src/util/types.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type {PartialDeep} from 'type-fest'
|
|
|
4
4
|
export interface MuxInputConfig {
|
|
5
5
|
/**
|
|
6
6
|
* Enable static renditions by setting this to 'standard'. Can be overwritten on a per-asset basis.
|
|
7
|
-
* Requires `"
|
|
7
|
+
* Requires `"video_quality": "plus"`
|
|
8
8
|
* @see {@link https://docs.mux.com/guides/video/enable-static-mp4-renditions#why-enable-mp4-support}
|
|
9
9
|
* @defaultValue 'none'
|
|
10
10
|
*/
|
|
@@ -12,18 +12,27 @@ export interface MuxInputConfig {
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Max resolution tier can be used to control the maximum resolution_tier your asset is encoded, stored, and streamed at.
|
|
15
|
-
* Requires `"
|
|
15
|
+
* Requires `"video_quality": "plus"`
|
|
16
16
|
* @see {@link https://docs.mux.com/guides/stream-videos-in-4k}
|
|
17
17
|
* @defaultValue '1080p'
|
|
18
18
|
*/
|
|
19
19
|
max_resolution_tier: '2160p' | '1440p' | '1080p'
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
+
* @deprecated Use {@link video_quality}
|
|
23
|
+
* <br>
|
|
22
24
|
* The encoding tier informs the cost, quality, and available platform features for the asset.
|
|
23
25
|
* @see {@link https://docs.mux.com/guides/use-encoding-tiers}
|
|
24
26
|
* @defaultValue 'smart'
|
|
25
27
|
*/
|
|
26
|
-
encoding_tier
|
|
28
|
+
encoding_tier?: 'baseline' | 'smart'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The video quality level informs the cost, quality, and available platform features for the asset.
|
|
32
|
+
* @see {@link https://www.mux.com/docs/guides/use-video-quality-levels}
|
|
33
|
+
* @defaultValue 'plus'
|
|
34
|
+
*/
|
|
35
|
+
video_quality: 'basic' | 'plus' | 'premium'
|
|
27
36
|
|
|
28
37
|
/**
|
|
29
38
|
* Normalize the audio track loudness level.
|
|
@@ -41,7 +50,7 @@ export interface MuxInputConfig {
|
|
|
41
50
|
|
|
42
51
|
/**
|
|
43
52
|
* Auto-generate captions for these languages by default.
|
|
44
|
-
* Requires `"
|
|
53
|
+
* Requires `"video_quality": "plus"`
|
|
45
54
|
*
|
|
46
55
|
* @see {@link https://docs.mux.com/guides/add-autogenerated-captions-and-use-transcripts}
|
|
47
56
|
* @deprecated use `defaultAutogeneratedSubtitleLang` instead. Only a single autogenerated
|
|
@@ -50,7 +59,7 @@ export interface MuxInputConfig {
|
|
|
50
59
|
|
|
51
60
|
/**
|
|
52
61
|
* Auto-generate captions for this language by default. Users can still
|
|
53
|
-
* Requires `"
|
|
62
|
+
* Requires `"video_quality": "plus"`
|
|
54
63
|
*
|
|
55
64
|
* @see {@link https://docs.mux.com/guides/add-autogenerated-captions-and-use-transcripts}
|
|
56
65
|
*/
|
|
@@ -123,9 +132,10 @@ export const SUPPORTED_MUX_LANGUAGES = [
|
|
|
123
132
|
{label: 'Bulgarian', code: 'bg', state: 'Beta'},
|
|
124
133
|
] as const
|
|
125
134
|
|
|
126
|
-
export const
|
|
127
|
-
{label: '
|
|
128
|
-
{label: '
|
|
135
|
+
export const VIDEO_QUALITY_LEVELS = [
|
|
136
|
+
{label: 'Basic', value: 'basic'},
|
|
137
|
+
{label: 'Plus', value: 'plus'},
|
|
138
|
+
{label: 'Premium', value: 'premium'},
|
|
129
139
|
] as const
|
|
130
140
|
|
|
131
141
|
export const SUPPORTED_MUX_LANGUAGES_VALUES = SUPPORTED_MUX_LANGUAGES.map((l) => l.code)
|
|
@@ -168,7 +178,7 @@ export type UploadTextTrack = AutogeneratedTextTrack | CustomTextTrack
|
|
|
168
178
|
export interface UploadConfig
|
|
169
179
|
extends Pick<
|
|
170
180
|
MuxInputConfig,
|
|
171
|
-
'
|
|
181
|
+
'max_resolution_tier' | 'mp4_support' | 'normalize_audio' | 'video_quality'
|
|
172
182
|
> {
|
|
173
183
|
text_tracks: UploadTextTrack[]
|
|
174
184
|
signed_policy: boolean
|
|
@@ -182,7 +192,7 @@ export interface UploadConfig
|
|
|
182
192
|
export interface MuxNewAssetSettings
|
|
183
193
|
extends Pick<
|
|
184
194
|
MuxInputConfig,
|
|
185
|
-
'
|
|
195
|
+
'max_resolution_tier' | 'mp4_support' | 'normalize_audio' | 'video_quality'
|
|
186
196
|
> {
|
|
187
197
|
/** An array of objects that each describe an input file to be used to create the asset.*/
|
|
188
198
|
input?: {
|