@tomorrowos/sdk 0.4.0 → 0.4.2

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);
@@ -52,6 +52,15 @@ export interface DeviceListItem {
52
52
  version: number;
53
53
  publishedAt: string;
54
54
  }>;
55
+ latestErrorAt: string | null;
56
+ latestErrorMessage: string | null;
57
+ }
58
+ export interface DeviceLogEntry {
59
+ timestamp: string;
60
+ level: "info" | "warn" | "error";
61
+ message: string;
62
+ source?: string;
63
+ details?: unknown;
55
64
  }
56
65
  export declare class TomorrowOS extends EventEmitter {
57
66
  readonly brand: TomorrowOSBrand;
@@ -59,6 +68,7 @@ export declare class TomorrowOS extends EventEmitter {
59
68
  readonly playlists: PlaylistCatalog;
60
69
  private readonly devices;
61
70
  private readonly pendingDeviceMeta;
71
+ private readonly deviceLogs;
62
72
  private httpServer;
63
73
  private wss;
64
74
  private staticRoot;
@@ -90,6 +100,8 @@ export declare class TomorrowOS extends EventEmitter {
90
100
  };
91
101
  /** List paired devices with live connection + timing fields for the CMS panel. */
92
102
  listDevices(): Promise<DeviceListItem[]>;
103
+ private pushDeviceLog;
104
+ private getDeviceLogs;
93
105
  private isDeviceConnected;
94
106
  private captureHelloMeta;
95
107
  private getOrCreatePermanentPairingCode;
@@ -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";
@@ -131,6 +132,7 @@ export class TomorrowOS extends EventEmitter {
131
132
  playlists;
132
133
  devices = new Map();
133
134
  pendingDeviceMeta = new Map();
135
+ deviceLogs = new Map();
134
136
  httpServer = null;
135
137
  wss = null;
136
138
  staticRoot = null;
@@ -266,6 +268,8 @@ export class TomorrowOS extends EventEmitter {
266
268
  }
267
269
  }
268
270
  const screenOnlineSince = connected && record.lastBootAt ? record.lastBootAt : null;
271
+ const logs = this.deviceLogs.get(deviceId) ?? [];
272
+ const latestError = [...logs].reverse().find((entry) => entry.level === "error");
269
273
  return {
270
274
  deviceId,
271
275
  pairingCode: reg?.permanentPairingCode ?? null,
@@ -287,10 +291,28 @@ export class TomorrowOS extends EventEmitter {
287
291
  lastPolicyPushAt: record.lastPolicyPushAt ?? null,
288
292
  screenOnlineActive,
289
293
  screenOnlineLabel,
290
- screenOnlineSince
294
+ screenOnlineSince,
295
+ latestErrorAt: latestError?.timestamp ?? null,
296
+ latestErrorMessage: latestError?.message ?? null
291
297
  };
292
298
  }));
293
299
  }
