{"record":{"id":"75d53b95babe924f","repo":"immich-app/immich","slug":"asset-metadata-is-not-yet-ready-for-streaming","errorCode":null,"errorMessage":"Asset metadata is not yet ready for streaming","messagePattern":"Asset metadata is not yet ready for streaming","errorType":"http","errorClass":"NotFoundException","httpStatus":404,"severity":"warning","filePath":"server/src/services/hls.service.ts","lineNumber":56,"sourceCode":"    this.sessions.delete(sessionId);\n    this.pendingSegments.rejectByPrefix(`${sessionId}:`, 'Session ended');\n  }\n\n  @OnEvent({ name: 'HlsSegmentResult', server: true, workers: [ImmichWorker.Api] })\n  onSegmentResult(event: ArgOf<'HlsSegmentResult'>) {\n    this.pendingSegments.complete(this.getSegmentKey(event), event);\n  }\n\n  async getMainPlaylist(auth: AuthDto, assetId: string) {\n    await this.requireAccess({ auth, permission: Permission.AssetView, ids: [assetId] });\n    const { ffmpeg } = await this.getConfig({ withCache: true });\n    if (!ffmpeg.realtime.enabled) {\n      throw new BadRequestException('Real-time transcoding is not enabled');\n    }\n\n    const asset = await this.videoStreamRepository.getForMainPlaylist(assetId);\n    if (!asset) {\n      throw new NotFoundException('Asset metadata is not yet ready for streaming');\n    }\n\n    // Sharing the sessionId allows only one microservices worker to successfully insert to the session table.\n    // The microservices worker that creates a session owns the transcoding lifecycle for it.\n    const sessionId = this.cryptoRepository.randomUUID();\n    this.websocketRepository.serverSend('HlsSessionRequest', { sessionId, assetId, ownerId: auth.user.id });\n    await this.pendingSessions.wait(sessionId);\n    this.trackSession(sessionId);\n\n    return this.generateMainPlaylist(sessionId, ffmpeg, asset);\n  }\n\n  async getMediaPlaylist(auth: AuthDto, assetId: string, sessionId: string, variantIndex: number, position?: number) {\n    await this.requireAccess({ auth, permission: Permission.AssetView, ids: [assetId] });\n\n    const asset = await this.videoStreamRepository.getForMediaPlaylist(assetId, sessionId);\n    if (!asset) {\n      throw new NotFoundException('Asset not found or metadata not yet ready for streaming');","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/immich-app/immich/blob/199723261c6ffa897fec8ccdaea6359e39c37cc3/server/src/services/hls.service.ts#L38-L74","documentation":"Before serving a video's main HLS playlist, Immich fetches the asset's prepared video metadata (codec, streams, packets) via videoStreamRepository.getForMainPlaylist. If that returns null - typically because metadata extraction / video probe has not completed for a freshly uploaded asset - it throws 404 NotFoundException. The asset exists and is accessible, but the data needed to plan transcoding variants is not ready yet.","triggerScenarios":"User opens a just-uploaded video in the viewer before the MetadataExtraction / video probe job has finished writing the stream info getForMainPlaylist needs. Asset row exists, but its videoStream/segments data is absent.","commonSituations":"Background jobs backlog; microservices worker not running or crashed; very large video still being probed; metadata job failed silently for that asset.","solutions":["Wait a few seconds and retry - the playlist becomes available once the metadata/probe job completes.","Check the Jobs dashboard for a stuck or failed MetadataExtraction / video probe queue and re-run it for the asset.","Confirm the microservices container/worker is running (without it the job never processes).","If persistent, trigger metadata re-extraction for the asset from the admin maintenance menu."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// check asset is ready before opening the player\nconst asset = await api.assetApi.get(assetId);\nif (!asset?.encodedVideoPath && asset?.type === 'VIDEO') {\n  // metadata may still be processing - wait or show a 'preparing' state\n  await waitForJob('MetadataExtraction', assetId);\n}","typeGuard":"const isAssetReadyForStreaming = (a: { encodedVideoPath?: string; type: string }) =>\n  a.type === 'VIDEO' && !!a.encodedVideoPath;","tryCatchPattern":"for (let attempt = 0; attempt < 5; attempt++) {\n  try {\n    return await api.hlsApi.getMainPlaylist(auth, assetId);\n  } catch (e) {\n    if (e.status === 404 && /not yet ready/.test(e.message)) {\n      await delay(2000 * (attempt + 1));\n      continue;\n    }\n    throw e;\n  }\n}\nthrow new Error('Asset metadata never became ready');","preventionTips":["Show a 'Preparing video' state in the UI for freshly uploaded videos.","Monitor the MetadataExtraction job queue for backlogs.","Ensure at least one microservices worker is running."],"tags":["video","hls","metadata","job","async","not-found","immich","nestjs"],"backgroundTag":null,"analyzedSha":"199723261c6ffa897fec8ccdaea6359e39c37cc3","analyzedAt":"2026-08-12T04:54:27.085Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}