@hyperframes/studio-server 0.7.94 → 0.7.96
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.
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { P as PreviewApiAdapter, i as injectMediaCodecMap, e as injectMediaCodecMapIntoHtml, f as isAutoProxyEnabled, p as proxyEtagSalt, r as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-
|
|
1
|
+
export { P as PreviewApiAdapter, i as injectMediaCodecMap, e as injectMediaCodecMapIntoHtml, f as isAutoProxyEnabled, p as proxyEtagSalt, r as resolvePreviewMediaCodecProbeCache } from '../mediaProxyPreview-DVTxBkRO.js';
|
|
2
2
|
import './mediaCodecMap.js';
|
|
3
3
|
import '@hyperframes/core';
|
|
4
4
|
import '@hyperframes/parsers';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import { S as StudioApiAdapter, M as MediaProcessingJobState } from './mediaProxyPreview-
|
|
3
|
-
export { L as LintResult, P as PreviewApiAdapter, R as RenderJobState, a as ResolvedProject, b as StudioSelectionResponse, c as StudioSelectionSnapshot, d as StudioSelectionTextField } from './mediaProxyPreview-
|
|
2
|
+
import { S as StudioApiAdapter, M as MediaProcessingJobState } from './mediaProxyPreview-DVTxBkRO.js';
|
|
3
|
+
export { L as LintResult, P as PreviewApiAdapter, R as RenderJobState, a as ResolvedProject, b as StudioSelectionResponse, c as StudioSelectionSnapshot, d as StudioSelectionTextField } from './mediaProxyPreview-DVTxBkRO.js';
|
|
4
4
|
export { ScreenshotClip, getElementScreenshotClip } from './helpers/screenshotClip.js';
|
|
5
5
|
export { STUDIO_MANUAL_EDITS_PATH, StudioManualEditsRenderScriptOptions, createStudioManualEditsRenderBodyScript, createStudioPositionSeekReapplyScript } from './helpers/manualEditsRenderScript.js';
|
|
6
6
|
export { STUDIO_MOTION_PATH, StudioMotionRenderScriptOptions, createStudioMotionRenderBodyScript } from './helpers/studioMotionRenderScript.js';
|
|
@@ -57,6 +57,15 @@ declare function consumeFileWriteReceipt(absPath: string): FileWriteReceipt | nu
|
|
|
57
57
|
*/
|
|
58
58
|
declare function buildSubCompositionHtml(projectDir: string, compPath: string, runtimeUrl: string, baseHref?: string, rawOverride?: string): string | null;
|
|
59
59
|
|
|
60
|
+
interface ThumbnailOutputDimensions {
|
|
61
|
+
width: number;
|
|
62
|
+
height: number;
|
|
63
|
+
outputWidth: number;
|
|
64
|
+
outputHeight: number;
|
|
65
|
+
}
|
|
66
|
+
/** Sole adapter rule for capturing authored layout at bounded physical dimensions. */
|
|
67
|
+
declare function thumbnailDeviceScaleFactor({ width, height, outputWidth, outputHeight, }: ThumbnailOutputDimensions): number;
|
|
68
|
+
|
|
60
69
|
type BackgroundRemovalJobOptions = Parameters<NonNullable<StudioApiAdapter["startBackgroundRemoval"]>>[0];
|
|
61
70
|
type BackgroundRemovalProgressEvent = {
|
|
62
71
|
kind: "info";
|
|
@@ -88,4 +97,4 @@ type BackgroundRemovalRender = (options: {
|
|
|
88
97
|
}>;
|
|
89
98
|
declare function createBackgroundRemovalJob(opts: BackgroundRemovalJobOptions, render: BackgroundRemovalRender): MediaProcessingJobState;
|
|
90
99
|
|
|
91
|
-
export { type BackgroundRemovalRender, type FileWriteReceipt, MIME_TYPES, MediaProcessingJobState, StudioApiAdapter, buildSubCompositionHtml, consumeFileWriteReceipt, createBackgroundRemovalJob, createProjectSignature, createStudioApi, fileContentVersion, getMimeType, walkDir };
|
|
100
|
+
export { type BackgroundRemovalRender, type FileWriteReceipt, MIME_TYPES, MediaProcessingJobState, StudioApiAdapter, type ThumbnailOutputDimensions, buildSubCompositionHtml, consumeFileWriteReceipt, createBackgroundRemovalJob, createProjectSignature, createStudioApi, fileContentVersion, getMimeType, thumbnailDeviceScaleFactor, walkDir };
|
package/dist/index.js
CHANGED
|
@@ -3918,10 +3918,170 @@ function registerRenderRoutes(api, adapter) {
|
|
|
3918
3918
|
}
|
|
3919
3919
|
|
|
3920
3920
|
// src/routes/thumbnail.ts
|
|
3921
|
-
import {
|
|
3921
|
+
import {
|
|
3922
|
+
existsSync as existsSync8,
|
|
3923
|
+
mkdirSync as mkdirSync5,
|
|
3924
|
+
readFileSync as readFileSync11,
|
|
3925
|
+
readdirSync as readdirSync6,
|
|
3926
|
+
renameSync as renameSync2,
|
|
3927
|
+
rmSync as rmSync3,
|
|
3928
|
+
statSync as statSync4,
|
|
3929
|
+
unlinkSync as unlinkSync4,
|
|
3930
|
+
writeFileSync as writeFileSync6
|
|
3931
|
+
} from "fs";
|
|
3922
3932
|
import { join as join11 } from "path";
|
|
3923
|
-
import { createHash as createHash4 } from "crypto";
|
|
3933
|
+
import { createHash as createHash4, randomUUID as randomUUID3 } from "crypto";
|
|
3934
|
+
|
|
3935
|
+
// src/routes/thumbnailGenerationCoordinator.ts
|
|
3936
|
+
var ThumbnailGenerationCoordinator = class {
|
|
3937
|
+
constructor(concurrency = 1) {
|
|
3938
|
+
this.concurrency = concurrency;
|
|
3939
|
+
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
|
3940
|
+
throw new RangeError("Thumbnail concurrency must be a positive integer");
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
concurrency;
|
|
3944
|
+
entries = /* @__PURE__ */ new Map();
|
|
3945
|
+
queue = [];
|
|
3946
|
+
activeEntries = /* @__PURE__ */ new Set();
|
|
3947
|
+
active = 0;
|
|
3948
|
+
acquire(key, signal, work) {
|
|
3949
|
+
if (signal.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
3950
|
+
let entry = this.entries.get(key);
|
|
3951
|
+
if (!entry) {
|
|
3952
|
+
let resolve5;
|
|
3953
|
+
let reject;
|
|
3954
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
3955
|
+
resolve5 = resolvePromise;
|
|
3956
|
+
reject = rejectPromise;
|
|
3957
|
+
});
|
|
3958
|
+
entry = {
|
|
3959
|
+
key,
|
|
3960
|
+
controller: new AbortController(),
|
|
3961
|
+
leases: 0,
|
|
3962
|
+
state: "queued",
|
|
3963
|
+
work,
|
|
3964
|
+
promise,
|
|
3965
|
+
resolve: resolve5,
|
|
3966
|
+
reject
|
|
3967
|
+
};
|
|
3968
|
+
this.entries.set(key, entry);
|
|
3969
|
+
this.queue.push(entry);
|
|
3970
|
+
}
|
|
3971
|
+
entry.leases++;
|
|
3972
|
+
this.pump();
|
|
3973
|
+
return this.lease(entry, signal);
|
|
3974
|
+
}
|
|
3975
|
+
protectedKeys() {
|
|
3976
|
+
return /* @__PURE__ */ new Set([...this.entries.keys(), ...[...this.activeEntries].map((entry) => entry.key)]);
|
|
3977
|
+
}
|
|
3978
|
+
lease(entry, signal) {
|
|
3979
|
+
return new Promise((resolve5, reject) => {
|
|
3980
|
+
let released = false;
|
|
3981
|
+
const release = () => {
|
|
3982
|
+
if (released) return;
|
|
3983
|
+
released = true;
|
|
3984
|
+
signal.removeEventListener("abort", onAbort);
|
|
3985
|
+
entry.leases--;
|
|
3986
|
+
if (entry.leases > 0 || !this.entries.has(entry.key)) return;
|
|
3987
|
+
entry.controller.abort();
|
|
3988
|
+
if (this.entries.get(entry.key) === entry) this.entries.delete(entry.key);
|
|
3989
|
+
if (entry.state === "queued") {
|
|
3990
|
+
const index = this.queue.indexOf(entry);
|
|
3991
|
+
if (index >= 0) this.queue.splice(index, 1);
|
|
3992
|
+
entry.reject(new DOMException("Aborted", "AbortError"));
|
|
3993
|
+
}
|
|
3994
|
+
};
|
|
3995
|
+
const onAbort = () => {
|
|
3996
|
+
release();
|
|
3997
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
3998
|
+
};
|
|
3999
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
4000
|
+
entry.promise.then(
|
|
4001
|
+
(value) => {
|
|
4002
|
+
release();
|
|
4003
|
+
resolve5(value);
|
|
4004
|
+
},
|
|
4005
|
+
(reason) => {
|
|
4006
|
+
release();
|
|
4007
|
+
reject(reason);
|
|
4008
|
+
}
|
|
4009
|
+
);
|
|
4010
|
+
});
|
|
4011
|
+
}
|
|
4012
|
+
pump() {
|
|
4013
|
+
while (this.active < this.concurrency) {
|
|
4014
|
+
const entry = this.queue.shift();
|
|
4015
|
+
if (!entry) return;
|
|
4016
|
+
if (entry.leases === 0) continue;
|
|
4017
|
+
entry.state = "active";
|
|
4018
|
+
this.activeEntries.add(entry);
|
|
4019
|
+
this.active++;
|
|
4020
|
+
void this.run(entry);
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
async run(entry) {
|
|
4024
|
+
try {
|
|
4025
|
+
entry.resolve(await entry.work(entry.controller.signal));
|
|
4026
|
+
} catch (error) {
|
|
4027
|
+
entry.reject(error);
|
|
4028
|
+
} finally {
|
|
4029
|
+
this.active--;
|
|
4030
|
+
this.activeEntries.delete(entry);
|
|
4031
|
+
if (this.entries.get(entry.key) === entry) this.entries.delete(entry.key);
|
|
4032
|
+
this.pump();
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
};
|
|
4036
|
+
var thumbnailGenerationCoordinator = new ThumbnailGenerationCoordinator(1);
|
|
4037
|
+
|
|
4038
|
+
// src/routes/thumbnail.ts
|
|
3924
4039
|
var THUMBNAIL_CACHE_VERSION = "v4";
|
|
4040
|
+
var THUMBNAIL_MAX_OUTPUT_WIDTH = 240;
|
|
4041
|
+
var THUMBNAIL_MAX_OUTPUT_HEIGHT = 135;
|
|
4042
|
+
var THUMBNAIL_CACHE_MAX_BYTES = 512 * 1024 * 1024;
|
|
4043
|
+
var THUMBNAIL_CACHE_MAX_AGE_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
4044
|
+
var prunedCacheDirs = /* @__PURE__ */ new Set();
|
|
4045
|
+
function pruneThumbnailCache(cacheDir, protectedPaths, now = Date.now()) {
|
|
4046
|
+
if (!existsSync8(cacheDir)) return;
|
|
4047
|
+
const files = readdirSync6(cacheDir, { withFileTypes: true }).flatMap((entry) => {
|
|
4048
|
+
if (!entry.isFile()) return [];
|
|
4049
|
+
const path = join11(cacheDir, entry.name);
|
|
4050
|
+
try {
|
|
4051
|
+
const stats = statSync4(path);
|
|
4052
|
+
return [{ path, bytes: stats.size, mtimeMs: stats.mtimeMs }];
|
|
4053
|
+
} catch {
|
|
4054
|
+
return [];
|
|
4055
|
+
}
|
|
4056
|
+
});
|
|
4057
|
+
const retained = [];
|
|
4058
|
+
for (const file of files) {
|
|
4059
|
+
if (!protectedPaths.has(file.path) && now - file.mtimeMs > THUMBNAIL_CACHE_MAX_AGE_MS) {
|
|
4060
|
+
rmSync3(file.path, { force: true });
|
|
4061
|
+
} else {
|
|
4062
|
+
retained.push(file);
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
let bytes = retained.reduce((total, file) => total + file.bytes, 0);
|
|
4066
|
+
for (const file of retained.sort((left, right) => left.mtimeMs - right.mtimeMs)) {
|
|
4067
|
+
if (bytes <= THUMBNAIL_CACHE_MAX_BYTES) break;
|
|
4068
|
+
if (protectedPaths.has(file.path)) continue;
|
|
4069
|
+
try {
|
|
4070
|
+
unlinkSync4(file.path);
|
|
4071
|
+
bytes -= file.bytes;
|
|
4072
|
+
} catch {
|
|
4073
|
+
}
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
function writeThumbnailAtomically(path, buffer) {
|
|
4077
|
+
const temporaryPath = `${path}.${process.pid}.${randomUUID3()}.tmp`;
|
|
4078
|
+
try {
|
|
4079
|
+
writeFileSync6(temporaryPath, buffer, { flag: "wx" });
|
|
4080
|
+
renameSync2(temporaryPath, path);
|
|
4081
|
+
} finally {
|
|
4082
|
+
rmSync3(temporaryPath, { force: true });
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
3925
4085
|
function registerThumbnailRoutes(api, adapter) {
|
|
3926
4086
|
api.get("/projects/:id/thumbnail/*", async (c) => {
|
|
3927
4087
|
if (!adapter.generateThumbnail) {
|
|
@@ -3942,6 +4102,8 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
3942
4102
|
const selector = url.searchParams.get("selector") || void 0;
|
|
3943
4103
|
const format = url.searchParams.get("format") === "png" ? "png" : "jpeg";
|
|
3944
4104
|
const contentType = format === "png" ? "image/png" : "image/jpeg";
|
|
4105
|
+
const requestedOutput = url.searchParams.get("output");
|
|
4106
|
+
const outputMode = requestedOutput === "source" || requestedOutput !== "preview" && format === "png" ? "source" : "preview";
|
|
3945
4107
|
const rawSelectorIndex = Number.parseInt(url.searchParams.get("selectorIndex") || "0", 10);
|
|
3946
4108
|
const selectorIndex = Number.isFinite(rawSelectorIndex) && rawSelectorIndex > 0 ? rawSelectorIndex : void 0;
|
|
3947
4109
|
const urlVersion = url.searchParams.get("v") || "";
|
|
@@ -3979,37 +4141,62 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
3979
4141
|
const cacheDir = join11(project.dir, ".thumbnails");
|
|
3980
4142
|
const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${selectorIndex ?? 0}` : "";
|
|
3981
4143
|
const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
|
|
3982
|
-
const
|
|
4144
|
+
const outputScale = outputMode === "source" ? 1 : Math.min(1, THUMBNAIL_MAX_OUTPUT_WIDTH / compW, THUMBNAIL_MAX_OUTPUT_HEIGHT / compH);
|
|
4145
|
+
const outputWidth = Math.max(1, Math.round(compW * outputScale));
|
|
4146
|
+
const outputHeight = Math.max(1, Math.round(compH * outputScale));
|
|
4147
|
+
const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${manualEditsKey}${motionKey}${sourceKey}_${format}_${outputMode}_${compPath.replace(/\//g, "_")}_${compW}x${compH}_${outputWidth}x${outputHeight}_${sourceMtime}_${seekTime.toFixed(2)}${selectorKey}.${format === "png" ? "png" : "jpg"}`;
|
|
3983
4148
|
const cachePath = join11(cacheDir, cacheKey);
|
|
4149
|
+
if (!prunedCacheDirs.has(cacheDir)) {
|
|
4150
|
+
prunedCacheDirs.add(cacheDir);
|
|
4151
|
+
pruneThumbnailCache(
|
|
4152
|
+
cacheDir,
|
|
4153
|
+
/* @__PURE__ */ new Set([...thumbnailGenerationCoordinator.protectedKeys(), cachePath])
|
|
4154
|
+
);
|
|
4155
|
+
}
|
|
3984
4156
|
if (existsSync8(cachePath)) {
|
|
3985
4157
|
return new Response(new Uint8Array(readFileSync11(cachePath)), {
|
|
3986
4158
|
headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
|
|
3987
4159
|
});
|
|
3988
4160
|
}
|
|
3989
4161
|
try {
|
|
3990
|
-
const buffer = await
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4162
|
+
const buffer = await thumbnailGenerationCoordinator.acquire(
|
|
4163
|
+
cachePath,
|
|
4164
|
+
c.req.raw.signal,
|
|
4165
|
+
async (signal) => {
|
|
4166
|
+
const generated = await adapter.generateThumbnail({
|
|
4167
|
+
project,
|
|
4168
|
+
compPath,
|
|
4169
|
+
seekTime,
|
|
4170
|
+
width: compW,
|
|
4171
|
+
height: compH,
|
|
4172
|
+
outputWidth,
|
|
4173
|
+
outputHeight,
|
|
4174
|
+
previewUrl,
|
|
4175
|
+
selector,
|
|
4176
|
+
format,
|
|
4177
|
+
selectorIndex,
|
|
4178
|
+
signal
|
|
4179
|
+
});
|
|
4180
|
+
if (!generated) return null;
|
|
4181
|
+
if (!existsSync8(cacheDir)) mkdirSync5(cacheDir, { recursive: true });
|
|
4182
|
+
writeThumbnailAtomically(cachePath, generated);
|
|
4183
|
+
return generated;
|
|
4184
|
+
}
|
|
4185
|
+
);
|
|
4001
4186
|
if (!buffer) {
|
|
4002
4187
|
return c.json(
|
|
4003
4188
|
{ error: "Thumbnail generation failed \u2014 Chrome browser may not be available" },
|
|
4004
4189
|
500
|
|
4005
4190
|
);
|
|
4006
4191
|
}
|
|
4007
|
-
|
|
4008
|
-
writeFileSync6(cachePath, buffer);
|
|
4192
|
+
pruneThumbnailCache(cacheDir, thumbnailGenerationCoordinator.protectedKeys());
|
|
4009
4193
|
return new Response(new Uint8Array(buffer), {
|
|
4010
4194
|
headers: { "Content-Type": contentType, "Cache-Control": "no-cache" }
|
|
4011
4195
|
});
|
|
4012
4196
|
} catch (err) {
|
|
4197
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
4198
|
+
return new Response(null, { status: 499 });
|
|
4199
|
+
}
|
|
4013
4200
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4014
4201
|
return c.json({ error: `Thumbnail generation failed: ${msg}` }, 500);
|
|
4015
4202
|
}
|
|
@@ -4562,6 +4749,20 @@ function createStudioApi(adapter) {
|
|
|
4562
4749
|
return api;
|
|
4563
4750
|
}
|
|
4564
4751
|
|
|
4752
|
+
// src/helpers/thumbnailOutput.ts
|
|
4753
|
+
function thumbnailDeviceScaleFactor({
|
|
4754
|
+
width,
|
|
4755
|
+
height,
|
|
4756
|
+
outputWidth,
|
|
4757
|
+
outputHeight
|
|
4758
|
+
}) {
|
|
4759
|
+
const dimensions = [width, height, outputWidth, outputHeight];
|
|
4760
|
+
if (dimensions.some((value) => !Number.isFinite(value) || value <= 0)) {
|
|
4761
|
+
throw new RangeError("Thumbnail dimensions must be positive finite numbers");
|
|
4762
|
+
}
|
|
4763
|
+
return Math.min(1, outputWidth / width, outputHeight / height);
|
|
4764
|
+
}
|
|
4765
|
+
|
|
4565
4766
|
// src/helpers/backgroundRemovalJob.ts
|
|
4566
4767
|
function createBackgroundRemovalJob(opts, render) {
|
|
4567
4768
|
const state = {
|
|
@@ -4631,6 +4832,7 @@ export {
|
|
|
4631
4832
|
getElementScreenshotClip,
|
|
4632
4833
|
getMimeType,
|
|
4633
4834
|
isSafePath,
|
|
4835
|
+
thumbnailDeviceScaleFactor,
|
|
4634
4836
|
walkDir
|
|
4635
4837
|
};
|
|
4636
4838
|
//# sourceMappingURL=index.js.map
|