Files
av-tools/tests/unit/media/stream-playback.test.ts

175 lines
4.2 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
assessBrowserPlayback,
assessGeneratedResultPreview,
buildPlaybackMimeCandidates,
} from '../../../src/media/browser-playback';
import type {
MediaProbe,
MediaStreamProbe,
} from '../../../src/media/media.types';
import {
selectDefaultStreams,
streamSelectionMapArgs,
validateStreamSelection,
} from '../../../src/media/stream-selection';
function stream(
index: number,
type: MediaStreamProbe['type'],
overrides: Partial<MediaStreamProbe> = {}
): MediaStreamProbe {
return {
index,
type,
disposition: {},
tags: {},
...overrides,
};
}
function probe(streams: MediaStreamProbe[]): MediaProbe {
return {
formatNames: ['mov', 'mp4'],
tags: {},
streams,
chapters: [],
warnings: [],
};
}
describe('stream selection', () => {
const media = probe([
stream(0, 'video', { codecName: 'h264' }),
stream(1, 'audio', {
language: 'eng',
disposition: { default: true },
}),
stream(2, 'audio', {
language: 'de-DE',
}),
stream(3, 'subtitle', { language: 'deu' }),
stream(4, 'video', { disposition: { attached_pic: true } }),
]);
it('chooses default streams, avoids cover art and can include subtitles', () => {
expect(
selectDefaultStreams(media, {
preferredAudioLanguages: ['de'],
preferredSubtitleLanguages: ['deu'],
includeSubtitles: true,
})
).toEqual({
videoStreamIndex: 0,
audioStreamIndexes: [2],
subtitleStreamIndexes: [3],
});
});
it('validates stream existence, type and duplicate mappings', () => {
const result = validateStreamSelection(media, {
videoStreamIndex: 1,
audioStreamIndexes: [2, 2, 99],
subtitleStreamIndexes: [],
});
expect(result.valid).toBe(false);
expect(result.issues.map((issue) => issue.code)).toEqual([
'wrong-stream-type',
'duplicate-stream',
'stream-not-found',
]);
});
it('generates structured map arguments in deterministic order', () => {
expect(
streamSelectionMapArgs({
videoStreamIndex: 0,
audioStreamIndexes: [2],
subtitleStreamIndexes: [3],
})
).toEqual(['-map', '0:0', '-map', '0:2', '-map', '0:3']);
});
});
describe('browser playback assessment', () => {
const mp4 = probe([
stream(0, 'video', { codecName: 'h264' }),
stream(1, 'audio', { codecName: 'aac' }),
]);
it('builds codec-qualified and container-only MIME candidates', () => {
expect(buildPlaybackMimeCandidates(mp4)).toEqual([
'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
'video/mp4',
]);
});
it('keeps browser support separate from FFmpeg readability', () => {
expect(
assessBrowserPlayback(mp4, (mime) =>
mime.includes('codecs') ? 'probably' : ''
)
).toMatchObject({
support: 'probably',
needsPreviewProxy: false,
});
expect(assessBrowserPlayback(mp4, () => '')).toMatchObject({
support: 'unsupported',
needsPreviewProxy: true,
});
});
it('does not treat an audio/video MIME prefix as playable when the browser rejects it', () => {
expect(
assessGeneratedResultPreview(
{
fileName: 'generated.flac',
mimeType: 'audio/flac',
},
() => ''
)
).toMatchObject({
kind: 'audio',
previewable: false,
support: 'unsupported',
});
expect(
assessGeneratedResultPreview(
{
fileName: 'generated.mp4',
mimeType: 'video/mp4',
probe: mp4,
},
(mime) => (mime.includes('codecs') ? 'probably' : '')
)
).toMatchObject({
kind: 'video',
previewable: true,
support: 'probably',
});
expect(
assessGeneratedResultPreview(
{
fileName: 'frame.png',
mimeType: 'image/png',
},
() => ''
)
).toMatchObject({ kind: 'image', previewable: true });
expect(
assessGeneratedResultPreview(
{
fileName: 'frame.tiff',
mimeType: 'image/tiff',
},
() => ''
)
).toMatchObject({
kind: 'image',
previewable: false,
support: 'unsupported',
});
});
});