// // MediaPreviewVideoViewModel.swift // Mastodon // // Created by MainasuK on 2022-2-9. // import os.log import UIKit import AVKit import Combine import AlamofireImage final class MediaPreviewVideoViewModel { let logger = Logger(subsystem: "MediaPreviewVideoViewModel", category: "ViewModel") var disposeBag = Set() // input let context: AppContext let item: Item // output public private(set) var player: AVPlayer? private var playerLooper: AVPlayerLooper? @Published var playbackState = PlaybackState.unknown init(context: AppContext, item: Item) { self.context = context self.item = item // end init switch item { case .video(let mediaContext): guard let assertURL = mediaContext.assetURL else { return } let playerItem = AVPlayerItem(url: assertURL) let _player = AVPlayer(playerItem: playerItem) self.player = _player case .gif(let mediaContext): guard let assertURL = mediaContext.assetURL else { return } let playerItem = AVPlayerItem(url: assertURL) let _player = AVQueuePlayer(playerItem: playerItem) _player.isMuted = true self.player = _player if let templateItem = _player.items().first { let _playerLooper = AVPlayerLooper(player: _player, templateItem: templateItem) self.playerLooper = _playerLooper } } guard let player = player else { assertionFailure() return } // setup player state observer $playbackState .receive(on: DispatchQueue.main) .sink { [weak self] status in guard let self = self else { return } self.logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): player state: \(status.description)") switch status { case .unknown, .buffering, .readyToPlay: break case .playing: try? AVAudioSession.sharedInstance().setCategory(.playback) try? AVAudioSession.sharedInstance().setActive(true) case .paused, .stopped, .failed: try? AVAudioSession.sharedInstance().setCategory(.ambient) // set to ambient to allow mixed (needed for GIFV) try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) } } .store(in: &disposeBag) player.publisher(for: \.status, options: [.initial, .new]) .sink(receiveValue: { [weak self] status in guard let self = self else { return } switch status { case .failed: self.playbackState = .failed case .readyToPlay: self.playbackState = .readyToPlay case .unknown: self.playbackState = .unknown @unknown default: assertionFailure() } }) .store(in: &disposeBag) NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime, object: nil) .sink { [weak self] notification in guard let self = self else { return } guard let playerItem = notification.object as? AVPlayerItem, let urlAsset = playerItem.asset as? AVURLAsset else { return } print(urlAsset.url) guard urlAsset.url == item.assetURL else { return } self.playbackState = .stopped } .store(in: &disposeBag) } } extension MediaPreviewVideoViewModel { enum Item { case video(RemoteVideoContext) case gif(RemoteGIFContext) var previewURL: URL? { switch self { case .video(let mediaContext): return mediaContext.previewURL case .gif(let mediaContext): return mediaContext.previewURL } } var assetURL: URL? { switch self { case .video(let mediaContext): return mediaContext.assetURL case .gif(let mediaContext): return mediaContext.assetURL } } } struct RemoteVideoContext { let assetURL: URL? let previewURL: URL? // let thumbnail: UIImage? } struct RemoteGIFContext { let assetURL: URL? let previewURL: URL? } }