@tomorrowos/sdk 0.4.5 → 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.
package/dist/media-probe.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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
|
|
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"}
|
package/dist/media-probe.js
CHANGED
|
@@ -1,54 +1,133 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
81
|
+
if (box.type === "mvhd") {
|
|
82
|
+
const ms = parseMvhdDurationMs(buf, box.contentStart, box.end);
|
|
83
|
+
if (ms != null)
|
|
84
|
+
return ms;
|
|
19
85
|
}
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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 <
|
|
43
|
-
const idx =
|
|
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 >
|
|
126
|
+
if (sizePos + 9 > header.length)
|
|
48
127
|
break;
|
|
49
|
-
const size =
|
|
50
|
-
if (size === 0x84 && sizePos + 9 <=
|
|
51
|
-
const duration =
|
|
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) ??
|
|
144
|
+
return probeWebmDurationMs(body) ?? probeMp4DurationMs(body);
|
|
71
145
|
}
|
|
72
|
-
return
|
|
146
|
+
return probeMp4DurationMs(body) ?? probeWebmDurationMs(body);
|
|
73
147
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomorrowos/sdk",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
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.
|
|
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.
|
|
13
|
+
"@tomorrowos/sdk": "^0.4.6"
|
|
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
|
-
|
|
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));
|