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.
@@ -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
- const [references, referencesLoading] = useDocReferences({
21
- documentStore,
22
- id: props.asset._id as string,
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<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,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
+ }
@@ -1,6 +1,5 @@
1
1
  import {useMemo, useState} from 'react'
2
- import {useObservable} from 'react-rx'
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
- documentStore,
20
- sort,
21
- searchQuery,
22
- }: {
23
- documentStore: DocumentStore
24
- sort: SortOption
25
- searchQuery: string
26
- }): VideoAssetDocument[] | undefined => {
27
- const memoizedObservable = useMemo(() => {
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
- return useObservable(memoizedObservable, undefined)
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 assetDocumentsObservable = useAssetDocuments({documentStore, sort, searchQuery})
49
- const isLoading = assetDocumentsObservable === undefined
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>(assetDocumentsObservable ?? []).map(
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
- [assetDocumentsObservable]
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 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
@@ -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
- signed: boolean
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 {