2021-03-10 07:36:28 +01:00
//
// V i d e o P l a y e r V i e w M o d e l . s w i f t
// M a s t o d o n
//
// C r e a t e d b y x i a o j i a n s u n o n 2 0 2 1 / 3 / 1 0 .
//
import AVKit
import Combine
2021-03-10 10:14:12 +01:00
import CoreDataStack
import os . log
import UIKit
2021-03-10 07:36:28 +01:00
final class VideoPlayerViewModel {
var disposeBag = Set < AnyCancellable > ( )
2021-03-15 08:08:58 +01:00
static let appWillPlayVideoNotification = NSNotification . Name ( rawValue : " org.joinmastodon.Mastodon.video-playback-service.appWillPlayVideo " )
2021-03-10 07:36:28 +01:00
// i n p u t
let previewImageURL : URL ?
let videoURL : URL
let videoSize : CGSize
let videoKind : Kind
var isTransitioning = false
var isFullScreenPresentationing = false
var isPlayingWhenEndDisplaying = false
// p r e v e n t p l a y e r s t a t e f l i c k w h e n t a b l e V i e w r e l o a d
private typealias Play = Bool
private let debouncePlayingState = PassthroughSubject < Play , Never > ( )
private var updateDate = Date ( )
// o u t p u t
let player : AVPlayer
2021-03-10 10:14:12 +01:00
private ( set ) var looper : AVPlayerLooper ? // w o r k s w i t h A V Q u e u e P l a y e r ( i O S 1 0 + )
2021-03-10 07:36:28 +01:00
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 }
2021-03-10 10:14:12 +01:00
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: player state: %s " , ( #file as NSString ) . lastPathComponent , #line , #function , player . timeControlStatus . debugDescription )
2021-03-10 07:36:28 +01:00
self . timeControlStatus . value = player . timeControlStatus
}
// u p d a t e a u d i o s e s s i o n c a t e g o r y f o r u s e r i n t e r a c t i v e e v e n t s t r e a m
timeControlStatus
. sink { [ weak self ] timeControlStatus in
guard let _ = self else { return }
guard timeControlStatus = = . playing else { return }
2021-03-10 14:19:56 +01:00
NotificationCenter . default . post ( name : VideoPlayerViewModel . appWillPlayVideoNotification , object : nil )
2021-03-10 07:36:28 +01:00
switch videoKind {
2021-03-10 10:14:12 +01:00
case . gif :
break
case . video :
try ? AVAudioSession . sharedInstance ( ) . setCategory ( . soloAmbient , mode : . default )
2021-03-10 07:36:28 +01:00
}
}
. 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 {
2021-03-10 10:14:12 +01:00
case . gif :
break
case . video :
try ? AVAudioSession . sharedInstance ( ) . setCategory ( . soloAmbient , mode : . default )
2021-03-10 07:36:28 +01:00
}
2021-03-10 10:14:12 +01:00
2021-03-10 07:36:28 +01:00
player . play ( )
updateDate = Date ( )
}
func pause ( ) {
player . pause ( )
updateDate = Date ( )
}
func willDisplay ( ) {
2021-03-10 10:14:12 +01:00
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: url: %s " , ( #file as NSString ) . lastPathComponent , #line , #function , videoURL . debugDescription )
2021-03-10 07:36:28 +01:00
switch videoKind {
case . gif :
2021-03-10 10:14:12 +01:00
play ( ) // a l w a y s a u t o p l a y G I F
2021-03-10 07:36:28 +01:00
case . video :
guard isPlayingWhenEndDisplaying else { return }
// m u t e b e f o r e r e s u m e
if updateDate . timeIntervalSinceNow < - 3 {
player . isMuted = true
}
debouncePlayingState . send ( true )
}
updateDate = Date ( )
}
func didEndDisplaying ( ) {
2021-03-10 10:14:12 +01:00
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: url: %s " , ( #file as NSString ) . lastPathComponent , #line , #function , videoURL . debugDescription )
2021-03-10 07:36:28 +01:00
isPlayingWhenEndDisplaying = timeControlStatus . value != . paused
switch videoKind {
case . gif :
2021-03-10 10:14:12 +01:00
pause ( ) // a l w a y s p a u s e G I F i m m e d i a t e l y
2021-03-10 07:36:28 +01:00
case . video :
debouncePlayingState . send ( false )
}
updateDate = Date ( )
}
}