diff --git a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift index 243eb2a5d..6064036a5 100644 --- a/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift +++ b/kDrive/UI/Controller/Files/Preview/PreviewViewController.swift @@ -16,6 +16,7 @@ along with this program. If not, see . */ +import AVFoundation import FloatingPanel import InfomaniakCore import InfomaniakDI @@ -451,6 +452,8 @@ final class PreviewViewController: UIViewController, PreviewContentCellDelegate, let file = previewFiles[index] if ConvertedType.documentTypes.contains(file.convertedType) { handleOfficePreviewError(error, previewIndex: index) + } else if file.convertedType == .audio { + handleAudioPreviewError(error, previewIndex: index) } // We have to delay reload because errorWhilePreviewing can be called when the collectionView requests a new cell in @@ -460,6 +463,19 @@ final class PreviewViewController: UIViewController, PreviewContentCellDelegate, } } + func handleAudioPreviewError(_ error: Error, previewIndex: Int) { + let file = previewFiles[previewIndex] + + guard let avError = error as? AVError, + avError.code == .fileFormatNotRecognized else { + return + } + + previewErrors[file.id] = PreviewError(fileId: file.id, downloadError: nil) + guard file.isLocalVersionOlderThanRemote else { return } + downloadFile(at: IndexPath(item: previewIndex, section: 0)) + } + func handleOfficePreviewError(_ error: Error, previewIndex: Int) { let file = previewFiles[previewIndex] @@ -526,6 +542,10 @@ final class PreviewViewController: UIViewController, PreviewContentCellDelegate, return } + downloadFile(at: indexPath) + } + + private func downloadFile(at indexPath: IndexPath) { DownloadQueue.instance.temporaryDownload( file: currentFile, userId: accountManager.currentUserId, @@ -558,6 +578,7 @@ final class PreviewViewController: UIViewController, PreviewContentCellDelegate, } else { (collectionView.cellForItem(at: indexPath) as? DownloadingPreviewCollectionViewCell)? .previewDownloadTask?.cancel() + previewErrors[currentFile.id] = nil collectionView.endEditing(true) collectionView.reloadItems(at: [indexPath]) updateNavigationBar() diff --git a/kDrive/UI/View/Files/Preview/AudioCollectionViewCell.swift b/kDrive/UI/View/Files/Preview/AudioCollectionViewCell.swift index 7522a2c24..a357afef4 100644 --- a/kDrive/UI/View/Files/Preview/AudioCollectionViewCell.swift +++ b/kDrive/UI/View/Files/Preview/AudioCollectionViewCell.swift @@ -20,6 +20,7 @@ import Combine import InfomaniakCore import kDriveCore import kDriveResources +import Kingfisher import MediaPlayer import UIKit @@ -39,6 +40,7 @@ final class AudioCollectionViewCell: PreviewCollectionViewCell { public lazy var singleTrackPlayer = SingleTrackPlayer(driveFileManager: driveFileManager) private var cancellables = Set() + private var thumbnailDownloadTask: Kingfisher.DownloadTask? override func awakeFromNib() { super.awakeFromNib() @@ -77,6 +79,7 @@ final class AudioCollectionViewCell: PreviewCollectionViewCell { override func prepareForReuse() { super.prepareForReuse() + thumbnailDownloadTask?.cancel() setControls(enabled: false) singleTrackPlayer.reset() songTitleLabel.text = "" @@ -91,12 +94,12 @@ final class AudioCollectionViewCell: PreviewCollectionViewCell { Task { @MainActor in await singleTrackPlayer.setup(with: frozenFile) setControls(enabled: true) - setupObservation() + setupObservationFor(frozenFile: frozenFile) } } /// Setup data flow - @MainActor func setupObservation() { + @MainActor func setupObservationFor(frozenFile: File) { cancellables.forEach { $0.cancel() } cancellables.removeAll() @@ -153,9 +156,15 @@ final class AudioCollectionViewCell: PreviewCollectionViewCell { .onCurrentTrackMetadata .receive(on: DispatchQueue.main) .sink { metadata in - self.artworkImageView.image = metadata.artwork ?? KDriveResourcesAsset.music.image - self.artistNameLabel.text = metadata.artist - self.songTitleLabel.text = metadata.title + self.handleMetadata(metadata, file: frozenFile) + } + .store(in: &cancellables) + + singleTrackPlayer + .onItemError + .receive(on: DispatchQueue.main) + .sink { error in + self.previewDelegate?.errorWhilePreviewing(fileId: frozenFile.id, error: error) } .store(in: &cancellables) } @@ -167,6 +176,20 @@ final class AudioCollectionViewCell: PreviewCollectionViewCell { iconHeightConstraint.constant = isPortrait ? 254 : 120 } + func handleMetadata(_ metadata: MediaMetadata, file: File) { + if let artwork = metadata.artwork { + artworkImageView.image = artwork + } else { + artworkImageView.image = KDriveResourcesAsset.music.image + thumbnailDownloadTask = file.getThumbnail { thumbnail, isThumbnailAvailable in + guard isThumbnailAvailable else { return } + self.artworkImageView.image = thumbnail + } + } + artistNameLabel.text = metadata.artist + songTitleLabel.text = metadata.title + } + @objc func rotated() { setUpPlayButtons() } diff --git a/kDriveCore/AudioPlayer/SingleTrackPlayer.swift b/kDriveCore/AudioPlayer/SingleTrackPlayer.swift index 3af228a57..fc69d2fe6 100644 --- a/kDriveCore/AudioPlayer/SingleTrackPlayer.swift +++ b/kDriveCore/AudioPlayer/SingleTrackPlayer.swift @@ -45,6 +45,7 @@ public final class SingleTrackPlayer: Pausable { private var timeObserver: Any? private var rateObserver: NSKeyValueObservation? private var statusObserver: NSKeyValueObservation? + private var currentItemStatusObserver: NSKeyValueObservation? private var isInterrupted = false // MARK: Data flow @@ -56,6 +57,7 @@ public final class SingleTrackPlayer: Pausable { public let onPositionChange = PassthroughSubject() public let onPositionMaximumChange = PassthroughSubject() public let onCurrentTrackMetadata = PassthroughSubject() + public let onItemError = PassthroughSubject() var player: AVPlayer? @@ -113,6 +115,10 @@ public final class SingleTrackPlayer: Pausable { self.player = AVPlayer(playerItem: AVPlayerItem(asset: asset)) await self.setMetaData(from: asset.commonMetadata, playableFileName: playableFile.name) self.setUpObservers() + + self.currentItemStatusObserver = self.player?.observe(\.currentItem?.status) { _, _ in + self.handleItemStatusChange() + } } } } else { @@ -223,6 +229,12 @@ public final class SingleTrackPlayer: Pausable { } } + private func handleItemStatusChange() { + guard let error = player?.currentItem?.error else { return } + + onItemError.send(error) + } + // MARK: - Observation private func setUpObservers() { @@ -271,6 +283,9 @@ public final class SingleTrackPlayer: Pausable { statusObserver?.invalidate() statusObserver = nil + currentItemStatusObserver?.invalidate() + currentItemStatusObserver = nil + removeAllRemoteControlEvents() }