openvibers/openvibe.media Commits
The health scan calls a recording under 1 KB empty, and looks at one rated ok at once (#284)
Changes
@@@ -417,13 +417,15 @@ async function repairVodDuration(vodId, duration, fileSize, source = 'probe') {}// VODs the periodic health job should scan: finished (not recording), and either never--// scanned or last scanned longer ago than `staleDays`. Never-scanned + oldest-scanned first.++// scanned, last scanned longer ago than `staleDays`, or rated ok while under 1 KB (too small to hold a frame: the++// scanner calls it empty now). Never-scanned, then those tiny ones, then oldest-scanned first.async function getVodsNeedingHealthScan({ staleDays = 30, limit = 3 } = {}) {return await all(`SELECT * FROM vodsWHERE COALESCE(is_recording, 0) = 0AND (health_status IS NULL OR health_status NOT IN ('corrupt','zero_byte','missing_file'))-- AND (last_health_scan_at IS NULL OR last_health_scan_at <= datetime('now', ?))-- ORDER BY (last_health_scan_at IS NULL) DESC, last_health_scan_at ASC++ AND (last_health_scan_at IS NULL OR last_health_scan_at <= datetime('now', ?)++ OR (COALESCE(file_size, 0) BETWEEN 1 AND 1023 AND COALESCE(health_status, 'ok') = 'ok'))++ ORDER BY (last_health_scan_at IS NULL) DESC, (COALESCE(file_size, 0) BETWEEN 1 AND 1023) DESC, last_health_scan_at ASCLIMIT ?`, [`-${Math.max(1, staleDays)} days`, limit]);}@@@ -139,9 +139,11 @@ async function scanVod(vod, options = {}) {let stat;try {stat = fs.statSync(filePath);-- if (stat.size <= 0) {++ // Under 1 KB is a container header at most: nothing to play or to take a picture from (VOD 1803: 526 bytes for++ // "51 seconds", rated ok, the #1 VOD on its channel and blank, 2026-11-02). It is as empty as 0 bytes.++ if (stat.size < 1024) {result.status = 'zero_byte';-- result.issues.push('zero_byte');++ result.issues.push(stat.size <= 0 ? 'zero_byte' : 'too_small');return result;}} catch (err) {nnew file mode 100644@@@ -0,0 +1,40 @@++'use strict';++// A recording under 1 KB is empty (2026-11-02): VOD 1803 was 526 bytes for "51 seconds", rated ok, the #1 VOD on its++// channel with a blank card and nothing to play. The scanner calls such a file zero_byte (an empty one already was),++// and the health job picks one rated ok at once instead of waiting out its 30 days.++const assert = require('assert');++const fs = require('fs');++const os = require('os');++const path = require('path');++++(async () => {++ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-tiny-'));++ const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };++ Object.assign(process.env, {++ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),++ THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), MEDIA_INVARIANT_SCAN_HOURS: '0',++ });++ const db = require('../server/db/database');++ const scanner = require('../server/vod/health-scanner');++ await db.upsertApp({ app_id: 'live', api_key: 'live-key-tiny' });++ const V = process.env.VOD_PATH;++ const tiny = path.join(V, 'tiny.webm');++ fs.writeFileSync(tiny, Buffer.alloc(526, 1));++ const empty = path.join(V, 'empty.webm');++ fs.writeFileSync(empty, '');++ const ins = async (file, size) => Number((await db.run(`INSERT INTO vods (app_id, title, file_path, file_size, duration_seconds, is_public, visibility, health_status, last_health_scan_at)++ VALUES ('live', 't', ?, ?, 51, 1, 'public', 'ok', now()) RETURNING id`, [file, size])).lastInsertRowid);++ const tinyId = await ins(tiny, 526);++ const bigId = await ins(path.join(V, 'big.webm'), 5_000_000); // scanned just now, a normal size: not due++++ const due = (await db.getVodsNeedingHealthScan({ limit: 10 })).map((v) => Number(v.id));++ assert.ok(due.includes(tinyId), 'a tiny file rated ok is scanned at once, not in 30 days');++ assert.ok(!due.includes(bigId), 'a normal one scanned just now is not');++++ const r = await scanner.scanVod({ id: tinyId, file_path: tiny, duration_seconds: 51 });++ assert.deepStrictEqual([r.status, r.issues], ['zero_byte', ['too_small']], 'under 1 KB is empty');++ const e = await scanner.scanVod({ id: 0, file_path: empty, duration_seconds: 0 });++ assert.deepStrictEqual([e.status, e.issues], ['zero_byte', ['zero_byte']], 'an empty file, as before');++ console.log('health tiny file: all checks passed');++ process.exit(0);++})().catch((err) => { console.error(err); process.exit(1); });
@@ -417,13 +417,15 @@ async function repairVodDuration(vodId, duration, fileSize, source = 'probe') {
@@ -417,13 +417,15 @@ async function repairVodDuration(vodId, duration, fileSize, source = 'probe') {
}
}
// VODs the periodic health job should scan: finished (not recording), and either never
// VODs the periodic health job should scan: finished (not recording), and either never
-// scanned or last scanned longer ago than `staleDays`. Never-scanned + oldest-scanned first.
+// scanned, last scanned longer ago than `staleDays`, or rated ok while under 1 KB (too small to hold a frame: the
+// scanner calls it empty now). Never-scanned, then those tiny ones, then oldest-scanned first.
async function getVodsNeedingHealthScan({ staleDays = 30, limit = 3 } = {}) {
async function getVodsNeedingHealthScan({ staleDays = 30, limit = 3 } = {}) {
return await all(`SELECT * FROM vods
return await all(`SELECT * FROM vods
WHERE COALESCE(is_recording, 0) = 0
WHERE COALESCE(is_recording, 0) = 0
AND (health_status IS NULL OR health_status NOT IN ('corrupt','zero_byte','missing_file'))
AND (health_status IS NULL OR health_status NOT IN ('corrupt','zero_byte','missing_file'))
- AND (last_health_scan_at IS NULL OR last_health_scan_at <= datetime('now', ?))
- ORDER BY (last_health_scan_at IS NULL) DESC, last_health_scan_at ASC
+ AND (last_health_scan_at IS NULL OR last_health_scan_at <= datetime('now', ?)
+ OR (COALESCE(file_size, 0) BETWEEN 1 AND 1023 AND COALESCE(health_status, 'ok') = 'ok'))
+ ORDER BY (last_health_scan_at IS NULL) DESC, (COALESCE(file_size, 0) BETWEEN 1 AND 1023) DESC, last_health_scan_at ASC
LIMIT ?`, [`-${Math.max(1, staleDays)} days`, limit]);
LIMIT ?`, [`-${Math.max(1, staleDays)} days`, limit]);
}
}
@@ -139,9 +139,11 @@ async function scanVod(vod, options = {}) {
@@ -139,9 +139,11 @@ async function scanVod(vod, options = {}) {
let stat;
let stat;
try {
try {
stat = fs.statSync(filePath);
stat = fs.statSync(filePath);
- if (stat.size <= 0) {
+ // Under 1 KB is a container header at most: nothing to play or to take a picture from (VOD 1803: 526 bytes for
+ // "51 seconds", rated ok, the #1 VOD on its channel and blank, 2026-11-02). It is as empty as 0 bytes.
+ if (stat.size < 1024) {
result.status = 'zero_byte';
result.status = 'zero_byte';
- result.issues.push('zero_byte');
+ result.issues.push(stat.size <= 0 ? 'zero_byte' : 'too_small');
return result;
return result;
}
}
} catch (err) {
} catch (err) {
new file mode 100644
new file mode 100644
@@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
+'use strict';
+// A recording under 1 KB is empty (2026-11-02): VOD 1803 was 526 bytes for "51 seconds", rated ok, the #1 VOD on its
+// channel with a blank card and nothing to play. The scanner calls such a file zero_byte (an empty one already was),
+// and the health job picks one rated ok at once instead of waiting out its 30 days.
+const assert = require('assert');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+(async () => {
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-media-tiny-'));
+ const dir = (n) => { const d = path.join(tmp, n); fs.mkdirSync(d, { recursive: true }); return d; };
+ Object.assign(process.env, {
+ VOD_PATH: dir('vods'), CLIPS_PATH: dir('clips'), FILES_PATH: dir('files'),
+ THUMBNAILS_PATH: dir('thumbnails'), PASTES_PATH: dir('pastes'), OBJECTS_PATH: dir('objects'), MEDIA_INVARIANT_SCAN_HOURS: '0',
+ });
+ const db = require('../server/db/database');
+ const scanner = require('../server/vod/health-scanner');
+ await db.upsertApp({ app_id: 'live', api_key: 'live-key-tiny' });
+ const V = process.env.VOD_PATH;
+ const tiny = path.join(V, 'tiny.webm');
+ fs.writeFileSync(tiny, Buffer.alloc(526, 1));
+ const empty = path.join(V, 'empty.webm');
+ fs.writeFileSync(empty, '');
+ const ins = async (file, size) => Number((await db.run(`INSERT INTO vods (app_id, title, file_path, file_size, duration_seconds, is_public, visibility, health_status, last_health_scan_at)
+ VALUES ('live', 't', ?, ?, 51, 1, 'public', 'ok', now()) RETURNING id`, [file, size])).lastInsertRowid);
+ const tinyId = await ins(tiny, 526);
+ const bigId = await ins(path.join(V, 'big.webm'), 5_000_000); // scanned just now, a normal size: not due
+
+ const due = (await db.getVodsNeedingHealthScan({ limit: 10 })).map((v) => Number(v.id));
+ assert.ok(due.includes(tinyId), 'a tiny file rated ok is scanned at once, not in 30 days');
+ assert.ok(!due.includes(bigId), 'a normal one scanned just now is not');
+
+ const r = await scanner.scanVod({ id: tinyId, file_path: tiny, duration_seconds: 51 });
+ assert.deepStrictEqual([r.status, r.issues], ['zero_byte', ['too_small']], 'under 1 KB is empty');
+ const e = await scanner.scanVod({ id: 0, file_path: empty, duration_seconds: 0 });
+ assert.deepStrictEqual([e.status, e.issues], ['zero_byte', ['zero_byte']], 'an empty file, as before');
+ console.log('health tiny file: all checks passed');
+ process.exit(0);
+})().catch((err) => { console.error(err); process.exit(1); });