From 6c71cd72b16fb78c9a103307ab5021b69edc5216 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sat, 9 Jul 2022 02:21:04 +0200 Subject: [PATCH] Player layout fixes --- Model/NavigationModel.swift | 8 +- Model/Player/Backends/AVPlayerBackend.swift | 16 ++- Model/Player/Backends/MPVBackend.swift | 4 + Model/Player/Backends/MPVClient.swift | 33 +++++- Model/Player/Backends/PlayerBackend.swift | 2 + Model/Player/PlayerModel.swift | 44 +++---- Shared/Navigation/AppTabNavigation.swift | 46 +++++--- Shared/Navigation/ContentView.swift | 2 +- Shared/Player/VideoPlayerSizeModifier.swift | 13 ++- Shared/Player/VideoPlayerView.swift | 120 ++++++++++++-------- Shared/Views/ChannelPlaylistView.swift | 6 +- Shared/Views/ChannelVideosView.swift | 6 +- 12 files changed, 198 insertions(+), 102 deletions(-) diff --git a/Model/NavigationModel.swift b/Model/NavigationModel.swift index e073d560..1caf2e21 100644 --- a/Model/NavigationModel.swift +++ b/Model/NavigationModel.swift @@ -110,7 +110,9 @@ final class NavigationModel: ObservableObject { navigation.sidebarSectionChanged.toggle() navigation.tabSelection = .recentlyOpened(recent.tag) } else { - navigation.presentingChannel = true + withAnimation { + navigation.presentingChannel = true + } } } } @@ -139,7 +141,9 @@ final class NavigationModel: ObservableObject { navigation.sidebarSectionChanged.toggle() navigation.tabSelection = .recentlyOpened(recent.tag) } else { - navigation.presentingPlaylist = true + withAnimation { + navigation.presentingPlaylist = true + } } } } diff --git a/Model/Player/Backends/AVPlayerBackend.swift b/Model/Player/Backends/AVPlayerBackend.swift index a4937983..b7904bf7 100644 --- a/Model/Player/Backends/AVPlayerBackend.swift +++ b/Model/Player/Backends/AVPlayerBackend.swift @@ -33,6 +33,14 @@ final class AVPlayerBackend: PlayerBackend { avPlayer.timeControlStatus == .playing } + var aspectRatio: Double { + #if os(tvOS) + VideoPlayerView.defaultAspectRatio + #else + controller?.aspectRatio ?? VideoPlayerView.defaultAspectRatio + #endif + } + var isSeeking: Bool { // TODO: implement this maybe? false @@ -144,14 +152,10 @@ final class AVPlayerBackend: PlayerBackend { } func enterFullScreen() { - controller?.playerView - .perform(NSSelectorFromString("enterFullScreenAnimated:completionHandler:"), with: false, with: nil) + model.toggleFullscreen(model?.playingFullScreen ?? false) } - func exitFullScreen() { - controller?.playerView - .perform(NSSelectorFromString("exitFullScreenAnimated:completionHandler:"), with: false, with: nil) - } + func exitFullScreen() {} #if os(tvOS) func closePiP(wasPlaying: Bool) { diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index 473cc7a5..e6fc3c1b 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -86,6 +86,10 @@ final class MPVBackend: PlayerBackend { client?.tracksCount ?? -1 } + var aspectRatio: Double { + client?.aspectRatio ?? VideoPlayerView.defaultAspectRatio + } + var frameDropCount: Int { client?.frameDropCount ?? 0 } diff --git a/Model/Player/Backends/MPVClient.swift b/Model/Player/Backends/MPVClient.swift index 23a2d878..53365245 100644 --- a/Model/Player/Backends/MPVClient.swift +++ b/Model/Player/Backends/MPVClient.swift @@ -185,6 +185,20 @@ final class MPVClient: ObservableObject { mpv.isNil ? 0.0 : getDouble("demuxer-cache-duration") } + var aspectRatio: Double { + guard !mpv.isNil else { return VideoPlayerView.defaultAspectRatio } + let aspect = getDouble("video-params/aspect") + return aspect.isZero ? VideoPlayerView.defaultAspectRatio : aspect + } + + var dh: Double { + let defaultDh = 500.0 + guard !mpv.isNil else { return defaultDh } + + let dh = getDouble("video-params/dh") + return dh.isZero ? defaultDh : dh + } + var duration: CMTime { CMTime.secondsInDefaultTimescale(mpv.isNil ? -1 : getDouble("duration")) } @@ -240,7 +254,24 @@ final class MPVClient: ObservableObject { return } - glView?.frame = CGRect(x: 0, y: 0, width: roundedWidth, height: roundedHeight) + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + UIView.animate(withDuration: 0.2, animations: { + let height = [self.backend.model.playerSize.height, self.backend.model.playerSize.width / self.aspectRatio].min()! + let offsetY = self.backend.model.playingFullScreen ? ((self.backend.model.playerSize.height / 2.0) - (height / 2)) : 0 + self.glView?.frame = CGRect(x: 0, y: offsetY, width: roundedWidth, height: height) + }) { completion in + if completion { + self.logger.info("setting player size to \(roundedWidth),\(roundedHeight) FINISHED") + + self.glView?.queue.async { + self.glView.display() + } + self.backend?.controls?.objectWillChange.send() + } + } + } + #endif } diff --git a/Model/Player/Backends/PlayerBackend.swift b/Model/Player/Backends/PlayerBackend.swift index faa454f0..bd788af4 100644 --- a/Model/Player/Backends/PlayerBackend.swift +++ b/Model/Player/Backends/PlayerBackend.swift @@ -19,6 +19,8 @@ protocol PlayerBackend { var isSeeking: Bool { get } var playerItemDuration: CMTime? { get } + var aspectRatio: Double { get } + func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream? func canPlay(_ stream: Stream) -> Bool diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 4590e0d9..1cd8cd10 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -170,7 +170,9 @@ final class PlayerModel: ObservableObject { #endif DispatchQueue.main.async { [weak self] in - self?.presentingPlayer = true + withAnimation { + self?.presentingPlayer = true + } } #if os(macOS) @@ -182,7 +184,9 @@ final class PlayerModel: ObservableObject { func hide() { DispatchQueue.main.async { [weak self] in self?.playingFullScreen = false - self?.presentingPlayer = false + withAnimation { + self?.presentingPlayer = false + } } #if os(iOS) @@ -625,26 +629,28 @@ final class PlayerModel: ObservableObject { controls.resetTimer() #if os(macOS) - Windows.player.toggleFullScreen() - #endif - - #if os(iOS) - setNeedsDrawing(false) - #endif - - playingFullScreen = !isFullScreen - - #if os(iOS) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in - self?.setNeedsDrawing(true) + if isFullScreen { + Windows.player.toggleFullScreen() } + #endif + #if os(iOS) + withAnimation(.linear(duration: 0.2)) { + playingFullScreen = !isFullScreen + } + #else + playingFullScreen = !isFullScreen + #endif - if playingFullScreen { - guard !(UIApplication.shared.windows.first?.windowScene?.interfaceOrientation.isLandscape ?? true) else { - return + #if os(macOS) + if !isFullScreen { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { + Windows.player.toggleFullScreen() } - Orientation.lockOrientation(.landscape, andRotateTo: .landscapeRight) - } else { + } + #endif + + #if os(iOS) + if !playingFullScreen { Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait) } #endif diff --git a/Shared/Navigation/AppTabNavigation.swift b/Shared/Navigation/AppTabNavigation.swift index 6ffceb98..66a70cd4 100644 --- a/Shared/Navigation/AppTabNavigation.swift +++ b/Shared/Navigation/AppTabNavigation.swift @@ -145,25 +145,35 @@ struct AppTabNavigation: View { #endif } - private var channelView: some View { - ChannelVideosView() - .environment(\.managedObjectContext, persistenceController.container.viewContext) - .environment(\.inChannelView, true) - .environment(\.navigationStyle, .tab) - .environmentObject(accounts) - .environmentObject(navigation) - .environmentObject(player) - .environmentObject(subscriptions) - .environmentObject(thumbnailsModel) + @ViewBuilder private var channelView: some View { + if navigation.presentingChannel { + ChannelVideosView() + .environment(\.managedObjectContext, persistenceController.container.viewContext) + .environment(\.inChannelView, true) + .environment(\.navigationStyle, .tab) + .environmentObject(accounts) + .environmentObject(navigation) + .environmentObject(player) + .environmentObject(subscriptions) + .environmentObject(thumbnailsModel) + .transition(.asymmetric(insertion: .flipFromBottom, removal: .move(edge: .bottom))) + } else { + EmptyView() + } } - private var playlistView: some View { - ChannelPlaylistView() - .environment(\.managedObjectContext, persistenceController.container.viewContext) - .environmentObject(accounts) - .environmentObject(navigation) - .environmentObject(player) - .environmentObject(subscriptions) - .environmentObject(thumbnailsModel) + @ViewBuilder private var playlistView: some View { + if navigation.presentingPlaylist { + ChannelPlaylistView() + .environment(\.managedObjectContext, persistenceController.container.viewContext) + .environmentObject(accounts) + .environmentObject(navigation) + .environmentObject(player) + .environmentObject(subscriptions) + .environmentObject(thumbnailsModel) + .transition(.asymmetric(insertion: .flipFromBottom, removal: .move(edge: .bottom))) + } else { + EmptyView() + } } } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 8636e309..1989b89a 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -130,7 +130,7 @@ struct ContentView: View { #endif } - var videoPlayer: some View { + @ViewBuilder var videoPlayer: some View { VideoPlayerView() .environmentObject(accounts) .environmentObject(comments) diff --git a/Shared/Player/VideoPlayerSizeModifier.swift b/Shared/Player/VideoPlayerSizeModifier.swift index 21f426e4..1f979de1 100644 --- a/Shared/Player/VideoPlayerSizeModifier.swift +++ b/Shared/Player/VideoPlayerSizeModifier.swift @@ -25,12 +25,15 @@ struct VideoPlayerSizeModifier: ViewModifier { func body(content: Content) -> some View { content - .frame(maxHeight: fullScreen ? .infinity : maxHeight) - .aspectRatio(usedAspectRatio, contentMode: .fit) + .frame(width: geometry.size.width) + .frame(maxHeight: maxHeight) + #if !os(macOS) + .aspectRatio(fullScreen ? nil : usedAspectRatio, contentMode: usedAspectRatioContentMode) + #endif } var usedAspectRatio: Double { - guard aspectRatio != nil else { + guard aspectRatio != nil, aspectRatio != 0 else { return VideoPlayerView.defaultAspectRatio } @@ -53,6 +56,10 @@ struct VideoPlayerSizeModifier: ViewModifier { } var maxHeight: Double { + guard !fullScreen else { + return .infinity + } + #if os(iOS) let height = verticalSizeClass == .regular ? geometry.size.height - minimumHeightLeft : .infinity #else diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 44cf8aed..f0a24832 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -21,10 +21,12 @@ struct VideoPlayerView: View { } @State private var playerSize: CGSize = .zero { didSet { - if playerSize.width > 900 && Defaults[.playerSidebar] == .whenFits { - sidebarQueue = true - } else { - sidebarQueue = false + withAnimation { + if playerSize.width > 900 && Defaults[.playerSidebar] == .whenFits { + sidebarQueue = true + } else { + sidebarQueue = false + } } }} @State private var hoveringPlayer = false @@ -92,6 +94,7 @@ struct VideoPlayerView: View { playerSize = geometry.size } } +// .ignoresSafeArea(.all, edges: playerEdgesIgnoringSafeArea) .onChange(of: geometry.size) { size in self.playerSize = size } @@ -134,6 +137,15 @@ struct VideoPlayerView: View { #endif } + var playerEdgesIgnoringSafeArea: Edge.Set { + #if os(iOS) + if fullScreenLayout, UIDevice.current.orientation.isLandscape { + return [.vertical] + } + #endif + return [] + } + var content: some View { Group { ZStack(alignment: .bottomLeading) { @@ -173,23 +185,25 @@ struct VideoPlayerView: View { } #else GeometryReader { geometry in - VStack(spacing: 0) { + Group { if player.playingInPictureInPicture { pictureInPicturePlaceholder } else { playerView + #if !os(tvOS) .modifier( VideoPlayerSizeModifier( geometry: geometry, - aspectRatio: player.avPlayerBackend.controller?.aspectRatio, - fullScreen: player.playingFullScreen + aspectRatio: player.backend.aspectRatio, + fullScreen: fullScreenLayout ) ) .overlay(playerPlaceholder) #endif } } +// .ignoresSafeArea(.all, edges: fullScreenLayout ? .bottom : Edge.Set()) .frame(maxWidth: fullScreenLayout ? .infinity : nil, maxHeight: fullScreenLayout ? .infinity : nil) .onHover { hovering in hoveringPlayer = hovering @@ -197,7 +211,7 @@ struct VideoPlayerView: View { } #if !os(macOS) .gesture( - DragGesture(coordinateSpace: .global) + DragGesture(minimumDistance: 0, coordinateSpace: .global) .onChanged { value in guard player.presentingPlayer, !playerControls.presentingControlsOverlay else { return } @@ -242,20 +256,19 @@ struct VideoPlayerView: View { if !player.playingFullScreen { VStack(spacing: 0) { #if os(iOS) - if verticalSizeClass == .regular { - VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails) - .edgesIgnoringSafeArea(.bottom) - } - + VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails) + .edgesIgnoringSafeArea(.bottom) #else VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails) - #endif } + #if !os(macOS) + .transition(.move(edge: .bottom)) + #endif .background(colorScheme == .dark ? Color.black : Color.white) .modifier(VideoDetailsPaddingModifier( playerSize: player.playerSize, - aspectRatio: player.avPlayerBackend.controller?.aspectRatio, + aspectRatio: player.backend.aspectRatio, fullScreen: fullScreenDetails )) } @@ -263,6 +276,7 @@ struct VideoPlayerView: View { } #endif } + .background(((colorScheme == .dark || fullScreenLayout) ? Color.black : Color.white).edgesIgnoringSafeArea(.all)) #if os(macOS) .frame(minWidth: 650) @@ -272,6 +286,7 @@ struct VideoPlayerView: View { if sidebarQueue { PlayerQueueView(sidebarQueue: true, fullScreen: $fullScreenDetails) .frame(maxWidth: 350) + .transition(.move(edge: .trailing)) } #elseif os(macOS) if Defaults[.playerSidebar] != .never { @@ -281,49 +296,66 @@ struct VideoPlayerView: View { #endif } } - .ignoresSafeArea(.all, edges: fullScreenLayout ? .vertical : Edge.Set()) #if os(iOS) - .statusBar(hidden: player.playingFullScreen) - .navigationBarHidden(true) + .statusBar(hidden: player.playingFullScreen) #endif } var playerView: some View { ZStack(alignment: .top) { - switch player.activeBackend { - case .mpv: - player.mpvPlayerView - .overlay(GeometryReader { proxy in - Color.clear - .onAppear { - player.playerSize = proxy.size - } - .onChange(of: proxy.size) { _ in - player.playerSize = proxy.size - } - }) - case .appleAVPlayer: - player.avPlayerView - #if os(iOS) - .onAppear { - player.pipController = .init(playerLayer: player.playerLayerView.playerLayer) - let pipDelegate = PiPDelegate() - pipDelegate.player = player + Group { + switch player.activeBackend { + case .mpv: + player.mpvPlayerView + .overlay(GeometryReader { proxy in + Color.clear + .onAppear { + player.playerSize = proxy.size + } + .onChange(of: proxy.size) { _ in + player.playerSize = proxy.size + } + }) + 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 + player.pipDelegate = pipDelegate + player.pipController?.delegate = pipDelegate + player.playerLayerView.playerLayer.player = player.avPlayerBackend.avPlayer + } + #endif + } } + #if os(iOS) + .padding(.top, player.playingFullScreen && verticalSizeClass == .regular ? 20 : 0) + #endif #if !os(tvOS) PlayerGestures() PlayerControls(player: player, thumbnails: thumbnails) + #if os(iOS) + .padding(.top, fullScreenLayout ? (safeAreaInsets.top.isZero ? safeAreaInsets.bottom : safeAreaInsets.top) : 0) + .padding(.bottom, fullScreenLayout ? safeAreaInsets.bottom : 0) + #endif #endif } + .ignoresSafeArea(.all, edges: fullScreenLayout ? .vertical : Edge.Set()) + #if os(iOS) + .statusBarHidden(fullScreenLayout) + #endif } + #if os(iOS) + var safeAreaInsets: UIEdgeInsets { + UIApplication.shared.windows.first?.safeAreaInsets ?? .init() + } + #endif + var fullScreenLayout: Bool { #if os(iOS) player.playingFullScreen || verticalSizeClass == .compact @@ -471,10 +503,6 @@ struct VideoPlayerView: View { return } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { - player.exitFullScreen() - } - Orientation.lockOrientation(.portrait) } } diff --git a/Shared/Views/ChannelPlaylistView.swift b/Shared/Views/ChannelPlaylistView.swift index 85f610b2..06cbdd2e 100644 --- a/Shared/Views/ChannelPlaylistView.swift +++ b/Shared/Views/ChannelPlaylistView.swift @@ -61,8 +61,6 @@ struct ChannelPlaylistView: View { viewVerticalOffset = Self.hiddenOffset } } - .offset(y: viewVerticalOffset) - .animation(.easeIn(duration: 0.2), value: viewVerticalOffset) #endif } else { BrowserPlayerControls { @@ -105,7 +103,9 @@ struct ChannelPlaylistView: View { ToolbarItem(placement: .navigation) { if navigationStyle == .tab { Button("Done") { - navigation.presentingPlaylist = false + withAnimation { + navigation.presentingPlaylist = false + } } } } diff --git a/Shared/Views/ChannelVideosView.swift b/Shared/Views/ChannelVideosView.swift index 373c5247..fc4062b4 100644 --- a/Shared/Views/ChannelVideosView.swift +++ b/Shared/Views/ChannelVideosView.swift @@ -57,8 +57,6 @@ struct ChannelVideosView: View { viewVerticalOffset = Self.hiddenOffset } } - .offset(y: viewVerticalOffset) - .animation(.easeIn(duration: 0.2), value: viewVerticalOffset) #endif } else { BrowserPlayerControls { @@ -104,7 +102,9 @@ struct ChannelVideosView: View { ToolbarItem(placement: .navigation) { if navigationStyle == .tab { Button("Done") { - navigation.presentingChannel = false + withAnimation { + navigation.presentingChannel = false + } } } }