sanity-plugin-mux-input 2.10.1 → 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.1",
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",
@@ -11,6 +11,7 @@ export const defaultConfig: PluginConfig = {
11
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: [],
@@ -143,7 +143,7 @@ export default function UploadConfiguration({
143
143
  max_resolution_tier: pluginConfig.max_resolution_tier,
144
144
  mp4_support: pluginConfig.mp4_support,
145
145
  signed_policy: secrets.enableSignedUrls && pluginConfig.defaultSigned,
146
- public_policy: true,
146
+ public_policy: pluginConfig.defaultPublic,
147
147
  normalize_audio: pluginConfig.normalize_audio,
148
148
  text_tracks: autoTextTracks,
149
149
  } as UploadConfig
@@ -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/util/types.ts CHANGED
@@ -48,6 +48,12 @@ export interface MuxInputConfig {
48
48
  */
49
49
  defaultSigned?: boolean
50
50
 
51
+ /**
52
+ * Enables public URLs by default.
53
+ * @defaultValue true
54
+ */
55
+ defaultPublic?: boolean
56
+
51
57
  /**
52
58
  * Auto-generate captions for these languages by default.
53
59
  * Requires `"video_quality": "plus"`