@tomorrowos/sdk 0.4.4 → 0.4.6

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
  }
@@ -34,11 +34,15 @@ export interface PlaylistItemRecord {
34
34
  type?: string;
35
35
  durationMs?: number;
36
36
  }
37
+ /** Device-local run window: startDate+start → endDate+end (one continuous period, not daily repeat). */
37
38
  export interface PlaylistSchedule {
38
39
  startDate?: string;
39
40
  endDate?: string;
41
+ /** @deprecated Ignored by player; kept for backward compatibility in stored JSON. */
40
42
  daysOfWeek?: number[];
43
+ /** HH:mm — combined with startDate for run-from datetime. */
41
44
  start?: string;
45
+ /** HH:mm — combined with endDate for run-until datetime (exclusive). */
42
46
  end?: string;
43
47
  }
44
48
  export interface StoredPlaylist {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tomorrowos/sdk",
3
- "version": "0.4.4",
4
- "description": "TomorrowOS CMS server SDK WebSocket transport, pairing, device commands, optional static CMS UI. Includes CLI (tomorrowos init / build) and starter templates.",
3
+ "version": "0.4.6",
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",
7
7
  "types": "./dist/index.d.ts",
@@ -41,3 +41,4 @@
41
41
  },
42
42
  "license": "Apache-2.0"
43
43
  }
44
+
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-cms",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
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.4"
13
+ "@tomorrowos/sdk": "^0.4.6"
14
14
  },
15
15
  "devDependencies": {
16
16
  "@types/node": "^20.0.0",
@@ -18,3 +18,4 @@
18
18
  "typescript": "^5.5.0"
19
19
  }
20
20
  }
21
+
@@ -66,35 +66,27 @@
66
66
  <h3 class="subheading">When this playlist plays</h3>
67
67
  <p class="hint">
68
68
  Leave blank for always on. Uses the <strong>TV/device local clock</strong> (not CMS server time).
69
- “Until” includes that minute (e.g. Until 18:00 plays through 18:00).
69
+ Schedule is one continuous run: <strong>from date/time until date/time</strong>
70
+ (e.g. May 5 13:00 through May 8 14:00), not a daily repeat inside those dates.
71
+ “Until” time stops at that moment (exclusive).
70
72
  </p>
71
73
  <div class="schedule-grid">
72
74
  <label>
73
- Start date
75
+ Run from (date)
74
76
  <input type="date" id="scheduleStartDate" />
75
77
  </label>
76
78
  <label>
77
- End date
78
- <input type="date" id="scheduleEndDate" />
79
+ Run from (time)
80
+ <input type="time" id="scheduleStartTime" />
79
81
  </label>
80
82
  <label>
81
- From
82
- <input type="time" id="scheduleStartTime" />
83
+ Run until (date)
84
+ <input type="date" id="scheduleEndDate" />
83
85
  </label>
84
86
  <label>
85
- Until
87
+ Run until (time)
86
88
  <input type="time" id="scheduleEndTime" />
87
89
  </label>
88
- <div class="days-row">
89
- <span style="width: 100%; color: #444">Days</span>
90
- <label><input type="checkbox" class="day-checkbox" value="0" /> Sun</label>
91
- <label><input type="checkbox" class="day-checkbox" value="1" /> Mon</label>
92
- <label><input type="checkbox" class="day-checkbox" value="2" /> Tue</label>
93
- <label><input type="checkbox" class="day-checkbox" value="3" /> Wed</label>
94
- <label><input type="checkbox" class="day-checkbox" value="4" /> Thu</label>
95
- <label><input type="checkbox" class="day-checkbox" value="5" /> Fri</label>
96
- <label><input type="checkbox" class="day-checkbox" value="6" /> Sat</label>
97
- </div>
98
90
  </div>
99
91
 
100
92
  <div class="editor-actions">
@@ -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));
@@ -306,14 +309,10 @@ function buildScheduleFromForm() {
306
309
  const endDate = document.getElementById("scheduleEndDate")?.value?.trim();
307
310
  const startTime = document.getElementById("scheduleStartTime")?.value?.trim();
308
311
  const endTime = document.getElementById("scheduleEndTime")?.value?.trim();
309
- const days = [...document.querySelectorAll(".day-checkbox:checked")].map((el) =>
310
- Number(el.value)
311
- );
312
312
  if (startDate) schedule.startDate = startDate;
313
313
  if (endDate) schedule.endDate = endDate;
314
314
  if (startTime) schedule.start = startTime;
315
315
  if (endTime) schedule.end = endTime;
316
- if (days.length > 0) schedule.daysOfWeek = days;
317
316
  return Object.keys(schedule).length > 0 ? schedule : undefined;
318
317
  }
319
318
 
@@ -323,10 +322,6 @@ function loadScheduleIntoForm(schedule) {
323
322
  document.getElementById("scheduleEndDate").value = s.endDate || "";
324
323
  document.getElementById("scheduleStartTime").value = s.start || "";
325
324
  document.getElementById("scheduleEndTime").value = s.end || "";
326
- document.querySelectorAll(".day-checkbox").forEach((el) => {
327
- el.checked =
328
- Array.isArray(s.daysOfWeek) && s.daysOfWeek.includes(Number(el.value));
329
- });
330
325
  }
331
326
 
332
327
  function getSelectedPlaylist() {
@@ -492,11 +487,21 @@ function renderEditorAssets() {
492
487
  const actions = document.createElement("div");
493
488
  actions.className = "playlist-item-actions";
494
489
  const durInput = document.createElement("input");
490
+ const isVideo = item.type === "video";
495
491
  durInput.type = "number";
496
492
  durInput.min = "1";
497
493
  durInput.max = "3600";
498
494
  durInput.value = String(Math.round(item.durationMs / 1000));
495
+ if (isVideo) {
496
+ durInput.readOnly = true;
497
+ durInput.disabled = true;
498
+ durInput.title = "Video duration is auto-detected and cannot be edited.";
499
+ }
499
500
  durInput.addEventListener("change", () => {
501
+ if (isVideo) {
502
+ durInput.value = String(Math.round(item.durationMs / 1000));
503
+ return;
504
+ }
500
505
  item.durationMs = Math.min(3600, Math.max(1, Number(durInput.value) || 10)) * 1000;
501
506
  meta.textContent = `${item.type} · ${Math.round(item.durationMs / 1000)}s`;
502
507
  });