@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.
Files changed (27) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/declarations/src/getSha256.d.ts +55 -0
  3. package/dist/declarations/src/index.d.ts +20 -3
  4. package/dist/declarations/src/initSchema.d.ts +75 -55
  5. package/dist/declarations/src/initVal.d.ts +25 -2
  6. package/dist/declarations/src/remote/validationBasis.d.ts +3 -2
  7. package/dist/declarations/src/schema/file.d.ts +7 -0
  8. package/dist/declarations/src/schema/image.d.ts +9 -2
  9. package/dist/declarations/src/schema/index.d.ts +2 -1
  10. package/dist/declarations/src/schema/record.d.ts +16 -2
  11. package/dist/declarations/src/schema/richtext.d.ts +20 -1
  12. package/dist/declarations/src/schema/validation/ValidationFix.d.ts +1 -1
  13. package/dist/declarations/src/schema/video.d.ts +249 -0
  14. package/dist/declarations/src/schema/videoset.d.ts +141 -0
  15. package/dist/declarations/src/source/galleries.d.ts +20 -0
  16. package/dist/declarations/src/source/media.d.ts +172 -12
  17. package/dist/declarations/src/val/index.d.ts +4 -2
  18. package/dist/{index-7d334094.cjs.prod.js → index-13cf45b1.cjs.prod.js} +1535 -151
  19. package/dist/{index-eb9f6480.esm.js → index-a6c53286.esm.js} +1532 -152
  20. package/dist/{index-90000f7a.cjs.dev.js → index-fae161b5.cjs.dev.js} +1535 -151
  21. package/dist/valbuild-core.cjs.dev.js +5 -1
  22. package/dist/valbuild-core.cjs.prod.js +5 -1
  23. package/dist/valbuild-core.esm.js +1 -1
  24. package/package.json +1 -1
  25. package/patch/dist/valbuild-core-patch.cjs.dev.js +1 -1
  26. package/patch/dist/valbuild-core-patch.cjs.prod.js +1 -1
  27. 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
- * Fill in what a gallery-backed field does not carry itself.
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
- * `s.image(galleryModule)` stores only `{path, alt?, hotspot?}` — the
82
- * dimensions and mime type live in the gallery module, keyed by path. This is
83
- * the single implementation of that lookup; core, stega/RSC and the Studio all
84
- * call it rather than each rolling their own.
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
- * `alt` is filled in too, because a gallery holds the alt text an editor typed
87
- * once for a file used in several places — but only when the field does not
88
- * have its own, so a per-image override wins. A gallery whose `alt` schema is a
89
- * locale record holds an object rather than a string; that is left alone rather
90
- * than copied into a field typed `string`, and making the override
91
- * locale-shaped is a separate change.
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 MediaSource ? T & {
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]>;