From 7c99a333dade991a972037c753d2662c0117fe29 Mon Sep 17 00:00:00 2001 From: ctrlaltking Date: Sun, 21 Jun 2026 23:21:08 -0700 Subject: [PATCH] fix(preview): flag unplayable media when previewing a standalone asset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit previewAsset() created AVPlayerItem(url:) directly with no error handling, so a corrupt or unsupported source file just left the preview blank/frozen with no feedback. The timeline composition path already detects this (offlineMediaRefs/unprocessableMediaRefs via CompositionBuilder) and surfaces a "Couldn't Prepare Media" overlay — this path never ran for assets previewed before being placed on the timeline. Probe the asset's tracks after swapping in the player item and mark it unprocessable on failure, reusing the existing offline/unprocessable UI instead of leaving a silent blank player. --- Sources/PalmierPro/Preview/VideoEngine.swift | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Sources/PalmierPro/Preview/VideoEngine.swift b/Sources/PalmierPro/Preview/VideoEngine.swift index 39f5022de..a0c18de55 100644 --- a/Sources/PalmierPro/Preview/VideoEngine.swift +++ b/Sources/PalmierPro/Preview/VideoEngine.swift @@ -101,6 +101,32 @@ final class VideoEngine { return } replacePlayerItem(AVPlayerItem(url: asset.url), reason: "previewAsset") + verifyAssetPlayable(asset) + } + + /// `AVPlayerItem(url:)` fails silently — a corrupt or unsupported file just + /// leaves the preview blank with no error. Probe the tracks so we can flag + /// it through the same offline/unprocessable UI the timeline path uses. + private func verifyAssetPlayable(_ asset: MediaAsset) { + let mediaType: AVMediaType = asset.type == .audio ? .audio : .video + let url = asset.url + let assetId = asset.id + Task { @MainActor [weak self] in + let hasTrack: Bool + do { + hasTrack = try await AVURLAsset(url: url).loadTracks(withMediaType: mediaType).first != nil + } catch { + hasTrack = false + } + guard let self, !Task.isCancelled, let editor = self.editor else { return } + guard case .mediaAsset(let activeId, _, _) = editor.activePreviewTab, activeId == assetId else { return } + if hasTrack { + editor.unprocessableMediaRefs.remove(assetId) + } else { + Log.preview.error("previewAsset: media unplayable assetId=\(assetId.prefix(8))") + editor.unprocessableMediaRefs.insert(assetId) + } + } } func activateTab(_ tab: PreviewTab) {