# Understanding media.service.ts: Immich's Central Hub for Media Processing and Video Transcoding

> Discover media.service.ts, Immich's central hub for media processing. It handles thumbnail generation, FFmpeg video transcoding, and hardware acceleration for your media library.

- Repository: [Immich/immich](https://github.com/immich-app/immich)
- Tags: deep-dive
- Published: 2026-02-27

---

**[`media.service.ts`](https://github.com/immich-app/immich/blob/main/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`](https://github.com/immich-app/immich/blob/main/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`: Returns `TranscodeTarget.Video`, `TranscodeTarget.Audio`, `TranscodeTarget.Both`, or `TranscodeTarget.None` based on codec compatibility
- `isVideoTranscodeRequired` and `isAudioTranscodeRequired`: Check if streams meet the configured output requirements
- `isRemuxRequired`: 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`](https://github.com/immich-app/immich/blob/main/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:

```typescript
// 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:

```typescript
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:

```typescript
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:

```typescript
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`](https://github.com/immich-app/immich/blob/main/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`](https://github.com/immich-app/immich/blob/main/server/src/cores/storage.core.ts)): Handles path generation and folder creation. When `MediaService` generates new thumbnails or transcodes, it uses `StorageCore` to determine where files should reside before writing them via `MediaRepository`.

- **Job Queue System**: Methods like `handleGenerateThumbnails` and `handleVideoConversion` are 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`](https://github.com/immich-app/immich/blob/main/server/src/config.ts)): The service consumes `SystemConfig` to determine transcoding policies, target codecs, and hardware acceleration preferences, ensuring processing decisions respect administrator settings.

## Summary

- **[`media.service.ts`](https://github.com/immich-app/immich/blob/main/media.service.ts)** is the central orchestrator for all media processing in Immich, located at [`server/src/services/media.service.ts`](https://github.com/immich-app/immich/blob/main/server/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 `onBootstrap` and passed to FFmpeg command builders for GPU-accelerated encoding
- **Database synchronization** via `syncFiles` maintains consistency between `AssetFile` records 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`](https://github.com/immich-app/immich/blob/main/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.