highlightRecordingService.test.js 8.2 KB
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const {
    HighlightRecordingService,
    buildFileInfo,
    buildTaskKey,
    formatShanghaiDate,
    getPreviousShanghaiDayRange,
    md5,
    normalizeFileLookup,
    normalizeHighlight,
    normalizeOutputNamespace
} = require('../services/highlightRecordingService');

async function run() {
    assert.strictEqual(md5('14867586201784628401782'), 'a66749831542e5e090e41d72f455918d');

    const shanghaiMidnight = Date.UTC(2026, 0, 1, 16, 0, 0, 0);
    assert.strictEqual(formatShanghaiDate(shanghaiMidnight), '20260102');
    assert.deepStrictEqual(getPreviousShanghaiDayRange(Date.UTC(2026, 7, 6, 0, 0, 0)), {
        beginTime: Date.UTC(2026, 7, 4, 16, 0, 0),
        endTime: Date.UTC(2026, 7, 5, 15, 59, 59, 999)
    });

    const item = normalizeHighlight({
        id: 4,
        meetingNumber: '1486758620',
        siteId: 'doctest',
        beginTime: 1779159793000,
        endTime: 1779159893000,
        type: 0,
        more: 'ext'
    }, 3600000);
    assert.strictEqual(item.highlightId, 4);
    assert.strictEqual(item.classId, '1486758620');
    assert.strictEqual(buildTaskKey(item), 'doctest:4');

    assert.throws(() => normalizeHighlight({
        id: 4,
        meetingNumber: '1486758620',
        siteId: '../bad',
        beginTime: 1779159793000,
        endTime: 1779159893000
    }, 3600000), /siteId 无效/);

    assert.throws(() => normalizeHighlight({
        id: 4,
        meetingNumber: '1486758620',
        siteId: 'doctest',
        beginTime: 1779159893000,
        endTime: 1779159793000
    }, 3600000), /endTime 必须大于 beginTime/);

    assert.deepStrictEqual(normalizeFileLookup({
        highlightId: 4,
        classId: '1486758620',
        siteId: 'doctest',
        beginTime: 1779159793000
    }), {
        highlightId: 4,
        classId: '1486758620',
        siteId: 'doctest',
        beginTime: 1779159793000
    });

    const fileInfo = buildFileInfo(item, '/capture', 'https://xdymp4.xuedianyun.com/');
    assert.strictEqual(fileInfo.fileName, '1486758620_highlight_4.mp4');
    assert.strictEqual(fileInfo.localPath, path.join('/capture', 'media', 'doctest', '20260519', fileInfo.fileName));
    assert.strictEqual(fileInfo.ossKey, 'oss/doctest/20260519/1486758620_highlight_4.mp4');
    assert.strictEqual(fileInfo.url, `https://xdymp4.xuedianyun.com/${fileInfo.ossKey}`);

    const validationFileInfo = buildFileInfo(item, '/capture', 'https://xdymp4.xuedianyun.com/', 'validation-20260805');
    assert.strictEqual(
        validationFileInfo.localPath,
        path.join('/capture', 'media', 'validation-20260805', 'doctest', '20260519', validationFileInfo.fileName)
    );
    assert.strictEqual(
        validationFileInfo.ossKey,
        'oss/validation-20260805/doctest/20260519/1486758620_highlight_4.mp4'
    );
    assert.strictEqual(normalizeOutputNamespace('validation_1'), 'validation_1');
    assert.throws(() => normalizeOutputNamespace('../validation'), /outputNamespace/);

    const service = new HighlightRecordingService({ configPath: '/not-used-in-this-test' });
    const playbackUrl = new URL(service.buildPlaybackUrl(item, {
        url: 'https://pclive.xuedianyun.com/pcBase/pclive2/dev/index.html',
        portalIP: 'saas.xuedianyun.com',
        portalPort: 80,
        channels: 2,
        userRole: 'normal'
    }));
    assert.strictEqual(playbackUrl.searchParams.get('classId'), '1486758620');
    assert.strictEqual(playbackUrl.searchParams.get('playRecord'), '1');
    assert.strictEqual(playbackUrl.searchParams.get('recBeginTime'), '1779159793000');
    assert.strictEqual(playbackUrl.searchParams.get('recEndTime'), '1779159893000');

    const queueService = new HighlightRecordingService({ configPath: '/not-used-in-this-test' });
    queueService.readConfig = () => ({
        projectCatalog: '/capture',
        highlight: {
            siteIds: ['doctest'],
            maxDurationMs: 3600000,
            outputBaseUrl: 'https://xdymp4.xuedianyun.com',
            taskRetentionMs: 86400000,
            maxConcurrent: 1
        }
    });
    queueService.inspectFile = async () => ({ local: false, oss: true, generated: true });
    const firstResult = await queueService.enqueue([item]);
    assert.strictEqual(firstResult[0].status, 'generated');
    const changedResult = await queueService.enqueue([Object.assign({}, item, { beginTime: item.beginTime + 1000 })]);
    assert.strictEqual(changedResult[0].status, 'invalid');
    assert.match(changedResult[0].error, /时间段发生变化/);

    const previewService = new HighlightRecordingService({ configPath: '/not-used-in-this-test' });
    previewService.readConfig = () => ({
        projectCatalog: '/capture',
        playback: {
            url: 'https://pclive.xuedianyun.com/pcBase/pclive2/dev/index.html',
            portalIP: 'saas.xuedianyun.com',
            portalPort: 80,
            channels: 2
        },
        highlight: {
            siteIds: ['doctest'],
            maxDurationMs: 3600000,
            outputBaseUrl: 'https://xdymp4.xuedianyun.com',
            outputNamespace: 'validation-20260805'
        }
    });
    previewService.inspectFile = async () => ({ local: false, oss: false, generated: false });
    const preview = await previewService.preview([item]);
    assert.strictEqual(preview[0].status, 'not_generated');
    assert.strictEqual(preview[0].duration, 100000);
    assert.match(preview[0].ossKey, /^oss\/validation-20260805\//);
    assert.strictEqual(new URL(preview[0].playbackUrl).searchParams.get('recBeginTime'), '1779159793000');
    assert.strictEqual(previewService.getQueueInfo().queued, 0);

    const scheduledSiteService = new HighlightRecordingService({ configPath: '/not-used-in-this-test' });
    scheduledSiteService.readConfig = () => ({
        highlight: {
            enabled: true,
            siteIds: ['xdyui2'],
            maxConcurrent: 1,
            taskRetentionMs: 86400000
        }
    });
    let siteQuery;
    scheduledSiteService.fetchBySite = async params => {
        siteQuery = params;
        return [];
    };
    scheduledSiteService.enqueue = async records => records;
    const scheduledSite = await scheduledSiteService.runScheduledRecording(Date.UTC(2026, 7, 6, 0, 0, 0));
    assert.strictEqual(scheduledSite.mode, 'site');
    assert.strictEqual(siteQuery.siteId, 'xdyui2');
    assert.strictEqual(scheduledSite.received, 0);

    const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'webscreen-highlight-'));
    try {
        const captureService = new HighlightRecordingService({ configPath: '/not-used-in-this-test' });
        captureService.readConfig = () => ({
            projectCatalog: tempRoot,
            captureCatalog: '/capture',
            playback: { url: 'https://example.com/index.html' },
            highlight: {
                enabled: true,
                siteIds: ['doctest'],
                maxDurationMs: 3600000,
                outputBaseUrl: 'https://xdymp4.xuedianyun.com',
                outputNamespace: '',
                taskRetentionMs: 86400000,
                maxConcurrent: 1,
                loadGraceMs: 100,
                endGraceMs: 100
            }
        });
        captureService.inspectFile = async fileInfo => ({
            local: fs.existsSync(fileInfo.localPath),
            oss: false,
            generated: false
        });
        captureService.runCapture = async task => {
            fs.writeFileSync(task.fileInfo.tempPath, 'test-mp4');
        };
        await captureService.enqueue([item]);
        for (let attempt = 0; attempt < 100 && captureService.getQueueInfo().recording > 0; attempt += 1) {
            await new Promise(resolve => setTimeout(resolve, 10));
        }
        const capturedFile = buildFileInfo(item, tempRoot, 'https://xdymp4.xuedianyun.com');
        assert.strictEqual(fs.readFileSync(capturedFile.localPath, 'utf8'), 'test-mp4');
        assert.ok(fs.existsSync(path.join(capturedFile.localDir, 'download.json')));
        assert.strictEqual(fs.existsSync(capturedFile.tempPath), false);
    } finally {
        fs.rmSync(tempRoot, { recursive: true, force: true });
    }

    console.log('highlightRecordingService tests passed');
}

run().catch(error => {
    console.error(error);
    process.exitCode = 1;
});