@tomorrowos/sdk 0.3.10 → 0.4.1

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.
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Best-effort video duration from file bytes (no ffprobe).
3
+ * Supports common MP4/MOV (mvhd) and WebM (Duration element).
4
+ */
5
+ export declare function probeVideoDurationMs(body: Buffer, filename: string): number | null;
6
+ //# sourceMappingURL=media-probe.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Best-effort video duration from file bytes (no ffprobe).
3
+ * Supports common MP4/MOV (mvhd) and WebM (Duration element).
4
+ */
5
+ function isVideoFilename(name) {
6
+ const lower = String(name || "").toLowerCase();
7
+ return /\.(mp4|m4v|mov|webm|mkv|avi)$/.test(lower);
8
+ }
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)
14
+ break;
15
+ const boxStart = idx - 4;
16
+ if (boxStart < 0) {
17
+ searchFrom = idx + 4;
18
+ continue;
19
+ }
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
+ }
27
+ }
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
+ }
34
+ }
35
+ searchFrom = idx + 4;
36
+ }
37
+ return null;
38
+ }
39
+ function probeWebmDurationMs(buf) {
40
+ const marker = Buffer.from([0x44, 0x89]);
41
+ let searchFrom = 0;
42
+ while (searchFrom < buf.length - 12) {
43
+ const idx = buf.indexOf(marker, searchFrom);
44
+ if (idx < 0)
45
+ break;
46
+ const sizePos = idx + 2;
47
+ if (sizePos + 9 > buf.length)
48
+ break;
49
+ const size = buf[sizePos];
50
+ if (size === 0x84 && sizePos + 9 <= buf.length) {
51
+ const duration = buf.readFloatBE(sizePos + 1);
52
+ if (Number.isFinite(duration) && duration > 0) {
53
+ return clampDurationMs(duration);
54
+ }
55
+ }
56
+ searchFrom = idx + 2;
57
+ }
58
+ return null;
59
+ }
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
+ export function probeVideoDurationMs(body, filename) {
66
+ if (!body?.length || !isVideoFilename(filename))
67
+ return null;
68
+ const lower = filename.toLowerCase();
69
+ if (lower.endsWith(".webm")) {
70
+ return probeWebmDurationMs(body) ?? probeMp4MvhdDurationMs(body);
71
+ }
72
+ return probeMp4MvhdDurationMs(body) ?? probeWebmDurationMs(body);
73
+ }
@@ -30,6 +30,8 @@ export declare class PlaylistCatalog {
30
30
  getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
31
31
  publishPlaylistsToDevice(deviceId: string, playlistIds: string[], options?: PolicyBuildOptions): Promise<BuiltDevicePolicy>;
32
32
  removePlaylistFromDevice(deviceId: string, playlistId: string): Promise<BuiltDevicePolicy>;
33
+ /** Remove every playlist assignment from a device (CMS catalog + empty policy). */
34
+ clearAllAssignmentsFromDevice(deviceId: string): Promise<BuiltDevicePolicy>;
33
35
  buildPolicyForDevice(deviceId: string, options?: PolicyBuildOptions): Promise<BuiltDevicePolicy>;
34
36
  private buildPolicyFromAssignments;
35
37
  canPublishPlaylistToNewDevice(playlistId: string): Promise<boolean>;
@@ -1 +1 @@
1
- {"version":3,"file":"playlist-catalog.d.ts","sourceRoot":"","sources":["../src/playlist-catalog.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,wBAAwB,EACxB,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,KAAK,EAAE,kBAAkB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE;QACN,SAAS,EAAE,yBAAyB,EAAE,CAAC;QACvC,QAAQ,EAAE;YAAE,IAAI,EAAE,OAAO,CAAA;SAAE,CAAC;QAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;KAClC,CAAC;CACH;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA+CD,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,eAAe;IAE7C,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAK1C,6BAA6B,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAI1D,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAI5D,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC;IAgC/D,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAcnD,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAIhC,wBAAwB,CAC5B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EAAE,EACrB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC;IA2CvB,wBAAwB,CAC5B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,iBAAiB,CAAC;IAOvB,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC;YAKf,0BAA0B;IAgCxC,6BAA6B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAGpE"}
1
+ {"version":3,"file":"playlist-catalog.d.ts","sourceRoot":"","sources":["../src/playlist-catalog.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,wBAAwB,EACxB,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,KAAK,EAAE,kBAAkB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE;QACN,SAAS,EAAE,yBAAyB,EAAE,CAAC;QACvC,QAAQ,EAAE;YAAE,IAAI,EAAE,OAAO,CAAA;SAAE,CAAC;QAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;KAClC,CAAC;CACH;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA+CD,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,eAAe;IAE7C,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAK1C,6BAA6B,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAI1D,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAI5D,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC;IAgC/D,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAcnD,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAIhC,wBAAwB,CAC5B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EAAE,EACrB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC;IA2CvB,wBAAwB,CAC5B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,iBAAiB,CAAC;IAO7B,mFAAmF;IAC7E,6BAA6B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAK3E,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC;YAKf,0BAA0B;IAgCxC,6BAA6B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAGpE"}
@@ -143,6 +143,11 @@ export class PlaylistCatalog {
143
143
  await this.store.setDeviceAssignments(deviceId, next);
144
144
  return this.buildPolicyFromAssignments(next, { useLatest: false });
145
145
  }
146
+ /** Remove every playlist assignment from a device (CMS catalog + empty policy). */
147
+ async clearAllAssignmentsFromDevice(deviceId) {
148
+ await this.store.setDeviceAssignments(deviceId, []);
149
+ return this.buildPolicyFromAssignments([], { useLatest: false });
150
+ }
146
151
  async buildPolicyForDevice(deviceId, options = {}) {
147
152
  const assignments = await this.store.getDeviceAssignments(deviceId);
148
153
  return this.buildPolicyFromAssignments(assignments, options);
@@ -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":"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"}
@@ -8,6 +8,7 @@ import { generateRandomPairingCode, isValidPairingCodeFormat, normalizePairingCo
8
8
  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
+ import { probeVideoDurationMs } from "./media-probe.js";
11
12
  function formatDurationMs(ms) {
12
13
  if (!Number.isFinite(ms) || ms < 0)
13
14
  return "0s";
@@ -548,12 +549,16 @@ export class TomorrowOS extends EventEmitter {
548
549
  await fs.mkdir(uploadsDir, { recursive: true });
549
550
  const filePath = path.join(uploadsDir, storedName);
550
551
  await fs.writeFile(filePath, body);
551
- sendJson(res, 200, {
552
+ const durationMs = probeVideoDurationMs(body, safeName);
553
+ const payload = {
552
554
  status: "success",
553
555
  url: `/uploads/${storedName}`,
554
556
  filename: storedName,
555
557
  size: body.length
556
- });
558
+ };
559
+ if (durationMs != null)
560
+ payload.durationMs = durationMs;
561
+ sendJson(res, 200, payload);
557
562
  }
558
563
  catch (e) {
559
564
  const msg = e instanceof Error ? e.message : "Upload failed";
@@ -705,6 +710,33 @@ export class TomorrowOS extends EventEmitter {
705
710
  sendJson(res, 200, { status: "success", assignments });
706
711
  return;
707
712
  }
713
+ if (req.method === "DELETE" && deviceAssignmentsGet) {
714
+ const deviceId = decodeURIComponent(deviceAssignmentsGet[1]);
715
+ try {
716
+ const built = await this.playlists.clearAllAssignmentsFromDevice(deviceId);
717
+ const ws = this.devices.get(deviceId);
718
+ let policyPushed = false;
719
+ let policyResult = null;
720
+ if (ws && ws.readyState === WebSocket.OPEN) {
721
+ policyResult = await this.sendDeviceCommand(deviceId, "device.content.setPolicy", { policy: built.policy });
722
+ policyPushed = policyResult.status === "success";
723
+ if (policyPushed)
724
+ await this.recordPolicyPush(deviceId);
725
+ }
726
+ sendJson(res, 200, {
727
+ status: "success",
728
+ assignmentsCleared: true,
729
+ policyPushed,
730
+ policy: built.policy,
731
+ result: policyResult
732
+ });
733
+ }
734
+ catch (e) {
735
+ const msg = e instanceof Error ? e.message : "Clear assignments failed";
736
+ sendJson(res, 400, { status: "failed", error: msg });
737
+ }
738
+ return;
739
+ }
708
740
  const deviceAssignmentsPost = /^\/device\/([^/]+)\/assignments$/.exec(pathname);
709
741
  if (req.method === "POST" && deviceAssignmentsPost) {
710
742
  const deviceId = decodeURIComponent(deviceAssignmentsPost[1]);
@@ -834,6 +866,26 @@ export class TomorrowOS extends EventEmitter {
834
866
  (result.status === "success" || result.status === "accepted")) {
835
867
  this.forceDeviceOffline(deviceId);
836
868
  }
869
+ if (action === "content/clear" &&
870
+ (result.status === "success" || result.status === "accepted")) {
871
+ const built = await this.playlists.clearAllAssignmentsFromDevice(deviceId);
872
+ let assignmentsCleared = true;
873
+ let policyPushed = false;
874
+ if (ws.readyState === WebSocket.OPEN) {
875
+ const policyResult = await this.sendDeviceCommand(deviceId, "device.content.setPolicy", { policy: built.policy });
876
+ policyPushed = policyResult.status === "success";
877
+ if (policyPushed)
878
+ await this.recordPolicyPush(deviceId);
879
+ }
880
+ sendJson(res, 200, {
881
+ status: result.status,
882
+ data: result.data,
883
+ assignmentsCleared,
884
+ policyPushed,
885
+ policy: built.policy
886
+ });
887
+ return;
888
+ }
837
889
  sendJson(res, 200, { status: result.status, data: result.data });
838
890
  }
839
891
  catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomorrowos/sdk",
3
- "version": "0.3.10",
3
+ "version": "0.4.1",
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.3.10",
3
+ "version": "0.4.1",
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.3.10"
13
+ "@tomorrowos/sdk": "^0.4.1"
14
14
  },
15
15
  "devDependencies": {
16
16
  "@types/node": "^20.0.0",
@@ -168,6 +168,96 @@ function defaultDurationMs(type) {
168
168
  return 10000;
169
169
  }
170
170
 
171
+ function clampVideoDurationMs(ms) {
172
+ if (!Number.isFinite(ms) || ms <= 0) return null;
173
+ return Math.min(3600 * 1000, Math.max(1000, Math.round(ms)));
174
+ }
175
+
176
+ /** Probe duration from a local File or an absolute media URL. */
177
+ function probeVideoDurationInBrowser(fileOrUrl) {
178
+ if (!fileOrUrl) return Promise.resolve(null);
179
+
180
+ let objectUrl = null;
181
+ const src = typeof fileOrUrl === "string" ? fileOrUrl : null;
182
+ if (!src && fileOrUrl instanceof File) {
183
+ objectUrl = URL.createObjectURL(fileOrUrl);
184
+ }
185
+
186
+ return new Promise((resolve) => {
187
+ const video = document.createElement("video");
188
+ let settled = false;
189
+ const timer = setTimeout(() => done(null), 20000);
190
+
191
+ const cleanup = () => {
192
+ clearTimeout(timer);
193
+ video.removeAttribute("src");
194
+ try {
195
+ video.load();
196
+ } catch (_) {}
197
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
198
+ };
199
+
200
+ const done = (value) => {
201
+ if (settled) return;
202
+ settled = true;
203
+ cleanup();
204
+ resolve(clampVideoDurationMs(value));
205
+ };
206
+
207
+ const readDuration = () => {
208
+ const seconds = Number(video.duration);
209
+ if (Number.isFinite(seconds) && seconds > 0 && seconds !== Infinity) {
210
+ done(seconds * 1000);
211
+ }
212
+ };
213
+
214
+ video.preload = "auto";
215
+ video.muted = true;
216
+ video.playsInline = true;
217
+ video.setAttribute("playsinline", "");
218
+ video.addEventListener("loadedmetadata", readDuration);
219
+ video.addEventListener("durationchange", readDuration);
220
+ video.addEventListener("loadeddata", readDuration);
221
+ video.addEventListener("canplay", readDuration);
222
+ video.addEventListener("error", () => done(null));
223
+
224
+ if (src) {
225
+ video.src = src;
226
+ } else if (objectUrl) {
227
+ video.src = objectUrl;
228
+ } else {
229
+ done(null);
230
+ return;
231
+ }
232
+ try {
233
+ video.load();
234
+ } catch (_) {
235
+ done(null);
236
+ }
237
+ });
238
+ }
239
+
240
+ async function resolveVideoDurationMs(file, type, uploadData) {
241
+ if (type !== "video") return defaultDurationMs(type);
242
+
243
+ const fromServer = Number(uploadData?.durationMs);
244
+ if (Number.isFinite(fromServer) && fromServer > 0) {
245
+ return clampVideoDurationMs(fromServer) ?? defaultDurationMs(type);
246
+ }
247
+
248
+ const fromFile = await probeVideoDurationInBrowser(file);
249
+ if (fromFile) return fromFile;
250
+
251
+ if (uploadData?.url) {
252
+ try {
253
+ const fromUrl = await probeVideoDurationInBrowser(absoluteMediaUrl(uploadData.url));
254
+ if (fromUrl) return fromUrl;
255
+ } catch (_) {}
256
+ }
257
+
258
+ return defaultDurationMs(type);
259
+ }
260
+
171
261
  function normalizeDurationMs(item) {
172
262
  const minMs = 1000;
173
263
  const maxMs = 3600 * 1000;
@@ -766,17 +856,23 @@ async function addAssetFromFile(file) {
766
856
  if (!isPlaylistEditorOpen()) {
767
857
  newPlaylistDraft();
768
858
  }
769
- const data = await uploadFile(file);
770
859
  const type = inferMediaType(file.name, file.type);
860
+ const data = await uploadFile(file);
861
+ const durationMs = await resolveVideoDurationMs(file, type, data);
771
862
  editorItems.push({
772
863
  id: crypto.randomUUID(),
773
864
  url: data.url,
774
865
  name: file.name,
775
866
  type,
776
- durationMs: defaultDurationMs(type)
867
+ durationMs
777
868
  });
778
869
  renderEditorAssets();
779
- showResult({ status: "uploaded", ...data });
870
+ showResult({
871
+ status: "uploaded",
872
+ ...data,
873
+ detectedDurationMs: durationMs,
874
+ detectedDurationSec: Math.round(durationMs / 1000)
875
+ });
780
876
  }
781
877
 
782
878
  async function verify() {
@@ -816,13 +912,25 @@ async function unpairDevice(deviceId) {
816
912
  async function deviceAction(deviceId, action) {
817
913
  if (!deviceId) return;
818
914
  if (action === "reboot" && !confirm("Reboot this device?")) return;
819
- if (action === "content/clear" && !confirm("Clear content on this device?")) return;
915
+ if (
916
+ action === "content/clear" &&
917
+ !confirm("Clear content on this device and remove all published playlists?")
918
+ ) {
919
+ return;
920
+ }
820
921
  const res = await fetch(`/device/${encodeURIComponent(deviceId)}/${action}`, {
821
922
  method: "POST"
822
923
  });
823
924
  const data = await res.json();
824
925
  showResult({ deviceId, action, ...data });
825
- if (action === "reboot" && res.ok) await fetchDevices();
926
+ if (!res.ok) {
927
+ alert(data.error || `${action} failed`);
928
+ return;
929
+ }
930
+ if (action === "content/clear" && data.assignmentsCleared !== true) {
931
+ alert("Content cleared on device, but playlist assignments were not removed. Restart CMS with latest SDK.");
932
+ }
933
+ if (action === "reboot" || action === "content/clear") await fetchDevices();
826
934
  }
827
935
 
828
936
  function startDevicePolling() {