sanity-plugin-mux-input 2.4.1 → 2.5.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 +57 -55
- package/dist/index.js +364 -460
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +367 -463
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -9
- package/src/components/EditThumbnailDialog.tsx +122 -0
- package/src/components/PlayerActionsMenu.tsx +13 -0
- package/src/components/Uploader.tsx +21 -15
- package/src/components/VideoPlayer.tsx +66 -49
- package/src/components/VideoThumbnail.tsx +15 -8
- package/src/context/DialogStateContext.tsx +36 -0
- 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 +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sanity-plugin-mux-input",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "An input component that integrates Sanity Studio with Mux video encoding/hosting service.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -41,8 +41,7 @@
|
|
|
41
41
|
],
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "plugin-kit verify-package --silent && pkg-utils build --strict --check --clean",
|
|
44
|
-
"
|
|
45
|
-
"dev": "plugin-kit link-watch --strict",
|
|
44
|
+
"dev": "sanity dev",
|
|
46
45
|
"format": "prettier --write --cache --ignore-unknown .",
|
|
47
46
|
"link-watch": "plugin-kit link-watch",
|
|
48
47
|
"lint": "eslint .",
|
|
@@ -75,11 +74,10 @@
|
|
|
75
74
|
"@sanity/pkg-utils": "^6.12.1",
|
|
76
75
|
"@sanity/plugin-kit": "4.0.19",
|
|
77
76
|
"@sanity/semantic-release-preset": "^5.0.0",
|
|
78
|
-
"@sanity/vision": "^3.
|
|
77
|
+
"@sanity/vision": "^3.74.0",
|
|
79
78
|
"@types/lodash": "^4.17.13",
|
|
80
79
|
"@types/react": "^18.3.17",
|
|
81
80
|
"@types/react-is": "^18.3.1",
|
|
82
|
-
"@types/styled-components": "^5.1.34",
|
|
83
81
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
84
82
|
"@typescript-eslint/parser": "^7.18.0",
|
|
85
83
|
"eslint": "^8.57.1",
|
|
@@ -98,12 +96,10 @@
|
|
|
98
96
|
"react": "^18.3.1",
|
|
99
97
|
"react-dom": "^18.3.1",
|
|
100
98
|
"react-is": "^18.3.1",
|
|
101
|
-
"
|
|
102
|
-
"sanity": "^3.68.1",
|
|
99
|
+
"sanity": "^3.74.0",
|
|
103
100
|
"semantic-release": "^24.2.0",
|
|
104
101
|
"styled-components": "^6.1.13",
|
|
105
|
-
"typescript": "
|
|
106
|
-
"yalc": "1.0.0-pre.53"
|
|
102
|
+
"typescript": "5.7.3"
|
|
107
103
|
},
|
|
108
104
|
"peerDependencies": {
|
|
109
105
|
"react": "^18",
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {Button, Dialog, Flex, Stack, Text, TextInput} from '@sanity/ui'
|
|
2
|
+
import React, {useId, useMemo, useState} from 'react'
|
|
3
|
+
import {getDevicePixelRatio} from 'use-device-pixel-ratio'
|
|
4
|
+
|
|
5
|
+
import {useDialogStateContext} from '../context/DialogStateContext'
|
|
6
|
+
import {useClient} from '../hooks/useClient'
|
|
7
|
+
import {
|
|
8
|
+
formatSecondsToHHMMSS,
|
|
9
|
+
getSecondsFromTimeFormat,
|
|
10
|
+
isValidTimeFormat,
|
|
11
|
+
} from '../util/formatSeconds'
|
|
12
|
+
import type {VideoAssetDocument} from '../util/types'
|
|
13
|
+
import VideoThumbnail from './VideoThumbnail'
|
|
14
|
+
|
|
15
|
+
export interface Props {
|
|
16
|
+
asset: VideoAssetDocument
|
|
17
|
+
currentTime?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function EditThumbnailDialog({asset, currentTime = 0}: Props) {
|
|
21
|
+
const client = useClient()
|
|
22
|
+
|
|
23
|
+
const {setDialogState} = useDialogStateContext()
|
|
24
|
+
const dialogId = `EditThumbnailDialog${useId()}`
|
|
25
|
+
|
|
26
|
+
const [timeFormatted, setTimeFormatted] = useState<string>(() =>
|
|
27
|
+
formatSecondsToHHMMSS(currentTime)
|
|
28
|
+
)
|
|
29
|
+
const [nextTime, setNextTime] = useState<number>(currentTime)
|
|
30
|
+
const [inputError, setInputError] = useState<string>('')
|
|
31
|
+
|
|
32
|
+
const assetWithNewThumbnail = useMemo(() => ({...asset, thumbTime: nextTime}), [asset, nextTime])
|
|
33
|
+
const [saving, setSaving] = useState(false)
|
|
34
|
+
const [saveThumbnailError, setSaveThumbnailError] = useState<Error | null>(null)
|
|
35
|
+
const handleSave = () => {
|
|
36
|
+
setSaving(true)
|
|
37
|
+
client
|
|
38
|
+
.patch(asset._id!)
|
|
39
|
+
.set({thumbTime: nextTime})
|
|
40
|
+
.commit({returnDocuments: false})
|
|
41
|
+
.then(() => void setDialogState(false))
|
|
42
|
+
.catch(setSaveThumbnailError)
|
|
43
|
+
.finally(() => void setSaving(false))
|
|
44
|
+
}
|
|
45
|
+
const width = 300 * getDevicePixelRatio({maxDpr: 2})
|
|
46
|
+
|
|
47
|
+
if (saveThumbnailError) {
|
|
48
|
+
// eslint-disable-next-line no-warning-comments
|
|
49
|
+
// @TODO handle errors more gracefully
|
|
50
|
+
throw saveThumbnailError
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const handleInputChange = (event: React.FormEvent<HTMLInputElement>) => {
|
|
54
|
+
const value = event.currentTarget.value
|
|
55
|
+
setTimeFormatted(value)
|
|
56
|
+
|
|
57
|
+
if (isValidTimeFormat(value)) {
|
|
58
|
+
setInputError('')
|
|
59
|
+
const totalSeconds = getSecondsFromTimeFormat(value)
|
|
60
|
+
setNextTime(totalSeconds)
|
|
61
|
+
} else {
|
|
62
|
+
setInputError('Invalid time format')
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Dialog
|
|
68
|
+
id={dialogId}
|
|
69
|
+
header="Edit thumbnail"
|
|
70
|
+
onClose={() => setDialogState(false)}
|
|
71
|
+
footer={
|
|
72
|
+
<Stack padding={3}>
|
|
73
|
+
<Button
|
|
74
|
+
key="thumbnail"
|
|
75
|
+
disabled={inputError !== ''}
|
|
76
|
+
mode="ghost"
|
|
77
|
+
tone="primary"
|
|
78
|
+
loading={saving}
|
|
79
|
+
onClick={handleSave}
|
|
80
|
+
text="Set new thumbnail"
|
|
81
|
+
/>
|
|
82
|
+
</Stack>
|
|
83
|
+
}
|
|
84
|
+
>
|
|
85
|
+
<Stack space={3} padding={3}>
|
|
86
|
+
<Stack space={2}>
|
|
87
|
+
<Text size={1} weight="semibold">
|
|
88
|
+
Current:
|
|
89
|
+
</Text>
|
|
90
|
+
<VideoThumbnail asset={asset} width={width} staticImage />
|
|
91
|
+
</Stack>
|
|
92
|
+
<Stack space={2}>
|
|
93
|
+
<Text size={1} weight="semibold">
|
|
94
|
+
New:
|
|
95
|
+
</Text>
|
|
96
|
+
<VideoThumbnail asset={assetWithNewThumbnail} width={width} staticImage />
|
|
97
|
+
</Stack>
|
|
98
|
+
|
|
99
|
+
<Stack space={2}>
|
|
100
|
+
<Flex align={'center'} justify={'center'}>
|
|
101
|
+
<Text size={5} weight="semibold">
|
|
102
|
+
Or
|
|
103
|
+
</Text>
|
|
104
|
+
</Flex>
|
|
105
|
+
</Stack>
|
|
106
|
+
|
|
107
|
+
<Stack space={2}>
|
|
108
|
+
<Text size={1} weight="semibold">
|
|
109
|
+
Selected time for thumbnail (hh:mm:ss):
|
|
110
|
+
</Text>
|
|
111
|
+
<TextInput
|
|
112
|
+
size={1}
|
|
113
|
+
value={timeFormatted}
|
|
114
|
+
placeholder="hh:mm:ss"
|
|
115
|
+
onChange={handleInputChange}
|
|
116
|
+
customValidity={inputError}
|
|
117
|
+
/>
|
|
118
|
+
</Stack>
|
|
119
|
+
</Stack>
|
|
120
|
+
</Dialog>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
EllipsisHorizontalIcon,
|
|
3
|
+
ImageIcon,
|
|
3
4
|
LockIcon,
|
|
4
5
|
PlugIcon,
|
|
5
6
|
ResetIcon,
|
|
@@ -43,6 +44,11 @@ const LockButton = styled(Button)`
|
|
|
43
44
|
color: white;
|
|
44
45
|
`
|
|
45
46
|
|
|
47
|
+
// @TODO: add support for audio type (asset._type) when uploading an audio file so we can hide the thumbnail option.
|
|
48
|
+
const isVideoAsset = (asset: VideoAssetDocument) => {
|
|
49
|
+
return asset._type === 'mux.videoAsset'
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
function PlayerActionsMenu(
|
|
47
53
|
props: Pick<MuxInputProps, 'onChange' | 'readOnly'> & {
|
|
48
54
|
asset: VideoAssetDocument
|
|
@@ -111,6 +117,13 @@ function PlayerActionsMenu(
|
|
|
111
117
|
text="Browse"
|
|
112
118
|
onClick={() => setDialogState('select-video')}
|
|
113
119
|
/>
|
|
120
|
+
{isVideoAsset(asset) && (
|
|
121
|
+
<MenuItem
|
|
122
|
+
icon={ImageIcon}
|
|
123
|
+
text="Thumbnail"
|
|
124
|
+
onClick={() => setDialogState('edit-thumbnail')}
|
|
125
|
+
/>
|
|
126
|
+
)}
|
|
114
127
|
<MenuDivider />
|
|
115
128
|
<MenuItem
|
|
116
129
|
icon={PlugIcon}
|
|
@@ -7,6 +7,7 @@ import type {SanityClient} from 'sanity'
|
|
|
7
7
|
import {PatchEvent, set, setIfMissing} from 'sanity'
|
|
8
8
|
|
|
9
9
|
import {uploadFile, uploadUrl} from '../actions/upload'
|
|
10
|
+
import {DialogStateProvider} from '../context/DialogStateContext'
|
|
10
11
|
import {type DialogState, type SetDialogState} from '../hooks/useDialogState'
|
|
11
12
|
import {isValidUrl} from '../util/asserters'
|
|
12
13
|
import {extractDroppedFiles} from '../util/extractFiles'
|
|
@@ -347,21 +348,26 @@ export default function Uploader(props: Props) {
|
|
|
347
348
|
ref={containerRef}
|
|
348
349
|
>
|
|
349
350
|
{props.asset ? (
|
|
350
|
-
<
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
351
|
+
<DialogStateProvider
|
|
352
|
+
dialogState={props.dialogState}
|
|
353
|
+
setDialogState={props.setDialogState}
|
|
354
|
+
>
|
|
355
|
+
<Player
|
|
356
|
+
readOnly={props.readOnly}
|
|
357
|
+
asset={props.asset}
|
|
358
|
+
onChange={props.onChange}
|
|
359
|
+
buttons={
|
|
360
|
+
<PlayerActionsMenu
|
|
361
|
+
asset={props.asset}
|
|
362
|
+
dialogState={props.dialogState}
|
|
363
|
+
setDialogState={props.setDialogState}
|
|
364
|
+
onChange={props.onChange}
|
|
365
|
+
onSelect={handleUpload}
|
|
366
|
+
readOnly={props.readOnly}
|
|
367
|
+
/>
|
|
368
|
+
}
|
|
369
|
+
/>
|
|
370
|
+
</DialogStateProvider>
|
|
365
371
|
) : (
|
|
366
372
|
<UploadPlaceholder
|
|
367
373
|
hovering={dragState !== null}
|
|
@@ -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,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
|
+
}
|
|
@@ -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
|
@@ -262,6 +262,10 @@ export interface AnimatedThumbnailOptions {
|
|
|
262
262
|
fps?: number
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
export interface AssetThumbnailOptions {
|
|
266
|
+
asset: Pick<VideoAssetDocument, 'playbackId' | 'data' | 'thumbTime' | 'filename' | 'assetId'>
|
|
267
|
+
}
|
|
268
|
+
|
|
265
269
|
export type PlaybackPolicy = 'signed' | 'public'
|
|
266
270
|
|
|
267
271
|
export interface MuxErrors {
|