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
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ErrorOutlineIcon,
|
|
8
8
|
RevertIcon,
|
|
9
9
|
SearchIcon,
|
|
10
|
+
SyncIcon,
|
|
10
11
|
TagIcon,
|
|
11
12
|
TrashIcon,
|
|
12
13
|
} from '@sanity/icons'
|
|
@@ -27,7 +28,7 @@ import {
|
|
|
27
28
|
import React, {useEffect, useState} from 'react'
|
|
28
29
|
|
|
29
30
|
import {DIALOGS_Z_INDEX} from '../../util/constants'
|
|
30
|
-
import
|
|
31
|
+
import {MuxPlaybackId, MuxTextTrack, PlaybackPolicy} from '../../util/types'
|
|
31
32
|
import FormField from '../FormField'
|
|
32
33
|
import IconInfo from '../IconInfo'
|
|
33
34
|
import {ResolutionIcon} from '../icons/Resolution'
|
|
@@ -71,6 +72,8 @@ const VideoDetails: React.FC<VideoDetailsProps> = (props) => {
|
|
|
71
72
|
handleClose,
|
|
72
73
|
confirmClose,
|
|
73
74
|
saveChanges,
|
|
75
|
+
handleResync,
|
|
76
|
+
isResyncing,
|
|
74
77
|
} = useVideoDetails(props)
|
|
75
78
|
|
|
76
79
|
const isSaving = state === 'saving'
|
|
@@ -97,16 +100,29 @@ const VideoDetails: React.FC<VideoDetailsProps> = (props) => {
|
|
|
97
100
|
footer={
|
|
98
101
|
<Card padding={3}>
|
|
99
102
|
<Flex justify="space-between" align="center">
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
<Flex gap={2}>
|
|
104
|
+
<Button
|
|
105
|
+
icon={TrashIcon}
|
|
106
|
+
fontSize={2}
|
|
107
|
+
padding={3}
|
|
108
|
+
mode="bleed"
|
|
109
|
+
text="Delete"
|
|
110
|
+
tone="critical"
|
|
111
|
+
onClick={() => setState('deleting')}
|
|
112
|
+
disabled={isSaving || isResyncing}
|
|
113
|
+
/>
|
|
114
|
+
<Button
|
|
115
|
+
icon={SyncIcon}
|
|
116
|
+
fontSize={2}
|
|
117
|
+
padding={3}
|
|
118
|
+
mode="bleed"
|
|
119
|
+
text="Resync"
|
|
120
|
+
tone="primary"
|
|
121
|
+
onClick={handleResync}
|
|
122
|
+
disabled={isSaving || isResyncing}
|
|
123
|
+
iconRight={isResyncing && Spinner}
|
|
124
|
+
/>
|
|
125
|
+
</Flex>
|
|
110
126
|
{modified && (
|
|
111
127
|
<Button
|
|
112
128
|
icon={CheckmarkIcon}
|
|
@@ -117,7 +133,7 @@ const VideoDetails: React.FC<VideoDetailsProps> = (props) => {
|
|
|
117
133
|
tone="positive"
|
|
118
134
|
onClick={saveChanges}
|
|
119
135
|
iconRight={isSaving && Spinner}
|
|
120
|
-
disabled={isSaving}
|
|
136
|
+
disabled={isSaving || isResyncing}
|
|
121
137
|
/>
|
|
122
138
|
)}
|
|
123
139
|
</Flex>
|
|
@@ -295,13 +311,7 @@ const VideoDetails: React.FC<VideoDetailsProps> = (props) => {
|
|
|
295
311
|
size={2}
|
|
296
312
|
/>
|
|
297
313
|
<IconInfo text={`Mux ID: \n${displayInfo.id}`} icon={TagIcon} size={2} />
|
|
298
|
-
{displayInfo
|
|
299
|
-
<IconInfo
|
|
300
|
-
text={`Playback ID: ${displayInfo.playbackId}`}
|
|
301
|
-
icon={TagIcon}
|
|
302
|
-
size={2}
|
|
303
|
-
/>
|
|
304
|
-
)}
|
|
314
|
+
<PlaybackIds playback_ids={displayInfo.playback_ids} />
|
|
305
315
|
</Stack>
|
|
306
316
|
</Stack>
|
|
307
317
|
</TabPanel>
|
|
@@ -319,4 +329,30 @@ const VideoDetails: React.FC<VideoDetailsProps> = (props) => {
|
|
|
319
329
|
)
|
|
320
330
|
}
|
|
321
331
|
|
|
332
|
+
const PlaybackIds = ({playback_ids}: {playback_ids?: MuxPlaybackId[]}) => {
|
|
333
|
+
if (playback_ids) {
|
|
334
|
+
return playback_ids.map((entry) => (
|
|
335
|
+
<IconInfo
|
|
336
|
+
key={entry.id}
|
|
337
|
+
text={`Playback ID [${policyToText(entry.policy)}]: ${entry.id}`}
|
|
338
|
+
icon={TagIcon}
|
|
339
|
+
size={2}
|
|
340
|
+
/>
|
|
341
|
+
))
|
|
342
|
+
}
|
|
343
|
+
return <IconInfo text={'No Playback ID'} icon={TagIcon} size={2} />
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const policyToText = (policy: PlaybackPolicy) => {
|
|
347
|
+
switch (policy) {
|
|
348
|
+
case 'drm':
|
|
349
|
+
return 'DRM'
|
|
350
|
+
case 'signed':
|
|
351
|
+
return 'Signed'
|
|
352
|
+
case 'public':
|
|
353
|
+
return 'Public'
|
|
354
|
+
default:
|
|
355
|
+
return policy
|
|
356
|
+
}
|
|
357
|
+
}
|
|
322
358
|
export default VideoDetails
|
|
@@ -4,9 +4,12 @@ import {useDocumentStore} from 'sanity'
|
|
|
4
4
|
|
|
5
5
|
import {useClient} from '../../hooks/useClient'
|
|
6
6
|
import useDocReferences from '../../hooks/useDocReferences'
|
|
7
|
+
import {useResyncAsset} from '../../hooks/useResyncAsset'
|
|
7
8
|
import getVideoMetadata from '../../util/getVideoMetadata'
|
|
8
9
|
import {VideoAssetDocument} from '../../util/types'
|
|
9
10
|
|
|
11
|
+
type VideoDetailsState = 'idle' | 'saving' | 'deleting' | 'closing' | 'resyncing'
|
|
12
|
+
|
|
10
13
|
export interface VideoDetailsProps {
|
|
11
14
|
closeDialog: () => void
|
|
12
15
|
asset: VideoAssetDocument & {autoPlay?: boolean}
|
|
@@ -27,7 +30,16 @@ export default function useVideoDetails(props: VideoDetailsProps) {
|
|
|
27
30
|
|
|
28
31
|
const displayInfo = getVideoMetadata({...props.asset, filename})
|
|
29
32
|
|
|
30
|
-
const [state, setState] = useState<
|
|
33
|
+
const [state, setState] = useState<VideoDetailsState>('idle')
|
|
34
|
+
|
|
35
|
+
const {resyncAsset, isResyncing} = useResyncAsset({showToast: true})
|
|
36
|
+
|
|
37
|
+
async function handleResync() {
|
|
38
|
+
if (state !== 'idle') return
|
|
39
|
+
setState('resyncing')
|
|
40
|
+
await resyncAsset(props.asset)
|
|
41
|
+
setState('idle')
|
|
42
|
+
}
|
|
31
43
|
|
|
32
44
|
function handleClose() {
|
|
33
45
|
if (state !== 'idle') return
|
|
@@ -85,5 +97,7 @@ export default function useVideoDetails(props: VideoDetailsProps) {
|
|
|
85
97
|
handleClose,
|
|
86
98
|
confirmClose,
|
|
87
99
|
saveChanges,
|
|
100
|
+
handleResync,
|
|
101
|
+
isResyncing,
|
|
88
102
|
}
|
|
89
103
|
}
|
|
@@ -3,6 +3,7 @@ import {Button, Card, Stack, Text, Tooltip} from '@sanity/ui'
|
|
|
3
3
|
import React, {useState} from 'react'
|
|
4
4
|
import {styled} from 'styled-components'
|
|
5
5
|
|
|
6
|
+
import {DRMWarningDialog, useDrmPlaybackWarningContext} from '../context/DrmPlaybackWarningContext'
|
|
6
7
|
import {THUMBNAIL_ASPECT_RATIO} from '../util/constants'
|
|
7
8
|
import {getPlaybackPolicy} from '../util/getPlaybackPolicy'
|
|
8
9
|
import {VideoAssetDocument} from '../util/types'
|
|
@@ -71,6 +72,8 @@ const PlayButton = styled.button`
|
|
|
71
72
|
}
|
|
72
73
|
`
|
|
73
74
|
|
|
75
|
+
type RenderState = 'render-video' | 'pre-render-warn' | false
|
|
76
|
+
|
|
74
77
|
export default function VideoInBrowser({
|
|
75
78
|
onSelect,
|
|
76
79
|
onEdit,
|
|
@@ -80,16 +83,23 @@ export default function VideoInBrowser({
|
|
|
80
83
|
onEdit?: (asset: VideoAssetDocument) => void
|
|
81
84
|
asset: VideoAssetDocument
|
|
82
85
|
}) {
|
|
83
|
-
const [renderVideo, setRenderVideo] = useState(false)
|
|
86
|
+
const [renderVideo, setRenderVideo] = useState<RenderState>(false)
|
|
84
87
|
const select = React.useCallback(() => onSelect?.(asset), [onSelect, asset])
|
|
85
88
|
const edit = React.useCallback(() => onEdit?.(asset), [onEdit, asset])
|
|
89
|
+
const {hasShownWarning} = useDrmPlaybackWarningContext()
|
|
86
90
|
|
|
87
91
|
if (!asset) {
|
|
88
92
|
return null
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
const playbackPolicy = getPlaybackPolicy(asset)
|
|
92
|
-
|
|
96
|
+
const onClickPlay = () => {
|
|
97
|
+
if (playbackPolicy?.policy === 'drm' && !hasShownWarning) {
|
|
98
|
+
setRenderVideo('pre-render-warn')
|
|
99
|
+
} else {
|
|
100
|
+
setRenderVideo('render-video')
|
|
101
|
+
}
|
|
102
|
+
}
|
|
93
103
|
return (
|
|
94
104
|
<Card
|
|
95
105
|
border
|
|
@@ -100,7 +110,7 @@ export default function VideoInBrowser({
|
|
|
100
110
|
position: 'relative',
|
|
101
111
|
}}
|
|
102
112
|
>
|
|
103
|
-
{playbackPolicy === 'signed' && (
|
|
113
|
+
{playbackPolicy?.policy === 'signed' && (
|
|
104
114
|
<Tooltip
|
|
105
115
|
animate
|
|
106
116
|
content={
|
|
@@ -119,7 +129,7 @@ export default function VideoInBrowser({
|
|
|
119
129
|
position: 'absolute',
|
|
120
130
|
left: '1em',
|
|
121
131
|
top: '1em',
|
|
122
|
-
zIndex:
|
|
132
|
+
zIndex: 11,
|
|
123
133
|
}}
|
|
124
134
|
padding={2}
|
|
125
135
|
border
|
|
@@ -130,6 +140,36 @@ export default function VideoInBrowser({
|
|
|
130
140
|
</Card>
|
|
131
141
|
</Tooltip>
|
|
132
142
|
)}
|
|
143
|
+
{playbackPolicy?.policy === 'drm' && (
|
|
144
|
+
<Tooltip
|
|
145
|
+
animate
|
|
146
|
+
content={
|
|
147
|
+
<Card padding={2} radius={2}>
|
|
148
|
+
<IconInfo icon={LockIcon} text="DRM playback policy" size={2} />
|
|
149
|
+
</Card>
|
|
150
|
+
}
|
|
151
|
+
placement="right"
|
|
152
|
+
fallbackPlacements={['top', 'bottom']}
|
|
153
|
+
portal
|
|
154
|
+
>
|
|
155
|
+
<Card
|
|
156
|
+
tone="caution"
|
|
157
|
+
style={{
|
|
158
|
+
borderRadius: '0.25rem',
|
|
159
|
+
position: 'absolute',
|
|
160
|
+
left: '1em',
|
|
161
|
+
top: '1em',
|
|
162
|
+
zIndex: 11,
|
|
163
|
+
}}
|
|
164
|
+
padding={2}
|
|
165
|
+
border
|
|
166
|
+
>
|
|
167
|
+
<Text muted size={1} weight="semibold" style={{color: 'var(--card-icon-color)'}}>
|
|
168
|
+
DRM
|
|
169
|
+
</Text>
|
|
170
|
+
</Card>
|
|
171
|
+
</Tooltip>
|
|
172
|
+
)}
|
|
133
173
|
<Stack
|
|
134
174
|
space={3}
|
|
135
175
|
height="fill"
|
|
@@ -137,10 +177,17 @@ export default function VideoInBrowser({
|
|
|
137
177
|
gridTemplateRows: 'min-content min-content 1fr',
|
|
138
178
|
}}
|
|
139
179
|
>
|
|
140
|
-
{renderVideo
|
|
180
|
+
{renderVideo === 'pre-render-warn' && (
|
|
181
|
+
<DRMWarningDialog
|
|
182
|
+
onClose={() => {
|
|
183
|
+
setRenderVideo('render-video')
|
|
184
|
+
}}
|
|
185
|
+
/>
|
|
186
|
+
)}
|
|
187
|
+
{renderVideo === 'render-video' ? (
|
|
141
188
|
<VideoPlayer asset={asset} autoPlay forceAspectRatio={THUMBNAIL_ASPECT_RATIO} />
|
|
142
189
|
) : (
|
|
143
|
-
<PlayButton onClick={
|
|
190
|
+
<PlayButton onClick={onClickPlay}>
|
|
144
191
|
<div data-play>
|
|
145
192
|
<PlayIcon />
|
|
146
193
|
</div>
|
|
@@ -2,13 +2,17 @@ import {type MuxPlayerProps, type MuxPlayerRefAttributes} from '@mux/mux-player-
|
|
|
2
2
|
import MuxPlayer from '@mux/mux-player-react/lazy'
|
|
3
3
|
import {ErrorOutlineIcon} from '@sanity/icons'
|
|
4
4
|
import {Card, Text} from '@sanity/ui'
|
|
5
|
-
import {type PropsWithChildren, useMemo, useRef} from 'react'
|
|
5
|
+
import {type PropsWithChildren, Suspense, useMemo, useRef, useState} from 'react'
|
|
6
6
|
|
|
7
7
|
import {useDialogStateContext} from '../context/DialogStateContext'
|
|
8
8
|
import {useClient} from '../hooks/useClient'
|
|
9
9
|
import {AUDIO_ASPECT_RATIO, MIN_ASPECT_RATIO} from '../util/constants'
|
|
10
|
+
import {generateJwt} from '../util/generateJwt'
|
|
11
|
+
import {getPlaybackId} from '../util/getPlaybackPolicy'
|
|
12
|
+
import {getPlaybackPolicyById} from '../util/getPlaybackPolicy'
|
|
10
13
|
import {getPosterSrc} from '../util/getPosterSrc'
|
|
11
14
|
import {getVideoSrc} from '../util/getVideoSrc'
|
|
15
|
+
import {tryWithSuspend} from '../util/tryWithSuspend'
|
|
12
16
|
import type {VideoAssetDocument} from '../util/types'
|
|
13
17
|
import CaptionsDialog from './CaptionsDialog'
|
|
14
18
|
import EditThumbnailDialog from './EditThumbnailDialog'
|
|
@@ -33,32 +37,101 @@ export default function VideoPlayer({
|
|
|
33
37
|
|
|
34
38
|
const isAudio = assetIsAudio(asset)
|
|
35
39
|
const muxPlayer = useRef<MuxPlayerRefAttributes>(null)
|
|
40
|
+
const [error, setError] = useState<Error>()
|
|
36
41
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
thumbnail: thumbnailSrc,
|
|
40
|
-
error,
|
|
41
|
-
} = useMemo(() => {
|
|
42
|
+
/* Playback ID that will be used to play the video */
|
|
43
|
+
const playbackId = useMemo(() => {
|
|
42
44
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return {error: new TypeError('Asset has no playback ID')}
|
|
48
|
-
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
49
|
-
} catch (error) {
|
|
50
|
-
return {error}
|
|
45
|
+
return getPlaybackId(asset, ['public', 'signed', 'drm'])
|
|
46
|
+
} catch (e) {
|
|
47
|
+
setError(new TypeError('Asset has no playback ID'))
|
|
48
|
+
return undefined
|
|
51
49
|
}
|
|
50
|
+
}, [asset])
|
|
51
|
+
|
|
52
|
+
const muxPlaybackId = useMemo(() => {
|
|
53
|
+
if (!playbackId) return undefined
|
|
54
|
+
return getPlaybackPolicyById(asset, playbackId)
|
|
55
|
+
}, [asset, playbackId])
|
|
56
|
+
|
|
57
|
+
const src = useMemo(() => {
|
|
58
|
+
if (!playbackId) return undefined
|
|
59
|
+
if (!muxPlaybackId) return undefined
|
|
60
|
+
return tryWithSuspend(
|
|
61
|
+
() => getVideoSrc({muxPlaybackId, client}),
|
|
62
|
+
(e: Error) => {
|
|
63
|
+
setError(e)
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
}, [muxPlaybackId, playbackId, client])
|
|
68
|
+
|
|
69
|
+
const poster = useMemo(() => {
|
|
70
|
+
return tryWithSuspend(
|
|
71
|
+
() => getPosterSrc({asset, client, width: thumbnailWidth}),
|
|
72
|
+
(e: Error) => {
|
|
73
|
+
setError(e)
|
|
74
|
+
return undefined
|
|
75
|
+
}
|
|
76
|
+
)
|
|
52
77
|
}, [asset, client, thumbnailWidth])
|
|
53
78
|
|
|
54
79
|
const signedToken = useMemo(() => {
|
|
55
80
|
try {
|
|
56
|
-
const url = new URL(
|
|
81
|
+
const url = new URL(src!)
|
|
57
82
|
return url.searchParams.get('token')
|
|
58
83
|
} catch {
|
|
59
|
-
return
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
}, [src])
|
|
87
|
+
const drmToken = useMemo(() => {
|
|
88
|
+
if (!playbackId) return undefined
|
|
89
|
+
if (muxPlaybackId?.policy !== 'drm') return undefined
|
|
90
|
+
|
|
91
|
+
return tryWithSuspend(
|
|
92
|
+
() => generateJwt(client, playbackId, 'd'),
|
|
93
|
+
(e: Error) => {
|
|
94
|
+
setError(e)
|
|
95
|
+
return undefined
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
}, [client, muxPlaybackId?.policy, playbackId])
|
|
99
|
+
const tokens:
|
|
100
|
+
| Partial<{
|
|
101
|
+
playback?: string
|
|
102
|
+
thumbnail?: string
|
|
103
|
+
storyboard?: string
|
|
104
|
+
drm?: string
|
|
105
|
+
}>
|
|
106
|
+
| undefined = useMemo(() => {
|
|
107
|
+
try {
|
|
108
|
+
const partialTokens: {
|
|
109
|
+
playback?: string
|
|
110
|
+
thumbnail?: string
|
|
111
|
+
storyboard?: string
|
|
112
|
+
drm?: string
|
|
113
|
+
} = {
|
|
114
|
+
playback: undefined,
|
|
115
|
+
thumbnail: undefined,
|
|
116
|
+
storyboard: undefined,
|
|
117
|
+
drm: undefined,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (signedToken) {
|
|
121
|
+
partialTokens.playback = signedToken
|
|
122
|
+
partialTokens.thumbnail = signedToken
|
|
123
|
+
partialTokens.storyboard = signedToken
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (drmToken) {
|
|
127
|
+
partialTokens.drm = drmToken
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {...partialTokens}
|
|
131
|
+
} catch {
|
|
132
|
+
return undefined
|
|
60
133
|
}
|
|
61
|
-
}, [
|
|
134
|
+
}, [signedToken, drmToken])
|
|
62
135
|
|
|
63
136
|
const [width, height] = (asset?.data?.aspect_ratio ?? '16:9').split(':').map(Number)
|
|
64
137
|
const targetAspectRatio =
|
|
@@ -71,6 +144,8 @@ export default function VideoPlayer({
|
|
|
71
144
|
: AUDIO_ASPECT_RATIO
|
|
72
145
|
}
|
|
73
146
|
|
|
147
|
+
/* We use Suspense here because `generateJwt` and related functions use suspend()
|
|
148
|
+
under the hood */
|
|
74
149
|
return (
|
|
75
150
|
<>
|
|
76
151
|
<Card
|
|
@@ -81,7 +156,7 @@ export default function VideoPlayer({
|
|
|
81
156
|
...(isAudio && {display: 'flex', alignItems: 'flex-end'}),
|
|
82
157
|
}}
|
|
83
158
|
>
|
|
84
|
-
{
|
|
159
|
+
{src && poster && (
|
|
85
160
|
<>
|
|
86
161
|
{isAudio && (
|
|
87
162
|
<AudioIcon
|
|
@@ -96,35 +171,33 @@ export default function VideoPlayer({
|
|
|
96
171
|
}}
|
|
97
172
|
/>
|
|
98
173
|
)}
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
/>
|
|
127
|
-
{children}
|
|
174
|
+
<Suspense fallback={null}>
|
|
175
|
+
<MuxPlayer
|
|
176
|
+
poster={isAudio ? undefined : poster}
|
|
177
|
+
ref={muxPlayer}
|
|
178
|
+
{...props}
|
|
179
|
+
playsInline
|
|
180
|
+
playbackId={playbackId}
|
|
181
|
+
tokens={tokens}
|
|
182
|
+
preload="metadata"
|
|
183
|
+
crossOrigin="anonymous"
|
|
184
|
+
metadata={{
|
|
185
|
+
player_name: 'Sanity Admin Dashboard',
|
|
186
|
+
player_version: process.env.PKG_VERSION,
|
|
187
|
+
page_type: 'Preview Player',
|
|
188
|
+
}}
|
|
189
|
+
audio={isAudio}
|
|
190
|
+
_hlsConfig={hlsConfig}
|
|
191
|
+
style={{
|
|
192
|
+
...(!isAudio && {height: '100%'}),
|
|
193
|
+
width: '100%',
|
|
194
|
+
display: 'block',
|
|
195
|
+
objectFit: 'contain',
|
|
196
|
+
...(isAudio && {alignSelf: 'end'}),
|
|
197
|
+
}}
|
|
198
|
+
/>
|
|
199
|
+
{children}
|
|
200
|
+
</Suspense>
|
|
128
201
|
</>
|
|
129
202
|
)}
|
|
130
203
|
{error ? (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {ErrorOutlineIcon} from '@sanity/icons'
|
|
2
2
|
import {Box, Card, CardTone, Spinner, Stack, Text} from '@sanity/ui'
|
|
3
|
-
import {useMemo, useRef, useState} from 'react'
|
|
3
|
+
import {Suspense, useMemo, useRef, useState} from 'react'
|
|
4
4
|
import {styled} from 'styled-components'
|
|
5
5
|
|
|
6
6
|
import {useClient} from '../hooks/useClient'
|
|
@@ -8,6 +8,7 @@ import {useInView} from '../hooks/useInView'
|
|
|
8
8
|
import {THUMBNAIL_ASPECT_RATIO} from '../util/constants'
|
|
9
9
|
import {getAnimatedPosterSrc} from '../util/getAnimatedPosterSrc'
|
|
10
10
|
import {getPosterSrc} from '../util/getPosterSrc'
|
|
11
|
+
import {tryWithSuspend} from '../util/tryWithSuspend'
|
|
11
12
|
import {AssetThumbnailOptions, MuxAnimatedThumbnailUrl, MuxThumbnailUrl} from '../util/types'
|
|
12
13
|
|
|
13
14
|
const Image = styled.img`
|
|
@@ -36,91 +37,102 @@ export default function VideoThumbnail({
|
|
|
36
37
|
width?: number
|
|
37
38
|
staticImage?: boolean
|
|
38
39
|
}) {
|
|
40
|
+
const posterWidth = width || 250
|
|
41
|
+
const client = useClient()
|
|
39
42
|
const ref = useRef<HTMLDivElement | null>(null)
|
|
40
43
|
const inView = useInView(ref)
|
|
41
|
-
const posterWidth = width || 250
|
|
42
44
|
|
|
43
45
|
const [status, setStatus] = useState<ImageStatus>('loading')
|
|
44
|
-
const
|
|
46
|
+
const [error, setError] = useState<string | null>(null)
|
|
45
47
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
else thumbnail = getAnimatedPosterSrc({asset, client, width: posterWidth})
|
|
48
|
+
const thumbnailSrc = useMemo(() => {
|
|
49
|
+
return tryWithSuspend(
|
|
50
|
+
() => {
|
|
51
|
+
let thumbnail: MuxAnimatedThumbnailUrl | MuxThumbnailUrl | undefined
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
if (staticImage) thumbnail = getPosterSrc({asset, client, width: posterWidth})
|
|
54
|
+
else thumbnail = getAnimatedPosterSrc({asset, client, width: posterWidth})
|
|
55
|
+
return thumbnail
|
|
56
|
+
},
|
|
57
|
+
(err: Error) => {
|
|
58
|
+
handleError(err.message)
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
}, [asset, client, posterWidth, staticImage])
|
|
58
63
|
|
|
59
64
|
function handleLoad() {
|
|
60
65
|
setStatus('loaded')
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
function handleError() {
|
|
68
|
+
function handleError(err?: string) {
|
|
64
69
|
setStatus('error')
|
|
70
|
+
if (err) {
|
|
71
|
+
setError(err)
|
|
72
|
+
} else {
|
|
73
|
+
setError('Failed loading thumbnail')
|
|
74
|
+
}
|
|
65
75
|
}
|
|
66
76
|
|
|
67
77
|
return (
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
78
|
+
<Suspense fallback={<span>Preparing thumbnail</span>}>
|
|
79
|
+
<Card
|
|
80
|
+
style={{
|
|
81
|
+
aspectRatio: THUMBNAIL_ASPECT_RATIO,
|
|
82
|
+
position: 'relative',
|
|
83
|
+
maxWidth: width ? `${width}px` : undefined,
|
|
84
|
+
width: '100%',
|
|
85
|
+
flex: 1,
|
|
86
|
+
}}
|
|
87
|
+
border
|
|
88
|
+
radius={2}
|
|
89
|
+
ref={ref}
|
|
90
|
+
tone={STATUS_TO_TONE[status]}
|
|
91
|
+
>
|
|
92
|
+
{inView ? (
|
|
93
|
+
<>
|
|
94
|
+
{status === 'loading' && (
|
|
95
|
+
<Box
|
|
96
|
+
style={{
|
|
97
|
+
position: 'absolute',
|
|
98
|
+
left: '50%',
|
|
99
|
+
top: '50%',
|
|
100
|
+
transform: 'translate(-50%, -50%)',
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<Spinner />
|
|
104
|
+
</Box>
|
|
105
|
+
)}
|
|
106
|
+
{status === 'error' && (
|
|
107
|
+
<Stack
|
|
108
|
+
space={4}
|
|
109
|
+
style={{
|
|
110
|
+
position: 'absolute',
|
|
111
|
+
width: '100%',
|
|
112
|
+
left: 0,
|
|
113
|
+
top: '50%',
|
|
114
|
+
transform: 'translateY(-50%)',
|
|
115
|
+
justifyItems: 'center',
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
<Text size={4} muted>
|
|
119
|
+
<ErrorOutlineIcon style={{fontSize: '1.75em'}} />
|
|
120
|
+
</Text>
|
|
121
|
+
<Text muted align="center">
|
|
122
|
+
{error}
|
|
123
|
+
</Text>
|
|
124
|
+
</Stack>
|
|
125
|
+
)}
|
|
126
|
+
<Image
|
|
127
|
+
src={thumbnailSrc ?? undefined}
|
|
128
|
+
alt={`Preview for ${staticImage ? 'image' : 'video'} ${asset.filename || asset.assetId}`}
|
|
129
|
+
onLoad={handleLoad}
|
|
130
|
+
onError={() => handleError()}
|
|
131
|
+
style={{opacity: status === 'loaded' ? 1 : 0}}
|
|
132
|
+
/>
|
|
133
|
+
</>
|
|
134
|
+
) : null}
|
|
135
|
+
</Card>
|
|
136
|
+
</Suspense>
|
|
125
137
|
)
|
|
126
138
|
}
|