Skip to content

Commit

Permalink
fix: fix HLS concat on new FFmpeg pipeline
Browse files Browse the repository at this point in the history
Includes some other fixes around audio channels and padding on VAAPI

Fixes #974
  • Loading branch information
chrisbenincasa committed Nov 21, 2024
1 parent f2000fb commit 3a73965
Show file tree
Hide file tree
Showing 22 changed files with 276 additions and 164 deletions.
18 changes: 18 additions & 0 deletions server/src/ffmpeg/FFmpegFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Channel } from '@/db/schema/Channel.ts';
import { FfmpegSettings } from '@tunarr/types';
import { FfmpegStreamFactory } from './FfmpegStreamFactory.ts';
import { FFMPEG } from './ffmpeg.ts';
import { IFFMPEG } from './ffmpegBase.ts';

export class FFmpegFactory {
static getFFmpegPipelineBuilder(
settings: FfmpegSettings,
channel: Channel,
): IFFMPEG {
if (settings.useNewFfmpegPipeline) {
return new FfmpegStreamFactory(settings, channel);
} else {
return new FFMPEG(settings, channel);
}
}
}
53 changes: 37 additions & 16 deletions server/src/ffmpeg/FfmpegStreamFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ import { FfmpegState } from './builder/state/FfmpegState.ts';
import { FrameState } from './builder/state/FrameState.ts';
import { FrameSize } from './builder/types.ts';
import { ConcatOptions, StreamSessionOptions } from './ffmpeg.ts';
import { IFFMPEG } from './ffmpegBase.ts';
import {
ConcatStreamModeToChildMode,
HlsWrapperOptions,
IFFMPEG,
} from './ffmpegBase.ts';
import { FFMPEGInfo } from './ffmpegInfo.ts';

