immich-app/immich · error · BadRequestException

Real-time transcoding is not enabled

Error message

Real-time transcoding is not enabled

What it means

getMainPlaylist serves HLS playlists for real-time transcoded playback. If the server config has ffmpeg.realtime.enabled=false, streaming playlists are not produced, so the endpoint rejects with 400 BadRequestException before touching the asset.

Solutions

  1. Enable real-time transcoding: set transcoding policy/realtime in the admin Transcoding settings (or ffmpeg.realtime.enabled=true in config) and restart.
  2. Use normal progressive playback endpoints instead of the HLS playlist routes.
  3. Upgrade the clients to a version that only requests HLS when realtime is enabled.

Example fix

// before
ffmpeg: { realtime: { enabled: false }, ... }
// after
ffmpeg: { realtime: { enabled: true, crf: 23 }, ... }
Defensive patterns

Strategy: try-catch

Validate before calling

const config = await api.getConfig();
if (!config.ffmpeg.realtime.enabled) console.warn('HLS realtime endpoints unavailable; use progressive playback');

Try / catch

try {
  const playlist = await api.getMainPlaylist(assetId);
} catch (e) {
  if (/Real-time transcoding is not enabled/.test(e.message)) {
    fallbackToProgressivePlayback(assetId);
  } else throw e;
}

Prevention

When it happens

Trigger: Requesting GET /assets/:id/video/master.m3u8 (or equivalent playlist route) while the transcoding settings use the default non-realtime (throttled/after-upload) transcode mode.

Common situations: Mobile/web client attempting HLS streaming playback when admin left transcoding policy at default; config reset after restore; custom settings disabling realtime.

Related errors


AI-assisted analysis of immich-app/immich@e55ac299a4 (2026-09-15). Data as JSON: /api/errors/78c1c8bca63844ea. Report an issue: GitHub.

Appendix: source

Thrown at server/src/services/hls.service.ts:51

    }
  }

  @OnEvent({ name: 'HlsSessionEnd', server: true, workers: [ImmichWorker.Api] })
  onSessionEnd({ sessionId }: ArgOf<'HlsSessionEnd'>) {
    this.sessions.delete(sessionId);
    this.pendingSegments.rejectByPrefix(`${sessionId}:`, 'Session ended');
  }

  @OnEvent({ name: 'HlsSegmentResult', server: true, workers: [ImmichWorker.Api] })
  onSegmentResult(event: ArgOf<'HlsSegmentResult'>) {
    this.pendingSegments.complete(this.getSegmentKey(event), event);
  }

  async getMainPlaylist(auth: AuthDto, assetId: string) {
    await this.requireAccess({ auth, permission: Permission.AssetView, ids: [assetId] });
    const { ffmpeg } = await this.getConfig({ withCache: true });
    if (!ffmpeg.realtime.enabled) {
      throw new BadRequestException('Real-time transcoding is not enabled');
    }

    const asset = await this.videoStreamRepository.getForMainPlaylist(assetId);
    if (!asset) {
      throw new NotFoundException('Asset metadata is not yet ready for streaming');
    }

    // Sharing the sessionId allows only one microservices worker to successfully insert to the session table.
    // The microservices worker that creates a session owns the transcoding lifecycle for it.
    const sessionId = this.cryptoRepository.randomUUID();
    this.websocketRepository.serverSend('HlsSessionRequest', { sessionId, assetId, ownerId: auth.user.id });
    await this.pendingSessions.wait(sessionId);
    this.trackSession(sessionId);

    return this.generateMainPlaylist(sessionId, ffmpeg, asset);
  }

  async getMediaPlaylist(auth: AuthDto, assetId: string, sessionId: string, variantIndex: number, position?: number) {

View on GitHub (pinned to e55ac299a4)