Add PiP for iOS

This commit is contained in:
Arkadiusz Fal 2022-05-20 23:23:14 +02:00
parent 0c7f963378
commit 0d6f481470
9 changed files with 137 additions and 21 deletions

View File

@ -36,8 +36,9 @@ final class AVPlayerBackend: PlayerBackend {
} }
private(set) var avPlayer = AVPlayer() private(set) var avPlayer = AVPlayer()
var controller: AppleAVPlayerViewController? var controller: AppleAVPlayerViewController?
var enterPiPOnPlay = false
var switchToMPVOnPipClose = false
private var asset: AVURLAsset? private var asset: AVURLAsset?
private var composition = AVMutableComposition() private var composition = AVMutableComposition()
@ -535,13 +536,26 @@ final class AVPlayerBackend: PlayerBackend {
} }
if player.timeControlStatus != .waitingToPlayAtSpecifiedRate { 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 DispatchQueue.main.async { [weak self] in
self?.model.objectWillChange.send() self?.model.objectWillChange.send()
} }
} }
if player.timeControlStatus == .playing, player.rate != self.model.currentRate { if player.timeControlStatus == .playing {
player.rate = self.model.currentRate if player.rate != self.model.currentRate {
player.rate = self.model.currentRate
}
} }
#if os(macOS) #if os(macOS)

View File

@ -41,7 +41,9 @@ final class MPVBackend: PlayerBackend {
updateControlsIsPlaying() updateControlsIsPlaying()
#if !os(macOS) #if !os(macOS)
UIApplication.shared.isIdleTimerDisabled = model.presentingPlayer && isPlaying DispatchQueue.main.async {
UIApplication.shared.isIdleTimerDisabled = self.model.presentingPlayer && self.isPlaying
}
#endif #endif
}} }}
var playerItemDuration: CMTime? var playerItemDuration: CMTime?

View File

@ -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
) {}
}

View File

