@bardway/trpg-media 0.0.0-stage → 0.1.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.
@@ -0,0 +1,77 @@
1
+ /** Source moduleMediaUpload browser decoding, poster probing/downsampling and upload compensation. */
2
+ export interface VideoMetadata {
3
+ width: number;
4
+ height: number;
5
+ duration: number;
6
+ }
7
+ export interface VideoLimits {
8
+ mimeTypes: readonly string[];
9
+ maximumBytes: number;
10
+ maximumWidth: number;
11
+ maximumHeight: number;
12
+ maximumDurationSeconds: number;
13
+ }
14
+ export interface PosterOptions {
15
+ limits: VideoLimits;
16
+ maximumPosterBytes?: number;
17
+ eventTimeoutMs?: number;
18
+ signal?: AbortSignal;
19
+ environment?: PosterEnvironment;
20
+ }
21
+ export interface PosterCanvas {
22
+ readonly width: number;
23
+ readonly height: number;
24
+ readonly source: CanvasImageSource;
25
+ clear(): void;
26
+ drawVideo(video: HTMLVideoElement): void;
27
+ drawCanvas(canvas: PosterCanvas): void;
28
+ hasVisiblePixels(): boolean;
29
+ toWebp(): Promise<Blob>;
30
+ }
31
+ export interface PosterEnvironment {
32
+ createVideo(): HTMLVideoElement;
33
+ createCanvas(width: number, height: number): PosterCanvas;
34
+ createObjectURL(file: File): string;
35
+ revokeObjectURL(url: string): void;
36
+ }
37
+ export declare function validateVideoMetadata(file: Pick<File, 'type' | 'size'>, metadata: VideoMetadata, limits: VideoLimits): void;
38
+ export declare const browserPosterEnvironment: PosterEnvironment;
39
+ export declare const posterFilename: (filename: string) => string;
40
+ export declare const posterSeekTimes: (duration: number) => number[];
41
+ export declare function createVideoPoster(file: File, options: PosterOptions): Promise<{
42
+ poster: File;
43
+ metadata: VideoMetadata;
44
+ }>;
45
+ export declare function readWebpDimensions(file: File, signal?: AbortSignal): Promise<{
46
+ width: number;
47
+ height: number;
48
+ }>;
49
+ export interface UploadedMediaAsset {
50
+ key: string;
51
+ publicUrl: string;
52
+ }
53
+ export interface UploadedDynamicImage {
54
+ imageUrl: string;
55
+ videoUrl?: string;
56
+ width: number;
57
+ height: number;
58
+ }
59
+ export interface MediaUploadDependencies {
60
+ upload(file: File): Promise<UploadedMediaAsset>;
61
+ remove(key: string): Promise<void>;
62
+ createPoster?: (file: File, options: PosterOptions) => Promise<{
63
+ poster: File;
64
+ metadata: VideoMetadata;
65
+ }>;
66
+ readImage?: (file: File, signal?: AbortSignal) => Promise<{
67
+ width: number;
68
+ height: number;
69
+ }>;
70
+ }
71
+ export declare class MediaCompensationError extends AggregateError {
72
+ readonly uploadError: unknown;
73
+ readonly cleanupError: unknown;
74
+ constructor(uploadError: unknown, cleanupError: unknown);
75
+ }
76
+ /** Host upload/remove closures retain their original authorization, namespace and account. */
77
+ export declare function uploadDynamicImageAsset(file: File, options: PosterOptions, dependencies: MediaUploadDependencies): Promise<UploadedDynamicImage>;
package/dist/video.js ADDED
@@ -0,0 +1,259 @@
1
+ // src/video-poster.ts
2
+ var abortError = () => new DOMException("Media operation aborted", "AbortError");
3
+ var assertActive = (signal) => {
4
+ if (signal?.aborted) throw abortError();
5
+ };
6
+ function validateLimits(limits) {
7
+ if (!limits.mimeTypes.length || limits.mimeTypes.some((mime) => typeof mime !== "string" || !mime.trim()))
8
+ throw new Error("Video MIME policy required");
9
+ for (const value of [limits.maximumBytes, limits.maximumWidth, limits.maximumHeight])
10
+ if (!Number.isSafeInteger(value) || value <= 0) throw new Error("Invalid video size limit");
11
+ if (!Number.isFinite(limits.maximumDurationSeconds) || limits.maximumDurationSeconds <= 0)
12
+ throw new Error("Invalid video duration limit");
13
+ }
14
+ function validateFile(file, limits) {
15
+ validateLimits(limits);
16
+ if (!limits.mimeTypes.includes(file.type)) throw new Error("Unsupported video MIME type");
17
+ if (!Number.isSafeInteger(file.size) || file.size < 0 || file.size > limits.maximumBytes)
18
+ throw new Error("Video exceeds byte limit");
19
+ }
20
+ function validateVideoMetadata(file, metadata, limits) {
21
+ validateFile(file, limits);
22
+ if (!Number.isFinite(metadata.width) || !Number.isFinite(metadata.height) || metadata.width <= 0 || metadata.height <= 0)
23
+ throw new Error("Invalid decoded video dimensions");
24
+ if (metadata.width > limits.maximumWidth || metadata.height > limits.maximumHeight)
25
+ throw new Error("Video exceeds dimension limit");
26
+ if (!Number.isFinite(metadata.duration) || metadata.duration <= 0 || metadata.duration > limits.maximumDurationSeconds)
27
+ throw new Error("Invalid video duration or duration limit exceeded");
28
+ }
29
+ function waitForMediaEvent(target, event, timeoutMs, signal, cleanups) {
30
+ assertActive(signal);
31
+ return new Promise((resolve, reject) => {
32
+ const dispose = () => {
33
+ clearTimeout(timer);
34
+ target.removeEventListener(event, success);
35
+ target.removeEventListener("error", failure);
36
+ signal?.removeEventListener("abort", cancelled);
37
+ cleanups.delete(dispose);
38
+ };
39
+ const finish = (error) => {
40
+ dispose();
41
+ if (error) reject(error);
42
+ else resolve();
43
+ };
44
+ const success = () => finish();
45
+ const failure = () => finish(new Error("Browser cannot decode video"));
46
+ const cancelled = () => finish(abortError());
47
+ const timer = setTimeout(() => finish(new Error("Media event timed out")), timeoutMs);
48
+ cleanups.add(dispose);
49
+ target.addEventListener(event, success, { once: true });
50
+ target.addEventListener("error", failure, { once: true });
51
+ signal?.addEventListener("abort", cancelled, { once: true });
52
+ });
53
+ }
54
+ var browserPosterEnvironment = {
55
+ createVideo: () => document.createElement("video"),
56
+ createObjectURL: (file) => URL.createObjectURL(file),
57
+ revokeObjectURL: (url) => URL.revokeObjectURL(url),
58
+ createCanvas(width, height) {
59
+ const canvas = document.createElement("canvas");
60
+ canvas.width = width;
61
+ canvas.height = height;
62
+ const context = canvas.getContext("2d", { willReadFrequently: true });
63
+ if (!context) throw new Error("Browser cannot create poster canvas");
64
+ return {
65
+ get width() {
66
+ return canvas.width;
67
+ },
68
+ get height() {
69
+ return canvas.height;
70
+ },
71
+ source: canvas,
72
+ clear() {
73
+ context.clearRect(0, 0, canvas.width, canvas.height);
74
+ },
75
+ drawVideo(video) {
76
+ context.drawImage(video, 0, 0, canvas.width, canvas.height);
77
+ },
78
+ drawCanvas(source) {
79
+ context.drawImage(source.source, 0, 0, canvas.width, canvas.height);
80
+ },
81
+ hasVisiblePixels() {
82
+ const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
83
+ for (let index = 3; index < pixels.length; index += 4) if (pixels[index] !== 0) return true;
84
+ return false;
85
+ },
86
+ toWebp: () => new Promise(
87
+ (resolve, reject) => canvas.toBlob(
88
+ (blob) => blob ? resolve(blob) : reject(new Error("Cannot create WebP poster")),
89
+ "image/webp",
90
+ 0.9
91
+ )
92
+ )
93
+ };
94
+ }
95
+ };
96
+ var posterFilename = (filename) => {
97
+ const stem = (filename.split(/[\\/]/).pop() ?? "video").replace(/\.webm$/i, "").replace(/[^A-Za-z0-9_.-]/g, "_");
98
+ return `${stem || "video"}-poster.webp`;
99
+ };
100
+ var posterSeekTimes = (duration) => Array.from(new Set([0.1, 0.3, 0.5].map((time) => Math.min(time, duration / 2))));
101
+ async function createVideoPoster(file, options) {
102
+ validateFile(file, options.limits);
103
+ assertActive(options.signal);
104
+ const maximum = options.maximumPosterBytes ?? Infinity;
105
+ const timeout = options.eventTimeoutMs ?? 1e4;
106
+ if (options.maximumPosterBytes !== void 0 && (!Number.isSafeInteger(maximum) || maximum <= 0) || !Number.isFinite(timeout) || timeout <= 0)
107
+ throw new Error("Invalid poster limit or event timeout");
108
+ const environment = options.environment ?? browserPosterEnvironment;
109
+ const video = environment.createVideo();
110
+ const objectUrl = environment.createObjectURL(file);
111
+ const cleanups = /* @__PURE__ */ new Set();
112
+ video.muted = true;
113
+ video.playsInline = true;
114
+ video.preload = "auto";
115
+ try {
116
+ const ready = waitForMediaEvent(video, "loadedmetadata", timeout, options.signal, cleanups);
117
+ video.src = objectUrl;
118
+ video.load();
119
+ await ready;
120
+ assertActive(options.signal);
121
+ const metadata = {
122
+ width: video.videoWidth,
123
+ height: video.videoHeight,
124
+ duration: video.duration
125
+ };
126
+ validateVideoMetadata(file, metadata, options.limits);
127
+ if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA)
128
+ await waitForMediaEvent(video, "loadeddata", timeout, options.signal, cleanups);
129
+ const canvas = environment.createCanvas(metadata.width, metadata.height);
130
+ for (const seekTime of posterSeekTimes(metadata.duration)) {
131
+ const seeked = waitForMediaEvent(video, "seeked", timeout, options.signal, cleanups);
132
+ video.currentTime = seekTime;
133
+ await seeked;
134
+ assertActive(options.signal);
135
+ if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA)
136
+ throw new Error("Browser did not decode the selected frame");
137
+ canvas.clear();
138
+ canvas.drawVideo(video);
139
+ if (!canvas.hasVisiblePixels()) continue;
140
+ let output = canvas;
141
+ let blob = await output.toWebp();
142
+ assertActive(options.signal);
143
+ while (blob.size > maximum) {
144
+ if (Math.min(output.width, output.height) < 16)
145
+ throw new Error("Cannot meet poster byte limit");
146
+ const smaller = environment.createCanvas(
147
+ Math.max(1, Math.floor(output.width * 0.75)),
148
+ Math.max(1, Math.floor(output.height * 0.75))
149
+ );
150
+ smaller.drawCanvas(output);
151
+ output = smaller;
152
+ blob = await output.toWebp();
153
+ assertActive(options.signal);
154
+ }
155
+ return {
156
+ poster: new File([blob], posterFilename(file.name), { type: "image/webp" }),
157
+ metadata
158
+ };
159
+ }
160
+ throw new Error("Browser could not generate a visible video poster");
161
+ } finally {
162
+ for (const cleanup of [...cleanups]) cleanup();
163
+ try {
164
+ video.pause();
165
+ video.removeAttribute("src");
166
+ video.load();
167
+ } finally {
168
+ environment.revokeObjectURL(objectUrl);
169
+ }
170
+ }
171
+ }
172
+ function readWebpDimensions(file, signal) {
173
+ assertActive(signal);
174
+ if (file.type !== "image/webp") throw new Error("Image must use WebP MIME type");
175
+ return new Promise((resolve, reject) => {
176
+ const objectUrl = URL.createObjectURL(file);
177
+ const image = new Image();
178
+ const cleanup = () => {
179
+ URL.revokeObjectURL(objectUrl);
180
+ signal?.removeEventListener("abort", cancelled);
181
+ image.onload = null;
182
+ image.onerror = null;
183
+ };
184
+ const cancelled = () => {
185
+ cleanup();
186
+ reject(abortError());
187
+ };
188
+ image.onload = () => {
189
+ const size = { width: image.naturalWidth, height: image.naturalHeight };
190
+ cleanup();
191
+ if (size.width <= 0 || size.height <= 0) reject(new Error("Invalid image dimensions"));
192
+ else resolve(size);
193
+ };
194
+ image.onerror = () => {
195
+ cleanup();
196
+ reject(new Error("Cannot read WebP image dimensions"));
197
+ };
198
+ signal?.addEventListener("abort", cancelled, { once: true });
199
+ image.src = objectUrl;
200
+ });
201
+ }
202
+ var MediaCompensationError = class extends AggregateError {
203
+ constructor(uploadError, cleanupError) {
204
+ super([uploadError, cleanupError], "Video upload failed and poster cleanup failed");
205
+ this.uploadError = uploadError;
206
+ this.cleanupError = cleanupError;
207
+ this.name = "MediaCompensationError";
208
+ }
209
+ uploadError;
210
+ cleanupError;
211
+ };
212
+ async function uploadDynamicImageAsset(file, options, dependencies) {
213
+ if (typeof dependencies.upload !== "function" || typeof dependencies.remove !== "function")
214
+ throw new Error("Real media upload and cleanup drivers are required");
215
+ assertActive(options.signal);
216
+ if (file.type === "image/webp") {
217
+ const size = await (dependencies.readImage ?? readWebpDimensions)(file, options.signal);
218
+ if (!Number.isFinite(size.width) || !Number.isFinite(size.height) || size.width <= 0 || size.height <= 0)
219
+ throw new Error("Invalid image dimensions");
220
+ assertActive(options.signal);
221
+ const image2 = await dependencies.upload(file);
222
+ return { imageUrl: image2.publicUrl, ...size };
223
+ }
224
+ const { poster, metadata } = await (dependencies.createPoster ?? createVideoPoster)(
225
+ file,
226
+ options
227
+ );
228
+ validateVideoMetadata(file, metadata, options.limits);
229
+ assertActive(options.signal);
230
+ const image = await dependencies.upload(poster);
231
+ try {
232
+ assertActive(options.signal);
233
+ const video = await dependencies.upload(file);
234
+ return {
235
+ imageUrl: image.publicUrl,
236
+ videoUrl: video.publicUrl,
237
+ width: metadata.width,
238
+ height: metadata.height
239
+ };
240
+ } catch (error) {
241
+ try {
242
+ await dependencies.remove(image.key);
243
+ } catch (cleanupError) {
244
+ throw new MediaCompensationError(error, cleanupError);
245
+ }
246
+ throw error;
247
+ }
248
+ }
249
+ export {
250
+ MediaCompensationError,
251
+ browserPosterEnvironment,
252
+ createVideoPoster,
253
+ posterFilename,
254
+ posterSeekTimes,
255
+ readWebpDimensions,
256
+ uploadDynamicImageAsset,
257
+ validateVideoMetadata
258
+ };
259
+ //# sourceMappingURL=video.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/video-poster.ts"],
4
+ "sourcesContent": ["/** Source moduleMediaUpload browser decoding, poster probing/downsampling and upload compensation. */\nexport interface VideoMetadata {\n width: number;\n height: number;\n duration: number;\n}\nexport interface VideoLimits {\n mimeTypes: readonly string[];\n maximumBytes: number;\n maximumWidth: number;\n maximumHeight: number;\n maximumDurationSeconds: number;\n}\nexport interface PosterOptions {\n limits: VideoLimits;\n maximumPosterBytes?: number;\n eventTimeoutMs?: number;\n signal?: AbortSignal;\n environment?: PosterEnvironment;\n}\nexport interface PosterCanvas {\n readonly width: number;\n readonly height: number;\n readonly source: CanvasImageSource;\n clear(): void;\n drawVideo(video: HTMLVideoElement): void;\n drawCanvas(canvas: PosterCanvas): void;\n hasVisiblePixels(): boolean;\n toWebp(): Promise<Blob>;\n}\nexport interface PosterEnvironment {\n createVideo(): HTMLVideoElement;\n createCanvas(width: number, height: number): PosterCanvas;\n createObjectURL(file: File): string;\n revokeObjectURL(url: string): void;\n}\n\nconst abortError = (): DOMException => new DOMException('Media operation aborted', 'AbortError');\nconst assertActive = (signal?: AbortSignal): void => {\n if (signal?.aborted) throw abortError();\n};\nfunction validateLimits(limits: VideoLimits): void {\n if (\n !limits.mimeTypes.length ||\n limits.mimeTypes.some((mime) => typeof mime !== 'string' || !mime.trim())\n )\n throw new Error('Video MIME policy required');\n for (const value of [limits.maximumBytes, limits.maximumWidth, limits.maximumHeight])\n if (!Number.isSafeInteger(value) || value <= 0) throw new Error('Invalid video size limit');\n if (!Number.isFinite(limits.maximumDurationSeconds) || limits.maximumDurationSeconds <= 0)\n throw new Error('Invalid video duration limit');\n}\nfunction validateFile(file: Pick<File, 'type' | 'size'>, limits: VideoLimits): void {\n validateLimits(limits);\n if (!limits.mimeTypes.includes(file.type)) throw new Error('Unsupported video MIME type');\n if (!Number.isSafeInteger(file.size) || file.size < 0 || file.size > limits.maximumBytes)\n throw new Error('Video exceeds byte limit');\n}\nexport function validateVideoMetadata(\n file: Pick<File, 'type' | 'size'>,\n metadata: VideoMetadata,\n limits: VideoLimits,\n): void {\n validateFile(file, limits);\n if (\n !Number.isFinite(metadata.width) ||\n !Number.isFinite(metadata.height) ||\n metadata.width <= 0 ||\n metadata.height <= 0\n )\n throw new Error('Invalid decoded video dimensions');\n if (metadata.width > limits.maximumWidth || metadata.height > limits.maximumHeight)\n throw new Error('Video exceeds dimension limit');\n if (\n !Number.isFinite(metadata.duration) ||\n metadata.duration <= 0 ||\n metadata.duration > limits.maximumDurationSeconds\n )\n throw new Error('Invalid video duration or duration limit exceeded');\n}\nfunction waitForMediaEvent(\n target: HTMLMediaElement,\n event: 'loadedmetadata' | 'loadeddata' | 'seeked',\n timeoutMs: number,\n signal: AbortSignal | undefined,\n cleanups: Set<() => void>,\n): Promise<void> {\n assertActive(signal);\n return new Promise((resolve, reject) => {\n const dispose = (): void => {\n clearTimeout(timer);\n target.removeEventListener(event, success);\n target.removeEventListener('error', failure);\n signal?.removeEventListener('abort', cancelled);\n cleanups.delete(dispose);\n };\n const finish = (error?: Error): void => {\n dispose();\n if (error) reject(error);\n else resolve();\n };\n const success = () => finish();\n const failure = () => finish(new Error('Browser cannot decode video'));\n const cancelled = () => finish(abortError());\n const timer = setTimeout(() => finish(new Error('Media event timed out')), timeoutMs);\n cleanups.add(dispose);\n target.addEventListener(event, success, { once: true });\n target.addEventListener('error', failure, { once: true });\n signal?.addEventListener('abort', cancelled, { once: true });\n });\n}\nexport const browserPosterEnvironment: PosterEnvironment = {\n createVideo: () => document.createElement('video'),\n createObjectURL: (file) => URL.createObjectURL(file),\n revokeObjectURL: (url) => URL.revokeObjectURL(url),\n createCanvas(width, height) {\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n const context = canvas.getContext('2d', { willReadFrequently: true });\n if (!context) throw new Error('Browser cannot create poster canvas');\n return {\n get width() {\n return canvas.width;\n },\n get height() {\n return canvas.height;\n },\n source: canvas,\n clear() {\n context.clearRect(0, 0, canvas.width, canvas.height);\n },\n drawVideo(video) {\n context.drawImage(video, 0, 0, canvas.width, canvas.height);\n },\n drawCanvas(source) {\n context.drawImage(source.source, 0, 0, canvas.width, canvas.height);\n },\n hasVisiblePixels() {\n const pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;\n for (let index = 3; index < pixels.length; index += 4) if (pixels[index] !== 0) return true;\n return false;\n },\n toWebp: () =>\n new Promise((resolve, reject) =>\n canvas.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error('Cannot create WebP poster'))),\n 'image/webp',\n 0.9,\n ),\n ),\n };\n },\n};\nexport const posterFilename = (filename: string): string => {\n const stem = (filename.split(/[\\\\/]/).pop() ?? 'video')\n .replace(/\\.webm$/i, '')\n .replace(/[^A-Za-z0-9_.-]/g, '_');\n return `${stem || 'video'}-poster.webp`;\n};\nexport const posterSeekTimes = (duration: number): number[] =>\n Array.from(new Set([0.1, 0.3, 0.5].map((time) => Math.min(time, duration / 2))));\n\nexport async function createVideoPoster(\n file: File,\n options: PosterOptions,\n): Promise<{ poster: File; metadata: VideoMetadata }> {\n validateFile(file, options.limits);\n assertActive(options.signal);\n const maximum = options.maximumPosterBytes ?? Infinity;\n const timeout = options.eventTimeoutMs ?? 10_000;\n if (\n (options.maximumPosterBytes !== undefined &&\n (!Number.isSafeInteger(maximum) || maximum <= 0)) ||\n !Number.isFinite(timeout) ||\n timeout <= 0\n )\n throw new Error('Invalid poster limit or event timeout');\n const environment = options.environment ?? browserPosterEnvironment;\n const video = environment.createVideo();\n const objectUrl = environment.createObjectURL(file);\n const cleanups = new Set<() => void>();\n video.muted = true;\n video.playsInline = true;\n video.preload = 'auto';\n try {\n const ready = waitForMediaEvent(video, 'loadedmetadata', timeout, options.signal, cleanups);\n video.src = objectUrl;\n video.load();\n await ready;\n assertActive(options.signal);\n const metadata = {\n width: video.videoWidth,\n height: video.videoHeight,\n duration: video.duration,\n };\n validateVideoMetadata(file, metadata, options.limits);\n if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA)\n await waitForMediaEvent(video, 'loadeddata', timeout, options.signal, cleanups);\n const canvas = environment.createCanvas(metadata.width, metadata.height);\n for (const seekTime of posterSeekTimes(metadata.duration)) {\n const seeked = waitForMediaEvent(video, 'seeked', timeout, options.signal, cleanups);\n video.currentTime = seekTime;\n await seeked;\n assertActive(options.signal);\n if (video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA)\n throw new Error('Browser did not decode the selected frame');\n canvas.clear();\n canvas.drawVideo(video);\n if (!canvas.hasVisiblePixels()) continue;\n let output = canvas;\n let blob = await output.toWebp();\n assertActive(options.signal);\n while (blob.size > maximum) {\n if (Math.min(output.width, output.height) < 16)\n throw new Error('Cannot meet poster byte limit');\n const smaller = environment.createCanvas(\n Math.max(1, Math.floor(output.width * 0.75)),\n Math.max(1, Math.floor(output.height * 0.75)),\n );\n smaller.drawCanvas(output);\n output = smaller;\n blob = await output.toWebp();\n assertActive(options.signal);\n }\n return {\n poster: new File([blob], posterFilename(file.name), { type: 'image/webp' }),\n metadata,\n };\n }\n throw new Error('Browser could not generate a visible video poster');\n } finally {\n for (const cleanup of [...cleanups]) cleanup();\n try {\n video.pause();\n video.removeAttribute('src');\n video.load();\n } finally {\n environment.revokeObjectURL(objectUrl);\n }\n }\n}\n\nexport function readWebpDimensions(\n file: File,\n signal?: AbortSignal,\n): Promise<{ width: number; height: number }> {\n assertActive(signal);\n if (file.type !== 'image/webp') throw new Error('Image must use WebP MIME type');\n return new Promise((resolve, reject) => {\n const objectUrl = URL.createObjectURL(file);\n const image = new Image();\n const cleanup = () => {\n URL.revokeObjectURL(objectUrl);\n signal?.removeEventListener('abort', cancelled);\n image.onload = null;\n image.onerror = null;\n };\n const cancelled = () => {\n cleanup();\n reject(abortError());\n };\n image.onload = () => {\n const size = { width: image.naturalWidth, height: image.naturalHeight };\n cleanup();\n if (size.width <= 0 || size.height <= 0) reject(new Error('Invalid image dimensions'));\n else resolve(size);\n };\n image.onerror = () => {\n cleanup();\n reject(new Error('Cannot read WebP image dimensions'));\n };\n signal?.addEventListener('abort', cancelled, { once: true });\n image.src = objectUrl;\n });\n}\nexport interface UploadedMediaAsset {\n key: string;\n publicUrl: string;\n}\nexport interface UploadedDynamicImage {\n imageUrl: string;\n videoUrl?: string;\n width: number;\n height: number;\n}\nexport interface MediaUploadDependencies {\n upload(file: File): Promise<UploadedMediaAsset>;\n remove(key: string): Promise<void>;\n createPoster?: (\n file: File,\n options: PosterOptions,\n ) => Promise<{ poster: File; metadata: VideoMetadata }>;\n readImage?: (file: File, signal?: AbortSignal) => Promise<{ width: number; height: number }>;\n}\nexport class MediaCompensationError extends AggregateError {\n constructor(\n readonly uploadError: unknown,\n readonly cleanupError: unknown,\n ) {\n super([uploadError, cleanupError], 'Video upload failed and poster cleanup failed');\n this.name = 'MediaCompensationError';\n }\n}\n/** Host upload/remove closures retain their original authorization, namespace and account. */\nexport async function uploadDynamicImageAsset(\n file: File,\n options: PosterOptions,\n dependencies: MediaUploadDependencies,\n): Promise<UploadedDynamicImage> {\n if (typeof dependencies.upload !== 'function' || typeof dependencies.remove !== 'function')\n throw new Error('Real media upload and cleanup drivers are required');\n assertActive(options.signal);\n if (file.type === 'image/webp') {\n const size = await (dependencies.readImage ?? readWebpDimensions)(file, options.signal);\n if (\n !Number.isFinite(size.width) ||\n !Number.isFinite(size.height) ||\n size.width <= 0 ||\n size.height <= 0\n )\n throw new Error('Invalid image dimensions');\n assertActive(options.signal);\n const image = await dependencies.upload(file);\n return { imageUrl: image.publicUrl, ...size };\n }\n const { poster, metadata } = await (dependencies.createPoster ?? createVideoPoster)(\n file,\n options,\n );\n validateVideoMetadata(file, metadata, options.limits);\n assertActive(options.signal);\n const image = await dependencies.upload(poster);\n try {\n assertActive(options.signal);\n const video = await dependencies.upload(file);\n return {\n imageUrl: image.publicUrl,\n videoUrl: video.publicUrl,\n width: metadata.width,\n height: metadata.height,\n };\n } catch (error) {\n try {\n await dependencies.remove(image.key);\n } catch (cleanupError) {\n throw new MediaCompensationError(error, cleanupError);\n }\n throw error;\n }\n}\n"],
5
+ "mappings": ";AAqCA,IAAM,aAAa,MAAoB,IAAI,aAAa,2BAA2B,YAAY;AAC/F,IAAM,eAAe,CAAC,WAA+B;AACnD,MAAI,QAAQ,QAAS,OAAM,WAAW;AACxC;AACA,SAAS,eAAe,QAA2B;AACjD,MACE,CAAC,OAAO,UAAU,UAClB,OAAO,UAAU,KAAK,CAAC,SAAS,OAAO,SAAS,YAAY,CAAC,KAAK,KAAK,CAAC;AAExE,UAAM,IAAI,MAAM,4BAA4B;AAC9C,aAAW,SAAS,CAAC,OAAO,cAAc,OAAO,cAAc,OAAO,aAAa;AACjF,QAAI,CAAC,OAAO,cAAc,KAAK,KAAK,SAAS,EAAG,OAAM,IAAI,MAAM,0BAA0B;AAC5F,MAAI,CAAC,OAAO,SAAS,OAAO,sBAAsB,KAAK,OAAO,0BAA0B;AACtF,UAAM,IAAI,MAAM,8BAA8B;AAClD;AACA,SAAS,aAAa,MAAmC,QAA2B;AAClF,iBAAe,MAAM;AACrB,MAAI,CAAC,OAAO,UAAU,SAAS,KAAK,IAAI,EAAG,OAAM,IAAI,MAAM,6BAA6B;AACxF,MAAI,CAAC,OAAO,cAAc,KAAK,IAAI,KAAK,KAAK,OAAO,KAAK,KAAK,OAAO,OAAO;AAC1E,UAAM,IAAI,MAAM,0BAA0B;AAC9C;AACO,SAAS,sBACd,MACA,UACA,QACM;AACN,eAAa,MAAM,MAAM;AACzB,MACE,CAAC,OAAO,SAAS,SAAS,KAAK,KAC/B,CAAC,OAAO,SAAS,SAAS,MAAM,KAChC,SAAS,SAAS,KAClB,SAAS,UAAU;AAEnB,UAAM,IAAI,MAAM,kCAAkC;AACpD,MAAI,SAAS,QAAQ,OAAO,gBAAgB,SAAS,SAAS,OAAO;AACnE,UAAM,IAAI,MAAM,+BAA+B;AACjD,MACE,CAAC,OAAO,SAAS,SAAS,QAAQ,KAClC,SAAS,YAAY,KACrB,SAAS,WAAW,OAAO;AAE3B,UAAM,IAAI,MAAM,mDAAmD;AACvE;AACA,SAAS,kBACP,QACA,OACA,WACA,QACA,UACe;AACf,eAAa,MAAM;AACnB,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,UAAU,MAAY;AAC1B,mBAAa,KAAK;AAClB,aAAO,oBAAoB,OAAO,OAAO;AACzC,aAAO,oBAAoB,SAAS,OAAO;AAC3C,cAAQ,oBAAoB,SAAS,SAAS;AAC9C,eAAS,OAAO,OAAO;AAAA,IACzB;AACA,UAAM,SAAS,CAAC,UAAwB;AACtC,cAAQ;AACR,UAAI,MAAO,QAAO,KAAK;AAAA,UAClB,SAAQ;AAAA,IACf;AACA,UAAM,UAAU,MAAM,OAAO;AAC7B,UAAM,UAAU,MAAM,OAAO,IAAI,MAAM,6BAA6B,CAAC;AACrE,UAAM,YAAY,MAAM,OAAO,WAAW,CAAC;AAC3C,UAAM,QAAQ,WAAW,MAAM,OAAO,IAAI,MAAM,uBAAuB,CAAC,GAAG,SAAS;AACpF,aAAS,IAAI,OAAO;AACpB,WAAO,iBAAiB,OAAO,SAAS,EAAE,MAAM,KAAK,CAAC;AACtD,WAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AACxD,YAAQ,iBAAiB,SAAS,WAAW,EAAE,MAAM,KAAK,CAAC;AAAA,EAC7D,CAAC;AACH;AACO,IAAM,2BAA8C;AAAA,EACzD,aAAa,MAAM,SAAS,cAAc,OAAO;AAAA,EACjD,iBAAiB,CAAC,SAAS,IAAI,gBAAgB,IAAI;AAAA,EACnD,iBAAiB,CAAC,QAAQ,IAAI,gBAAgB,GAAG;AAAA,EACjD,aAAa,OAAO,QAAQ;AAC1B,UAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,WAAO,QAAQ;AACf,WAAO,SAAS;AAChB,UAAM,UAAU,OAAO,WAAW,MAAM,EAAE,oBAAoB,KAAK,CAAC;AACpE,QAAI,CAAC,QAAS,OAAM,IAAI,MAAM,qCAAqC;AACnE,WAAO;AAAA,MACL,IAAI,QAAQ;AACV,eAAO,OAAO;AAAA,MAChB;AAAA,MACA,IAAI,SAAS;AACX,eAAO,OAAO;AAAA,MAChB;AAAA,MACA,QAAQ;AAAA,MACR,QAAQ;AACN,gBAAQ,UAAU,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;AAAA,MACrD;AAAA,MACA,UAAU,OAAO;AACf,gBAAQ,UAAU,OAAO,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;AAAA,MAC5D;AAAA,MACA,WAAW,QAAQ;AACjB,gBAAQ,UAAU,OAAO,QAAQ,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;AAAA,MACpE;AAAA,MACA,mBAAmB;AACjB,cAAM,SAAS,QAAQ,aAAa,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM,EAAE;AACvE,iBAAS,QAAQ,GAAG,QAAQ,OAAO,QAAQ,SAAS,EAAG,KAAI,OAAO,KAAK,MAAM,EAAG,QAAO;AACvF,eAAO;AAAA,MACT;AAAA,MACA,QAAQ,MACN,IAAI;AAAA,QAAQ,CAAC,SAAS,WACpB,OAAO;AAAA,UACL,CAAC,SAAU,OAAO,QAAQ,IAAI,IAAI,OAAO,IAAI,MAAM,2BAA2B,CAAC;AAAA,UAC/E;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACJ;AAAA,EACF;AACF;AACO,IAAM,iBAAiB,CAAC,aAA6B;AAC1D,QAAM,QAAQ,SAAS,MAAM,OAAO,EAAE,IAAI,KAAK,SAC5C,QAAQ,YAAY,EAAE,EACtB,QAAQ,oBAAoB,GAAG;AAClC,SAAO,GAAG,QAAQ,OAAO;AAC3B;AACO,IAAM,kBAAkB,CAAC,aAC9B,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,KAAK,GAAG,EAAE,IAAI,CAAC,SAAS,KAAK,IAAI,MAAM,WAAW,CAAC,CAAC,CAAC,CAAC;AAEjF,eAAsB,kBACpB,MACA,SACoD;AACpD,eAAa,MAAM,QAAQ,MAAM;AACjC,eAAa,QAAQ,MAAM;AAC3B,QAAM,UAAU,QAAQ,sBAAsB;AAC9C,QAAM,UAAU,QAAQ,kBAAkB;AAC1C,MACG,QAAQ,uBAAuB,WAC7B,CAAC,OAAO,cAAc,OAAO,KAAK,WAAW,MAChD,CAAC,OAAO,SAAS,OAAO,KACxB,WAAW;AAEX,UAAM,IAAI,MAAM,uCAAuC;AACzD,QAAM,cAAc,QAAQ,eAAe;AAC3C,QAAM,QAAQ,YAAY,YAAY;AACtC,QAAM,YAAY,YAAY,gBAAgB,IAAI;AAClD,QAAM,WAAW,oBAAI,IAAgB;AACrC,QAAM,QAAQ;AACd,QAAM,cAAc;AACpB,QAAM,UAAU;AAChB,MAAI;AACF,UAAM,QAAQ,kBAAkB,OAAO,kBAAkB,SAAS,QAAQ,QAAQ,QAAQ;AAC1F,UAAM,MAAM;AACZ,UAAM,KAAK;AACX,UAAM;AACN,iBAAa,QAAQ,MAAM;AAC3B,UAAM,WAAW;AAAA,MACf,OAAO,MAAM;AAAA,MACb,QAAQ,MAAM;AAAA,MACd,UAAU,MAAM;AAAA,IAClB;AACA,0BAAsB,MAAM,UAAU,QAAQ,MAAM;AACpD,QAAI,MAAM,aAAa,iBAAiB;AACtC,YAAM,kBAAkB,OAAO,cAAc,SAAS,QAAQ,QAAQ,QAAQ;AAChF,UAAM,SAAS,YAAY,aAAa,SAAS,OAAO,SAAS,MAAM;AACvE,eAAW,YAAY,gBAAgB,SAAS,QAAQ,GAAG;AACzD,YAAM,SAAS,kBAAkB,OAAO,UAAU,SAAS,QAAQ,QAAQ,QAAQ;AACnF,YAAM,cAAc;AACpB,YAAM;AACN,mBAAa,QAAQ,MAAM;AAC3B,UAAI,MAAM,aAAa,iBAAiB;AACtC,cAAM,IAAI,MAAM,2CAA2C;AAC7D,aAAO,MAAM;AACb,aAAO,UAAU,KAAK;AACtB,UAAI,CAAC,OAAO,iBAAiB,EAAG;AAChC,UAAI,SAAS;AACb,UAAI,OAAO,MAAM,OAAO,OAAO;AAC/B,mBAAa,QAAQ,MAAM;AAC3B,aAAO,KAAK,OAAO,SAAS;AAC1B,YAAI,KAAK,IAAI,OAAO,OAAO,OAAO,MAAM,IAAI;AAC1C,gBAAM,IAAI,MAAM,+BAA+B;AACjD,cAAM,UAAU,YAAY;AAAA,UAC1B,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,QAAQ,IAAI,CAAC;AAAA,UAC3C,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,SAAS,IAAI,CAAC;AAAA,QAC9C;AACA,gBAAQ,WAAW,MAAM;AACzB,iBAAS;AACT,eAAO,MAAM,OAAO,OAAO;AAC3B,qBAAa,QAAQ,MAAM;AAAA,MAC7B;AACA,aAAO;AAAA,QACL,QAAQ,IAAI,KAAK,CAAC,IAAI,GAAG,eAAe,KAAK,IAAI,GAAG,EAAE,MAAM,aAAa,CAAC;AAAA,QAC1E;AAAA,MACF;AAAA,IACF;AACA,UAAM,IAAI,MAAM,mDAAmD;AAAA,EACrE,UAAE;AACA,eAAW,WAAW,CAAC,GAAG,QAAQ,EAAG,SAAQ;AAC7C,QAAI;AACF,YAAM,MAAM;AACZ,YAAM,gBAAgB,KAAK;AAC3B,YAAM,KAAK;AAAA,IACb,UAAE;AACA,kBAAY,gBAAgB,SAAS;AAAA,IACvC;AAAA,EACF;AACF;AAEO,SAAS,mBACd,MACA,QAC4C;AAC5C,eAAa,MAAM;AACnB,MAAI,KAAK,SAAS,aAAc,OAAM,IAAI,MAAM,+BAA+B;AAC/E,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,YAAY,IAAI,gBAAgB,IAAI;AAC1C,UAAM,QAAQ,IAAI,MAAM;AACxB,UAAM,UAAU,MAAM;AACpB,UAAI,gBAAgB,SAAS;AAC7B,cAAQ,oBAAoB,SAAS,SAAS;AAC9C,YAAM,SAAS;AACf,YAAM,UAAU;AAAA,IAClB;AACA,UAAM,YAAY,MAAM;AACtB,cAAQ;AACR,aAAO,WAAW,CAAC;AAAA,IACrB;AACA,UAAM,SAAS,MAAM;AACnB,YAAM,OAAO,EAAE,OAAO,MAAM,cAAc,QAAQ,MAAM,cAAc;AACtE,cAAQ;AACR,UAAI,KAAK,SAAS,KAAK,KAAK,UAAU,EAAG,QAAO,IAAI,MAAM,0BAA0B,CAAC;AAAA,UAChF,SAAQ,IAAI;AAAA,IACnB;AACA,UAAM,UAAU,MAAM;AACpB,cAAQ;AACR,aAAO,IAAI,MAAM,mCAAmC,CAAC;AAAA,IACvD;AACA,YAAQ,iBAAiB,SAAS,WAAW,EAAE,MAAM,KAAK,CAAC;AAC3D,UAAM,MAAM;AAAA,EACd,CAAC;AACH;AAoBO,IAAM,yBAAN,cAAqC,eAAe;AAAA,EACzD,YACW,aACA,cACT;AACA,UAAM,CAAC,aAAa,YAAY,GAAG,+CAA+C;AAHzE;AACA;AAGT,SAAK,OAAO;AAAA,EACd;AAAA,EALW;AAAA,EACA;AAKb;AAEA,eAAsB,wBACpB,MACA,SACA,cAC+B;AAC/B,MAAI,OAAO,aAAa,WAAW,cAAc,OAAO,aAAa,WAAW;AAC9E,UAAM,IAAI,MAAM,oDAAoD;AACtE,eAAa,QAAQ,MAAM;AAC3B,MAAI,KAAK,SAAS,cAAc;AAC9B,UAAM,OAAO,OAAO,aAAa,aAAa,oBAAoB,MAAM,QAAQ,MAAM;AACtF,QACE,CAAC,OAAO,SAAS,KAAK,KAAK,KAC3B,CAAC,OAAO,SAAS,KAAK,MAAM,KAC5B,KAAK,SAAS,KACd,KAAK,UAAU;AAEf,YAAM,IAAI,MAAM,0BAA0B;AAC5C,iBAAa,QAAQ,MAAM;AAC3B,UAAMA,SAAQ,MAAM,aAAa,OAAO,IAAI;AAC5C,WAAO,EAAE,UAAUA,OAAM,WAAW,GAAG,KAAK;AAAA,EAC9C;AACA,QAAM,EAAE,QAAQ,SAAS,IAAI,OAAO,aAAa,gBAAgB;AAAA,IAC/D;AAAA,IACA;AAAA,EACF;AACA,wBAAsB,MAAM,UAAU,QAAQ,MAAM;AACpD,eAAa,QAAQ,MAAM;AAC3B,QAAM,QAAQ,MAAM,aAAa,OAAO,MAAM;AAC9C,MAAI;AACF,iBAAa,QAAQ,MAAM;AAC3B,UAAM,QAAQ,MAAM,aAAa,OAAO,IAAI;AAC5C,WAAO;AAAA,MACL,UAAU,MAAM;AAAA,MAChB,UAAU,MAAM;AAAA,MAChB,OAAO,SAAS;AAAA,MAChB,QAAQ,SAAS;AAAA,IACnB;AAAA,EACF,SAAS,OAAO;AACd,QAAI;AACF,YAAM,aAAa,OAAO,MAAM,GAAG;AAAA,IACrC,SAAS,cAAc;AACrB,YAAM,IAAI,uBAAuB,OAAO,YAAY;AAAA,IACtD;AACA,UAAM;AAAA,EACR;AACF;",
6
+ "names": ["image"]
7
+ }
package/package.json CHANGED
@@ -1,6 +1,51 @@
1
1
  {
2
2
  "name": "@bardway/trpg-media",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Extracted reusable media runtime for TRPG hosts",
6
+ "license": "MIT",
7
+ "files": [
8
+ "dist",
9
+ "NOTICE.md",
10
+ "LICENSE",
11
+ "NOTICE"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/bardway/mingriAgency_TRPGEngine.git"
16
+ },
17
+ "publishConfig": {
18
+ "registry": "https://registry.npmjs.org/",
19
+ "access": "public"
20
+ },
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ },
26
+ "./tuning": {
27
+ "types": "./dist/tuning.d.ts",
28
+ "import": "./dist/tuning.js"
29
+ },
30
+ "./audio": {
31
+ "types": "./dist/audio.d.ts",
32
+ "import": "./dist/audio.js"
33
+ },
34
+ "./video": {
35
+ "types": "./dist/video-poster.d.ts",
36
+ "import": "./dist/video.js"
37
+ }
38
+ },
39
+ "types": "./dist/index.d.ts",
40
+ "dependencies": {
41
+ "zod": "4.3.6"
42
+ },
43
+ "trpg": {
44
+ "entries": {
45
+ "index": "src/index.ts",
46
+ "tuning": "src/tuning.ts",
47
+ "audio": "src/audio.ts",
48
+ "video": "src/video-poster.ts"
49
+ }
50
+ }
51
+ }