import { Buffer } from 'node:buffer'; import process from 'node:process'; import { PassThrough, Readable } from 'node:stream'; import { opus, VolumeTransformer } from 'prism-media'; import { describe, test, expect, vitest, type MockedFunction, beforeAll, beforeEach } from 'vitest'; import { SILENCE_FRAME } from '../src/audio/AudioPlayer'; import { AudioResource, createAudioResource, NO_CONSTRAINT, VOLUME_CONSTRAINT } from '../src/audio/AudioResource'; import { findPipeline as _findPipeline, StreamType, TransformerType, type Edge } from '../src/audio/TransformerGraph'; vitest.mock('../src/audio/TransformerGraph'); async function wait() { // eslint-disable-next-line no-promise-executor-return return new Promise((resolve) => process.nextTick(resolve)); } async function started(resource: AudioResource) { while (!resource.started) { await wait(); } return resource; } const findPipeline = _findPipeline as unknown as MockedFunction; beforeAll(() => { // @ts-expect-error: No type findPipeline.mockImplementation((from: StreamType, constraint: (path: Edge[]) => boolean) => { const base = [ { cost: 1, transformer: () => new PassThrough(), type: TransformerType.FFmpegPCM, }, ]; if (constraint === VOLUME_CONSTRAINT) { base.push({ cost: 1, // Transformer type shouldn't matter: we are not testing prism-media, but rather the expectation that the stream is VolumeTransformer transformer: () => new VolumeTransformer({ type: 's16le' } as any), type: TransformerType.InlineVolume, }); } return base as any[]; }); }); beforeEach(() => { findPipeline.mockClear(); }); describe('createAudioResource', () => { test('Creates a resource from string path', () => { const resource = createAudioResource('mypath.mp3'); expect(findPipeline).toHaveBeenCalledWith(StreamType.Arbitrary, NO_CONSTRAINT); expect(resource.volume).toBeUndefined(); }); test('Creates a resource from string path (volume)', () => { const resource = createAudioResource('mypath.mp3', { inlineVolume: true }); expect(findPipeline).toHaveBeenCalledWith(StreamType.Arbitrary, VOLUME_CONSTRAINT); expect(resource.volume).toBeInstanceOf(VolumeTransformer); }); test('Only infers type if not explicitly given', () => { const resource = createAudioResource(new opus.Encoder({ rate: 48_000, channels: 2, frameSize: 960 }), { inputType: StreamType.Arbitrary, }); expect(findPipeline).toHaveBeenCalledWith(StreamType.Arbitrary, NO_CONSTRAINT); expect(resource.volume).toBeUndefined(); }); test('Infers from opus.Encoder', () => { const resource = createAudioResource(new opus.Encoder({ rate: 48_000, channels: 2, frameSize: 960 }), { inlineVolume: true, }); expect(findPipeline).toHaveBeenCalledWith(StreamType.Opus, VOLUME_CONSTRAINT); expect(resource.volume).toBeInstanceOf(VolumeTransformer); expect(resource.encoder).toBeInstanceOf(opus.Encoder); }); test('Infers from opus.OggDemuxer', () => { const resource = createAudioResource(new opus.OggDemuxer()); expect(findPipeline).toHaveBeenCalledWith(StreamType.Opus, NO_CONSTRAINT); expect(resource.volume).toBeUndefined(); expect(resource.encoder).toBeUndefined(); }); test('Infers from opus.WebmDemuxer', () => { const resource = createAudioResource(new opus.WebmDemuxer()); expect(findPipeline).toHaveBeenCalledWith(StreamType.Opus, NO_CONSTRAINT); expect(resource.volume).toBeUndefined(); }); test('Infers from opus.Decoder', () => { const resource = createAudioResource(new opus.Decoder({ rate: 48_000, channels: 2, frameSize: 960 })); expect(findPipeline).toHaveBeenCalledWith(StreamType.Raw, NO_CONSTRAINT); expect(resource.volume).toBeUndefined(); }); test('Infers from VolumeTransformer', () => { // Transformer type shouldn't matter: we are not testing prism-media, but rather the expectation that the stream is VolumeTransformer const stream = new VolumeTransformer({ type: 's16le' } as any); const resource = createAudioResource(stream, { inlineVolume: true }); expect(findPipeline).toHaveBeenCalledWith(StreamType.Raw, NO_CONSTRAINT); expect(resource.volume).toEqual(stream); }); test('Falls back to Arbitrary for unknown stream type', () => { const resource = createAudioResource(new PassThrough()); expect(findPipeline).toHaveBeenCalledWith(StreamType.Arbitrary, NO_CONSTRAINT); expect(resource.volume).toBeUndefined(); }); test('Appends silence frames when ended', async () => { const stream = Readable.from(Buffer.from([1])); const resource = new AudioResource([], [stream], null, 5); await started(resource); expect(resource.readable).toEqual(true); expect(resource.read()).toEqual(Buffer.from([1])); for (let index = 0; index < 5; index++) { await wait(); expect(resource.readable).toEqual(true); expect(resource.read()).toEqual(SILENCE_FRAME); } await wait(); expect(resource.readable).toEqual(false); expect(resource.read()).toEqual(null); }); });