300
+ pushDeviceLog(deviceId, entry) {
301
+ const key = String(deviceId || "").trim();
302
+ if (!key)
303
+ return;
304
+ const existing = this.deviceLogs.get(key) ?? [];
305
+ existing.push(entry);
306
+ if (existing.length > 80)
307
+ existing.splice(0, existing.length - 80);
308
+ this.deviceLogs.set(key, existing);
309
+ }
310
+ getDeviceLogs(deviceId) {
311
+ const key = String(deviceId || "").trim();
312
+ if (!key)
313
+ return [];
314
+ return [...(this.deviceLogs.get(key) ?? [])].reverse();
315
+ }
294
316
  isDeviceConnected(deviceId) {
295
317
  const ws = this.devices.get(deviceId);
296
318
  return !!ws && ws.readyState === WebSocket.OPEN;
@@ -548,12 +570,16 @@ export class TomorrowOS extends EventEmitter {
548
570
  await fs.mkdir(uploadsDir, { recursive: true });
549
571
  const filePath = path.join(uploadsDir, storedName);
550
572
  await fs.writeFile(filePath, body);
551
- sendJson(res, 200, {
573
+ const durationMs = probeVideoDurationMs(body, safeName);
574
+ const payload = {
552
575
  status: "success",
553
576
  url: `/uploads/${storedName}`,
554
577
  filename: storedName,
555
578
  size: body.length
556
- });
579
+ };
580
+ if (durationMs != null)
581
+ payload.durationMs = durationMs;
582
+ sendJson(res, 200, payload);
557
583
  }
558
584
  catch (e) {
559
585
  const msg = e instanceof Error ? e.message : "Upload failed";
@@ -705,6 +731,39 @@ export class TomorrowOS extends EventEmitter {
705
731
  sendJson(res, 200, { status: "success", assignments });
706
732
  return;
707
733
  }
734
+ const deviceLogsGet = /^\/device\/([^/]+)\/logs$/.exec(pathname);
735
+ if (req.method === "GET" && deviceLogsGet) {
736
+ const deviceId = decodeURIComponent(deviceLogsGet[1]);
737
+ sendJson(res, 200, { status: "success", logs: this.getDeviceLogs(deviceId) });
738
+ return;
739
+ }
740
+ if (req.method === "DELETE" && deviceAssignmentsGet) {
741
+ const deviceId = decodeURIComponent(deviceAssignmentsGet[1]);
742
+ try {
743
+ const built = await this.playlists.clearAllAssignmentsFromDevice(deviceId);
744
+ const ws = this.devices.get(deviceId);
745
+ let policyPushed = false;
746
+ let policyResult = null;
747
+ if (ws && ws.readyState === WebSocket.OPEN) {
748
+ policyResult = await this.sendDeviceCommand(deviceId, "device.content.setPolicy", { policy: built.policy });
749
+ policyPushed = policyResult.status === "success";
750
+ if (policyPushed)
751
+ await this.recordPolicyPush(deviceId);
752
+ }
753
+ sendJson(res, 200, {
754
+ status: "success",
755
+ assignmentsCleared: true,
756
+ policyPushed,
757
+ policy: built.policy,
758
+ result: policyResult
759
+ });
760
+ }
761
+ catch (e) {
762
+ const msg = e instanceof Error ? e.message : "Clear assignments failed";
763
+ sendJson(res, 400, { status: "failed", error: msg });
764
+ }
765
+ return;
766
+ }
708
767
  const deviceAssignmentsPost = /^\/device\/([^/]+)\/assignments$/.exec(pathname);
709
768
  if (req.method === "POST" && deviceAssignmentsPost) {
710
769
  const deviceId = decodeURIComponent(deviceAssignmentsPost[1]);
@@ -834,6 +893,26 @@ export class TomorrowOS extends EventEmitter {
834
893
  (result.status === "success" || result.status === "accepted")) {
835
894
  this.forceDeviceOffline(deviceId);
836
895
  }
896
+ if (action === "content/clear" &&
897
+ (result.status === "success" || result.status === "accepted")) {
898
+ const built = await this.playlists.clearAllAssignmentsFromDevice(deviceId);
899
+ let assignmentsCleared = true;
900
+ let policyPushed = false;
901
+ if (ws.readyState === WebSocket.OPEN) {
902
+ const policyResult = await this.sendDeviceCommand(deviceId, "device.content.setPolicy", { policy: built.policy });
903
+ policyPushed = policyResult.status === "success";
904
+ if (policyPushed)
905
+ await this.recordPolicyPush(deviceId);
906
+ }
907
+ sendJson(res, 200, {
908
+ status: result.status,
909
+ data: result.data,
910
+ assignmentsCleared,
911
+ policyPushed,
912
+ policy: built.policy
913
+ });
914
+ return;
915
+ }
837
916
  sendJson(res, 200, { status: result.status, data: result.data });
838
917
  }
839
918
  catch (e) {
@@ -919,6 +998,26 @@ export class TomorrowOS extends EventEmitter {
919
998
  console.error("[TomorrowOS] pushLatestPolicy on resume failed:", err);
920
999
  });
921
1000
  })();
1001
+ return;
1002
+ }
1003
+ if (type === "device.log") {
1004
+ const deviceId = ws.deviceId;
1005
+ if (!deviceId)
1006
+ return;
1007
+ const rawLevel = String(msg.level ?? "info").toLowerCase();
1008
+ const level = rawLevel === "error" || rawLevel === "warn" ? rawLevel : "info";
1009
+ const message = String(msg.message ?? "").trim();
1010
+ if (!message)
1011
+ return;
1012
+ const source = typeof msg.source === "string" ? msg.source : undefined;
1013
+ const details = msg.details;
1014
+ const timestamp = typeof msg.timestamp === "string" && msg.timestamp.trim()
1015
+ ? msg.timestamp
1016
+ : new Date().toISOString();
1017
+ this.pushDeviceLog(deviceId, { timestamp, level, message, source, details });
1018
+ if (level === "error") {
1019
+ console.error(`[TomorrowOS][device-log][${deviceId}] ${message}`);
1020
+ }
922
1021
  }
923
1022
  });
