mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(all): transcoding improvements (#2171)
* test: rename some fixtures and add text for vertical video conversion * feat: transcode video asset when audio or container don't match target * chore: add niceness to the ffmpeg command to allow other processes to be prioritised * chore: change video conversion queue to one concurrency * feat: add transcode disabled preset to completely turn off transcoding * linter * Change log level and remove unused await * opps forgot to save * better logging --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
		| @@ -165,12 +165,14 @@ class SystemConfigFFmpegDtoTranscodeEnum { | |||||||
|   static const all = SystemConfigFFmpegDtoTranscodeEnum._(r'all'); |   static const all = SystemConfigFFmpegDtoTranscodeEnum._(r'all'); | ||||||
|   static const optimal = SystemConfigFFmpegDtoTranscodeEnum._(r'optimal'); |   static const optimal = SystemConfigFFmpegDtoTranscodeEnum._(r'optimal'); | ||||||
|   static const required_ = SystemConfigFFmpegDtoTranscodeEnum._(r'required'); |   static const required_ = SystemConfigFFmpegDtoTranscodeEnum._(r'required'); | ||||||
|  |   static const disabled = SystemConfigFFmpegDtoTranscodeEnum._(r'disabled'); | ||||||
| 
 | 
 | ||||||
|   /// List of all possible values in this [enum][SystemConfigFFmpegDtoTranscodeEnum]. |   /// List of all possible values in this [enum][SystemConfigFFmpegDtoTranscodeEnum]. | ||||||
|   static const values = <SystemConfigFFmpegDtoTranscodeEnum>[ |   static const values = <SystemConfigFFmpegDtoTranscodeEnum>[ | ||||||
|     all, |     all, | ||||||
|     optimal, |     optimal, | ||||||
|     required_, |     required_, | ||||||
|  |     disabled, | ||||||
|   ]; |   ]; | ||||||
| 
 | 
 | ||||||
|   static SystemConfigFFmpegDtoTranscodeEnum? fromJson(dynamic value) => SystemConfigFFmpegDtoTranscodeEnumTypeTransformer().decode(value); |   static SystemConfigFFmpegDtoTranscodeEnum? fromJson(dynamic value) => SystemConfigFFmpegDtoTranscodeEnumTypeTransformer().decode(value); | ||||||
| @@ -212,6 +214,7 @@ class SystemConfigFFmpegDtoTranscodeEnumTypeTransformer { | |||||||
|         case r'all': return SystemConfigFFmpegDtoTranscodeEnum.all; |         case r'all': return SystemConfigFFmpegDtoTranscodeEnum.all; | ||||||
|         case r'optimal': return SystemConfigFFmpegDtoTranscodeEnum.optimal; |         case r'optimal': return SystemConfigFFmpegDtoTranscodeEnum.optimal; | ||||||
|         case r'required': return SystemConfigFFmpegDtoTranscodeEnum.required_; |         case r'required': return SystemConfigFFmpegDtoTranscodeEnum.required_; | ||||||
|  |         case r'disabled': return SystemConfigFFmpegDtoTranscodeEnum.disabled; | ||||||
|         default: |         default: | ||||||
|           if (!allowNull) { |           if (!allowNull) { | ||||||
|             throw ArgumentError('Unknown enum value to decode: $data'); |             throw ArgumentError('Unknown enum value to decode: $data'); | ||||||
|   | |||||||
| @@ -163,7 +163,7 @@ export class VideoTranscodeProcessor { | |||||||
|     await this.mediaService.handleQueueVideoConversion(job.data); |     await this.mediaService.handleQueueVideoConversion(job.data); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @Process({ name: JobName.VIDEO_CONVERSION, concurrency: 2 }) |   @Process({ name: JobName.VIDEO_CONVERSION, concurrency: 1 }) | ||||||
|   async onVideoConversion(job: Job<IAssetJob>) { |   async onVideoConversion(job: Job<IAssetJob>) { | ||||||
|     await this.mediaService.handleVideoConversion(job.data); |     await this.mediaService.handleVideoConversion(job.data); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -4679,7 +4679,8 @@ | |||||||
|             "enum": [ |             "enum": [ | ||||||
|               "all", |               "all", | ||||||
|               "optimal", |               "optimal", | ||||||
|               "required" |               "required", | ||||||
|  |               "disabled" | ||||||
|             ] |             ] | ||||||
|           } |           } | ||||||
|         }, |         }, | ||||||
|   | |||||||
| @@ -14,8 +14,21 @@ export interface VideoStreamInfo { | |||||||
|   frameCount: number; |   frameCount: number; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export interface AudioStreamInfo { | ||||||
|  |   codecName?: string; | ||||||
|  |   codecType?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface VideoFormat { | ||||||
|  |   formatName?: string; | ||||||
|  |   formatLongName?: string; | ||||||
|  |   duration: number; | ||||||
|  | } | ||||||
|  |  | ||||||
| export interface VideoInfo { | export interface VideoInfo { | ||||||
|   streams: VideoStreamInfo[]; |   format: VideoFormat; | ||||||
|  |   videoStreams: VideoStreamInfo[]; | ||||||
|  |   audioStreams: AudioStreamInfo[]; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface IMediaRepository { | export interface IMediaRepository { | ||||||
|   | |||||||
| @@ -222,7 +222,7 @@ describe(MediaService.name, () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should transcode the longest stream', async () => { |     it('should transcode the longest stream', async () => { | ||||||
|       mediaMock.probe.mockResolvedValue(probeStub.multiple); |       mediaMock.probe.mockResolvedValue(probeStub.multipleVideoStreams); | ||||||
|  |  | ||||||
|       await sut.handleVideoConversion({ asset: assetEntityStub.video }); |       await sut.handleVideoConversion({ asset: assetEntityStub.video }); | ||||||
|  |  | ||||||
| @@ -237,7 +237,7 @@ describe(MediaService.name, () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should skip a video without any streams', async () => { |     it('should skip a video without any streams', async () => { | ||||||
|       mediaMock.probe.mockResolvedValue(probeStub.empty); |       mediaMock.probe.mockResolvedValue(probeStub.noVideoStreams); | ||||||
|       await sut.handleVideoConversion({ asset: assetEntityStub.video }); |       await sut.handleVideoConversion({ asset: assetEntityStub.video }); | ||||||
|       expect(mediaMock.transcode).not.toHaveBeenCalled(); |       expect(mediaMock.transcode).not.toHaveBeenCalled(); | ||||||
|     }); |     }); | ||||||
| @@ -249,7 +249,7 @@ describe(MediaService.name, () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should transcode when set to all', async () => { |     it('should transcode when set to all', async () => { | ||||||
|       mediaMock.probe.mockResolvedValue(probeStub.multiple); |       mediaMock.probe.mockResolvedValue(probeStub.multipleVideoStreams); | ||||||
|       configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: 'all' }]); |       configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: 'all' }]); | ||||||
|       await sut.handleVideoConversion({ asset: assetEntityStub.video }); |       await sut.handleVideoConversion({ asset: assetEntityStub.video }); | ||||||
|       expect(mediaMock.transcode).toHaveBeenCalledWith( |       expect(mediaMock.transcode).toHaveBeenCalledWith( | ||||||
| @@ -260,7 +260,40 @@ describe(MediaService.name, () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should transcode when optimal and too big', async () => { |     it('should transcode when optimal and too big', async () => { | ||||||
|       mediaMock.probe.mockResolvedValue(probeStub.tooBig); |       mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p); | ||||||
|  |       configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: 'optimal' }]); | ||||||
|  |       await sut.handleVideoConversion({ asset: assetEntityStub.video }); | ||||||
|  |       expect(mediaMock.transcode).toHaveBeenCalledWith( | ||||||
|  |         '/original/path.ext', | ||||||
|  |         'upload/encoded-video/user-id/asset-id.mp4', | ||||||
|  |         ['-crf 23', '-preset ultrafast', '-vcodec h264', '-acodec aac', '-movflags faststart', '-vf scale=-2:720'], | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('should transcode with alternate scaling video is vertical', async () => { | ||||||
|  |       mediaMock.probe.mockResolvedValue(probeStub.videoStreamVertical2160p); | ||||||
|  |       configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: 'optimal' }]); | ||||||
|  |       await sut.handleVideoConversion({ asset: assetEntityStub.video }); | ||||||
|  |       expect(mediaMock.transcode).toHaveBeenCalledWith( | ||||||
|  |         '/original/path.ext', | ||||||
|  |         'upload/encoded-video/user-id/asset-id.mp4', | ||||||
|  |         ['-crf 23', '-preset ultrafast', '-vcodec h264', '-acodec aac', '-movflags faststart', '-vf scale=720:-2'], | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('should transcode when audio doesnt match target', async () => { | ||||||
|  |       mediaMock.probe.mockResolvedValue(probeStub.audioStreamMp3); | ||||||
|  |       configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: 'optimal' }]); | ||||||
|  |       await sut.handleVideoConversion({ asset: assetEntityStub.video }); | ||||||
|  |       expect(mediaMock.transcode).toHaveBeenCalledWith( | ||||||
|  |         '/original/path.ext', | ||||||
|  |         'upload/encoded-video/user-id/asset-id.mp4', | ||||||
|  |         ['-crf 23', '-preset ultrafast', '-vcodec h264', '-acodec aac', '-movflags faststart', '-vf scale=-2:720'], | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     it('should transcode when container doesnt match target', async () => { | ||||||
|  |       mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); | ||||||
|       configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: 'optimal' }]); |       configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: 'optimal' }]); | ||||||
|       await sut.handleVideoConversion({ asset: assetEntityStub.video }); |       await sut.handleVideoConversion({ asset: assetEntityStub.video }); | ||||||
|       expect(mediaMock.transcode).toHaveBeenCalledWith( |       expect(mediaMock.transcode).toHaveBeenCalledWith( | ||||||
| @@ -271,7 +304,7 @@ describe(MediaService.name, () => { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     it('should not transcode an invalid transcode value', async () => { |     it('should not transcode an invalid transcode value', async () => { | ||||||
|       mediaMock.probe.mockResolvedValue(probeStub.tooBig); |       mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p); | ||||||
|       configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: 'invalid' }]); |       configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TRANSCODE, value: 'invalid' }]); | ||||||
|       await sut.handleVideoConversion({ asset: assetEntityStub.video }); |       await sut.handleVideoConversion({ asset: assetEntityStub.video }); | ||||||
|       expect(mediaMock.transcode).not.toHaveBeenCalled(); |       expect(mediaMock.transcode).not.toHaveBeenCalled(); | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ import { IAssetJob, IBaseJob, IJobRepository, JobName } from '../job'; | |||||||
| import { IStorageRepository, StorageCore, StorageFolder } from '../storage'; | import { IStorageRepository, StorageCore, StorageFolder } from '../storage'; | ||||||
| import { ISystemConfigRepository, SystemConfigFFmpegDto } from '../system-config'; | import { ISystemConfigRepository, SystemConfigFFmpegDto } from '../system-config'; | ||||||
| import { SystemConfigCore } from '../system-config/system-config.core'; | import { SystemConfigCore } from '../system-config/system-config.core'; | ||||||
| import { IMediaRepository, VideoStreamInfo } from './media.repository'; | import { AudioStreamInfo, IMediaRepository, VideoStreamInfo } from './media.repository'; | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class MediaService { | export class MediaService { | ||||||
| @@ -127,23 +127,27 @@ export class MediaService { | |||||||
|       const output = join(outputFolder, `${asset.id}.mp4`); |       const output = join(outputFolder, `${asset.id}.mp4`); | ||||||
|       this.storageRepository.mkdirSync(outputFolder); |       this.storageRepository.mkdirSync(outputFolder); | ||||||
|  |  | ||||||
|       const { streams } = await this.mediaRepository.probe(input); |       const { videoStreams, audioStreams, format } = await this.mediaRepository.probe(input); | ||||||
|       const stream = await this.getLongestStream(streams); |       const mainVideoStream = this.getMainVideoStream(videoStreams); | ||||||
|       if (!stream) { |       const mainAudioStream = this.getMainAudioStream(audioStreams); | ||||||
|  |       const containerExtension = format.formatName; | ||||||
|  |       if (!mainVideoStream || !mainAudioStream || !containerExtension) { | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       const { ffmpeg: config } = await this.configCore.getConfig(); |       const { ffmpeg: config } = await this.configCore.getConfig(); | ||||||
|  |  | ||||||
|       const required = this.isTranscodeRequired(stream, config); |       const required = this.isTranscodeRequired(mainVideoStream, mainAudioStream, containerExtension, config); | ||||||
|       if (!required) { |       if (!required) { | ||||||
|         return; |         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); |       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 }); |       await this.assetRepository.save({ id: asset.id, encodedVideoPath: output }); | ||||||
|     } catch (error: any) { |     } catch (error: any) { | ||||||
| @@ -151,32 +155,48 @@ export class MediaService { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private getLongestStream(streams: VideoStreamInfo[]): VideoStreamInfo | null { |   private getMainVideoStream(streams: VideoStreamInfo[]): VideoStreamInfo | null { | ||||||
|     return streams |     return streams.sort((stream1, stream2) => stream2.frameCount - stream1.frameCount)[0]; | ||||||
|       .filter((stream) => stream.codecType === 'video') |  | ||||||
|       .sort((stream1, stream2) => stream2.frameCount - stream1.frameCount)[0]; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private isTranscodeRequired(stream: VideoStreamInfo, ffmpegConfig: SystemConfigFFmpegDto): boolean { |   private getMainAudioStream(streams: AudioStreamInfo[]): AudioStreamInfo | null { | ||||||
|     if (!stream.height || !stream.width) { |     return streams[0]; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   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'); |       this.logger.error('Skipping transcode, height or width undefined for video stream'); | ||||||
|       return false; |       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 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) { |     switch (ffmpegConfig.transcode) { | ||||||
|  |       case TranscodePreset.DISABLED: | ||||||
|  |         return false; | ||||||
|  |  | ||||||
|       case TranscodePreset.ALL: |       case TranscodePreset.ALL: | ||||||
|         return true; |         return true; | ||||||
|  |  | ||||||
|       case TranscodePreset.REQUIRED: |       case TranscodePreset.REQUIRED: | ||||||
|         return !isTargetVideoCodec; |         return !allTargetsMatching; | ||||||
|  |  | ||||||
|       case TranscodePreset.OPTIMAL: |       case TranscodePreset.OPTIMAL: | ||||||
|         return !isTargetVideoCodec || isLargerThanTargetResolution; |         return !allTargetsMatching || isLargerThanTargetResolution; | ||||||
|  |  | ||||||
|       default: |       default: | ||||||
|         return false; |         return false; | ||||||
| @@ -184,8 +204,6 @@ export class MediaService { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private getFfmpegOptions(stream: VideoStreamInfo, ffmpeg: SystemConfigFFmpegDto) { |   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 = [ |     const options = [ | ||||||
|       `-crf ${ffmpeg.crf}`, |       `-crf ${ffmpeg.crf}`, | ||||||
|       `-preset ${ffmpeg.preset}`, |       `-preset ${ffmpeg.preset}`, | ||||||
|   | |||||||
| @@ -13,12 +13,15 @@ import { | |||||||
| import { | import { | ||||||
|   AlbumResponseDto, |   AlbumResponseDto, | ||||||
|   AssetResponseDto, |   AssetResponseDto, | ||||||
|  |   AudioStreamInfo, | ||||||
|   AuthUserDto, |   AuthUserDto, | ||||||
|   ExifResponseDto, |   ExifResponseDto, | ||||||
|   mapUser, |   mapUser, | ||||||
|   SearchResult, |   SearchResult, | ||||||
|   SharedLinkResponseDto, |   SharedLinkResponseDto, | ||||||
|  |   VideoFormat, | ||||||
|   VideoInfo, |   VideoInfo, | ||||||
|  |   VideoStreamInfo, | ||||||
| } from '../src'; | } from '../src'; | ||||||
|  |  | ||||||
| const today = new Date(); | const today = new Date(); | ||||||
| @@ -706,10 +709,29 @@ export const searchStub = { | |||||||
|   }), |   }), | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | const probeStubDefaultFormat: VideoFormat = { | ||||||
|  |   formatName: 'mov,mp4,m4a,3gp,3g2,mj2', | ||||||
|  |   formatLongName: 'QuickTime / MOV', | ||||||
|  |   duration: 0, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const probeStubDefaultVideoStream: VideoStreamInfo[] = [ | ||||||
|  |   { height: 1080, width: 1920, codecName: 'h265', codecType: 'video', frameCount: 100, rotation: 0 }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | const probeStubDefaultAudioStream: AudioStreamInfo[] = [{ codecName: 'aac', codecType: 'audio' }]; | ||||||
|  |  | ||||||
|  | const probeStubDefault: VideoInfo = { | ||||||
|  |   format: probeStubDefaultFormat, | ||||||
|  |   videoStreams: probeStubDefaultVideoStream, | ||||||
|  |   audioStreams: probeStubDefaultAudioStream, | ||||||
|  | }; | ||||||
|  |  | ||||||
| export const probeStub = { | export const probeStub = { | ||||||
|   empty: { streams: [] }, |   noVideoStreams: Object.freeze<VideoInfo>({ ...probeStubDefault, videoStreams: [] }), | ||||||
|   multiple: Object.freeze<VideoInfo>({ |   multipleVideoStreams: Object.freeze<VideoInfo>({ | ||||||
|     streams: [ |     ...probeStubDefault, | ||||||
|  |     videoStreams: [ | ||||||
|       { |       { | ||||||
|         height: 1080, |         height: 1080, | ||||||
|         width: 400, |         width: 400, | ||||||
| @@ -729,7 +751,8 @@ export const probeStub = { | |||||||
|     ], |     ], | ||||||
|   }), |   }), | ||||||
|   noHeight: Object.freeze<VideoInfo>({ |   noHeight: Object.freeze<VideoInfo>({ | ||||||
|     streams: [ |     ...probeStubDefault, | ||||||
|  |     videoStreams: [ | ||||||
|       { |       { | ||||||
|         height: 0, |         height: 0, | ||||||
|         width: 400, |         width: 400, | ||||||
| @@ -740,11 +763,12 @@ export const probeStub = { | |||||||
|       }, |       }, | ||||||
|     ], |     ], | ||||||
|   }), |   }), | ||||||
|   tooBig: Object.freeze<VideoInfo>({ |   videoStream2160p: Object.freeze<VideoInfo>({ | ||||||
|     streams: [ |     ...probeStubDefault, | ||||||
|  |     videoStreams: [ | ||||||
|       { |       { | ||||||
|         height: 10000, |         height: 2160, | ||||||
|         width: 10000, |         width: 3840, | ||||||
|         codecName: 'h264', |         codecName: 'h264', | ||||||
|         codecType: 'video', |         codecType: 'video', | ||||||
|         frameCount: 100, |         frameCount: 100, | ||||||
| @@ -752,4 +776,29 @@ export const probeStub = { | |||||||
|       }, |       }, | ||||||
|     ], |     ], | ||||||
|   }), |   }), | ||||||
|  |   videoStreamVertical2160p: Object.freeze<VideoInfo>({ | ||||||
|  |     ...probeStubDefault, | ||||||
|  |     videoStreams: [ | ||||||
|  |       { | ||||||
|  |         height: 2160, | ||||||
|  |         width: 3840, | ||||||
|  |         codecName: 'h264', | ||||||
|  |         codecType: 'video', | ||||||
|  |         frameCount: 100, | ||||||
|  |         rotation: 90, | ||||||
|  |       }, | ||||||
|  |     ], | ||||||
|  |   }), | ||||||
|  |   audioStreamMp3: Object.freeze<VideoInfo>({ | ||||||
|  |     ...probeStubDefault, | ||||||
|  |     audioStreams: [{ codecType: 'audio', codecName: 'aac' }], | ||||||
|  |   }), | ||||||
|  |   matroskaContainer: Object.freeze<VideoInfo>({ | ||||||
|  |     ...probeStubDefault, | ||||||
|  |     format: { | ||||||
|  |       formatName: 'matroska,webm', | ||||||
|  |       formatLongName: 'Matroska / WebM', | ||||||
|  |       duration: 0, | ||||||
|  |     }, | ||||||
|  |   }), | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -37,6 +37,7 @@ export enum TranscodePreset { | |||||||
|   ALL = 'all', |   ALL = 'all', | ||||||
|   OPTIMAL = 'optimal', |   OPTIMAL = 'optimal', | ||||||
|   REQUIRED = 'required', |   REQUIRED = 'required', | ||||||
|  |   DISABLED = 'disabled', | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface SystemConfig { | export interface SystemConfig { | ||||||
|   | |||||||
| @@ -50,20 +50,33 @@ export class MediaRepository implements IMediaRepository { | |||||||
|     const results = await probe(input); |     const results = await probe(input); | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|       streams: results.streams.map((stream) => ({ |       format: { | ||||||
|         height: stream.height || 0, |         formatName: results.format.format_name, | ||||||
|         width: stream.width || 0, |         formatLongName: results.format.format_long_name, | ||||||
|         codecName: stream.codec_name, |         duration: results.format.duration || 0, | ||||||
|         codecType: stream.codec_type, |       }, | ||||||
|         frameCount: Number.parseInt(stream.nb_frames ?? '0'), |       videoStreams: results.streams | ||||||
|         rotation: Number.parseInt(`${stream.rotation ?? 0}`), |         .filter((stream) => stream.codec_type === 'video') | ||||||
|       })), |         .map((stream) => ({ | ||||||
|  |           height: stream.height || 0, | ||||||
|  |           width: stream.width || 0, | ||||||
|  |           codecName: stream.codec_name, | ||||||
|  |           codecType: stream.codec_type, | ||||||
|  |           frameCount: Number.parseInt(stream.nb_frames ?? '0'), | ||||||
|  |           rotation: Number.parseInt(`${stream.rotation ?? 0}`), | ||||||
|  |         })), | ||||||
|  |       audioStreams: results.streams | ||||||
|  |         .filter((stream) => stream.codec_type === 'audio') | ||||||
|  |         .map((stream) => ({ | ||||||
|  |           codecType: stream.codec_type, | ||||||
|  |           codecName: stream.codec_name, | ||||||
|  |         })), | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   transcode(input: string, output: string, options: string[]): Promise<void> { |   transcode(input: string, output: string, options: string[]): Promise<void> { | ||||||
|     return new Promise((resolve, reject) => { |     return new Promise((resolve, reject) => { | ||||||
|       ffmpeg(input) |       ffmpeg(input, { niceness: 10 }) | ||||||
|         // |         // | ||||||
|         .outputOptions(options) |         .outputOptions(options) | ||||||
|         .output(output) |         .output(output) | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -2046,7 +2046,8 @@ export interface SystemConfigFFmpegDto { | |||||||
| export const SystemConfigFFmpegDtoTranscodeEnum = { | export const SystemConfigFFmpegDtoTranscodeEnum = { | ||||||
|     All: 'all', |     All: 'all', | ||||||
|     Optimal: 'optimal', |     Optimal: 'optimal', | ||||||
|     Required: 'required' |     Required: 'required', | ||||||
|  |     Disabled: 'disabled' | ||||||
| } as const; | } as const; | ||||||
| 
 | 
 | ||||||
| export type SystemConfigFFmpegDtoTranscodeEnum = typeof SystemConfigFFmpegDtoTranscodeEnum[keyof typeof SystemConfigFFmpegDtoTranscodeEnum]; | export type SystemConfigFFmpegDtoTranscodeEnum = typeof SystemConfigFFmpegDtoTranscodeEnum[keyof typeof SystemConfigFFmpegDtoTranscodeEnum]; | ||||||
|   | |||||||
| @@ -93,16 +93,20 @@ | |||||||
| 						isEdited={!(ffmpegConfig.preset == savedConfig.preset)} | 						isEdited={!(ffmpegConfig.preset == savedConfig.preset)} | ||||||
| 					/> | 					/> | ||||||
|  |  | ||||||
| 					<SettingInputField | 					<SettingSelect | ||||||
| 						inputType={SettingInputFieldType.TEXT} | 						label="AUDIO CODEC" | ||||||
| 						label="AUDIO CODEC (-acodec)" |  | ||||||
| 						bind:value={ffmpegConfig.targetAudioCodec} | 						bind:value={ffmpegConfig.targetAudioCodec} | ||||||
| 						required={true} | 						options={[ | ||||||
|  | 							{ value: 'aac', text: 'aac' }, | ||||||
|  | 							{ value: 'mp3', text: 'mp3' }, | ||||||
|  | 							{ value: 'opus', text: 'opus' } | ||||||
|  | 						]} | ||||||
|  | 						name="acodec" | ||||||
| 						isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)} | 						isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)} | ||||||
| 					/> | 					/> | ||||||
|  |  | ||||||
| 					<SettingSelect | 					<SettingSelect | ||||||
| 						label="VIDEO CODEC (-vcodec)" | 						label="VIDEO CODEC" | ||||||
| 						bind:value={ffmpegConfig.targetVideoCodec} | 						bind:value={ffmpegConfig.targetVideoCodec} | ||||||
| 						options={[ | 						options={[ | ||||||
| 							{ value: 'h264', text: 'h264' }, | 							{ value: 'h264', text: 'h264' }, | ||||||
| @@ -140,6 +144,10 @@ | |||||||
| 							{ | 							{ | ||||||
| 								value: SystemConfigFFmpegDtoTranscodeEnum.Required, | 								value: SystemConfigFFmpegDtoTranscodeEnum.Required, | ||||||
| 								text: 'Only videos not in the desired format' | 								text: 'Only videos not in the desired format' | ||||||
|  | 							}, | ||||||
|  | 							{ | ||||||
|  | 								value: SystemConfigFFmpegDtoTranscodeEnum.Disabled, | ||||||
|  | 								text: "Don't transcode any videos, may break playback on some clients" | ||||||
| 							} | 							} | ||||||
| 						]} | 						]} | ||||||
| 						isEdited={!(ffmpegConfig.transcode == savedConfig.transcode)} | 						isEdited={!(ffmpegConfig.transcode == savedConfig.transcode)} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user