@pro-laico/payload-mux 0.0.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/LICENSE.md +52 -0
- package/README.md +11 -0
- package/dist/collections/MuxVideo.d.ts +7 -0
- package/dist/collections/MuxVideo.d.ts.map +1 -0
- package/dist/collections/MuxVideo.js +266 -0
- package/dist/collections/MuxVideo.js.map +1 -0
- package/dist/components/MuxUploaderField.d.ts +10 -0
- package/dist/components/MuxUploaderField.d.ts.map +1 -0
- package/dist/components/MuxUploaderField.js +143 -0
- package/dist/components/MuxUploaderField.js.map +1 -0
- package/dist/components/MuxVideoGifCell.d.ts +5 -0
- package/dist/components/MuxVideoGifCell.d.ts.map +1 -0
- package/dist/components/MuxVideoGifCell.js +21 -0
- package/dist/components/MuxVideoGifCell.js.map +1 -0
- package/dist/components/MuxVideoImageCell.d.ts +5 -0
- package/dist/components/MuxVideoImageCell.d.ts.map +1 -0
- package/dist/components/MuxVideoImageCell.js +21 -0
- package/dist/components/MuxVideoImageCell.js.map +1 -0
- package/dist/components/mux-uploader.css +23 -0
- package/dist/endpoints/upload.d.ts +10 -0
- package/dist/endpoints/upload.d.ts.map +1 -0
- package/dist/endpoints/upload.js +49 -0
- package/dist/endpoints/upload.js.map +1 -0
- package/dist/endpoints/webhook.d.ts +16 -0
- package/dist/endpoints/webhook.d.ts.map +1 -0
- package/dist/endpoints/webhook.js +148 -0
- package/dist/endpoints/webhook.js.map +1 -0
- package/dist/hooks/afterDelete.d.ts +6 -0
- package/dist/hooks/afterDelete.d.ts.map +1 -0
- package/dist/hooks/afterDelete.js +12 -0
- package/dist/hooks/afterDelete.js.map +1 -0
- package/dist/hooks/beforeChange.d.ts +11 -0
- package/dist/hooks/beforeChange.d.ts.map +1 -0
- package/dist/hooks/beforeChange.js +95 -0
- package/dist/hooks/beforeChange.js.map +1 -0
- package/dist/hooks/beforeValidate.d.ts +17 -0
- package/dist/hooks/beforeValidate.d.ts.map +1 -0
- package/dist/hooks/beforeValidate.js +61 -0
- package/dist/hooks/beforeValidate.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/defaultAccess.d.ts +5 -0
- package/dist/lib/defaultAccess.d.ts.map +1 -0
- package/dist/lib/defaultAccess.js +4 -0
- package/dist/lib/defaultAccess.js.map +1 -0
- package/dist/lib/delay.d.ts +3 -0
- package/dist/lib/delay.d.ts.map +1 -0
- package/dist/lib/delay.js +3 -0
- package/dist/lib/delay.js.map +1 -0
- package/dist/lib/getAssetMetadata.d.ts +34 -0
- package/dist/lib/getAssetMetadata.d.ts.map +1 -0
- package/dist/lib/getAssetMetadata.js +21 -0
- package/dist/lib/getAssetMetadata.js.map +1 -0
- package/dist/lib/ingest.d.ts +53 -0
- package/dist/lib/ingest.d.ts.map +1 -0
- package/dist/lib/ingest.js +109 -0
- package/dist/lib/ingest.js.map +1 -0
- package/dist/lib/isAllowed.d.ts +7 -0
- package/dist/lib/isAllowed.d.ts.map +1 -0
- package/dist/lib/isAllowed.js +6 -0
- package/dist/lib/isAllowed.js.map +1 -0
- package/dist/plugin.d.ts +13 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +80 -0
- package/dist/plugin.js.map +1 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +89 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Pro Laico LLC <contact@prolaico.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
'Software'), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Third-party notices
|
|
27
|
+
|
|
28
|
+
Portions of this package are derived from
|
|
29
|
+
[`@oversightstudio/mux-video`](https://github.com/oversightstudio/payload-plugins),
|
|
30
|
+
used under the MIT License:
|
|
31
|
+
|
|
32
|
+
MIT License
|
|
33
|
+
|
|
34
|
+
Copyright (c) 2025 OVERSIGHT STUDIO LIMITED
|
|
35
|
+
|
|
36
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
37
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
38
|
+
in the Software without restriction, including without limitation the rights
|
|
39
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
40
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
41
|
+
furnished to do so, subject to the following conditions:
|
|
42
|
+
|
|
43
|
+
The above copyright notice and this permission notice shall be included in all
|
|
44
|
+
copies or substantial portions of the Software.
|
|
45
|
+
|
|
46
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
47
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
48
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
49
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
50
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
51
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
52
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @pro-laico/payload-mux
|
|
2
|
+
|
|
3
|
+
[Mux Video](https://www.mux.com/) for [Payload CMS](https://payloadcms.com/) — a Videos collection that uploads directly to Mux, supports public or signed playback, exposes virtual playback/poster/gif URLs, and keeps Payload and Mux in sync.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add @pro-laico/payload-mux
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
**[Documentation →](https://payload-plugins.prolaico.com/docs/plugins/payload-mux)**
|
|
10
|
+
|
|
11
|
+
Based on [`@oversightstudio/mux-video`](https://github.com/oversightstudio/payload-plugins) (MIT) by [Idan Yekutiel](https://github.com/idanyekutiel).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type Mux from '@mux/mux-node';
|
|
2
|
+
import type { CollectionConfig } from 'payload';
|
|
3
|
+
import type { MuxVideoPluginOptions } from '../types';
|
|
4
|
+
/** The Videos collection: an uploader field, identifying metadata Mux fills in, and a
|
|
5
|
+
* `playbackOptions` array whose virtual fields resolve to signed-or-public Mux URLs. */
|
|
6
|
+
export declare const MuxVideo: (mux: Mux, options: MuxVideoPluginOptions) => CollectionConfig;
|
|
7
|
+
//# sourceMappingURL=MuxVideo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MuxVideo.d.ts","sourceRoot":"","sources":["../../src/collections/MuxVideo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,eAAe,CAAA;AACpC,OAAO,KAAK,EAAE,gBAAgB,EAAS,MAAM,SAAS,CAAA;AAKtD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AA2DrD;yFACyF;AACzF,eAAO,MAAM,QAAQ,GAAI,KAAK,GAAG,EAAE,SAAS,qBAAqB,KAAG,gBAoHlE,CAAA"}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { getAfterDeleteHook } from "../hooks/afterDelete.js";
|
|
2
|
+
import { getBeforeChangeHook } from "../hooks/beforeChange.js";
|
|
3
|
+
import { getBeforeValidateHook } from "../hooks/beforeValidate.js";
|
|
4
|
+
import { isAllowed } from "../lib/isAllowed.js";
|
|
5
|
+
const C = '@pro-laico/payload-mux/components';
|
|
6
|
+
/** Resolve the list-view Cell component for the configured `adminThumbnail` mode. */ const thumbnailCell = (mode)=>{
|
|
7
|
+
if (mode === 'image') return `${C}/MuxVideoImageCell#MuxVideoImageCell`;
|
|
8
|
+
if (mode === 'none') return undefined;
|
|
9
|
+
return `${C}/MuxVideoGifCell#MuxVideoGifCell`;
|
|
10
|
+
};
|
|
11
|
+
/** Sign a playback id for a given media `type` when the policy is `signed`, and append the
|
|
12
|
+
* token to `url`. A no-op for public playback. */ const signIfNeeded = async (mux, options, url, playbackId, policy, type, posterTimestamp)=>{
|
|
13
|
+
if (policy !== 'signed') return;
|
|
14
|
+
const params = typeof posterTimestamp === 'number' ? {
|
|
15
|
+
time: posterTimestamp.toString()
|
|
16
|
+
} : undefined;
|
|
17
|
+
const token = await mux.jwt.signPlaybackId(playbackId, {
|
|
18
|
+
expiration: options.signedUrlOptions?.expiration ?? '1d',
|
|
19
|
+
type,
|
|
20
|
+
params
|
|
21
|
+
});
|
|
22
|
+
url.searchParams.set('token', token);
|
|
23
|
+
};
|
|
24
|
+
/** Build one virtual, read-only URL field (`playbackUrl` / `posterUrl` / `gifUrl`). On read it
|
|
25
|
+
* builds the Mux URL from the sibling `playbackId`, stamps the poster `time` for thumbnail/gif,
|
|
26
|
+
* and signs it when the policy is `signed`. Returns null until a playback id exists. */ const signableUrlField = (mux, options, name, label, type, buildUrl)=>({
|
|
27
|
+
name,
|
|
28
|
+
label,
|
|
29
|
+
type: 'text',
|
|
30
|
+
virtual: true,
|
|
31
|
+
admin: {
|
|
32
|
+
hidden: true
|
|
33
|
+
},
|
|
34
|
+
hooks: {
|
|
35
|
+
afterRead: [
|
|
36
|
+
async ({ data, siblingData })=>{
|
|
37
|
+
const playbackId = siblingData?.playbackId;
|
|
38
|
+
if (!playbackId) return null;
|
|
39
|
+
const posterTimestamp = type !== 'video' && typeof data?.posterTimestamp === 'number' ? data.posterTimestamp : undefined;
|
|
40
|
+
const url = buildUrl(playbackId);
|
|
41
|
+
if (posterTimestamp !== undefined) url.searchParams.set('time', posterTimestamp.toString());
|
|
42
|
+
await signIfNeeded(mux, options, url, playbackId, siblingData.playbackPolicy, type, posterTimestamp);
|
|
43
|
+
return url.toString();
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
/** The Videos collection: an uploader field, identifying metadata Mux fills in, and a
|
|
49
|
+
* `playbackOptions` array whose virtual fields resolve to signed-or-public Mux URLs. */ export const MuxVideo = (mux, options)=>({
|
|
50
|
+
slug: options.extendCollection ?? 'mux-video',
|
|
51
|
+
labels: {
|
|
52
|
+
singular: 'Video',
|
|
53
|
+
plural: 'Videos'
|
|
54
|
+
},
|
|
55
|
+
// Declares this as a `@pro-laico/payload-seed` asset collection: instead of uploading bytes,
|
|
56
|
+
// the seed engine hands a doc's `_file` to the `source` field below, whose beforeValidate hook
|
|
57
|
+
// uploads it to Mux. Plain config — payload-mux doesn't import the seed package.
|
|
58
|
+
custom: {
|
|
59
|
+
seedAsset: {
|
|
60
|
+
sourceField: 'source'
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
access: {
|
|
64
|
+
read: ({ req })=>isAllowed(options, req)
|
|
65
|
+
},
|
|
66
|
+
// Grouped with the other asset collections (images/icons/fonts) — but never re-grouping a
|
|
67
|
+
// collection the plugin merely extends.
|
|
68
|
+
admin: {
|
|
69
|
+
...options.extendCollection ? {} : {
|
|
70
|
+
group: 'Assets'
|
|
71
|
+
},
|
|
72
|
+
useAsTitle: 'title',
|
|
73
|
+
defaultColumns: [
|
|
74
|
+
'title',
|
|
75
|
+
'muxUploader',
|
|
76
|
+
'duration'
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
hooks: {
|
|
80
|
+
afterDelete: [
|
|
81
|
+
getAfterDeleteHook(mux)
|
|
82
|
+
],
|
|
83
|
+
beforeValidate: [
|
|
84
|
+
getBeforeValidateHook(mux, options)
|
|
85
|
+
],
|
|
86
|
+
beforeChange: [
|
|
87
|
+
getBeforeChangeHook(mux)
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
fields: [
|
|
91
|
+
{
|
|
92
|
+
name: 'muxUploader',
|
|
93
|
+
label: 'Video Preview',
|
|
94
|
+
type: 'ui',
|
|
95
|
+
admin: {
|
|
96
|
+
components: {
|
|
97
|
+
Field: `${C}/MuxUploaderField#MuxUploaderField`,
|
|
98
|
+
Cell: thumbnailCell(options.adminThumbnail)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
// Transient server-side ingest input (a local path / URL, or `{ file|url, playbackPolicy,
|
|
103
|
+
// posterTimestamp }`). The beforeValidate hook uploads it to Mux and strips it — never
|
|
104
|
+
// persisted. Lets a video be created from a file without the admin's client-side upload.
|
|
105
|
+
{
|
|
106
|
+
name: 'source',
|
|
107
|
+
type: 'json',
|
|
108
|
+
admin: {
|
|
109
|
+
hidden: true,
|
|
110
|
+
disableListColumn: true,
|
|
111
|
+
disableBulkEdit: true
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'title',
|
|
116
|
+
type: 'text',
|
|
117
|
+
label: 'Title',
|
|
118
|
+
required: true,
|
|
119
|
+
unique: true,
|
|
120
|
+
admin: {
|
|
121
|
+
description: 'A unique title for this video that will help you identify it later.'
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
// Not `required`: it's filled by the beforeValidate (server-side ingest) / beforeChange
|
|
125
|
+
// (admin upload) hooks before the row is written. Marking it required would reject a
|
|
126
|
+
// create that supplies a `source` instead of an already-resolved `assetId`.
|
|
127
|
+
// Indexed: the webhook looks a doc up by assetId on every Mux event.
|
|
128
|
+
{
|
|
129
|
+
name: 'assetId',
|
|
130
|
+
type: 'text',
|
|
131
|
+
index: true,
|
|
132
|
+
admin: {
|
|
133
|
+
readOnly: true,
|
|
134
|
+
condition: (data)=>data.assetId
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
// Encoding lifecycle, hook-written: 'preparing' until Mux reports the asset ready (short
|
|
138
|
+
// poll or webhook), 'ready' once playable, 'errored' when Mux rejects it (see `error`).
|
|
139
|
+
// Not required — docs created before this field existed have no status.
|
|
140
|
+
{
|
|
141
|
+
name: 'status',
|
|
142
|
+
type: 'select',
|
|
143
|
+
options: [
|
|
144
|
+
{
|
|
145
|
+
label: 'Preparing',
|
|
146
|
+
value: 'preparing'
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
label: 'Ready',
|
|
150
|
+
value: 'ready'
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
label: 'Errored',
|
|
154
|
+
value: 'errored'
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
admin: {
|
|
158
|
+
position: 'sidebar',
|
|
159
|
+
readOnly: true,
|
|
160
|
+
condition: (data)=>data.status
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
// Mux's error messages when `status` is 'errored'; surfaced by the uploader field.
|
|
164
|
+
{
|
|
165
|
+
name: 'error',
|
|
166
|
+
type: 'text',
|
|
167
|
+
admin: {
|
|
168
|
+
hidden: true,
|
|
169
|
+
readOnly: true
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'duration',
|
|
174
|
+
label: 'Duration',
|
|
175
|
+
type: 'number',
|
|
176
|
+
admin: {
|
|
177
|
+
readOnly: true,
|
|
178
|
+
condition: (data)=>data.duration
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'posterTimestamp',
|
|
183
|
+
type: 'number',
|
|
184
|
+
label: 'Poster Timestamp',
|
|
185
|
+
min: 0,
|
|
186
|
+
admin: {
|
|
187
|
+
description: 'A timestamp (in seconds) from the video to be used as the poster image. When unset, defaults to the middle of the video.',
|
|
188
|
+
condition: (data)=>data.duration,
|
|
189
|
+
step: 0.25
|
|
190
|
+
},
|
|
191
|
+
validate: (value, { siblingData })=>{
|
|
192
|
+
if (!siblingData.duration || !value) return true;
|
|
193
|
+
return siblingData.duration > value || 'Poster timestamp must be less than the video duration.';
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'aspectRatio',
|
|
198
|
+
label: 'Aspect Ratio',
|
|
199
|
+
type: 'text',
|
|
200
|
+
admin: {
|
|
201
|
+
readOnly: true,
|
|
202
|
+
condition: (data)=>data.aspectRatio
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: 'maxWidth',
|
|
207
|
+
type: 'number',
|
|
208
|
+
admin: {
|
|
209
|
+
readOnly: true,
|
|
210
|
+
condition: (data)=>data.maxWidth
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'maxHeight',
|
|
215
|
+
type: 'number',
|
|
216
|
+
admin: {
|
|
217
|
+
readOnly: true,
|
|
218
|
+
condition: (data)=>data.maxHeight
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'playbackOptions',
|
|
223
|
+
type: 'array',
|
|
224
|
+
admin: {
|
|
225
|
+
readOnly: true,
|
|
226
|
+
condition: (data)=>!!data.playbackOptions
|
|
227
|
+
},
|
|
228
|
+
fields: [
|
|
229
|
+
// Both required: the pair is always written together, and it tightens the generated types.
|
|
230
|
+
{
|
|
231
|
+
name: 'playbackId',
|
|
232
|
+
label: 'Playback ID',
|
|
233
|
+
type: 'text',
|
|
234
|
+
required: true,
|
|
235
|
+
admin: {
|
|
236
|
+
readOnly: true
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: 'playbackPolicy',
|
|
241
|
+
label: 'Playback Policy',
|
|
242
|
+
type: 'select',
|
|
243
|
+
required: true,
|
|
244
|
+
options: [
|
|
245
|
+
{
|
|
246
|
+
label: 'signed',
|
|
247
|
+
value: 'signed'
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
label: 'public',
|
|
251
|
+
value: 'public'
|
|
252
|
+
}
|
|
253
|
+
],
|
|
254
|
+
admin: {
|
|
255
|
+
readOnly: true
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
signableUrlField(mux, options, 'playbackUrl', 'Playback URL', 'video', (id)=>new URL(`https://stream.mux.com/${id}.m3u8`)),
|
|
259
|
+
signableUrlField(mux, options, 'posterUrl', 'Poster URL', 'thumbnail', (id)=>new URL(`https://image.mux.com/${id}/thumbnail.${options.posterExtension ?? 'png'}`)),
|
|
260
|
+
signableUrlField(mux, options, 'gifUrl', 'Gif URL', 'gif', (id)=>new URL(`https://image.mux.com/${id}/animated.${options.animatedGifExtension ?? 'gif'}`))
|
|
261
|
+
]
|
|
262
|
+
}
|
|
263
|
+
]
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
//# sourceMappingURL=MuxVideo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/collections/MuxVideo.ts"],"sourcesContent":["import type Mux from '@mux/mux-node'\nimport type { CollectionConfig, Field } from 'payload'\nimport { getAfterDeleteHook } from '../hooks/afterDelete'\nimport { getBeforeChangeHook } from '../hooks/beforeChange'\nimport { getBeforeValidateHook } from '../hooks/beforeValidate'\nimport { isAllowed } from '../lib/isAllowed'\nimport type { MuxVideoPluginOptions } from '../types'\n\nconst C = '@pro-laico/payload-mux/components'\n\n/** Resolve the list-view Cell component for the configured `adminThumbnail` mode. */\nconst thumbnailCell = (mode: MuxVideoPluginOptions['adminThumbnail']): string | undefined => {\n if (mode === 'image') return `${C}/MuxVideoImageCell#MuxVideoImageCell`\n if (mode === 'none') return undefined\n return `${C}/MuxVideoGifCell#MuxVideoGifCell`\n}\n\n/** Sign a playback id for a given media `type` when the policy is `signed`, and append the\n * token to `url`. A no-op for public playback. */\nconst signIfNeeded = async (\n mux: Mux,\n options: MuxVideoPluginOptions,\n url: URL,\n playbackId: string,\n policy: unknown,\n type: 'video' | 'thumbnail' | 'gif',\n posterTimestamp?: number,\n): Promise<void> => {\n if (policy !== 'signed') return\n const params = typeof posterTimestamp === 'number' ? { time: posterTimestamp.toString() } : undefined\n const token = await mux.jwt.signPlaybackId(playbackId, { expiration: options.signedUrlOptions?.expiration ?? '1d', type, params })\n url.searchParams.set('token', token)\n}\n\n/** Build one virtual, read-only URL field (`playbackUrl` / `posterUrl` / `gifUrl`). On read it\n * builds the Mux URL from the sibling `playbackId`, stamps the poster `time` for thumbnail/gif,\n * and signs it when the policy is `signed`. Returns null until a playback id exists. */\nconst signableUrlField = (\n mux: Mux,\n options: MuxVideoPluginOptions,\n name: string,\n label: string,\n type: 'video' | 'thumbnail' | 'gif',\n buildUrl: (playbackId: string) => URL,\n): Field => ({\n name,\n label,\n type: 'text',\n virtual: true,\n admin: { hidden: true },\n hooks: {\n afterRead: [\n async ({ data, siblingData }) => {\n const playbackId = siblingData?.playbackId\n if (!playbackId) return null\n const posterTimestamp = type !== 'video' && typeof data?.posterTimestamp === 'number' ? data.posterTimestamp : undefined\n const url = buildUrl(playbackId)\n if (posterTimestamp !== undefined) url.searchParams.set('time', posterTimestamp.toString())\n await signIfNeeded(mux, options, url, playbackId, siblingData.playbackPolicy, type, posterTimestamp)\n return url.toString()\n },\n ],\n },\n})\n\n/** The Videos collection: an uploader field, identifying metadata Mux fills in, and a\n * `playbackOptions` array whose virtual fields resolve to signed-or-public Mux URLs. */\nexport const MuxVideo = (mux: Mux, options: MuxVideoPluginOptions): CollectionConfig => ({\n slug: (options.extendCollection as string) ?? 'mux-video',\n labels: { singular: 'Video', plural: 'Videos' },\n // Declares this as a `@pro-laico/payload-seed` asset collection: instead of uploading bytes,\n // the seed engine hands a doc's `_file` to the `source` field below, whose beforeValidate hook\n // uploads it to Mux. Plain config — payload-mux doesn't import the seed package.\n custom: { seedAsset: { sourceField: 'source' } },\n access: { read: ({ req }) => isAllowed(options, req) },\n // Grouped with the other asset collections (images/icons/fonts) — but never re-grouping a\n // collection the plugin merely extends.\n admin: {\n ...(options.extendCollection ? {} : { group: 'Assets' }),\n useAsTitle: 'title',\n defaultColumns: ['title', 'muxUploader', 'duration'],\n },\n hooks: {\n afterDelete: [getAfterDeleteHook(mux)],\n beforeValidate: [getBeforeValidateHook(mux, options)],\n beforeChange: [getBeforeChangeHook(mux)],\n },\n fields: [\n {\n name: 'muxUploader',\n label: 'Video Preview',\n type: 'ui',\n admin: { components: { Field: `${C}/MuxUploaderField#MuxUploaderField`, Cell: thumbnailCell(options.adminThumbnail) } },\n },\n // Transient server-side ingest input (a local path / URL, or `{ file|url, playbackPolicy,\n // posterTimestamp }`). The beforeValidate hook uploads it to Mux and strips it — never\n // persisted. Lets a video be created from a file without the admin's client-side upload.\n { name: 'source', type: 'json', admin: { hidden: true, disableListColumn: true, disableBulkEdit: true } },\n {\n name: 'title',\n type: 'text',\n label: 'Title',\n required: true,\n unique: true,\n admin: { description: 'A unique title for this video that will help you identify it later.' },\n },\n // Not `required`: it's filled by the beforeValidate (server-side ingest) / beforeChange\n // (admin upload) hooks before the row is written. Marking it required would reject a\n // create that supplies a `source` instead of an already-resolved `assetId`.\n // Indexed: the webhook looks a doc up by assetId on every Mux event.\n { name: 'assetId', type: 'text', index: true, admin: { readOnly: true, condition: (data) => data.assetId } },\n // Encoding lifecycle, hook-written: 'preparing' until Mux reports the asset ready (short\n // poll or webhook), 'ready' once playable, 'errored' when Mux rejects it (see `error`).\n // Not required — docs created before this field existed have no status.\n {\n name: 'status',\n type: 'select',\n options: [\n { label: 'Preparing', value: 'preparing' },\n { label: 'Ready', value: 'ready' },\n { label: 'Errored', value: 'errored' },\n ],\n admin: { position: 'sidebar', readOnly: true, condition: (data) => data.status },\n },\n // Mux's error messages when `status` is 'errored'; surfaced by the uploader field.\n { name: 'error', type: 'text', admin: { hidden: true, readOnly: true } },\n { name: 'duration', label: 'Duration', type: 'number', admin: { readOnly: true, condition: (data) => data.duration } },\n {\n name: 'posterTimestamp',\n type: 'number',\n label: 'Poster Timestamp',\n min: 0,\n admin: {\n description: 'A timestamp (in seconds) from the video to be used as the poster image. When unset, defaults to the middle of the video.',\n condition: (data) => data.duration,\n step: 0.25,\n },\n validate: (value: number | null | undefined, { siblingData }: { siblingData: Partial<{ duration: number }> }) => {\n if (!siblingData.duration || !value) return true\n return siblingData.duration > value || 'Poster timestamp must be less than the video duration.'\n },\n },\n { name: 'aspectRatio', label: 'Aspect Ratio', type: 'text', admin: { readOnly: true, condition: (data) => data.aspectRatio } },\n { name: 'maxWidth', type: 'number', admin: { readOnly: true, condition: (data) => data.maxWidth } },\n { name: 'maxHeight', type: 'number', admin: { readOnly: true, condition: (data) => data.maxHeight } },\n {\n name: 'playbackOptions',\n type: 'array',\n admin: { readOnly: true, condition: (data) => !!data.playbackOptions },\n fields: [\n // Both required: the pair is always written together, and it tightens the generated types.\n { name: 'playbackId', label: 'Playback ID', type: 'text', required: true, admin: { readOnly: true } },\n {\n name: 'playbackPolicy',\n label: 'Playback Policy',\n type: 'select',\n required: true,\n options: [\n { label: 'signed', value: 'signed' },\n { label: 'public', value: 'public' },\n ],\n admin: { readOnly: true },\n },\n signableUrlField(mux, options, 'playbackUrl', 'Playback URL', 'video', (id) => new URL(`https://stream.mux.com/${id}.m3u8`)),\n signableUrlField(\n mux,\n options,\n 'posterUrl',\n 'Poster URL',\n 'thumbnail',\n (id) => new URL(`https://image.mux.com/${id}/thumbnail.${options.posterExtension ?? 'png'}`),\n ),\n signableUrlField(\n mux,\n options,\n 'gifUrl',\n 'Gif URL',\n 'gif',\n (id) => new URL(`https://image.mux.com/${id}/animated.${options.animatedGifExtension ?? 'gif'}`),\n ),\n ],\n },\n ],\n})\n"],"names":["getAfterDeleteHook","getBeforeChangeHook","getBeforeValidateHook","isAllowed","C","thumbnailCell","mode","undefined","signIfNeeded","mux","options","url","playbackId","policy","type","posterTimestamp","params","time","toString","token","jwt","signPlaybackId","expiration","signedUrlOptions","searchParams","set","signableUrlField","name","label","buildUrl","virtual","admin","hidden","hooks","afterRead","data","siblingData","playbackPolicy","MuxVideo","slug","extendCollection","labels","singular","plural","custom","seedAsset","sourceField","access","read","req","group","useAsTitle","defaultColumns","afterDelete","beforeValidate","beforeChange","fields","components","Field","Cell","adminThumbnail","disableListColumn","disableBulkEdit","required","unique","description","index","readOnly","condition","assetId","value","position","status","duration","min","step","validate","aspectRatio","maxWidth","maxHeight","playbackOptions","id","URL","posterExtension","animatedGifExtension"],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import './mux-uploader.css';
|
|
2
|
+
/**
|
|
3
|
+
* The admin Field for the `muxUploader` UI field. Four states: an uploader before a video
|
|
4
|
+
* exists, a "processing" notice while Mux encodes, an error notice when Mux rejected the
|
|
5
|
+
* upload (`status: 'errored'`), and the Mux player once a playback URL is available. Uploads
|
|
6
|
+
* go directly to Mux via a direct-upload URL the plugin's endpoint mints.
|
|
7
|
+
*/
|
|
8
|
+
export declare const MuxUploaderField: () => import("react").JSX.Element;
|
|
9
|
+
export default MuxUploaderField;
|
|
10
|
+
//# sourceMappingURL=MuxUploaderField.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MuxUploaderField.d.ts","sourceRoot":"","sources":["../../src/components/MuxUploaderField.tsx"],"names":[],"mappings":"AAMA,OAAO,oBAAoB,CAAA;AAK3B;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,mCAyF5B,CAAA;AAED,eAAe,gBAAgB,CAAA"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import MuxPlayer from "@mux/mux-player-react";
|
|
4
|
+
import MuxUploader from "@mux/mux-uploader-react";
|
|
5
|
+
import { toast, useConfig, useForm, useFormFields } from "@payloadcms/ui";
|
|
6
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
7
|
+
import "./mux-uploader.css";
|
|
8
|
+
/** Strip the extension from a filename (`clip.final.mp4` → `clip.final`). */ const stripExtension = (name)=>name.replace(/\.[^./\\]+$/, '');
|
|
9
|
+
/**
|
|
10
|
+
* The admin Field for the `muxUploader` UI field. Four states: an uploader before a video
|
|
11
|
+
* exists, a "processing" notice while Mux encodes, an error notice when Mux rejected the
|
|
12
|
+
* upload (`status: 'errored'`), and the Mux player once a playback URL is available. Uploads
|
|
13
|
+
* go directly to Mux via a direct-upload URL the plugin's endpoint mints.
|
|
14
|
+
*/ export const MuxUploaderField = ()=>{
|
|
15
|
+
const { config } = useConfig();
|
|
16
|
+
const apiUrl = config.routes.api;
|
|
17
|
+
const [uploadId, setUploadId] = useState('');
|
|
18
|
+
const { assetId, setAssetId, title, setTitle, setFile, playbackUrl, status, error } = useFormFields(([fields, dispatch])=>({
|
|
19
|
+
assetId: fields.assetId,
|
|
20
|
+
setAssetId: (value)=>dispatch({
|
|
21
|
+
type: 'UPDATE',
|
|
22
|
+
path: 'assetId',
|
|
23
|
+
value
|
|
24
|
+
}),
|
|
25
|
+
title: fields.title,
|
|
26
|
+
setTitle: (value)=>dispatch({
|
|
27
|
+
type: 'UPDATE',
|
|
28
|
+
path: 'title',
|
|
29
|
+
value
|
|
30
|
+
}),
|
|
31
|
+
setFile: (value)=>dispatch({
|
|
32
|
+
type: 'UPDATE',
|
|
33
|
+
path: 'file',
|
|
34
|
+
value
|
|
35
|
+
}),
|
|
36
|
+
playbackUrl: fields['playbackOptions.0.playbackUrl']?.value,
|
|
37
|
+
status: fields.status?.value,
|
|
38
|
+
error: fields.error?.value
|
|
39
|
+
}));
|
|
40
|
+
const { submit, setProcessing } = useForm();
|
|
41
|
+
const containerRef = useRef(null);
|
|
42
|
+
const getUploadUrl = useCallback(async ()=>{
|
|
43
|
+
const response = await fetch(`${apiUrl}/mux/upload`, {
|
|
44
|
+
method: 'POST'
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const body = (await response.text()).slice(0, 200);
|
|
48
|
+
toast.error(`Could not create a Mux upload (${response.status}): ${body}`);
|
|
49
|
+
throw new Error(`[payload-mux] upload URL request failed (${response.status}): ${body}`);
|
|
50
|
+
}
|
|
51
|
+
const { id, url } = await response.json();
|
|
52
|
+
setUploadId(id);
|
|
53
|
+
return url;
|
|
54
|
+
}, [
|
|
55
|
+
apiUrl
|
|
56
|
+
]);
|
|
57
|
+
const onUploadStart = useCallback(({ detail: { file } })=>{
|
|
58
|
+
const resolvedTitle = title?.value || stripExtension(file.name);
|
|
59
|
+
if (!title?.value) setTitle(resolvedTitle);
|
|
60
|
+
setFile(new File([], resolvedTitle, {
|
|
61
|
+
type: file.type,
|
|
62
|
+
lastModified: file.lastModified
|
|
63
|
+
}));
|
|
64
|
+
}, [
|
|
65
|
+
title?.value,
|
|
66
|
+
setTitle,
|
|
67
|
+
setFile
|
|
68
|
+
]);
|
|
69
|
+
const onSuccess = useCallback(async ()=>{
|
|
70
|
+
setProcessing(true);
|
|
71
|
+
const fetchUpload = async ()=>(await fetch(`${apiUrl}/mux/upload?id=${uploadId}`)).json();
|
|
72
|
+
let upload = await fetchUpload();
|
|
73
|
+
// The asset_id may lag the upload by a moment; poll until Mux assigns it — but bound it so a
|
|
74
|
+
// stuck/errored upload can't hang the form on "processing" forever.
|
|
75
|
+
const deadline = Date.now() + 60_000;
|
|
76
|
+
while(!upload.asset_id && Date.now() < deadline){
|
|
77
|
+
await new Promise((resolve)=>setTimeout(resolve, 1000));
|
|
78
|
+
upload = await fetchUpload();
|
|
79
|
+
}
|
|
80
|
+
if (!upload.asset_id) {
|
|
81
|
+
setProcessing(false);
|
|
82
|
+
toast.error('The upload did not produce a Mux asset id within 60 seconds — please try again.');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const { asset_id } = upload;
|
|
86
|
+
setAssetId(asset_id);
|
|
87
|
+
// Defer the submit so the assetId dispatch is flushed first.
|
|
88
|
+
setTimeout(()=>submit({
|
|
89
|
+
overrides: {
|
|
90
|
+
assetId: asset_id
|
|
91
|
+
}
|
|
92
|
+
}), 0);
|
|
93
|
+
}, [
|
|
94
|
+
apiUrl,
|
|
95
|
+
uploadId,
|
|
96
|
+
setAssetId,
|
|
97
|
+
setProcessing,
|
|
98
|
+
submit
|
|
99
|
+
]);
|
|
100
|
+
// Hide Payload's default `.file-field` (present only when extending an upload collection),
|
|
101
|
+
// scoped to this field's own form so it can't hide a file field elsewhere on the page.
|
|
102
|
+
useEffect(()=>{
|
|
103
|
+
const fileField = containerRef.current?.closest('form')?.querySelector('.file-field');
|
|
104
|
+
if (fileField) fileField.style.display = 'none';
|
|
105
|
+
}, []);
|
|
106
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
107
|
+
className: "mux-uploader",
|
|
108
|
+
ref: containerRef,
|
|
109
|
+
children: [
|
|
110
|
+
!assetId?.value && /*#__PURE__*/ _jsx(MuxUploader, {
|
|
111
|
+
endpoint: getUploadUrl,
|
|
112
|
+
onUploadStart: onUploadStart,
|
|
113
|
+
onSuccess: onSuccess
|
|
114
|
+
}),
|
|
115
|
+
Boolean(assetId?.value) && status === 'errored' && /*#__PURE__*/ _jsxs("div", {
|
|
116
|
+
className: "mux-uploader__error",
|
|
117
|
+
children: [
|
|
118
|
+
"Mux could not process this video",
|
|
119
|
+
error ? `: ${error}` : '',
|
|
120
|
+
". Delete this video and upload it again."
|
|
121
|
+
]
|
|
122
|
+
}),
|
|
123
|
+
Boolean(assetId?.value) && !playbackUrl && status !== 'errored' && /*#__PURE__*/ _jsxs("div", {
|
|
124
|
+
className: "mux-uploader__processing",
|
|
125
|
+
children: [
|
|
126
|
+
"Video is being encoded. This typically takes less than 90 seconds, please refresh the page in a moment. If this persists, ensure your Mux webhook points at ",
|
|
127
|
+
`${apiUrl}/mux/webhook`,
|
|
128
|
+
" (see docs)"
|
|
129
|
+
]
|
|
130
|
+
}),
|
|
131
|
+
playbackUrl && /*#__PURE__*/ _jsx(MuxPlayer, {
|
|
132
|
+
src: playbackUrl,
|
|
133
|
+
streamType: "on-demand",
|
|
134
|
+
style: {
|
|
135
|
+
height: '60vh'
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
]
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
export default MuxUploaderField;
|
|
142
|
+
|
|
143
|
+
//# sourceMappingURL=MuxUploaderField.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/MuxUploaderField.tsx"],"sourcesContent":["'use client'\n\nimport MuxPlayer from '@mux/mux-player-react'\nimport MuxUploader from '@mux/mux-uploader-react'\nimport { toast, useConfig, useForm, useFormFields } from '@payloadcms/ui'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport './mux-uploader.css'\n\n/** Strip the extension from a filename (`clip.final.mp4` → `clip.final`). */\nconst stripExtension = (name: string): string => name.replace(/\\.[^./\\\\]+$/, '')\n\n/**\n * The admin Field for the `muxUploader` UI field. Four states: an uploader before a video\n * exists, a \"processing\" notice while Mux encodes, an error notice when Mux rejected the\n * upload (`status: 'errored'`), and the Mux player once a playback URL is available. Uploads\n * go directly to Mux via a direct-upload URL the plugin's endpoint mints.\n */\nexport const MuxUploaderField = () => {\n const { config } = useConfig()\n const apiUrl = config.routes.api\n\n const [uploadId, setUploadId] = useState('')\n const { assetId, setAssetId, title, setTitle, setFile, playbackUrl, status, error } = useFormFields(([fields, dispatch]) => ({\n assetId: fields.assetId,\n setAssetId: (value: string) => dispatch({ type: 'UPDATE', path: 'assetId', value }),\n title: fields.title,\n setTitle: (value: string) => dispatch({ type: 'UPDATE', path: 'title', value }),\n setFile: (value: File) => dispatch({ type: 'UPDATE', path: 'file', value }),\n playbackUrl: fields['playbackOptions.0.playbackUrl']?.value as string | undefined,\n status: fields.status?.value as string | undefined,\n error: fields.error?.value as string | undefined,\n }))\n\n const { submit, setProcessing } = useForm()\n const containerRef = useRef<HTMLDivElement>(null)\n\n const getUploadUrl = useCallback(async () => {\n const response = await fetch(`${apiUrl}/mux/upload`, { method: 'POST' })\n if (!response.ok) {\n const body = (await response.text()).slice(0, 200)\n toast.error(`Could not create a Mux upload (${response.status}): ${body}`)\n throw new Error(`[payload-mux] upload URL request failed (${response.status}): ${body}`)\n }\n const { id, url } = (await response.json()) as { id: string; url: string }\n setUploadId(id)\n return url\n }, [apiUrl])\n\n const onUploadStart = useCallback(\n ({ detail: { file } }: { detail: { file: File } }) => {\n const resolvedTitle = (title?.value as string) || stripExtension(file.name)\n if (!title?.value) setTitle(resolvedTitle)\n setFile(new File([], resolvedTitle, { type: file.type, lastModified: file.lastModified }))\n },\n [title?.value, setTitle, setFile],\n )\n\n const onSuccess = useCallback(async () => {\n setProcessing(true)\n\n const fetchUpload = async () => (await fetch(`${apiUrl}/mux/upload?id=${uploadId}`)).json()\n let upload = await fetchUpload()\n // The asset_id may lag the upload by a moment; poll until Mux assigns it — but bound it so a\n // stuck/errored upload can't hang the form on \"processing\" forever.\n const deadline = Date.now() + 60_000\n while (!upload.asset_id && Date.now() < deadline) {\n await new Promise((resolve) => setTimeout(resolve, 1000))\n upload = await fetchUpload()\n }\n\n if (!upload.asset_id) {\n setProcessing(false)\n toast.error('The upload did not produce a Mux asset id within 60 seconds — please try again.')\n return\n }\n\n const { asset_id } = upload\n setAssetId(asset_id)\n // Defer the submit so the assetId dispatch is flushed first.\n setTimeout(() => submit({ overrides: { assetId: asset_id } }), 0)\n }, [apiUrl, uploadId, setAssetId, setProcessing, submit])\n\n // Hide Payload's default `.file-field` (present only when extending an upload collection),\n // scoped to this field's own form so it can't hide a file field elsewhere on the page.\n useEffect(() => {\n const fileField = containerRef.current?.closest('form')?.querySelector('.file-field') as HTMLElement | null\n if (fileField) fileField.style.display = 'none'\n }, [])\n\n return (\n <div className=\"mux-uploader\" ref={containerRef}>\n {!assetId?.value && <MuxUploader endpoint={getUploadUrl} onUploadStart={onUploadStart} onSuccess={onSuccess} />}\n {Boolean(assetId?.value) && status === 'errored' && (\n <div className=\"mux-uploader__error\">\n Mux could not process this video{error ? `: ${error}` : ''}. Delete this video and upload it again.\n </div>\n )}\n {Boolean(assetId?.value) && !playbackUrl && status !== 'errored' && (\n <div className=\"mux-uploader__processing\">\n Video is being encoded. This typically takes less than 90 seconds, please refresh the page in a moment. If this persists, ensure your\n Mux webhook points at {`${apiUrl}/mux/webhook`} (see docs)\n </div>\n )}\n {playbackUrl && <MuxPlayer src={playbackUrl} streamType=\"on-demand\" style={{ height: '60vh' }} />}\n </div>\n )\n}\n\nexport default MuxUploaderField\n"],"names":["MuxPlayer","MuxUploader","toast","useConfig","useForm","useFormFields","useCallback","useEffect","useRef","useState","stripExtension","name","replace","MuxUploaderField","config","apiUrl","routes","api","uploadId","setUploadId","assetId","setAssetId","title","setTitle","setFile","playbackUrl","status","error","fields","dispatch","value","type","path","submit","setProcessing","containerRef","getUploadUrl","response","fetch","method","ok","body","text","slice","Error","id","url","json","onUploadStart","detail","file","resolvedTitle","File","lastModified","onSuccess","fetchUpload","upload","deadline","Date","now","asset_id","Promise","resolve","setTimeout","overrides","fileField","current","closest","querySelector","style","display","div","className","ref","endpoint","Boolean","src","streamType","height"],"mappings":"AAAA;;AAEA,OAAOA,eAAe,wBAAuB;AAC7C,OAAOC,iBAAiB,0BAAyB;AACjD,SAASC,KAAK,EAAEC,SAAS,EAAEC,OAAO,EAAEC,aAAa,QAAQ,iBAAgB;AACzE,SAASC,WAAW,EAAEC,SAAS,EAAEC,MAAM,EAAEC,QAAQ,QAAQ,QAAO;AAChE,OAAO,qBAAoB;AAE3B,2EAA2E,GAC3E,MAAMC,iBAAiB,CAACC,OAAyBA,KAAKC,OAAO,CAAC,eAAe;AAE7E;;;;;CAKC,GACD,OAAO,MAAMC,mBAAmB;IAC9B,MAAM,EAAEC,MAAM,EAAE,GAAGX;IACnB,MAAMY,SAASD,OAAOE,MAAM,CAACC,GAAG;IAEhC,MAAM,CAACC,UAAUC,YAAY,GAAGV,SAAS;IACzC,MAAM,EAAEW,OAAO,EAAEC,UAAU,EAAEC,KAAK,EAAEC,QAAQ,EAAEC,OAAO,EAAEC,WAAW,EAAEC,MAAM,EAAEC,KAAK,EAAE,GAAGtB,cAAc,CAAC,CAACuB,QAAQC,SAAS,GAAM,CAAA;YAC3HT,SAASQ,OAAOR,OAAO;YACvBC,YAAY,CAACS,QAAkBD,SAAS;oBAAEE,MAAM;oBAAUC,MAAM;oBAAWF;gBAAM;YACjFR,OAAOM,OAAON,KAAK;YACnBC,UAAU,CAACO,QAAkBD,SAAS;oBAAEE,MAAM;oBAAUC,MAAM;oBAASF;gBAAM;YAC7EN,SAAS,CAACM,QAAgBD,SAAS;oBAAEE,MAAM;oBAAUC,MAAM;oBAAQF;gBAAM;YACzEL,aAAaG,MAAM,CAAC,gCAAgC,EAAEE;YACtDJ,QAAQE,OAAOF,MAAM,EAAEI;YACvBH,OAAOC,OAAOD,KAAK,EAAEG;QACvB,CAAA;IAEA,MAAM,EAAEG,MAAM,EAAEC,aAAa,EAAE,GAAG9B;IAClC,MAAM+B,eAAe3B,OAAuB;IAE5C,MAAM4B,eAAe9B,YAAY;QAC/B,MAAM+B,WAAW,MAAMC,MAAM,GAAGvB,OAAO,WAAW,CAAC,EAAE;YAAEwB,QAAQ;QAAO;QACtE,IAAI,CAACF,SAASG,EAAE,EAAE;YAChB,MAAMC,OAAO,AAAC,CAAA,MAAMJ,SAASK,IAAI,EAAC,EAAGC,KAAK,CAAC,GAAG;YAC9CzC,MAAMyB,KAAK,CAAC,CAAC,+BAA+B,EAAEU,SAASX,MAAM,CAAC,GAAG,EAAEe,MAAM;YACzE,MAAM,IAAIG,MAAM,CAAC,yCAAyC,EAAEP,SAASX,MAAM,CAAC,GAAG,EAAEe,MAAM;QACzF;QACA,MAAM,EAAEI,EAAE,EAAEC,GAAG,EAAE,GAAI,MAAMT,SAASU,IAAI;QACxC5B,YAAY0B;QACZ,OAAOC;IACT,GAAG;QAAC/B;KAAO;IAEX,MAAMiC,gBAAgB1C,YACpB,CAAC,EAAE2C,QAAQ,EAAEC,IAAI,EAAE,EAA8B;QAC/C,MAAMC,gBAAgB,AAAC7B,OAAOQ,SAAoBpB,eAAewC,KAAKvC,IAAI;QAC1E,IAAI,CAACW,OAAOQ,OAAOP,SAAS4B;QAC5B3B,QAAQ,IAAI4B,KAAK,EAAE,EAAED,eAAe;YAAEpB,MAAMmB,KAAKnB,IAAI;YAAEsB,cAAcH,KAAKG,YAAY;QAAC;IACzF,GACA;QAAC/B,OAAOQ;QAAOP;QAAUC;KAAQ;IAGnC,MAAM8B,YAAYhD,YAAY;QAC5B4B,cAAc;QAEd,MAAMqB,cAAc,UAAY,AAAC,CAAA,MAAMjB,MAAM,GAAGvB,OAAO,eAAe,EAAEG,UAAU,CAAA,EAAG6B,IAAI;QACzF,IAAIS,SAAS,MAAMD;QACnB,6FAA6F;QAC7F,oEAAoE;QACpE,MAAME,WAAWC,KAAKC,GAAG,KAAK;QAC9B,MAAO,CAACH,OAAOI,QAAQ,IAAIF,KAAKC,GAAG,KAAKF,SAAU;YAChD,MAAM,IAAII,QAAQ,CAACC,UAAYC,WAAWD,SAAS;YACnDN,SAAS,MAAMD;QACjB;QAEA,IAAI,CAACC,OAAOI,QAAQ,EAAE;YACpB1B,cAAc;YACdhC,MAAMyB,KAAK,CAAC;YACZ;QACF;QAEA,MAAM,EAAEiC,QAAQ,EAAE,GAAGJ;QACrBnC,WAAWuC;QACX,6DAA6D;QAC7DG,WAAW,IAAM9B,OAAO;gBAAE+B,WAAW;oBAAE5C,SAASwC;gBAAS;YAAE,IAAI;IACjE,GAAG;QAAC7C;QAAQG;QAAUG;QAAYa;QAAeD;KAAO;IAExD,2FAA2F;IAC3F,uFAAuF;IACvF1B,UAAU;QACR,MAAM0D,YAAY9B,aAAa+B,OAAO,EAAEC,QAAQ,SAASC,cAAc;QACvE,IAAIH,WAAWA,UAAUI,KAAK,CAACC,OAAO,GAAG;IAC3C,GAAG,EAAE;IAEL,qBACE,MAACC;QAAIC,WAAU;QAAeC,KAAKtC;;YAChC,CAACf,SAASU,uBAAS,KAAC7B;gBAAYyE,UAAUtC;gBAAcY,eAAeA;gBAAeM,WAAWA;;YACjGqB,QAAQvD,SAASU,UAAUJ,WAAW,2BACrC,MAAC6C;gBAAIC,WAAU;;oBAAsB;oBACF7C,QAAQ,CAAC,EAAE,EAAEA,OAAO,GAAG;oBAAG;;;YAG9DgD,QAAQvD,SAASU,UAAU,CAACL,eAAeC,WAAW,2BACrD,MAAC6C;gBAAIC,WAAU;;oBAA2B;oBAEjB,GAAGzD,OAAO,YAAY,CAAC;oBAAC;;;YAGlDU,6BAAe,KAACzB;gBAAU4E,KAAKnD;gBAAaoD,YAAW;gBAAYR,OAAO;oBAAES,QAAQ;gBAAO;;;;AAGlG,EAAC;AAED,eAAejE,iBAAgB"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DefaultCellComponentProps } from 'payload';
|
|
2
|
+
/** List-view Cell showing an animated Mux gif preview (the `adminThumbnail: 'gif'` mode). */
|
|
3
|
+
export declare const MuxVideoGifCell: ({ rowData }: DefaultCellComponentProps) => import("react").JSX.Element;
|
|
4
|
+
export default MuxVideoGifCell;
|
|
5
|
+
//# sourceMappingURL=MuxVideoGifCell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MuxVideoGifCell.d.ts","sourceRoot":"","sources":["../../src/components/MuxVideoGifCell.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAA;AAExD,6FAA6F;AAC7F,eAAO,MAAM,eAAe,GAAI,aAAa,yBAAyB,gCAMrE,CAAA;AAED,eAAe,eAAe,CAAA"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/** List-view Cell showing an animated Mux gif preview (the `adminThumbnail: 'gif'` mode). */ export const MuxVideoGifCell = ({ rowData })=>{
|
|
3
|
+
const playbackOption = rowData?.playbackOptions?.[0];
|
|
4
|
+
if (!playbackOption) return /*#__PURE__*/ _jsx(_Fragment, {
|
|
5
|
+
children: "Preview not available."
|
|
6
|
+
});
|
|
7
|
+
// biome-ignore lint/performance/noImgElement: admin list-view thumbnail (Mux image URL), not a Next page
|
|
8
|
+
return /*#__PURE__*/ _jsx("img", {
|
|
9
|
+
style: {
|
|
10
|
+
width: 80,
|
|
11
|
+
height: 80,
|
|
12
|
+
objectFit: 'cover'
|
|
13
|
+
},
|
|
14
|
+
loading: "lazy",
|
|
15
|
+
alt: rowData?.title,
|
|
16
|
+
src: playbackOption.gifUrl
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
export default MuxVideoGifCell;
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=MuxVideoGifCell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/MuxVideoGifCell.tsx"],"sourcesContent":["import type { DefaultCellComponentProps } from 'payload'\n\n/** List-view Cell showing an animated Mux gif preview (the `adminThumbnail: 'gif'` mode). */\nexport const MuxVideoGifCell = ({ rowData }: DefaultCellComponentProps) => {\n const playbackOption = rowData?.playbackOptions?.[0]\n if (!playbackOption) return <>Preview not available.</>\n\n // biome-ignore lint/performance/noImgElement: admin list-view thumbnail (Mux image URL), not a Next page\n return <img style={{ width: 80, height: 80, objectFit: 'cover' }} loading=\"lazy\" alt={rowData?.title} src={playbackOption.gifUrl} />\n}\n\nexport default MuxVideoGifCell\n"],"names":["MuxVideoGifCell","rowData","playbackOption","playbackOptions","img","style","width","height","objectFit","loading","alt","title","src","gifUrl"],"mappings":";AAEA,2FAA2F,GAC3F,OAAO,MAAMA,kBAAkB,CAAC,EAAEC,OAAO,EAA6B;IACpE,MAAMC,iBAAiBD,SAASE,iBAAiB,CAAC,EAAE;IACpD,IAAI,CAACD,gBAAgB,qBAAO;kBAAE;;IAE9B,yGAAyG;IACzG,qBAAO,KAACE;QAAIC,OAAO;YAAEC,OAAO;YAAIC,QAAQ;YAAIC,WAAW;QAAQ;QAAGC,SAAQ;QAAOC,KAAKT,SAASU;QAAOC,KAAKV,eAAeW,MAAM;;AAClI,EAAC;AAED,eAAeb,gBAAe"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DefaultCellComponentProps } from 'payload';
|
|
2
|
+
/** List-view Cell showing a static Mux poster image (the `adminThumbnail: 'image'` mode). */
|
|
3
|
+
export declare const MuxVideoImageCell: ({ rowData }: DefaultCellComponentProps) => import("react").JSX.Element;
|
|
4
|
+
export default MuxVideoImageCell;
|
|
5
|
+
//# sourceMappingURL=MuxVideoImageCell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MuxVideoImageCell.d.ts","sourceRoot":"","sources":["../../src/components/MuxVideoImageCell.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAA;AAExD,6FAA6F;AAC7F,eAAO,MAAM,iBAAiB,GAAI,aAAa,yBAAyB,gCAMvE,CAAA;AAED,eAAe,iBAAiB,CAAA"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/** List-view Cell showing a static Mux poster image (the `adminThumbnail: 'image'` mode). */ export const MuxVideoImageCell = ({ rowData })=>{
|
|
3
|
+
const playbackOption = rowData?.playbackOptions?.[0];
|
|
4
|
+
if (!playbackOption) return /*#__PURE__*/ _jsx(_Fragment, {
|
|
5
|
+
children: "Preview not available."
|
|
6
|
+
});
|
|
7
|
+
// biome-ignore lint/performance/noImgElement: admin list-view thumbnail (Mux image URL), not a Next page
|
|
8
|
+
return /*#__PURE__*/ _jsx("img", {
|
|
9
|
+
style: {
|
|
10
|
+
width: 80,
|
|
11
|
+
height: 80,
|
|
12
|
+
objectFit: 'cover'
|
|
13
|
+
},
|
|
14
|
+
loading: "lazy",
|
|
15
|
+
alt: rowData?.title,
|
|
16
|
+
src: playbackOption.posterUrl
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
export default MuxVideoImageCell;
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=MuxVideoImageCell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/components/MuxVideoImageCell.tsx"],"sourcesContent":["import type { DefaultCellComponentProps } from 'payload'\n\n/** List-view Cell showing a static Mux poster image (the `adminThumbnail: 'image'` mode). */\nexport const MuxVideoImageCell = ({ rowData }: DefaultCellComponentProps) => {\n const playbackOption = rowData?.playbackOptions?.[0]\n if (!playbackOption) return <>Preview not available.</>\n\n // biome-ignore lint/performance/noImgElement: admin list-view thumbnail (Mux image URL), not a Next page\n return <img style={{ width: 80, height: 80, objectFit: 'cover' }} loading=\"lazy\" alt={rowData?.title} src={playbackOption.posterUrl} />\n}\n\nexport default MuxVideoImageCell\n"],"names":["MuxVideoImageCell","rowData","playbackOption","playbackOptions","img","style","width","height","objectFit","loading","alt","title","src","posterUrl"],"mappings":";AAEA,2FAA2F,GAC3F,OAAO,MAAMA,oBAAoB,CAAC,EAAEC,OAAO,EAA6B;IACtE,MAAMC,iBAAiBD,SAASE,iBAAiB,CAAC,EAAE;IACpD,IAAI,CAACD,gBAAgB,qBAAO;kBAAE;;IAE9B,yGAAyG;IACzG,qBAAO,KAACE;QAAIC,OAAO;YAAEC,OAAO;YAAIC,QAAQ;YAAIC,WAAW;QAAQ;QAAGC,SAAQ;QAAOC,KAAKT,SAASU;QAAOC,KAAKV,eAAeW,SAAS;;AACrI,EAAC;AAED,eAAeb,kBAAiB"}
|