AVPlayer system controls on iOS

This commit is contained in:
Arkadiusz Fal
2023-05-20 22:49:10 +02:00
parent a4fdd50388
commit 5383cf0e90
16 changed files with 405 additions and 69 deletions

View File

@@ -2,15 +2,119 @@ import AVKit
import Defaults
import SwiftUI
#if !os(macOS)
final class AppleAVPlayerViewControllerDelegate: NSObject, AVPlayerViewControllerDelegate {
var player: PlayerModel { .shared }
func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_: AVPlayerViewController) -> Bool {
false
}
func playerViewController(_: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator) {
Delay.by(0.5) { [weak self] in
self?.player.playingFullScreen = true
}
}
func playerViewController(_: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
let wasPlaying = player.isPlaying
coordinator.animate(alongsideTransition: nil) { context in
#if os(iOS)
if wasPlaying {
self.player.play()
}
#endif
if !context.isCancelled {
#if os(iOS)
self.player.lockedOrientation = nil
if Defaults[.rotateToPortraitOnExitFullScreen] {
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
}
if wasPlaying {
self.player.play()
}
self.player.playingFullScreen = false
#endif
}
}
}
func playerViewControllerWillStartPictureInPicture(_: AVPlayerViewController) {}
func playerViewControllerWillStopPictureInPicture(_: AVPlayerViewController) {}
func playerViewControllerDidStartPictureInPicture(_: AVPlayerViewController) {
player.playingInPictureInPicture = true
player.avPlayerBackend.startPictureInPictureOnPlay = false
player.avPlayerBackend.startPictureInPictureOnSwitch = false
player.controls.objectWillChange.send()
if Defaults[.closePlayerOnOpeningPiP] { Delay.by(0.1) { self.player.hide() } }
}
func playerViewControllerDidStopPictureInPicture(_: AVPlayerViewController) {
player.playingInPictureInPicture = false
player.controls.objectWillChange.send()
}
func playerViewController(_: AVPlayerViewController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
player.presentingPlayer = true
withAnimation(.linear(duration: 0.3)) {
self.player.playingInPictureInPicture = false
Delay.by(0.5) {
completionHandler(true)
Delay.by(0.2) {
self.player.play()
}
}
}
}
func playerViewController(_: AVPlayerViewController, restoreUserInterfaceForFullScreenExitWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
withAnimation(nil) {
player.presentingPlayer = true
}
completionHandler(true)
}
}
#endif
#if os(iOS)
struct AppleAVPlayerView: UIViewRepresentable {
struct AppleAVPlayerView: UIViewControllerRepresentable {
@State private var controller = AVPlayerViewController()
func makeUIViewController(context _: Context) -> AVPlayerViewController {
setupController()
return controller
}
func updateUIViewController(_: AVPlayerViewController, context _: Context) {
setupController()
}
func setupController() {
controller.delegate = PlayerModel.shared.appleAVPlayerViewControllerDelegate
controller.allowsPictureInPicturePlayback = true
if #available(iOS 14.2, *) {
controller.canStartPictureInPictureAutomaticallyFromInline = true
}
PlayerModel.shared.avPlayerBackend.controller = controller
}
}
struct AppleAVPlayerLayerView: UIViewRepresentable {
func makeUIView(context _: Context) -> some UIView {
PlayerLayerView()
PlayerLayerView(frame: .zero)
}
func updateUIView(_: UIViewType, context _: Context) {}
}
#else
#elseif os(tvOS)
struct AppleAVPlayerView: UIViewControllerRepresentable {
func makeUIViewController(context _: Context) -> AppleAVPlayerViewController {
let controller = AppleAVPlayerViewController()
@@ -23,4 +127,27 @@ import SwiftUI
PlayerModel.shared.rebuildTVMenu()
}
}
#else
struct AppleAVPlayerView: NSViewRepresentable {
@State private var pictureInPictureDelegate = MacOSPiPDelegate()
func makeNSView(context _: Context) -> some NSView {
let view = AVPlayerView()
view.player = PlayerModel.shared.avPlayerBackend.avPlayer
view.showsFullScreenToggleButton = true
view.allowsPictureInPicturePlayback = true
view.pictureInPictureDelegate = pictureInPictureDelegate
return view
}
func updateNSView(_: NSViewType, context _: Context) {}
}
struct AppleAVPlayerLayerView: NSViewRepresentable {
func makeNSView(context _: Context) -> some NSView {
PlayerLayerView(frame: .zero)
}
func updateNSView(_: NSViewType, context _: Context) {}
}
#endif

