Layout and PiP improvements, new settings

- player is now a separate window on macOS
- add setting to disable pause when player is closed (fixes #40)
- add PiP settings:
  * Close PiP when starting playing other video
  * Close PiP when player is opened
  * Close PiP and open player when application
    enters foreground (iOS/tvOS) (fixes #37)
- new player placeholder when in PiP, context menu with exit option
This commit is contained in:
Arkadiusz Fal
2021-12-19 18:17:04 +01:00
parent cef0b2594a
commit 61a4951831
25 changed files with 443 additions and 94 deletions

View File

@@ -11,7 +11,9 @@ struct PlaybackBar: View {
var body: some View {
HStack {
closeButton
#if !os(macOS)
closeButton
#endif
if player.currentItem != nil {
HStack {
@@ -20,6 +22,9 @@ struct PlaybackBar: View {
rateMenu
}
.font(.caption2)
#if os(macOS)
.padding(.leading, 4)
#endif
Spacer()
@@ -68,7 +73,7 @@ struct PlaybackBar: View {
message: Text(player.playerError?.localizedDescription ?? "")
)
}
.frame(minWidth: 0, maxWidth: .infinity)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 20)
.padding(4)
.background(colorScheme == .dark ? Color.black : Color.white)
}

View File

@@ -1,3 +1,4 @@
import Defaults
import Foundation
import SwiftUI
@@ -8,6 +9,8 @@ struct PlayerQueueRow: View {
@EnvironmentObject<PlayerModel> private var player
@Default(.closePiPOnNavigation) var closePiPOnNavigation
var body: some View {
Group {
Button {
@@ -24,6 +27,10 @@ struct PlayerQueueRow: View {
fullScreen = false
}
}
if closePiPOnNavigation, player.playingInPictureInPicture {
player.closePiP()
}
} label: {
VideoBanner(video: item.video, playbackTime: item.playbackTime, videoDuration: item.videoDuration)
}

View File

@@ -1,4 +1,5 @@
import AVKit
import Defaults
import SwiftUI
final class PlayerViewController: UIViewController {
@@ -7,11 +8,11 @@ final class PlayerViewController: UIViewController {
var navigationModel: NavigationModel!
var playerModel: PlayerModel!
var subscriptionsModel: SubscriptionsModel!
var playerViewController = AVPlayerViewController()
var playerView = AVPlayerViewController()
#if !os(tvOS)
var aspectRatio: Double? {
let ratio = Double(playerViewController.videoBounds.width) / Double(playerViewController.videoBounds.height)
let ratio = Double(playerView.videoBounds.width) / Double(playerView.videoBounds.height)
guard ratio.isFinite else {
return VideoPlayerView.defaultAspectRatio // swiftlint:disable:this implicit_return
@@ -27,24 +28,35 @@ final class PlayerViewController: UIViewController {
loadPlayer()
#if os(tvOS)
if !playerViewController.isBeingPresented, !playerViewController.isBeingDismissed {
present(playerViewController, animated: false)
if !playerView.isBeingPresented, !playerView.isBeingDismissed {
present(playerView, animated: false)
}
#endif
}
#if os(tvOS)
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if !playerModel.presentingPlayer, !Defaults[.pauseOnHidingPlayer], !playerModel.isPlaying {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.playerModel.play()
}
}
}
#endif
func loadPlayer() {
guard !playerLoaded else {
return
}
playerModel.controller = self
playerViewController.player = playerModel.player
playerViewController.allowsPictureInPicturePlayback = true
playerViewController.delegate = self
playerView.player = playerModel.player
playerView.allowsPictureInPicturePlayback = true
playerView.delegate = self
#if os(tvOS)
playerModel.avPlayerViewController = playerViewController
var infoViewControllers = [UIHostingController<AnyView>]()
if CommentsModel.enabled {
infoViewControllers.append(infoViewController([.comments], title: "Comments"))
@@ -54,7 +66,7 @@ final class PlayerViewController: UIViewController {
infoViewController([.playingNext, .playedPreviously], title: "Playing Next")
])
playerViewController.customInfoViewControllers = infoViewControllers
playerView.customInfoViewControllers = infoViewControllers
#else
embedViewController()
#endif
@@ -81,12 +93,12 @@ final class PlayerViewController: UIViewController {
}
#else
func embedViewController() {
playerViewController.view.frame = view.bounds
playerView.view.frame = view.bounds
addChild(playerViewController)
view.addSubview(playerViewController.view)
addChild(playerView)
view.addSubview(playerView.view)
playerViewController.didMove(toParent: self)
playerView.didMove(toParent: self)
}
#endif
}
@@ -127,19 +139,19 @@ extension PlayerViewController: AVPlayerViewControllerDelegate {
}
func playerViewController(
_ playerViewController: AVPlayerViewController,
_: AVPlayerViewController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if self.navigationModel.presentingChannel {
self.playerModel.playerNavigationLinkActive = true
} else {
self.playerModel.presentPlayer()
self.playerModel.show()
}
#if os(tvOS)
if self.playerModel.playingInPictureInPicture {
self.present(playerViewController, animated: false) {
self.present(self.playerView, animated: false) {
completionHandler(true)
}
}

View File

@@ -31,7 +31,8 @@ struct VideoPlayerView: View {
HSplitView {
content
}
.frame(idealWidth: 1000, maxWidth: 1100, minHeight: 700)
.onOpenURL(perform: handleOpenedURL)
.frame(minWidth: 950, minHeight: 700)
#else
GeometryReader { geometry in
HStack(spacing: 0) {
@@ -66,15 +67,16 @@ struct VideoPlayerView: View {
if player.currentItem.isNil {
playerPlaceholder(geometry: geometry)
} else if player.playingInPictureInPicture {
pictureInPicturePlaceholder(geometry: geometry)
} else {
#if os(macOS)
Player()
.modifier(VideoPlayerSizeModifier(geometry: geometry, aspectRatio: player.controller?.aspectRatio))
#else
player.playerView
.modifier(VideoPlayerSizeModifier(geometry: geometry, aspectRatio: player.controller?.aspectRatio))
#endif
player.playerView
.modifier(
VideoPlayerSizeModifier(
geometry: geometry,
aspectRatio: player.controller?.aspectRatio
)
)
}
}
#if os(iOS)
@@ -143,6 +145,35 @@ struct VideoPlayerView: View {
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / VideoPlayerView.defaultAspectRatio)
}
func pictureInPicturePlaceholder(geometry: GeometryProxy) -> some View {
HStack {
Spacer()
VStack {
Spacer()
VStack(spacing: 10) {
#if !os(tvOS)
Image(systemName: "pip")
.font(.system(size: 120))
#endif
Text("Playing in Picture in Picture")
}
Spacer()
}
.foregroundColor(.gray)
Spacer()
}
.contextMenu {
Button {
player.closePiP()
} label: {
Label("Exit Picture in Picture", systemImage: "pip.exit")
}
}
.contentShape(Rectangle())
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / VideoPlayerView.defaultAspectRatio)
}
var sidebarQueue: Bool {
switch Defaults[.playerSidebar] {
case .never:
@@ -160,6 +191,27 @@ struct VideoPlayerView: View {
set: { _ in }
)
}
#if !os(tvOS)
func handleOpenedURL(_ url: URL) {
guard !player.accounts.current.isNil else {
return
}
let parser = VideoURLParser(url: url)
guard let id = parser.id else {
return
}
player.accounts.api.video(id).load().onSuccess { response in
if let video: Video = response.typedContent() {
self.player.playNow(video, at: parser.time)
self.player.show()
}
}
}
#endif
}
struct VideoPlayerView_Previews: PreviewProvider {