Re-encode cached videos to HEVC when it saves at least 15 percent and serve them only to devices that can decode HEVC

This commit is contained in:
Jonathan Sykes
2026-09-13 21:09:15 +08:00
parent 91dab289c3
commit 8b0060d42a
7 changed files with 159 additions and 55 deletions

View File

@@ -9,7 +9,7 @@ import { join } from 'node:path';
const root = mkdtempSync(join(tmpdir(), 'ytp-media-test-'));
process.env.DB_PATH = join(root, 'test.db');
const dbmod = await import('./db.js');
const { createMediaCache, validateMedia, MediaSkip, HIGH, LOW } = await import('./media-cache.js');
const { createMediaCache, validateMedia, MediaSkip, HIGH, LOW, OPT_REV } = await import('./media-cache.js');
const fx = (name) => join(root, name);
const ff = (...args) => {
@@ -33,6 +33,11 @@ beforeAll(async () => {
'-c:v', 'libx264', '-preset', 'ultrafast', '-pix_fmt', 'yuv420p', fx('noaudio.mp4'));
ff('-f', 'lavfi', '-i', 'testsrc=size=640x360:rate=30:duration=10', '-f', 'lavfi', '-i', 'sine=frequency=440:duration=10',
'-c:v', 'mpeg4', '-c:a', 'aac', '-shortest', fx('mpeg4.mp4'));
// HEVC as the compression lane writes it (hvc1) and as Safari rejects it (hev1).
ff('-f', 'lavfi', '-i', 'testsrc=size=640x360:rate=30:duration=10', '-f', 'lavfi', '-i', 'sine=frequency=440:duration=10',
'-c:v', 'libx265', '-preset', 'ultrafast', '-pix_fmt', 'yuv420p', '-tag:v', 'hvc1', '-x265-params', 'log-level=error',
'-c:a', 'aac', '-shortest', fx('hevc.mp4'));
ff('-i', fx('hevc.mp4'), '-c', 'copy', '-tag:v', 'hev1', fx('hevc-hev1.mp4'));
const buf = readFileSync(fx('good.mp4'));
writeFileSync(fx('truncated.mp4'), buf.subarray(0, Math.floor(buf.length * 0.6)));
// The nasty case: moov up front (index says 10 s) but the media data is cut.
@@ -61,6 +66,14 @@ describe('validateMedia', () => {
test('rejects a non-h264 file', async () => {
await expect(validateMedia(fx('mpeg4.mp4'), 10)).rejects.toThrow(/not h264/);
});
test('accepts hvc1-tagged HEVC for the compression lane, but not as a download', async () => {
const p = await validateMedia(fx('hevc.mp4'), 10);
expect(p.vcodec).toBe('hevc');
await expect(validateMedia(fx('hevc.mp4'), 10, { codecs: ['h264'] })).rejects.toThrow(/not h264/);
});
test('rejects hev1-tagged HEVC (Safari refuses it)', async () => {
await expect(validateMedia(fx('hevc-hev1.mp4'), 10)).rejects.toThrow(/hvc1/);
});
test('rejects a file shorter than the source video', async () => {
await expect(validateMedia(fx('good.mp4'), 60)).rejects.toThrow(/truncated/);
});
@@ -210,19 +223,39 @@ describe('media cache jobs', () => {
expect(calls).toEqual(['bootAAAAAA1']);
});
test('x264 compression lane swaps in a smaller copy under a new gen', async () => {
test('HEVC compression lane swaps in a smaller copy under a new gen', async () => {
await clearDb();
const { cache } = makeCache({ transcode: { enabled: true, crf: 35, preset: 'ultrafast', threads: 2 } });
const { cache } = makeCache({ transcode: { enabled: true, codec: 'hevc', crf: 34, preset: 'ultrafast', threads: 2 } });
await cache.init();
const first = await cache.ensureCached('optAAAAAAA1', { priority: HIGH });
expect(await until(async () => (await dbmod.getMedia('optAAAAAAA1')).optimized === 1)).toBe(true);
expect(first.vcodec).toBe('h264');
expect(await until(async () => (await dbmod.getMedia('optAAAAAAA1')).optimized === OPT_REV)).toBe(true);
const after = await dbmod.getMedia('optAAAAAAA1');
if (after.gen !== first.gen) {
expect(after.size).toBeLessThan(first.size);
// old generation is kept for in-flight playback (grace period)
expect(await cache.filePath('optAAAAAAA1', first.gen)).not.toBeNull();
}
expect(after.gen).toBeGreaterThan(first.gen);
expect(after.vcodec).toBe('hevc');
expect(after.size).toBeLessThan(first.size);
// old generation is kept for in-flight playback (grace period)
expect(await cache.filePath('optAAAAAAA1', first.gen)).not.toBeNull();
expect(await cache.filePath('optAAAAAAA1', after.gen, 'm4a')).not.toBeNull();
// and the HEVC copy still passes the gate verify() uses
expect(await cache.verify('optAAAAAAA1')).toBe(true);
});
test('copies evaluated by an older lane revision are re-queued at boot', async () => {
await clearDb();
const seed = makeCache();
await seed.cache.init();
await seed.cache.ensureCached('revAAAAAAA1', { priority: HIGH });
await dbmod.upsertMedia('revAAAAAAA1', { optimized: 1 }); // the old x264 revision
// A fresh instance over the same dir + DB, as after a deploy.
const same = createMediaCache({
dir: seed.dir, db: { getMedia: dbmod.getMedia, upsertMedia: dbmod.upsertMedia, deleteMedia: dbmod.deleteMedia,
listMedia: dbmod.listMedia, listMediaLru: dbmod.listMediaLru, touchMedia: dbmod.touchMedia, mediaStats: dbmod.mediaStats },
log: silentLog, getInfo: async () => ({ duration: 10 }), download: async () => { throw new Error('no download expected'); },
transcode: { enabled: true, codec: 'hevc', crf: 34, preset: 'ultrafast', threads: 2 }, freeBytes: () => 100 * 1024 ** 3,
});
await same.init();
expect(await until(async () => (await dbmod.getMedia('revAAAAAAA1')).optimized === OPT_REV)).toBe(true);
});
test('verify() drops and refetches a copy that went bad on disk, keeps a good one', async () => {