View File

@@ -1,3 +1,4 @@
import Defaults
import SwiftUI
struct PlayerBackendView: View {
@@ -7,6 +8,8 @@ struct PlayerBackendView: View {
@ObservedObject private var player = PlayerModel.shared
@ObservedObject private var safeAreaModel = SafeAreaModel.shared
@Default(.avPlayerUsesSystemControls) private var avPlayerUsesSystemControls
var body: some View {
ZStack(alignment: .top) {
Group {
@@ -16,7 +19,20 @@ struct PlayerBackendView: View {
case .mpv:
player.mpvPlayerView
case .appleAVPlayer:
player.avPlayerView
#if os(tvOS)
AppleAVPlayerView()
#else
if avPlayerUsesSystemControls,
!player.playingInPictureInPicture,
!player.avPlayerBackend.isStartingPiP
{
AppleAVPlayerView()
} else if !avPlayerUsesSystemControls ||
player.avPlayerBackend.isStartingPiP
{
AppleAVPlayerLayerView()
}
#endif
}
}
.zIndex(0)
@@ -31,17 +47,16 @@ struct PlayerBackendView: View {
.onChange(of: proxy.size) { _ in player.playerSize = proxy.size }
.onChange(of: player.controls.presentingOverlays) { _ in player.playerSize = proxy.size }
})
#if os(iOS)
.padding(.top, player.playingFullScreen && verticalSizeClass == .regular ? 20 : 0)
#endif
#if !os(tvOS)
PlayerGestures()
PlayerControls()
#if os(iOS)
.padding(.top, controlsTopPadding)
.padding(.bottom, controlsBottomPadding)
#endif
if player.activeBackend == .mpv || !avPlayerUsesSystemControls {
PlayerGestures()
PlayerControls()
#if os(iOS)
.padding(.top, controlsTopPadding)
.padding(.bottom, controlsBottomPadding)
#endif
}
#else
hiddenControlsButton
#endif

View File

@@ -3,7 +3,7 @@ import SwiftUI
extension VideoPlayerView {
var playerDragGesture: some Gesture {
DragGesture(minimumDistance: 0, coordinateSpace: .global)
DragGesture(minimumDistance: 30, coordinateSpace: .global)
#if os(iOS)
.updating($dragGestureOffset) { value, state, _ in
guard isVerticalDrag else { return }
@@ -36,7 +36,12 @@ extension VideoPlayerView {
}
#endif
if !isVerticalDrag, horizontalPlayerGestureEnabled, abs(horizontalDrag) > seekGestureSensitivity, !isHorizontalDrag {
if !isVerticalDrag,
horizontalPlayerGestureEnabled,
abs(horizontalDrag) > seekGestureSensitivity,
!isHorizontalDrag,
player.activeBackend == .mpv || !avPlayerUsesSystemControls
{
isHorizontalDrag = true
player.seek.onSeekGestureStart()
viewDragOffset = 0
@@ -80,6 +85,16 @@ extension VideoPlayerView {
player.seek.onSeekGestureEnd()
}
if viewDragOffset > 60,
player.playingFullScreen
{
#if os(iOS)
player.lockedOrientation = nil
#endif
player.exitFullScreen(showControls: false)
viewDragOffset = 0
return
}
isVerticalDrag = false
guard player.presentingPlayer,

View File

@@ -4,8 +4,8 @@ import SwiftUI
struct VideoPlayerSizeModifier: ViewModifier {
let geometry: GeometryProxy
let aspectRatio: Double?
let minimumHeightLeft: Double
let fullScreen: Bool
var detailsHiddenInFullScreen = true
#if os(iOS)
@Environment(\.verticalSizeClass) private var verticalSizeClass
@@ -14,26 +14,31 @@ struct VideoPlayerSizeModifier: ViewModifier {
init(
geometry: GeometryProxy,
aspectRatio: Double? = nil,
minimumHeightLeft: Double? = nil,
fullScreen: Bool = false
fullScreen: Bool = false,
detailsHiddenInFullScreen: Bool = false
) {
self.geometry = geometry
self.aspectRatio = aspectRatio ?? VideoPlayerView.defaultAspectRatio
self.minimumHeightLeft = minimumHeightLeft ?? VideoPlayerView.defaultMinimumHeightLeft
self.fullScreen = fullScreen
self.detailsHiddenInFullScreen = detailsHiddenInFullScreen
}
func body(content: Content) -> some View {
content
.frame(width: geometry.size.width)
.frame(maxWidth: geometry.size.width)
.frame(maxHeight: maxHeight)
#if !os(macOS)
.aspectRatio(fullScreen ? nil : usedAspectRatio, contentMode: usedAspectRatioContentMode)
.aspectRatio(ratio, contentMode: usedAspectRatioContentMode)
#endif
}
var ratio: CGFloat? {
fullScreen ? detailsHiddenInFullScreen ? nil : usedAspectRatio : usedAspectRatio
}
var usedAspectRatio: Double {
guard let aspectRatio, aspectRatio > 0, aspectRatio < VideoPlayerView.defaultAspectRatio else {
guard let aspectRatio, aspectRatio > 0 else {
return VideoPlayerView.defaultAspectRatio
}
@@ -50,15 +55,13 @@ struct VideoPlayerSizeModifier: ViewModifier {
var maxHeight: Double {
guard !fullScreen else {
return .infinity
if detailsHiddenInFullScreen {
return geometry.size.height
} else {
return geometry.size.width / usedAspectRatio
}
}
#if os(iOS)
let height = verticalSizeClass == .regular ? geometry.size.height - minimumHeightLeft : .infinity
#else
let height = geometry.size.height - minimumHeightLeft
#endif
return [height, 0].max()!
return max(geometry.size.height - VideoPlayerView.defaultMinimumHeightLeft, 0)
}
}

View File

@@ -64,6 +64,7 @@ struct VideoPlayerView: View {
@Default(.playerSidebar) var playerSidebar
@Default(.gestureBackwardSeekDuration) private var gestureBackwardSeekDuration
@Default(.gestureForwardSeekDuration) private var gestureForwardSeekDuration
@Default(.avPlayerUsesSystemControls) internal var avPlayerUsesSystemControls
@ObservedObject internal var controlsOverlayModel = ControlOverlaysModel.shared
@@ -98,12 +99,12 @@ struct VideoPlayerView: View {
return GeometryReader { geometry in
HStack(spacing: 0) {
content
.ignoresSafeArea(.all, edges: .bottom)
.frame(height: playerHeight.isNil ? nil : Double(playerHeight!))
.onAppear {
playerSize = geometry.size
}
}
.ignoresSafeArea(.all, edges: .bottom)
.frame(height: playerHeight.isNil ? nil : Double(playerHeight!))
.onChange(of: geometry.size) { _ in
self.playerSize = geometry.size
}
@@ -279,7 +280,8 @@ struct VideoPlayerView: View {
VideoPlayerSizeModifier(
geometry: geometry,
aspectRatio: player.aspectRatio,
fullScreen: fullScreenPlayer
fullScreen: fullScreenPlayer,
detailsHiddenInFullScreen: detailsHiddenInFullScreen
)
)
.onHover { hovering in
@@ -303,15 +305,12 @@ struct VideoPlayerView: View {
.background(Color.black)
if !fullScreenPlayer {
if !detailsHiddenInFullScreen {
VideoDetails(
video: player.videoForDisplay,
fullScreen: $fullScreenDetails,
sidebarQueue: $sidebarQueue
)
#if os(iOS)
.ignoresSafeArea(.all, edges: .bottom)
#endif
.modifier(VideoDetailsPaddingModifier(
playerSize: player.playerSize,
fullScreen: fullScreenDetails
@@ -369,7 +368,7 @@ struct VideoPlayerView: View {
}
}
#endif
if !fullScreenPlayer {
if !detailsHiddenInFullScreen {
#if os(iOS)
if sidebarQueue {
List {
@@ -404,6 +403,12 @@ struct VideoPlayerView: View {
}
#if os(iOS)
.statusBar(hidden: fullScreenPlayer)
.backport
.toolbarBackground(colorScheme == .light ? .white : .black)
.backport
.toolbarBackgroundVisibility(true)
.backport
.toolbarColorScheme(colorScheme)
#endif
#if os(macOS)
.background(
@@ -414,6 +419,16 @@ struct VideoPlayerView: View {
#endif
}
var detailsHiddenInFullScreen: Bool {
guard fullScreenPlayer else { return false }
if player.activeBackend == .mpv {
return true
}
return !avPlayerUsesSystemControls || verticalSizeClass == .compact
}
var fullScreenPlayer: Bool {
#if os(iOS)
player.playingFullScreen || verticalSizeClass == .compact