From aa02e0143d4fb2b8693dfee333716944be0b4145 Mon Sep 17 00:00:00 2001 From: Rob Rogers Date: Mon, 2 Feb 2026 14:08:09 -0500 Subject: [PATCH 1/2] Fix video playback on Safari/iOS by ensuring seekable streams. Implement caching (same as images) --- .../Logic/PooledImmichFrameLogic.cs | 47 ++++++++++++++----- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs b/ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs index a13eead4..2178c4c2 100644 --- a/ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs +++ b/ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs @@ -163,26 +163,49 @@ public Task> GetAssets() private async Task<(string fileName, string ContentType, Stream fileStream)> GetVideoAsset(Guid id) { var videoResponse = await _immichApi.PlayAssetVideoAsync(id, string.Empty); - if (videoResponse == null) throw new AssetNotFoundException($"Video asset {id} was not found!"); - var contentType = ""; - if (videoResponse.Headers.ContainsKey("Content-Type")) - { - contentType = videoResponse.Headers["Content-Type"].FirstOrDefault() ?? ""; - } + var contentType = videoResponse.Headers.ContainsKey("Content-Type") + ? videoResponse.Headers["Content-Type"].FirstOrDefault() ?? "video/mp4" + : "video/mp4"; - if (string.IsNullOrWhiteSpace(contentType)) + var fileName = $"{id}.mp4"; + + if (_generalSettings.DownloadImages) { - contentType = "video/mp4"; - } + if (!Directory.Exists(_downloadLocation)) + { + Directory.CreateDirectory(_downloadLocation); + } - var fileName = $"{id}.mp4"; + var filePath = Path.Combine(_downloadLocation, fileName); - return (fileName, contentType, videoResponse.Stream); - } + if (File.Exists(filePath)) + { + if (_generalSettings.RenewImagesDuration > (DateTime.UtcNow - File.GetCreationTimeUtc(filePath)).Days) + { + return (fileName, contentType, File.OpenRead(filePath)); + } + File.Delete(filePath); + } + using (var fileStream = File.Create(filePath)) + { + await videoResponse.Stream.CopyToAsync(fileStream); + } + + return (fileName, contentType, File.OpenRead(filePath)); + } + else + { + var memoryStream = new MemoryStream(); + await videoResponse.Stream.CopyToAsync(memoryStream); + memoryStream.Position = 0; + + return (fileName, contentType, memoryStream); + } + } public Task SendWebhookNotification(IWebhookNotification notification) => WebhookHelper.SendWebhookNotification(notification, _generalSettings.Webhook); From 4ed4fb33d68b9d8262d70be638b1314ac1d76d44 Mon Sep 17 00:00:00 2001 From: Rob Rogers Date: Mon, 2 Feb 2026 14:43:23 -0500 Subject: [PATCH 2/2] coderabbit suggestion --- .../Logic/PooledImmichFrameLogic.cs | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs b/ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs index 2178c4c2..d3dbb705 100644 --- a/ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs +++ b/ImmichFrame.Core/Logic/PooledImmichFrameLogic.cs @@ -162,14 +162,6 @@ public Task> GetAssets() private async Task<(string fileName, string ContentType, Stream fileStream)> GetVideoAsset(Guid id) { - 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 fileName = $"{id}.mp4"; if (_generalSettings.DownloadImages) @@ -185,11 +177,20 @@ public Task> GetAssets() { if (_generalSettings.RenewImagesDuration > (DateTime.UtcNow - File.GetCreationTimeUtc(filePath)).Days) { - return (fileName, contentType, File.OpenRead(filePath)); + return (fileName, "video/mp4", File.OpenRead(filePath)); } File.Delete(filePath); } + 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"; + using (var fileStream = File.Create(filePath)) { await videoResponse.Stream.CopyToAsync(fileStream); @@ -199,6 +200,15 @@ public Task> GetAssets() } else { + 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 memoryStream = new MemoryStream(); await videoResponse.Stream.CopyToAsync(memoryStream); memoryStream.Position = 0;