Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(server): always set transcoding device, prefer renderD* #14455

Merged
merged 1 commit into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 37 additions & 26 deletions server/src/services/media.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1637,7 +1637,10 @@ describe(MediaService.name, () => {
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
expect.objectContaining({
inputOptions: expect.arrayContaining(['-init_hw_device qsv=hw', '-filter_hw_device hw']),
inputOptions: expect.arrayContaining([
'-init_hw_device qsv=hw,child_device=/dev/dri/renderD128',
'-filter_hw_device hw',
]),
outputOptions: expect.arrayContaining([
`-c:v h264_qsv`,
'-c:a copy',
Expand Down Expand Up @@ -1696,7 +1699,10 @@ describe(MediaService.name, () => {
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
expect.objectContaining({
inputOptions: expect.arrayContaining(['-init_hw_device qsv=hw', '-filter_hw_device hw']),
inputOptions: expect.arrayContaining([
'-init_hw_device qsv=hw,child_device=/dev/dri/renderD128',
'-filter_hw_device hw',
]),
outputOptions: expect.not.arrayContaining([expect.stringContaining('-preset')]),
twoPass: false,
}),
Expand All @@ -1713,7 +1719,10 @@ describe(MediaService.name, () => {
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
expect.objectContaining({
inputOptions: expect.arrayContaining(['-init_hw_device qsv=hw', '-filter_hw_device hw']),
inputOptions: expect.arrayContaining([
'-init_hw_device qsv=hw,child_device=/dev/dri/renderD128',
'-filter_hw_device hw',
]),
outputOptions: expect.arrayContaining(['-low_power 1']),
twoPass: false,
}),
Expand All @@ -1730,6 +1739,26 @@ describe(MediaService.name, () => {
expect(loggerMock.debug).toHaveBeenCalledWith('No devices found in /dev/dri.');
});

it('should prefer higher index renderD* device for qsv', async () => {
storageMock.readdir.mockResolvedValue(['card1', 'renderD129', 'card0', 'renderD128']);
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV } });
assetMock.getByIds.mockResolvedValue([assetStub.video]);
await sut.handleVideoConversion({ id: assetStub.video.id });
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device qsv=hw,child_device=/dev/dri/renderD129',
'-filter_hw_device hw',
]),
outputOptions: expect.arrayContaining([`-c:v h264_qsv`]),
twoPass: false,
}),
);
});

it('should use hardware decoding for qsv if enabled', async () => {
storageMock.readdir.mockResolvedValue(['renderD128']);
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
Expand All @@ -1750,6 +1779,7 @@ describe(MediaService.name, () => {
'-async_depth 4',
'-noautorotate',
'-threads 1',
'-qsv_device /dev/dri/renderD128',
]),
outputOptions: expect.arrayContaining([
expect.stringContaining('scale_qsv=-1:720:async_depth=4:mode=hq:format=nv12'),
Expand Down Expand Up @@ -1939,28 +1969,8 @@ describe(MediaService.name, () => {
);
});

it('should prefer gpu for vaapi if available', async () => {
storageMock.readdir.mockResolvedValue(['renderD129', 'card1', 'card0', 'renderD128']);
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
assetMock.getByIds.mockResolvedValue([assetStub.video]);
await sut.handleVideoConversion({ id: assetStub.video.id });
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device vaapi=accel:/dev/dri/card1',
'-filter_hw_device accel',
]),
outputOptions: expect.arrayContaining([`-c:v h264_vaapi`]),
twoPass: false,
}),
);
});