@ -83,6 +83,8 @@ final class PlayerModel: ObservableObject {
var context: NSManagedObjectContext = PersistenceController.shared.container.viewContext var context: NSManagedObjectContext = PersistenceController.shared.container.viewContext
@Published var playingInPictureInPicture = false @Published var playingInPictureInPicture = false
var pipController: AVPictureInPictureController?
var pipDelegate = PiPDelegate()
@Published var presentingErrorDetails = false @Published var presentingErrorDetails = false
var playerError: Error? { didSet { var playerError: Error? { didSet {
@ -102,6 +104,9 @@ final class PlayerModel: ObservableObject {
#endif #endif
private var currentArtwork: MPMediaItemArtwork? private var currentArtwork: MPMediaItemArtwork?
#if !os(macOS)
var playerLayerView: PlayerLayerView!
#endif
init(accounts: AccountsModel? = nil, comments: CommentsModel? = nil, controls: PlayerControlsModel? = nil) { init(accounts: AccountsModel? = nil, comments: CommentsModel? = nil, controls: PlayerControlsModel? = nil) {
self.accounts = accounts ?? AccountsModel() self.accounts = accounts ?? AccountsModel()

View File

@ -1,25 +1,14 @@
import AVKit
import Defaults import Defaults
import SwiftUI import SwiftUI
struct AppleAVPlayerView: UIViewControllerRepresentable { struct AppleAVPlayerView: UIViewRepresentable {
@EnvironmentObject<CommentsModel> private var comments
@EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> private var player @EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<SubscriptionsModel> private var subscriptions
func makeUIViewController(context _: Context) -> UIViewController { func makeUIView(context _: Context) -> some UIView {
let controller = AppleAVPlayerViewController() player.playerLayerView = PlayerLayerView(frame: .zero)
return player.playerLayerView
controller.commentsModel = comments
controller.navigationModel = navigation
controller.playerModel = player
controller.subscriptionsModel = subscriptions
player.avPlayerBackend.controller = controller
return controller
} }
func updateUIViewController(_: UIViewController, context _: Context) { func updateUIView(_: UIViewType, context _: Context) {}
player.rebuildTVMenu()
}
} }

View File

@ -173,6 +173,9 @@ struct PlayerControls: View {
HStack { HStack {
#if !os(tvOS) #if !os(tvOS)
fullscreenButton fullscreenButton
#if os(iOS)
pipButton
#endif
rateButton rateButton
Spacer() Spacer()
@ -235,6 +238,26 @@ struct PlayerControls: View {
.init(get: { player.currentRate }, set: { rate in player.currentRate = rate }) .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 { var mediumButtonsBar: some View {
HStack { HStack {
#if !os(tvOS) #if !os(tvOS)

View File

@ -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
}
}

View File

@ -226,6 +226,17 @@ struct VideoPlayerView: View {
}) })
case .appleAVPlayer: case .appleAVPlayer:
player.avPlayerView 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) #if !os(tvOS)

View File

@ -190,6 +190,11 @@
372915E62687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; }; 372915E62687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
372915E72687E3B900F5A35B /* 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 */; }; 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 */; }; 3730D8A02712E2B70020ED53 /* NowPlayingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3730D89F2712E2B70020ED53 /* NowPlayingView.swift */; };
3730F75A2733481E00F385FC /* RelatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373197D82732015300EF734F /* RelatedView.swift */; }; 3730F75A2733481E00F385FC /* RelatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373197D82732015300EF734F /* RelatedView.swift */; };
373197D92732015300EF734F /* 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 = "<group>"; }; 3727B74927872A920021C15E /* VisualEffectBlur-iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisualEffectBlur-iOS.swift"; sourceTree = "<group>"; };
3729037D2739E47400EA99F6 /* MenuCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuCommands.swift; sourceTree = "<group>"; }; 3729037D2739E47400EA99F6 /* MenuCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuCommands.swift; sourceTree = "<group>"; };
372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = "<group>"; }; 372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = "<group>"; };
373031F22838388A000CFD59 /* PlayerLayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerLayerView.swift; sourceTree = "<group>"; };
373031F428383A89000CFD59 /* PiPDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPDelegate.swift; sourceTree = "<group>"; };
3730D89F2712E2B70020ED53 /* NowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingView.swift; sourceTree = "<group>"; }; 3730D89F2712E2B70020ED53 /* NowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingView.swift; sourceTree = "<group>"; };
373197D82732015300EF734F /* RelatedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedView.swift; sourceTree = "<group>"; }; 373197D82732015300EF734F /* RelatedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedView.swift; sourceTree = "<group>"; };
37319F0427103F94004ECCD0 /* PlayerQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueue.swift; sourceTree = "<group>"; }; 37319F0427103F94004ECCD0 /* PlayerQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueue.swift; sourceTree = "<group>"; };
@ -1351,6 +1358,7 @@
37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */, 37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */,
37E8B0EB27B326C00024006F /* TimelineView.swift */, 37E8B0EB27B326C00024006F /* TimelineView.swift */,
37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */, 37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */,
373031F22838388A000CFD59 /* PlayerLayerView.swift */,
); );
path = Player; path = Player;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1439,6 +1447,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
37EBD8C227AF0D7C00F1C24B /* Backends */, 37EBD8C227AF0D7C00F1C24B /* Backends */,
373031F428383A89000CFD59 /* PiPDelegate.swift */,
37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */, 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */,
37319F0427103F94004ECCD0 /* PlayerQueue.swift */, 37319F0427103F94004ECCD0 /* PlayerQueue.swift */,
37CC3F44270CE30600608308 /* PlayerQueueItem.swift */, 37CC3F44270CE30600608308 /* PlayerQueueItem.swift */,
@ -2548,6 +2557,7 @@
37EF9A76275BEB8E0043B585 /* CommentView.swift in Sources */, 37EF9A76275BEB8E0043B585 /* CommentView.swift in Sources */,
373C8FE4275B955100CB5936 /* CommentsPage.swift in Sources */, 373C8FE4275B955100CB5936 /* CommentsPage.swift in Sources */,
3700155B271B0D4D0049C794 /* PipedAPI.swift in Sources */, 3700155B271B0D4D0049C794 /* PipedAPI.swift in Sources */,
373031F32838388A000CFD59 /* PlayerLayerView.swift in Sources */,
37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */, 37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */,
377FC7E3267A084A00A6BBAF /* VideoCell.swift in Sources */, 377FC7E3267A084A00A6BBAF /* VideoCell.swift in Sources */,
37C3A251272366440087A57A /* ChannelPlaylistView.swift in Sources */, 37C3A251272366440087A57A /* ChannelPlaylistView.swift in Sources */,
@ -2600,6 +2610,7 @@
3788AC2726F6840700F6BAA9 /* FavoriteItemView.swift in Sources */, 3788AC2726F6840700F6BAA9 /* FavoriteItemView.swift in Sources */,
375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */, 375DFB5826F9DA010013F468 /* InstancesModel.swift in Sources */,
3751BA8327E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */, 3751BA8327E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */,
373031F528383A89000CFD59 /* PiPDelegate.swift in Sources */,
37DD9DC62785D63A00539416 /* UIResponder+Extensions.swift in Sources */, 37DD9DC62785D63A00539416 /* UIResponder+Extensions.swift in Sources */,
37C3A24927235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */, 37C3A24927235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */, 373CFACB26966264003CB2C6 /* SearchQuery.swift in Sources */,
@ -2758,6 +2769,7 @@
37A9965F26D6F9B9006E3224 /* FavoritesView.swift in Sources */, 37A9965F26D6F9B9006E3224 /* FavoritesView.swift in Sources */,
37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */, 37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */,
37001560271B12DD0049C794 /* SiestaConfiguration.swift in Sources */, 37001560271B12DD0049C794 /* SiestaConfiguration.swift in Sources */,
372D85DE283841B800FF3C7D /* PiPDelegate.swift in Sources */,
37B81AFD26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */, 37B81AFD26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */,
37C0697F2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */, 37C0697F2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */, 37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
@ -3000,6 +3012,8 @@
37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */, 37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */,
37BE0BD726A1D4A90092E2DB /* AppleAVPlayerViewController.swift in Sources */, 37BE0BD726A1D4A90092E2DB /* AppleAVPlayerViewController.swift in Sources */,
37484C3326FCB8F900287258 /* AccountValidator.swift in Sources */, 37484C3326FCB8F900287258 /* AccountValidator.swift in Sources */,
372D85DF283842EC00FF3C7D /* PiPDelegate.swift in Sources */,
372D85E0283842EE00FF3C7D /* PlayerLayerView.swift in Sources */,
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */, 37CEE4C32677B697005A1EFE /* Stream.swift in Sources */,
37F64FE626FE70A60081B69E /* RedrawOnModifier.swift in Sources */, 37F64FE626FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
37B2631C2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */, 37B2631C2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */,