mirror of
https://github.com/chrisbenincasa/tunarr.git
synced 2026-04-18 09:03:35 -04:00
fix: vaapi tonemap pixel format fixes (#1720)
This commit is contained in:
@@ -1,152 +0,0 @@
|
||||
import { bootstrapTunarr } from '@/bootstrap.js';
|
||||
import { globalOptions, setGlobalOptions } from '@/globals.js';
|
||||
import tmp from 'tmp';
|
||||
import { container } from '../../container.ts';
|
||||
import { TranscodeConfig } from '../../db/schema/TranscodeConfig.ts';
|
||||
import { HttpStreamSource } from '../../stream/types.ts';
|
||||
import { KEYS } from '../../types/inject.ts';
|
||||
import { FfmpegVersionResult } from '../ffmpegInfo.ts';
|
||||
import { FfmpegCommandGenerator } from './FfmpegCommandGenerator.ts';
|
||||
import { AudioStream, StillImageStream, VideoStream } from './MediaStream.ts';
|
||||
import { VideoFormats } from './constants.ts';
|
||||
import {
|
||||
PixelFormat,
|
||||
PixelFormatYuv420P,
|
||||
PixelFormatYuv420P10Le,
|
||||
} from './format/PixelFormat.ts';
|
||||
import { AudioInputSource } from './input/AudioInputSource.ts';
|
||||
import { VideoInputSource } from './input/VideoInputSource.ts';
|
||||
import { WatermarkInputSource } from './input/WatermarkInputSource.ts';
|
||||
import { PipelineBuilderFactory } from './pipeline/PipelineBuilderFactory.ts';
|
||||
import { AudioState } from './state/AudioState.ts';
|
||||
import { FfmpegState } from './state/FfmpegState.ts';
|
||||
import { FrameState } from './state/FrameState.ts';
|
||||
import { FrameSize } from './types.ts';
|
||||
|
||||
let dbDir: tmp.DirResult;
|
||||
beforeAll(async () => {
|
||||
// const dir = path.join(os.tmpdir(),
|
||||
dbDir = tmp.dirSync({ unsafeCleanup: true });
|
||||
setGlobalOptions({
|
||||
// databaseDirectory: dbDir.name,
|
||||
database: dbDir.name,
|
||||
verbose: 0,
|
||||
log_level: 'debug',
|
||||
});
|
||||
await bootstrapTunarr(globalOptions(), undefined, {
|
||||
system: {
|
||||
logging: {
|
||||
logLevel: 'debug',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
ffmpeg: {
|
||||
ffmpegExecutablePath: '/usr/local/bin/ffmpeg7',
|
||||
ffprobeExecutablePath: '/usr/local/bin/ffprobe7',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// dbDir?.removeCallback();
|
||||
});
|
||||
|
||||
describe('FfmpegCommandGenerator', () => {
|
||||
test('args', async () => {
|
||||
const pixelFormat: PixelFormat = new PixelFormatYuv420P();
|
||||
|
||||
const videoStream = VideoStream.create({
|
||||
index: 0,
|
||||
codec: VideoFormats.H264,
|
||||
profile: 'main',
|
||||
pixelFormat,
|
||||
frameSize: FrameSize.create({ width: 640, height: 480 }),
|
||||
providedSampleAspectRatio: null,
|
||||
displayAspectRatio: '4/3',
|
||||
});
|
||||
|
||||
const audioState = AudioState.create({
|
||||
audioEncoder: 'ac3',
|
||||
audioChannels: 2,
|
||||
audioBitrate: 192,
|
||||
audioSampleRate: 48,
|
||||
audioBufferSize: 50,
|
||||
audioDuration: 11_000,
|
||||
});
|
||||
|
||||
const target = FrameSize.withDimensions(1280, 720);
|
||||
|
||||
const desiredState = new FrameState({
|
||||
scaledSize: videoStream.squarePixelFrameSize(target),
|
||||
paddedSize: FrameSize.withDimensions(1280, 720),
|
||||
isAnamorphic: false,
|
||||
realtime: true,
|
||||
videoFormat: VideoFormats.H264,
|
||||
frameRate: 20,
|
||||
videoBitrate: 30_000,
|
||||
deinterlace: true,
|
||||
pixelFormat: new PixelFormatYuv420P10Le(),
|
||||
});
|
||||
|
||||
const generator = new FfmpegCommandGenerator();
|
||||
|
||||
const videoInputFile = new VideoInputSource(
|
||||
new HttpStreamSource('http://fakesource.com/video'),
|
||||
[videoStream],
|
||||
);
|
||||
|
||||
const audioInputFile = new AudioInputSource(
|
||||
videoInputFile.source,
|
||||
[AudioStream.create({ index: 1, codec: 'flac', channels: 6 })],
|
||||
audioState,
|
||||
);
|
||||
|
||||
const watermarkInputFile = new WatermarkInputSource(
|
||||
new HttpStreamSource('http://localhost:8000/images/tunarr.png'),
|
||||
StillImageStream.create({
|
||||
index: 0,
|
||||
frameSize: FrameSize.create({ width: 19, height: -1 }),
|
||||
}),
|
||||
{
|
||||
duration: 0,
|
||||
enabled: true,
|
||||
horizontalMargin: 10,
|
||||
verticalMargin: 10,
|
||||
position: 'bottom-right',
|
||||
width: 19,
|
||||
opacity: 1.0,
|
||||
},
|
||||
);
|
||||
|
||||
const config: TranscodeConfig = {};
|
||||
const builder = await container
|
||||
.get<PipelineBuilderFactory>(KEYS.PipelineBuilderFactory)(config)
|
||||
.setHardwareAccelerationMode('vaapi')
|
||||
.setVideoInputSource(videoInputFile)
|
||||
.setAudioInputSource(audioInputFile)
|
||||
.setWatermarkInputSource(watermarkInputFile)
|
||||
.build();
|
||||
|
||||
const pipeline = builder.build(
|
||||
FfmpegState.create({
|
||||
version: {
|
||||
versionString: '7.0.1',
|
||||
isUnknown: false,
|
||||
} satisfies FfmpegVersionResult,
|
||||
vaapiDevice: '/dev/dri/renderD128',
|
||||
}),
|
||||
desiredState,
|
||||
);
|
||||
|
||||
const result = generator.generateArgs(
|
||||
videoInputFile,
|
||||
audioInputFile,
|
||||
watermarkInputFile,
|
||||
null,
|
||||
pipeline.steps,
|
||||
);
|
||||
|
||||
console.log(result.join(' '));
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,14 @@
|
||||
import type { FfmpegState } from '@/ffmpeg/builder/state/FfmpegState.js';
|
||||
import type { FrameState } from '@/ffmpeg/builder/state/FrameState.js';
|
||||
import { FrameDataLocation, type FrameSize } from '@/ffmpeg/builder/types.js';
|
||||
import { isNonEmptyString } from '@tunarr/shared/util';
|
||||
import { HardwareAccelerationMode } from '../../../db/schema/TranscodeConfig.ts';
|
||||
import { FilterOption } from './FilterOption.ts';
|
||||
import { HardwareDownloadFilter } from './HardwareDownloadFilter.ts';
|
||||
import { HardwareDownloadCudaFilter } from './nvidia/HardwareDownloadCudaFilter.ts';
|
||||
|
||||
export class ScaleFilter extends FilterOption {
|
||||
private hardwareDownloadFilter?: FilterOption;
|
||||
readonly filter: string;
|
||||
|
||||
readonly affectsFrameState = true;
|
||||
@@ -49,13 +54,23 @@ export class ScaleFilter extends FilterOption {
|
||||
}
|
||||
|
||||
if (this.currentState.frameDataLocation === FrameDataLocation.Hardware) {
|
||||
scaleFilter = `hwdownload,${scaleFilter}`;
|
||||
const hwdownload =
|
||||
this.ffmpegState.decoderHwAccelMode === HardwareAccelerationMode.Cuda
|
||||
? new HardwareDownloadCudaFilter(this.currentState, null)
|
||||
: new HardwareDownloadFilter(this.currentState);
|
||||
this.hardwareDownloadFilter = hwdownload;
|
||||
const hwdownloadFilter = hwdownload.filter;
|
||||
if (isNonEmptyString(hwdownloadFilter)) {
|
||||
scaleFilter = `${hwdownloadFilter},${scaleFilter}`;
|
||||
}
|
||||
}
|
||||
|
||||
return scaleFilter;
|
||||
}
|
||||
|
||||
nextState(currentState: FrameState): FrameState {
|
||||
currentState =
|
||||
this.hardwareDownloadFilter?.nextState(currentState) ?? currentState;
|
||||
return currentState.update({
|
||||
scaledSize: this.desiredScaledSize,
|
||||
paddedSize: this.desiredScaledSize,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { TonemapVaapiFilter } from '@/ffmpeg/builder/filter/vaapi/TonemapVaapiFilter.js';
|
||||
import {
|
||||
ColorRanges,
|
||||
ColorTransferFormats,
|
||||
} from '@/ffmpeg/builder/constants.js';
|
||||
import { TonemapVaapiFilter } from '@/ffmpeg/builder/filter/vaapi/TonemapVaapiFilter.js';
|
||||
import {
|
||||
PixelFormatNv12,
|
||||
PixelFormatYuv420P,
|
||||
@@ -107,7 +107,7 @@ describe('TonemapVaapiFilter', () => {
|
||||
const nextState = filter.nextState(currentState);
|
||||
|
||||
expect(nextState.pixelFormat).toMatchPixelFormat(
|
||||
new PixelFormatNv12(new PixelFormatYuv420P10Le()),
|
||||
new PixelFormatNv12(new PixelFormatYuv420P()),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -122,6 +122,8 @@ describe('TonemapVaapiFilter', () => {
|
||||
const filter = new TonemapVaapiFilter(currentState);
|
||||
const nextState = filter.nextState(currentState);
|
||||
|
||||
expect(nextState.pixelFormat).toBeNull();
|
||||
expect(nextState.pixelFormat).toMatchPixelFormat(
|
||||
new PixelFormatNv12(new PixelFormatYuv420P()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { FilterOption } from '@/ffmpeg/builder/filter/FilterOption.js';
|
||||
import { PixelFormatNv12 } from '@/ffmpeg/builder/format/PixelFormat.js';
|
||||
import {
|
||||
PixelFormatNv12,
|
||||
PixelFormatUnknown,
|
||||
PixelFormatYuv420P,
|
||||
} from '@/ffmpeg/builder/format/PixelFormat.js';
|
||||
import type { FrameState } from '@/ffmpeg/builder/state/FrameState.js';
|
||||
import { FrameDataLocation } from '@/ffmpeg/builder/types.js';
|
||||
import { ColorFormat } from '../../format/ColorFormat.ts';
|
||||
@@ -19,13 +23,15 @@ export class TonemapVaapiFilter extends FilterOption {
|
||||
public readonly affectsFrameState: boolean = true;
|
||||
|
||||
nextState(currentState: FrameState): FrameState {
|
||||
const currentPixelFormat = currentState.pixelFormat;
|
||||
const currentPixelFormat = currentState.pixelFormat?.unwrap();
|
||||
return currentState.update({
|
||||
colorFormat: ColorFormat.bt709,
|
||||
frameDataLocation: FrameDataLocation.Hardware,
|
||||
pixelFormat: currentPixelFormat
|
||||
? new PixelFormatNv12(currentPixelFormat)
|
||||
: null,
|
||||
pixelFormat: new PixelFormatNv12(
|
||||
(currentPixelFormat && currentPixelFormat.bitDepth === 8
|
||||
? currentPixelFormat
|
||||
: new PixelFormatYuv420P()) ?? PixelFormatUnknown(8),
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,10 @@ export abstract class HardwarePixelFormat extends BasePixelFormat {
|
||||
equals(other: PixelFormat): boolean {
|
||||
return super.equals(other) && this.unwrap().equals(other.unwrap());
|
||||
}
|
||||
|
||||
prettyPrint() {
|
||||
return `${this.constructor.name}(name=${this.name}, bitDepth=${this.bitDepth}, underlying=${this.underlying.prettyPrint()})`;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class SoftwarePixelFormat extends BasePixelFormat {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ColorFormat } from '@/ffmpeg/builder/format/ColorFormat.js';
|
||||
import { TONEMAP_ENABLED, TUNARR_ENV_VARS } from '@/util/env.js';
|
||||
import { FileStreamSource } from '../../../../stream/types.ts';
|
||||
import { FfmpegCapabilities } from '../../capabilities/FfmpegCapabilities.ts';
|
||||
@@ -12,9 +13,15 @@ import {
|
||||
ColorRanges,
|
||||
ColorSpaces,
|
||||
ColorTransferFormats,
|
||||
VideoFormats,
|
||||
} from '../../constants.ts';
|
||||
import { PadFilter } from '../../filter/PadFilter.ts';
|
||||
import { PadVaapiFilter } from '../../filter/vaapi/PadVaapiFilter.ts';
|
||||
import { ScaleVaapiFilter } from '../../filter/vaapi/ScaleVaapiFilter.ts';
|
||||
import { TonemapVaapiFilter } from '../../filter/vaapi/TonemapVaapiFilter.ts';
|
||||
import {
|
||||
PixelFormatRgba,
|
||||
PixelFormatUnknown,
|
||||
PixelFormatYuv420P,
|
||||
PixelFormatYuv420P10Le,
|
||||
} from '../../format/PixelFormat.ts';
|
||||
@@ -34,11 +41,12 @@ import { AudioState } from '../../state/AudioState.ts';
|
||||
import {
|
||||
DefaultPipelineOptions,
|
||||
FfmpegState,
|
||||
PipelineOptions,
|
||||
} from '../../state/FfmpegState.ts';
|
||||
import { FrameState } from '../../state/FrameState.ts';
|
||||
import { FrameState, FrameStateOpts } from '../../state/FrameState.ts';
|
||||
import { FrameSize } from '../../types.ts';
|
||||
import { Pipeline } from '../Pipeline.ts';
|
||||
import { VaapiPipelineBuilder } from './VaapiPipelineBuilder.ts';
|
||||
import { ColorFormat } from '@/ffmpeg/builder/format/ColorFormat.js';
|
||||
|
||||
describe('VaapiPipelineBuilder', () => {
|
||||
test('should work', () => {
|
||||
@@ -479,6 +487,21 @@ describe('VaapiPipelineBuilder pad', () => {
|
||||
isUnknown: false,
|
||||
};
|
||||
|
||||
// 16:9 FHD video that exactly fills the target: no padding needed
|
||||
// squarePixelFrameSize(FHD) = 1920x1080 = paddedSize
|
||||
function create169FhdVideoStream(): VideoStream {
|
||||
return VideoStream.create({
|
||||
index: 0,
|
||||
codec: 'h264',
|
||||
profile: 'main',
|
||||
pixelFormat: new PixelFormatYuv420P(),
|
||||
frameSize: FrameSize.FHD,
|
||||
displayAspectRatio: '16:9',
|
||||
providedSampleAspectRatio: '1:1',
|
||||
colorFormat: null,
|
||||
});
|
||||
}
|
||||
|
||||
// 4:3 video that needs pillarboxing to fit in 16:9 FHD:
|
||||
// squarePixelFrameSize(FHD) = 1440x1080, paddedSize = 1920x1080
|
||||
function create43VideoStream(): VideoStream {
|
||||
@@ -664,6 +687,57 @@ describe('VaapiPipelineBuilder pad', () => {
|
||||
expect(args).toContain('pad_vaapi=w=1920:h=1080');
|
||||
expect(args).not.toContain('pad=1920:1080');
|
||||
});
|
||||
|
||||
test('skips pad filter when current paddedSize already equals desired paddedSize (pad_vaapi available)', () => {
|
||||
// 16:9 FHD source fills the target frame exactly — no padding needed
|
||||
const pipeline = buildWithPad({ videoStream: create169FhdVideoStream() });
|
||||
|
||||
const videoFilters =
|
||||
pipeline.getComplexFilter()!.filterChain.videoFilterSteps;
|
||||
expect(videoFilters.some((f) => f instanceof PadVaapiFilter)).toBe(false);
|
||||
expect(videoFilters.some((f) => f instanceof PadFilter)).toBe(false);
|
||||
|
||||
const args = pipeline.getCommandArgs().join(' ');
|
||||
expect(args).not.toContain('pad_vaapi');
|
||||
expect(args).not.toContain('pad=');
|
||||
});
|
||||
|
||||
test('skips pad filter when current paddedSize already equals desired paddedSize (no pad_vaapi capability)', () => {
|
||||
const pipeline = buildWithPad({
|
||||
videoStream: create169FhdVideoStream(),
|
||||
binaryCapabilities: new FfmpegCapabilities(
|
||||
new Set(),
|
||||
new Map(),
|
||||
new Set(),
|
||||
new Set(),
|
||||
),
|
||||
});
|
||||
|
||||
const videoFilters =
|
||||
pipeline.getComplexFilter()!.filterChain.videoFilterSteps;
|
||||
expect(videoFilters.some((f) => f instanceof PadVaapiFilter)).toBe(false);
|
||||
expect(videoFilters.some((f) => f instanceof PadFilter)).toBe(false);
|
||||
|
||||
const args = pipeline.getCommandArgs().join(' ');
|
||||
expect(args).not.toContain('pad_vaapi');
|
||||
expect(args).not.toContain('pad=');
|
||||
});
|
||||
|
||||
test('skips pad filter when current paddedSize already equals desired paddedSize (hardware decoding disabled)', () => {
|
||||
const pipeline = buildWithPad({
|
||||
videoStream: create169FhdVideoStream(),
|
||||
disableHardwareDecoding: true,
|
||||
});
|
||||
|
||||
const videoFilters =
|
||||
pipeline.getComplexFilter()!.filterChain.videoFilterSteps;
|
||||
expect(videoFilters.some((f) => f instanceof PadVaapiFilter)).toBe(false);
|
||||
expect(videoFilters.some((f) => f instanceof PadFilter)).toBe(false);
|
||||
|
||||
const args = pipeline.getCommandArgs().join(' ');
|
||||
expect(args).not.toContain('pad_vaapi');
|
||||
expect(args).not.toContain('pad=');
|
||||
});
|
||||
});
|
||||
|
||||
describe('VaapiPipelineBuilder tonemap', () => {
|
||||
@@ -707,8 +781,9 @@ describe('VaapiPipelineBuilder tonemap', () => {
|
||||
function buildWithTonemap(opts: {
|
||||
videoStream: VideoStream;
|
||||
binaryCapabilities?: FfmpegCapabilities;
|
||||
disableHardwareFilters?: boolean;
|
||||
}) {
|
||||
pipelineOptions?: Partial<PipelineOptions>;
|
||||
desiredState?: Partial<FrameStateOpts>;
|
||||
}): Pipeline {
|
||||
const capabilities = new VaapiHardwareCapabilities([
|
||||
new VaapiProfileEntrypoint(
|
||||
VaapiProfiles.HevcMain10,
|
||||
@@ -725,7 +800,7 @@ describe('VaapiPipelineBuilder tonemap', () => {
|
||||
new FfmpegCapabilities(
|
||||
new Set(),
|
||||
new Map(),
|
||||
new Set([KnownFfmpegFilters.TonemapVaapi]),
|
||||
new Set([KnownFfmpegFilters.TonemapOpencl]),
|
||||
new Set(),
|
||||
);
|
||||
|
||||
@@ -746,40 +821,40 @@ describe('VaapiPipelineBuilder tonemap', () => {
|
||||
|
||||
const state = FfmpegState.create({ version: fakeVersion });
|
||||
|
||||
const pipeline = builder.build(
|
||||
state,
|
||||
new FrameState({
|
||||
isAnamorphic: false,
|
||||
scaledSize: FrameSize.FHD,
|
||||
paddedSize: FrameSize.FHD,
|
||||
pixelFormat: new PixelFormatYuv420P(),
|
||||
videoFormat: 'hevc',
|
||||
}),
|
||||
{
|
||||
...DefaultPipelineOptions,
|
||||
vaapiDevice: '/dev/dri/renderD128',
|
||||
disableHardwareFilters: opts.disableHardwareFilters ?? false,
|
||||
const desiredState = new FrameState({
|
||||
isAnamorphic: false,
|
||||
scaledSize: FrameSize.FHD,
|
||||
paddedSize: FrameSize.FHD,
|
||||
pixelFormat: new PixelFormatYuv420P(),
|
||||
videoFormat: VideoFormats.Hevc,
|
||||
...(opts.desiredState ?? {}),
|
||||
});
|
||||
|
||||
const pipeline = builder.build(state, desiredState, {
|
||||
...DefaultPipelineOptions,
|
||||
...(opts.pipelineOptions ?? {}),
|
||||
vaapiDevice: '/dev/dri/renderD128',
|
||||
vaapiPipelineOptions: {
|
||||
tonemapPreference: 'opencl',
|
||||
...(opts.pipelineOptions?.vaapiPipelineOptions ?? {}),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
function hasTonemapFilter(pipeline: ReturnType<typeof buildWithTonemap>) {
|
||||
const args = pipeline.getCommandArgs().join(' ');
|
||||
return args.includes('tonemap_vaapi');
|
||||
function hasVaapiTonemapFilter(pipeline: Pipeline) {
|
||||
const filterChain =
|
||||
pipeline.getComplexFilter()?.filterChain.videoFilterSteps ?? [];
|
||||
return filterChain.some((filter) => filter instanceof TonemapVaapiFilter);
|
||||
}
|
||||
|
||||
function hasOpenclTonemapFilter(
|
||||
pipeline: ReturnType<typeof buildWithTonemap>,
|
||||
) {
|
||||
function hasOpenclTonemapFilter(pipeline: Pipeline) {
|
||||
const args = pipeline.getCommandArgs().join(' ');
|
||||
return args.includes('tonemap_opencl');
|
||||
}
|
||||
|
||||
function hasSoftwareTonemapFilter(
|
||||
pipeline: ReturnType<typeof buildWithTonemap>,
|
||||
) {
|
||||
function hasSoftwareTonemapFilter(pipeline: Pipeline) {
|
||||
const args = pipeline.getCommandArgs().join(' ');
|
||||
return args.includes('zscale') && args.includes('tonemap=tonemap=hable');
|
||||
}
|
||||
@@ -798,10 +873,13 @@ describe('VaapiPipelineBuilder tonemap', () => {
|
||||
),
|
||||
});
|
||||
|
||||
expect(hasTonemapFilter(pipeline)).to.eq(true);
|
||||
expect(hasOpenclTonemapFilter(pipeline)).to.eq(true);
|
||||
expect(hasVaapiTonemapFilter(pipeline)).to.eq(false);
|
||||
|
||||
const args = pipeline.getCommandArgs().join(' ');
|
||||
expect(args).toContain('tonemap_vaapi=format=nv12:t=bt709:m=bt709:p=bt709');
|
||||
expect(args).toContain('tonemap_opencl=tonemap=hable');
|
||||
expect(args).toContain('hwmap=derive_device=opencl');
|
||||
expect(args).toContain('hwmap=derive_device=vaapi:reverse=1');
|
||||
});
|
||||
|
||||
test('applies tonemap filter for HLG (arib-std-b67) content', () => {
|
||||
@@ -818,7 +896,8 @@ describe('VaapiPipelineBuilder tonemap', () => {
|
||||
),
|
||||
});
|
||||
|
||||
expect(hasTonemapFilter(pipeline)).to.eq(true);
|
||||
expect(hasOpenclTonemapFilter(pipeline)).to.eq(true);
|
||||
expect(hasVaapiTonemapFilter(pipeline)).to.eq(false);
|
||||
});
|
||||
|
||||
test('skips tonemap when TONEMAP_ENABLED is false', () => {
|
||||
@@ -828,7 +907,7 @@ describe('VaapiPipelineBuilder tonemap', () => {
|
||||
videoStream: createHdrVideoStream(),
|
||||
});
|
||||
|
||||
expect(hasTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(hasVaapiTonemapFilter(pipeline)).to.eq(false);
|
||||
});
|
||||
|
||||
test('skips tonemap when content is SDR', () => {
|
||||
@@ -852,7 +931,7 @@ describe('VaapiPipelineBuilder tonemap', () => {
|
||||
|
||||
const pipeline = buildWithTonemap({ videoStream: sdrStream });
|
||||
|
||||
expect(hasTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(hasVaapiTonemapFilter(pipeline)).to.eq(false);
|
||||
});
|
||||
|
||||
test('falls back to software tonemap when neither tonemap_vaapi nor tonemap_opencl is available', () => {
|
||||
@@ -868,7 +947,7 @@ describe('VaapiPipelineBuilder tonemap', () => {
|
||||
),
|
||||
});
|
||||
|
||||
expect(hasTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(hasVaapiTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(hasOpenclTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(hasSoftwareTonemapFilter(pipeline)).to.eq(true);
|
||||
});
|
||||
@@ -878,10 +957,12 @@ describe('VaapiPipelineBuilder tonemap', () => {
|
||||
|
||||
const pipeline = buildWithTonemap({
|
||||
videoStream: createHdrVideoStream(),
|
||||
disableHardwareFilters: true,
|
||||
pipelineOptions: {
|
||||
disableHardwareFilters: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(hasTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(hasVaapiTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(hasOpenclTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(hasSoftwareTonemapFilter(pipeline)).to.eq(true);
|
||||
});
|
||||
@@ -894,7 +975,7 @@ describe('VaapiPipelineBuilder tonemap', () => {
|
||||
});
|
||||
|
||||
const args = pipeline.getCommandArgs().join(' ');
|
||||
const tonemapIndex = args.indexOf('tonemap_vaapi');
|
||||
const tonemapIndex = args.indexOf('tonemap_opencl');
|
||||
const scaleIndex = args.indexOf('scale_vaapi');
|
||||
|
||||
expect(tonemapIndex).toBeGreaterThan(-1);
|
||||
@@ -902,7 +983,7 @@ describe('VaapiPipelineBuilder tonemap', () => {
|
||||
expect(tonemapIndex).toBeLessThan(scaleIndex);
|
||||
});
|
||||
|
||||
test('falls back to tonemap_opencl when tonemap_vaapi is unavailable', () => {
|
||||
test('uses tonemap_vaapi when preference is explicitly vaapi and opencl is unavailable', () => {
|
||||
process.env[TONEMAP_ENABLED] = 'true';
|
||||
|
||||
const pipeline = buildWithTonemap({
|
||||
@@ -910,20 +991,21 @@ describe('VaapiPipelineBuilder tonemap', () => {
|
||||
binaryCapabilities: new FfmpegCapabilities(
|
||||
new Set(),
|
||||
new Map(),
|
||||
new Set([KnownFfmpegFilters.TonemapOpencl]),
|
||||
new Set([KnownFfmpegFilters.TonemapVaapi]),
|
||||
new Set(),
|
||||
),
|
||||
pipelineOptions: {
|
||||
vaapiPipelineOptions: { tonemapPreference: 'vaapi' },
|
||||
},
|
||||
});
|
||||
|
||||
const args = pipeline.getCommandArgs().join(' ');
|
||||
expect(hasTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(hasOpenclTonemapFilter(pipeline)).to.eq(true);
|
||||
expect(args).toContain('tonemap_opencl=tonemap=hable');
|
||||
expect(args).toContain('hwmap=derive_device=opencl');
|
||||
expect(args).toContain('hwmap=derive_device=vaapi:reverse=1');
|
||||
expect(hasVaapiTonemapFilter(pipeline)).to.eq(true);
|
||||
expect(hasOpenclTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(args).toContain('tonemap_vaapi=format=nv12:t=bt709:m=bt709:p=bt709');
|
||||
});
|
||||
|
||||
test('prefers tonemap_vaapi over tonemap_opencl when both are available', () => {
|
||||
test('prefers tonemap_opencl over tonemap_vaapi when both are available', () => {
|
||||
process.env[TONEMAP_ENABLED] = 'true';
|
||||
|
||||
const pipeline = buildWithTonemap({
|
||||
@@ -939,8 +1021,8 @@ describe('VaapiPipelineBuilder tonemap', () => {
|
||||
),
|
||||
});
|
||||
|
||||
expect(hasTonemapFilter(pipeline)).to.eq(true);
|
||||
expect(hasOpenclTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(hasOpenclTonemapFilter(pipeline)).to.eq(true);
|
||||
expect(hasVaapiTonemapFilter(pipeline)).to.eq(false);
|
||||
});
|
||||
|
||||
test('opencl tonemap filter appears before scale in the filter chain', () => {
|
||||
@@ -976,14 +1058,16 @@ describe('VaapiPipelineBuilder tonemap', () => {
|
||||
new Set([KnownFfmpegFilters.TonemapOpencl]),
|
||||
new Set(),
|
||||
),
|
||||
disableHardwareFilters: true,
|
||||
pipelineOptions: {
|
||||
disableHardwareFilters: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(hasOpenclTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(hasSoftwareTonemapFilter(pipeline)).to.eq(true);
|
||||
});
|
||||
|
||||
test('applies tonemap_vaapi for Dolby Vision content (dvhe codec)', () => {
|
||||
test('applies tonemap_opencl for Dolby Vision content (dvhe codec)', () => {
|
||||
process.env[TONEMAP_ENABLED] = 'true';
|
||||
|
||||
const dvStream = VideoStream.create({
|
||||
@@ -1004,8 +1088,8 @@ describe('VaapiPipelineBuilder tonemap', () => {
|
||||
|
||||
const pipeline = buildWithTonemap({ videoStream: dvStream });
|
||||
|
||||
expect(hasTonemapFilter(pipeline)).to.eq(true);
|
||||
expect(hasOpenclTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(hasOpenclTonemapFilter(pipeline)).to.eq(true);
|
||||
expect(hasVaapiTonemapFilter(pipeline)).to.eq(false);
|
||||
});
|
||||
|
||||
test('applies software tonemap for Dolby Vision (dvhe codec) when hardware filters are disabled', () => {
|
||||
@@ -1029,10 +1113,12 @@ describe('VaapiPipelineBuilder tonemap', () => {
|
||||
|
||||
const pipeline = buildWithTonemap({
|
||||
videoStream: dvStream,
|
||||
disableHardwareFilters: true,
|
||||
pipelineOptions: {
|
||||
disableHardwareFilters: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(hasTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(hasVaapiTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(hasOpenclTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(hasSoftwareTonemapFilter(pipeline)).to.eq(true);
|
||||
});
|
||||
@@ -1067,8 +1153,208 @@ describe('VaapiPipelineBuilder tonemap', () => {
|
||||
});
|
||||
|
||||
// No hardware tonemap filter, falls back to software
|
||||
expect(hasTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(hasVaapiTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(hasOpenclTonemapFilter(pipeline)).to.eq(false);
|
||||
expect(hasSoftwareTonemapFilter(pipeline)).to.eq(true);
|
||||
});
|
||||
|
||||
test('yuv420p10le input ensures outputted pixel format is 8-bit nv12', () => {
|
||||
process.env[TONEMAP_ENABLED] = 'true';
|
||||
|
||||
const stream = VideoStream.create({
|
||||
index: 0,
|
||||
codec: VideoFormats.Hevc,
|
||||
profile: 'main 10',
|
||||
pixelFormat: new PixelFormatYuv420P10Le(),
|
||||
frameSize: FrameSize.withDimensions(3840, 2076),
|
||||
displayAspectRatio: '16:9',
|
||||
providedSampleAspectRatio: '1:1',
|
||||
colorFormat: new ColorFormat({
|
||||
colorRange: ColorRanges.Tv,
|
||||
colorSpace: ColorSpaces.Bt2020nc,
|
||||
colorPrimaries: ColorPrimaries.Bt2020,
|
||||
colorTransfer: ColorTransferFormats.Smpte2084,
|
||||
}),
|
||||
});
|
||||
|
||||
const pipeline = buildWithTonemap({
|
||||
videoStream: stream,
|
||||
binaryCapabilities: new FfmpegCapabilities(
|
||||
new Set(),
|
||||
new Map(),
|
||||
new Set([KnownFfmpegFilters.TonemapVaapi]),
|
||||
new Set(),
|
||||
),
|
||||
pipelineOptions: {
|
||||
vaapiPipelineOptions: { tonemapPreference: 'vaapi' },
|
||||
},
|
||||
desiredState: {
|
||||
scaledSize: stream.squarePixelFrameSize(FrameSize.FourK),
|
||||
paddedSize: FrameSize.FourK,
|
||||
},
|
||||
});
|
||||
|
||||
const padFilter = pipeline
|
||||
.getComplexFilter()!
|
||||
.filterChain.videoFilterSteps.find((step) => step instanceof PadFilter);
|
||||
console.log(pipeline.getCommandArgs().join(' '));
|
||||
expect(padFilter).toBeDefined();
|
||||
expect(padFilter!.filter).toEqual(
|
||||
'hwdownload,format=nv12,pad=3840:2160:-1:-1:color=black',
|
||||
);
|
||||
});
|
||||
|
||||
test('unknown pixel format properly wraps in nv12 after tonemapping', () => {
|
||||
process.env[TONEMAP_ENABLED] = 'true';
|
||||
|
||||
const stream = VideoStream.create({
|
||||
index: 0,
|
||||
codec: VideoFormats.Hevc,
|
||||
profile: 'main 10',
|
||||
pixelFormat: PixelFormatUnknown(10),
|
||||
frameSize: FrameSize.withDimensions(3840, 2076),
|
||||
displayAspectRatio: '16:9',
|
||||
providedSampleAspectRatio: '1:1',
|
||||
colorFormat: new ColorFormat({
|
||||
colorRange: ColorRanges.Tv,
|
||||
colorSpace: ColorSpaces.Bt2020nc,
|
||||
colorPrimaries: ColorPrimaries.Bt2020,
|
||||
colorTransfer: ColorTransferFormats.Smpte2084,
|
||||
}),
|
||||
});
|
||||
|
||||
const pipeline = buildWithTonemap({
|
||||
videoStream: stream,
|
||||
binaryCapabilities: new FfmpegCapabilities(
|
||||
new Set(),
|
||||
new Map(),
|
||||
new Set([KnownFfmpegFilters.TonemapVaapi]),
|
||||
new Set(),
|
||||
),
|
||||
pipelineOptions: {
|
||||
vaapiPipelineOptions: { tonemapPreference: 'vaapi' },
|
||||
},
|
||||
desiredState: {
|
||||
scaledSize: stream.squarePixelFrameSize(FrameSize.FourK),
|
||||
paddedSize: FrameSize.FourK,
|
||||
},
|
||||
});
|
||||
|
||||
const padFilter = pipeline
|
||||
.getComplexFilter()!
|
||||
.filterChain.videoFilterSteps.find((step) => step instanceof PadFilter);
|
||||
expect(padFilter).toBeDefined();
|
||||
expect(padFilter!.filter).toEqual(
|
||||
'hwdownload,format=nv12,pad=3840:2160:-1:-1:color=black',
|
||||
);
|
||||
});
|
||||
|
||||
test('tonemap_vaapi includes format upload prefix when frame data is in software (hardware decoding disabled)', () => {
|
||||
process.env[TONEMAP_ENABLED] = 'true';
|
||||
|
||||
const pipeline = buildWithTonemap({
|
||||
videoStream: createHdrVideoStream(),
|
||||
binaryCapabilities: new FfmpegCapabilities(
|
||||
new Set(),
|
||||
new Map(),
|
||||
new Set([KnownFfmpegFilters.TonemapVaapi]),
|
||||
new Set(),
|
||||
),
|
||||
pipelineOptions: {
|
||||
disableHardwareDecoding: true,
|
||||
vaapiPipelineOptions: { tonemapPreference: 'vaapi' },
|
||||
},
|
||||
});
|
||||
|
||||
const args = pipeline.getCommandArgs().join(' ');
|
||||
expect(args).toContain(
|
||||
'format=vaapi|nv12|p010le,tonemap_vaapi=format=nv12:t=bt709:m=bt709:p=bt709',
|
||||
);
|
||||
});
|
||||
|
||||
// This test verifies that software decode triggers a scale_vaapi because of the tonemap
|
||||
// to ensure we don't excessively move frames from hardware <-> software
|
||||
test('8-bit yuv420p HDR input uses vaapi tonemap and scale_vaapi (software decode)', () => {
|
||||
process.env[TONEMAP_ENABLED] = 'true';
|
||||
|
||||
// Unusual but valid: 8-bit stream tagged with HDR color metadata
|
||||
const stream = VideoStream.create({
|
||||
index: 0,
|
||||
codec: VideoFormats.Hevc,
|
||||
// Explicitly trigger a software decode
|
||||
profile: 'main',
|
||||
pixelFormat: new PixelFormatYuv420P(),
|
||||
frameSize: FrameSize.FourK,
|
||||
displayAspectRatio: '16:9',
|
||||
providedSampleAspectRatio: '1:1',
|
||||
colorFormat: new ColorFormat({
|
||||
colorRange: ColorRanges.Tv,
|
||||
colorSpace: ColorSpaces.Bt2020nc,
|
||||
colorPrimaries: ColorPrimaries.Bt2020,
|
||||
colorTransfer: ColorTransferFormats.Smpte2084,
|
||||
}),
|
||||
});
|
||||
|
||||
const pipeline = buildWithTonemap({
|
||||
videoStream: stream,
|
||||
binaryCapabilities: new FfmpegCapabilities(
|
||||
new Set(),
|
||||
new Map(),
|
||||
new Set([KnownFfmpegFilters.TonemapVaapi]),
|
||||
new Set(),
|
||||
),
|
||||
pipelineOptions: {
|
||||
vaapiPipelineOptions: { tonemapPreference: 'vaapi' },
|
||||
},
|
||||
});
|
||||
|
||||
const filters = pipeline.getComplexFilter()!.filterChain.videoFilterSteps;
|
||||
expect(hasVaapiTonemapFilter(pipeline)).to.eq(true);
|
||||
const scaleFilter = filters.find(
|
||||
(filter) => filter instanceof ScaleVaapiFilter,
|
||||
);
|
||||
expect(scaleFilter).toBeDefined();
|
||||
});
|
||||
|
||||
// This test verifies that hardware decode also uses scale_vaapi after vaapi tonemap
|
||||
test('8-bit yuv420p HDR input uses vaapi tonemap and scale_vaapi (hardware decode)', () => {
|
||||
process.env[TONEMAP_ENABLED] = 'true';
|
||||
|
||||
// Unusual but valid: 8-bit stream tagged with HDR color metadata
|
||||
const stream = VideoStream.create({
|
||||
index: 0,
|
||||
codec: VideoFormats.Hevc,
|
||||
profile: 'main 10',
|
||||
pixelFormat: new PixelFormatYuv420P(),
|
||||
frameSize: FrameSize.FourK,
|
||||
displayAspectRatio: '16:9',
|
||||
providedSampleAspectRatio: '1:1',
|
||||
colorFormat: new ColorFormat({
|
||||
colorRange: ColorRanges.Tv,
|
||||
colorSpace: ColorSpaces.Bt2020nc,
|
||||
colorPrimaries: ColorPrimaries.Bt2020,
|
||||
colorTransfer: ColorTransferFormats.Smpte2084,
|
||||
}),
|
||||
});
|
||||
|
||||
const pipeline = buildWithTonemap({
|
||||
videoStream: stream,
|
||||
binaryCapabilities: new FfmpegCapabilities(
|
||||
new Set(),
|
||||
new Map(),
|
||||
new Set([KnownFfmpegFilters.TonemapVaapi]),
|
||||
new Set(),
|
||||
),
|
||||
pipelineOptions: {
|
||||
vaapiPipelineOptions: { tonemapPreference: 'vaapi' },
|
||||
},
|
||||
});
|
||||
|
||||
const filters = pipeline.getComplexFilter()!.filterChain.videoFilterSteps;
|
||||
expect(hasVaapiTonemapFilter(pipeline)).to.eq(true);
|
||||
const scaleFilter = filters.find(
|
||||
(filter) => filter instanceof ScaleVaapiFilter,
|
||||
);
|
||||
expect(scaleFilter).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
PixelFormats,
|
||||
} from '../../format/PixelFormat.ts';
|
||||
import type { SubtitlesInputSource } from '../../input/SubtitlesInputSource.ts';
|
||||
import type { VideoStream } from '../../MediaStream.ts';
|
||||
import { CopyTimestampInputOption } from '../../options/input/CopyTimestampInputOption.ts';
|
||||
import {
|
||||
NoAutoScaleOutputOption,
|
||||
@@ -356,21 +357,29 @@ export class VaapiPipelineBuilder extends SoftwarePipelineBuilder {
|
||||
|
||||
const { videoStream, pipelineOptions } = this.context;
|
||||
|
||||
if (
|
||||
!getBooleanEnvVar(TONEMAP_ENABLED, false) ||
|
||||
!isHdrContent(videoStream)
|
||||
) {
|
||||
if (!this.shouldPerformTonemap(videoStream)) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
let filter: FilterOption | undefined;
|
||||
if (!pipelineOptions.disableHardwareFilters) {
|
||||
if (this.ffmpegCapabilities.hasFilter(KnownFfmpegFilters.TonemapVaapi)) {
|
||||
filter = new TonemapVaapiFilter(currentState);
|
||||
} else if (
|
||||
this.ffmpegCapabilities.hasFilter(KnownFfmpegFilters.TonemapOpencl)
|
||||
) {
|
||||
let preference = pipelineOptions.vaapiPipelineOptions?.tonemapPreference;
|
||||
const hasOpencl = this.ffmpegCapabilities.hasFilter(
|
||||
KnownFfmpegFilters.TonemapOpencl,
|
||||
);
|
||||
const hasVaapi = this.ffmpegCapabilities.hasFilter(
|
||||
KnownFfmpegFilters.TonemapVaapi,
|
||||
);
|
||||
|
||||
// Default preference is opencl.
|
||||
if (!preference) {
|
||||
preference = 'opencl';
|
||||
}
|
||||
|
||||
if (preference === 'opencl' && hasOpencl) {
|
||||
filter = new TonemapOpenclFilter(currentState);
|
||||
} else if (preference === 'vaapi' && hasVaapi) {
|
||||
filter = new TonemapVaapiFilter(currentState);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,15 +394,26 @@ export class VaapiPipelineBuilder extends SoftwarePipelineBuilder {
|
||||
}
|
||||
|
||||
protected setScale(currentState: FrameState): FrameState {
|
||||
if (!isVideoPipelineContext(this.context)) {
|
||||
return currentState;
|
||||
}
|
||||
let nextState = currentState;
|
||||
const { desiredState, ffmpegState, shouldDeinterlace } = this.context;
|
||||
|
||||
const { desiredState, ffmpegState, shouldDeinterlace, videoStream } =
|
||||
this.context;
|
||||
|
||||
let scaleOption: FilterOption;
|
||||
if (
|
||||
!currentState.scaledSize.equals(desiredState.scaledSize) &&
|
||||
((ffmpegState.decoderHwAccelMode === HardwareAccelerationMode.None &&
|
||||
ffmpegState.encoderHwAccelMode === HardwareAccelerationMode.None &&
|
||||
!shouldDeinterlace) ||
|
||||
ffmpegState.decoderHwAccelMode !== HardwareAccelerationMode.Vaapi)
|
||||
// Software decode and no tonemap implies we're already in software. If we're tonemapping but
|
||||
// performed a software decode, we'll have had to upload to hardware to tonemap anyway (most likely)
|
||||
// so try to continue on hardware if possible
|
||||
(ffmpegState.decoderHwAccelMode !== HardwareAccelerationMode.Vaapi &&
|
||||
!this.shouldPerformTonemap(videoStream) &&
|
||||
this.canTonemapOnHardware()))
|
||||
) {
|
||||
scaleOption = ScaleFilter.create(
|
||||
currentState,
|
||||
@@ -428,7 +448,7 @@ export class VaapiPipelineBuilder extends SoftwarePipelineBuilder {
|
||||
|
||||
protected setPad(currentState: FrameState) {
|
||||
if (
|
||||
this.desiredState.croppedSize &&
|
||||
this.desiredState.croppedSize ||
|
||||
currentState.paddedSize.equals(this.desiredState.paddedSize)
|
||||
) {
|
||||
return currentState;
|
||||
@@ -613,4 +633,18 @@ export class VaapiPipelineBuilder extends SoftwarePipelineBuilder {
|
||||
!(this.ffmpegState.vaapiDriver ?? '').toLowerCase().startsWith('radeon')
|
||||
);
|
||||
}
|
||||
|
||||
private shouldPerformTonemap(videoStream: VideoStream) {
|
||||
return (
|
||||
getBooleanEnvVar(TONEMAP_ENABLED, false) && isHdrContent(videoStream)
|
||||
);
|
||||
}
|
||||
|
||||
private canTonemapOnHardware() {
|
||||
return (
|
||||
!this.context.pipelineOptions.disableHardwareFilters &&
|
||||
(this.ffmpegCapabilities.hasFilter(KnownFfmpegFilters.TonemapVaapi) ||
|
||||
this.ffmpegCapabilities.hasFilter(KnownFfmpegFilters.TonemapOpencl))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HardwareAccelerationMode } from '@/db/schema/TranscodeConfig.js';
|
||||
import type { FfmpegVersionResult } from '@/ffmpeg/ffmpegInfo.js';
|
||||
import type { DataProps, Maybe, Nullable } from '@/types/util.js';
|
||||
import type { TupleToUnion } from '@tunarr/types';
|
||||
import type { FfmpegLogLevel } from '@tunarr/types/schemas';
|
||||
import type { Duration } from 'dayjs/plugin/duration.js';
|
||||
import { merge } from 'lodash-es';
|
||||
@@ -13,6 +14,15 @@ import {
|
||||
OutputLocation,
|
||||
} from '../constants.ts';
|
||||
|
||||
export const VaapiTonemapType = ['vaapi', 'opencl'] as const;
|
||||
export type VaapiTonemapType = TupleToUnion<typeof VaapiTonemapType>;
|
||||
|
||||
export type VaapiPipelineOptions = {
|
||||
// Ordered list of preferred tonemap types. Pipeline will cross
|
||||
// reference the list based on what is available on the system.
|
||||
tonemapPreference: VaapiTonemapType;
|
||||
};
|
||||
|
||||
export type PipelineOptions = {
|
||||
decoderThreadCount: Nullable<number>;
|
||||
encoderThreadCount: Nullable<number>;
|
||||
@@ -20,8 +30,11 @@ export type PipelineOptions = {
|
||||
disableHardwareDecoding?: boolean;
|
||||
disableHardwareEncoding?: boolean;
|
||||
disableHardwareFilters?: boolean;
|
||||
// TODO: Move these into the vaapi pipeline options
|
||||
vaapiDevice: Nullable<string>;
|
||||
vaapiDriver: Nullable<string>;
|
||||
// VAAPI
|
||||
vaapiPipelineOptions: Nullable<VaapiPipelineOptions>;
|
||||
};
|
||||
|
||||
export const DefaultPipelineOptions: PipelineOptions = {
|
||||
@@ -33,6 +46,9 @@ export const DefaultPipelineOptions: PipelineOptions = {
|
||||
disableHardwareFilters: false,
|
||||
vaapiDevice: null,
|
||||
vaapiDriver: null,
|
||||
vaapiPipelineOptions: {
|
||||
tonemapPreference: 'opencl',
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultFfmpegState: Partial<DataProps<FfmpegState>> = {
|
||||
|
||||
@@ -34,6 +34,11 @@ export const DefaultFrameState: Omit<
|
||||
infiniteLoop: false,
|
||||
};
|
||||
|
||||
export type FrameStateOpts = MarkOptional<
|
||||
FrameStateFields,
|
||||
keyof typeof DefaultFrameState
|
||||
>;
|
||||
|
||||
export class FrameState {
|
||||
scaledSize: FrameSize;
|
||||
paddedSize: FrameSize;
|
||||
@@ -55,9 +60,7 @@ export class FrameState {
|
||||
|
||||
forceSoftwareOverlay = false;
|
||||
|
||||
constructor(
|
||||
fields: MarkOptional<FrameStateFields, keyof typeof DefaultFrameState>,
|
||||
) {
|
||||
constructor(fields: FrameStateOpts) {
|
||||
merge(this, DefaultFrameState, fields);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import type { Dayjs } from 'dayjs';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
filter,
|
||||
findIndex,
|
||||
first,
|
||||
indexOf,
|
||||
isEmpty,
|
||||
last,
|
||||
merge,
|
||||
@@ -202,7 +202,13 @@ export class HlsPlaylistMutator {
|
||||
const startSequence = first(allSegments)?.startSequence ?? 0;
|
||||
|
||||
if (!isEmpty(allSegments)) {
|
||||
const index = indexOf(items, first(allSegments));
|
||||
const target = first(allSegments);
|
||||
const index = target
|
||||
? findIndex(
|
||||
items,
|
||||
(item) => item.type === 'segment' && item.equals(target),
|
||||
)
|
||||
: -1;
|
||||
discontinuitySequence += filter(take(items, index + 1), {
|
||||
type: 'discontinuity',
|
||||
}).length;
|
||||
@@ -267,6 +273,15 @@ class PlaylistSegment {
|
||||
const match = nth(matches, 1);
|
||||
return match ? parseInt(match) : null;
|
||||
}
|
||||
|
||||
equals(other: PlaylistSegment) {
|
||||
return (
|
||||
this === other ||
|
||||
(this.startTime.isSame(other.startTime) &&
|
||||
this.extInf === other.extInf &&
|
||||
this.line === other.line)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type PlaylistDiscontinuity = {
|
||||
|
||||
Reference in New Issue
Block a user