export class FfmpegStreamFactory extends IFFMPEG {
Expand All @@ -57,7 +61,7 @@ export class FfmpegStreamFactory extends IFFMPEG {

async createConcatSession(
streamUrl: string,
opts: DeepReadonly<Partial<ConcatOptions>>,
opts: DeepReadonly<ConcatOptions>,
): Promise<FfmpegTranscodeSession> {
const concatInput = new ConcatInputSource(
new HttpStreamSource(streamUrl),
Expand All @@ -73,24 +77,25 @@ export class FfmpegStreamFactory extends IFFMPEG {

const calculator = new FfmpegPlaybackParamsCalculator(this.ffmpegSettings);

const pipeline = pipelineBuilder.build(
const pipeline = pipelineBuilder.concat(
concatInput,
FfmpegState.create({
version: await this.ffmpegInfo.getVersion(),
outputFormat: opts.outputFormat ?? MpegTsOutputFormat,
metadataServiceProvider: 'Tunarr',
metadataServiceName: this.channel.name,
ptsOffset: 0,
}),
new FrameState({
...calculator.calculateForHlsConcat(),
scaledSize: FrameSize.fromResolution(
this.ffmpegSettings.targetResolution,
),
paddedSize: FrameSize.fromResolution(
this.ffmpegSettings.targetResolution,
),
isAnamorphic: false,
}),
// new FrameState({
// ...calculator.calculateForHlsConcat(),
// scaledSize: FrameSize.fromResolution(
// this.ffmpegSettings.targetResolution,
// ),
// paddedSize: FrameSize.fromResolution(
// this.ffmpegSettings.targetResolution,
// ),
// isAnamorphic: false,
// }),
);

return new FfmpegTranscodeSession(
Expand All @@ -105,8 +110,9 @@ export class FfmpegStreamFactory extends IFFMPEG {
);
}

async createWrapperConcatSession(
async createHlsWrapperSession(
streamUrl: string,
opts: HlsWrapperOptions,
): Promise<FfmpegTranscodeSession> {
const concatInput = new ConcatInputSource(
new HttpStreamSource(streamUrl),
Expand All @@ -116,12 +122,21 @@ export class FfmpegStreamFactory extends IFFMPEG {
}),
);

switch (ConcatStreamModeToChildMode[opts.mode]) {
case 'hls':
return this.createHlsConcatSession(concatInput);
case 'hls_slower':
return this.createHlsSlowerConcatSession(concatInput);
}
}

private async createHlsConcatSession(concatInput: ConcatInputSource) {
const pipelineBuilder = await this.pipelineBuilderFactory
.builder(this.ffmpegSettings)
.setConcatInputSource(concatInput)
.build();

const pipeline = pipelineBuilder.hlsConcat(
const pipeline = pipelineBuilder.hlsWrap(
concatInput,
FfmpegState.forConcat(
await this.ffmpegInfo.getVersion(),
Expand All @@ -141,6 +156,12 @@ export class FfmpegStreamFactory extends IFFMPEG {
);
}

private async createHlsSlowerConcatSession(
concatInput: ConcatInputSource,
): Promise<FfmpegTranscodeSession> {
throw new Error('not yet implemented');
}

async createStreamSession({
// TODO Fix these dumb params
streamSource,
Expand Down Expand Up @@ -234,7 +255,7 @@ export class FfmpegStreamFactory extends IFFMPEG {
AudioStream.create({
index: isNaN(audioStreamIndex) ? 1 : audioStreamIndex,
codec: audioStream.codec ?? 'unknown',
channels: playbackParams.audioChannels ?? -2,
channels: audioStream.channels ?? -2,
}),
],
audioState,
Expand Down
6 changes: 5 additions & 1 deletion server/src/ffmpeg/builder/filter/vaapi/ScaleVaapiFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ export class ScaleVaapiFilter extends FilterOption {
private paddedSize: FrameSize,
) {
super();
this.filter = this.genFilter();
}

public get filter(): string {
public readonly filter: string;

private genFilter(): string {
let scale = '';

if (this.currentState.scaledSize.equals(this.scaledSize)) {
Expand All @@ -23,6 +26,7 @@ export class ScaleVaapiFilter extends FilterOption {
let aspectRatio = '';
if (!this.scaledSize.equals(this.paddedSize)) {
// Set cropped size
aspectRatio = ':force_original_aspect_ratio=decrease';
}

let squareScale = '';
Expand Down
1 change: 0 additions & 1 deletion server/src/ffmpeg/builder/options/AudioOutputOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export const AudioChannelsOutputOption = (
desiredChannels: number,
) => {
const opts: string[] = [];
// TODO Audio format constants
if (
sourceChannels !== desiredChannels ||
(audioFormat === 'aac' && desiredChannels > 2)
Expand Down
10 changes: 7 additions & 3 deletions server/src/ffmpeg/builder/options/OutputOption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export const MetadataServiceNameOutputOption = (serviceName: string) =>
export const DoNotMapMetadataOutputOption = () =>
makeConstantOutputOption(['-map_metadata', '-1']);

export const MapAllStreamsOutputOption = () =>
makeConstantOutputOption(['-map', '0']);

export const NoSceneDetectOutputOption = (
value: number,
): ConstantOutputOption =>
Expand Down Expand Up @@ -90,12 +93,13 @@ export const FrameRateOutputOption = (
export const VideoTrackTimescaleOutputOption = (scale: number) =>
makeConstantOutputOption(['-video_track_timescale', scale.toString()]);

export const MpegTsOutputFormatOption = () =>
export const MpegTsOutputFormatOption = (initialDiscontinuity?: boolean) =>
makeConstantOutputOption([
'-f',
'mpegts',
'-mpegts_flags',
'+initial_discontinuity',
...(initialDiscontinuity
? ['-mpegts_flags', '+initial_discontinuity']
: []),
]);

export const Mp4OutputFormatOption = () =>
Expand Down
61 changes: 54 additions & 7 deletions server/src/ffmpeg/builder/pipeline/BasePipelineBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {
DoNotMapMetadataOutputOption,
FastStartOutputOption,
FrameRateOutputOption,
MapAllStreamsOutputOption,
MatroskaOutputFormatOption,
MetadataServiceNameOutputOption,
MetadataServiceProviderOutputOption,
Expand Down Expand Up @@ -169,18 +170,23 @@ export abstract class BasePipelineBuilder implements PipelineBuilder {
protected context: PipelineBuilderContext;

constructor(
protected videoInputSource: VideoInputSource,
protected nullableVideoInputSource: Nullable<VideoInputSource>,
private audioInputSource: Nullable<AudioInputSource>,
protected watermarkInputSource: Nullable<WatermarkInputSource>,
protected concatInputSource: Nullable<ConcatInputSource>,
protected ffmpegCapabilities: FfmpegCapabilities,
) {}

get videoInputSource(): VideoInputSource {
// Only use this on video pipelines!!!
return this.nullableVideoInputSource!;
}

validate(): Nullable<Error> {
return null;
}

hlsConcat(input: ConcatInputSource, state: FfmpegState) {
concat(input: ConcatInputSource, state: FfmpegState) {
const pipelineSteps: PipelineStep[] = [
new NoStdInOption(),
new HideBannerOption(),
Expand All @@ -203,10 +209,8 @@ export abstract class BasePipelineBuilder implements PipelineBuilder {
input.addOption(new ConcatHttpReconnectOptions());
}

if (this.ffmpegState.threadCount) {
pipelineSteps.unshift(
new ThreadCountOption(this.ffmpegState.threadCount),
);
if (state.threadCount) {
pipelineSteps.unshift(new ThreadCountOption(state.threadCount));
}

pipelineSteps.push(NoSceneDetectOutputOption(0), new CopyAllEncoder());
Expand All @@ -231,6 +235,46 @@ export abstract class BasePipelineBuilder implements PipelineBuilder {
});
}

hlsWrap(input: ConcatInputSource, state: FfmpegState) {
const pipelineSteps: PipelineStep[] = [
new NoStdInOption(),
new HideBannerOption(),
new ThreadCountOption(1),
new LogLevelOption(state.logLevel),
new NoStatsOption(),
new StandardFormatFlags(),
MapAllStreamsOutputOption(),
new CopyAllEncoder(),
];

if (input.protocol === 'http') {
input.addOption(new ConcatHttpReconnectOptions());
}

input.addOption(new ReadrateInputOption(this.ffmpegCapabilities, 0));
if (state.metadataServiceName) {
pipelineSteps.push(
MetadataServiceNameOutputOption(state.metadataServiceName),
);
}
if (state.metadataServiceProvider) {
pipelineSteps.push(
MetadataServiceProviderOutputOption(state.metadataServiceProvider),
);
}
pipelineSteps.push(
MpegTsOutputFormatOption(false),
PipeProtocolOutputOption(),
);

return new Pipeline(pipelineSteps, {
videoInput: null,
audioInput: null,
concatInput: input,
watermarkInput: null,
});
}

build(ffmpegState: FfmpegState, desiredState: FrameState): Pipeline {
this.context = {
videoStream: first(this.videoInputSource.streams),
Expand Down Expand Up @@ -284,6 +328,10 @@ export abstract class BasePipelineBuilder implements PipelineBuilder {
this.pipelineSteps.push(TimeLimitOutputOption(this.ffmpegState.duration));
}

if (isNull(this.nullableVideoInputSource)) {
throw new Error('FFmpeg pipeline currently requires a video input');
}

if (
this.videoInputSource.protocol === 'http' &&
this.videoInputSource.continuity === 'discrete'
Expand Down Expand Up @@ -585,7 +633,6 @@ export abstract class BasePipelineBuilder implements PipelineBuilder {
isNonEmptyString(this.ffmpegState.hlsSegmentTemplate) &&
isNonEmptyString(this.ffmpegState.hlsBaseStreamUrl)
) {
console.log(this.ffmpegState);
this.pipelineSteps.push(
new HlsOutputFormat(
this.desiredState,
Expand Down
24 changes: 23 additions & 1 deletion server/src/ffmpeg/builder/pipeline/PipelineBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ import { Pipeline } from './Pipeline.ts';

export interface PipelineBuilder {
validate(): Nullable<Error>;
hlsConcat(input: ConcatInputSource, state: FfmpegState): Pipeline;

/**
* Takes input source in ffconcat format and returns the concatenated output stream
* in the given output format. Simply copies input streams.
* @param input
* @param state
*/
concat(input: ConcatInputSource, state: FfmpegState): Pipeline;

/**
* Takes m3u8 HLS playlist as input and returns a continuous output stream
* in the given output format. Simple copies input streams.
* @param input
* @param state
*/
hlsWrap(input: ConcatInputSource, state: FfmpegState): Pipeline;

/**
* Calculates an ffmpeg pipeline using the inputted ffmpeg state and desired
* output state.
* @param currentState
* @param desiredState
*/
build(currentState: FfmpegState, desiredState: FrameState): Pipeline;
}
8 changes: 1 addition & 7 deletions server/src/ffmpeg/builder/pipeline/PipelineBuilderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { HardwareAccelerationMode } from '@/ffmpeg/builder/types.ts';
import { FFMPEGInfo } from '@/ffmpeg/ffmpegInfo.ts';
import { Nullable } from '@/types/util.ts';
import { FfmpegSettings } from '@tunarr/types';
import { isNull, isUndefined } from 'lodash-es';
import { isUndefined } from 'lodash-es';
import { DeepReadonly } from 'ts-essentials';
import { PipelineBuilder } from './PipelineBuilder.js';
import { NvidiaPipelineBuilder } from './hardware/NvidiaPipelineBuilder.ts';
Expand Down Expand Up @@ -78,12 +78,6 @@ class PipelineBuilderFactory$Builder {
);
const binaryCapabilities = await info.getCapabilities();

if (isNull(this.videoInputSource)) {
// Audio-only pipeline builder??
throw new Error('Not yet implemented');
// return new SoftwarePipelineBuilder()
}

switch (this.hardwareAccelerationMode) {
case 'cuda':
return new NvidiaPipelineBuilder(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class NvidiaPipelineBuilder extends SoftwarePipelineBuilder {
constructor(
private hardwareCapabilities: BaseFfmpegHardwareCapabilities,
binaryCapabilities: FfmpegCapabilities,
videoInputFile: VideoInputSource,
videoInputFile: Nullable<VideoInputSource>,
audioInputFile: Nullable<AudioInputSource>,
concatInputSource: Nullable<ConcatInputSource>,
watermarkInputSource: Nullable<WatermarkInputSource>,
Expand Down Expand Up @@ -358,7 +358,6 @@ export class NvidiaPipelineBuilder extends SoftwarePipelineBuilder {
this.watermarkInputSource &&
currentState.frameDataLocation === FrameDataLocation.Hardware
) {
this.logger.debug('Using software encoder');
const hwDownloadFilter = new HardwareDownloadCudaFilter(
currentState.pixelFormat,
null,
Expand All @@ -367,7 +366,6 @@ export class NvidiaPipelineBuilder extends SoftwarePipelineBuilder {
steps.push(hwDownloadFilter);
}

console.debug('pixelFormat', currentState, this.ffmpegState);
if (
currentState.frameDataLocation === FrameDataLocation.Hardware &&
this.ffmpegState.encoderHwAccelMode === HardwareAccelerationMode.None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class VaapiPipelineBuilder extends SoftwarePipelineBuilder {
constructor(
private hardwareCapabilities: BaseFfmpegHardwareCapabilities,
binaryCapabilities: FfmpegCapabilities,
videoInputFile: VideoInputSource,
videoInputFile: Nullable<VideoInputSource>,
audioInputFile: Nullable<AudioInputSource>,
watermarkInputSource: Nullable<WatermarkInputSource>,
concatInputSource: Nullable<ConcatInputSource>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class VideoToolboxPipelineBuilder extends SoftwarePipelineBuilder {
constructor(
private hardwareCapabilities: BaseFfmpegHardwareCapabilities,
binaryCapabilities: FfmpegCapabilities,
videoInputFile: VideoInputSource,
videoInputFile: Nullable<VideoInputSource>,
audioInputFile: Nullable<AudioInputSource>,
concatInputSource: Nullable<ConcatInputSource>,
watermarkInputSource: Nullable<WatermarkInputSource>,
Expand Down
Loading

0 comments on commit 3a73965

Please sign in to comment.