This page covers thumbnail and preview generation for photos and videos, video transcoding with FFmpeg, supported image and video formats, and hardware acceleration integration for transcoding. For EXIF and metadata extraction see 3.3. For the job queue system that drives these operations see 3.2. For hardware acceleration setup and deployment see 5.2.
All media processing is orchestrated by MediaService (server/src/services/media.service.ts61-700). It uses @OnJob() decorators to register handlers onto BullMQ queues. The actual image and video operations are delegated to MediaRepository (server/src/repositories/media.repository.ts) which wraps sharp (image processing) and fluent-ffmpeg (video probing and transcoding).
Job handler summary:
| Job Name (enum) | Queue | Handler Method |
|---|---|---|
AssetGenerateThumbnailsQueueAll | ThumbnailGeneration | handleQueueGenerateThumbnails |
AssetGenerateThumbnails | ThumbnailGeneration | handleGenerateThumbnails |
AssetEditThumbnailGeneration | Editor | handleAssetEditThumbnailGeneration |
PersonGenerateThumbnail | ThumbnailGeneration | handleGeneratePersonThumbnail |
AssetEncodeVideo | VideoConversion | handleEncodeVideo |
FileMigrationQueueAll | Migration | handleQueueMigration |
AssetFileMigration | Migration | handleAssetMigration |
Sources: server/src/services/media.service.ts70-118 server/src/services/media.service.ts155-170
The handleGenerateThumbnails method (server/src/services/media.service.ts210-250) dispatches to one of two code paths depending on asset type:
asset.type === AssetType.Video or GIF → generateVideoThumbnails()asset.type === AssetType.Image → generateImageThumbnails()Hidden assets (AssetVisibility.Hidden) are skipped. Edited assets trigger an additional generateEditedThumbnails() call. After all files are written, the thumbhash column on the asset is updated.
Thumbnail generation flow:
Sources: server/src/services/media.service.ts210-250 server/src/services/media.service.ts309-406
Three file types, stored as asset_file records (type AssetFileType), are produced per asset:
AssetFileType | Default Format | Default Size | Description |
|---|---|---|---|
Preview | JPEG | 1440 px | Used in asset viewer |
Thumbnail | WebP | 250 px | Used in grid timeline |
FullSize | JPEG | original | Non-web-native formats and panoramas |
FullSize is only generated when image.fullsize.enabled is true or the asset is an EQUIRECTANGULAR panorama, or when the source is a RAW format not natively viewable in a browser. Progressive encoding is configurable per file type.
Storage paths follow the pattern (from StorageCore):
{UPLOAD_LOCATION}/thumbs/{ownerId}/{id[0..1]}/{id[2..3]}/{id}_preview.jpeg
{UPLOAD_LOCATION}/thumbs/{ownerId}/{id[0..1]}/{id[2..3]}/{id}_thumbnail.webp
{UPLOAD_LOCATION}/thumbs/{ownerId}/{id[0..1]}/{id[2..3]}/{id}_fullsize.jpeg
Sources: server/src/services/media.service.ts314-406 server/src/services/media.service.spec.ts319-340
decodeImage() in MediaRepository uses Sharp to decode the image to a raw pixel buffer. The colorspace is determined by MediaService.isSRGB():
Colorspace.Srgbimage.colorspace (default Colorspace.P3)For RAW files (mimeTypes.isRaw()), MediaRepository.extract() attempts to pull an embedded JPEG preview. If the extracted preview is large enough relative to the target size, it is used as the decode source; otherwise the RAW file itself is decoded directly.
Sources: server/src/services/media.service.ts252-308 server/src/services/media.service.spec.ts413-490
MediaRepository.generateThumbhash() computes a compact (≈20 byte) perceptual hash from the decoded image buffer. This is stored on the asset.thumbhash column and used by the web and mobile clients to display a blurry placeholder before the full thumbnail loads.
For video assets, generateVideoThumbnails() runs FFmpeg with a specialized filter chain designed to select the most representative frame:
-vf fps=12:start_time=0:eof_action=pass:round=down,
thumbnail=12,
select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),
trim=end_frame=2,reverse,
scale=-2:1440:flags=lanczos+accurate_rnd+full_chroma_int:out_range=pc
Input options include -skip_frame nointra to speed up seeking. The output is a single image (-frames:v 1), which is then processed by generateImageThumbnails().
Sources: server/src/services/media.service.spec.ts493-514
handleGeneratePersonThumbnail (server/src/services/media.service.ts408-470) generates face crop thumbnails. The input is the asset's preview path for videos, or the original file (with optional embedded extraction for RAW). The face bounding box from asset_face is used to crop using FACE_THUMBNAIL_SIZE (constant). Output is stored at StorageCore.getPersonThumbnailPath().
When an asset has edits (AssetEditAction records such as crop, rotate, mirror), generateEditedThumbnails() applies those edits using asset.edits passed to generateThumbnail(). Edited thumbnails are stored as asset_file rows with isEdited = true. The job AssetEditThumbnailGeneration handles regeneration when edits change.
handleEncodeVideo reads config.ffmpeg.transcode to determine whether to transcode. The TranscodePolicy enum controls this:
| Policy | Behavior |
|---|---|
Disabled | Never transcode; serve original |
All | Always transcode every video |
Optimal | Transcode only if codec/container not web-compatible |
BitrateThreshold | Transcode if bitrate exceeds config.ffmpeg.maxBitrate |
Sources: server/src/utils/media.ts14-84 server/src/services/media.service.spec.ts1-30
BaseConfig.create() (server/src/utils/media.ts18-83) is the factory entry point. It returns a VideoCodecSWConfig or VideoCodecHWConfig depending on the accel setting. All configurations implement getCommand() which returns a TranscodeCommand containing inputOptions, outputOptions, and a twoPass flag.
Codec class hierarchy:
Sources: server/src/utils/media.ts14-84
On AppBootstrap, MediaService.onBootstrap() (server/src/services/media.service.ts64-68) scans for available hardware interfaces:
/dev/dri/*) — used for VAAPI and RKMPPThe videoInterfaces: VideoInterfaces field is passed into BaseConfig.create() to inform the HW config classes which devices are available.
TranscodeHardwareAcceleration | SW Decode Class | HW Decode Class | Supported Codecs |
|---|---|---|---|
Nvenc | NvencSwDecodeConfig | NvencHwDecodeConfig | H264, HEVC, AV1 |
Qsv | QsvSwDecodeConfig | QsvHwDecodeConfig | H264, HEVC, VP9, AV1 |
Vaapi | VaapiSwDecodeConfig | VaapiHwDecodeConfig | H264, HEVC, VP9, AV1 |
Rkmpp | RkmppSwDecodeConfig | RkmppHwDecodeConfig | H264, HEVC |
config.ffmpeg.accelDecode controls whether to use hardware-accelerated decoding in addition to hardware encoding. Some codec/acceleration combinations are validated at startup; an unsupported combination throws immediately.
Sources: server/src/utils/media.ts45-83
getCommand() in BaseConfig builds options in layers:
-hwaccel, -hwaccel_output_format, etc.)-movflags faststart, stream mapping, -map_metadata -1The twoPass flag in TranscodeCommand triggers a two-pass encode in MediaRepository.transcode() by running FFmpeg twice.
Sources: server/src/utils/media.ts85-155
Encoded video is written to:
{UPLOAD_LOCATION}/encoded-video/{ownerId}/{id[0..1]}/{id[2..3]}/{id}.mp4
After successful encoding, the asset's encodedVideoPath column is updated. Video playback in the web and mobile clients uses the encoded video when available and falls back to the original.
Immich's format support is defined in mimeTypes (server/src/utils/mime-types.ts):
| Category | Extensions |
|---|---|
| RAW | .3fr .ari .arw .cap .cin .cr2 .cr3 .crw .dcr .dng .erf .fff .iiq .k25 .kdc .mrw .nef .orf .ori .pef .raf .raw .rwl .sr2 .srf .srw .x3f |
| Standard | .avif .gif .heic .heif .jp2 .jpeg .jpg .jxl .png .psd .svg .tiff .webp |
mimeTypes.isRaw() returns true for RAW extensions, enabling embedded preview extraction. mimeTypes.isWebSupportedImage() determines whether fullsize conversion is needed. mimeTypes.canBeTransparent() identifies formats requiring transparency handling (PNG, WebP, etc.).
.3gp .avi .flv .m2ts .mkv .mov .mp4 .mpg .mts .ogg .ts .webm .wmv
Sources: server/src/utils/mime-types.ts1-150 server/src/services/asset-media.service.spec.ts59-100
When the storage template or output format configuration changes, the Migration queue handles moving existing generated files to their new paths.
handleQueueMigration (server/src/services/media.service.ts120-153) streams all assets via assetJobRepository.streamForMigrationJob() and queues AssetFileMigration jobs. It also removes empty directories from the Thumbnails and EncodedVideo storage folders.
handleAssetMigration (server/src/services/media.service.ts155-169) calls StorageCore.moveAssetImage() for each of the three image file types and StorageCore.moveAssetVideo() for the encoded video.
Sources: server/src/services/media.service.ts120-169
Sources: server/src/services/media.service.ts61-700 server/src/repositories/media.repository.ts1-400 server/src/utils/media.ts1-800
Refresh this wiki
This wiki was recently refreshed. Please wait 2 days to refresh again.