2021-03-10 07:36:28 +01:00
|
|
|
//
|
|
|
|
// ViedeoPlaybackService.swift
|
|
|
|
// Mastodon
|
|
|
|
//
|
|
|
|
// Created by xiaojian sun on 2021/3/10.
|
|
|
|
//
|
|
|
|
|
|
|
|
import AVKit
|
|
|
|
import Combine
|
|
|
|
import CoreDataStack
|
2021-03-10 10:14:12 +01:00
|
|
|
import Foundation
|
|
|
|
import os.log
|
2021-03-10 07:36:28 +01:00
|
|
|
|
|
|
|
final class VideoPlaybackService {
|
|
|
|
var disposeBag = Set<AnyCancellable>()
|
|
|
|
|
2021-03-29 11:44:52 +02:00
|
|
|
let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.VideoPlaybackService.working-queue")
|
2021-03-10 07:36:28 +01:00
|
|
|
private(set) var viewPlayerViewModelDict: [URL: VideoPlayerViewModel] = [:]
|
|
|
|
|
|
|
|
// only for video kind
|
|
|
|
weak var latestPlayingVideoPlayerViewModel: VideoPlayerViewModel?
|
|
|
|
}
|
|
|
|
|
|
|
|
extension VideoPlaybackService {
|
|
|
|
private func playerViewModel(_ playerViewModel: VideoPlayerViewModel, didUpdateTimeControlStatus: AVPlayer.TimeControlStatus) {
|
|
|
|
switch playerViewModel.videoKind {
|
|
|
|
case .gif:
|
|
|
|
// do nothing
|
|
|
|
return
|
|
|
|
case .video:
|
|
|
|
if playerViewModel.timeControlStatus.value != .paused {
|
|
|
|
latestPlayingVideoPlayerViewModel = playerViewModel
|
|
|
|
|
|
|
|
// pause other player
|
|
|
|
for viewModel in viewPlayerViewModelDict.values {
|
|
|
|
guard viewModel.timeControlStatus.value != .paused else { continue }
|
|
|
|
guard viewModel !== playerViewModel else { continue }
|
|
|
|
viewModel.pause()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if latestPlayingVideoPlayerViewModel === playerViewModel {
|
|
|
|
latestPlayingVideoPlayerViewModel = nil
|
|
|
|
try? AVAudioSession.sharedInstance().setCategory(.soloAmbient, mode: .default)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension VideoPlaybackService {
|
|
|
|
func dequeueVideoPlayerViewModel(for media: Attachment) -> VideoPlayerViewModel? {
|
|
|
|
// Core Data entity not thread-safe. Save attribute before enter working queue
|
|
|
|
guard let height = media.meta?.original?.height,
|
|
|
|
let width = media.meta?.original?.width,
|
|
|
|
let url = URL(string: media.url),
|
2021-03-10 10:14:12 +01:00
|
|
|
media.type == .gifv || media.type == .video
|
|
|
|
else { return nil }
|
2021-03-10 07:36:28 +01:00
|
|
|
|
2021-03-10 10:14:12 +01:00
|
|
|
let previewImageURL = media.previewURL.flatMap { URL(string: $0) }
|
2021-03-10 07:36:28 +01:00
|
|
|
let videoKind: VideoPlayerViewModel.Kind = media.type == .gifv ? .gif : .video
|
|
|
|
|
|
|
|
var _viewModel: VideoPlayerViewModel?
|
|
|
|
workingQueue.sync {
|
|
|
|
if let viewModel = viewPlayerViewModelDict[url] {
|
|
|
|
_viewModel = viewModel
|
|
|
|
} else {
|
|
|
|
let viewModel = VideoPlayerViewModel(
|
|
|
|
previewImageURL: previewImageURL,
|
|
|
|
videoURL: url,
|
|
|
|
videoSize: CGSize(width: width, height: height),
|
|
|
|
videoKind: videoKind
|
|
|
|
)
|
|
|
|
viewPlayerViewModelDict[url] = viewModel
|
|
|
|
setupListener(for: viewModel)
|
|
|
|
_viewModel = viewModel
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return _viewModel
|
|
|
|
}
|
|
|
|
|
|
|
|
func playerViewModel(for playerViewController: AVPlayerViewController) -> VideoPlayerViewModel? {
|
|
|
|
guard let url = (playerViewController.player?.currentItem?.asset as? AVURLAsset)?.url else { return nil }
|
|
|
|
return viewPlayerViewModelDict[url]
|
|
|
|
}
|
|
|
|
|
|
|
|
private func setupListener(for viewModel: VideoPlayerViewModel) {
|
|
|
|
viewModel.timeControlStatus
|
|
|
|
.sink { [weak self] timeControlStatus in
|
|
|
|
guard let self = self else { return }
|
|
|
|
self.playerViewModel(viewModel, didUpdateTimeControlStatus: timeControlStatus)
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
2021-03-10 14:19:56 +01:00
|
|
|
|
2021-03-11 06:11:13 +01:00
|
|
|
NotificationCenter.default.publisher(for: AudioPlaybackService.appWillPlayAudioNotification)
|
2021-03-10 14:19:56 +01:00
|
|
|
.sink { [weak self] _ in
|
|
|
|
guard let self = self else { return }
|
|
|
|
self.pauseWhenPlayAudio()
|
|
|
|
}
|
|
|
|
.store(in: &disposeBag)
|
2021-03-10 07:36:28 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension VideoPlaybackService {
|
|
|
|
func markTransitioning(for toot: Toot) {
|
|
|
|
guard let videoAttachment = toot.mediaAttachments?.filter({ $0.type == .gifv || $0.type == .video }).first else { return }
|
|
|
|
guard let videoPlayerViewModel = dequeueVideoPlayerViewModel(for: videoAttachment) else { return }
|
|
|
|
videoPlayerViewModel.isTransitioning = true
|
|
|
|
}
|
|
|
|
|
|
|
|
func viewDidDisappear(from viewController: UIViewController?) {
|
2021-03-10 10:14:12 +01:00
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
2021-03-10 07:36:28 +01:00
|
|
|
|
|
|
|
// note: do not retain view controller
|
|
|
|
// pause all player when view disppear exclude full screen player and other transitioning scene
|
|
|
|
for viewModel in viewPlayerViewModelDict.values {
|
|
|
|
guard !viewModel.isTransitioning else {
|
|
|
|
viewModel.isTransitioning = false
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
guard !viewModel.isFullScreenPresentationing else {
|
2021-03-10 10:14:12 +01:00
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: isFullScreenPresentationing", (#file as NSString).lastPathComponent, #line, #function)
|
2021-03-10 07:36:28 +01:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
guard viewModel.videoKind == .video else { continue }
|
|
|
|
viewModel.pause()
|
|
|
|
}
|
|
|
|
}
|
2021-03-10 10:14:12 +01:00
|
|
|
|
|
|
|
func pauseWhenPlayAudio() {
|
|
|
|
for viewModel in viewPlayerViewModelDict.values {
|
|
|
|
guard !viewModel.isTransitioning else {
|
|
|
|
viewModel.isTransitioning = false
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
guard !viewModel.isFullScreenPresentationing else {
|
|
|
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: isFullScreenPresentationing", (#file as NSString).lastPathComponent, #line, #function)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
viewModel.pause()
|
|
|
|
}
|
|
|
|
}
|
2021-03-10 07:36:28 +01:00
|
|
|
}
|