Skip to content

Conversation

@3rob3
Copy link
Collaborator

@3rob3 3rob3 commented Feb 2, 2026

Fix Safari/iOS video playback by buffering videos to seekable streams

Safari requires seekable streams for video range requests. Previously, non-seekable network streams from Immich caused playback failures(error code 4 - MEDIA_ERR_SRC_NOT_SUPPORTED). Now videos are either cached to disk (when DownloadImages=true) or buffered to memory to provide seekability.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed missing Content-Type for video assets; now reliably defaults to video/mp4 when not provided.
  • Performance

    • Added configurable video caching: videos are stored on disk and reused until renewal, with expired files auto-refreshed.
    • When disk caching is disabled, videos are served from memory to avoid persistent storage.

@3rob3 3rob3 requested a review from JW-CH February 2, 2026 19:11
@3rob3 3rob3 added the enhancement New feature or request label Feb 2, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 2, 2026

📝 Walkthrough

Walkthrough

GetVideoAsset in PooledImmichFrameLogic.cs adds disk-based caching with renewal checks and a DownloadImages toggle. When enabled, files are stored as id.mp4, reused if fresh or re-fetched when expired; when disabled, videos are returned as in-memory streams. Missing Content-Type defaults to video/mp4.

Changes

Cohort / File(s) Summary
Video Asset Caching Logic
ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs
Reworked GetVideoAsset to support disk caching (id.mp4) with directory creation, expiration/renewal checks, and deletion+refetch for stale files when DownloadImages is true. When false, response bodies are returned as MemoryStream. Ensures Content-Type defaults to video/mp4 if absent.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Logic as PooledImmichFrameLogic
  participant API as Immich API
  participant Disk as Filesystem

  Caller->>Logic: Request GetVideoAsset(id)
  Logic->>Disk: compute path "id.mp4" + check DownloadImages
  alt DownloadImages = true
    Logic->>Disk: ensure directory exists
    Disk-->>Logic: dir ready
    Logic->>Disk: file exists & check age
    alt file fresh
      Disk-->>Logic: return file stream
      Logic-->>Caller: stream (disk-backed)
    else expired or missing
      Logic->>API: fetch video bytes
      API-->>Logic: response (body, Content-Type?)
      Logic->>Disk: write bytes to id.mp4
      Disk-->>Logic: file ready
      Logic-->>Caller: stream (disk-backed)
    end
  else DownloadImages = false
    Logic->>API: fetch video bytes
    API-->>Logic: response (body, Content-Type?)
    Logic->>Logic: default Content-Type -> "video/mp4" if missing
    Logic-->>Caller: MemoryStream with bytes
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I fetched a clip, I stored a byte,
On disk it naps or takes to flight,
Freshly renewed or wrapped in RAM,
A hopping cache — quick as I am! 🎬✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: implementing seekable streams for video playback, addressing the Safari/iOS compatibility issue mentioned in the PR objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev.3rob3.SeekableStream

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs`:
- Around line 165-206: In GetVideoAsset (PooledImmichFrameLogic.GetVideoAsset)
move the local cache check (using _downloadLocation, fileName and
_generalSettings.RenewImagesDuration) to run before calling
_immichApi.PlayAssetVideoAsync so we short‑circuit on cache hits; then call the
API only if cache miss or expired, wrap the returned videoResponse (and its
Stream) in a using block to ensure disposal, and on API failure fall back to
returning the cached file if present; mirror the flow used in GetImageAsset to
ensure no leaked streams and proper cache-first behavior.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs`:
- Around line 176-180: The cache-hit branch in PooledImmichFrameLogic is
returning a hardcoded content-type "video/mp4" for cached files (see filePath,
fileName and the caller that expects a tuple of (fileName, contentType,
Stream)), which can mismatch original responses; fix by persisting the original
content-type when first saving the file (e.g., write a companion metadata file
next to filePath like filePath + ".meta" or embed a suffix) and on cache hit
read that metadata and return the stored content-type instead of the hardcoded
"video/mp4"; also add a safe fallback to "video/mp4" if the metadata is missing
or unreadable and ensure any write/read logic is added where the file is created
and where the cached file is returned.
🧹 Nitpick comments (1)
ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs (1)

190-192: Consider extracting Content-Type parsing to reduce duplication.

The Content-Type extraction logic is duplicated across both branches. A small helper could improve maintainability.

♻️ Optional refactor
+    private static string GetContentType(FileResponse response, string defaultType)
+    {
+        return response.Headers.ContainsKey("Content-Type")
+            ? response.Headers["Content-Type"].FirstOrDefault() ?? defaultType
+            : defaultType;
+    }
+
     private async Task<(string fileName, string ContentType, Stream fileStream)> GetVideoAsset(Guid id)
     {
         // ... cache check ...

         using var videoResponse = await _immichApi.PlayAssetVideoAsync(id, string.Empty);

         if (videoResponse == null)
             throw new AssetNotFoundException($"Video asset {id} was not found!");

-        var contentType = videoResponse.Headers.ContainsKey("Content-Type")
-            ? videoResponse.Headers["Content-Type"].FirstOrDefault() ?? "video/mp4"
-            : "video/mp4";
+        var contentType = GetContentType(videoResponse, "video/mp4");

Also applies to: 208-210

@3rob3
Copy link
Collaborator Author

3rob3 commented Feb 2, 2026

Workflow is not running, I don't know why.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants