Understanding media.service.ts: Immich's Central Hub for Media Processing and Video Transcoding
media.service.ts serves as the core orchestrator for all server-side media workflows in Immich, managing thumbnail generation, video transcoding with FFmpeg, hardware acceleration detection, and database synchronization for processed assets.
Immich relies on a sophisticated background job system to transform raw uploads into web-ready previews and optimized video formats. At the heart of this pipeline sits server/src/services/media.service.ts, a TypeScript service that coordinates between FFmpeg, Sharp, database repositories, and the storage subsystem to process images and videos at scale.
Core Responsibilities of MediaService
The MediaService class handles the complete lifecycle of media processing through several specialized pipelines. It receives jobs via the @OnJob decorators from the background queue system and delegates low-level operations to the MediaRepository while maintaining database consistency.
Thumbnail Generation for Images and Videos
Thumbnail generation is the primary workflow for both image and video assets. The service detects asset types and routes them through distinct pipelines:
- Image pipeline: Extracts RAW previews when necessary, decodes the source, applies color-space transformations, and generates preview and thumbnail files using Sharp
- Video pipeline: Extracts frames from videos and animated GIFs using FFmpeg, then processes them through the image pipeline
- Thumbhash creation: Generates perceptual hashes for blurred placeholder images during loading
The entry point handleGenerateThumbnails retrieves the asset and configuration, then dispatches to generateImageThumbnails or generateVideoThumbnails based on the asset type. For RAW images, the service first calls shouldUseExtractedImage to determine whether to extract the embedded preview or process the raw sensor data directly.
Video Transcoding and Format Optimization
Video transcoding decisions happen in handleVideoConversion, which probes source files to determine if processing is required. The service implements intelligent targeting through:
getTranscodeTarget: ReturnsTranscodeTarget.Video,TranscodeTarget.Audio,TranscodeTarget.Both, orTranscodeTarget.Nonebased on codec compatibilityisVideoTranscodeRequiredandisAudioTranscodeRequired: Check if streams meet the configured output requirementsisRemuxRequired: Determines if container format changes are needed without re-encoding
When transcoding is necessary, the service builds FFmpeg commands through BaseConfig.create(), applying hardware acceleration interfaces detected at startup. The service handles bitrate calculations via parseBitrateToBps and warns about transparency loss for certain codec combinations using warnOnTransparencyLoss.
Handling User Edits and RAW Images
Edited-image handling regenerates thumbnails after users apply crops, rotations, or adjustments. The handleAssetEditThumbnailGeneration method calls generateEditedThumbnails, which processes the modified image data and updates visibility for associated face detection and OCR records.
For RAW photography workflows, extractOriginalImage handles the complexity of various camera formats. It integrates with the MediaRepository to extract embedded JPEG previews when shouldUseExtractedImage determines that processing the full raw sensor data would be inefficient or unnecessary.
Hardware Acceleration Discovery
Device discovery occurs during the onBootstrap lifecycle hook. The service scans for available Direct Rendering Infrastructure (DRI) devices and detects Mali OpenCL support through getDevices and hasMaliOpenCL. These capabilities are stored in videoInterfaces and passed to the FFmpeg command builder to enable hardware-accelerated encoding when configured.
Database and File System Synchronization
File-system sync ensures database records match on-disk files created during processing. The syncFiles method compares existing AssetFile records against newly generated files, producing lists for upsert operations and path deletions. This maintains referential integrity when thumbnails are regenerated or when edited versions replace previous outputs.
Inside the Implementation: Key Code Patterns
The following excerpts from server/src/services/media.service.ts demonstrate the service's orchestration logic.
Thumbnail Generation Workflow
The main entry point coordinates asset retrieval, pipeline selection, and file synchronization:
// Called by the ThumbnailGeneration job queue
async handleGenerateThumbnails({ id }: JobOf<JobName.AssetGenerateThumbnails>) {
const asset = await this.assetJobRepository.getForGenerateThumbnailJob(id);
const config = await this.getConfig({ withCache: true });
// Choose pipeline based on asset type
const generated = asset.type === AssetType.Video || asset.originalFileName.toLowerCase().endsWith('.gif')
? await this.generateVideoThumbnails(asset, config)
: await this.generateImageThumbnails(asset, config);
// Merge edited thumbnails (if any) and sync DB/files
const edited = await this.generateEditedThumbnails(asset, config);
if (edited) generated.files.push(...edited.files);
await this.syncFiles(asset.files, generated.files);
// ... additional processing
}
This method leverages generateImageThumbnails for standard images and generateVideoThumbnails for motion content, then merges any edited versions before calling syncFiles to persist the changes.
Image Processing Pipeline
The image thumbnail generation handles color-space detection and parallel output generation:
private async generateImageThumbnails(
asset: ThumbnailAsset,
{ image }: SystemConfig,
useEdits = false,
) {
const extracted = await this.extractOriginalImage(asset, image, useEdits);
const { data, colorspace, isTransparent } = extracted;
// Build preview & thumbnail files
const previewFile = this.getImageFile(asset, { /* ... */ });
const thumbnailFile = this.getImageFile(asset, { /* ... */ });
// Options passed to MediaRepository (Sharp)
const thumbnailOptions = { ...image.thumbnail, colorspace, format: thumbnailFormat };
const previewOptions = { ...image.preview, colorspace, format: previewFormat };
// Run generation in parallel
const [thumbhash] = await Promise.all([
this.mediaRepository.generateThumbhash(data, baseOptions),
this.mediaRepository.generateThumbnail(data, thumbnailOptions, thumbnailFile.path),
this.mediaRepository.generateThumbnail(data, previewOptions, previewFile.path),
]);
// ... return results
}
The service extracts the source data, determines the appropriate colorspace using isSRGB, and executes three Sharp operations concurrently: thumbhash generation, thumbnail creation, and preview generation.
Video Transcoding Decision Logic
The video conversion handler probes streams and decides whether to transcode, remux, or skip:
private async handleVideoConversion({ id }: JobOf<JobName.AssetEncodeVideo>) {
const asset = await this.assetJobRepository.getForVideoConversion(id);
const { videoStreams, audioStreams, format } = await this.mediaRepository.probe(asset.originalPath);
const videoStream = this.getMainStream(videoStreams);
const audioStream = this.getMainStream(audioStreams);
const target = this.getTranscodeTarget(ffmpegConfig, videoStream, audioStream);
if (target === TranscodeTarget.None && !this.isRemuxRequired(ffmpegConfig, format)) {
return JobStatus.Skipped;
}
const command = BaseConfig.create(ffmpegConfig, this.videoInterfaces).getCommand(target, videoStream, audioStream);
await this.mediaRepository.transcode(asset.originalPath, outputPath, command);
// ... post-processing
}
This implementation avoids unnecessary re-encoding when source formats already match target specifications, falling back to remuxing only when container formats differ.
Database Synchronization
The file synchronization method ensures consistency between the database and storage layer:
private async syncFiles(oldFiles, newFiles) {
const toUpsert: UpsertFileOptions[] = [];
const pathsToDelete: string[] = [];
// Detect updates and deletions
for (const newFile of newFiles) {
const existing = oldFiles.find(f => f.type === newFile.type && f.isEdited === newFile.isEdited);
if (existing?.path !== newFile.path) {
toUpsert.push(newFile);
if (existing) pathsToDelete.push(existing.path);
}
}
// ... execute database and filesystem operations
}
This logic identifies changed paths for cleanup and prepares upsert operations for new file records, ensuring no orphaned files remain on disk.
Integration with the Immich Architecture
MediaService operates as the coordination layer between several critical subsystems according to the immich-app/immich source code architecture:
-
MediaRepository (
server/src/repositories/media.repository.ts): Provides the low-level interface to FFmpeg for video probing/transcoding and Sharp for image manipulation. The service delegates all binary operations to this repository, maintaining separation between business logic and media library bindings. -
StorageCore (
server/src/cores/storage.core.ts): Handles path generation and folder creation. WhenMediaServicegenerates new thumbnails or transcodes, it usesStorageCoreto determine where files should reside before writing them viaMediaRepository. -
Job Queue System: Methods like
handleGenerateThumbnailsandhandleVideoConversionare decorated with@OnJob, registering them as consumers for the background job processor. This allows the service to handle heavy processing asynchronously without blocking API requests. -
Configuration System (
server/src/config.ts): The service consumesSystemConfigto determine transcoding policies, target codecs, and hardware acceleration preferences, ensuring processing decisions respect administrator settings.
Summary
media.service.tsis the central orchestrator for all media processing in Immich, located atserver/src/services/media.service.ts- Thumbnail generation handles both images (via Sharp) and videos (via FFmpeg frame extraction), including RAW preview extraction and thumbhash creation
- Video transcoding intelligently decides between transcoding, remuxing, or skipping based on stream analysis and configuration policies
- Hardware acceleration is detected at startup through
onBootstrapand passed to FFmpeg command builders for GPU-accelerated encoding - Database synchronization via
syncFilesmaintains consistency betweenAssetFilerecords and the actual files on disk - The service integrates with
MediaRepository,StorageCore, and the job queue system to process assets asynchronously at scale
Frequently Asked Questions
What triggers the media processing workflows in Immich?
The MediaService methods are triggered by the background job queue system. When users upload assets, the API enqueues jobs like AssetGenerateThumbnails or AssetEncodeVideo, which the service picks up via @OnJob decorators on methods such as handleGenerateThumbnails and handleVideoConversion.
How does Immich handle RAW image formats?
According to the source code in media.service.ts, the service first attempts to extract embedded JPEG previews from RAW files when shouldUseExtractedImage determines it would be more efficient than processing the raw sensor data. If no preview exists or if the configuration demands full processing, it falls back to decoding the raw data directly through Sharp.
Can MediaService utilize GPU acceleration for video transcoding?
Yes. During the onBootstrap lifecycle, the service detects available hardware through getDevices and hasMaliOpenCL, storing capabilities in videoInterfaces. These interfaces are passed to BaseConfig.create() when building FFmpeg commands in handleVideoConversion, enabling VA-API, NVENC, or other acceleration methods when configured.
What happens to old thumbnail files when images are edited?
The syncFiles method compares the previous AssetFile records against newly generated files. When paths differ—which occurs during edit regeneration—it adds the old paths to pathsToDelete and upserts new records. This ensures the database stays synchronized with the storage layer and prevents accumulation of orphaned thumbnail files.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →