From 0d6f4814706cd6cfeda6d1c545a206a861d46289 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Fri, 20 May 2022 23:23:14 +0200 Subject: [PATCH] Add PiP for iOS --- Model/Player/Backends/AVPlayerBackend.swift | 20 ++++++++++-- Model/Player/Backends/MPVBackend.swift | 4 ++- Model/Player/PiPDelegate.swift | 35 +++++++++++++++++++++ Model/Player/PlayerModel.swift | 5 +++ Shared/Player/AppleAVPlayerView.swift | 23 ++++---------- Shared/Player/Controls/PlayerControls.swift | 23 ++++++++++++++ Shared/Player/PlayerLayerView.swift | 23 ++++++++++++++ Shared/Player/VideoPlayerView.swift | 11 +++++++ Yattee.xcodeproj/project.pbxproj | 14 +++++++++ 9 files changed, 137 insertions(+), 21 deletions(-) create mode 100644 Model/Player/PiPDelegate.swift create mode 100644 Shared/Player/PlayerLayerView.swift diff --git a/Model/Player/Backends/AVPlayerBackend.swift b/Model/Player/Backends/AVPlayerBackend.swift index f0b841c4..8b607ea0 100644 --- a/Model/Player/Backends/AVPlayerBackend.swift +++ b/Model/Player/Backends/AVPlayerBackend.swift @@ -36,8 +36,9 @@ final class AVPlayerBackend: PlayerBackend { } private(set) var avPlayer = AVPlayer() - var controller: AppleAVPlayerViewController? + var enterPiPOnPlay = false + var switchToMPVOnPipClose = false private var asset: AVURLAsset? private var composition = AVMutableComposition() @@ -535,13 +536,26 @@ final class AVPlayerBackend: PlayerBackend { } if player.timeControlStatus != .waitingToPlayAtSpecifiedRate { + if let controller = self.model.pipController { + if controller.isPictureInPicturePossible { + if self.enterPiPOnPlay { + self.enterPiPOnPlay = false + DispatchQueue.main.async { [weak self] in + self?.model.pipController?.startPictureInPicture() + } + } + } + } + DispatchQueue.main.async { [weak self] in self?.model.objectWillChange.send() } } - if player.timeControlStatus == .playing, player.rate != self.model.currentRate { - player.rate = self.model.currentRate + if player.timeControlStatus == .playing { + if player.rate != self.model.currentRate { + player.rate = self.model.currentRate + } } #if os(macOS) diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index 7de798b8..c41af091 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -41,7 +41,9 @@ final class MPVBackend: PlayerBackend { updateControlsIsPlaying() #if !os(macOS) - UIApplication.shared.isIdleTimerDisabled = model.presentingPlayer && isPlaying + DispatchQueue.main.async { + UIApplication.shared.isIdleTimerDisabled = self.model.presentingPlayer && self.isPlaying + } #endif }} var playerItemDuration: CMTime? diff --git a/Model/Player/PiPDelegate.swift b/Model/Player/PiPDelegate.swift new file mode 100644 index 00000000..bbe164ca --- /dev/null +++ b/Model/Player/PiPDelegate.swift @@ -0,0 +1,35 @@ +import AVKit +import Foundation + +final class PiPDelegate: NSObject, AVPictureInPictureControllerDelegate { + var player: PlayerModel! + + func pictureInPictureController( + _: AVPictureInPictureController, + failedToStartPictureInPictureWithError error: Error + ) { + print(error.localizedDescription) + } + + func pictureInPictureControllerWillStartPictureInPicture(_: AVPictureInPictureController) {} + + func pictureInPictureControllerDidStartPictureInPicture(_: AVPictureInPictureController) {} + + func pictureInPictureControllerDidStopPictureInPicture(_: AVPictureInPictureController) { + if player?.avPlayerBackend.switchToMPVOnPipClose ?? false { + DispatchQueue.main.async { [weak player] in + player?.avPlayerBackend.switchToMPVOnPipClose = false + player?.saveTime { [weak player] in + player?.changeActiveBackend(from: .appleAVPlayer, to: .mpv) + } + } + } + } + + func pictureInPictureControllerWillStopPictureInPicture(_: AVPictureInPictureController) {} + + func pictureInPictureController( + _: AVPictureInPictureController, + restoreUserInterfaceForPictureInPictureStopWithCompletionHandler _: @escaping (Bool) -> Void + ) {} +} diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index ba349bf6..a44607f5 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -83,6 +83,8 @@ final class PlayerModel: ObservableObject { var context: NSManagedObjectContext = PersistenceController.shared.container.viewContext @Published var playingInPictureInPicture = false + var pipController: AVPictureInPictureController? + var pipDelegate = PiPDelegate() @Published var presentingErrorDetails = false var playerError: Error? { didSet { @@ -102,6 +104,9 @@ final class PlayerModel: ObservableObject { #endif private var currentArtwork: MPMediaItemArtwork? + #if !os(macOS) + var playerLayerView: PlayerLayerView! + #endif init(accounts: AccountsModel? = nil, comments: CommentsModel? = nil, controls: PlayerControlsModel? = nil) { self.accounts = accounts ?? AccountsModel() diff --git a/Shared/Player/AppleAVPlayerView.swift b/Shared/Player/AppleAVPlayerView.swift index 55c16c3b..7e558e6d 100644 --- a/Shared/Player/AppleAVPlayerView.swift +++ b/Shared/Player/AppleAVPlayerView.swift @@ -1,25 +1,14 @@ +import AVKit import Defaults import SwiftUI -struct AppleAVPlayerView: UIViewControllerRepresentable { - @EnvironmentObject private var comments - @EnvironmentObject private var navigation +struct AppleAVPlayerView: UIViewRepresentable { @EnvironmentObject private var player - @EnvironmentObject private var subscriptions - func makeUIViewController(context _: Context) -> UIViewController { - let controller = AppleAVPlayerViewController() - - controller.commentsModel = comments - controller.navigationModel = navigation - controller.playerModel = player - controller.subscriptionsModel = subscriptions - player.avPlayerBackend.controller = controller - - return controller + func makeUIView(context _: Context) -> some UIView { + player.playerLayerView = PlayerLayerView(frame: .zero) + return player.playerLayerView } - func updateUIViewController(_: UIViewController, context _: Context) { - player.rebuildTVMenu() - } + func updateUIView(_: UIViewType, context _: Context) {} } diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index be854d16..cf21b373 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -173,6 +173,9 @@ struct PlayerControls: View { HStack { #if !os(tvOS) fullscreenButton + #if os(iOS) + pipButton + #endif rateButton Spacer() @@ -235,6 +238,26 @@ struct PlayerControls: View { .init(get: { player.currentRate }, set: { rate in player.currentRate = rate }) } + private var pipButton: some View { + button("PiP", systemImage: "pip") { + if player.activeBackend == .mpv { + player.avPlayerBackend.switchToMPVOnPipClose = true + } + + if player.activeBackend != PlayerBackendType.appleAVPlayer { + player.saveTime { + player.changeActiveBackend(from: .mpv, to: .appleAVPlayer) + } + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + print(player.pipController?.isPictureInPicturePossible ?? false ? "possible" : "NOT possible") + player.avPlayerBackend.enterPiPOnPlay = true + player.pipController?.startPictureInPicture() + } + } + } + var mediumButtonsBar: some View { HStack { #if !os(tvOS) diff --git a/Shared/Player/PlayerLayerView.swift b/Shared/Player/PlayerLayerView.swift new file mode 100644 index 00000000..a23cefcc --- /dev/null +++ b/Shared/Player/PlayerLayerView.swift @@ -0,0 +1,23 @@ +import AVFoundation +import Foundation +import UIKit + +final class PlayerLayerView: UIView { + var playerLayer = AVPlayerLayer() + + override init(frame: CGRect) { + super.init(frame: frame) + + layer.addSublayer(playerLayer) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + playerLayer.frame = bounds + } +} diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 3966920c..f521e950 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -226,6 +226,17 @@ struct VideoPlayerView: View { }) case .appleAVPlayer: player.avPlayerView + #if os(iOS) + .onAppear { + player.pipController = .init(playerLayer: player.playerLayerView.playerLayer) + let pipDelegate = PiPDelegate() + pipDelegate.player = player + + player.pipDelegate = pipDelegate + player.pipController!.delegate = pipDelegate + player.playerLayerView.playerLayer.player = player.avPlayerBackend.avPlayer + } + #endif } #if !os(tvOS) diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index 81a53b0c..48a8561e 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -190,6 +190,11 @@ 372915E62687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; }; 372915E72687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; }; 372915E82687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; }; + 372D85DE283841B800FF3C7D /* PiPDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373031F428383A89000CFD59 /* PiPDelegate.swift */; }; + 372D85DF283842EC00FF3C7D /* PiPDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373031F428383A89000CFD59 /* PiPDelegate.swift */; }; + 372D85E0283842EE00FF3C7D /* PlayerLayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373031F22838388A000CFD59 /* PlayerLayerView.swift */; }; + 373031F32838388A000CFD59 /* PlayerLayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373031F22838388A000CFD59 /* PlayerLayerView.swift */; }; + 373031F528383A89000CFD59 /* PiPDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373031F428383A89000CFD59 /* PiPDelegate.swift */; }; 3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3730D89F2712E2B70020ED53 /* NowPlayingView.swift */; }; 3730F75A2733481E00F385FC /* RelatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373197D82732015300EF734F /* RelatedView.swift */; }; 373197D92732015300EF734F /* RelatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373197D82732015300EF734F /* RelatedView.swift */; }; @@ -864,6 +869,8 @@ 3727B74927872A920021C15E /* VisualEffectBlur-iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisualEffectBlur-iOS.swift"; sourceTree = ""; }; 3729037D2739E47400EA99F6 /* MenuCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuCommands.swift; sourceTree = ""; }; 372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = ""; }; + 373031F22838388A000CFD59 /* PlayerLayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerLayerView.swift; sourceTree = ""; }; + 373031F428383A89000CFD59 /* PiPDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPDelegate.swift; sourceTree = ""; }; 3730D89F2712E2B70020ED53 /* NowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingView.swift; sourceTree = ""; }; 373197D82732015300EF734F /* RelatedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedView.swift; sourceTree = ""; }; 37319F0427103F94004ECCD0 /* PlayerQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueue.swift; sourceTree = ""; }; @@ -1351,6 +1358,7 @@ 37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */, 37E8B0EB27B326C00024006F /* TimelineView.swift */, 37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */, + 373031F22838388A000CFD59 /* PlayerLayerView.swift */, ); path = Player; sourceTree = ""; @@ -1439,6 +1447,7 @@ isa = PBXGroup; children = ( 37EBD8C227AF0D7C00F1C24B /* Backends */, + 373031F428383A89000CFD59 /* PiPDelegate.swift */, 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */, 37319F0427103F94004ECCD0 /* PlayerQueue.swift */, 37CC3F44270CE30600608308 /* PlayerQueueItem.swift */, @@ -2548,6 +2557,7 @@ 37EF9A76275BEB8E0043B585 /* CommentView.swift in Sources */, 373C8FE4275B955100CB5936 /* CommentsPage.swift in Sources */, 3700155B271B0D4D0049C794 /* PipedAPI.swift in Sources */, + 373031F32838388A000CFD59 /* PlayerLayerView.swift in Sources */, 37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */, 377FC7E3267A084A00A6BBAF /* VideoCell.swift in Sources */, 37C3A251272366440087A57A /* ChannelPlaylistView.swift in Sources */, @@ -2600,6 +2610,7 @@ 3788AC2726F6840700F6BAA9 /* FavoriteItemView.swift in Sources */, 375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */, 3751BA8327E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */, + 373031F528383A89000CFD59 /* PiPDelegate.swift in Sources */, 37DD9DC62785D63A00539416 /* UIResponder+Extensions.swift in Sources */, 37C3A24927235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */, 373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */, @@ -2758,6 +2769,7 @@ 37A9965F26D6F9B9006E3224 /* FavoritesView.swift in Sources */, 37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */, 37001560271B12DD0049C794 /* SiestaConfiguration.swift in Sources */, + 372D85DE283841B800FF3C7D /* PiPDelegate.swift in Sources */, 37B81AFD26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */, 37C0697F2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */, 37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */, @@ -3000,6 +3012,8 @@ 37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */, 37BE0BD726A1D4A90092E2DB /* AppleAVPlayerViewController.swift in Sources */, 37484C3326FCB8F900287258 /* AccountValidator.swift in Sources */, + 372D85DF283842EC00FF3C7D /* PiPDelegate.swift in Sources */, + 372D85E0283842EE00FF3C7D /* PlayerLayerView.swift in Sources */, 37CEE4C32677B697005A1EFE /* Stream.swift in Sources */, 37F64FE626FE70A60081B69E /* RedrawOnModifier.swift in Sources */, 37B2631C2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */,