sanity-plugin-mux-input 2.5.0 → 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.
@@ -19,6 +19,7 @@ import {
19
19
  type UploadTextTrack,
20
20
  } from '../util/types'
21
21
  import TextTracksEditor, {type TrackAction} from './TextTracksEditor'
22
+ import PlaybackPolicy from './uploadConfiguration/PlaybackPolicy'
22
23
  import type {StagedUpload} from './Uploader'
23
24
 
24
25
  export type UploadConfigurationStateAction =
@@ -26,7 +27,8 @@ export type UploadConfigurationStateAction =
26
27
  | {action: 'max_resolution_tier'; value: UploadConfig['max_resolution_tier']}
27
28
  | {action: 'mp4_support'; value: UploadConfig['mp4_support']}
28
29
  | {action: 'normalize_audio'; value: UploadConfig['normalize_audio']}
29
- | {action: 'signed'; value: UploadConfig['signed']}
30
+ | {action: 'signed_policy'; value: UploadConfig['signed_policy']}
31
+ | {action: 'public_policy'; value: UploadConfig['public_policy']}
30
32
  | TrackAction
31
33
 
32
34
  const ENCODING_OPTIONS = [
@@ -84,6 +86,8 @@ export default function UploadConfiguration({
84
86
  mp4_support: 'none',
85
87
  max_resolution_tier: '1080p',
86
88
  text_tracks: prev.text_tracks?.filter(({type}) => type !== 'autogenerated'),
89
+ public_policy: true,
90
+ signed_policy: false,
87
91
  })
88
92
  // If encoding tier switches to smart, add back in default smart features
89
93
  }
@@ -97,7 +101,9 @@ export default function UploadConfiguration({
97
101
  case 'mp4_support':
98
102
  case 'max_resolution_tier':
99
103
  case 'normalize_audio':
100
- case 'signed':
104
+ case 'signed_policy':
105
+ return Object.assign({}, prev, {[action.action]: action.value})
106
+ case 'public_policy':
101
107
  return Object.assign({}, prev, {[action.action]: action.value})
102
108
  // Updating individual tracks
103
109
  case 'track': {
@@ -135,7 +141,8 @@ export default function UploadConfiguration({
135
141
  encoding_tier: pluginConfig.encoding_tier,
136
142
  max_resolution_tier: pluginConfig.max_resolution_tier,
137
143
  mp4_support: pluginConfig.mp4_support,
138
- signed: secrets.enableSignedUrls && pluginConfig.defaultSigned,
144
+ signed_policy: secrets.enableSignedUrls && pluginConfig.defaultSigned,
145
+ public_policy: true,
139
146
  normalize_audio: pluginConfig.normalize_audio,
140
147
  text_tracks: autoTextTracks,
141
148
  } as UploadConfig
@@ -279,31 +286,13 @@ export default function UploadConfiguration({
279
286
  </FormField>
280
287
  )}
281
288
 
282
- {(secrets.enableSignedUrls || config.encoding_tier === 'smart') && (
289
+ {config.encoding_tier === 'smart' && (
283
290
  <FormField title="Additional Configuration">
284
291
  <Stack space={2}>
285
- {secrets.enableSignedUrls && (
286
- <Flex align="center" gap={2}>
287
- <Checkbox
288
- id={`${id}--signed`}
289
- style={{display: 'block'}}
290
- name="signed"
291
- required
292
- checked={config.signed}
293
- onChange={(e) =>
294
- dispatch({
295
- action: 'signed',
296
- value: e.currentTarget.checked,
297
- })
298
- }
299
- />
300
- <Text>
301
- <label htmlFor={`${id}--signed`}>Signed playback URL</label>
302
- </Text>
303
- </Flex>
304
- )}
292
+ <PlaybackPolicy id={id} config={config} secrets={secrets} dispatch={dispatch} />
293
+
305
294
  {config.encoding_tier === 'smart' && (
306
- <Flex align="center" gap={2}>
295
+ <Flex align="center" gap={2} padding={[0, 2]}>
307
296
  <Checkbox
308
297
  id={`${id}--mp4_support`}
309
298
  style={{display: 'block'}}
@@ -340,6 +329,9 @@ export default function UploadConfiguration({
340
329
 
341
330
  <Box marginTop={4}>
342
331
  <Button
332
+ disabled={
333
+ config.encoding_tier === 'smart' && !config.public_policy && !config.signed_policy
334
+ }
343
335
  icon={UploadIcon}
344
336
  text="Upload"
345
337
  tone="positive"
@@ -351,6 +343,17 @@ export default function UploadConfiguration({
351
343
  )
352
344
  }
353
345
 
346
+ function setPlaybackPolicy(config: UploadConfig): MuxNewAssetSettings['playback_policy'] {
347
+ const playback_policy: MuxNewAssetSettings['playback_policy'] = []
348
+ if (config.public_policy) {
349
+ playback_policy.push('public')
350
+ }
351
+ if (config.signed_policy) {
352
+ playback_policy.push('signed')
353
+ }
354
+ return playback_policy
355
+ }
356
+
354
357
  function formatUploadConfig(config: UploadConfig): MuxNewAssetSettings {
355
358
  const generated_subtitles = config.text_tracks
356
359
  .filter<AutogeneratedTextTrack>(isAutogeneratedTrack)
@@ -383,7 +386,7 @@ function formatUploadConfig(config: UploadConfig): MuxNewAssetSettings {
383
386
  ),
384
387
  ],
385
388
  mp4_support: config.mp4_support,
386
- playback_policy: config.signed ? ['public', 'signed'] : ['public'],
389
+ playback_policy: setPlaybackPolicy(config),
387
390
  max_resolution_tier: config.max_resolution_tier,
388
391
  encoding_tier: config.encoding_tier,
389
392
  normalize_audio: config.normalize_audio,
@@ -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)
@@ -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
+ }
@@ -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 {
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