forked from zelo72/mastodon-ios
feature: video & gifv support
This commit is contained in:
parent
1b654dcabc
commit
e1143b0ce4
|
@ -89,8 +89,13 @@
|
|||
2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */; };
|
||||
2DF75BB925D1474100694EC8 /* ManagedObjectObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BB825D1474100694EC8 /* ManagedObjectObserver.swift */; };
|
||||
2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */; };
|
||||
45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; };
|
||||
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; };
|
||||
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */; };
|
||||
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */; };
|
||||
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */; };
|
||||
5DF1057925F88A1D00D6C0D4 /* MosaicPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1057825F88A1D00D6C0D4 /* MosaicPlayerView.swift */; };
|
||||
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */; };
|
||||
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */; };
|
||||
5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; };
|
||||
5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; };
|
||||
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */; };
|
||||
|
@ -329,6 +334,12 @@
|
|||
3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViedeoPlaybackService.swift; sourceTree = "<group>"; };
|
||||
5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = "<group>"; };
|
||||
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = "<group>"; };
|
||||
5DF1057825F88A1D00D6C0D4 /* MosaicPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MosaicPlayerView.swift; sourceTree = "<group>"; };
|
||||
5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchBlockingView.swift; sourceTree = "<group>"; };
|
||||
5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NeedsDependency+AVPlayerViewControllerDelegate.swift"; sourceTree = "<group>"; };
|
||||
75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = "<group>"; };
|
||||
9780A4C98FFC65B32B50D1C0 /* Pods-MastodonTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
|
@ -459,7 +470,6 @@
|
|||
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
|
||||
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */,
|
||||
2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */,
|
||||
45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -651,6 +661,7 @@
|
|||
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */,
|
||||
2D206B8B25F6015000143C56 /* AudioPlayer.swift */,
|
||||
2DA6054625F716A2006356F9 /* PlaybackState.swift */,
|
||||
5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */,
|
||||
);
|
||||
path = Service;
|
||||
sourceTree = "<group>";
|
||||
|
@ -673,6 +684,7 @@
|
|||
DB59F10325EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift */,
|
||||
2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */,
|
||||
2D38F20725CD491300561493 /* DisposeBagCollectable.swift */,
|
||||
5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */,
|
||||
);
|
||||
path = Protocol;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1113,6 +1125,7 @@
|
|||
2D206B7F25F5F45E00143C56 /* UIImage.swift */,
|
||||
2D206B8525F5FB0900143C56 /* Double.swift */,
|
||||
2D206B9125F60EA700143C56 /* UIControl.swift */,
|
||||
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */,
|
||||
);
|
||||
path = Extension;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1165,6 +1178,8 @@
|
|||
children = (
|
||||
DB9D6C0D25E4F9780051B173 /* MosaicImageViewContainer.swift */,
|
||||
2D206B7125F5D27F00143C56 /* AudioContainerView.swift */,
|
||||
5DF1057825F88A1D00D6C0D4 /* MosaicPlayerView.swift */,
|
||||
5DF1057E25F88A4100D6C0D4 /* TouchBlockingView.swift */,
|
||||
);
|
||||
path = Container;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1174,6 +1189,7 @@
|
|||
children = (
|
||||
DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */,
|
||||
2DA6055025F74407006356F9 /* AudioContainerViewModel.swift */,
|
||||
5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */,
|
||||
);
|
||||
path = ViewModel;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1551,11 +1567,13 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */,
|
||||
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */,
|
||||
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
|
||||
2D206B8C25F6015000143C56 /* AudioPlayer.swift in Sources */,
|
||||
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
|
||||
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
|
||||
2D7631B325C159F700929FB9 /* Item.swift in Sources */,
|
||||
5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */,
|
||||
0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */,
|
||||
2D61335E25C1894B00CAE157 /* APIService.swift in Sources */,
|
||||
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||
|
@ -1603,6 +1621,7 @@
|
|||
2DF75B9B25D0E27500694EC8 /* StatusProviderFacade.swift in Sources */,
|
||||
DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */,
|
||||
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
|
||||
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
|
||||
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
|
||||
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
|
||||
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */,
|
||||
|
@ -1617,6 +1636,7 @@
|
|||
DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */,
|
||||
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */,
|
||||
2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */,
|
||||
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */,
|
||||
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
|
||||
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */,
|
||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
||||
|
@ -1662,6 +1682,7 @@
|
|||
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
||||
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
||||
5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */,
|
||||
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */,
|
||||
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */,
|
||||
2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */,
|
||||
|
@ -1686,6 +1707,7 @@
|
|||
0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */,
|
||||
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
||||
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
||||
5DF1057925F88A1D00D6C0D4 /* MosaicPlayerView.swift in Sources */,
|
||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
@ -51,8 +51,8 @@
|
|||
"repositoryURL": "https://github.com/onevcat/Kingfisher.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "daebf8ddf974164d1b9a050c8231e263f3106b09",
|
||||
"version": "6.1.0"
|
||||
"revision": "81dd1ce8401137637663046c7314e7c885bcc56d",
|
||||
"version": "6.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -34,7 +34,11 @@ extension StatusSection {
|
|||
// configure cell
|
||||
managedObjectContext.performAndWait {
|
||||
let timelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||
StatusSection.configure(cell: cell, readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: timelineIndex.toot, requestUserID: timelineIndex.userID, statusItemAttribute: attribute)
|
||||
StatusSection.configure(
|
||||
cell: cell,
|
||||
dependency: dependency,
|
||||
readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: timelineIndex.toot, requestUserID: timelineIndex.userID, statusItemAttribute: attribute
|
||||
)
|
||||
}
|
||||
cell.delegate = statusTableViewCellDelegate
|
||||
return cell
|
||||
|
@ -45,7 +49,11 @@ extension StatusSection {
|
|||
// configure cell
|
||||
managedObjectContext.performAndWait {
|
||||
let toot = managedObjectContext.object(with: objectID) as! Toot
|
||||
StatusSection.configure(cell: cell, readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: toot, requestUserID: requestUserID, statusItemAttribute: attribute)
|
||||
StatusSection.configure(
|
||||
cell: cell,
|
||||
dependency: dependency,
|
||||
readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: toot, requestUserID: requestUserID, statusItemAttribute: attribute
|
||||
)
|
||||
}
|
||||
cell.delegate = statusTableViewCellDelegate
|
||||
return cell
|
||||
|
@ -69,9 +77,9 @@ extension StatusSection {
|
|||
}
|
||||
|
||||
extension StatusSection {
|
||||
|
||||
static func configure(
|
||||
cell: StatusTableViewCell,
|
||||
dependency: NeedsDependency,
|
||||
readableLayoutFrame: CGRect?,
|
||||
timestampUpdatePublisher: AnyPublisher<Date, Never>,
|
||||
toot: Toot,
|
||||
|
@ -165,6 +173,39 @@ extension StatusSection {
|
|||
cell.statusView.audioView.isHidden = true
|
||||
}
|
||||
|
||||
// set GIF & video
|
||||
let playerViewMaxSize: CGSize = {
|
||||
let maxWidth: CGFloat = {
|
||||
// use statusView width as container width
|
||||
// that width follows readable width and keep constant width after rotate
|
||||
let containerFrame = readableLayoutFrame ?? cell.statusView.frame
|
||||
return containerFrame.width
|
||||
}()
|
||||
let scale: CGFloat = 1.3
|
||||
return CGSize(width: maxWidth, height: maxWidth * scale)
|
||||
}()
|
||||
|
||||
if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first,
|
||||
let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment)
|
||||
{
|
||||
let parent = cell.delegate?.parent()
|
||||
let mosaicPlayerView = cell.statusView.mosaicPlayerView
|
||||
let playerViewController = mosaicPlayerView.setupPlayer(
|
||||
aspectRatio: videoPlayerViewModel.videoSize,
|
||||
maxSize: playerViewMaxSize,
|
||||
parent: parent
|
||||
)
|
||||
playerViewController.delegate = cell.delegate?.playerViewControllerDelegate
|
||||
playerViewController.player = videoPlayerViewModel.player
|
||||
playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif
|
||||
|
||||
mosaicPlayerView.gifIndicatorLabel.isHidden = videoPlayerViewModel.videoKind != .gif
|
||||
mosaicPlayerView.isHidden = false
|
||||
|
||||
} else {
|
||||
cell.statusView.mosaicPlayerView.playerViewController.player?.pause()
|
||||
cell.statusView.mosaicPlayerView.playerViewController.player = nil
|
||||
}
|
||||
// set poll
|
||||
let poll = (toot.reblog ?? toot).poll
|
||||
StatusSection.configure(
|
||||
|
@ -244,7 +285,8 @@ extension StatusSection {
|
|||
timestampUpdatePublisher: AnyPublisher<Date, Never>
|
||||
) {
|
||||
guard let poll = poll,
|
||||
let managedObjectContext = poll.managedObjectContext else {
|
||||
let managedObjectContext = poll.managedObjectContext
|
||||
else {
|
||||
cell.statusView.pollTableView.isHidden = true
|
||||
cell.statusView.pollStatusStackView.isHidden = true
|
||||
cell.statusView.pollVoteButton.isHidden = true
|
||||
|
@ -288,10 +330,10 @@ extension StatusSection {
|
|||
cell.statusView.pollTableView.allowsSelection = !poll.expired
|
||||
|
||||
let votedOptions = poll.options.filter { option in
|
||||
(option.votedBy ?? Set()).map { $0.id }.contains(requestUserID)
|
||||
(option.votedBy ?? Set()).map(\.id).contains(requestUserID)
|
||||
}
|
||||
let didVotedLocal = !votedOptions.isEmpty
|
||||
let didVotedRemote = (poll.votedBy ?? Set()).map { $0.id }.contains(requestUserID)
|
||||
let didVotedRemote = (poll.votedBy ?? Set()).map(\.id).contains(requestUserID)
|
||||
cell.statusView.pollVoteButton.isEnabled = didVotedLocal
|
||||
cell.statusView.pollVoteButton.isHidden = !poll.multiple ? true : (didVotedRemote || poll.expired)
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// AVPlayer.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by xiaojian sun on 2021/3/10.
|
||||
//
|
||||
|
||||
import AVKit
|
||||
|
||||
// MARK: - CustomDebugStringConvertible
|
||||
extension AVPlayer.TimeControlStatus: CustomDebugStringConvertible {
|
||||
public var debugDescription: String {
|
||||
switch self {
|
||||
case .paused: return "paused"
|
||||
case .waitingToPlayAtSpecifiedRate: return "waitingToPlayAtSpecifiedRate"
|
||||
case .playing: return "playing"
|
||||
@unknown default:
|
||||
assertionFailure()
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// NeedsDependency+AVPlayerViewControllerDelegate.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by xiaojian sun on 2021/3/10.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVKit
|
||||
|
||||
extension NeedsDependency where Self: AVPlayerViewControllerDelegate {
|
||||
|
||||
func handlePlayerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||
context.videoPlaybackService.playerViewModel(for: playerViewController)?.isFullScreenPresentationing = true
|
||||
}
|
||||
|
||||
func handlePlayerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||
context.videoPlaybackService.playerViewModel(for: playerViewController)?.isFullScreenPresentationing = false
|
||||
}
|
||||
|
||||
}
|
|
@ -66,6 +66,18 @@ extension StatusTableViewCellDelegate where Self: StatusProvider {
|
|||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: poll %s info updated", ((#file as NSString).lastPathComponent), #line, #function, poll.id)
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
|
||||
toot(for: cell, indexPath: indexPath)
|
||||
.sink { [weak self] toot in
|
||||
guard let self = self else { return }
|
||||
guard let media = (toot?.mediaAttachments ?? Set()).first else { return }
|
||||
guard let videoPlayerViewModel = self.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: media) else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
videoPlayerViewModel.willDisplay()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -337,5 +337,21 @@ extension HomeTimelineViewController: ScrollViewContainer {
|
|||
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerViewControllerDelegate
|
||||
extension HomeTimelineViewController: AVPlayerViewControllerDelegate {
|
||||
|
||||
func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||
handlePlayerViewController(playerViewController, willBeginFullScreenPresentationWithAnimationCoordinator: coordinator)
|
||||
}
|
||||
|
||||
func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||
handlePlayerViewController(playerViewController, willEndFullScreenPresentationWithAnimationCoordinator: coordinator)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - StatusTableViewCellDelegate
|
||||
extension HomeTimelineViewController: StatusTableViewCellDelegate { }
|
||||
extension HomeTimelineViewController: StatusTableViewCellDelegate {
|
||||
weak var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { return self }
|
||||
func parent() -> UIViewController { return self }
|
||||
}
|
||||
|
|
|
@ -204,5 +204,21 @@ extension PublicTimelineViewController: TimelineMiddleLoaderTableViewCellDelegat
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerViewControllerDelegate
|
||||
extension PublicTimelineViewController: AVPlayerViewControllerDelegate {
|
||||
|
||||
func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||
handlePlayerViewController(playerViewController, willBeginFullScreenPresentationWithAnimationCoordinator: coordinator)
|
||||
}
|
||||
|
||||
func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||
handlePlayerViewController(playerViewController, willEndFullScreenPresentationWithAnimationCoordinator: coordinator)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - StatusTableViewCellDelegate
|
||||
extension PublicTimelineViewController: StatusTableViewCellDelegate { }
|
||||
extension PublicTimelineViewController: StatusTableViewCellDelegate {
|
||||
weak var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { return self }
|
||||
func parent() -> UIViewController { return self }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
//
|
||||
// MosaicPlayerView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by xiaojian sun on 2021/3/10.
|
||||
//
|
||||
|
||||
import AVKit
|
||||
import UIKit
|
||||
|
||||
final class MosaicPlayerView: UIView {
|
||||
static let cornerRadius: CGFloat = 8
|
||||
|
||||
private let container = UIView()
|
||||
private let touchBlockingView = TouchBlockingView()
|
||||
private var containerHeightLayoutConstraint: NSLayoutConstraint!
|
||||
|
||||
let playerViewController = AVPlayerViewController()
|
||||
|
||||
let gifIndicatorLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = .systemFont(ofSize: 16, weight: .heavy)
|
||||
label.text = "GIF"
|
||||
label.textColor = .white
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
}
|
||||
|
||||
extension MosaicPlayerView {
|
||||
private func _init() {
|
||||
container.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(container)
|
||||
containerHeightLayoutConstraint = container.heightAnchor.constraint(equalToConstant: 162).priority(.required - 1)
|
||||
NSLayoutConstraint.activate([
|
||||
container.topAnchor.constraint(equalTo: topAnchor),
|
||||
container.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||
bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
||||
containerHeightLayoutConstraint,
|
||||
])
|
||||
|
||||
addSubview(gifIndicatorLabel)
|
||||
gifIndicatorLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
gifIndicatorLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 4),
|
||||
gifIndicatorLabel.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||
])
|
||||
|
||||
// will not influence full-screen playback
|
||||
playerViewController.view.layer.masksToBounds = true
|
||||
playerViewController.view.layer.cornerRadius = MosaicPlayerView.cornerRadius
|
||||
playerViewController.view.layer.cornerCurve = .continuous
|
||||
}
|
||||
}
|
||||
|
||||
extension MosaicPlayerView {
|
||||
func reset() {
|
||||
// note: set playerViewController.player pause() and nil in data source configuration process make reloadData not break playing
|
||||
|
||||
gifIndicatorLabel.removeFromSuperview()
|
||||
|
||||
playerViewController.willMove(toParent: nil)
|
||||
playerViewController.view.removeFromSuperview()
|
||||
playerViewController.removeFromParent()
|
||||
|
||||
container.subviews.forEach { subview in
|
||||
subview.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
func setupPlayer(aspectRatio: CGSize, maxSize: CGSize, parent: UIViewController?) -> AVPlayerViewController {
|
||||
reset()
|
||||
|
||||
touchBlockingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
container.addSubview(touchBlockingView)
|
||||
NSLayoutConstraint.activate([
|
||||
touchBlockingView.topAnchor.constraint(equalTo: container.topAnchor),
|
||||
touchBlockingView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
||||
touchBlockingView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
||||
])
|
||||
|
||||
let rect = AVMakeRect(
|
||||
aspectRatio: aspectRatio,
|
||||
insideRect: CGRect(origin: .zero, size: maxSize)
|
||||
)
|
||||
|
||||
parent?.addChild(playerViewController)
|
||||
playerViewController.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
touchBlockingView.addSubview(playerViewController.view)
|
||||
parent.flatMap { playerViewController.didMove(toParent: $0) }
|
||||
NSLayoutConstraint.activate([
|
||||
playerViewController.view.topAnchor.constraint(equalTo: touchBlockingView.topAnchor),
|
||||
playerViewController.view.leadingAnchor.constraint(equalTo: touchBlockingView.leadingAnchor),
|
||||
playerViewController.view.trailingAnchor.constraint(equalTo: touchBlockingView.trailingAnchor),
|
||||
playerViewController.view.bottomAnchor.constraint(equalTo: touchBlockingView.bottomAnchor),
|
||||
touchBlockingView.widthAnchor.constraint(equalToConstant: floor(rect.width)).priority(.required - 1),
|
||||
])
|
||||
containerHeightLayoutConstraint.constant = floor(rect.height)
|
||||
containerHeightLayoutConstraint.isActive = true
|
||||
|
||||
gifIndicatorLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
touchBlockingView.addSubview(gifIndicatorLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
touchBlockingView.trailingAnchor.constraint(equalTo: gifIndicatorLabel.trailingAnchor, constant: 8),
|
||||
touchBlockingView.bottomAnchor.constraint(equalTo: gifIndicatorLabel.bottomAnchor, constant: 8),
|
||||
])
|
||||
|
||||
return playerViewController
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
//
|
||||
// TouchBlockingView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by xiaojian sun on 2021/3/10.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class TouchBlockingView: UIView {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension TouchBlockingView {
|
||||
|
||||
private func _init() {
|
||||
isUserInteractionEnabled = true
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
// Blocking responder chain by not call super
|
||||
// The subviews in this view will received touch event but superview not
|
||||
}
|
||||
}
|
|
@ -156,6 +156,8 @@ final class StatusView: UIView {
|
|||
return imageView
|
||||
}()
|
||||
|
||||
let mosaicPlayerView = MosaicPlayerView()
|
||||
|
||||
let audioView: AudioContainerView = {
|
||||
let audioView = AudioContainerView()
|
||||
return audioView
|
||||
|
@ -342,6 +344,7 @@ extension StatusView {
|
|||
pollCountdownLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
pollVoteButton.setContentHuggingPriority(.defaultHigh + 3, for: .horizontal)
|
||||
|
||||
// audio
|
||||
audioView.translatesAutoresizingMaskIntoConstraints = false
|
||||
statusContainerStackView.addArrangedSubview(audioView)
|
||||
NSLayoutConstraint.activate([
|
||||
|
@ -349,6 +352,8 @@ extension StatusView {
|
|||
audioView.trailingAnchor.constraint(equalTo: statusTextContainerView.trailingAnchor),
|
||||
audioView.heightAnchor.constraint(equalToConstant: 44)
|
||||
])
|
||||
// video gif
|
||||
statusContainerStackView.addArrangedSubview(mosaicPlayerView)
|
||||
|
||||
// action toolbar container
|
||||
containerStackView.addArrangedSubview(actionToolbarContainer)
|
||||
|
@ -359,7 +364,8 @@ extension StatusView {
|
|||
pollTableView.isHidden = true
|
||||
pollStatusStackView.isHidden = true
|
||||
audioView.isHidden = true
|
||||
|
||||
mosaicPlayerView.isHidden = true
|
||||
|
||||
contentWarningBlurContentImageView.isHidden = true
|
||||
statusContentWarningContainerStackView.isHidden = true
|
||||
statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false
|
||||
|
|
|
@ -16,6 +16,11 @@ protocol StatusTableViewCellDelegate: class {
|
|||
var context: AppContext! { get }
|
||||
var managedObjectContext: NSManagedObjectContext { get }
|
||||
|
||||
func parent() -> UIViewController
|
||||
var playerViewControllerDelegate: AVPlayerViewControllerDelegate? { get }
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, playerViewControllerDidPressed playerViewController: AVPlayerViewController)
|
||||
|
||||
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapContentWarningVisualEffectView visualEffectView: UIVisualEffectView)
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int)
|
||||
|
@ -25,6 +30,13 @@ protocol StatusTableViewCellDelegate: class {
|
|||
func statusTableViewCell(_ cell: StatusTableViewCell, pollTableView: PollTableView, didSelectRowAt indexPath: IndexPath)
|
||||
}
|
||||
|
||||
extension StatusTableViewCellDelegate {
|
||||
func statusTableViewCell(_ cell: StatusTableViewCell, playerViewControllerDidPressed playerViewController: AVPlayerViewController) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
playerViewController.showsPlaybackControls.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
final class StatusTableViewCell: UITableViewCell {
|
||||
|
||||
static let bottomPaddingHeight: CGFloat = 10
|
||||
|
@ -42,6 +54,8 @@ final class StatusTableViewCell: UITableViewCell {
|
|||
statusView.isStatusTextSensitive = false
|
||||
statusView.cleanUpContentWarning()
|
||||
statusView.pollTableView.dataSource = nil
|
||||
statusView.mosaicPlayerView.reset()
|
||||
statusView.mosaicPlayerView.isHidden = true
|
||||
disposeBag.removeAll()
|
||||
observations.removeAll()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
//
|
||||
// VideoPlayerViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by xiaojian sun on 2021/3/10.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import AVKit
|
||||
import CoreDataStack
|
||||
import Combine
|
||||
|
||||
final class VideoPlayerViewModel {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// 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 }
|
||||
switch videoKind {
|
||||
case .gif: try? AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default)
|
||||
case .video: try? AVAudioSession.sharedInstance().setCategory(.soloAmbient, mode: .default)
|
||||
}
|
||||
try? AVAudioSession.sharedInstance().setActive(true)
|
||||
}
|
||||
.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: try? AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default)
|
||||
case .video: try? AVAudioSession.sharedInstance().setCategory(.soloAmbient, mode: .default)
|
||||
}
|
||||
try? AVAudioSession.sharedInstance().setActive(true)
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
//
|
||||
// ViedeoPlaybackService.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by xiaojian sun on 2021/3/10.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import Foundation
|
||||
import AVKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
|
||||
final class VideoPlaybackService {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let workingQueue = DispatchQueue(label: "com.twidere.twiderex.video-playback-service.working-queue")
|
||||
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)
|
||||
try? AVAudioSession.sharedInstance().setActive(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
media.type == .gifv || media.type == .video else
|
||||
{ return nil }
|
||||
|
||||
let previewImageURL = media.previewURL.flatMap({ URL(string: $0) })
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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?) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
// 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 {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: isFullScreenPresentationing", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
continue
|
||||
}
|
||||
guard viewModel.videoKind == .video else { continue }
|
||||
viewModel.pause()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,6 +27,8 @@ class AppContext: ObservableObject {
|
|||
let documentStore: DocumentStore
|
||||
private var documentStoreSubscription: AnyCancellable!
|
||||
|
||||
let videoPlaybackService = VideoPlaybackService()
|
||||
|
||||
let overrideTraitCollection = CurrentValueSubject<UITraitCollection?, Never>(nil)
|
||||
|
||||
init() {
|
||||
|
|
Loading…
Reference in New Issue