Player layout fixes

This commit is contained in:
Arkadiusz Fal 2022-07-09 02:21:04 +02:00
parent 06b7bc79e8
commit 6c71cd72b1
12 changed files with 198 additions and 102 deletions

View File

@ -110,7 +110,9 @@ final class NavigationModel: ObservableObject {
navigation.sidebarSectionChanged.toggle() navigation.sidebarSectionChanged.toggle()
navigation.tabSelection = .recentlyOpened(recent.tag) navigation.tabSelection = .recentlyOpened(recent.tag)
} else { } else {
navigation.presentingChannel = true withAnimation {
navigation.presentingChannel = true
}
} }
} }
} }
@ -139,7 +141,9 @@ final class NavigationModel: ObservableObject {
navigation.sidebarSectionChanged.toggle() navigation.sidebarSectionChanged.toggle()
navigation.tabSelection = .recentlyOpened(recent.tag) navigation.tabSelection = .recentlyOpened(recent.tag)
} else { } else {
navigation.presentingPlaylist = true withAnimation {
navigation.presentingPlaylist = true
}
} }
} }
} }

View File

@ -33,6 +33,14 @@ final class AVPlayerBackend: PlayerBackend {
avPlayer.timeControlStatus == .playing avPlayer.timeControlStatus == .playing
} }
var aspectRatio: Double {
#if os(tvOS)
VideoPlayerView.defaultAspectRatio
#else
controller?.aspectRatio ?? VideoPlayerView.defaultAspectRatio
#endif
}
var isSeeking: Bool { var isSeeking: Bool {
// TODO: implement this maybe? // TODO: implement this maybe?
false false
@ -144,14 +152,10 @@ final class AVPlayerBackend: PlayerBackend {
} }
func enterFullScreen() { func enterFullScreen() {
controller?.playerView model.toggleFullscreen(model?.playingFullScreen ?? false)
.perform(NSSelectorFromString("enterFullScreenAnimated:completionHandler:"), with: false, with: nil)
} }
func exitFullScreen() { func exitFullScreen() {}
controller?.playerView
.perform(NSSelectorFromString("exitFullScreenAnimated:completionHandler:"), with: false, with: nil)
}
#if os(tvOS) #if os(tvOS)
func closePiP(wasPlaying: Bool) { func closePiP(wasPlaying: Bool) {

View File

@ -86,6 +86,10 @@ final class MPVBackend: PlayerBackend {
client?.tracksCount ?? -1 client?.tracksCount ?? -1
} }
var aspectRatio: Double {
client?.aspectRatio ?? VideoPlayerView.defaultAspectRatio
}
var frameDropCount: Int { var frameDropCount: Int {
client?.frameDropCount ?? 0 client?.frameDropCount ?? 0
} }

View File

@ -185,6 +185,20 @@ final class MPVClient: ObservableObject {
mpv.isNil ? 0.0 : getDouble("demuxer-cache-duration") 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 { var duration: CMTime {
CMTime.secondsInDefaultTimescale(mpv.isNil ? -1 : getDouble("duration")) CMTime.secondsInDefaultTimescale(mpv.isNil ? -1 : getDouble("duration"))
} }
@ -240,7 +254,24 @@ final class MPVClient: ObservableObject {
return 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 #endif
} }

View File

@ -19,6 +19,8 @@ protocol PlayerBackend {
var isSeeking: Bool { get } var isSeeking: Bool { get }
var playerItemDuration: CMTime? { get } var playerItemDuration: CMTime? { get }
var aspectRatio: Double { get }
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream? func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream?
func canPlay(_ stream: Stream) -> Bool func canPlay(_ stream: Stream) -> Bool

View File

@ -170,7 +170,9 @@ final class PlayerModel: ObservableObject {
#endif #endif
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
self?.presentingPlayer = true withAnimation {
self?.presentingPlayer = true
}
} }
#if os(macOS) #if os(macOS)
@ -182,7 +184,9 @@ final class PlayerModel: ObservableObject {
func hide() { func hide() {
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
self?.playingFullScreen = false self?.playingFullScreen = false
self?.presentingPlayer = false withAnimation {
self?.presentingPlayer = false
}
} }
#if os(iOS) #if os(iOS)
@ -625,26 +629,28 @@ final class PlayerModel: ObservableObject {
controls.resetTimer() controls.resetTimer()
#if os(macOS) #if os(macOS)
Windows.player.toggleFullScreen() if isFullScreen {
#endif Windows.player.toggleFullScreen()
#if os(iOS)
setNeedsDrawing(false)
#endif
playingFullScreen = !isFullScreen
#if os(iOS)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
self?.setNeedsDrawing(true)
} }
#endif
#if os(iOS)
withAnimation(.linear(duration: 0.2)) {
playingFullScreen = !isFullScreen
}
#else
playingFullScreen = !isFullScreen
#endif
if playingFullScreen { #if os(macOS)
guard !(UIApplication.shared.windows.first?.windowScene?.interfaceOrientation.isLandscape ?? true) else { if !isFullScreen {
return 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) Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
} }
#endif #endif

View File

@ -145,25 +145,35 @@ struct AppTabNavigation: View {
#endif #endif
} }
private var channelView: some View { @ViewBuilder private var channelView: some View {
ChannelVideosView() if navigation.presentingChannel {
.environment(\.managedObjectContext, persistenceController.container.viewContext) ChannelVideosView()
.environment(\.inChannelView, true) .environment(\.managedObjectContext, persistenceController.container.viewContext)
.environment(\.navigationStyle, .tab) .environment(\.inChannelView, true)
.environmentObject(accounts) .environment(\.navigationStyle, .tab)
.environmentObject(navigation) .environmentObject(accounts)
.environmentObject(player) .environmentObject(navigation)
.environmentObject(subscriptions) .environmentObject(player)
.environmentObject(thumbnailsModel) .environmentObject(subscriptions)
.environmentObject(thumbnailsModel)
.transition(.asymmetric(insertion: .flipFromBottom, removal: .move(edge: .bottom)))
} else {
EmptyView()
}
} }
private var playlistView: some View { @ViewBuilder private var playlistView: some View {
ChannelPlaylistView() if navigation.presentingPlaylist {
.environment(\.managedObjectContext, persistenceController.container.viewContext) ChannelPlaylistView()
.environmentObject(accounts) .environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(navigation) .environmentObject(accounts)
.environmentObject(player) .environmentObject(navigation)
.environmentObject(subscriptions) .environmentObject(player)
.environmentObject(thumbnailsModel) .environmentObject(subscriptions)
.environmentObject(thumbnailsModel)
.transition(.asymmetric(insertion: .flipFromBottom, removal: .move(edge: .bottom)))
} else {
EmptyView()
}
} }
} }

View File

@ -130,7 +130,7 @@ struct ContentView: View {
#endif #endif
} }
var videoPlayer: some View { @ViewBuilder var videoPlayer: some View {
VideoPlayerView() VideoPlayerView()
.environmentObject(accounts) .environmentObject(accounts)
.environmentObject(comments) .environmentObject(comments)

View File

@ -25,12 +25,15 @@ struct VideoPlayerSizeModifier: ViewModifier {
func body(content: Content) -> some View { func body(content: Content) -> some View {
content content
.frame(maxHeight: fullScreen ? .infinity : maxHeight) .frame(width: geometry.size.width)
.aspectRatio(usedAspectRatio, contentMode: .fit) .frame(maxHeight: maxHeight)
#if !os(macOS)
.aspectRatio(fullScreen ? nil : usedAspectRatio, contentMode: usedAspectRatioContentMode)
#endif
} }
var usedAspectRatio: Double { var usedAspectRatio: Double {
guard aspectRatio != nil else { guard aspectRatio != nil, aspectRatio != 0 else {
return VideoPlayerView.defaultAspectRatio return VideoPlayerView.defaultAspectRatio
} }
@ -53,6 +56,10 @@ struct VideoPlayerSizeModifier: ViewModifier {
} }
var maxHeight: Double { var maxHeight: Double {
guard !fullScreen else {
return .infinity
}
#if os(iOS) #if os(iOS)
let height = verticalSizeClass == .regular ? geometry.size.height - minimumHeightLeft : .infinity let height = verticalSizeClass == .regular ? geometry.size.height - minimumHeightLeft : .infinity
#else #else

View File

@ -21,10 +21,12 @@ struct VideoPlayerView: View {
} }
@State private var playerSize: CGSize = .zero { didSet { @State private var playerSize: CGSize = .zero { didSet {
if playerSize.width > 900 && Defaults[.playerSidebar] == .whenFits { withAnimation {
sidebarQueue = true if playerSize.width > 900 && Defaults[.playerSidebar] == .whenFits {
} else { sidebarQueue = true
sidebarQueue = false } else {
sidebarQueue = false
}
} }
}} }}
@State private var hoveringPlayer = false @State private var hoveringPlayer = false
@ -92,6 +94,7 @@ struct VideoPlayerView: View {
playerSize = geometry.size playerSize = geometry.size
} }
} }
// .ignoresSafeArea(.all, edges: playerEdgesIgnoringSafeArea)
.onChange(of: geometry.size) { size in .onChange(of: geometry.size) { size in
self.playerSize = size self.playerSize = size
} }
@ -134,6 +137,15 @@ struct VideoPlayerView: View {
#endif #endif
} }
var playerEdgesIgnoringSafeArea: Edge.Set {
#if os(iOS)
if fullScreenLayout, UIDevice.current.orientation.isLandscape {
return [.vertical]
}
#endif
return []
}
var content: some View { var content: some View {
Group { Group {
ZStack(alignment: .bottomLeading) { ZStack(alignment: .bottomLeading) {
@ -173,23 +185,25 @@ struct VideoPlayerView: View {
} }
#else #else
GeometryReader { geometry in GeometryReader { geometry in
VStack(spacing: 0) { Group {
if player.playingInPictureInPicture { if player.playingInPictureInPicture {
pictureInPicturePlaceholder pictureInPicturePlaceholder
} else { } else {
playerView playerView
#if !os(tvOS) #if !os(tvOS)
.modifier( .modifier(
VideoPlayerSizeModifier( VideoPlayerSizeModifier(
geometry: geometry, geometry: geometry,
aspectRatio: player.avPlayerBackend.controller?.aspectRatio, aspectRatio: player.backend.aspectRatio,
fullScreen: player.playingFullScreen fullScreen: fullScreenLayout
) )
) )
.overlay(playerPlaceholder) .overlay(playerPlaceholder)
#endif #endif
} }
} }
// .ignoresSafeArea(.all, edges: fullScreenLayout ? .bottom : Edge.Set())
.frame(maxWidth: fullScreenLayout ? .infinity : nil, maxHeight: fullScreenLayout ? .infinity : nil) .frame(maxWidth: fullScreenLayout ? .infinity : nil, maxHeight: fullScreenLayout ? .infinity : nil)
.onHover { hovering in .onHover { hovering in
hoveringPlayer = hovering hoveringPlayer = hovering
@ -197,7 +211,7 @@ struct VideoPlayerView: View {
} }
#if !os(macOS) #if !os(macOS)
.gesture( .gesture(
DragGesture(coordinateSpace: .global) DragGesture(minimumDistance: 0, coordinateSpace: .global)
.onChanged { value in .onChanged { value in
guard player.presentingPlayer, guard player.presentingPlayer,
!playerControls.presentingControlsOverlay else { return } !playerControls.presentingControlsOverlay else { return }
@ -242,20 +256,19 @@ struct VideoPlayerView: View {
if !player.playingFullScreen { if !player.playingFullScreen {
VStack(spacing: 0) { VStack(spacing: 0) {
#if os(iOS) #if os(iOS)
if verticalSizeClass == .regular { VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails)
VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails) .edgesIgnoringSafeArea(.bottom)
.edgesIgnoringSafeArea(.bottom)
}
#else #else
VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails) VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails)
#endif #endif
} }
#if !os(macOS)
.transition(.move(edge: .bottom))
#endif
.background(colorScheme == .dark ? Color.black : Color.white) .background(colorScheme == .dark ? Color.black : Color.white)
.modifier(VideoDetailsPaddingModifier( .modifier(VideoDetailsPaddingModifier(
playerSize: player.playerSize, playerSize: player.playerSize,
aspectRatio: player.avPlayerBackend.controller?.aspectRatio, aspectRatio: player.backend.aspectRatio,
fullScreen: fullScreenDetails fullScreen: fullScreenDetails
)) ))
} }
@ -263,6 +276,7 @@ struct VideoPlayerView: View {
} }
#endif #endif
} }
.background(((colorScheme == .dark || fullScreenLayout) ? Color.black : Color.white).edgesIgnoringSafeArea(.all)) .background(((colorScheme == .dark || fullScreenLayout) ? Color.black : Color.white).edgesIgnoringSafeArea(.all))
#if os(macOS) #if os(macOS)
.frame(minWidth: 650) .frame(minWidth: 650)
@ -272,6 +286,7 @@ struct VideoPlayerView: View {
if sidebarQueue { if sidebarQueue {
PlayerQueueView(sidebarQueue: true, fullScreen: $fullScreenDetails) PlayerQueueView(sidebarQueue: true, fullScreen: $fullScreenDetails)
.frame(maxWidth: 350) .frame(maxWidth: 350)
.transition(.move(edge: .trailing))
} }
#elseif os(macOS) #elseif os(macOS)
if Defaults[.playerSidebar] != .never { if Defaults[.playerSidebar] != .never {
@ -281,49 +296,66 @@ struct VideoPlayerView: View {
#endif #endif
} }
} }
.ignoresSafeArea(.all, edges: fullScreenLayout ? .vertical : Edge.Set())
#if os(iOS) #if os(iOS)
.statusBar(hidden: player.playingFullScreen) .statusBar(hidden: player.playingFullScreen)
.navigationBarHidden(true)
#endif #endif
} }
var playerView: some View { var playerView: some View {
ZStack(alignment: .top) { ZStack(alignment: .top) {
switch player.activeBackend { Group {
case .mpv: switch player.activeBackend {
player.mpvPlayerView case .mpv:
.overlay(GeometryReader { proxy in player.mpvPlayerView
Color.clear .overlay(GeometryReader { proxy in
.onAppear { Color.clear
player.playerSize = proxy.size .onAppear {
} player.playerSize = proxy.size
.onChange(of: proxy.size) { _ in }
player.playerSize = proxy.size .onChange(of: proxy.size) { _ in
} player.playerSize = proxy.size
}) }
case .appleAVPlayer: })
player.avPlayerView case .appleAVPlayer:
#if os(iOS) player.avPlayerView
.onAppear { #if os(iOS)
player.pipController = .init(playerLayer: player.playerLayerView.playerLayer) .onAppear {
let pipDelegate = PiPDelegate() player.pipController = .init(playerLayer: player.playerLayerView.playerLayer)
pipDelegate.player = player let pipDelegate = PiPDelegate()
pipDelegate.player = player
player.pipDelegate = pipDelegate player.pipDelegate = pipDelegate
player.pipController?.delegate = pipDelegate player.pipController?.delegate = pipDelegate
player.playerLayerView.playerLayer.player = player.avPlayerBackend.avPlayer player.playerLayerView.playerLayer.player = player.avPlayerBackend.avPlayer
} }
#endif #endif
}
} }
#if os(iOS)
.padding(.top, player.playingFullScreen && verticalSizeClass == .regular ? 20 : 0)
#endif
#if !os(tvOS) #if !os(tvOS)
PlayerGestures() PlayerGestures()
PlayerControls(player: player, thumbnails: thumbnails) 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 #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 { var fullScreenLayout: Bool {
#if os(iOS) #if os(iOS)
player.playingFullScreen || verticalSizeClass == .compact player.playingFullScreen || verticalSizeClass == .compact
@ -471,10 +503,6 @@ struct VideoPlayerView: View {
return return
} }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
player.exitFullScreen()
}
Orientation.lockOrientation(.portrait) Orientation.lockOrientation(.portrait)
} }
} }

View File

@ -61,8 +61,6 @@ struct ChannelPlaylistView: View {
viewVerticalOffset = Self.hiddenOffset viewVerticalOffset = Self.hiddenOffset
} }
} }
.offset(y: viewVerticalOffset)
.animation(.easeIn(duration: 0.2), value: viewVerticalOffset)
#endif #endif
} else { } else {
BrowserPlayerControls { BrowserPlayerControls {
@ -105,7 +103,9 @@ struct ChannelPlaylistView: View {
ToolbarItem(placement: .navigation) { ToolbarItem(placement: .navigation) {
if navigationStyle == .tab { if navigationStyle == .tab {
Button("Done") { Button("Done") {
navigation.presentingPlaylist = false withAnimation {
navigation.presentingPlaylist = false
}
} }
} }
} }

View File

@ -57,8 +57,6 @@ struct ChannelVideosView: View {
viewVerticalOffset = Self.hiddenOffset viewVerticalOffset = Self.hiddenOffset
} }
} }
.offset(y: viewVerticalOffset)
.animation(.easeIn(duration: 0.2), value: viewVerticalOffset)
#endif #endif
} else { } else {
BrowserPlayerControls { BrowserPlayerControls {
@ -104,7 +102,9 @@ struct ChannelVideosView: View {
ToolbarItem(placement: .navigation) { ToolbarItem(placement: .navigation) {
if navigationStyle == .tab { if navigationStyle == .tab {
Button("Done") { Button("Done") {
navigation.presentingChannel = false withAnimation {
navigation.presentingChannel = false
}
} }
} }
} }