@neta-art/cohub 8.22.0 → 8.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/board/headless/index.d.ts +1 -0
- package/dist/board/headless/index.js +17 -1
- package/dist/board/task.d.ts +2 -3
- package/dist/board/task.js +25 -95
- package/dist/chunks/environment.d.ts +23 -1
- package/dist/chunks/http.d.ts +83 -3
- package/dist/chunks/http.js +28 -7
- package/dist/chunks/websocket.d.ts +1 -1
- package/dist/generation-blocks.js +125 -0
- package/dist/http.d.ts +3 -3
- package/dist/index.d.ts +123 -3
- package/dist/index.js +675 -30
- package/dist/media.d.ts +81 -0
- package/dist/media.js +177 -0
- package/package.json +5 -1
package/dist/media.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//#region src/media.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Remote media delivery. Previews and metadata resolve through ordered
|
|
4
|
+
* strategies — the cheapest trustworthy source first, each later one only
|
|
5
|
+
* filling what the earlier ones left — so every consumer degrades the same way.
|
|
6
|
+
*
|
|
7
|
+
* URL rewrites happen locally without a request, so they apply only to hosts
|
|
8
|
+
* known to serve Aliyun OSS processing; any other URL is used as is.
|
|
9
|
+
*/
|
|
10
|
+
type MediaType = "image" | "video" | "audio";
|
|
11
|
+
/**
|
|
12
|
+
* `cover` guarantees the short edge (square tiles), `contain` bounds the long
|
|
13
|
+
* edge (viewers, textures), `width` bounds only the width, so flowing layouts
|
|
14
|
+
* keep the original's rendered size.
|
|
15
|
+
*/
|
|
16
|
+
type MediaFit = "cover" | "contain" | "width";
|
|
17
|
+
type MediaInfo = {
|
|
18
|
+
width?: number;
|
|
19
|
+
height?: number;
|
|
20
|
+
durationMs?: number;
|
|
21
|
+
frameCount?: number;
|
|
22
|
+
firstFrameUrl?: string;
|
|
23
|
+
lastFrameUrl?: string;
|
|
24
|
+
mimeType?: string;
|
|
25
|
+
bytes?: number;
|
|
26
|
+
};
|
|
27
|
+
/** Fixed variant widths keep CDN cache hits high; pick with `mediaVariantSize`. */
|
|
28
|
+
declare const MEDIA_VARIANT_SIZES: readonly [192, 384, 768, 1536];
|
|
29
|
+
type MediaVariantSize = (typeof MEDIA_VARIANT_SIZES)[number];
|
|
30
|
+
/** Smallest variant covering `pixels` device pixels, capped at the largest. */
|
|
31
|
+
declare function mediaVariantSize(pixels: number): MediaVariantSize;
|
|
32
|
+
/** Append an OSS process to `url`, keeping its query and hash. */
|
|
33
|
+
declare function ossProcessUrl(url: string, process: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* A resized WebP of an image, fitted per `MediaFit`. Never enlarges. GIFs keep
|
|
36
|
+
* their original, since a converted variant may lose the animation.
|
|
37
|
+
*/
|
|
38
|
+
declare function imageVariantUrl(url: string, size: MediaVariantSize, fit?: MediaFit): string | null;
|
|
39
|
+
/** A still JPEG of a video's first frame, `width` wide. */
|
|
40
|
+
declare function videoFrameUrl(url: string, width: MediaVariantSize): string | null;
|
|
41
|
+
/**
|
|
42
|
+
* The media a CDN variant was derived from, to fall back to when processing
|
|
43
|
+
* fails. Only the process parameter is cut, so the rest of the URL (signed
|
|
44
|
+
* queries included) stays byte-for-byte.
|
|
45
|
+
*/
|
|
46
|
+
declare function variantSource(url: string): {
|
|
47
|
+
url: string;
|
|
48
|
+
type: "image" | "video";
|
|
49
|
+
} | null;
|
|
50
|
+
type MediaPreviewSource = {
|
|
51
|
+
type: MediaType;
|
|
52
|
+
url?: string | null;
|
|
53
|
+
/** Provider cover or poster: art chosen for the work, so it ranks first. */
|
|
54
|
+
previewUrl?: string | null;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Preview images to try in order: the provider cover (resized, then as is),
|
|
58
|
+
* then a snapshot of the video itself, then the probed first frame.
|
|
59
|
+
* An empty list means only the media element itself can render a frame.
|
|
60
|
+
*/
|
|
61
|
+
declare function mediaPreviewCandidates(media: MediaPreviewSource, options: {
|
|
62
|
+
size: MediaVariantSize;
|
|
63
|
+
fit?: MediaFit;
|
|
64
|
+
}, info?: Pick<MediaInfo, "firstFrameUrl">): string[];
|
|
65
|
+
/** Media facts from response headers; `x-oss-meta-*` need CORS exposure in browsers. */
|
|
66
|
+
declare function mediaInfoFromHeaders(headers: Headers, url: string): MediaInfo;
|
|
67
|
+
/** Fields of `primary` win; `fallback` fills the rest. */
|
|
68
|
+
declare function mergeMediaInfo(primary: MediaInfo, fallback: MediaInfo): MediaInfo;
|
|
69
|
+
type ProbeOptions = {
|
|
70
|
+
type: MediaType;
|
|
71
|
+
fetch?: typeof globalThis.fetch;
|
|
72
|
+
signal?: AbortSignal;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Probe remote media: OSS meta headers (one HEAD, the only source of video
|
|
76
|
+
* duration and frames), then OSS `image/info` for image dimensions. Unknown
|
|
77
|
+
* hosts and failed steps resolve to what was learned so far; never throws.
|
|
78
|
+
*/
|
|
79
|
+
declare function probeMediaInfo(url: string, options: ProbeOptions): Promise<MediaInfo>;
|
|
80
|
+
//#endregion
|
|
81
|
+
export { MEDIA_VARIANT_SIZES, MediaFit, MediaInfo, MediaPreviewSource, MediaType, MediaVariantSize, imageVariantUrl, mediaInfoFromHeaders, mediaPreviewCandidates, mediaVariantSize, mergeMediaInfo, ossProcessUrl, probeMediaInfo, variantSource, videoFrameUrl };
|
package/dist/media.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
//#region src/media.ts
|
|
2
|
+
const RESIZE = {
|
|
3
|
+
cover: (size) => `m_mfit,w_${size},h_${size}`,
|
|
4
|
+
contain: (size) => `m_lfit,w_${size},h_${size}`,
|
|
5
|
+
width: (size) => `w_${size}`
|
|
6
|
+
};
|
|
7
|
+
const MEDIA_HOSTS = /* @__PURE__ */ new Map([["router-files.neta.art", {
|
|
8
|
+
process: true,
|
|
9
|
+
metaHeaders: true
|
|
10
|
+
}], ["public.cohub.live", {
|
|
11
|
+
process: true,
|
|
12
|
+
metaHeaders: false
|
|
13
|
+
}]]);
|
|
14
|
+
/** Snapshots render at the requested width, upscaling small videos; cap it. */
|
|
15
|
+
const VIDEO_FRAME_MAX_WIDTH = 768;
|
|
16
|
+
/** Fixed variant widths keep CDN cache hits high; pick with `mediaVariantSize`. */
|
|
17
|
+
const MEDIA_VARIANT_SIZES = [
|
|
18
|
+
192,
|
|
19
|
+
384,
|
|
20
|
+
768,
|
|
21
|
+
1536
|
|
22
|
+
];
|
|
23
|
+
/** Smallest variant covering `pixels` device pixels, capped at the largest. */
|
|
24
|
+
function mediaVariantSize(pixels) {
|
|
25
|
+
return MEDIA_VARIANT_SIZES.find((size) => size >= pixels) ?? 1536;
|
|
26
|
+
}
|
|
27
|
+
function capabilities(url) {
|
|
28
|
+
if (!/^https:\/\//i.test(url)) return void 0;
|
|
29
|
+
try {
|
|
30
|
+
return MEDIA_HOSTS.get(new URL(url).hostname);
|
|
31
|
+
} catch {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const OSS_PROCESS = /[?&]x-oss-process=/;
|
|
36
|
+
/** Append an OSS process to `url`, keeping its query and hash. */
|
|
37
|
+
function ossProcessUrl(url, process) {
|
|
38
|
+
if (/^(data|blob):/i.test(url) || OSS_PROCESS.test(url)) return url;
|
|
39
|
+
const hashIndex = url.indexOf("#");
|
|
40
|
+
const base = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
|
|
41
|
+
const hash = hashIndex >= 0 ? url.slice(hashIndex) : "";
|
|
42
|
+
return `${base}${base.includes("?") ? "&" : "?"}x-oss-process=${process}${hash}`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A resized WebP of an image, fitted per `MediaFit`. Never enlarges. GIFs keep
|
|
46
|
+
* their original, since a converted variant may lose the animation.
|
|
47
|
+
*/
|
|
48
|
+
function imageVariantUrl(url, size, fit = "contain") {
|
|
49
|
+
if (!capabilities(url) || /\.gif$/i.test(new URL(url).pathname)) return null;
|
|
50
|
+
return ossProcessUrl(url, `image/resize,${RESIZE[fit](size)}/quality,q_82/format,webp`);
|
|
51
|
+
}
|
|
52
|
+
/** A still JPEG of a video's first frame, `width` wide. */
|
|
53
|
+
function videoFrameUrl(url, width) {
|
|
54
|
+
if (!capabilities(url)) return null;
|
|
55
|
+
return ossProcessUrl(url, `video/snapshot,t_0,f_jpg,w_${width},m_fast`);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The media a CDN variant was derived from, to fall back to when processing
|
|
59
|
+
* fails. Only the process parameter is cut, so the rest of the URL (signed
|
|
60
|
+
* queries included) stays byte-for-byte.
|
|
61
|
+
*/
|
|
62
|
+
function variantSource(url) {
|
|
63
|
+
const match = /([?&])x-oss-process=([^&#]*)/.exec(url);
|
|
64
|
+
if (!match) return null;
|
|
65
|
+
const [segment, separator, process = ""] = match;
|
|
66
|
+
const head = url.slice(0, match.index);
|
|
67
|
+
const tail = url.slice(match.index + segment.length);
|
|
68
|
+
return {
|
|
69
|
+
url: separator === "?" && tail.startsWith("&") ? `${head}?${tail.slice(1)}` : head + tail,
|
|
70
|
+
type: process.startsWith("video/") ? "video" : "image"
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Preview images to try in order: the provider cover (resized, then as is),
|
|
75
|
+
* then a snapshot of the video itself, then the probed first frame.
|
|
76
|
+
* An empty list means only the media element itself can render a frame.
|
|
77
|
+
*/
|
|
78
|
+
function mediaPreviewCandidates(media, options, info) {
|
|
79
|
+
const candidates = [];
|
|
80
|
+
const add = (url) => {
|
|
81
|
+
if (url && !candidates.includes(url)) candidates.push(url);
|
|
82
|
+
};
|
|
83
|
+
const addImage = (url) => {
|
|
84
|
+
if (!url) return;
|
|
85
|
+
add(imageVariantUrl(url, options.size, options.fit));
|
|
86
|
+
add(url);
|
|
87
|
+
};
|
|
88
|
+
addImage(media.type === "image" ? media.url : media.previewUrl);
|
|
89
|
+
if (media.type === "video" && media.url) {
|
|
90
|
+
const width = options.fit === "cover" ? options.size * 2 : options.size;
|
|
91
|
+
add(videoFrameUrl(media.url, mediaVariantSize(Math.min(width, VIDEO_FRAME_MAX_WIDTH))));
|
|
92
|
+
addImage(info?.firstFrameUrl);
|
|
93
|
+
}
|
|
94
|
+
return candidates;
|
|
95
|
+
}
|
|
96
|
+
function positive(value) {
|
|
97
|
+
const number = typeof value === "string" ? Number(value) : value;
|
|
98
|
+
return typeof number === "number" && Number.isFinite(number) && number > 0 ? number : void 0;
|
|
99
|
+
}
|
|
100
|
+
/** Frame URLs must stay on the media's own origin. */
|
|
101
|
+
function sameOriginUrl(value, origin) {
|
|
102
|
+
if (!value) return void 0;
|
|
103
|
+
try {
|
|
104
|
+
const url = new URL(value);
|
|
105
|
+
return url.protocol === "https:" && url.origin === origin ? url.href : void 0;
|
|
106
|
+
} catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function compact(info) {
|
|
111
|
+
return Object.fromEntries(Object.entries(info).filter(([, value]) => value !== void 0));
|
|
112
|
+
}
|
|
113
|
+
/** Media facts from response headers; `x-oss-meta-*` need CORS exposure in browsers. */
|
|
114
|
+
function mediaInfoFromHeaders(headers, url) {
|
|
115
|
+
const origin = new URL(url).origin;
|
|
116
|
+
const meta = (name) => headers.get(`x-oss-meta-${name}`);
|
|
117
|
+
const seconds = positive(meta("duration"));
|
|
118
|
+
const frameCount = positive(meta("nb-frames"));
|
|
119
|
+
return compact({
|
|
120
|
+
width: positive(meta("width")),
|
|
121
|
+
height: positive(meta("height")),
|
|
122
|
+
durationMs: seconds ? Math.round(seconds * 1e3) : void 0,
|
|
123
|
+
frameCount: frameCount ? Math.round(frameCount) : void 0,
|
|
124
|
+
firstFrameUrl: sameOriginUrl(meta("first-frame"), origin),
|
|
125
|
+
lastFrameUrl: sameOriginUrl(meta("last-frame"), origin),
|
|
126
|
+
mimeType: headers.get("content-type")?.split(";")[0]?.trim() || void 0,
|
|
127
|
+
bytes: positive(headers.get("content-length"))
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** Fields of `primary` win; `fallback` fills the rest. */
|
|
131
|
+
function mergeMediaInfo(primary, fallback) {
|
|
132
|
+
return compact({
|
|
133
|
+
...fallback,
|
|
134
|
+
...compact(primary)
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function hasDimensions(info) {
|
|
138
|
+
return info.width !== void 0 && info.height !== void 0;
|
|
139
|
+
}
|
|
140
|
+
async function probeHeaders(url, options) {
|
|
141
|
+
const response = await (options.fetch ?? globalThis.fetch)(url, {
|
|
142
|
+
method: "HEAD",
|
|
143
|
+
signal: options.signal
|
|
144
|
+
});
|
|
145
|
+
return response.ok ? mediaInfoFromHeaders(response.headers, url) : {};
|
|
146
|
+
}
|
|
147
|
+
async function probeImageInfo(url, options) {
|
|
148
|
+
if (OSS_PROCESS.test(url)) return {};
|
|
149
|
+
const response = await (options.fetch ?? globalThis.fetch)(ossProcessUrl(url, "image/info"), { signal: options.signal });
|
|
150
|
+
if (!response.ok) return {};
|
|
151
|
+
const body = await response.json();
|
|
152
|
+
return compact({
|
|
153
|
+
width: positive(body.ImageWidth?.value),
|
|
154
|
+
height: positive(body.ImageHeight?.value),
|
|
155
|
+
bytes: positive(body.FileSize?.value)
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Probe remote media: OSS meta headers (one HEAD, the only source of video
|
|
160
|
+
* duration and frames), then OSS `image/info` for image dimensions. Unknown
|
|
161
|
+
* hosts and failed steps resolve to what was learned so far; never throws.
|
|
162
|
+
*/
|
|
163
|
+
async function probeMediaInfo(url, options) {
|
|
164
|
+
const host = capabilities(url);
|
|
165
|
+
if (!host) return {};
|
|
166
|
+
let info = {};
|
|
167
|
+
const attempt = async (strategy) => {
|
|
168
|
+
try {
|
|
169
|
+
info = mergeMediaInfo(info, await strategy());
|
|
170
|
+
} catch {}
|
|
171
|
+
};
|
|
172
|
+
if (host.metaHeaders) await attempt(() => probeHeaders(url, options));
|
|
173
|
+
if (options.type === "image" && !hasDimensions(info)) await attempt(() => probeImageInfo(url, options));
|
|
174
|
+
return info;
|
|
175
|
+
}
|
|
176
|
+
//#endregion
|
|
177
|
+
export { MEDIA_VARIANT_SIZES, imageVariantUrl, mediaInfoFromHeaders, mediaPreviewCandidates, mediaVariantSize, mergeMediaInfo, ossProcessUrl, probeMediaInfo, variantSource, videoFrameUrl };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.24.0",
|
|
4
4
|
"description": "Cohub SDK for spaces, sessions, boards, and realtime agent collaboration.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"private": false,
|
|
@@ -49,6 +49,10 @@
|
|
|
49
49
|
"types": "./dist/space-picker.d.ts",
|
|
50
50
|
"import": "./dist/space-picker.js"
|
|
51
51
|
},
|
|
52
|
+
"./media": {
|
|
53
|
+
"types": "./dist/media.d.ts",
|
|
54
|
+
"import": "./dist/media.js"
|
|
55
|
+
},
|
|
52
56
|
"./board": {
|
|
53
57
|
"types": "./dist/board/index.d.ts",
|
|
54
58
|
"import": "./dist/board/index.js"
|