import { describe, expect, it } from 'vitest'; import { avcCodec, FrameGate, isKeyFrame, pictureTransform, RecordingClock, recordingName, recordingSettingsOf, recordingSize, } from '../../src/core/recording/recording'; import { runDef, fakeContext } from '../helpers'; describe('recording settings (#405)', () => { it('names a recording after its start when no name is given', () => { const at = new Date(2026, 9, 11, 16, 42, 5); expect(recordingName('', at)).toBe('Recording 2026-10-11 16:42:05'); expect(recordingName(' ', at)).toBe('Recording 2026-10-11 16:42:05'); expect(recordingName(' Boss fight ', at)).toBe('Boss fight'); }); it('reads what the node hands over, an unknown choice as the default', () => { expect(recordingSettingsOf(' Run ', '1080p', '30')).toEqual({ name: 'Run', resolution: '1080p', fps: 30, }); expect(recordingSettingsOf('', 'huge', 25)).toEqual({ name: '', resolution: 'view', fps: 60 }); }); it('records the game view at its size on screen, even on both sides', () => { expect(recordingSize('view', { w: 1279, h: 721 }, { w: 1279, h: 721 })).toEqual({ w: 1280, h: 722 }); // A 5K screen is held to 4K. expect(recordingSize('view', { w: 5120, h: 2880 }, { w: 5120, h: 2880 })).toEqual({ w: 3840, h: 2160 }); }); it('holds the height of 720p and 1080p and takes the width from the camera', () => { expect(recordingSize('1080p', { w: 800, h: 450 }, { w: 800, h: 450 })).toEqual({ w: 1920, h: 1080 }); expect(recordingSize('720p', { w: 400, h: 400 }, { w: 400, h: 400 })).toEqual({ w: 720, h: 720 }); // 4:3 at 720p, and the camera's whole picture rules where the screen cuts it. expect(recordingSize('720p', { w: 300, h: 400 }, { w: 640, h: 480 })).toEqual({ w: 960, h: 720 }); }); it('picks the lowest H.264 level that takes the frames', () => { expect(avcCodec(1280, 720, 30)).toBe('avc1.64001f'); expect(avcCodec(1920, 1080, 30)).toBe('avc1.640028'); expect(avcCodec(1920, 1080, 60)).toBe('avc1.64002a'); expect(avcCodec(2560, 1440, 60)).toBe('avc1.640033'); expect(avcCodec(3840, 2160, 60)).toBe('avc1.640034'); }); it('starts a group every two seconds', () => { expect([0, 1, 119, 120, 240].map((i) => isKeyFrame(i, 60))).toEqual([true, false, false, true, true]); expect(isKeyFrame(60, 30)).toBe(true); }); }); describe('RecordingClock', () => { it('counts from the start and leaves the editor pause out', () => { const clock = new RecordingClock(); expect(clock.at(500)).toBe(0); clock.start(1000); expect(clock.at(1500)).toBe(500); clock.pause(2000); expect(clock.at(5000)).toBe(1000); clock.resume(6000); expect(clock.at(6500)).toBe(1500); // Pausing twice and resuming once still takes out the whole pause. clock.pause(7000); clock.pause(7500); clock.resume(8000); expect(clock.at(8000)).toBe(2000); }); it('leaves a hold before the first frame out of the time, not off it', () => { const clock = new RecordingClock(); clock.pause(0); clock.resume(2000); clock.start(2000); expect(clock.at(2500)).toBe(500); }); }); describe('FrameGate', () => { const taken = (fps: number, frameMs: number, seconds: number, jitter = 0) => { const gate = new FrameGate(fps); let count = 0; for (let i = 0; i * frameMs < seconds * 1000; i++) { const wobble = jitter ? ((i * 7919) % 11) / 10 - 0.5 : 0; if (gate.take(Math.max(0, i * frameMs + wobble * jitter))) count++; } return count; }; it('takes every frame at 60 on a 60 Hz screen, however unevenly they come', () => { expect(taken(60, 1000 / 60, 10)).toBe(600); expect(taken(60, 1000 / 60, 10, 6)).toBe(600); }); it('takes every other at 30', () => { expect(taken(30, 1000 / 60, 10)).toBe(300); }); it('takes about 60 a second from a 144 Hz screen', () => { expect(Math.abs(taken(60, 1000 / 144, 10) - 600)).toBeLessThanOrEqual(6); }); }); describe('pictureTransform', () => { it('maps the picture onto the whole frame', () => { // A picture of 640x360 CSS pixels centred at (400, 300), recorded at 1080p. const area = { x: 400, y: 300, w: 640, h: 360 }; const m = pictureTransform(area, { w: 1920, h: 1080 }); const at = (x: number, y: number) => ({ x: m.a * x + m.tx, y: m.d * y + m.ty }); expect(at(80, 120)).toEqual({ x: 0, y: 0 }); expect(at(720, 480)).toEqual({ x: 1920, y: 1080 }); expect(at(400, 300)).toEqual({ x: 960, y: 540 }); }); }); describe('the recording nodes', () => { it('Start recording hands its settings over and flows on', () => { const ctx = fakeContext(); const result = runDef('StartRecording', ctx, { name: 'Intro', resolution: '720p', fps: '30' }, {}); expect(result.next).toBe('exec'); expect(ctx.recordings).toEqual([{ name: 'Intro', resolution: '720p', fps: 30 }]); }); it('Start recording asks for the game view at 60 by default', () => { const ctx = fakeContext(); runDef('StartRecording', ctx, {}, {}); expect(ctx.recordings).toEqual([{ name: '', resolution: 'view', fps: 60 }]); }); it('Stop recording asks for the stop and flows on', () => { const ctx = fakeContext(); expect(runDef('StopRecording', ctx, {}, {}).next).toBe('exec'); expect(ctx.recordings).toEqual(['stop']); }); });