@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.
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Best-effort video duration from file bytes (no ffprobe).
3
- * Supports common MP4/MOV (mvhd) and WebM (Duration element).
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;;;GAGG;AA+DH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,GACf,MAAM,GAAG,IAAI,CAOf"}
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"}
@@ -1,54 +1,133 @@
1
1
  /**
2
- * Best-effort video duration from file bytes (no ffprobe).
3
- * Supports common MP4/MOV (mvhd) and WebM (Duration element).
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 probeMp4MvhdDurationMs(buf) {
10
- let searchFrom = 0;
11
- while (searchFrom < buf.length - 32) {
12
- const idx = buf.indexOf("mvhd", searchFrom, "utf8");
13
- if (idx < 8)
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
- const boxStart = idx - 4;
16
- if (boxStart < 0) {
17
- searchFrom = idx + 4;
18
- continue;
81
+ if (box.type === "mvhd") {
82
+ const ms = parseMvhdDurationMs(buf, box.contentStart, box.end);
83
+ if (ms != null)
84
+ return ms;
19
85
  }
20
- const version = buf[boxStart + 8];
21
- if (version === 0 && boxStart + 28 <= buf.length) {
22
- const timescale = buf.readUInt32BE(boxStart + 20);
23
- const duration = buf.readUInt32BE(boxStart + 24);
24
- if (timescale > 0 && duration > 0) {
25
- return clampDurationMs((duration / timescale) * 1000);
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
- if (version === 1 && boxStart + 40 <= buf.length) {
29
- const timescale = buf.readUInt32BE(boxStart + 28);
30
- const duration = buf.readUInt32BE(boxStart + 32) * 2 ** 32 + buf.readUInt32BE(boxStart + 36);
31
- if (timescale > 0 && duration > 0) {
32
- return clampDurationMs((duration / timescale) * 1000);
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
- searchFrom = idx + 4;
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 < buf.length - 12) {
43
- const idx = buf.indexOf(marker, searchFrom);
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 > buf.length)
126
+ if (sizePos + 9 > header.length)
48
127
  break;
49
- const size = buf[sizePos];
50
- if (size === 0x84 && sizePos + 9 <= buf.length) {
51
- const duration = buf.readFloatBE(sizePos + 1);
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) ?? probeMp4MvhdDurationMs(body);
144
+ return probeWebmDurationMs(body) ?? probeMp4DurationMs(body);
71
145
  }
72
- return probeMp4MvhdDurationMs(body) ?? probeWebmDurationMs(body);
146
+ return probeMp4DurationMs(body) ?? probeWebmDurationMs(body);
73
147
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
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;AAS1B,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,eAAe,CAAC;IACvB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,eAAe,CAAC;CACzB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAwBD,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mEAAmE;IACnE,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,kBAAkB,EAAE,KAAK,CAAC;QACxB,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC,CAAC;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAyID,qBAAa,UAAW,SAAQ,YAAY;IAC1C,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAsC;IACxE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAuC;IAClE,OAAO,CAAC,UAAU,CAA4B;IAC9C,OAAO,CAAC,GAAG,CAAgC;IAC3C,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,eAAe,CAAgB;gBAE3B,OAAO,EAAE,iBAAiB;IAOtC,yFAAyF;IACnF,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QACxD,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,CAAC,EAAE,iBAAiB,CAAC,QAAQ,CAAC,CAAC;KACtC,CAAC;YAkBY,iBAAiB;IAY/B,6EAA6E;IACvE,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAgEhE,6EAA6E;IACvE,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC;IA4BvF,OAAO;uBACU,MAAM;sBA9FgC,MAAM;;2BA+FxC,MAAM;sBA9BgC,MAAM;sBAAY,OAAO;;MA+BlF;IAEF,kFAAkF;IAC5E,WAAW,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAsD9C,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,gBAAgB;YAeV,+BAA+B;YAiC/B,iBAAiB;YASjB,iBAAiB;YA4BjB,kBAAkB;IAUhC,uFAAuF;IACvF,OAAO,CAAC,kBAAkB;YAmBZ,gBAAgB;YAMhB,uBAAuB;IAoCrC,MAAM,CAAC,QAAQ,EAAE,MAAM;oBAGD,CAAC,oBACT,MAAM,WACN,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;;IAU5E,OAAO,CAAC,mBAAmB;IA6D3B,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAAC,MAAM;YAmD7B,iBAAiB;YA6CjB,wBAAwB;YAiBxB,cAAc;YAiDd,UAAU;YAiRV,gBAAgB;IAqG9B,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,gBAAgB;CA4HzB"}
@@ -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 filePath = path.join(uploadsDir, storedName);
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.5",
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.5",
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.5"
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
- const fromServer = Number(uploadData?.durationMs);
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));