|
@@ -7,7 +7,7 @@ import { IAssetJob, IBaseJob, IJobRepository, JobName } from '../job';
|
|
|
import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
|
|
|
import { ISystemConfigRepository, SystemConfigFFmpegDto } from '../system-config';
|
|
|
import { SystemConfigCore } from '../system-config/system-config.core';
|
|
|
-import { IMediaRepository, VideoStreamInfo } from './media.repository';
|
|
|
+import { AudioStreamInfo, IMediaRepository, VideoStreamInfo } from './media.repository';
|
|
|
|
|
|
@Injectable()
|
|
|
export class MediaService {
|
|
@@ -127,23 +127,27 @@ export class MediaService {
|
|
|
const output = join(outputFolder, `${asset.id}.mp4`);
|
|
|
this.storageRepository.mkdirSync(outputFolder);
|
|
|
|
|
|
- const { streams } = await this.mediaRepository.probe(input);
|
|
|
- const stream = await this.getLongestStream(streams);
|
|
|
- if (!stream) {
|
|
|
+ const { videoStreams, audioStreams, format } = await this.mediaRepository.probe(input);
|
|
|
+ const mainVideoStream = this.getMainVideoStream(videoStreams);
|
|
|
+ const mainAudioStream = this.getMainAudioStream(audioStreams);
|
|
|
+ const containerExtension = format.formatName;
|
|
|
+ if (!mainVideoStream || !mainAudioStream || !containerExtension) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const { ffmpeg: config } = await this.configCore.getConfig();
|
|
|
|
|
|
- const required = this.isTranscodeRequired(stream, config);
|
|
|
+ const required = this.isTranscodeRequired(mainVideoStream, mainAudioStream, containerExtension, config);
|
|
|
if (!required) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const options = this.getFfmpegOptions(stream, config);
|
|
|
+ const options = this.getFfmpegOptions(mainVideoStream, config);
|
|
|
+
|
|
|
+ this.logger.log(`Start encoding video ${asset.id} ${options}`);
|
|
|
await this.mediaRepository.transcode(input, output, options);
|
|
|
|
|
|
- this.logger.log(`Converting Success ${asset.id}`);
|
|
|
+ this.logger.log(`Encoding success ${asset.id}`);
|
|
|
|
|
|
await this.assetRepository.save({ id: asset.id, encodedVideoPath: output });
|
|
|
} catch (error: any) {
|
|
@@ -151,32 +155,48 @@ export class MediaService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private getLongestStream(streams: VideoStreamInfo[]): VideoStreamInfo | null {
|
|
|
- return streams
|
|
|
- .filter((stream) => stream.codecType === 'video')
|
|
|
- .sort((stream1, stream2) => stream2.frameCount - stream1.frameCount)[0];
|
|
|
+ private getMainVideoStream(streams: VideoStreamInfo[]): VideoStreamInfo | null {
|
|
|
+ return streams.sort((stream1, stream2) => stream2.frameCount - stream1.frameCount)[0];
|
|
|
+ }
|
|
|
+
|
|
|
+ private getMainAudioStream(streams: AudioStreamInfo[]): AudioStreamInfo | null {
|
|
|
+ return streams[0];
|
|
|
}
|
|
|
|
|
|
- private isTranscodeRequired(stream: VideoStreamInfo, ffmpegConfig: SystemConfigFFmpegDto): boolean {
|
|
|
- if (!stream.height || !stream.width) {
|
|
|
+ private isTranscodeRequired(
|
|
|
+ videoStream: VideoStreamInfo,
|
|
|
+ audioStream: AudioStreamInfo,
|
|
|
+ containerExtension: string,
|
|
|
+ ffmpegConfig: SystemConfigFFmpegDto,
|
|
|
+ ): boolean {
|
|
|
+ if (!videoStream.height || !videoStream.width) {
|
|
|
this.logger.error('Skipping transcode, height or width undefined for video stream');
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
- const isTargetVideoCodec = stream.codecName === ffmpegConfig.targetVideoCodec;
|
|
|
+ const isTargetVideoCodec = videoStream.codecName === ffmpegConfig.targetVideoCodec;
|
|
|
+ const isTargetAudioCodec = audioStream.codecName === ffmpegConfig.targetAudioCodec;
|
|
|
+ const isTargetContainer = ['mov,mp4,m4a,3gp,3g2,mj2', 'mp4', 'mov'].includes(containerExtension);
|
|
|
+
|
|
|
+ this.logger.debug(audioStream.codecName, audioStream.codecType, containerExtension);
|
|
|
+
|
|
|
+ const allTargetsMatching = isTargetVideoCodec && isTargetAudioCodec && isTargetContainer;
|
|
|
|
|
|
const targetResolution = Number.parseInt(ffmpegConfig.targetResolution);
|
|
|
- const isLargerThanTargetResolution = Math.min(stream.height, stream.width) > targetResolution;
|
|
|
+ const isLargerThanTargetResolution = Math.min(videoStream.height, videoStream.width) > targetResolution;
|
|
|
|
|
|
switch (ffmpegConfig.transcode) {
|
|
|
+ case TranscodePreset.DISABLED:
|
|
|
+ return false;
|
|
|
+
|
|
|
case TranscodePreset.ALL:
|
|
|
return true;
|
|
|
|
|
|
case TranscodePreset.REQUIRED:
|
|
|
- return !isTargetVideoCodec;
|
|
|
+ return !allTargetsMatching;
|
|
|
|
|
|
case TranscodePreset.OPTIMAL:
|
|
|
- return !isTargetVideoCodec || isLargerThanTargetResolution;
|
|
|
+ return !allTargetsMatching || isLargerThanTargetResolution;
|
|
|
|
|
|
default:
|
|
|
return false;
|
|
@@ -184,8 +204,6 @@ export class MediaService {
|
|
|
}
|
|
|
|
|
|
private getFfmpegOptions(stream: VideoStreamInfo, ffmpeg: SystemConfigFFmpegDto) {
|
|
|
- // TODO: If video or audio are already the correct format, don't re-encode, copy the stream
|
|
|
-
|
|
|
const options = [
|
|
|
`-crf ${ffmpeg.crf}`,
|
|
|
`-preset ${ffmpeg.preset}`,
|