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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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