sanity-plugin-mux-input 2.16.0 → 2.18.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/dist/index.js +916 -78
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +916 -78
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/actions/upload.ts +47 -7
- package/src/components/AddCaptionDialog.tsx +28 -9
- package/src/components/DraggableWatermark.tsx +877 -0
- package/src/components/EditCaptionDialog.tsx +4 -2
- package/src/components/UploadConfiguration.tsx +259 -59
- package/src/components/Uploader.tsx +7 -1
- package/src/components/VideoPlayer.tsx +2 -0
- package/src/hooks/useMediaMetadata.ts +3 -0
- package/src/util/convertWatermarkToMux.ts +160 -0
- package/src/util/formatDriveShareLink.ts +64 -0
- package/src/util/roundPxString.ts +16 -0
- package/src/util/types.ts +43 -1
package/package.json
CHANGED
package/src/actions/upload.ts
CHANGED
|
@@ -4,10 +4,39 @@ import {catchError, mergeMap, mergeMapTo, switchMap} from 'rxjs/operators'
|
|
|
4
4
|
import type {SanityClient} from 'sanity'
|
|
5
5
|
|
|
6
6
|
import {createUpChunkObservable} from '../clients/upChunkObservable'
|
|
7
|
-
import
|
|
7
|
+
import {formatDriveShareLink} from '../util/formatDriveShareLink'
|
|
8
|
+
import {roundPxString} from '../util/roundPxString'
|
|
9
|
+
import type {MuxAsset, MuxNewAssetSettings, WatermarkConfig} from '../util/types'
|
|
8
10
|
import {getAsset} from './assets'
|
|
9
11
|
import {testSecretsObservable} from './secrets'
|
|
10
12
|
|
|
13
|
+
function sanitizeOverlaySettingsInPlace(settings: MuxNewAssetSettings) {
|
|
14
|
+
const inputs = settings.input
|
|
15
|
+
if (!inputs) return
|
|
16
|
+
for (const input of inputs) {
|
|
17
|
+
const overlay = (input as {overlay_settings?: Record<string, unknown>}).overlay_settings
|
|
18
|
+
if (!overlay) continue
|
|
19
|
+
|
|
20
|
+
const hm = roundPxString(overlay.horizontal_margin)
|
|
21
|
+
const vm = roundPxString(overlay.vertical_margin)
|
|
22
|
+
const w = roundPxString(overlay.width)
|
|
23
|
+
|
|
24
|
+
if (hm) overlay.horizontal_margin = hm
|
|
25
|
+
if (vm) overlay.vertical_margin = vm
|
|
26
|
+
if (w) overlay.width = w
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sanitizePxStringsInJson(json: string): string {
|
|
31
|
+
return json.replace(/"(-?\d+(?:\.\d+)?)px"/g, (_match, num) => {
|
|
32
|
+
const n = Number(num)
|
|
33
|
+
if (!Number.isFinite(n)) return _match
|
|
34
|
+
let rounded = Math.round(n)
|
|
35
|
+
if (rounded === 0) rounded = n < 0 ? -1 : 1
|
|
36
|
+
return `"${rounded}px"`
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
11
40
|
export function cancelUpload(client: SanityClient, uuid: string) {
|
|
12
41
|
return client.observable.request({
|
|
13
42
|
url: `/addons/mux/uploads/${client.config().dataset}/${uuid}`,
|
|
@@ -20,10 +49,12 @@ export function uploadUrl({
|
|
|
20
49
|
url,
|
|
21
50
|
settings,
|
|
22
51
|
client,
|
|
52
|
+
watermark,
|
|
23
53
|
}: {
|
|
24
54
|
url: string
|
|
25
55
|
settings: MuxNewAssetSettings
|
|
26
56
|
client: SanityClient
|
|
57
|
+
watermark?: WatermarkConfig
|
|
27
58
|
}) {
|
|
28
59
|
return testUrl(url).pipe(
|
|
29
60
|
switchMap((validUrl) => {
|
|
@@ -38,9 +69,10 @@ export function uploadUrl({
|
|
|
38
69
|
const muxBody = settings
|
|
39
70
|
if (!muxBody.input) muxBody.input = [{type: 'video'}]
|
|
40
71
|
muxBody.input[0].url = validUrl
|
|
72
|
+
sanitizeOverlaySettingsInPlace(muxBody)
|
|
41
73
|
|
|
42
74
|
const query = {
|
|
43
|
-
muxBody: JSON.stringify(muxBody),
|
|
75
|
+
muxBody: sanitizePxStringsInJson(JSON.stringify(muxBody)),
|
|
44
76
|
filename: validUrl.split('/').slice(-1)[0],
|
|
45
77
|
}
|
|
46
78
|
|
|
@@ -79,10 +111,12 @@ export function uploadFile({
|
|
|
79
111
|
settings,
|
|
80
112
|
client,
|
|
81
113
|
file,
|
|
114
|
+
watermark,
|
|
82
115
|
}: {
|
|
83
116
|
settings: MuxNewAssetSettings
|
|
84
117
|
client: SanityClient
|
|
85
118
|
file: File
|
|
119
|
+
watermark?: WatermarkConfig
|
|
86
120
|
}) {
|
|
87
121
|
return testFile(file).pipe(
|
|
88
122
|
switchMap((fileOptions) => {
|
|
@@ -95,6 +129,7 @@ export function uploadFile({
|
|
|
95
129
|
}
|
|
96
130
|
const uuid = generateUuid()
|
|
97
131
|
const body = settings
|
|
132
|
+
sanitizeOverlaySettingsInPlace(body)
|
|
98
133
|
|
|
99
134
|
return concat(
|
|
100
135
|
of({type: 'uuid' as const, uuid}),
|
|
@@ -129,7 +164,7 @@ export function uploadFile({
|
|
|
129
164
|
if (event.type !== 'success') {
|
|
130
165
|
return of(event)
|
|
131
166
|
}
|
|
132
|
-
return from(updateAssetDocumentFromUpload(client, uuid)).pipe(
|
|
167
|
+
return from(updateAssetDocumentFromUpload(client, uuid, watermark)).pipe(
|
|
133
168
|
// eslint-disable-next-line max-nested-callbacks
|
|
134
169
|
mergeMap((doc) => of({...event, asset: doc}))
|
|
135
170
|
)
|
|
@@ -201,7 +236,11 @@ function pollUpload(client: SanityClient, uuid: string): Promise<UploadResponse>
|
|
|
201
236
|
})
|
|
202
237
|
}
|
|
203
238
|
|
|
204
|
-
async function updateAssetDocumentFromUpload(
|
|
239
|
+
async function updateAssetDocumentFromUpload(
|
|
240
|
+
client: SanityClient,
|
|
241
|
+
uuid: string,
|
|
242
|
+
_watermark?: WatermarkConfig
|
|
243
|
+
) {
|
|
205
244
|
let upload: UploadResponse
|
|
206
245
|
let asset: {data: MuxAsset}
|
|
207
246
|
try {
|
|
@@ -242,17 +281,18 @@ export function testUrl(url: string): Observable<string> {
|
|
|
242
281
|
if (typeof url !== 'string') {
|
|
243
282
|
return throwError(error)
|
|
244
283
|
}
|
|
245
|
-
|
|
284
|
+
let formattedUrl = url.trim()
|
|
285
|
+
formattedUrl = formatDriveShareLink(formattedUrl)
|
|
246
286
|
let parsed
|
|
247
287
|
try {
|
|
248
|
-
parsed = new URL(
|
|
288
|
+
parsed = new URL(formattedUrl)
|
|
249
289
|
} catch (err) {
|
|
250
290
|
return throwError(error)
|
|
251
291
|
}
|
|
252
292
|
if (parsed && !parsed.protocol.match(/http:|https:/)) {
|
|
253
293
|
return throwError(error)
|
|
254
294
|
}
|
|
255
|
-
return of(
|
|
295
|
+
return of(formattedUrl)
|
|
256
296
|
}
|
|
257
297
|
|
|
258
298
|
function optionsFromFile(opts: {preserveFilename?: boolean}, file: File) {
|
|
@@ -18,6 +18,7 @@ import {useId, useRef, useState} from 'react'
|
|
|
18
18
|
|
|
19
19
|
import {addTextTrackFromUrl, generateSubtitles, getAsset} from '../actions/assets'
|
|
20
20
|
import {useClient} from '../hooks/useClient'
|
|
21
|
+
import {addKeysToMuxData} from '../util/addKeysToMuxData'
|
|
21
22
|
import {extractErrorMessage, pollTrackStatus} from '../util/textTracks'
|
|
22
23
|
import {type MuxTextTrack, SUPPORTED_MUX_LANGUAGES, type VideoAssetDocument} from '../util/types'
|
|
23
24
|
|
|
@@ -60,6 +61,20 @@ export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
|
60
61
|
return assetDocument.url
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
const refreshAssetData = async () => {
|
|
65
|
+
if (!asset._id || !asset.assetId) return
|
|
66
|
+
try {
|
|
67
|
+
const latestAssetData = await getAsset(client, asset.assetId)
|
|
68
|
+
const dataWithKeys = addKeysToMuxData(latestAssetData.data)
|
|
69
|
+
await client
|
|
70
|
+
.patch(asset._id)
|
|
71
|
+
.set({data: dataWithKeys, status: latestAssetData.data.status})
|
|
72
|
+
.commit({returnDocuments: false})
|
|
73
|
+
} catch (refreshError) {
|
|
74
|
+
console.error('Failed to refresh asset data:', refreshError)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
const handleAddTrackFromUrl = async () => {
|
|
64
79
|
if (!asset.assetId) {
|
|
65
80
|
throw new Error('Asset ID is required')
|
|
@@ -75,9 +90,9 @@ export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
|
75
90
|
vttUrlToUse = await uploadVttFile(selectedFile)
|
|
76
91
|
} catch (uploadError) {
|
|
77
92
|
toast.push({
|
|
78
|
-
title: 'Failed to upload
|
|
93
|
+
title: 'Failed to upload caption file',
|
|
79
94
|
status: 'error',
|
|
80
|
-
description: 'Could not upload the
|
|
95
|
+
description: 'Could not upload the caption file to Sanity. Please try again.',
|
|
81
96
|
})
|
|
82
97
|
setIsSubmitting(false)
|
|
83
98
|
throw uploadError
|
|
@@ -95,7 +110,7 @@ export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
|
95
110
|
assetId: asset.assetId,
|
|
96
111
|
trackName: trimmedName,
|
|
97
112
|
trackLanguageCode: trimmedLanguageCode,
|
|
98
|
-
onTrackErrored: (track) => {
|
|
113
|
+
onTrackErrored: async (track) => {
|
|
99
114
|
const errorMessage =
|
|
100
115
|
track.error?.messages?.[0] ||
|
|
101
116
|
track.error?.type ||
|
|
@@ -105,6 +120,7 @@ export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
|
105
120
|
status: 'error',
|
|
106
121
|
description: errorMessage,
|
|
107
122
|
})
|
|
123
|
+
await refreshAssetData()
|
|
108
124
|
onAdd(track)
|
|
109
125
|
onClose()
|
|
110
126
|
},
|
|
@@ -132,11 +148,13 @@ export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
|
132
148
|
description:
|
|
133
149
|
'The track was created and is being processed. It will appear in the list shortly.',
|
|
134
150
|
})
|
|
151
|
+
await refreshAssetData()
|
|
135
152
|
onAdd(result.track)
|
|
136
153
|
onClose()
|
|
137
154
|
return
|
|
138
155
|
}
|
|
139
156
|
|
|
157
|
+
await refreshAssetData()
|
|
140
158
|
toast.push({
|
|
141
159
|
title: 'Caption track added',
|
|
142
160
|
status: 'success',
|
|
@@ -195,9 +213,9 @@ export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
|
195
213
|
if (!isAutogenerated) {
|
|
196
214
|
if (!selectedFile && !vttUrl.trim()) {
|
|
197
215
|
toast.push({
|
|
198
|
-
title: '
|
|
216
|
+
title: 'Caption file or URL required',
|
|
199
217
|
status: 'error',
|
|
200
|
-
description: 'Please select a VTT file or enter a
|
|
218
|
+
description: 'Please select a VTT or SRT file or enter a caption file URL',
|
|
201
219
|
})
|
|
202
220
|
return
|
|
203
221
|
}
|
|
@@ -209,7 +227,8 @@ export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
|
209
227
|
toast.push({
|
|
210
228
|
title: 'Invalid URL',
|
|
211
229
|
status: 'error',
|
|
212
|
-
description:
|
|
230
|
+
description:
|
|
231
|
+
'Please enter a valid URL (e.g., https://example.com/subtitles.vtt or subtitles.srt)',
|
|
213
232
|
})
|
|
214
233
|
return
|
|
215
234
|
}
|
|
@@ -303,7 +322,7 @@ export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
|
303
322
|
<input
|
|
304
323
|
ref={fileInputRef}
|
|
305
324
|
type="file"
|
|
306
|
-
accept=".vtt,text/vtt"
|
|
325
|
+
accept=".vtt,text/vtt,.srt,application/x-subrip"
|
|
307
326
|
style={{display: 'none'}}
|
|
308
327
|
onChange={(e) => {
|
|
309
328
|
if (e.target.files && e.target.files.length > 0 && !isSubmitting) {
|
|
@@ -314,10 +333,10 @@ export default function AddCaptionDialog({asset, onAdd, onClose}: Props) {
|
|
|
314
333
|
/>
|
|
315
334
|
</Card>
|
|
316
335
|
<Text size={1} muted style={{textAlign: 'center'}}>
|
|
317
|
-
Or enter the
|
|
336
|
+
Or enter the caption file URL
|
|
318
337
|
</Text>
|
|
319
338
|
<Stack space={2}>
|
|
320
|
-
<Label htmlFor="vtt-url">
|
|
339
|
+
<Label htmlFor="vtt-url">Caption File URL (.vtt or .srt)</Label>
|
|
321
340
|
<TextInput
|
|
322
341
|
id="vtt-url"
|
|
323
342
|
placeholder="https://example.com/subtitles.vtt"
|