sanity-plugin-mux-input 2.14.0 → 2.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -24
- package/dist/index.d.mts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +771 -351
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +773 -353
- 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 +1 -1
- package/src/components/SelectAsset.tsx +9 -3
- package/src/components/StudioTool.tsx +2 -2
- package/src/components/UploadConfiguration.tsx +104 -343
- package/src/components/Uploader.tsx +18 -7
- package/src/components/VideoDetails/VideoDetails.tsx +28 -8
- 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/useSaveSecrets.ts +10 -3
- package/src/hooks/useSecretsDocumentValues.ts +9 -1
- package/src/hooks/useSecretsFormState.ts +6 -3
- 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
package/package.json
CHANGED
package/src/_exports/index.ts
CHANGED
package/src/actions/secrets.ts
CHANGED
|
@@ -9,6 +9,7 @@ interface SecretsDocument {
|
|
|
9
9
|
enableSignedUrls: boolean
|
|
10
10
|
signingKeyId: string
|
|
11
11
|
signingKeyPrivate: string
|
|
12
|
+
drmConfigId: string
|
|
12
13
|
}
|
|
13
14
|
// eslint-disable-next-line max-params
|
|
14
15
|
export function saveSecrets(
|
|
@@ -17,7 +18,8 @@ export function saveSecrets(
|
|
|
17
18
|
secretKey: string,
|
|
18
19
|
enableSignedUrls: boolean,
|
|
19
20
|
signingKeyId: string,
|
|
20
|
-
signingKeyPrivate: string
|
|
21
|
+
signingKeyPrivate: string,
|
|
22
|
+
drmConfigId: string
|
|
21
23
|
): Promise<SecretsDocument> {
|
|
22
24
|
const doc: SecretsDocument = {
|
|
23
25
|
_id: 'secrets.mux',
|
|
@@ -27,7 +29,10 @@ export function saveSecrets(
|
|
|
27
29
|
enableSignedUrls,
|
|
28
30
|
signingKeyId,
|
|
29
31
|
signingKeyPrivate,
|
|
32
|
+
drmConfigId,
|
|
30
33
|
}
|
|
34
|
+
doc.signingKeyId = enableSignedUrls ? signingKeyId : ''
|
|
35
|
+
doc.signingKeyPrivate = enableSignedUrls ? signingKeyPrivate : ''
|
|
31
36
|
|
|
32
37
|
return client.createOrReplace(doc)
|
|
33
38
|
}
|
package/src/actions/upload.ts
CHANGED
|
@@ -158,7 +158,7 @@ type UploadResponse = {
|
|
|
158
158
|
new_asset_settings: {
|
|
159
159
|
static_renditions?: {resolution: string}[]
|
|
160
160
|
passthrough: string
|
|
161
|
-
playback_policies: ['public' | 'signed']
|
|
161
|
+
playback_policies: ['public' | 'signed' | 'drm']
|
|
162
162
|
}
|
|
163
163
|
status: string
|
|
164
164
|
timeout: number
|
|
@@ -32,7 +32,7 @@ export interface ConfigureApiDialogProps {
|
|
|
32
32
|
secrets: Secrets
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const fieldNames = ['token', 'secretKey', 'enableSignedUrls'] as const
|
|
35
|
+
const fieldNames = ['token', 'secretKey', 'enableSignedUrls', 'drmConfigId'] as const
|
|
36
36
|
|
|
37
37
|
// Internal dialog component that can be used with external state
|
|
38
38
|
export function ConfigureApiDialog({secrets, setDialogState}: ConfigureApiDialogProps) {
|
|
@@ -44,11 +44,12 @@ export function ConfigureApiDialog({secrets, setDialogState}: ConfigureApiDialog
|
|
|
44
44
|
() =>
|
|
45
45
|
secrets.token !== state.token ||
|
|
46
46
|
secrets.secretKey !== state.secretKey ||
|
|
47
|
-
secrets.enableSignedUrls !== state.enableSignedUrls
|
|
47
|
+
secrets.enableSignedUrls !== state.enableSignedUrls ||
|
|
48
|
+
secrets.drmConfigId !== state.drmConfigId,
|
|
48
49
|
[secrets, state]
|
|
49
50
|
)
|
|
50
51
|
const id = `ConfigureApi${useId()}`
|
|
51
|
-
const [tokenId, secretKeyId, enableSignedUrlsId] = useMemo<typeof fieldNames>(
|
|
52
|
+
const [tokenId, secretKeyId, enableSignedUrlsId, drmConfigIdId] = useMemo<typeof fieldNames>(
|
|
52
53
|
() => fieldNames.map((field) => `${id}-${field}`) as unknown as typeof fieldNames,
|
|
53
54
|
[id]
|
|
54
55
|
)
|
|
@@ -63,8 +64,8 @@ export function ConfigureApiDialog({secrets, setDialogState}: ConfigureApiDialog
|
|
|
63
64
|
if (!saving.current && event.currentTarget.reportValidity()) {
|
|
64
65
|
saving.current = true
|
|
65
66
|
dispatch({type: 'submit'})
|
|
66
|
-
const {token, secretKey, enableSignedUrls} = state
|
|
67
|
-
handleSaveSecrets({token, secretKey, enableSignedUrls})
|
|
67
|
+
const {token, secretKey, enableSignedUrls, drmConfigId} = state
|
|
68
|
+
handleSaveSecrets({token, secretKey, enableSignedUrls, drmConfigId})
|
|
68
69
|
.then((savedSecrets) => {
|
|
69
70
|
const {projectId, dataset} = client.config()
|
|
70
71
|
clear([cacheNs, secretsId, projectId, dataset])
|
|
@@ -106,6 +107,15 @@ export function ConfigureApiDialog({secrets, setDialogState}: ConfigureApiDialog
|
|
|
106
107
|
},
|
|
107
108
|
[dispatch]
|
|
108
109
|
)
|
|
110
|
+
const handleChangeDrmConfigId = useCallback(
|
|
111
|
+
(event: React.FormEvent<HTMLInputElement>) => {
|
|
112
|
+
dispatch({
|
|
113
|
+
type: 'change',
|
|
114
|
+
payload: {name: 'drmConfigId', value: event.currentTarget.value},
|
|
115
|
+
})
|
|
116
|
+
},
|
|
117
|
+
[dispatch]
|
|
118
|
+
)
|
|
109
119
|
|
|
110
120
|
useEffect(() => {
|
|
111
121
|
if (firstField.current) {
|
|
@@ -202,6 +212,42 @@ export function ConfigureApiDialog({secrets, setDialogState}: ConfigureApiDialog
|
|
|
202
212
|
) : null}
|
|
203
213
|
</Stack>
|
|
204
214
|
|
|
215
|
+
<FormField title="DRM Configuration ID" inputId={drmConfigIdId}>
|
|
216
|
+
<TextInput
|
|
217
|
+
id={drmConfigIdId}
|
|
218
|
+
onChange={handleChangeDrmConfigId}
|
|
219
|
+
type="text"
|
|
220
|
+
value={state.drmConfigId ?? ''}
|
|
221
|
+
required={false}
|
|
222
|
+
/>
|
|
223
|
+
</FormField>
|
|
224
|
+
<Card padding={[3, 3, 3]} radius={2} shadow={1} tone="neutral">
|
|
225
|
+
<Stack space={3}>
|
|
226
|
+
<Text size={1}>
|
|
227
|
+
DRM (Digital Rights Management) provides an extra layer of content security for
|
|
228
|
+
video content streamed from Mux. For additional information check out our{' '}
|
|
229
|
+
<a
|
|
230
|
+
href="https://www.mux.com/docs/guides/protect-videos-with-drm#play-drm-protected-videos"
|
|
231
|
+
target="_blank"
|
|
232
|
+
rel="noopener noreferrer"
|
|
233
|
+
>
|
|
234
|
+
DRM Guide
|
|
235
|
+
</a>
|
|
236
|
+
.
|
|
237
|
+
</Text>
|
|
238
|
+
<Text size={1}>
|
|
239
|
+
<a
|
|
240
|
+
href="https://www.mux.com/support/human"
|
|
241
|
+
target="_blank"
|
|
242
|
+
rel="noopener noreferrer"
|
|
243
|
+
>
|
|
244
|
+
Contact us
|
|
245
|
+
</a>{' '}
|
|
246
|
+
to get started using DRM.
|
|
247
|
+
</Text>
|
|
248
|
+
</Stack>
|
|
249
|
+
</Card>
|
|
250
|
+
|
|
205
251
|
<Inline space={2}>
|
|
206
252
|
<Button
|
|
207
253
|
text="Save"
|
|
@@ -18,7 +18,7 @@ import {useEffect, useId, useRef, useState} from 'react'
|
|
|
18
18
|
import {addTextTrackFromUrl, deleteTextTrack, getAsset} from '../actions/assets'
|
|
19
19
|
import {useClient} from '../hooks/useClient'
|
|
20
20
|
import {generateJwt} from '../util/generateJwt'
|
|
21
|
-
import {getPlaybackId} from '../util/
|
|
21
|
+
import {getPlaybackId} from '../util/getPlaybackPolicy'
|
|
22
22
|
import {getPlaybackPolicy} from '../util/getPlaybackPolicy'
|
|
23
23
|
import {downloadVttFile, extractErrorMessage, pollTrackStatus} from '../util/textTracks'
|
|
24
24
|
import type {MuxTextTrack, VideoAssetDocument} from '../util/types'
|
|
@@ -276,7 +276,7 @@ export default function EditCaptionDialog({asset, track, onUpdate, onClose}: Pro
|
|
|
276
276
|
const playbackId = getPlaybackId(asset)
|
|
277
277
|
if (!playbackId) return ''
|
|
278
278
|
let url = `https://stream.mux.com/${playbackId}/text/${track.id}.vtt`
|
|
279
|
-
if (getPlaybackPolicy(asset) === 'signed') {
|
|
279
|
+
if (getPlaybackPolicy(asset)?.policy === 'signed') {
|
|
280
280
|
const token = generateJwt(client, playbackId, 'v')
|
|
281
281
|
url += `?token=${token}`
|
|
282
282
|
}
|
|
@@ -16,7 +16,8 @@ export default function InputBrowser({
|
|
|
16
16
|
setDialogState,
|
|
17
17
|
asset,
|
|
18
18
|
onChange,
|
|
19
|
-
|
|
19
|
+
config,
|
|
20
|
+
}: Pick<SelectAssetProps, 'onChange' | 'asset' | 'config'> & {
|
|
20
21
|
setDialogState: SetDialogState
|
|
21
22
|
}) {
|
|
22
23
|
const id = `InputBrowser${useId()}`
|
|
@@ -29,7 +30,12 @@ export default function InputBrowser({
|
|
|
29
30
|
onClose={handleClose}
|
|
30
31
|
width={2}
|
|
31
32
|
>
|
|
32
|
-
<SelectAsset
|
|
33
|
+
<SelectAsset
|
|
34
|
+
config={config}
|
|
35
|
+
asset={asset}
|
|
36
|
+
onChange={onChange}
|
|
37
|
+
setDialogState={setDialogState}
|
|
38
|
+
/>
|
|
33
39
|
</StyledDialog>
|
|
34
40
|
)
|
|
35
41
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-shadow */
|
|
2
1
|
import {ChevronLeftIcon, ChevronRightIcon} from '@sanity/icons'
|
|
3
2
|
import {Button, Label} from '@sanity/ui'
|
|
4
3
|
import {Dispatch, SetStateAction, useEffect} from 'react'
|
|
@@ -7,8 +6,6 @@ const PageSelector = (props: {
|
|
|
7
6
|
page: number
|
|
8
7
|
setPage: Dispatch<SetStateAction<number>>
|
|
9
8
|
total: number
|
|
10
|
-
// eslint-disable-next-line react/no-unused-prop-types
|
|
11
|
-
limit: number
|
|
12
9
|
}) => {
|
|
13
10
|
const page = props.page
|
|
14
11
|
const setPage = props.setPage
|
|
@@ -30,8 +27,8 @@ const PageSelector = (props: {
|
|
|
30
27
|
style={{cursor: 'pointer'}}
|
|
31
28
|
disabled={page <= 0}
|
|
32
29
|
onClick={() => {
|
|
33
|
-
setPage((
|
|
34
|
-
return Math.min(props.total - 1, Math.max(0,
|
|
30
|
+
setPage((p) => {
|
|
31
|
+
return Math.min(props.total - 1, Math.max(0, p - 1))
|
|
35
32
|
})
|
|
36
33
|
}}
|
|
37
34
|
/>
|
|
@@ -45,8 +42,8 @@ const PageSelector = (props: {
|
|
|
45
42
|
style={{cursor: 'pointer'}}
|
|
46
43
|
disabled={page >= props.total - 1}
|
|
47
44
|
onClick={() => {
|
|
48
|
-
setPage((
|
|
49
|
-
return Math.min(props.total - 1, Math.max(0,
|
|
45
|
+
setPage((p) => {
|
|
46
|
+
return Math.min(props.total - 1, Math.max(0, p + 1))
|
|
50
47
|
})
|
|
51
48
|
}}
|
|
52
49
|
/>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {useState} from 'react'
|
|
1
|
+
import {Suspense, useState} from 'react'
|
|
2
2
|
import {styled} from 'styled-components'
|
|
3
3
|
|
|
4
4
|
import {useClient} from '../hooks/useClient'
|
|
@@ -46,5 +46,10 @@ export function ThumbnailsMetadataTrack({asset}: ThumbnailsMetadataTrackProps) {
|
|
|
46
46
|
// Why useState instead of useMemo? Because we really really only want to run it exactly once and useMemo doesn't make that guarantee
|
|
47
47
|
const [src] = useState<string>(() => getStoryboardSrc({asset, client}))
|
|
48
48
|
|
|
49
|
-
return
|
|
49
|
+
return (
|
|
50
|
+
/* We use Suspense here because `getStoryboardSrc` uses suspend() under the hood */
|
|
51
|
+
<Suspense fallback={null}>
|
|
52
|
+
<track label="thumbnails" default kind="metadata" src={src} />
|
|
53
|
+
</Suspense>
|
|
54
|
+
)
|
|
50
55
|
}
|
|
@@ -64,7 +64,7 @@ function PlayerActionsMenu(
|
|
|
64
64
|
const {asset, readOnly, dialogState, setDialogState, onChange, onSelect, accept} = props
|
|
65
65
|
const [open, setOpen] = useState(false)
|
|
66
66
|
const [menuElement, setMenuRef] = useState<HTMLDivElement | null>(null)
|
|
67
|
-
const isSigned = useMemo(() => getPlaybackPolicy(asset) === 'signed', [asset])
|
|
67
|
+
const isSigned = useMemo(() => getPlaybackPolicy(asset)?.policy === 'signed', [asset])
|
|
68
68
|
const {hasConfigAccess} = useAccessControl(props.config)
|
|
69
69
|
|
|
70
70
|
const onReset = useCallback(() => onChange(PatchEvent.from(unset([]))), [onChange])
|
|
@@ -2,15 +2,21 @@ import {useCallback} from 'react'
|
|
|
2
2
|
import {PatchEvent, set, setIfMissing, unset} from 'sanity'
|
|
3
3
|
|
|
4
4
|
import type {SetDialogState} from '../hooks/useDialogState'
|
|
5
|
-
import type {MuxInputProps, VideoAssetDocument} from '../util/types'
|
|
5
|
+
import type {MuxInputProps, PluginConfig, VideoAssetDocument} from '../util/types'
|
|
6
6
|
import VideosBrowser, {type VideosBrowserProps} from './VideosBrowser'
|
|
7
7
|
|
|
8
8
|
export interface Props extends Pick<MuxInputProps, 'onChange'> {
|
|
9
9
|
asset?: VideoAssetDocument | null | undefined
|
|
10
10
|
setDialogState: SetDialogState
|
|
11
|
+
config: PluginConfig
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export default function SelectAssets({
|
|
14
|
+
export default function SelectAssets({
|
|
15
|
+
asset: selectedAsset,
|
|
16
|
+
onChange,
|
|
17
|
+
setDialogState,
|
|
18
|
+
config,
|
|
19
|
+
}: Props) {
|
|
14
20
|
const handleSelect = useCallback<Required<VideosBrowserProps>['onSelect']>(
|
|
15
21
|
(chosenAsset) => {
|
|
16
22
|
if (!chosenAsset?._id) {
|
|
@@ -29,5 +35,5 @@ export default function SelectAssets({asset: selectedAsset, onChange, setDialogS
|
|
|
29
35
|
[onChange, setDialogState, selectedAsset]
|
|
30
36
|
)
|
|
31
37
|
|
|
32
|
-
return <VideosBrowser onSelect={handleSelect} />
|
|
38
|
+
return <VideosBrowser onSelect={handleSelect} config={config} />
|
|
33
39
|
}
|
|
@@ -4,8 +4,8 @@ import type {PluginConfig} from '../util/types'
|
|
|
4
4
|
import ToolIcon from './icons/ToolIcon'
|
|
5
5
|
import VideosBrowser from './VideosBrowser'
|
|
6
6
|
|
|
7
|
-
const StudioTool: React.FC<PluginConfig> = () => {
|
|
8
|
-
return <VideosBrowser />
|
|
7
|
+
const StudioTool: React.FC<PluginConfig> = (config) => {
|
|
8
|
+
return <VideosBrowser config={config} />
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export const DEFAULT_TOOL_CONFIG = {
|