sanity-plugin-mux-input 2.4.1 → 2.6.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 +59 -55
- package/dist/index.js +511 -1079
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +515 -1083
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -19
- package/src/actions/secrets.ts +19 -9
- package/src/components/ConfigureApi.tsx +2 -0
- package/src/components/EditThumbnailDialog.tsx +122 -0
- package/src/components/MuxLogo.tsx +26 -447
- package/src/components/PlayerActionsMenu.tsx +13 -0
- package/src/components/UploadConfiguration.tsx +29 -26
- package/src/components/Uploader.tsx +21 -15
- package/src/components/VideoDetails/useVideoDetails.ts +5 -5
- package/src/components/VideoPlayer.tsx +66 -49
- package/src/components/VideoThumbnail.tsx +15 -8
- package/src/components/uploadConfiguration/PlaybackPolicy.tsx +44 -0
- package/src/components/uploadConfiguration/PlaybackPolicyOption.tsx +60 -0
- package/src/components/uploadConfiguration/PlaybackPolicyWarning.tsx +29 -0
- package/src/context/DialogStateContext.tsx +36 -0
- package/src/hooks/useAssets.ts +26 -29
- package/src/util/createUrlParamsObject.ts +25 -0
- package/src/util/formatSeconds.ts +28 -1
- package/src/util/getAnimatedPosterSrc.ts +5 -13
- package/src/util/getPosterSrc.ts +10 -15
- package/src/util/getVideoMetadata.ts +1 -1
- package/src/util/types.ts +7 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {useToast} from '@sanity/ui'
|
|
2
|
-
import {useState} from 'react'
|
|
2
|
+
import {useMemo, useState} from 'react'
|
|
3
3
|
import {useDocumentStore} from 'sanity'
|
|
4
4
|
|
|
5
5
|
import {useClient} from '../../hooks/useClient'
|
|
@@ -17,10 +17,10 @@ export default function useVideoDetails(props: VideoDetailsProps) {
|
|
|
17
17
|
const documentStore = useDocumentStore()
|
|
18
18
|
const toast = useToast()
|
|
19
19
|
const client = useClient()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
id: props.asset._id
|
|
23
|
-
|
|
20
|
+
|
|
21
|
+
const [references, referencesLoading] = useDocReferences(
|
|
22
|
+
useMemo(() => ({documentStore, id: props.asset._id}), [documentStore, props.asset._id])
|
|
23
|
+
)
|
|
24
24
|
|
|
25
25
|
const [originalAsset, setOriginalAsset] = useState(() => props.asset)
|
|
26
26
|
const [filename, setFilename] = useState(props.asset.filename)
|
|
@@ -1,23 +1,32 @@
|
|
|
1
|
-
import MuxPlayer, {type MuxPlayerProps} from '@mux/mux-player-react'
|
|
1
|
+
import MuxPlayer, {type MuxPlayerProps, type MuxPlayerRefAttributes} from '@mux/mux-player-react'
|
|
2
2
|
import {ErrorOutlineIcon} from '@sanity/icons'
|
|
3
3
|
import {Card, Text} from '@sanity/ui'
|
|
4
|
-
import {type PropsWithChildren, useMemo} from 'react'
|
|
4
|
+
import {type PropsWithChildren, useMemo, useRef} from 'react'
|
|
5
5
|
|
|
6
|
+
import {useDialogStateContext} from '../context/DialogStateContext'
|
|
6
7
|
import {useClient} from '../hooks/useClient'
|
|
7
8
|
import {AUDIO_ASPECT_RATIO, MIN_ASPECT_RATIO} from '../util/constants'
|
|
9
|
+
import {getPosterSrc} from '../util/getPosterSrc'
|
|
8
10
|
import {getVideoSrc} from '../util/getVideoSrc'
|
|
9
11
|
import type {VideoAssetDocument} from '../util/types'
|
|
12
|
+
import EditThumbnailDialog from './EditThumbnailDialog'
|
|
10
13
|
|
|
11
14
|
export default function VideoPlayer({
|
|
12
15
|
asset,
|
|
16
|
+
thumbnailWidth = 250,
|
|
13
17
|
children,
|
|
14
18
|
...props
|
|
15
19
|
}: PropsWithChildren<
|
|
16
|
-
{asset: VideoAssetDocument; forceAspectRatio?: number} & Partial<
|
|
20
|
+
{asset: VideoAssetDocument; thumbnailWidth?: number; forceAspectRatio?: number} & Partial<
|
|
21
|
+
Pick<MuxPlayerProps, 'autoPlay'>
|
|
22
|
+
>
|
|
17
23
|
>) {
|
|
18
24
|
const client = useClient()
|
|
25
|
+
const {dialogState} = useDialogStateContext()
|
|
19
26
|
|
|
20
27
|
const isAudio = assetIsAudio(asset)
|
|
28
|
+
const muxPlayer = useRef<MuxPlayerRefAttributes>(null)
|
|
29
|
+
const thumbnail = getPosterSrc({asset, client, width: thumbnailWidth})
|
|
21
30
|
|
|
22
31
|
const {src: videoSrc, error} = useMemo(() => {
|
|
23
32
|
try {
|
|
@@ -52,55 +61,63 @@ export default function VideoPlayer({
|
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
return (
|
|
55
|
-
|
|
56
|
-
{
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
64
|
+
<>
|
|
65
|
+
<Card tone="transparent" style={{aspectRatio: aspectRatio, position: 'relative'}}>
|
|
66
|
+
{videoSrc && (
|
|
67
|
+
<>
|
|
68
|
+
<MuxPlayer
|
|
69
|
+
poster={thumbnail}
|
|
70
|
+
ref={muxPlayer}
|
|
71
|
+
{...props}
|
|
72
|
+
playsInline
|
|
73
|
+
playbackId={asset.playbackId}
|
|
74
|
+
tokens={
|
|
75
|
+
signedToken
|
|
76
|
+
? {playback: signedToken, thumbnail: signedToken, storyboard: signedToken}
|
|
77
|
+
: undefined
|
|
78
|
+
}
|
|
79
|
+
preload="metadata"
|
|
80
|
+
crossOrigin="anonymous"
|
|
81
|
+
metadata={{
|
|
82
|
+
player_name: 'Sanity Admin Dashboard',
|
|
83
|
+
player_version: process.env.PKG_VERSION,
|
|
84
|
+
page_type: 'Preview Player',
|
|
85
|
+
}}
|
|
86
|
+
audio={isAudio}
|
|
87
|
+
style={{
|
|
88
|
+
height: '100%',
|
|
89
|
+
width: '100%',
|
|
90
|
+
display: 'block',
|
|
91
|
+
objectFit: 'contain',
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
{children}
|
|
95
|
+
</>
|
|
96
|
+
)}
|
|
97
|
+
{error ? (
|
|
98
|
+
<div
|
|
75
99
|
style={{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
100
|
+
position: 'absolute',
|
|
101
|
+
top: '50%',
|
|
102
|
+
left: '50%',
|
|
103
|
+
transform: 'translate(-50%, -50%)',
|
|
80
104
|
}}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
105
|
+
>
|
|
106
|
+
<Text muted>
|
|
107
|
+
<ErrorOutlineIcon style={{marginRight: '0.15em'}} />
|
|
108
|
+
{typeof error === 'object' && 'message' in error && typeof error.message === 'string'
|
|
109
|
+
? error.message
|
|
110
|
+
: 'Error loading video'}
|
|
111
|
+
</Text>
|
|
112
|
+
</div>
|
|
113
|
+
) : null}
|
|
114
|
+
{children}
|
|
115
|
+
</Card>
|
|
116
|
+
|
|
117
|
+
{dialogState === 'edit-thumbnail' && (
|
|
118
|
+
<EditThumbnailDialog asset={asset} currentTime={muxPlayer?.current?.currentTime} />
|
|
84
119
|
)}
|
|
85
|
-
|
|
86
|
-
<div
|
|
87
|
-
style={{
|
|
88
|
-
position: 'absolute',
|
|
89
|
-
top: '50%',
|
|
90
|
-
left: '50%',
|
|
91
|
-
transform: 'translate(-50%, -50%)',
|
|
92
|
-
}}
|
|
93
|
-
>
|
|
94
|
-
<Text muted>
|
|
95
|
-
<ErrorOutlineIcon style={{marginRight: '0.15em'}} />
|
|
96
|
-
{typeof error === 'object' && 'message' in error && typeof error.message === 'string'
|
|
97
|
-
? error.message
|
|
98
|
-
: 'Error loading video'}
|
|
99
|
-
</Text>
|
|
100
|
-
</div>
|
|
101
|
-
) : null}
|
|
102
|
-
{children}
|
|
103
|
-
</Card>
|
|
120
|
+
</>
|
|
104
121
|
)
|
|
105
122
|
}
|
|
106
123
|
|
|
@@ -6,8 +6,9 @@ import {styled} from 'styled-components'
|
|
|
6
6
|
import {useClient} from '../hooks/useClient'
|
|
7
7
|
import useInView from '../hooks/useInView'
|
|
8
8
|
import {THUMBNAIL_ASPECT_RATIO} from '../util/constants'
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import {getAnimatedPosterSrc} from '../util/getAnimatedPosterSrc'
|
|
10
|
+
import {getPosterSrc} from '../util/getPosterSrc'
|
|
11
|
+
import {AssetThumbnailOptions, MuxAnimatedThumbnailUrl, MuxThumbnailUrl} from '../util/types'
|
|
11
12
|
|
|
12
13
|
const Image = styled.img`
|
|
13
14
|
transition: opacity 0.175s ease-out 0s;
|
|
@@ -29,9 +30,11 @@ const STATUS_TO_TONE: Record<ImageStatus, CardTone> = {
|
|
|
29
30
|
export default function VideoThumbnail({
|
|
30
31
|
asset,
|
|
31
32
|
width,
|
|
33
|
+
staticImage = false,
|
|
32
34
|
}: {
|
|
33
|
-
asset:
|
|
35
|
+
asset: AssetThumbnailOptions['asset']
|
|
34
36
|
width?: number
|
|
37
|
+
staticImage?: boolean
|
|
35
38
|
}) {
|
|
36
39
|
const {inView, ref} = useInView()
|
|
37
40
|
const posterWidth = width || 250
|
|
@@ -39,14 +42,18 @@ export default function VideoThumbnail({
|
|
|
39
42
|
const [status, setStatus] = useState<ImageStatus>('loading')
|
|
40
43
|
const client = useClient()
|
|
41
44
|
|
|
42
|
-
const
|
|
45
|
+
const src = useMemo(() => {
|
|
43
46
|
try {
|
|
44
|
-
|
|
47
|
+
let thumbnail: MuxAnimatedThumbnailUrl | MuxThumbnailUrl
|
|
48
|
+
if (staticImage) thumbnail = getPosterSrc({asset, client, width: posterWidth})
|
|
49
|
+
else thumbnail = getAnimatedPosterSrc({asset, client, width: posterWidth})
|
|
50
|
+
|
|
51
|
+
return thumbnail
|
|
45
52
|
} catch {
|
|
46
53
|
if (status !== 'error') setStatus('error')
|
|
47
54
|
return undefined
|
|
48
55
|
}
|
|
49
|
-
}, [asset, client, posterWidth, status])
|
|
56
|
+
}, [asset, client, posterWidth, status, staticImage])
|
|
50
57
|
|
|
51
58
|
function handleLoad() {
|
|
52
59
|
setStatus('loaded')
|
|
@@ -105,8 +112,8 @@ export default function VideoThumbnail({
|
|
|
105
112
|
</Stack>
|
|
106
113
|
)}
|
|
107
114
|
<Image
|
|
108
|
-
src={
|
|
109
|
-
alt={`Preview for video ${asset.filename || asset.assetId}`}
|
|
115
|
+
src={src}
|
|
116
|
+
alt={`Preview for ${staticImage ? 'image' : 'video'} ${asset.filename || asset.assetId}`}
|
|
110
117
|
onLoad={handleLoad}
|
|
111
118
|
onError={handleError}
|
|
112
119
|
style={{
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {Grid, Text} from '@sanity/ui'
|
|
2
|
+
|
|
3
|
+
import {Secrets, UploadConfig} from '../../util/types'
|
|
4
|
+
import PlaybackPolicyOption from './PlaybackPolicyOption'
|
|
5
|
+
import PlaybackPolicyWarning from './PlaybackPolicyWarning'
|
|
6
|
+
|
|
7
|
+
export default function PlaybackPolicy({
|
|
8
|
+
id,
|
|
9
|
+
config,
|
|
10
|
+
secrets,
|
|
11
|
+
dispatch,
|
|
12
|
+
}: {
|
|
13
|
+
id: string
|
|
14
|
+
config: UploadConfig
|
|
15
|
+
secrets: Secrets
|
|
16
|
+
dispatch: any
|
|
17
|
+
}) {
|
|
18
|
+
const noPolicySelected = !(config.public_policy || config.signed_policy)
|
|
19
|
+
return (
|
|
20
|
+
<Grid gap={3}>
|
|
21
|
+
<Text weight="bold">Advanced Playback Policies</Text>
|
|
22
|
+
<PlaybackPolicyOption
|
|
23
|
+
id={`${id}--public`}
|
|
24
|
+
checked={config.public_policy}
|
|
25
|
+
optionName="Public"
|
|
26
|
+
description="Playback IDs are accessible by constructing an HLS URL like https://stream.mux.com/{PLAYBACK_ID}"
|
|
27
|
+
dispatch={dispatch}
|
|
28
|
+
action="public_policy"
|
|
29
|
+
/>
|
|
30
|
+
{secrets.enableSignedUrls && (
|
|
31
|
+
<PlaybackPolicyOption
|
|
32
|
+
id={`${id}--signed`}
|
|
33
|
+
checked={config.signed_policy}
|
|
34
|
+
optionName="Signed"
|
|
35
|
+
description="Playback IDs should be used with tokens https://stream.mux.com/{PLAYBACK_ID}?token={TOKEN}.
|
|
36
|
+
// See Secure video playback for details about creating tokens."
|
|
37
|
+
dispatch={dispatch}
|
|
38
|
+
action="signed_policy"
|
|
39
|
+
/>
|
|
40
|
+
)}
|
|
41
|
+
{noPolicySelected && <PlaybackPolicyWarning />}
|
|
42
|
+
</Grid>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {Box, Checkbox, Flex, Grid, Stack, Text} from '@sanity/ui'
|
|
2
|
+
import {CSSProperties, useState} from 'react'
|
|
3
|
+
|
|
4
|
+
import {UploadConfigurationStateAction} from '../UploadConfiguration'
|
|
5
|
+
|
|
6
|
+
export default function PlaybackPolicyOption({
|
|
7
|
+
id,
|
|
8
|
+
checked,
|
|
9
|
+
optionName,
|
|
10
|
+
description,
|
|
11
|
+
dispatch,
|
|
12
|
+
action,
|
|
13
|
+
}: {
|
|
14
|
+
id: string
|
|
15
|
+
checked: boolean
|
|
16
|
+
optionName: string
|
|
17
|
+
description: string
|
|
18
|
+
dispatch: any
|
|
19
|
+
action: UploadConfigurationStateAction['action']
|
|
20
|
+
}) {
|
|
21
|
+
const [scale, setScale] = useState(1)
|
|
22
|
+
|
|
23
|
+
const boxStyle: CSSProperties = {
|
|
24
|
+
outline: '0.01rem solid grey',
|
|
25
|
+
transform: `scale(${scale})`,
|
|
26
|
+
transition: 'transform 0.1s ease-in-out',
|
|
27
|
+
cursor: 'pointer',
|
|
28
|
+
borderRadius: '0.25rem',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const triggerAnimation = () => {
|
|
32
|
+
setScale(0.98)
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
setScale(1)
|
|
35
|
+
}, 100)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const handleBoxClick = () => {
|
|
39
|
+
triggerAnimation()
|
|
40
|
+
dispatch({
|
|
41
|
+
action,
|
|
42
|
+
value: !checked,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
return (
|
|
46
|
+
<label>
|
|
47
|
+
<Flex gap={3} padding={3} style={boxStyle}>
|
|
48
|
+
<Checkbox id={id} required checked={checked} onChange={handleBoxClick} />
|
|
49
|
+
<Grid gap={3}>
|
|
50
|
+
<Text size={3} weight="bold">
|
|
51
|
+
{optionName}
|
|
52
|
+
</Text>
|
|
53
|
+
<Text size={2} muted>
|
|
54
|
+
{description}
|
|
55
|
+
</Text>
|
|
56
|
+
</Grid>
|
|
57
|
+
</Flex>
|
|
58
|
+
</label>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {WarningFilledIcon} from '@sanity/icons'
|
|
2
|
+
import {Box, Flex, Text} from '@sanity/ui'
|
|
3
|
+
import {CSSProperties} from 'react'
|
|
4
|
+
|
|
5
|
+
export default function PlaybackPolicyWarning() {
|
|
6
|
+
const textStyle: CSSProperties = {
|
|
7
|
+
color: '#13141A',
|
|
8
|
+
fontWeight: 500,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const boxStyle: CSSProperties = {
|
|
12
|
+
outline: '0.01rem solid grey',
|
|
13
|
+
backgroundColor: '#979cb0',
|
|
14
|
+
borderRadius: '0.5rem',
|
|
15
|
+
width: 'max-content',
|
|
16
|
+
color: '#13141A',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Box padding={2} style={boxStyle}>
|
|
21
|
+
<Flex align="center" gap={2}>
|
|
22
|
+
<WarningFilledIcon />
|
|
23
|
+
<Text size={1} style={textStyle}>
|
|
24
|
+
Please select at least one Playback Policy
|
|
25
|
+
</Text>
|
|
26
|
+
</Flex>
|
|
27
|
+
</Box>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React, {createContext, useContext} from 'react'
|
|
2
|
+
|
|
3
|
+
import {type DialogState, type SetDialogState} from '../hooks/useDialogState'
|
|
4
|
+
|
|
5
|
+
type DialogStateContextProps = {
|
|
6
|
+
dialogState: DialogState
|
|
7
|
+
setDialogState: SetDialogState
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const DialogStateContext = createContext<DialogStateContextProps>({
|
|
11
|
+
dialogState: false,
|
|
12
|
+
setDialogState: () => {
|
|
13
|
+
return null
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
interface DialogStateProviderProps extends DialogStateContextProps {
|
|
18
|
+
children: React.ReactNode
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const DialogStateProvider = ({
|
|
22
|
+
dialogState,
|
|
23
|
+
setDialogState,
|
|
24
|
+
children,
|
|
25
|
+
}: DialogStateProviderProps) => {
|
|
26
|
+
return (
|
|
27
|
+
<DialogStateContext.Provider value={{dialogState, setDialogState}}>
|
|
28
|
+
{children}
|
|
29
|
+
</DialogStateContext.Provider>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const useDialogStateContext = () => {
|
|
34
|
+
const context = useContext(DialogStateContext)
|
|
35
|
+
return context
|
|
36
|
+
}
|
package/src/hooks/useAssets.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {useMemo, useState} from 'react'
|
|
2
|
-
import {
|
|
3
|
-
import {collate, DocumentStore, useDocumentStore} from 'sanity'
|
|
2
|
+
import {collate, createHookFromObservableFactory, DocumentStore, useDocumentStore} from 'sanity'
|
|
4
3
|
|
|
5
4
|
import {SANITY_API_VERSION} from '../hooks/useClient'
|
|
6
5
|
import {createSearchFilter} from '../util/createSearchFilter'
|
|
@@ -15,49 +14,47 @@ export const ASSET_SORT_OPTIONS = {
|
|
|
15
14
|
|
|
16
15
|
export type SortOption = keyof typeof ASSET_SORT_OPTIONS
|
|
17
16
|
|
|
18
|
-
const useAssetDocuments =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const search = createSearchFilter(searchQuery)
|
|
29
|
-
const filter = [`_type == "mux.videoAsset"`, ...search.filter].filter(Boolean).join(' && ')
|
|
30
|
-
const sortFragment = ASSET_SORT_OPTIONS[sort].groq
|
|
31
|
-
return documentStore.listenQuery(
|
|
32
|
-
/* groq */ `*[${filter}] | order(${sortFragment})`,
|
|
33
|
-
search.params,
|
|
34
|
-
{
|
|
35
|
-
apiVersion: SANITY_API_VERSION,
|
|
36
|
-
}
|
|
37
|
-
)
|
|
38
|
-
}, [documentStore, sort, searchQuery])
|
|
17
|
+
const useAssetDocuments = createHookFromObservableFactory<
|
|
18
|
+
VideoAssetDocument[],
|
|
19
|
+
{
|
|
20
|
+
documentStore: DocumentStore
|
|
21
|
+
sort: SortOption
|
|
22
|
+
searchQuery: string
|
|
23
|
+
}
|
|
24
|
+
>(({documentStore, sort, searchQuery}) => {
|
|
25
|
+
const search = createSearchFilter(searchQuery)
|
|
26
|
+
const filter = [`_type == "mux.videoAsset"`, ...search.filter].filter(Boolean).join(' && ')
|
|
39
27
|
|
|
40
|
-
|
|
41
|
-
|
|
28
|
+
const sortFragment = ASSET_SORT_OPTIONS[sort].groq
|
|
29
|
+
return documentStore.listenQuery(
|
|
30
|
+
/* groq */ `*[${filter}] | order(${sortFragment})`,
|
|
31
|
+
search.params,
|
|
32
|
+
{
|
|
33
|
+
apiVersion: SANITY_API_VERSION,
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
})
|
|
42
37
|
|
|
43
38
|
export default function useAssets() {
|
|
44
39
|
const documentStore = useDocumentStore()
|
|
45
40
|
const [sort, setSort] = useState<SortOption>('createdDesc')
|
|
46
41
|
const [searchQuery, setSearchQuery] = useState('')
|
|
47
42
|
|
|
48
|
-
const
|
|
49
|
-
|
|
43
|
+
const [assetDocuments = [], isLoading] = useAssetDocuments(
|
|
44
|
+
useMemo(() => ({documentStore, sort, searchQuery}), [documentStore, sort, searchQuery])
|
|
45
|
+
)
|
|
46
|
+
|
|
50
47
|
const assets = useMemo(
|
|
51
48
|
() =>
|
|
52
49
|
// Avoid displaying both drafts & published assets by collating them together and giving preference to drafts
|
|
53
|
-
collate<VideoAssetDocument>(
|
|
50
|
+
collate<VideoAssetDocument>(assetDocuments).map(
|
|
54
51
|
(collated) =>
|
|
55
52
|
({
|
|
56
53
|
...(collated.draft || collated.published || {}),
|
|
57
54
|
_id: collated.id,
|
|
58
55
|
}) as VideoAssetDocument
|
|
59
56
|
),
|
|
60
|
-
[
|
|
57
|
+
[assetDocuments]
|
|
61
58
|
)
|
|
62
59
|
|
|
63
60
|
return {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type {SanityClient} from 'sanity'
|
|
2
|
+
|
|
3
|
+
import {Audience, generateJwt} from './generateJwt'
|
|
4
|
+
import {getPlaybackId} from './getPlaybackId'
|
|
5
|
+
import {getPlaybackPolicy} from './getPlaybackPolicy'
|
|
6
|
+
import type {AssetThumbnailOptions} from './types'
|
|
7
|
+
|
|
8
|
+
export function createUrlParamsObject(
|
|
9
|
+
client: SanityClient,
|
|
10
|
+
asset: AssetThumbnailOptions['asset'],
|
|
11
|
+
params: object,
|
|
12
|
+
audience: Audience
|
|
13
|
+
) {
|
|
14
|
+
const playbackId = getPlaybackId(asset)
|
|
15
|
+
|
|
16
|
+
let searchParams = new URLSearchParams(
|
|
17
|
+
JSON.parse(JSON.stringify(params, (_, v) => v ?? undefined))
|
|
18
|
+
)
|
|
19
|
+
if (getPlaybackPolicy(asset) === 'signed') {
|
|
20
|
+
const token = generateJwt(client, playbackId, audience, params)
|
|
21
|
+
searchParams = new URLSearchParams({token})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {playbackId, searchParams}
|
|
25
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
2
|
// From: https://stackoverflow.com/a/11486026/10433647
|
|
3
|
-
export
|
|
3
|
+
export function formatSeconds(seconds: number): string {
|
|
4
4
|
if (typeof seconds !== 'number' || Number.isNaN(seconds)) {
|
|
5
5
|
return ''
|
|
6
6
|
}
|
|
@@ -20,3 +20,30 @@ export default function formatSeconds(seconds: number): string {
|
|
|
20
20
|
ret += '' + secs
|
|
21
21
|
return ret
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
// Output like "05:14:01"
|
|
25
|
+
export function formatSecondsToHHMMSS(seconds: number): string {
|
|
26
|
+
const hrs = Math.floor(seconds / 3600)
|
|
27
|
+
.toString()
|
|
28
|
+
.padStart(2, '0')
|
|
29
|
+
const mins = Math.floor((seconds % 3600) / 60)
|
|
30
|
+
.toString()
|
|
31
|
+
.padStart(2, '0')
|
|
32
|
+
const secs = Math.floor(seconds % 60)
|
|
33
|
+
.toString()
|
|
34
|
+
.padStart(2, '0')
|
|
35
|
+
|
|
36
|
+
return `${hrs}:${mins}:${secs}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Checks if time has a HH:MM:SS format like "05:14:01"
|
|
40
|
+
export function isValidTimeFormat(time: string) {
|
|
41
|
+
const regex = /^([0-1]?[0-9]|2[0-3]):([0-5]?[0-9]):([0-5]?[0-9])$/
|
|
42
|
+
return regex.test(time) || time === ''
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Converts a time like "05:14:01" to seconds
|
|
46
|
+
export function getSecondsFromTimeFormat(time: string): number {
|
|
47
|
+
const [hh = 0, mm = 0, ss = 0] = time.split(':').map(Number)
|
|
48
|
+
return hh * 3600 + mm * 60 + ss
|
|
49
|
+
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import type {SanityClient} from 'sanity'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import type {AnimatedThumbnailOptions, MuxAnimatedThumbnailUrl, VideoAssetDocument} from './types'
|
|
3
|
+
import {createUrlParamsObject} from './createUrlParamsObject'
|
|
4
|
+
import type {AnimatedThumbnailOptions, MuxAnimatedThumbnailUrl} from './types'
|
|
5
|
+
import {AssetThumbnailOptions} from './types'
|
|
7
6
|
|
|
8
7
|
export interface AnimatedPosterSrcOptions extends AnimatedThumbnailOptions {
|
|
9
|
-
asset:
|
|
8
|
+
asset: AssetThumbnailOptions['asset']
|
|
10
9
|
client: SanityClient
|
|
11
10
|
}
|
|
12
11
|
|
|
@@ -20,15 +19,8 @@ export function getAnimatedPosterSrc({
|
|
|
20
19
|
fps = 15,
|
|
21
20
|
}: AnimatedPosterSrcOptions): MuxAnimatedThumbnailUrl {
|
|
22
21
|
const params = {height, width, start, end, fps}
|
|
23
|
-
const playbackId = getPlaybackId(asset)
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
JSON.parse(JSON.stringify(params, (_, v) => v ?? undefined))
|
|
27
|
-
)
|
|
28
|
-
if (getPlaybackPolicy(asset) === 'signed') {
|
|
29
|
-
const token = generateJwt(client, playbackId, 'g', params)
|
|
30
|
-
searchParams = new URLSearchParams({token})
|
|
31
|
-
}
|
|
23
|
+
const {playbackId, searchParams} = createUrlParamsObject(client, asset, params, 'g')
|
|
32
24
|
|
|
33
25
|
return `https://image.mux.com/${playbackId}/animated.gif?${searchParams}`
|
|
34
26
|
}
|
package/src/util/getPosterSrc.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import type {SanityClient} from 'sanity'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import type {MuxThumbnailUrl, ThumbnailOptions, VideoAssetDocument} from './types'
|
|
3
|
+
import {createUrlParamsObject} from './createUrlParamsObject'
|
|
4
|
+
import type {MuxThumbnailUrl, ThumbnailOptions} from './types'
|
|
5
|
+
import {AssetThumbnailOptions} from './types'
|
|
7
6
|
|
|
8
7
|
export interface PosterSrcOptions extends ThumbnailOptions {
|
|
9
|
-
asset:
|
|
8
|
+
asset: AssetThumbnailOptions['asset']
|
|
10
9
|
client: SanityClient
|
|
11
10
|
}
|
|
12
11
|
|
|
@@ -15,19 +14,15 @@ export function getPosterSrc({
|
|
|
15
14
|
client,
|
|
16
15
|
fit_mode,
|
|
17
16
|
height,
|
|
18
|
-
time = asset.thumbTime,
|
|
17
|
+
time = asset.thumbTime ?? undefined,
|
|
19
18
|
width,
|
|
20
19
|
}: PosterSrcOptions): MuxThumbnailUrl {
|
|
21
|
-
const params = {fit_mode, height,
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
let searchParams = new URLSearchParams(
|
|
25
|
-
JSON.parse(JSON.stringify(params, (_, v) => v ?? undefined))
|
|
26
|
-
)
|
|
27
|
-
if (getPlaybackPolicy(asset) === 'signed') {
|
|
28
|
-
const token = generateJwt(client, playbackId, 't', params)
|
|
29
|
-
searchParams = new URLSearchParams({token})
|
|
20
|
+
const params = {fit_mode, height, width}
|
|
21
|
+
if (time) {
|
|
22
|
+
;(params as any).time = time
|
|
30
23
|
}
|
|
31
24
|
|
|
25
|
+
const {playbackId, searchParams} = createUrlParamsObject(client, asset, params, 't')
|
|
26
|
+
|
|
32
27
|
return `https://image.mux.com/${playbackId}/thumbnail.png?${searchParams}`
|
|
33
28
|
}
|
package/src/util/types.ts
CHANGED
|
@@ -163,7 +163,8 @@ export interface UploadConfig
|
|
|
163
163
|
'encoding_tier' | 'max_resolution_tier' | 'mp4_support' | 'normalize_audio'
|
|
164
164
|
> {
|
|
165
165
|
text_tracks: UploadTextTrack[]
|
|
166
|
-
|
|
166
|
+
signed_policy: boolean
|
|
167
|
+
public_policy: boolean
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
/**
|
|
@@ -208,7 +209,7 @@ export interface MuxNewAssetSettings
|
|
|
208
209
|
}[]
|
|
209
210
|
|
|
210
211
|
/** An array of playback policy names that you want applied to this asset and available through playback_ids. */
|
|
211
|
-
playback_policy: ('public' | 'signed')[]
|
|
212
|
+
playback_policy: ('public' | 'signed' | 'drm')[]
|
|
212
213
|
|
|
213
214
|
/** Arbitrary user-supplied metadata that will be included in the asset details and related webhooks. */
|
|
214
215
|
passthrough?: string
|
|
@@ -262,6 +263,10 @@ export interface AnimatedThumbnailOptions {
|
|
|
262
263
|
fps?: number
|
|
263
264
|
}
|
|
264
265
|
|
|
266
|
+
export interface AssetThumbnailOptions {
|
|
267
|
+
asset: Pick<VideoAssetDocument, 'playbackId' | 'data' | 'thumbTime' | 'filename' | 'assetId'>
|
|
268
|
+
}
|
|
269
|
+
|
|
265
270
|
export type PlaybackPolicy = 'signed' | 'public'
|
|
266
271
|
|
|
267
272
|
export interface MuxErrors {
|