@tomorrowos/sdk 0.4.5 → 0.4.7
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/media-probe.d.ts +3 -2
- package/dist/media-probe.d.ts.map +1 -1
- package/dist/media-probe.js +112 -38
- package/dist/tomorrowos.d.ts.map +1 -1
- package/dist/tomorrowos.js +5 -4
- package/dist/upload-storage.d.ts +12 -0
- package/dist/upload-storage.d.ts.map +1 -0
- package/dist/upload-storage.js +30 -0
- package/package.json +1 -1
- package/templates/cms-starter/package.json +3 -3
- package/templates/cms-starter/public/methods.js +8 -5
package/dist/media-probe.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Video duration from file bytes (no ffprobe).
|
|
3
|
+
* MP4/MOV: ISO BMFF box walk (moov/mvhd only — never scans inside mdat).
|
|
4
|
+
* WebM: Duration element in the file header region only.
|
|
4
5
|
*/
|
|
5
6
|
export declare function probeVideoDurationMs(body: Buffer, filename: string): number | null;
|
|
6
7
|
//# sourceMappingURL=media-probe.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media-probe.d.ts","sourceRoot":"","sources":["../src/media-probe.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"media-probe.d.ts","sourceRoot":"","sources":["../src/media-probe.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA2IH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,MAAM,GAAG,IAAI,CAOf"}
|
package/dist/media-probe.js
CHANGED
|
@@ -1,54 +1,133 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Video duration from file bytes (no ffprobe).
|
|
3
|
+
* MP4/MOV: ISO BMFF box walk (moov/mvhd only — never scans inside mdat).
|
|
4
|
+
* WebM: Duration element in the file header region only.
|
|
4
5
|
*/
|
|
5
6
|
function isVideoFilename(name) {
|
|
6
7
|
const lower = String(name || "").toLowerCase();
|
|
7
8
|
return /\.(mp4|m4v|mov|webm|mkv|avi)$/.test(lower);
|
|
8
9
|
}
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
function clampDurationMs(ms) {
|
|
11
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
12
|
+
return 0;
|
|
13
|
+
return Math.min(3600 * 1000, Math.max(1000, Math.round(ms)));
|
|
14
|
+
}
|
|
15
|
+
function readBoxBounds(buf, offset) {
|
|
16
|
+
if (offset + 8 > buf.length)
|
|
17
|
+
return null;
|
|
18
|
+
let size = buf.readUInt32BE(offset);
|
|
19
|
+
const type = buf.toString("ascii", offset + 4, offset + 8);
|
|
20
|
+
let headerSize = 8;
|
|
21
|
+
if (size === 1) {
|
|
22
|
+
if (offset + 16 > buf.length)
|
|
23
|
+
return null;
|
|
24
|
+
const sizeBig = buf.readBigUInt64BE(offset + 8);
|
|
25
|
+
if (sizeBig < BigInt(headerSize))
|
|
26
|
+
return null;
|
|
27
|
+
if (sizeBig > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
28
|
+
// Box larger than JS can index; still validate against buffer length.
|
|
29
|
+
const endBig = BigInt(offset) + sizeBig;
|
|
30
|
+
if (endBig > BigInt(buf.length))
|
|
31
|
+
return null;
|
|
32
|
+
size = buf.length - offset;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
size = Number(sizeBig);
|
|
36
|
+
}
|
|
37
|
+
headerSize = 16;
|
|
38
|
+
}
|
|
39
|
+
else if (size === 0) {
|
|
40
|
+
size = buf.length - offset;
|
|
41
|
+
}
|
|
42
|
+
if (size < headerSize)
|
|
43
|
+
return null;
|
|
44
|
+
const end = offset + size;
|
|
45
|
+
if (end > buf.length)
|
|
46
|
+
return null;
|
|
47
|
+
return { type, contentStart: offset + headerSize, end };
|
|
48
|
+
}
|
|
49
|
+
function parseMvhdDurationMs(buf, contentStart, boxEnd) {
|
|
50
|
+
if (contentStart + 4 > boxEnd)
|
|
51
|
+
return null;
|
|
52
|
+
const version = buf[contentStart];
|
|
53
|
+
if (version === 0) {
|
|
54
|
+
if (contentStart + 28 > boxEnd)
|
|
55
|
+
return null;
|
|
56
|
+
const timescale = buf.readUInt32BE(contentStart + 20);
|
|
57
|
+
const duration = buf.readUInt32BE(contentStart + 24);
|
|
58
|
+
if (timescale <= 0 || duration <= 0)
|
|
59
|
+
return null;
|
|
60
|
+
return (duration / timescale) * 1000;
|
|
61
|
+
}
|
|
62
|
+
if (version === 1) {
|
|
63
|
+
if (contentStart + 40 > boxEnd)
|
|
64
|
+
return null;
|
|
65
|
+
const timescale = buf.readUInt32BE(contentStart + 28);
|
|
66
|
+
const durationHi = buf.readUInt32BE(contentStart + 32);
|
|
67
|
+
const durationLo = buf.readUInt32BE(contentStart + 36);
|
|
68
|
+
const duration = durationHi * 2 ** 32 + durationLo;
|
|
69
|
+
if (timescale <= 0 || duration <= 0)
|
|
70
|
+
return null;
|
|
71
|
+
return (duration / timescale) * 1000;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function findMvhdDurationInContainer(buf, start, end) {
|
|
76
|
+
let offset = start;
|
|
77
|
+
while (offset + 8 <= end) {
|
|
78
|
+
const box = readBoxBounds(buf, offset);
|
|
79
|
+
if (!box || box.end > end)
|
|
14
80
|
break;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
81
|
+
if (box.type === "mvhd") {
|
|
82
|
+
const ms = parseMvhdDurationMs(buf, box.contentStart, box.end);
|
|
83
|
+
if (ms != null)
|
|
84
|
+
return ms;
|
|
19
85
|
}
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
86
|
+
// moov/trak/mdia/minf/stbl can nest further boxes; only recurse into containers.
|
|
87
|
+
if (box.type === "moov" ||
|
|
88
|
+
box.type === "trak" ||
|
|
89
|
+
box.type === "mdia" ||
|
|
90
|
+
box.type === "minf" ||
|
|
91
|
+
box.type === "stbl") {
|
|
92
|
+
const nested = findMvhdDurationInContainer(buf, box.contentStart, box.end);
|
|
93
|
+
if (nested != null)
|
|
94
|
+
return nested;
|
|
27
95
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
96
|
+
offset = box.end;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
/** Walk top-level ISO boxes; skip mdat without scanning its payload. */
|
|
101
|
+
function probeMp4DurationMs(buf) {
|
|
102
|
+
let offset = 0;
|
|
103
|
+
while (offset + 8 <= buf.length) {
|
|
104
|
+
const box = readBoxBounds(buf, offset);
|
|
105
|
+
if (!box)
|
|
106
|
+
break;
|
|
107
|
+
if (box.type === "moov") {
|
|
108
|
+
const ms = findMvhdDurationInContainer(buf, box.contentStart, box.end);
|
|
109
|
+
if (ms != null)
|
|
110
|
+
return clampDurationMs(ms);
|
|
34
111
|
}
|
|
35
|
-
|
|
112
|
+
offset = box.end;
|
|
36
113
|
}
|
|
37
114
|
return null;
|
|
38
115
|
}
|
|
116
|
+
const WEBM_HEADER_PROBE_BYTES = 512 * 1024;
|
|
39
117
|
function probeWebmDurationMs(buf) {
|
|
118
|
+
const header = buf.subarray(0, Math.min(buf.length, WEBM_HEADER_PROBE_BYTES));
|
|
40
119
|
const marker = Buffer.from([0x44, 0x89]);
|
|
41
120
|
let searchFrom = 0;
|
|
42
|
-
while (searchFrom <
|
|
43
|
-
const idx =
|
|
121
|
+
while (searchFrom < header.length - 12) {
|
|
122
|
+
const idx = header.indexOf(marker, searchFrom);
|
|
44
123
|
if (idx < 0)
|
|
45
124
|
break;
|
|
46
125
|
const sizePos = idx + 2;
|
|
47
|
-
if (sizePos + 9 >
|
|
126
|
+
if (sizePos + 9 > header.length)
|
|
48
127
|
break;
|
|
49
|
-
const size =
|
|
50
|
-
if (size === 0x84 && sizePos + 9 <=
|
|
51
|
-
const duration =
|
|
128
|
+
const size = header[sizePos];
|
|
129
|
+
if (size === 0x84 && sizePos + 9 <= header.length) {
|
|
130
|
+
const duration = header.readFloatBE(sizePos + 1);
|
|
52
131
|
if (Number.isFinite(duration) && duration > 0) {
|
|
53
132
|
return clampDurationMs(duration);
|
|
54
133
|
}
|
|
@@ -57,17 +136,12 @@ function probeWebmDurationMs(buf) {
|
|
|
57
136
|
}
|
|
58
137
|
return null;
|
|
59
138
|
}
|
|
60
|
-
function clampDurationMs(ms) {
|
|
61
|
-
if (!Number.isFinite(ms) || ms <= 0)
|
|
62
|
-
return 0;
|
|
63
|
-
return Math.min(3600 * 1000, Math.max(1000, Math.round(ms)));
|
|
64
|
-
}
|
|
65
139
|
export function probeVideoDurationMs(body, filename) {
|
|
66
140
|
if (!body?.length || !isVideoFilename(filename))
|
|
67
141
|
return null;
|
|
68
142
|
const lower = filename.toLowerCase();
|
|
69
143
|
if (lower.endsWith(".webm")) {
|
|
70
|
-
return probeWebmDurationMs(body) ??
|
|
144
|
+
return probeWebmDurationMs(body) ?? probeMp4DurationMs(body);
|
|
71
145
|
}
|
|
72
|
-
return
|
|
146
|
+
return probeMp4DurationMs(body) ?? probeWebmDurationMs(body);
|
|
73
147
|
}
|
package/dist/tomorrowos.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,IAAI,MAAM,MAAM,CAAC;AAUxB,OAAO,EAAE,eAAe,EAAE,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,KAAK,EAIV,eAAe,EAChB,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
|
package/dist/tomorrowos.js
CHANGED
|
@@ -9,6 +9,7 @@ import { PlaylistCatalog } from "./playlist-catalog.js";
|
|
|
9
9
|
import { MemoryStore } from "./store/memory-store.js";
|
|
10
10
|
import { resolveBrandLogoPath, syncProjectAssetsToStaticRoot } from "./brand-assets.js";
|
|
11
11
|
import { probeVideoDurationMs } from "./media-probe.js";
|
|
12
|
+
import { storeUploadIfNeeded } from "./upload-storage.js";
|
|
12
13
|
function formatDurationMs(ms) {
|
|
13
14
|
if (!Number.isFinite(ms) || ms < 0)
|
|
14
15
|
return "0s";
|
|
@@ -565,17 +566,17 @@ export class TomorrowOS extends EventEmitter {
|
|
|
565
566
|
}
|
|
566
567
|
const rawName = url.searchParams.get("filename") || "upload";
|
|
567
568
|
const safeName = sanitizeUploadFilename(rawName);
|
|
568
|
-
const storedName = `${randomUUID()}-${safeName}`;
|
|
569
569
|
const uploadsDir = path.join(path.resolve(this.staticRoot), "uploads");
|
|
570
570
|
await fs.mkdir(uploadsDir, { recursive: true });
|
|
571
|
-
const
|
|
572
|
-
await fs.writeFile(filePath, body);
|
|
571
|
+
const { storedName, deduplicated, contentHash } = await storeUploadIfNeeded(uploadsDir, body, safeName);
|
|
573
572
|
const durationMs = probeVideoDurationMs(body, safeName);
|
|
574
573
|
const payload = {
|
|
575
574
|
status: "success",
|
|
576
575
|
url: `/uploads/${storedName}`,
|
|
577
576
|
filename: storedName,
|
|
578
|
-
size: body.length
|
|
577
|
+
size: body.length,
|
|
578
|
+
contentHash,
|
|
579
|
+
deduplicated
|
|
579
580
|
};
|
|
580
581
|
if (durationMs != null)
|
|
581
582
|
payload.durationMs = durationMs;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Stable on-disk name: first 16 chars of SHA-256 + original safe filename. */
|
|
2
|
+
export declare function buildContentAddressedName(body: Buffer, safeName: string): string;
|
|
3
|
+
export declare function contentHashHex(body: Buffer): string;
|
|
4
|
+
/**
|
|
5
|
+
* Write upload once per unique file bytes; reuse existing file when present.
|
|
6
|
+
*/
|
|
7
|
+
export declare function storeUploadIfNeeded(uploadsDir: string, body: Buffer, safeName: string): Promise<{
|
|
8
|
+
storedName: string;
|
|
9
|
+
deduplicated: boolean;
|
|
10
|
+
contentHash: string;
|
|
11
|
+
}>;
|
|
12
|
+
//# sourceMappingURL=upload-storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-storage.d.ts","sourceRoot":"","sources":["../src/upload-storage.ts"],"names":[],"mappings":"AAIA,+EAA+E;AAC/E,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGhF;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,CAgB7E"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
/** Stable on-disk name: first 16 chars of SHA-256 + original safe filename. */
|
|
5
|
+
export function buildContentAddressedName(body, safeName) {
|
|
6
|
+
const hash = createHash("sha256").update(body).digest("hex");
|
|
7
|
+
return `${hash.slice(0, 16)}-${safeName}`;
|
|
8
|
+
}
|
|
9
|
+
export function contentHashHex(body) {
|
|
10
|
+
return createHash("sha256").update(body).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Write upload once per unique file bytes; reuse existing file when present.
|
|
14
|
+
*/
|
|
15
|
+
export async function storeUploadIfNeeded(uploadsDir, body, safeName) {
|
|
16
|
+
const contentHash = contentHashHex(body);
|
|
17
|
+
const storedName = buildContentAddressedName(body, safeName);
|
|
18
|
+
const filePath = path.join(uploadsDir, storedName);
|
|
19
|
+
try {
|
|
20
|
+
const stat = await fs.stat(filePath);
|
|
21
|
+
if (stat.size === body.length) {
|
|
22
|
+
return { storedName, deduplicated: true, contentHash };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// missing — write below
|
|
27
|
+
}
|
|
28
|
+
await fs.writeFile(filePath, body);
|
|
29
|
+
return { storedName, deduplicated: false, contentHash };
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomorrowos/sdk",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "TomorrowOS CMS server SDK - WebSocket transport, pairing, device commands, optional static CMS UI. Includes CLI (tomorrowos init / build) and starter templates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-cms",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "CMS server on @tomorrowos/sdk. Add your UI (React, static files, etc.) alongside this server.",
|
|
5
5
|
"private": true,
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"build-player": "tomorrowos build --platform tizen"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@tomorrowos/sdk": "^0.4.
|
|
13
|
+
"@tomorrowos/sdk": "^0.4.7"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@types/node": "^20.0.0",
|
|
@@ -18,4 +18,4 @@
|
|
|
18
18
|
"typescript": "^5.5.0"
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
|
|
@@ -273,14 +273,17 @@ function probeVideoDurationInBrowser(fileOrUrl) {
|
|
|
273
273
|
async function resolveVideoDurationMs(file, type, uploadData) {
|
|
274
274
|
if (type !== "video") return defaultDurationMs(type);
|
|
275
275
|
|
|
276
|
-
|
|
277
|
-
if (Number.isFinite(fromServer) && fromServer > 0) {
|
|
278
|
-
return clampVideoDurationMs(fromServer) ?? defaultDurationMs(type);
|
|
279
|
-
}
|
|
280
|
-
|
|
276
|
+
// Local File metadata is the most reliable source during upload.
|
|
281
277
|
const fromFile = await probeVideoDurationInBrowser(file);
|
|
282
278
|
if (fromFile) return fromFile;
|
|
283
279
|
|
|
280
|
+
const fromServerRaw = Number(uploadData?.durationMs);
|
|
281
|
+
const fromServer =
|
|
282
|
+
Number.isFinite(fromServerRaw) && fromServerRaw > 0
|
|
283
|
+
? clampVideoDurationMs(fromServerRaw)
|
|
284
|
+
: null;
|
|
285
|
+
if (fromServer) return fromServer;
|
|
286
|
+
|
|
284
287
|
if (uploadData?.url) {
|
|
285
288
|
try {
|
|
286
289
|
const fromUrl = await probeVideoDurationInBrowser(absoluteMediaUrl(uploadData.url));
|