Skip to content

Commit

Permalink
vaapi checkpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbenincasa committed Oct 24, 2024
1 parent 00c26a6 commit c2da98b
Show file tree
Hide file tree
Showing 10 changed files with 180 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { Maybe } from '../../../types/util';
import { FFMPEGInfo } from '../../ffmpegInfo';
import { PixelFormat } from '../format/PixelFormat';
import { NvidiaHardwareCapabilities } from './NvidiaHardwareCapabilities';

export abstract class BaseFfmpegHardwareCapabilities {
readonly type: string;
constructor(protected ffmpegInfo: FFMPEGInfo) {}
constructor() {}

abstract canDecode(
videoFormat: string,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { FFMPEGInfo } from '../../ffmpegInfo';
import { BaseFfmpegHardwareCapabilities } from './BaseFfmpegHardwareCapabilities';

export class NoHardwareCapabilities extends BaseFfmpegHardwareCapabilities {
readonly type = 'none' as const;
constructor(ffmpegInfo: FFMPEGInfo) {
super(ffmpegInfo);
constructor() {
super();
}

canDecode(): Promise<boolean> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const MaxwellGm206Models = new Set([
'GTX 965M',
]);

// https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new
export class NvidiaHardwareCapabilities extends BaseFfmpegHardwareCapabilities {
readonly type = 'nvidia' as const;

Expand Down
126 changes: 126 additions & 0 deletions server/src/ffmpeg/builder/capabilities/VaapiHardwareCapabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { isEmpty, split } from 'lodash-es';
import { Maybe } from '../../../types/util';
import { LoggerFactory } from '../../../util/logging/LoggerFactory';
import { PixelFormat } from '../format/PixelFormat';
import { RateControlMode } from '../types';
import { BaseFfmpegHardwareCapabilities } from './BaseFfmpegHardwareCapabilities';

export const VaapiEntrypoint = {
Decode: 'VAEntrypointVLD',
Encode: 'VAEntrypointEncSlice',
EncodeLowPower: 'VAEntrypointEncSliceLP',
} as const;

export const VaapiProfiles = {
Mpeg2Simple: 'VAProfileMPEG2Simple',
Mpeg2Main: 'VAProfileMPEG2Main',
H264ConstrainedBaseline: 'VAProfileH264ConstrainedBaseline',
H264Main: 'VAProfileH264Main',
H264High: 'VAProfileH264High',
H264MultiviewHigh: 'VAProfileH264MultiviewHigh',
H264StereoHigh: 'VAProfileH264StereoHigh',
Vc1Simple: 'VAProfileVC1Simple',
Vc1Main: 'VAProfileVC1Main',
Vc1Advanced: 'VAProfileVC1Advanced',
HevcMain: 'VAProfileHEVCMain',
HevcMain10: 'VAProfileHEVCMain10',
Vp9Profile0: 'VAProfileVP9Profile0',
Vp9Profile1: 'VAProfileVP9Profile1',
Vp9Profile2: 'VAProfileVP9Profile2',
Vp9Profile3: 'VAProfileVP9Profile3',
Av1Profile0: 'VAProfileAV1Profile0',
} as const;

export class VaapiProfileEntrypoint {
#rateControlModes: Set<RateControlMode> = new Set();

constructor(
public readonly profile: string,
public readonly entrypoint: string,
) {}

get rateControlModes(): Set<RateControlMode> {
return this.#rateControlModes;
}

addRateControlMode(mode: RateControlMode) {
this.#rateControlModes.add(mode);
}
}

export class VaapiHardwareCapabilitiesFactory {
private static logger = LoggerFactory.child({
className: this.constructor.name,
});
private static ProfileEntrypointPattern =
/(VAProfile\w*).*(VAEntrypoint\w*)/g;
private static ProfileRateControlPattern = /.*VA_RC_(\w*).*/g;

static extractEntrypointsFromVaInfo(result: string) {
const entrypoints: VaapiProfileEntrypoint[] = [];
for (const line of split(result, '\n')) {
const match = line.match(this.ProfileEntrypointPattern);
if (match) {
entrypoints.push(new VaapiProfileEntrypoint(match[1], match[2]));
}
}

return entrypoints;
}

static extractAllFromVaInfo(result: string) {
const entrypoints: VaapiProfileEntrypoint[] = [];
let currentEntrypoint: VaapiProfileEntrypoint | null = null;

for (const line of split(result, '\n')) {
let match = line.match(this.ProfileEntrypointPattern);
if (match) {
currentEntrypoint = new VaapiProfileEntrypoint(match[1], match[2]);
entrypoints.push(currentEntrypoint);
} else if (currentEntrypoint) {
match = line.match(this.ProfileRateControlPattern);
if (match) {
switch (match[1].trim().toLowerCase()) {
case 'cgp':
currentEntrypoint.addRateControlMode(RateControlMode.CQP);
break;
case 'cbr':
currentEntrypoint.addRateControlMode(RateControlMode.CBR);
break;
case 'vbr':
currentEntrypoint.addRateControlMode(RateControlMode.VBR);
break;
default:
break;
}
}
}
}

if (isEmpty(entrypoints)) {
return null;
}

return new VaapiHardwareCapabilities(entrypoints);
}
}

export class VaapiHardwareCapabilities extends BaseFfmpegHardwareCapabilities {
readonly type: string = 'vaapi';

constructor(private entrypoints: VaapiProfileEntrypoint[]) {
super();
}

canDecode(
videoFormat: string,
videoProfile: Maybe<string>,
pixelFormat: Maybe<PixelFormat>,
): Promise<boolean> {}

canEncode(
videoFormat: string,
videoProfile: Maybe<string>,
pixelFormat: Maybe<PixelFormat>,
): Promise<boolean> {}
}
6 changes: 6 additions & 0 deletions server/src/ffmpeg/builder/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,9 @@ export class WatermarkInputSource extends VideoInputSource<[StillImageStream]> {
super(path, [imageStream]);
}
}

export enum RateControlMode {
CBR,
CQP,
VBR,
}
3 changes: 2 additions & 1 deletion server/src/ffmpeg/ffmpeg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -831,8 +831,9 @@ export class FFMPEG {
// ... it might not be totally true, but we'll make this better.
let canEncode = false;
if (isSuccess(gpuCapabilities)) {
canEncode = gpuCapabilities.canEncode(
canEncode = await gpuCapabilities.canEncode(
this.opts.videoFormat,
streamStats?.videoProfile,
streamStats?.videoBitDepth
? { bitDepth: streamStats.videoBitDepth }
: undefined,
Expand Down
94 changes: 39 additions & 55 deletions server/src/ffmpeg/ffmpegInfo.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { FfmpegSettings } from '@tunarr/types';
import { exec } from 'child_process';
import _, { isEmpty, isError, nth, some, trim } from 'lodash-es';
import _, { isEmpty, isError, isUndefined, nth, some, trim } from 'lodash-es';
import NodeCache from 'node-cache';
import PQueue from 'p-queue';
import { format } from 'util';
import { Nullable } from '../types/util.js';
import { cacheGetOrSet } from '../util/cache.js';
import dayjs from '../util/dayjs.js';
import { fileExists } from '../util/fsUtil.js';
import { attempt, isNonEmptyString, parseIntOrNull } from '../util/index.js';
import {
attempt,
isLinux,
isNonEmptyString,
parseIntOrNull,
} from '../util/index.js';
import { LoggerFactory } from '../util/logging/LoggerFactory';
import { sanitizeForExec } from '../util/strings.js';
import { DefaultHardwareCapabilities } from './builder/capabilities/DefaultHardwareCapabilities.js';
Expand All @@ -20,6 +26,7 @@ const CacheKeys = {
HWACCELS: 'hwaccels',
OPTIONS: 'options',
NVIDIA: 'nvidia',
VAINFO: 'vainfo_%s_%s',
} as const;

export type FfmpegVersionResult = {
Expand All @@ -38,8 +45,6 @@ const CoderExtractionPattern = /[A-Z.]+\s([a-z0-9_-]+)\s*(.*)$/;
const OptionsExtractionPattern = /^-([a-z_]+)\s+.*/;
const NvidiaGpuArchPattern = /SM\s+(\d\.\d)/;
const NvidiaGpuModelPattern = /(GTX\s+[0-9a-zA-Z]+[\sTtIi]+)/;
const NvidiaGpuArchPattern = /SM\s+(\d\.\d)/;
const NvidiaGpuModelPattern = /(GTX\s+[0-9a-zA-Z]+[\sTtIi]+)/;

export class FFMPEGInfo {
private logger = LoggerFactory.child({
Expand All @@ -54,8 +59,13 @@ export class FFMPEGInfo {
private static makeCacheKey(
path: string,
command: keyof typeof CacheKeys,
...args: string[]
): string {
return `${path}_${CacheKeys[command]}`;
return format(`${path}_${CacheKeys[command]}`, ...args);
}

private static vaInfoCacheKey(driver: string, device: string) {
return `${CacheKeys.VAINFO}_${driver}_${device}`;
}

private ffmpegPath: string;
Expand Down Expand Up @@ -239,56 +249,6 @@ export class FFMPEGInfo {
});
}

async getNvidiaCapabilities() {
return attempt(async () => {
const out = await cacheGetOrSet(
FFMPEGInfo.resultCache,
this.cacheKey('NVIDIA'),
() =>
this.getFfmpegStdout(
[
'-hide_banner',
'-f',
'lavfi',
'-i',
'nullsrc',
'-c:v',
'h264_nvenc',
'-gpu',
'list',
'-f',
'null',
'-',
],
true,
),
);

const lines = _.chain(out)
.split('\n')
.drop(1)
.map(trim)
.reject(isEmpty)
.value();

for (const line of lines) {
const archMatch = line.match(NvidiaGpuArchPattern);
if (archMatch) {
const archString = archMatch[1];
const archNum = parseInt(archString.replaceAll('.', ''));
const model =
nth(line.match(NvidiaGpuModelPattern), 1)?.trim() ?? 'unknown';
this.logger.debug(
`Detected NVIDIA GPU (model = "${model}", arch = "${archString}")`,
);
return new NvidiaHardwareCapabilities(model, archNum);
}
}

throw new Error('Could not parse ffmepg output for Nvidia capabilities');
});
}

async hasOption(
option: string,
defaultOnMissing: boolean = false,
Expand Down Expand Up @@ -351,6 +311,30 @@ export class FFMPEGInfo {
});
}

async getVaapiCapabilities() {
const vaapiDevice = isNonEmptyString(this.opts.vaapiDevice)
? this.opts.vaapiDevice
: isLinux()
? '/dev/dri/renderD128'
: undefined;
// : isLinux()
// ? '/dev/dri/renderD128'
// : undefined;

if (isUndefined(vaapiDevice) || isEmpty(vaapiDevice)) {
this.logger.error('Cannot detect VAAPI capabilities without a device');
return new NoHardwareCapabilities();
}

const driver = this.opts.vaapiDriver ?? '';

await cacheGetOrSet(
FFMPEGInfo.resultCache,
FFMPEGInfo.vaInfoCacheKey(vaapiDevice, driver),
async () => {},
);
}

private getFfmpegStdout(
args: string[],
swallowError: boolean = false,
Expand Down
1 change: 1 addition & 0 deletions server/src/stream/jellyfin/JellyfinStreamDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ export class JellyfinStreamDetails {
// TODO Parse pixel aspect ratio
streamDetails.anamorphic = !!videoStream.IsAnamorphic;
streamDetails.videoCodec = nullToUndefined(videoStream.Codec);
streamDetails.videoProfile = nullToUndefined(videoStream.Profile);
// Keeping old behavior here for now
streamDetails.videoFramerate = videoStream.AverageFrameRate
? Math.round(videoStream.AverageFrameRate)
Expand Down
1 change: 1 addition & 0 deletions server/src/stream/plex/PlexStreamDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ export class PlexStreamDetails {
streamDetails.anamorphic =
videoStream.anamorphic === '1' || videoStream.anamorphic === true;
streamDetails.videoCodec = videoStream.codec;
streamDetails.videoProfile = videoStream.profile;
// Keeping old behavior here for now
streamDetails.videoFramerate = videoStream.frameRate
? Math.round(videoStream.frameRate)
Expand Down
1 change: 1 addition & 0 deletions server/src/stream/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type StreamDetails = {
pixelQ?: number;

videoCodec?: string;
videoProfile?: string;
videoWidth?: number;
videoHeight?: number;
videoFramerate?: number;
Expand Down

0 comments on commit c2da98b

Please sign in to comment.