openvibers/openvibe.media Commits

The health scan calls a recording under 1 KB empty, and looks at one rated ok at once (#284)

OP OpenVibers committed 11 days ago · 6e002837c4 · 3 files, +49 −5

Changes

ddiff --git a/server/db/database.js b/server/db/database.js
iindex 3255b26..29c0255 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -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 vods
WHERE COALESCE(is_recording, 0) = 0
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]);
}
ddiff --git a/server/vod/health-scanner.js b/server/vod/health-scanner.js
iindex 5fcdcfa..c9f2821 100644
---- a/server/vod/health-scanner.js
++++ b/server/vod/health-scanner.js
@@@ -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) {
ddiff --git a/test/health-tiny-file.test.js b/test/health-tiny-file.test.js
nnew file mode 100644
iindex 0000000..d0022df
---- /dev/null
++++ b/test/health-tiny-file.test.js
@@@ -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); });