mastodon-ios/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift

154 lines
5.0 KiB
Swift

//
// VideoPlayerViewModel.swift
// Mastodon
//
// Created by xiaojian sun on 2021/3/10.
//
import AVKit
import Combine
import CoreDataStack
import os.log
import UIKit
final class VideoPlayerViewModel {
var disposeBag = Set<AnyCancellable>()
static let appWillPlayVideoNotification = NSNotification.Name(rawValue: "org.joinmastodon.app.video-playback-service.appWillPlayVideo")
// input
let previewImageURL: URL?
let videoURL: URL
let videoSize: CGSize
let videoKind: Kind
var isTransitioning = false
var isFullScreenPresentationing = false
var isPlayingWhenEndDisplaying = false
// prevent player state flick when tableView reload
private typealias Play = Bool
private let debouncePlayingState = PassthroughSubject<Play, Never>()
private var updateDate = Date()
// output
let player: AVPlayer
private(set) var looper: AVPlayerLooper? // works with AVQueuePlayer (iOS 10+)
private var timeControlStatusObservation: NSKeyValueObservation?
let timeControlStatus = CurrentValueSubject<AVPlayer.TimeControlStatus, Never>(.paused)
init(previewImageURL: URL?, videoURL: URL, videoSize: CGSize, videoKind: VideoPlayerViewModel.Kind) {
self.previewImageURL = previewImageURL
self.videoURL = videoURL
self.videoSize = videoSize
self.videoKind = videoKind
let playerItem = AVPlayerItem(url: videoURL)
let player = videoKind == .gif ? AVQueuePlayer(playerItem: playerItem) : AVPlayer(playerItem: playerItem)
player.isMuted = true
self.player = player
if videoKind == .gif {
setupLooper()
}
timeControlStatusObservation = player.observe(\.timeControlStatus, options: [.initial, .new]) { [weak self] player, _ in
guard let self = self else { return }
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: player state: %s", (#file as NSString).lastPathComponent, #line, #function, player.timeControlStatus.debugDescription)
self.timeControlStatus.value = player.timeControlStatus
}
// update audio session category for user interactive event stream
timeControlStatus
.sink { [weak self] timeControlStatus in
guard let _ = self else { return }
guard timeControlStatus == .playing else { return }
NotificationCenter.default.post(name: VideoPlayerViewModel.appWillPlayVideoNotification, object: nil)
switch videoKind {
case .gif:
break
case .video:
try? AVAudioSession.sharedInstance().setCategory(.soloAmbient, mode: .default)
}
}
.store(in: &disposeBag)
debouncePlayingState
.debounce(for: 0.3, scheduler: DispatchQueue.main)
.sink { [weak self] isPlay in
guard let self = self else { return }
isPlay ? self.play() : self.pause()
}
.store(in: &disposeBag)
}
deinit {
timeControlStatusObservation = nil
}
}
extension VideoPlayerViewModel {
enum Kind {
case gif
case video
}
}
extension VideoPlayerViewModel {
func setupLooper() {
guard looper == nil, let queuePlayer = player as? AVQueuePlayer else { return }
guard let templateItem = queuePlayer.items().first else { return }
looper = AVPlayerLooper(player: queuePlayer, templateItem: templateItem)
}
func play() {
switch videoKind {
case .gif:
break
case .video:
try? AVAudioSession.sharedInstance().setCategory(.soloAmbient, mode: .default)
}
player.play()
updateDate = Date()
}
func pause() {
player.pause()
updateDate = Date()
}
func willDisplay() {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: url: %s", (#file as NSString).lastPathComponent, #line, #function, videoURL.debugDescription)
switch videoKind {
case .gif:
play() // always auto play GIF
case .video:
guard isPlayingWhenEndDisplaying else { return }
// mute before resume
if updateDate.timeIntervalSinceNow < -3 {
player.isMuted = true
}
debouncePlayingState.send(true)
}
updateDate = Date()
}
func didEndDisplaying() {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: url: %s", (#file as NSString).lastPathComponent, #line, #function, videoURL.debugDescription)
isPlayingWhenEndDisplaying = timeControlStatus.value != .paused
switch videoKind {
case .gif:
pause() // always pause GIF immediately
case .video:
debouncePlayingState.send(false)
}
updateDate = Date()
}
}