924
1023
  ws.on("close", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomorrowos/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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.0",
3
+ "version": "0.4.2",
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.0"
13
+ "@tomorrowos/sdk": "^0.4.2"
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;
@@ -567,7 +657,9 @@ function renderDeviceCards() {
567
657
  ["System", device.system || device.platform || "—"],
568
658
  ["Device online", formatDeviceOnlineLabel(device)],
569
659
  ["Last boot", formatDateTimeSeconds(device.lastBootAt)],
570
- ["Latest push", formatDateTimeSeconds(device.lastPolicyPushAt)]
660
+ ["Latest push", formatDateTimeSeconds(device.lastPolicyPushAt)],
661
+ ["Latest error", device.latestErrorMessage || "—"],
662
+ ["Error at", formatDateTimeSeconds(device.latestErrorAt)]
571
663
  ];
572
664
  for (const [label, value] of rows) {
573
665
  const row = document.createElement("div");
@@ -612,6 +704,11 @@ function renderDeviceCards() {
612
704
  clearBtn.textContent = "Clear";
613
705
  clearBtn.addEventListener("click", () => deviceAction(device.deviceId, "content/clear"));
614
706
 
707
+ const logsBtn = document.createElement("button");
708
+ logsBtn.type = "button";
709
+ logsBtn.textContent = "Logs";
710
+ logsBtn.addEventListener("click", () => viewDeviceLogs(device.deviceId));
711
+
615
712
  const unpairBtn = document.createElement("button");
616
713
  unpairBtn.type = "button";
617
714
  unpairBtn.className = "danger";
@@ -623,6 +720,7 @@ function renderDeviceCards() {
623
720
  actions.appendChild(capBtn);
624
721
  actions.appendChild(rebootBtn);
625
722
  actions.appendChild(clearBtn);
723
+ actions.appendChild(logsBtn);
626
724
  actions.appendChild(unpairBtn);
627
725
 
628
726
  card.appendChild(header);
@@ -766,17 +864,23 @@ async function addAssetFromFile(file) {
766
864
  if (!isPlaylistEditorOpen()) {
767
865
  newPlaylistDraft();
768
866
  }
769
- const data = await uploadFile(file);
770
867
  const type = inferMediaType(file.name, file.type);
868
+ const data = await uploadFile(file);
869
+ const durationMs = await resolveVideoDurationMs(file, type, data);
771
870
  editorItems.push({
772
871
  id: crypto.randomUUID(),
773
872
  url: data.url,
774
873
  name: file.name,
775
874
  type,
776
- durationMs: defaultDurationMs(type)
875
+ durationMs
777
876
  });
778
877
  renderEditorAssets();
779
- showResult({ status: "uploaded", ...data });
878
+ showResult({
879
+ status: "uploaded",
880
+ ...data,
881
+ detectedDurationMs: durationMs,
882
+ detectedDurationSec: Math.round(durationMs / 1000)
883
+ });
780
884
  }
781
885
 
782
886
  async function verify() {
@@ -816,13 +920,36 @@ async function unpairDevice(deviceId) {
816
920
  async function deviceAction(deviceId, action) {
817
921
  if (!deviceId) return;
818
922
  if (action === "reboot" && !confirm("Reboot this device?")) return;
819
- if (action === "content/clear" && !confirm("Clear content on this device?")) return;
923
+ if (
924
+ action === "content/clear" &&
925
+ !confirm("Clear content on this device and remove all published playlists?")
926
+ ) {
927
+ return;
928
+ }
820
929
  const res = await fetch(`/device/${encodeURIComponent(deviceId)}/${action}`, {
821
930
  method: "POST"
822
931
  });
823
932
  const data = await res.json();
824
933
  showResult({ deviceId, action, ...data });
825
- if (action === "reboot" && res.ok) await fetchDevices();
934
+ if (!res.ok) {
935
+ alert(data.error || `${action} failed`);
936
+ return;
937
+ }
938
+ if (action === "content/clear" && data.assignmentsCleared !== true) {
939
+ alert("Content cleared on device, but playlist assignments were not removed. Restart CMS with latest SDK.");
940
+ }
941
+ if (action === "reboot" || action === "content/clear") await fetchDevices();
942
+ }
943
+
944
+ async function viewDeviceLogs(deviceId) {
945
+ if (!deviceId) return;
946
+ const res = await fetch(`/device/${encodeURIComponent(deviceId)}/logs`);
947
+ const data = await res.json();
948
+ showResult({ deviceId, logs: data.logs || [] });
949
+ if (!res.ok) {
950
+ alert(data.error || "Failed to load logs");
951
+ return;
952
+ }
826
953
  }
827
954
 
828
955
  function startDevicePolling() {