it('should prefer higher index gpu node', async () => {
storageMock.readdir.mockResolvedValue(['renderD129', 'renderD130', 'renderD128']);
it('should prefer higher index renderD* device for vaapi', async () => {
storageMock.readdir.mockResolvedValue(['card1', 'renderD129', 'card0', 'renderD128']);
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
assetMock.getByIds.mockResolvedValue([assetStub.video]);
Expand All @@ -1970,7 +1980,7 @@ describe(MediaService.name, () => {
'upload/encoded-video/user-id/as/se/asset-id.mp4',
expect.objectContaining({
inputOptions: expect.arrayContaining([
'-init_hw_device vaapi=accel:/dev/dri/renderD130',
'-init_hw_device vaapi=accel:/dev/dri/renderD129',
'-filter_hw_device accel',
]),
outputOptions: expect.arrayContaining([`-c:v h264_vaapi`]),
Expand Down Expand Up @@ -2020,6 +2030,7 @@ describe(MediaService.name, () => {
'-hwaccel_output_format vaapi',
'-noautorotate',
'-threads 1',
'-hwaccel_device /dev/dri/renderD128',
]),
outputOptions: expect.arrayContaining([
expect.stringContaining('scale_vaapi=-2:720:mode=hq:out_range=pc:format=nv12'),
Expand Down
115 changes: 36 additions & 79 deletions server/src/utils/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,33 +322,44 @@ export class BaseConfig implements VideoCodecSWConfig {
}

export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig {
protected devices: string[];
protected device: string;

constructor(
protected config: SystemConfigFFmpegDto,
devices: string[] = [],
) {
super(config);
this.devices = this.validateDevices(devices);
this.device = this.getDevice(devices);
}

getSupportedCodecs() {
return [VideoCodec.H264, VideoCodec.HEVC];
}

validateDevices(devices: string[]) {
return devices
.filter((device) => device.startsWith('renderD') || device.startsWith('card'))
.sort((a, b) => {
// order GPU devices first
if (a.startsWith('card') && b.startsWith('renderD')) {
return -1;
}
if (a.startsWith('renderD') && b.startsWith('card')) {
return 1;
}
return -a.localeCompare(b);
});
if (devices.length === 0) {
throw new Error('No /dev/dri devices found. If using Docker, make sure at least one /dev/dri device is mounted');
}

return devices.filter(function (device) {
return device.startsWith('renderD') || device.startsWith('card');
});
}

getDevice(devices: string[]) {
if (this.config.preferredHwDevice === 'auto') {
// eslint-disable-next-line unicorn/no-array-reduce
return `/dev/dri/${this.validateDevices(devices).reduce(function (a, b) {
return a.localeCompare(b) < 0 ? b : a;
})}`;
}

const deviceName = this.config.preferredHwDevice.replace('/dev/dri/', '');
if (!devices.includes(deviceName)) {
throw new Error(`Device '${deviceName}' does not exist. If using Docker, make sure this device is mounted`);
}

return `/dev/dri/${deviceName}`;
}

getVideoCodec(): string {
Expand All @@ -361,20 +372,6 @@ export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig {
}
return this.config.gopSize;
}

getPreferredHardwareDevice(): string | undefined {
const device = this.config.preferredHwDevice;
if (device === 'auto') {
return;
}

const deviceName = device.replace('/dev/dri/', '');
if (!this.devices.includes(deviceName)) {
throw new Error(`Device '${device}' does not exist`);
}

return `/dev/dri/${deviceName}`;
}
}

export class ThumbnailConfig extends BaseConfig {
Expand Down Expand Up @@ -513,12 +510,16 @@ export class AV1Config extends BaseConfig {
}

export class NvencSwDecodeConfig extends BaseHWConfig {
getDevice() {
return '0';
}

getSupportedCodecs() {
return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.AV1];
}

getBaseInputOptions() {
return ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda'];
return [`-init_hw_device cuda=cuda:${this.device}`, '-filter_hw_device cuda'];
}

getBaseOutputOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
Expand Down Expand Up @@ -641,17 +642,7 @@ export class NvencHwDecodeConfig extends NvencSwDecodeConfig {

export class QsvSwDecodeConfig extends BaseHWConfig {
getBaseInputOptions() {
if (this.devices.length === 0) {
throw new Error('No QSV device found');
}

let qsvString = '';
const hwDevice = this.getPreferredHardwareDevice();
if (hwDevice) {
qsvString = `,child_device=${hwDevice}`;
}

return [`-init_hw_device qsv=hw${qsvString}`, '-filter_hw_device hw'];
return [`-init_hw_device qsv=hw,child_device=${this.device}`, '-filter_hw_device hw'];
}

getBaseOutputOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
Expand Down Expand Up @@ -721,23 +712,14 @@ export class QsvSwDecodeConfig extends BaseHWConfig {

export class QsvHwDecodeConfig extends QsvSwDecodeConfig {
getBaseInputOptions() {
if (this.devices.length === 0) {
throw new Error('No QSV device found');
}

const options = [
return [
'-hwaccel qsv',
'-hwaccel_output_format qsv',
'-async_depth 4',
'-noautorotate',
`-qsv_device ${this.device}`,
...this.getInputThreadOptions(),
];
const hwDevice = this.getPreferredHardwareDevice();
if (hwDevice) {
options.push(`-qsv_device ${hwDevice}`);
}

return options;
}

getFilterOptions(videoStream: VideoStreamInfo) {
Expand Down Expand Up @@ -789,16 +771,7 @@ export class QsvHwDecodeConfig extends QsvSwDecodeConfig {

export class VaapiSwDecodeConfig extends BaseHWConfig {
getBaseInputOptions() {
if (this.devices.length === 0) {
throw new Error('No VAAPI device found');
}

let hwDevice = this.getPreferredHardwareDevice();
if (!hwDevice) {
hwDevice = `/dev/dri/${this.devices[0]}`;
}

return [`-init_hw_device vaapi=accel:${hwDevice}`, '-filter_hw_device accel'];
return [`-init_hw_device vaapi=accel:${this.device}`, '-filter_hw_device accel'];
}

getFilterOptions(videoStream: VideoStreamInfo) {
Expand Down Expand Up @@ -856,22 +829,13 @@ export class VaapiSwDecodeConfig extends BaseHWConfig {

export class VaapiHwDecodeConfig extends VaapiSwDecodeConfig {
getBaseInputOptions() {
if (this.devices.length === 0) {
throw new Error('No VAAPI device found');
}

const options = [
return [
'-hwaccel vaapi',
'-hwaccel_output_format vaapi',
'-noautorotate',
`-hwaccel_device ${this.device}`,
...this.getInputThreadOptions(),
];
const hwDevice = this.getPreferredHardwareDevice();
if (hwDevice) {
options.push(`-hwaccel_device ${hwDevice}`);
}

return options;
}

getFilterOptions(videoStream: VideoStreamInfo) {
Expand Down Expand Up @@ -934,9 +898,6 @@ export class RkmppSwDecodeConfig extends BaseHWConfig {
}

getBaseInputOptions(): string[] {
if (this.devices.length === 0) {
throw new Error('No RKMPP device found');
}
return [];
}

Expand Down Expand Up @@ -987,10 +948,6 @@ export class RkmppHwDecodeConfig extends RkmppSwDecodeConfig {
}

getBaseInputOptions() {
if (this.devices.length === 0) {
throw new Error('No RKMPP device found');
}

return ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga', '-noautorotate'];
}

Expand Down
Loading