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-06-11 22:37:54 +02:00
static let appWillPlayVideoNotification = NSNotification . Name ( rawValue : " org.joinmastodon.app.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 )
2021-06-30 12:19:12 +02:00
let playbackState = CurrentValueSubject < PlaybackState , Never > ( PlaybackState . unknown )
2021-03-10 07:36:28 +01:00
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
}
2021-06-30 12:19:12 +02:00
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 . value = . failed
case . readyToPlay :
self . playbackState . value = . readyToPlay
case . unknown :
self . playbackState . value = . unknown
@ unknown default :
assertionFailure ( )
}
} )
. store ( in : & disposeBag )
2021-03-10 07:36:28 +01:00
timeControlStatus
. sink { [ weak self ] timeControlStatus in
2021-06-30 12:19:12 +02:00
guard let self = self else { return }
// e m i t p l a y i n g e v e n t
if timeControlStatus = = . playing {
NotificationCenter . default . post ( name : VideoPlayerViewModel . appWillPlayVideoNotification , object : nil )
}
switch timeControlStatus {
case . paused :
self . playbackState . value = . paused
case . waitingToPlayAtSpecifiedRate :
self . playbackState . value = . buffering
case . playing :
self . playbackState . value = . playing
@ unknown default :
assertionFailure ( )
self . playbackState . value = . unknown
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 )
2021-06-30 12:19:12 +02:00
let sessionName = videoKind = = . gif ? " GIF " : " Video "
playbackState
. receive ( on : RunLoop . main )
. sink { [ weak self ] status in
os_log ( . info , log : . debug , " %{public}s[%{public}ld], %{public}s: %s status: %s " , ( ( #file as NSString ) . lastPathComponent ) , #line , #function , sessionName , status . description )
guard let self = self else { return }
// o n l y u p d a t e a u d i o s e s s i o n f o r v i d e o
guard self . videoKind = = . video else { return }
switch status {
case . unknown , . buffering , . readyToPlay :
break
case . playing :
try ? AVAudioSession . sharedInstance ( ) . setCategory ( . soloAmbient )
try ? AVAudioSession . sharedInstance ( ) . setActive ( true )
case . paused , . stopped , . failed :
try ? AVAudioSession . sharedInstance ( ) . setCategory ( . ambient )
try ? AVAudioSession . sharedInstance ( ) . setActive ( false , options : . notifyOthersOnDeactivation )
}
}
. store ( in : & disposeBag )
2021-03-10 07:36:28 +01:00
}
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 :
2021-06-30 11:56:31 +02:00
break
// t r y ? A V A u d i o S e s s i o n . s h a r e d I n s t a n c e ( ) . s e t C a t e g o r y ( . s o l o A m b i e n t , m o d e : . d e f a u l t )
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 ( )
}
}