Skip to content

Commit

Permalink
feat(server): better transcoding logs (#13000)
Browse files Browse the repository at this point in the history
* better transcoding logs

* pr feedback
  • Loading branch information
mertalev authored Sep 27, 2024
1 parent 7579bc4 commit 4248594
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 210 deletions.
1 change: 1 addition & 0 deletions server/src/interfaces/logger.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface ILoggerRepository {
setAppName(name: string): void;
setContext(message: string): void;
setLogLevel(level: LogLevel): void;
isLevelEnabled(level: LogLevel): boolean;

verbose(message: any, ...args: any): void;
debug(message: any, ...args: any): void;
Expand Down
10 changes: 9 additions & 1 deletion server/src/interfaces/media.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ export interface TranscodeCommand {
inputOptions: string[];
outputOptions: string[];
twoPass: boolean;
progress: {
frameCount: number;
percentInterval: number;
};
}

export interface BitrateDistribution {
Expand All @@ -79,6 +83,10 @@ export interface VideoCodecHWConfig extends VideoCodecSWConfig {
getSupportedCodecs(): Array<VideoCodec>;
}

export interface ProbeOptions {
countFrames: boolean;
}

export interface IMediaRepository {
// image
extract(input: string, output: string): Promise<boolean>;
Expand All @@ -87,6 +95,6 @@ export interface IMediaRepository {
getImageDimensions(input: string): Promise<ImageDimensions>;

// video
probe(input: string): Promise<VideoInfo>;
probe(input: string, options?: ProbeOptions): Promise<VideoInfo>;
transcode(input: string, output: string | Writable, command: TranscodeCommand): Promise<void>;
}
62 changes: 51 additions & 11 deletions server/src/repositories/media.repository.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
import { Inject, Injectable } from '@nestjs/common';
import { exiftool } from 'exiftool-vendored';
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
import { Duration } from 'luxon';
import fs from 'node:fs/promises';
import { Writable } from 'node:stream';
import { promisify } from 'node:util';
import sharp from 'sharp';
import { Colorspace } from 'src/enum';
import { Colorspace, LogLevel } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import {
IMediaRepository,
ImageDimensions,
ProbeOptions,
ThumbnailOptions,
TranscodeCommand,
VideoInfo,
} from 'src/interfaces/media.interface';
import { Instrumentation } from 'src/utils/instrumentation';
import { handlePromiseError } from 'src/utils/misc';

const probe = promisify<string, FfprobeData>(ffmpeg.ffprobe);
const probe = (input: string, options: string[]): Promise<FfprobeData> =>
new Promise((resolve, reject) =>
ffmpeg.ffprobe(input, options, (error, data) => (error ? reject(error) : resolve(data))),
);
sharp.concurrency(0);
sharp.cache({ files: 0 });

type ProgressEvent = {
frames: number;
currentFps: number;
currentKbps: number;
targetSize: number;
timemark: string;
percent?: number;
};

@Instrumentation()
@Injectable()
export class MediaRepository implements IMediaRepository {
Expand Down Expand Up @@ -65,8 +78,8 @@ export class MediaRepository implements IMediaRepository {
.toFile(output);
}

async probe(input: string): Promise<VideoInfo> {
const results = await probe(input);
async probe(input: string, options?: ProbeOptions): Promise<VideoInfo> {
const results = await probe(input, options?.countFrames ? ['-count_packets'] : []); // gets frame count quickly: https://stackoverflow.com/a/28376817
return {
format: {
formatName: results.format.format_name,
Expand All @@ -83,18 +96,18 @@ export class MediaRepository implements IMediaRepository {
width: stream.width || 0,
codecName: stream.codec_name === 'h265' ? 'hevc' : stream.codec_name,
codecType: stream.codec_type,
frameCount: Number.parseInt(stream.nb_frames ?? '0'),
rotation: Number.parseInt(`${stream.rotation ?? 0}`),
frameCount: this.parseInt(options?.countFrames ? stream.nb_read_packets : stream.nb_frames),
rotation: this.parseInt(stream.rotation),
isHDR: stream.color_transfer === 'smpte2084' || stream.color_transfer === 'arib-std-b67',
bitrate: Number.parseInt(stream.bit_rate ?? '0'),
bitrate: this.parseInt(stream.bit_rate),
})),
audioStreams: results.streams
.filter((stream) => stream.codec_type === 'audio')
.map((stream) => ({
index: stream.index,
codecType: stream.codec_type,
codecName: stream.codec_name,
frameCount: Number.parseInt(stream.nb_frames ?? '0'),
frameCount: this.parseInt(options?.countFrames ? stream.nb_read_packets : stream.nb_frames),
})),
};
}
Expand Down Expand Up @@ -156,10 +169,37 @@ export class MediaRepository implements IMediaRepository {
}

private configureFfmpegCall(input: string, output: string | Writable, options: TranscodeCommand) {
return ffmpeg(input, { niceness: 10 })
const ffmpegCall = ffmpeg(input, { niceness: 10 })
.inputOptions(options.inputOptions)
.outputOptions(options.outputOptions)
.output(output)
.on('error', (error, stdout, stderr) => this.logger.error(stderr || error));
.on('start', (command: string) => this.logger.debug(command))
.on('error', (error, _, stderr) => this.logger.error(stderr || error));

const { frameCount, percentInterval } = options.progress;
const frameInterval = Math.ceil(frameCount / (100 / percentInterval));
if (this.logger.isLevelEnabled(LogLevel.DEBUG) && frameCount && frameInterval) {
let lastProgressFrame: number = 0;
ffmpegCall.on('progress', (progress: ProgressEvent) => {
if (progress.frames - lastProgressFrame < frameInterval) {
return;
}

lastProgressFrame = progress.frames;
const percent = ((progress.frames / frameCount) * 100).toFixed(2);
const ms = Math.floor((frameCount - progress.frames) / progress.currentFps) * 1000;
const duration = ms ? Duration.fromMillis(ms).rescale().toHuman({ unitDisplay: 'narrow' }) : '';
const outputText = output instanceof Writable ? 'stream' : output.split('/').pop();
this.logger.debug(
`Transcoding ${percent}% done${duration ? `, estimated ${duration} remaining` : ''} for output ${outputText}`,
);
});
}

return ffmpegCall;
}

private parseInt(value: string | number | undefined): number {
return Number.parseInt(value as string) || 0;
}
}
Loading

0 comments on commit 4248594

Please sign in to comment.