sanity-plugin-mux-input 2.10.0 → 2.11.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.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "An input component that integrates Sanity Studio with Mux video encoding/hosting service.",
5
5
  "keywords": [
6
6
  "sanity",
@@ -52,7 +52,7 @@
52
52
  "watch": "pkg-utils watch --strict"
53
53
  },
54
54
  "dependencies": {
55
- "@mux/mux-player-react": "^2.6.0",
55
+ "@mux/mux-player-react": "^3.8.0",
56
56
  "@mux/upchunk": "^3.4.0",
57
57
  "@sanity/icons": "^3.0.0",
58
58
  "@sanity/incompatible-plugin": "^1.0.4",
@@ -8,15 +8,28 @@ export type {VideoAssetDocument} from '../util/types'
8
8
 
9
9
  export const defaultConfig: PluginConfig = {
10
10
  mp4_support: 'none',
11
- encoding_tier: 'smart',
11
+ video_quality: 'plus',
12
12
  max_resolution_tier: '1080p',
13
13
  normalize_audio: false,
14
+ defaultPublic: true,
14
15
  defaultSigned: false,
15
16
  tool: DEFAULT_TOOL_CONFIG,
16
17
  allowedRolesForConfiguration: [],
17
18
  }
18
19
 
19
20
  export const muxInput = definePlugin<Partial<PluginConfig> | void>((userConfig) => {
21
+ // TODO: Remove this on next major version when we end support for encoding_tier
22
+ if (typeof userConfig === 'object' && 'encoding_tier' in userConfig) {
23
+ const deprecated_encoding_tier = userConfig.encoding_tier
24
+ if (!userConfig.video_quality) {
25
+ if (deprecated_encoding_tier === 'baseline') {
26
+ userConfig.video_quality = 'basic'
27
+ }
28
+ if (deprecated_encoding_tier === 'smart') {
29
+ userConfig.video_quality = 'plus'
30
+ }
31
+ }
32
+ }
20
33
  const config: PluginConfig = {...defaultConfig, ...(userConfig || {})}
21
34
  return {
22
35
  name: 'mux-input',
@@ -23,7 +23,7 @@ import PlaybackPolicy from './uploadConfiguration/PlaybackPolicy'
23
23
  import type {StagedUpload} from './Uploader'
24
24
 
25
25
  export type UploadConfigurationStateAction =
26
- | {action: 'encoding_tier'; value: UploadConfig['encoding_tier']}
26
+ | {action: 'video_quality'; value: UploadConfig['video_quality']}
27
27
  | {action: 'max_resolution_tier'; value: UploadConfig['max_resolution_tier']}
28
28
  | {action: 'mp4_support'; value: UploadConfig['mp4_support']}
29
29
  | {action: 'normalize_audio'; value: UploadConfig['normalize_audio']}
@@ -31,10 +31,11 @@ export type UploadConfigurationStateAction =
31
31
  | {action: 'public_policy'; value: UploadConfig['public_policy']}
32
32
  | TrackAction
33
33
 
34
- const ENCODING_OPTIONS = [
35
- {value: 'smart', label: 'Smart'},
36
- {value: 'baseline', label: 'Baseline'},
37
- ] as const satisfies {value: UploadConfig['encoding_tier']; label: string}[]
34
+ const VIDEO_QUALITY_LEVELS = [
35
+ {value: 'basic', label: 'Basic'},
36
+ {value: 'plus', label: 'Plus'},
37
+ {value: 'premium', label: 'Premium'},
38
+ ] as const satisfies {value: UploadConfig['video_quality']; label: string}[]
38
39
 
39
40
  const RESOLUTION_TIERS = [
40
41
  {value: '1080p', label: '1080p'},
@@ -63,7 +64,7 @@ export default function UploadConfiguration({
63
64
  }) {
64
65
  const id = useId()
65
66
  const autoTextTracks = useRef<NonNullable<UploadConfig['text_tracks']>>(
66
- pluginConfig.encoding_tier === 'smart' && pluginConfig.defaultAutogeneratedSubtitleLang
67
+ pluginConfig.video_quality === 'plus' && pluginConfig.defaultAutogeneratedSubtitleLang
67
68
  ? [
68
69
  {
69
70
  _id: uuid(),
@@ -78,21 +79,21 @@ export default function UploadConfiguration({
78
79
  const [config, dispatch] = useReducer(
79
80
  (prev: UploadConfig, action: UploadConfigurationStateAction) => {
80
81
  switch (action.action) {
81
- case 'encoding_tier':
82
- // If encoding tier switches to baseline, remove smart-only features
83
- if (action.value === 'baseline') {
82
+ case 'video_quality':
83
+ // If video quality level switches to basic, remove plus-only features
84
+ if (action.value === 'basic') {
84
85
  return Object.assign({}, prev, {
85
- encoding_tier: action.value,
86
+ video_quality: action.value,
86
87
  mp4_support: 'none',
87
88
  max_resolution_tier: '1080p',
88
89
  text_tracks: prev.text_tracks?.filter(({type}) => type !== 'autogenerated'),
89
90
  public_policy: true,
90
91
  signed_policy: false,
91
92
  })
92
- // If encoding tier switches to smart, add back in default smart features
93
+ // If video quality level switches to plus, add back in default plus features
93
94
  }
94
95
  return Object.assign({}, prev, {
95
- encoding_tier: action.value,
96
+ video_quality: action.value,
96
97
  mp4_support: pluginConfig.mp4_support,
97
98
  max_resolution_tier: pluginConfig.max_resolution_tier,
98
99
  text_tracks: [...autoTextTracks, ...(prev.text_tracks || [])],
@@ -138,11 +139,11 @@ export default function UploadConfiguration({
138
139
  }
139
140
  },
140
141
  {
141
- encoding_tier: pluginConfig.encoding_tier,
142
+ video_quality: pluginConfig.video_quality,
142
143
  max_resolution_tier: pluginConfig.max_resolution_tier,
143
144
  mp4_support: pluginConfig.mp4_support,
144
145
  signed_policy: secrets.enableSignedUrls && pluginConfig.defaultSigned,
145
- public_policy: true,
146
+ public_policy: pluginConfig.defaultPublic,
146
147
  normalize_audio: pluginConfig.normalize_audio,
147
148
  text_tracks: autoTextTracks,
148
149
  } as UploadConfig
@@ -159,6 +160,7 @@ export default function UploadConfiguration({
159
160
  }, [])
160
161
  if (skipConfig) return null
161
162
 
163
+ const basicConfig = config.video_quality !== 'plus' && config.video_quality !== 'premium'
162
164
  const maxSupportedResolution = RESOLUTION_TIERS.findIndex(
163
165
  (rt) => rt.value === pluginConfig.max_resolution_tier
164
166
  )
@@ -198,11 +200,11 @@ export default function UploadConfiguration({
198
200
  {!disableUploadConfig && (
199
201
  <Stack space={3} paddingBottom={2}>
200
202
  <FormField
201
- title="Encoding Tier"
203
+ title="Video Quality Level"
202
204
  description={
203
205
  <>
204
- The encoding tier informs the cost, quality, and available platform features for
205
- the asset.{' '}
206
+ The video quality level informs the cost, quality, and available platform features
207
+ for the asset.{' '}
206
208
  <a
207
209
  href="https://docs.mux.com/guides/use-encoding-tiers"
208
210
  target="_blank"
@@ -214,17 +216,17 @@ export default function UploadConfiguration({
214
216
  }
215
217
  >
216
218
  <Flex gap={3}>
217
- {ENCODING_OPTIONS.map(({value, label}) => {
219
+ {VIDEO_QUALITY_LEVELS.map(({value, label}) => {
218
220
  const inputId = `${id}--encodingtier-${value}`
219
221
  return (
220
222
  <Flex key={value} align="center" gap={2}>
221
223
  <Radio
222
- checked={config.encoding_tier === value}
224
+ checked={config.video_quality === value}
223
225
  name="asset-encodingtier"
224
226
  onChange={(e) =>
225
227
  dispatch({
226
- action: 'encoding_tier' as const,
227
- value: e.currentTarget.value as UploadConfig['encoding_tier'],
228
+ action: 'video_quality' as const,
229
+ value: e.currentTarget.value as UploadConfig['video_quality'],
228
230
  })
229
231
  }
230
232
  value={value}
@@ -239,7 +241,7 @@ export default function UploadConfiguration({
239
241
  </Flex>
240
242
  </FormField>
241
243
 
242
- {config.encoding_tier === 'smart' && maxSupportedResolution > 0 && (
244
+ {!basicConfig && maxSupportedResolution > 0 && (
243
245
  <FormField
244
246
  title="Resolution Tier"
245
247
  description={
@@ -286,12 +288,12 @@ export default function UploadConfiguration({
286
288
  </FormField>
287
289
  )}
288
290
 
289
- {config.encoding_tier === 'smart' && (
291
+ {!basicConfig && (
290
292
  <FormField title="Additional Configuration">
291
293
  <Stack space={2}>
292
294
  <PlaybackPolicy id={id} config={config} secrets={secrets} dispatch={dispatch} />
293
295
 
294
- {config.encoding_tier === 'smart' && (
296
+ {!basicConfig && (
295
297
  <Flex align="center" gap={2} padding={[0, 2]}>
296
298
  <Checkbox
297
299
  id={`${id}--mp4_support`}
@@ -319,7 +321,7 @@ export default function UploadConfiguration({
319
321
  </Stack>
320
322
  )}
321
323
 
322
- {!disableTextTrackConfig && config.encoding_tier === 'smart' && (
324
+ {!disableTextTrackConfig && !basicConfig && (
323
325
  <TextTracksEditor
324
326
  tracks={config.text_tracks}
325
327
  dispatch={dispatch}
@@ -329,9 +331,7 @@ export default function UploadConfiguration({
329
331
 
330
332
  <Box marginTop={4}>
331
333
  <Button
332
- disabled={
333
- config.encoding_tier === 'smart' && !config.public_policy && !config.signed_policy
334
- }
334
+ disabled={!basicConfig && !config.public_policy && !config.signed_policy}
335
335
  icon={UploadIcon}
336
336
  text="Upload"
337
337
  tone="positive"
@@ -388,7 +388,7 @@ function formatUploadConfig(config: UploadConfig): MuxNewAssetSettings {
388
388
  mp4_support: config.mp4_support,
389
389
  playback_policy: setPlaybackPolicy(config),
390
390
  max_resolution_tier: config.max_resolution_tier,
391
- encoding_tier: config.encoding_tier,
391
+ video_quality: config.video_quality,
392
392
  normalize_audio: config.normalize_audio,
393
393
  }
394
394
  }
@@ -42,6 +42,10 @@ export const UploadProgress = ({
42
42
  onCancel?: React.MouseEventHandler<HTMLButtonElement>
43
43
  text?: React.ReactNode
44
44
  }) => {
45
+ // Disable cancel button when upload is 90% or more complete
46
+ // to prevent inconsistency between Mux and Sanity
47
+ const isCancelDisabled = progress >= 90
48
+
45
49
  return (
46
50
  <CardWrapper tone="primary" padding={4} border height="fill">
47
51
  <FlexWrapper align="center" justify="space-between" height="fill" direction="row" gap={2}>
@@ -67,6 +71,7 @@ export const UploadProgress = ({
67
71
  mode="ghost"
68
72
  tone="critical"
69
73
  onClick={onCancel}
74
+ disabled={isCancelDisabled}
70
75
  />
71
76
  ) : null}
72
77
  </FlexWrapper>
@@ -92,6 +92,7 @@ export default function Uploader(props: Props) {
92
92
  ).current
93
93
 
94
94
  const uploadRef = useRef<Subscription | null>(null)
95
+ const uploadingDocumentId = useRef<string | null>(null)
95
96
  const [state, dispatch] = useReducer(
96
97
  (prev: State, action: UploaderStateAction) => {
97
98
  switch (action.action) {
@@ -121,11 +122,13 @@ export default function Uploader(props: Props) {
121
122
  // Clear upload observable on completion
122
123
  uploadRef.current?.unsubscribe()
123
124
  uploadRef.current = null
125
+ uploadingDocumentId.current = null
124
126
  return INITIAL_STATE
125
127
  case 'error':
126
128
  // Clear upload observable on error
127
129
  uploadRef.current?.unsubscribe()
128
130
  uploadRef.current = null
131
+ uploadingDocumentId.current = null
129
132
  return Object.assign({}, INITIAL_STATE, {error: action.error})
130
133
  default:
131
134
  return prev
@@ -139,13 +142,38 @@ export default function Uploader(props: Props) {
139
142
  )
140
143
 
141
144
  // Make sure we close out the upload observer on dismount
145
+ // and cleanup orphaned documents if upload was in progress
142
146
  useEffect(() => {
143
- return () => {
147
+ const cleanup = () => {
148
+ // Cancel subscription
144
149
  if (uploadRef.current && !uploadRef.current.closed) {
145
150
  uploadRef.current.unsubscribe()
146
151
  }
152
+
153
+ // Delete orphaned document if upload was in progress and document is different from the saved asset
154
+ if (uploadingDocumentId.current && props.asset?._id !== uploadingDocumentId.current) {
155
+ const docId = uploadingDocumentId.current
156
+ uploadingDocumentId.current = null
157
+
158
+ props.client.delete(docId).catch((err) => {
159
+ console.warn('Failed to cleanup orphaned upload document:', err)
160
+ })
161
+ }
162
+ }
163
+
164
+ const handleBeforeUnload = () => {
165
+ cleanup()
147
166
  }
148
- }, [])
167
+
168
+ window.addEventListener('beforeunload', handleBeforeUnload)
169
+ window.addEventListener('pagehide', handleBeforeUnload)
170
+
171
+ return () => {
172
+ window.removeEventListener('beforeunload', handleBeforeUnload)
173
+ window.removeEventListener('pagehide', handleBeforeUnload)
174
+ cleanup()
175
+ }
176
+ }, [props.client, props.asset?._id])
149
177
 
150
178
  /* -------------------------------------------------------------------------- */
151
179
  /* Uploading */
@@ -183,8 +211,9 @@ export default function Uploader(props: Props) {
183
211
  takeUntil(
184
212
  cancelUploadButton.observable.pipe(
185
213
  tap(() => {
186
- if (state.uploadStatus?.uuid) {
187
- props.client.delete(state.uploadStatus.uuid)
214
+ if (uploadingDocumentId.current) {
215
+ props.client.delete(uploadingDocumentId.current)
216
+ uploadingDocumentId.current = null
188
217
  }
189
218
  })
190
219
  )
@@ -196,6 +225,10 @@ export default function Uploader(props: Props) {
196
225
  next: (event) => {
197
226
  switch (event.type) {
198
227
  case 'uuid':
228
+ // Track the document ID for cleanup on unmount
229
+ uploadingDocumentId.current = event.uuid
230
+ dispatch({action: 'progressInfo', ...event})
231
+ break
199
232
  case 'file':
200
233
  case 'url':
201
234
  dispatch({action: 'progressInfo', ...event})
@@ -205,6 +238,7 @@ export default function Uploader(props: Props) {
205
238
  break
206
239
  case 'success':
207
240
  dispatch({action: 'progress', percent: 100})
241
+ uploadingDocumentId.current = null
208
242
  props.onChange(
209
243
  PatchEvent.from([
210
244
  setIfMissing({asset: {}}),
@@ -26,19 +26,23 @@ export default function VideoPlayer({
26
26
 
27
27
  const isAudio = assetIsAudio(asset)
28
28
  const muxPlayer = useRef<MuxPlayerRefAttributes>(null)
29
- const thumbnail = getPosterSrc({asset, client, width: thumbnailWidth})
30
29
 
31
- const {src: videoSrc, error} = useMemo(() => {
30
+ const {
31
+ src: videoSrc,
32
+ thumbnail: thumbnailSrc,
33
+ error,
34
+ } = useMemo(() => {
32
35
  try {
36
+ const thumbnail = getPosterSrc({asset, client, width: thumbnailWidth})
33
37
  const src = asset?.playbackId && getVideoSrc({client, asset})
34
- if (src) return {src: src}
38
+ if (src) return {src: src, thumbnail}
35
39
 
36
40
  return {error: new TypeError('Asset has no playback ID')}
37
41
  // eslint-disable-next-line @typescript-eslint/no-shadow
38
42
  } catch (error) {
39
43
  return {error}
40
44
  }
41
- }, [asset, client])
45
+ }, [asset, client, thumbnailWidth])
42
46
 
43
47
  const signedToken = useMemo(() => {
44
48
  try {
@@ -66,7 +70,7 @@ export default function VideoPlayer({
66
70
  {videoSrc && (
67
71
  <>
68
72
  <MuxPlayer
69
- poster={thumbnail}
73
+ poster={thumbnailSrc}
70
74
  ref={muxPlayer}
71
75
  {...props}
72
76
  playsInline
package/src/schema.ts CHANGED
@@ -98,6 +98,10 @@ const muxAssetData = {
98
98
  type: 'string',
99
99
  name: 'encoding_tier',
100
100
  },
101
+ {
102
+ type: 'string',
103
+ name: 'video_quality',
104
+ },
101
105
  {
102
106
  type: 'string',
103
107
  name: 'master_access',
package/src/util/types.ts CHANGED
@@ -4,7 +4,7 @@ import type {PartialDeep} from 'type-fest'
4
4
  export interface MuxInputConfig {
5
5
  /**
6
6
  * Enable static renditions by setting this to 'standard'. Can be overwritten on a per-asset basis.
7
- * Requires `"encoding_tier": "smart"`
7
+ * Requires `"video_quality": "plus"`
8
8
  * @see {@link https://docs.mux.com/guides/video/enable-static-mp4-renditions#why-enable-mp4-support}
9
9
  * @defaultValue 'none'
10
10
  */
@@ -12,18 +12,27 @@ export interface MuxInputConfig {
12
12
 
13
13
  /**
14
14
  * Max resolution tier can be used to control the maximum resolution_tier your asset is encoded, stored, and streamed at.
15
- * Requires `"encoding_tier": "smart"`
15
+ * Requires `"video_quality": "plus"`
16
16
  * @see {@link https://docs.mux.com/guides/stream-videos-in-4k}
17
17
  * @defaultValue '1080p'
18
18
  */
19
19
  max_resolution_tier: '2160p' | '1440p' | '1080p'
20
20
 
21
21
  /**
22
+ * @deprecated Use {@link video_quality}
23
+ * <br>
22
24
  * The encoding tier informs the cost, quality, and available platform features for the asset.
23
25
  * @see {@link https://docs.mux.com/guides/use-encoding-tiers}
24
26
  * @defaultValue 'smart'
25
27
  */
26
- encoding_tier: 'baseline' | 'smart'
28
+ encoding_tier?: 'baseline' | 'smart'
29
+
30
+ /**
31
+ * The video quality level informs the cost, quality, and available platform features for the asset.
32
+ * @see {@link https://www.mux.com/docs/guides/use-video-quality-levels}
33
+ * @defaultValue 'plus'
34
+ */
35
+ video_quality: 'basic' | 'plus' | 'premium'
27
36
 
28
37
  /**
29
38
  * Normalize the audio track loudness level.
@@ -39,9 +48,15 @@ export interface MuxInputConfig {
39
48
  */
40
49
  defaultSigned?: boolean
41
50
 
51
+ /**
52
+ * Enables public URLs by default.
53
+ * @defaultValue true
54
+ */
55
+ defaultPublic?: boolean
56
+
42
57
  /**
43
58
  * Auto-generate captions for these languages by default.
44
- * Requires `"encoding_tier": "smart"`
59
+ * Requires `"video_quality": "plus"`
45
60
  *
46
61
  * @see {@link https://docs.mux.com/guides/add-autogenerated-captions-and-use-transcripts}
47
62
  * @deprecated use `defaultAutogeneratedSubtitleLang` instead. Only a single autogenerated
@@ -50,7 +65,7 @@ export interface MuxInputConfig {
50
65
 
51
66
  /**
52
67
  * Auto-generate captions for this language by default. Users can still
53
- * Requires `"encoding_tier": "smart"`
68
+ * Requires `"video_quality": "plus"`
54
69
  *
55
70
  * @see {@link https://docs.mux.com/guides/add-autogenerated-captions-and-use-transcripts}
56
71
  */
@@ -123,9 +138,10 @@ export const SUPPORTED_MUX_LANGUAGES = [
123
138
  {label: 'Bulgarian', code: 'bg', state: 'Beta'},
124
139
  ] as const
125
140
 
126
- export const ENCODING_TIERS = [
127
- {label: 'Baseline', value: 'baseline'},
128
- {label: 'Smart', value: 'smart'},
141
+ export const VIDEO_QUALITY_LEVELS = [
142
+ {label: 'Basic', value: 'basic'},
143
+ {label: 'Plus', value: 'plus'},
144
+ {label: 'Premium', value: 'premium'},
129
145
  ] as const
130
146
 
131
147
  export const SUPPORTED_MUX_LANGUAGES_VALUES = SUPPORTED_MUX_LANGUAGES.map((l) => l.code)
@@ -168,7 +184,7 @@ export type UploadTextTrack = AutogeneratedTextTrack | CustomTextTrack
168
184
  export interface UploadConfig
169
185
  extends Pick<
170
186
  MuxInputConfig,
171
- 'encoding_tier' | 'max_resolution_tier' | 'mp4_support' | 'normalize_audio'
187
+ 'max_resolution_tier' | 'mp4_support' | 'normalize_audio' | 'video_quality'
172
188
  > {
173
189
  text_tracks: UploadTextTrack[]
174
190
  signed_policy: boolean
@@ -182,7 +198,7 @@ export interface UploadConfig
182
198
  export interface MuxNewAssetSettings
183
199
  extends Pick<
184
200
  MuxInputConfig,
185
- 'encoding_tier' | 'max_resolution_tier' | 'mp4_support' | 'normalize_audio'
201
+ 'max_resolution_tier' | 'mp4_support' | 'normalize_audio' | 'video_quality'
186
202
  > {
187
203
  /** An array of objects that each describe an input file to be used to create the asset.*/
188
204
  input?: {