@valbuild/core 0.139.1 → 0.141.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/CHANGELOG.md +63 -0
- package/dist/declarations/src/getSha256.d.ts +55 -0
- package/dist/declarations/src/index.d.ts +20 -3
- package/dist/declarations/src/initSchema.d.ts +75 -55
- package/dist/declarations/src/initVal.d.ts +25 -2
- package/dist/declarations/src/remote/validationBasis.d.ts +3 -2
- package/dist/declarations/src/schema/file.d.ts +7 -0
- package/dist/declarations/src/schema/image.d.ts +9 -2
- package/dist/declarations/src/schema/index.d.ts +2 -1
- package/dist/declarations/src/schema/record.d.ts +16 -2
- package/dist/declarations/src/schema/richtext.d.ts +20 -1
- package/dist/declarations/src/schema/validation/ValidationFix.d.ts +1 -1
- package/dist/declarations/src/schema/video.d.ts +249 -0
- package/dist/declarations/src/schema/videoset.d.ts +141 -0
- package/dist/declarations/src/source/galleries.d.ts +20 -0
- package/dist/declarations/src/source/media.d.ts +172 -12
- package/dist/declarations/src/val/index.d.ts +4 -2
- package/dist/{index-7d334094.cjs.prod.js → index-13cf45b1.cjs.prod.js} +1535 -151
- package/dist/{index-eb9f6480.esm.js → index-a6c53286.esm.js} +1532 -152
- package/dist/{index-90000f7a.cjs.dev.js → index-fae161b5.cjs.dev.js} +1535 -151
- package/dist/valbuild-core.cjs.dev.js +5 -1
- package/dist/valbuild-core.cjs.prod.js +5 -1
- package/dist/valbuild-core.esm.js +1 -1
- package/package.json +1 -1
- package/patch/dist/valbuild-core-patch.cjs.dev.js +1 -1
- package/patch/dist/valbuild-core-patch.cjs.prod.js +1 -1
- package/patch/dist/valbuild-core-patch.esm.js +2 -2
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { GalleryVideoSource, VideoSource } from "../source/media.js";
|
|
2
|
+
import { CustomValidateFunction, Schema, SchemaAssertResult, SerializedSchema } from "./index.js";
|
|
3
|
+
import { SourcePath } from "../val/index.js";
|
|
4
|
+
import type { ValModule } from "../module.js";
|
|
5
|
+
import type { AltSource } from "./imageset.js";
|
|
6
|
+
import type { VideosetEntryMetadata } from "./videoset.js";
|
|
7
|
+
import { ValidationError, ValidationErrors } from "./validation/ValidationError.js";
|
|
8
|
+
import { ItemPreviewInput, PreviewItem } from "../preview.js";
|
|
9
|
+
import { FieldRender } from "../render.js";
|
|
10
|
+
/**
|
|
11
|
+
* Turn an upload into an HLS stream, in the browser, before it is uploaded.
|
|
12
|
+
*
|
|
13
|
+
* The Studio transcodes the picked file with WebCodecs into one rendition per
|
|
14
|
+
* entry of `renditions` (never upscaled: a 720p upload gets no 1080p
|
|
15
|
+
* rendition), segments each into CMAF and writes the playlists. What is stored
|
|
16
|
+
* is a directory beside the other uploads; the field's `path` is its master
|
|
17
|
+
* playlist. See `architecture/media.md`.
|
|
18
|
+
*
|
|
19
|
+
* `type` is required for the same reason it is on `ImageEncodeOptions`: a
|
|
20
|
+
* second kind of stream would then be additive.
|
|
21
|
+
*/
|
|
22
|
+
export type VideoStreamOptions = {
|
|
23
|
+
type: "hls";
|
|
24
|
+
/**
|
|
25
|
+
* The heights of the renditions, in pixels. A rendition taller than the
|
|
26
|
+
* upload is skipped; if every one is, the upload's own height is used.
|
|
27
|
+
* @default [1080, 720, 480]
|
|
28
|
+
*/
|
|
29
|
+
renditions?: number[];
|
|
30
|
+
/** Target length of each segment, in seconds. @default 6 */
|
|
31
|
+
segmentDuration?: number;
|
|
32
|
+
};
|
|
33
|
+
/** `false` (or absent) uploads the file as it was picked. */
|
|
34
|
+
export type VideoStreamOption = false | VideoStreamOptions;
|
|
35
|
+
export type VideoOptions = {
|
|
36
|
+
/** Where uploads land. @default "/public/val" */
|
|
37
|
+
dir?: string;
|
|
38
|
+
/**
|
|
39
|
+
* What may be UPLOADED, in `<input accept>` syntax. @default "video/*"
|
|
40
|
+
*
|
|
41
|
+
* Not checked against an HLS stream: the playlist is what the Studio made of
|
|
42
|
+
* an upload that was accepted, not something an editor picked.
|
|
43
|
+
*/
|
|
44
|
+
accept?: string;
|
|
45
|
+
stream?: VideoStreamOption;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* What a SET-BACKED field (`s.video(videosVal)`) may say for itself.
|
|
49
|
+
*
|
|
50
|
+
* Not `VideoOptions`: `dir` and `accept` belong to the set. `stream` is the
|
|
51
|
+
* exception, as `encode` is for an image: it is about what happens to an
|
|
52
|
+
* upload on its way in, so a field that wants the file as it was picked,
|
|
53
|
+
* where its set streams, has to be able to say `stream: false`.
|
|
54
|
+
*/
|
|
55
|
+
export type GalleryVideoOptions = {
|
|
56
|
+
stream?: VideoStreamOption;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The set a field picks from: its entries as they were when the module was
|
|
60
|
+
* evaluated (the published set — see `GalleryImageSource` for why that is
|
|
61
|
+
* enough), and whether it stores its videos on Val Remote.
|
|
62
|
+
*/
|
|
63
|
+
export type VideoGallery = {
|
|
64
|
+
modulePath: string;
|
|
65
|
+
/**
|
|
66
|
+
* `null` for a schema that was DESERIALIZED: the serialized form names the
|
|
67
|
+
* set, not its contents, so whether the set has the entry is a question
|
|
68
|
+
* for the set's own module — which the Studio validates anyway.
|
|
69
|
+
*/
|
|
70
|
+
entries: Record<string, VideosetEntryMetadata<AltSource>> | null;
|
|
71
|
+
remote: boolean;
|
|
72
|
+
};
|
|
73
|
+
export type SerializedVideoSchema = {
|
|
74
|
+
type: "video";
|
|
75
|
+
/** Static layout config, carried whole in the serialized schema — see `render.ts`. */
|
|
76
|
+
render?: FieldRender;
|
|
77
|
+
/** Set when this schema declares a `preview`. The closure itself cannot serialize. */
|
|
78
|
+
preview?: true;
|
|
79
|
+
options?: VideoOptions;
|
|
80
|
+
opt: boolean;
|
|
81
|
+
remote?: boolean;
|
|
82
|
+
customValidate?: boolean;
|
|
83
|
+
/** The `s.videoset()` module this field picks from, if it is set-backed. */
|
|
84
|
+
referencedModule?: string;
|
|
85
|
+
readonly?: boolean;
|
|
86
|
+
hidden?: boolean;
|
|
87
|
+
description?: string;
|
|
88
|
+
};
|
|
89
|
+
export type VideoMetadata = {
|
|
90
|
+
mimeType?: string;
|
|
91
|
+
width?: number;
|
|
92
|
+
height?: number;
|
|
93
|
+
duration?: number;
|
|
94
|
+
};
|
|
95
|
+
export declare const DEFAULT_VIDEO_ACCEPT = "video/*";
|
|
96
|
+
export declare const DEFAULT_VIDEO_RENDITIONS: number[];
|
|
97
|
+
export declare const DEFAULT_VIDEO_SEGMENT_DURATION = 6;
|
|
98
|
+
export declare class VideoSchema<Src extends GalleryVideoSource | null> extends Schema<Src> {
|
|
99
|
+
private readonly options?;
|
|
100
|
+
private readonly opt;
|
|
101
|
+
protected readonly isRemote: boolean;
|
|
102
|
+
private readonly customValidateFunctions;
|
|
103
|
+
private readonly isReadonly;
|
|
104
|
+
private readonly isHidden;
|
|
105
|
+
private readonly description?;
|
|
106
|
+
private readonly renderInput;
|
|
107
|
+
private readonly previewInput;
|
|
108
|
+
private readonly gallery;
|
|
109
|
+
constructor(options?: VideoOptions | undefined, opt?: boolean, isRemote?: boolean, customValidateFunctions?: CustomValidateFunction<Src>[], isReadonly?: boolean, isHidden?: boolean, description?: string | undefined, renderInput?: FieldRender | null, previewInput?: ItemPreviewInput<Src> | null, gallery?: VideoGallery | null);
|
|
110
|
+
/**
|
|
111
|
+
* Describe this field.
|
|
112
|
+
*
|
|
113
|
+
* The description is INPUT HELP: it is shown where this field's value is
|
|
114
|
+
* entered — beside its input in the Val editor, and for a record's key
|
|
115
|
+
* schema in every form that asks for a key — so it is where you say what an
|
|
116
|
+
* editor needs to know to fill it in RIGHT, which the field name cannot
|
|
117
|
+
* carry. It is not a name for the value: that is `.preview(...)`, and it is
|
|
118
|
+
* read somewhere else. The description also travels in the serialized
|
|
119
|
+
* schema, which is what the AI assistant and the MCP tools read.
|
|
120
|
+
*
|
|
121
|
+
* Pass `null` to clear a description set earlier.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* const schema = s.video().describe("Plays muted behind the headline");
|
|
125
|
+
* export default c.define("/example.val.ts", schema, {
|
|
126
|
+
* path: "/public/val/example.mp4",
|
|
127
|
+
* mimeType: "video/mp4",
|
|
128
|
+
* });
|
|
129
|
+
*/
|
|
130
|
+
describe(description: string | null): VideoSchema<Src>;
|
|
131
|
+
/**
|
|
132
|
+
* Store the video on Val's remote content host instead of in your
|
|
133
|
+
* repository.
|
|
134
|
+
*
|
|
135
|
+
* The bytes still go into the patch store when the video is uploaded — the
|
|
136
|
+
* push to the remote host happens at publish. What changes is where the
|
|
137
|
+
* published video lives: `path` (and the poster's, and each caption track's)
|
|
138
|
+
* becomes a remote URL rather than a path under `/public`, so the repository
|
|
139
|
+
* does not grow with every upload. Upload remote videos in the Studio.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* const schema = s.video().remote();
|
|
143
|
+
* export default c.define("/example.val.ts", schema, {
|
|
144
|
+
* path: "https://remote.val.build/file/p/my-project/b/01/v/1.0.0/h/8f2a1c/f/3b9d70/p/public/val/example.mp4",
|
|
145
|
+
* mimeType: "video/mp4",
|
|
146
|
+
* });
|
|
147
|
+
*/
|
|
148
|
+
remote(): VideoSchema<Src>;
|
|
149
|
+
/**
|
|
150
|
+
* Add a custom validation rule to this field.
|
|
151
|
+
*
|
|
152
|
+
* The function is called with the field's value and returns `false` when the
|
|
153
|
+
* value is fine, or a STRING with the message to show when it is not. Call it
|
|
154
|
+
* more than once to add more rules — they all run, and every message is
|
|
155
|
+
* reported.
|
|
156
|
+
*
|
|
157
|
+
* Write the check as a ternary, not as `ok || "message"`: that returns `true`
|
|
158
|
+
* when the value is fine, and `true` is not one of the two answers.
|
|
159
|
+
*
|
|
160
|
+
* Validation runs in the Studio as you type, in `npx val validate` and
|
|
161
|
+
* before a publish.
|
|
162
|
+
*
|
|
163
|
+
* The second argument carries the `path` of the field being validated, for
|
|
164
|
+
* when the message needs to say where the problem is.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* const schema = s.video().validate((val) =>
|
|
168
|
+
* (val.duration ?? 0) <= 30 ? false : "Keep it under 30 seconds",
|
|
169
|
+
* );
|
|
170
|
+
* export default c.define("/example.val.ts", schema, {
|
|
171
|
+
* path: "/public/val/example.mp4",
|
|
172
|
+
* mimeType: "video/mp4",
|
|
173
|
+
* duration: 12,
|
|
174
|
+
* });
|
|
175
|
+
*/
|
|
176
|
+
validate(validationFunction: CustomValidateFunction<Src>): VideoSchema<Src>;
|
|
177
|
+
protected executeValidate(path: SourcePath, src: Src): ValidationErrors;
|
|
178
|
+
/**
|
|
179
|
+
* A set-backed field: the file and what was read from it are the set's,
|
|
180
|
+
* so the field is checked for what is its own, and for pointing at an
|
|
181
|
+
* entry the set has.
|
|
182
|
+
*/
|
|
183
|
+
private validateFromGallery;
|
|
184
|
+
/** A video of its own: it carries the file, and what was read from it. */
|
|
185
|
+
private validateOwn;
|
|
186
|
+
protected executeAssert(path: SourcePath, src: unknown): SchemaAssertResult<Src>;
|
|
187
|
+
nullable(): VideoSchema<Src | null>;
|
|
188
|
+
readonly(isReadonly?: boolean): VideoSchema<Src>;
|
|
189
|
+
hidden(isHidden?: boolean): VideoSchema<Src>;
|
|
190
|
+
protected executeCustomValidateAt(path: SourcePath, src: Src): ValidationError[];
|
|
191
|
+
/**
|
|
192
|
+
* How this field is laid out in the editor when it is the item of an array
|
|
193
|
+
* or record: `{ as: "inline" }` renders the field itself inside each row,
|
|
194
|
+
* instead of a preview row that navigates to it.
|
|
195
|
+
*
|
|
196
|
+
* Static configuration, not a callback — see `render.ts`.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* const schema = s.array(s.video().render({ as: "inline" }));
|
|
200
|
+
* export default c.define("/example.val.ts", schema, [
|
|
201
|
+
* { path: "/public/val/example.mp4", mimeType: "video/mp4" },
|
|
202
|
+
* ]);
|
|
203
|
+
*/
|
|
204
|
+
render(input: FieldRender): VideoSchema<Src>;
|
|
205
|
+
/**
|
|
206
|
+
* How this VALUE is shown where a preview of it is needed — a row in a
|
|
207
|
+
* sortable list, a reference dropdown, a search hit. Never how the field
|
|
208
|
+
* itself is edited (that is `render`). See `preview.ts`.
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* const schema = s.array(
|
|
212
|
+
* s.video().preview(({ val }) => ({
|
|
213
|
+
* title: val.alt ?? val.path,
|
|
214
|
+
* image: val.poster,
|
|
215
|
+
* })),
|
|
216
|
+
* );
|
|
217
|
+
* export default c.define("/example.val.ts", schema, [
|
|
218
|
+
* { path: "/public/val/example.mp4", mimeType: "video/mp4" },
|
|
219
|
+
* ]);
|
|
220
|
+
*/
|
|
221
|
+
preview(select: ItemPreviewInput<Src>): VideoSchema<Src>;
|
|
222
|
+
protected executePreviewItem(src: NonNullable<Src>): PreviewItem | null;
|
|
223
|
+
protected declaresItemPreview(): boolean;
|
|
224
|
+
protected executeSerialize(): SerializedSchema;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Every file a video value names: the video, its poster and each caption
|
|
228
|
+
* track. An HLS stream's segments are not here — they are named by its
|
|
229
|
+
* playlists, not by the value.
|
|
230
|
+
*/
|
|
231
|
+
export declare function filesOfVideoSource(src: GalleryVideoSource): string[];
|
|
232
|
+
/**
|
|
233
|
+
* A video picked from a set (`s.videoset()`). What is true of the file —
|
|
234
|
+
* its type, size and length — lives in the set; the field carries what one
|
|
235
|
+
* page chose about it.
|
|
236
|
+
*/
|
|
237
|
+
export declare function video(videosetModule: ValModule<Record<string, VideosetEntryMetadata<AltSource>>>, galleryOptions?: GalleryVideoOptions): VideoSchema<GalleryVideoSource>;
|
|
238
|
+
/**
|
|
239
|
+
* Define a video: an `.mp4` / `.webm` file, or an HLS stream the Studio makes
|
|
240
|
+
* from an upload when `stream` is set.
|
|
241
|
+
*/
|
|
242
|
+
export declare function video(options?: VideoOptions): VideoSchema<VideoSource>;
|
|
243
|
+
/**
|
|
244
|
+
* `s.video()` for a project whose media is all remote (`files.remote` in
|
|
245
|
+
* `val.config.ts`): the same schema, with `.remote()` already applied. See
|
|
246
|
+
* `initSchema`.
|
|
247
|
+
*/
|
|
248
|
+
export declare function remoteVideo(videosetModule: ValModule<Record<string, VideosetEntryMetadata<AltSource>>>, galleryOptions?: GalleryVideoOptions): VideoSchema<GalleryVideoSource>;
|
|
249
|
+
export declare function remoteVideo(options?: VideoOptions): VideoSchema<VideoSource>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Schema } from "./index.js";
|
|
2
|
+
import type { SerializedRecordSchema } from "./record.js";
|
|
3
|
+
import { RecordSchema } from "./record.js";
|
|
4
|
+
import { ObjectSchema } from "./object.js";
|
|
5
|
+
import { StringSchema } from "./string.js";
|
|
6
|
+
import { NumberSchema } from "./number.js";
|
|
7
|
+
import type { AltSchema, AltSource, AltSourceOf } from "./imageset.js";
|
|
8
|
+
import type { SerializedVideoSchema, VideoStreamOption } from "./video.js";
|
|
9
|
+
import type { MediaHotspot, VideoCaptionSource, VideoPosterSource } from "../source/media.js";
|
|
10
|
+
/**
|
|
11
|
+
* Options for s.videoset()
|
|
12
|
+
*/
|
|
13
|
+
export type VideosetOptions<Accept extends `video/${string}`, Alt extends AltSchema = StringSchema<string | null>> = VideosetOptionsBase<Accept> & VideosetAltOption<Alt>;
|
|
14
|
+
/** See `ImagesetAltOption`: `alt` is optional only when it is the default. */
|
|
15
|
+
type VideosetAltOption<Alt extends AltSchema> = [
|
|
16
|
+
StringSchema<string | null>
|
|
17
|
+
] extends [Alt] ? {
|
|
18
|
+
/**
|
|
19
|
+
* Description schema. Can be:
|
|
20
|
+
* - s.string() for a required description
|
|
21
|
+
* - s.string().nullable() for an optional one (default)
|
|
22
|
+
* - s.record(s.string(), s.string()) for one per locale
|
|
23
|
+
*/
|
|
24
|
+
alt?: Alt;
|
|
25
|
+
} : {
|
|
26
|
+
/** Description schema — required, because it is not the default one. */
|
|
27
|
+
alt: Alt;
|
|
28
|
+
};
|
|
29
|
+
type VideosetOptionsBase<Accept extends `video/${string}`> = {
|
|
30
|
+
/**
|
|
31
|
+
* What may be UPLOADED, in `<input accept>` syntax. Must be a video type.
|
|
32
|
+
* Not checked against an HLS stream: see `VideoOptions.accept`.
|
|
33
|
+
* @default "video/*"
|
|
34
|
+
*/
|
|
35
|
+
accept?: Accept;
|
|
36
|
+
/**
|
|
37
|
+
* The directory where videos are stored. Must start with "/public".
|
|
38
|
+
*
|
|
39
|
+
* Required for the same reason it is on `s.imageset()`: it decides where
|
|
40
|
+
* every upload in the set lands, and a default is a directory shared with
|
|
41
|
+
* every other set that did not say.
|
|
42
|
+
*/
|
|
43
|
+
dir: "/public" | `/public/${string}`;
|
|
44
|
+
/**
|
|
45
|
+
* Turn every upload into an HLS stream, in the browser. See
|
|
46
|
+
* `VideoStreamOptions`. A field backed by this set (`s.video(videosVal)`)
|
|
47
|
+
* inherits it, as it inherits `accept` and `dir`.
|
|
48
|
+
*/
|
|
49
|
+
stream?: VideoStreamOption;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* One video of a set, keyed by its file path (an HLS stream by its master
|
|
53
|
+
* playlist).
|
|
54
|
+
*
|
|
55
|
+
* Two kinds of thing are here. What is TRUE OF THE FILE — what it is, how
|
|
56
|
+
* big, how long — is read from the bytes and is the set's alone. The rest —
|
|
57
|
+
* the description, focal point, poster, start and end, and captions — is what
|
|
58
|
+
* a page chooses about a video, and an entry holds it as the DEFAULT: every
|
|
59
|
+
* field picked from the set (`s.video(videosVal)`) starts from it and may
|
|
60
|
+
* override it key by key, so a video described and trimmed once is described
|
|
61
|
+
* and trimmed everywhere it is used. `fillFromGallery` is the one merge.
|
|
62
|
+
*
|
|
63
|
+
* The poster is also the set's thumbnail of the entry: there is no separate
|
|
64
|
+
* still for the gallery, because two stills of one video would disagree.
|
|
65
|
+
*/
|
|
66
|
+
export type VideosetEntryMetadata<Alt extends AltSource = string | null> = {
|
|
67
|
+
mimeType: string;
|
|
68
|
+
width: number;
|
|
69
|
+
height: number;
|
|
70
|
+
/** In seconds. */
|
|
71
|
+
duration: number;
|
|
72
|
+
alt: Alt;
|
|
73
|
+
hotspot?: MediaHotspot;
|
|
74
|
+
/** Seconds. The frame the poster was taken from. */
|
|
75
|
+
posterTime?: number;
|
|
76
|
+
poster?: VideoPosterSource;
|
|
77
|
+
/** Seconds. Where playback starts. */
|
|
78
|
+
startTime?: number;
|
|
79
|
+
/** Seconds. Where playback stops. */
|
|
80
|
+
endTime?: number;
|
|
81
|
+
/** Overridden as a whole: a field with its own tracks has none of these. */
|
|
82
|
+
captions?: readonly VideoCaptionSource[];
|
|
83
|
+
};
|
|
84
|
+
export type SerializedVideosetSchema = SerializedRecordSchema;
|
|
85
|
+
type VideosetItemProps<Alt extends AltSchema> = {
|
|
86
|
+
mimeType: StringSchema<string>;
|
|
87
|
+
width: NumberSchema<number>;
|
|
88
|
+
height: NumberSchema<number>;
|
|
89
|
+
duration: NumberSchema<number>;
|
|
90
|
+
alt: Alt;
|
|
91
|
+
};
|
|
92
|
+
type VideosetItemSrc<Alt extends AltSchema> = {
|
|
93
|
+
mimeType: string;
|
|
94
|
+
width: number;
|
|
95
|
+
height: number;
|
|
96
|
+
duration: number;
|
|
97
|
+
alt: AltSourceOf<Alt>;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Define a collection of videos — the video twin of `s.imageset()`.
|
|
101
|
+
*
|
|
102
|
+
* A field picks from it with `s.video(videosVal)`. An entry may hold the
|
|
103
|
+
* description, poster, start and end, focal point and captions as defaults,
|
|
104
|
+
* and a field overrides any of them by setting its own. Uploading in such a
|
|
105
|
+
* field adds to the set. Call `.remote()` on the result
|
|
106
|
+
* to store the set on Val Remote.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```typescript
|
|
110
|
+
* const schema = s.videoset({
|
|
111
|
+
* dir: "/public/val/videos",
|
|
112
|
+
* stream: { type: "hls" },
|
|
113
|
+
* });
|
|
114
|
+
* export default c.define("/content/videos.val.ts", schema, {
|
|
115
|
+
* "/public/val/videos/intro_51df2.mp4": {
|
|
116
|
+
* mimeType: "video/mp4",
|
|
117
|
+
* width: 1280,
|
|
118
|
+
* height: 720,
|
|
119
|
+
* duration: 12.5,
|
|
120
|
+
* alt: "The team, introducing itself",
|
|
121
|
+
* startTime: 1.5,
|
|
122
|
+
* },
|
|
123
|
+
* });
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export declare const videoset: <Accept extends `video/${string}`, Alt extends AltSchema = StringSchema<string | null>>(options: VideosetOptions<Accept, Alt>) => RecordSchema<ObjectSchema<VideosetItemProps<Alt>, VideosetItemSrc<Alt>>, Schema<string>, Record<string, VideosetEntryMetadata<AltSourceOf<Alt>>>>;
|
|
127
|
+
/**
|
|
128
|
+
* The video schema a set's entry is hashed against, for a remote ref.
|
|
129
|
+
*
|
|
130
|
+
* The set's item schema is an object, but a remote ref's validation hash is
|
|
131
|
+
* computed over a MEDIA schema, so one is synthesized from the set — the one
|
|
132
|
+
* definition the CLI's upload (`videos:upload-remote`) and the Studio's (the
|
|
133
|
+
* set's gallery) both use, because two copies are two hashes for the same
|
|
134
|
+
* file. `stream` is left out: it says what is done to an upload on its way
|
|
135
|
+
* in, not whether the bytes that arrived are valid.
|
|
136
|
+
*/
|
|
137
|
+
export declare function videosetEntryVideoSchema(set: {
|
|
138
|
+
accept?: string;
|
|
139
|
+
dir?: string;
|
|
140
|
+
}): SerializedVideoSchema;
|
|
141
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Schema } from "../schema/index.js";
|
|
2
|
+
import type { SelectorSource } from "../selector/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* The PUBLISHED entries of every gallery a module's fields pick from — an
|
|
5
|
+
* `s.image(galleryVal)`, `s.file(filesVal)` or `s.video(videosVal)` — by the
|
|
6
|
+
* gallery's module path.
|
|
7
|
+
*
|
|
8
|
+
* A gallery-backed value names its file and nothing else that is the
|
|
9
|
+
* gallery's, so a reader has to look the entry up (`fillFromGallery`). In
|
|
10
|
+
* draft mode the reader has every module's draft source to look in; outside
|
|
11
|
+
* it, nothing — and the page got a set-backed video with no type, size,
|
|
12
|
+
* poster or description, and an image with no dimensions. But the field's
|
|
13
|
+
* schema was built from the gallery module (`s.video(videosVal)` reads it at
|
|
14
|
+
* definition), so the published entries are already here, on the schema
|
|
15
|
+
* instance. This is where they are read from.
|
|
16
|
+
*
|
|
17
|
+
* Stops at a view: the module it points at is read on its own, with its own
|
|
18
|
+
* galleries.
|
|
19
|
+
*/
|
|
20
|
+
export declare function galleriesOf(schema: Schema<SelectorSource> | undefined): Map<string, unknown>;
|
|
@@ -42,6 +42,119 @@ export type GalleryFileSource = {
|
|
|
42
42
|
export type FileSource = GalleryFileSource & {
|
|
43
43
|
readonly mimeType?: string;
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* The still shown before a video plays — `<video poster>`.
|
|
47
|
+
*
|
|
48
|
+
* Derived, like a video's `width` and `height`: the Studio grabs the frame at
|
|
49
|
+
* {@link VideoSource.posterTime} and uploads it as an image, so a page can show
|
|
50
|
+
* it without decoding the video. It is a media object of its own (a `path`,
|
|
51
|
+
* and a `patch_id` while unpublished) because it is a file of its own.
|
|
52
|
+
*/
|
|
53
|
+
export type VideoPosterSource = {
|
|
54
|
+
readonly path: string;
|
|
55
|
+
readonly width?: number;
|
|
56
|
+
readonly height?: number;
|
|
57
|
+
readonly mimeType?: string;
|
|
58
|
+
readonly patch_id?: string;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* One text track of a video — `<track src srclang label kind default>`.
|
|
62
|
+
*
|
|
63
|
+
* `path` is a WebVTT file, uploaded like any other file.
|
|
64
|
+
*/
|
|
65
|
+
export type VideoCaptionSource = {
|
|
66
|
+
readonly path: string;
|
|
67
|
+
/** BCP 47 language tag of the track, e.g. `en` or `nb-NO`. */
|
|
68
|
+
readonly srclang: string;
|
|
69
|
+
/** What a viewer picks from the player's caption menu, e.g. "English". */
|
|
70
|
+
readonly label?: string;
|
|
71
|
+
/**
|
|
72
|
+
* `subtitles` translate the dialogue; `captions` also describe the sound,
|
|
73
|
+
* for viewers who cannot hear it. @default "subtitles"
|
|
74
|
+
*/
|
|
75
|
+
readonly kind?: "subtitles" | "captions";
|
|
76
|
+
/** Shown without the viewer turning it on. At most one track may say so. */
|
|
77
|
+
readonly default?: boolean;
|
|
78
|
+
readonly patch_id?: string;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* What a video FIELD authors, on top of the file it names: the description,
|
|
82
|
+
* the crop, the poster, the trim and the caption tracks.
|
|
83
|
+
*
|
|
84
|
+
* This is the whole value of a field picked from an `s.videoset()`
|
|
85
|
+
* (`s.video(videosetVal)`) — the mime type, size and length live in the set,
|
|
86
|
+
* keyed by `path`, and repeating them here is how two copies of one fact get
|
|
87
|
+
* to disagree. The poster, captions and times stay with the FIELD: one video
|
|
88
|
+
* used in two places can be trimmed and captioned differently in each.
|
|
89
|
+
*
|
|
90
|
+
* Times are in seconds from the start of the file.
|
|
91
|
+
*/
|
|
92
|
+
export type GalleryVideoSource = {
|
|
93
|
+
readonly path: string;
|
|
94
|
+
/** Describes the video for someone who cannot see it. */
|
|
95
|
+
readonly alt?: string;
|
|
96
|
+
/** What must stay in frame when a page crops the video (`object-position`). */
|
|
97
|
+
readonly hotspot?: MediaHotspot;
|
|
98
|
+
/** Seconds. The frame the poster was taken from. */
|
|
99
|
+
readonly posterTime?: number;
|
|
100
|
+
readonly poster?: VideoPosterSource;
|
|
101
|
+
/** Seconds. Where playback starts. */
|
|
102
|
+
readonly startTime?: number;
|
|
103
|
+
/** Seconds. Where playback stops. */
|
|
104
|
+
readonly endTime?: number;
|
|
105
|
+
readonly captions?: readonly VideoCaptionSource[];
|
|
106
|
+
/**
|
|
107
|
+
* Set on a source whose bytes are not committed yet. Injected server-side and
|
|
108
|
+
* consumed only by {@link mediaUrl} — never written to a `.val.ts`.
|
|
109
|
+
*/
|
|
110
|
+
readonly patch_id?: string;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* What one page chooses about a video — as opposed to what is true of the
|
|
114
|
+
* file. A field (`s.video()`) carries these for itself; the entry of a set
|
|
115
|
+
* (`s.videoset()`) carries them as DEFAULTS, which every field picked from the
|
|
116
|
+
* set starts from and may override key by key. See {@link fillFromGallery}.
|
|
117
|
+
*/
|
|
118
|
+
export type VideoDefaults = Pick<GalleryVideoSource, "hotspot" | "posterTime" | "poster" | "startTime" | "endTime" | "captions">;
|
|
119
|
+
/**
|
|
120
|
+
* A video: a progressive file (`video/mp4`, `video/webm`) or an HLS stream
|
|
121
|
+
* (`application/vnd.apple.mpegurl`, whose `path` is the master playlist).
|
|
122
|
+
*
|
|
123
|
+
* `mimeType` is REQUIRED on a video of its own: a page has to know which of
|
|
124
|
+
* the two it is holding before it can play it at all — an `.m3u8` handed to a
|
|
125
|
+
* `<video src>` plays only in Safari. (What tells a video's TYPE apart from an
|
|
126
|
+
* image's is its declared keys, see {@link IsVideoSource}, because a field
|
|
127
|
+
* picked from a set has no `mimeType` of its own.)
|
|
128
|
+
*
|
|
129
|
+
* `width`, `height`, `duration` and `mimeType` are read from the bytes;
|
|
130
|
+
* everything else is authored.
|
|
131
|
+
*/
|
|
132
|
+
export type VideoSource = GalleryVideoSource & {
|
|
133
|
+
readonly mimeType: string;
|
|
134
|
+
readonly width?: number;
|
|
135
|
+
readonly height?: number;
|
|
136
|
+
/** Seconds. */
|
|
137
|
+
readonly duration?: number;
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Whether a source TYPE is a video's.
|
|
141
|
+
*
|
|
142
|
+
* Every media source is "a `path` plus optional fields", so assignability
|
|
143
|
+
* cannot tell an image from a video: an image is assignable to a set-backed
|
|
144
|
+
* video and the other way round. The keys a type DECLARES can — only a video
|
|
145
|
+
* declares `posterTime` — so the readers' conditional types ask this before
|
|
146
|
+
* they ask about images.
|
|
147
|
+
*/
|
|
148
|
+
export type IsVideoSource<T> = T extends {
|
|
149
|
+
readonly path: string;
|
|
150
|
+
} ? "posterTime" extends keyof T ? true : false : false;
|
|
151
|
+
/** The mime type of an HLS master playlist. */
|
|
152
|
+
export declare const HLS_MIME_TYPE = "application/vnd.apple.mpegurl";
|
|
153
|
+
/** Whether a video is an HLS stream rather than a single progressive file. */
|
|
154
|
+
export declare function isHlsVideo(src: {
|
|
155
|
+
readonly path: string;
|
|
156
|
+
readonly mimeType?: string;
|
|
157
|
+
}): boolean;
|
|
45
158
|
/**
|
|
46
159
|
* The structural supertype of every media source.
|
|
47
160
|
*
|
|
@@ -50,7 +163,7 @@ export type FileSource = GalleryFileSource & {
|
|
|
50
163
|
* `Source` excludes `undefined` — an object with optional properties does not
|
|
51
164
|
* satisfy it.
|
|
52
165
|
*/
|
|
53
|
-
export type MediaSource = ImageSource;
|
|
166
|
+
export type MediaSource = ImageSource | VideoSource;
|
|
54
167
|
/** A path is remote unless it is under `/public`. */
|
|
55
168
|
export declare function isRemoteMediaPath(path: string): boolean;
|
|
56
169
|
/**
|
|
@@ -76,23 +189,70 @@ export declare function resolveMedia<S extends {
|
|
|
76
189
|
url: string;
|
|
77
190
|
};
|
|
78
191
|
/**
|
|
79
|
-
*
|
|
192
|
+
* A video as a reader sees it: every file it names — the video itself, its
|
|
193
|
+
* poster and each caption track — carries the URL its bytes are served from.
|
|
80
194
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
|
|
195
|
+
* Each gets its own {@link mediaUrl} because each is its own file with its own
|
|
196
|
+
* `patch_id`: replacing only the poster drafts the poster, and the video keeps
|
|
197
|
+
* its published URL.
|
|
198
|
+
*/
|
|
199
|
+
export type ResolvedVideo<S extends GalleryVideoSource = VideoSource> = Omit<S, "poster" | "captions"> & {
|
|
200
|
+
readonly url: string;
|
|
201
|
+
readonly poster?: VideoPosterSource & {
|
|
202
|
+
readonly url: string;
|
|
203
|
+
};
|
|
204
|
+
readonly captions?: readonly (VideoCaptionSource & {
|
|
205
|
+
readonly url: string;
|
|
206
|
+
})[];
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* The single implementation of {@link ResolvedVideo}. `url` is passed in as a
|
|
210
|
+
* function so the reader can tag the video's own URL (stega) without tagging
|
|
211
|
+
* the poster's and the captions', which are not where an edit lands.
|
|
212
|
+
*/
|
|
213
|
+
export declare function resolveVideo<S extends GalleryVideoSource>(src: S, videoUrl?: (src: S) => string): ResolvedVideo<S>;
|
|
214
|
+
/**
|
|
215
|
+
* What a gallery entry holds as a DEFAULT for the fields picked from it, in
|
|
216
|
+
* the groups a field overrides together.
|
|
85
217
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
|
|
91
|
-
|
|
218
|
+
* Per key, not per value: a field that has set its own `startTime` still
|
|
219
|
+
* takes the gallery's `endTime`. The poster is the one pair — `poster` is the
|
|
220
|
+
* frame at `posterTime`, so a field with either has chosen its own still, and
|
|
221
|
+
* taking the other half from the gallery would describe a frame nobody chose.
|
|
222
|
+
*/
|
|
223
|
+
export declare const GALLERY_DEFAULT_GROUPS: readonly (readonly string[])[];
|
|
224
|
+
/**
|
|
225
|
+
* The gallery entry a gallery-backed value names, or `null` when it is not
|
|
226
|
+
* gallery-backed or the gallery has no such entry.
|
|
92
227
|
*
|
|
93
228
|
* The double lookup is load-bearing: a remote gallery keys its entries by the
|
|
94
229
|
* remote URL while the file itself stays on disk under its local path.
|
|
95
230
|
*/
|
|
231
|
+
export declare function galleryEntryOf(src: {
|
|
232
|
+
readonly path: string;
|
|
233
|
+
}, schema: unknown, getModuleSource: (modulePath: string) => unknown): Record<string, unknown> | null;
|
|
234
|
+
/**
|
|
235
|
+
* The value a gallery-backed field means: its own keys, and the gallery's
|
|
236
|
+
* where it has none.
|
|
237
|
+
*
|
|
238
|
+
* `s.image(galleryModule)` and `s.video(videosModule)` store only what one
|
|
239
|
+
* page chose — the rest lives in the gallery, keyed by path. Two things come
|
|
240
|
+
* from there:
|
|
241
|
+
*
|
|
242
|
+
* - **What is true of the file** — dimensions, mime type, length. The field
|
|
243
|
+
* never has these, so they are always the gallery's.
|
|
244
|
+
* - **The defaults** — description, focal point, a video's poster, start, end
|
|
245
|
+
* and captions ({@link GALLERY_DEFAULT_GROUPS}). An editor sets them once
|
|
246
|
+
* on the gallery entry; a field that sets its own wins, KEY BY KEY.
|
|
247
|
+
*
|
|
248
|
+
* The single implementation of that merge: core, stega/RSC and the Studio all
|
|
249
|
+
* call it rather than each rolling their own, so a page and the Studio's
|
|
250
|
+
* preview of it cannot disagree about which poster a video has.
|
|
251
|
+
*
|
|
252
|
+
* A gallery whose `alt` schema is a locale record holds an object rather than
|
|
253
|
+
* a string; that is left alone rather than copied into a field typed
|
|
254
|
+
* `string`, and making the override locale-shaped is a separate change.
|
|
255
|
+
*/
|
|
96
256
|
export declare function fillFromGallery<S extends {
|
|
97
257
|
readonly path: string;
|
|
98
258
|
}>(src: S, schema: unknown, getModuleSource: (modulePath: string) => unknown): S;
|
|
@@ -4,13 +4,15 @@ import { Val as ArrayVal } from "./array.js";
|
|
|
4
4
|
import { Val as PrimitiveVal } from "./primitive.js";
|
|
5
5
|
import { Json, JsonArray, JsonObject, JsonPrimitive } from "../Json.js";
|
|
6
6
|
import { Path, Selector } from "../selector/index.js";
|
|
7
|
-
import { MediaSource } from "../source/media.js";
|
|
7
|
+
import { GalleryVideoSource, IsVideoSource, MediaSource, ResolvedVideo } from "../source/media.js";
|
|
8
8
|
export type SerializedVal = {
|
|
9
9
|
val: SerializedVal | Json;
|
|
10
10
|
valPath: SourcePath | undefined;
|
|
11
11
|
};
|
|
12
12
|
export declare function isSerializedVal(val: unknown): val is SerializedVal;
|
|
13
|
-
export type JsonOfSource<T extends Source> = Json extends T ? Json : T extends
|
|
13
|
+
export type JsonOfSource<T extends Source> = Json extends T ? Json : T extends GalleryVideoSource ? IsVideoSource<T> extends true ? ResolvedVideo<T> : T & {
|
|
14
|
+
url: string;
|
|
15
|
+
} : T extends MediaSource ? T & {
|
|
14
16
|
url: string;
|
|
15
17
|
} : T extends SourceObject ? {
|
|
16
18
|
[key in keyof T]: JsonOfSource<T[key]>;
|