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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanity-plugin-mux-input",
3
- "version": "2.4.1",
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
- "clean": "rimraf lib",
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.68.1",
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
- "rimraf": "^5.0.7",
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": "^5.7.2",
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
- <Player
351
- readOnly={props.readOnly}
352
- asset={props.asset}
353
- onChange={props.onChange}
354
- buttons={
355
- <PlayerActionsMenu
356
- asset={props.asset}
357
- dialogState={props.dialogState}
358
- setDialogState={props.setDialogState}
359
- onChange={props.onChange}
360
- onSelect={handleUpload}
361
- readOnly={props.readOnly}
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<Pick<MuxPlayerProps, 'autoPlay'>>
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
- <Card tone="transparent" style={{aspectRatio: aspectRatio, position: 'relative'}}>
56
- {videoSrc && (
57
- <>
58
- <MuxPlayer
59
- {...props}
60
- playsInline
61
- playbackId={asset.playbackId}
62
- tokens={
63
- signedToken
64
- ? {playback: signedToken, thumbnail: signedToken, storyboard: signedToken}
65
- : undefined
66
- }
67
- preload="metadata"
68
- crossOrigin="anonymous"
69
- metadata={{
70
- player_name: 'Sanity Admin Dashboard',
71
- player_version: process.env.PKG_VERSION,
72
- page_type: 'Preview Player',
73
- }}
74
- audio={isAudio}
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
- height: '100%',
77
- width: '100%',
78
- display: 'block',
79
- objectFit: 'contain',
100
+ position: 'absolute',
101
+ top: '50%',
102
+ left: '50%',
103
+ transform: 'translate(-50%, -50%)',
80
104
  }}
81
- />
82
- {children}
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
- {error ? (
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 {type AnimatedPosterSrcOptions, getAnimatedPosterSrc} from '../util/getAnimatedPosterSrc'
10
- import {VideoAssetDocument} from '../util/types'
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: AnimatedPosterSrcOptions['asset'] & Pick<VideoAssetDocument, 'filename' | 'assetId'>
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 animatedSrc = useMemo(() => {
45
+ const src = useMemo(() => {
43
46
  try {
44
- return getAnimatedPosterSrc({asset, client, width: posterWidth})
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={animatedSrc}
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 default function formatSeconds(seconds: number): string {
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 {generateJwt} from './generateJwt'
4
- import {getPlaybackId} from './getPlaybackId'
5
- import {getPlaybackPolicy} from './getPlaybackPolicy'
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: Pick<VideoAssetDocument, 'playbackId' | 'data' | 'thumbTime'>
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
- let searchParams = new URLSearchParams(
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
  }
@@ -1,12 +1,11 @@
1
1
  import type {SanityClient} from 'sanity'
2
2
 
3
- import {generateJwt} from './generateJwt'
4
- import {getPlaybackId} from './getPlaybackId'
5
- import {getPlaybackPolicy} from './getPlaybackPolicy'
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: VideoAssetDocument
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, time, width}
22
- const playbackId = getPlaybackId(asset)
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
  }
@@ -1,4 +1,4 @@
1
- import formatSeconds from './formatSeconds'
1
+ import {formatSeconds} from './formatSeconds'
2
2
  import {VideoAssetDocument} from './types'
3
3
 
4
4
  export default function getVideoMetadata(doc: VideoAssetDocument) {
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 {