@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.
- package/dist/media-probe.d.ts +6 -0
- package/dist/media-probe.d.ts.map +1 -0
- package/dist/media-probe.js +73 -0
- package/dist/playlist-catalog.d.ts +2 -0
- package/dist/playlist-catalog.d.ts.map +1 -1
- package/dist/playlist-catalog.js +5 -0
- package/dist/tomorrowos.d.ts.map +1 -1
- package/dist/tomorrowos.js +54 -2
- package/package.json +1 -1
- package/templates/cms-starter/package.json +2 -2
- package/templates/cms-starter/public/methods.js +113 -5
|
@@ -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;
|
|
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"}
|
package/dist/playlist-catalog.js
CHANGED
|
@@ -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);
|
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
|
@@ -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
|
-
|
|
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
|
+
"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
|
+
"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.
|
|
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
|
|
867
|
+
durationMs
|
|
777
868
|
});
|
|
778
869
|
renderEditorAssets();
|
|
779
|
-
showResult({
|
|
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 (
|
|
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 (
|
|
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() {
|