simplified fullscreen and orientation handling

- iPad: rotate to device orientation on startup
- fixed controls in portrait fullscreen
- iOS: don’t call setNeedsDrawing multiple times
- On iOS we call set needs drawing only once.
- Added cooldown time to MPV.Client setNeedsDrawing to avoid multiple successive calls
- make fullscreen animation smoother
- dragGesture now calls toggleFullScreenAction
- fix tvOS and macOS build

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
This commit is contained in:
Toni Förster 2024-09-01 12:42:31 +02:00
parent 9bf3df1a29
commit 322a550666
No known key found for this signature in database
GPG Key ID: 292F3E5086C83FC7
16 changed files with 213 additions and 177 deletions

View File

@ -44,7 +44,7 @@ final class PlayerSettingsGroupExporter: SettingsGroupExporter {
#endif #endif
#if os(iOS) #if os(iOS)
export["honorSystemOrientationLock"].bool = Defaults[.honorSystemOrientationLock] export["isOrientationLocked"].bool = Defaults[.isOrientationLocked]
export["enterFullscreenInLandscape"].bool = Defaults[.enterFullscreenInLandscape] export["enterFullscreenInLandscape"].bool = Defaults[.enterFullscreenInLandscape]
export["rotateToLandscapeOnEnterFullScreen"].string = Defaults[.rotateToLandscapeOnEnterFullScreen].rawValue export["rotateToLandscapeOnEnterFullScreen"].string = Defaults[.rotateToLandscapeOnEnterFullScreen].rawValue
#endif #endif

View File

@ -97,8 +97,8 @@ struct PlayerSettingsGroupImporter {
#endif #endif
#if os(iOS) #if os(iOS)
if let honorSystemOrientationLock = json["honorSystemOrientationLock"].bool { if let isOrientationLocked = json["isOrientationLocked"].bool {
Defaults[.honorSystemOrientationLock] = honorSystemOrientationLock Defaults[.isOrientationLocked] = isOrientationLocked
} }
if let enterFullscreenInLandscape = json["enterFullscreenInLandscape"].bool { if let enterFullscreenInLandscape = json["enterFullscreenInLandscape"].bool {

View File

@ -14,6 +14,8 @@ final class MPVClient: ObservableObject {
} }
private var logger = Logger(label: "mpv-client") private var logger = Logger(label: "mpv-client")
private var needsDrawingCooldown = false
private var needsDrawingWorkItem: DispatchWorkItem?
var mpv: OpaquePointer! var mpv: OpaquePointer!
var mpvGL: OpaquePointer! var mpvGL: OpaquePointer!
@ -389,10 +391,30 @@ final class MPVClient: ObservableObject {
} }
func setNeedsDrawing(_ needsDrawing: Bool) { func setNeedsDrawing(_ needsDrawing: Bool) {
// Check if we are currently in a cooldown period
guard !needsDrawingCooldown else {
logger.info("Not drawing, cooldown in progress")
return
}
logger.info("needs drawing: \(needsDrawing)") logger.info("needs drawing: \(needsDrawing)")
// Set the cooldown flag to true and cancel any existing work item
needsDrawingCooldown = true
needsDrawingWorkItem?.cancel()
#if !os(macOS) #if !os(macOS)
glView?.needsDrawing = needsDrawing glView?.needsDrawing = needsDrawing
#endif #endif
// Create a new DispatchWorkItem to reset the cooldown flag after 0.1 seconds
let workItem = DispatchWorkItem { [weak self] in
self?.needsDrawingCooldown = false
}
needsDrawingWorkItem = workItem
// Schedule the cooldown reset after 0.1 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: workItem)
} }
func command( func command(

View File

@ -56,7 +56,6 @@ final class PlayerModel: ObservableObject {
@Published var presentingPlayer = false { didSet { handlePresentationChange() } } @Published var presentingPlayer = false { didSet { handlePresentationChange() } }
@Published var activeBackend = PlayerBackendType.mpv @Published var activeBackend = PlayerBackendType.mpv
@Published var forceBackendOnPlay: PlayerBackendType? @Published var forceBackendOnPlay: PlayerBackendType?
@Published var wasFullscreen = false
var avPlayerBackend = AVPlayerBackend() var avPlayerBackend = AVPlayerBackend()
var mpvBackend = MPVBackend() var mpvBackend = MPVBackend()
@ -131,6 +130,12 @@ final class PlayerModel: ObservableObject {
#if os(iOS) #if os(iOS)
@Published var lockedOrientation: UIInterfaceOrientationMask? @Published var lockedOrientation: UIInterfaceOrientationMask?
@Published var isOrientationLocked: Bool {
didSet {
Defaults[.isOrientationLocked] = isOrientationLocked
}
}
@Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen @Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen
#endif #endif
@ -201,6 +206,16 @@ final class PlayerModel: ObservableObject {
#endif #endif
init() { init() {
#if os(iOS)
isOrientationLocked = Defaults[.isOrientationLocked]
if isOrientationLocked, Defaults[.lockPortraitWhenBrowsing] {
lockedOrientation = UIInterfaceOrientationMask.portrait
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
} else if isOrientationLocked {
lockOrientationAction()
}
#endif
#if !os(macOS) #if !os(macOS)
mpvBackend.controller = mpvController mpvBackend.controller = mpvController
mpvBackend.client = mpvController.client mpvBackend.client = mpvController.client
@ -517,7 +532,10 @@ final class PlayerModel: ObservableObject {
} }
private func handlePresentationChange() { private func handlePresentationChange() {
backend.setNeedsDrawing(presentingPlayer) #if !os(iOS)
// TODO: Check whether this is neede on tvOS and macOS
backend.setNeedsDrawing(presentingPlayer)
#endif
#if os(iOS) #if os(iOS)
if presentingPlayer, activeBackend == .appleAVPlayer, avPlayerUsesSystemControls, Constants.isIPhone { if presentingPlayer, activeBackend == .appleAVPlayer, avPlayerUsesSystemControls, Constants.isIPhone {
@ -551,8 +569,6 @@ final class PlayerModel: ObservableObject {
} else { } else {
Orientation.lockOrientation(.allButUpsideDown) Orientation.lockOrientation(.allButUpsideDown)
} }
OrientationModel.shared.stopOrientationUpdates()
#endif #endif
} }
} }
@ -659,32 +675,37 @@ final class PlayerModel: ObservableObject {
} }
func closeCurrentItem(finished: Bool = false) { func closeCurrentItem(finished: Bool = false) {
pause() guard !closing else { return }
videoBeingOpened = nil
advancing = false
forceBackendOnPlay = nil
closing = true closing = true
controls.presentingControls = false
self.prepareCurrentItemForHistory(finished: finished) if playingFullScreen { exitFullScreen() }
self.hide() Delay.by(0.3) { [weak self] in
Delay.by(0.8) { [weak self] in
guard let self else { return } guard let self else { return }
self.closePiP() pause()
videoBeingOpened = nil
advancing = false
forceBackendOnPlay = nil
withAnimation { controls.presentingControls = false
self.currentItem = nil
self.prepareCurrentItemForHistory(finished: finished)
self.hide()
Delay.by(0.7) { [weak self] in
guard let self else { return }
if playingInPictureInPicture { self.closePiP() }
withAnimation {
self.currentItem = nil
}
self.updateNowPlayingInfo()
self.backend.closeItem()
self.aspectRatio = VideoPlayerView.defaultAspectRatio
self.resetAutoplay()
self.closing = false
} }
self.updateNowPlayingInfo()
self.backend.closeItem()
self.aspectRatio = VideoPlayerView.defaultAspectRatio
self.resetAutoplay()
self.closing = false
self.playingFullScreen = false
} }
} }
@ -773,7 +794,7 @@ final class PlayerModel: ObservableObject {
} }
func toggleFullScreenAction() { func toggleFullScreenAction() {
toggleFullscreen(playingFullScreen, showControls: false) toggleFullscreen(playingFullScreen, showControls: false, initiatedByButton: true)
} }
func togglePiPAction() { func togglePiPAction() {
@ -786,20 +807,21 @@ final class PlayerModel: ObservableObject {
#if os(iOS) #if os(iOS)
var lockOrientationImage: String { var lockOrientationImage: String {
lockedOrientation.isNil ? "lock.rotation.open" : "lock.rotation" isOrientationLocked ? "lock.rotation" : "lock.rotation.open"
} }
func lockOrientationAction() { func lockOrientationAction() {
if lockedOrientation.isNil { // This makes toggling orientation lock more robust
if lockedOrientation.isNil || !isOrientationLocked {
isOrientationLocked = true
let orientationMask = OrientationTracker.shared.currentInterfaceOrientationMask let orientationMask = OrientationTracker.shared.currentInterfaceOrientationMask
lockedOrientation = orientationMask lockedOrientation = orientationMask
let orientation = OrientationTracker.shared.currentInterfaceOrientation let orientation = OrientationTracker.shared.currentInterfaceOrientation
Orientation.lockOrientation(orientationMask, andRotateTo: .landscapeLeft) Orientation.lockOrientation(orientationMask, andRotateTo: playingFullScreen ? nil : orientation)
// iOS 16 workaround
Orientation.lockOrientation(orientationMask, andRotateTo: orientation)
} else { } else {
isOrientationLocked = false
lockedOrientation = nil lockedOrientation = nil
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation) Orientation.lockOrientation(.allButUpsideDown)
} }
} }
#endif #endif
@ -985,7 +1007,14 @@ final class PlayerModel: ObservableObject {
} }
#else #else
func handleEnterForeground() { func handleEnterForeground() {
setNeedsDrawing(presentingPlayer) #if os(iOS)
OrientationTracker.shared.startDeviceOrientationTracking()
#endif
#if os(tvOS)
// TODO: Not sure if this is realy needed on tvOS, maybe it can be removed.
setNeedsDrawing(presentingPlayer)
#endif
if !musicMode, activeBackend == .mpv { if !musicMode, activeBackend == .mpv {
mpvBackend.addVideoTrackFromStream() mpvBackend.addVideoTrackFromStream()
@ -995,17 +1024,6 @@ final class PlayerModel: ObservableObject {
avPlayerBackend.bindPlayerToLayer() avPlayerBackend.bindPlayerToLayer()
} }
#if os(iOS)
if wasFullscreen {
wasFullscreen = false
DispatchQueue.main.async { [weak self] in
Delay.by(0.3) {
self?.enterFullScreen()
}
}
}
#endif
guard closePiPAndOpenPlayerOnEnteringForeground, playingInPictureInPicture else { guard closePiPAndOpenPlayerOnEnteringForeground, playingInPictureInPicture else {
return return
} }
@ -1018,6 +1036,10 @@ final class PlayerModel: ObservableObject {
} }
func handleEnterBackground() { func handleEnterBackground() {
#if os(iOS)
OrientationTracker.shared.stopDeviceOrientationTracking()
#endif
if Defaults[.pauseOnEnteringBackground], !playingInPictureInPicture, !musicMode { if Defaults[.pauseOnEnteringBackground], !playingInPictureInPicture, !musicMode {
pause() pause()
} else if !playingInPictureInPicture, activeBackend == .appleAVPlayer { } else if !playingInPictureInPicture, activeBackend == .appleAVPlayer {
@ -1025,15 +1047,6 @@ final class PlayerModel: ObservableObject {
} else if activeBackend == .mpv, !musicMode { } else if activeBackend == .mpv, !musicMode {
mpvBackend.setVideoToNo() mpvBackend.setVideoToNo()
} }
#if os(iOS)
guard playingFullScreen else { return }
wasFullscreen = playingFullScreen
DispatchQueue.main.async { [weak self] in
Delay.by(0.3) {
self?.exitFullScreen(showControls: false)
}
}
#endif
} }
#endif #endif
@ -1124,7 +1137,7 @@ final class PlayerModel: ObservableObject {
task.resume() task.resume()
} }
func toggleFullscreen(_ isFullScreen: Bool, showControls: Bool = true) { func toggleFullscreen(_ isFullScreen: Bool, showControls: Bool = true, initiatedByButton: Bool = false) {
controls.presentingControls = showControls && isFullScreen controls.presentingControls = showControls && isFullScreen
#if os(macOS) #if os(macOS)
@ -1139,15 +1152,13 @@ final class PlayerModel: ObservableObject {
avPlayerBackend.controller.enterFullScreen(animated: true) avPlayerBackend.controller.enterFullScreen(animated: true)
return return
} }
guard rotateToLandscapeOnEnterFullScreen.isRotating else { return } let lockOrientation = rotateToLandscapeOnEnterFullScreen.interfaceOrientation
if currentVideoIsLandscape { if currentVideoIsLandscape {
let delay = activeBackend == .appleAVPlayer && avPlayerUsesSystemControls ? 0.8 : 0 if initiatedByButton {
// not sure why but first rotation call is ignore so doing rotate to same orientation first Orientation.lockOrientation(self.isOrientationLocked ? (lockOrientation == .landscapeRight ? .landscapeRight : .landscapeLeft) : .landscape)
Delay.by(delay) {
let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape ? OrientationTracker.shared.currentInterfaceOrientation : self.rotateToLandscapeOnEnterFullScreen.interaceOrientation
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation)
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: orientation)
} }
let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape ? OrientationTracker.shared.currentInterfaceOrientation : self.rotateToLandscapeOnEnterFullScreen.interfaceOrientation
Orientation.lockOrientation(self.isOrientationLocked ? (lockOrientation == .landscapeRight ? .landscapeRight : .landscapeLeft) : .landscape, andRotateTo: orientation)
} }
} else { } else {
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls { if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
@ -1155,10 +1166,12 @@ final class PlayerModel: ObservableObject {
avPlayerBackend.controller.dismiss(animated: true) avPlayerBackend.controller.dismiss(animated: true)
return return
} }
let rotationOrientation = Constants.isIPhone ? UIInterfaceOrientation.portrait : nil if Defaults[.lockPortraitWhenBrowsing] {
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: rotationOrientation) lockedOrientation = UIInterfaceOrientationMask.portrait
}
let rotationOrientation = Defaults[.lockPortraitWhenBrowsing] ? UIInterfaceOrientation.portrait : nil
Orientation.lockOrientation(Defaults[.lockPortraitWhenBrowsing] ? .portrait : .allButUpsideDown, andRotateTo: rotationOrientation)
} }
#endif #endif
} }

View File

@ -93,12 +93,9 @@ extension Defaults.Keys {
static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false) static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false)
#if os(iOS) #if os(iOS)
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true) static let isOrientationLocked = Key<Bool>("isOrientationLocked", default: Constants.isIPhone)
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: Constants.isIPhone) static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: Constants.isIPhone)
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>( static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>("rotateToLandscapeOnEnterFullScreen", default: .landscapeRight)
"rotateToLandscapeOnEnterFullScreen",
default: Constants.isIPhone ? .landscapeRight : .disabled
)
#endif #endif
static let closePiPOnNavigation = Key<Bool>("closePiPOnNavigation", default: false) static let closePiPOnNavigation = Key<Bool>("closePiPOnNavigation", default: false)
@ -612,26 +609,19 @@ enum PlayerTapGestureAction: String, CaseIterable, Defaults.Serializable {
} }
enum FullScreenRotationSetting: String, CaseIterable, Defaults.Serializable { enum FullScreenRotationSetting: String, CaseIterable, Defaults.Serializable {
case disabled
case landscapeLeft case landscapeLeft
case landscapeRight case landscapeRight
#if os(iOS) #if os(iOS)
var interaceOrientation: UIInterfaceOrientation { var interfaceOrientation: UIInterfaceOrientation {
switch self { switch self {
case .landscapeLeft: case .landscapeLeft:
return .landscapeLeft return .landscapeLeft
case .landscapeRight: case .landscapeRight:
return .landscapeRight return .landscapeRight
default:
return .portrait
} }
} }
#endif #endif
var isRotating: Bool {
self != .disabled
}
} }
struct WidgetSettings: Defaults.Serializable { struct WidgetSettings: Defaults.Serializable {

View File

@ -17,12 +17,11 @@ import SwiftUI
#if os(iOS) #if os(iOS)
func playerViewController(_: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator) { func playerViewController(_: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator) {
guard rotateToLandscapeOnEnterFullScreen.isRotating else { return }
if PlayerModel.shared.currentVideoIsLandscape { if PlayerModel.shared.currentVideoIsLandscape {
let delay = PlayerModel.shared.activeBackend == .appleAVPlayer && avPlayerUsesSystemControls ? 0.8 : 0 let delay = PlayerModel.shared.activeBackend == .appleAVPlayer && avPlayerUsesSystemControls ? 0.8 : 0
// not sure why but first rotation call is ignore so doing rotate to same orientation first // not sure why but first rotation call is ignore so doing rotate to same orientation first
Delay.by(delay) { Delay.by(delay) {
let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape ? OrientationTracker.shared.currentInterfaceOrientation : self.rotateToLandscapeOnEnterFullScreen.interaceOrientation let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape ? OrientationTracker.shared.currentInterfaceOrientation : self.rotateToLandscapeOnEnterFullScreen.interfaceOrientation
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation) Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation)
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: orientation) Orientation.lockOrientation(.allButUpsideDown, andRotateTo: orientation)
} }
@ -37,8 +36,6 @@ import SwiftUI
} }
if !context.isCancelled { if !context.isCancelled {
#if os(iOS) #if os(iOS)
self.player.lockedOrientation = nil
if Constants.isIPhone { if Constants.isIPhone {
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait) Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
} }

View File

@ -389,7 +389,7 @@ struct PlayerControls: View {
#if os(iOS) #if os(iOS)
private var lockOrientationButton: some View { private var lockOrientationButton: some View {
button("Lock Rotation", systemImage: player.lockOrientationImage, active: !player.lockedOrientation.isNil, action: player.lockOrientationAction) button("Lock Rotation", systemImage: player.lockOrientationImage, active: player.isOrientationLocked, action: player.lockOrientationAction)
} }
#endif #endif

View File

@ -64,11 +64,7 @@ extension VideoPlayerView {
// Toggle fullscreen on upward drag only when not disabled // Toggle fullscreen on upward drag only when not disabled
if verticalDrag < -50 { if verticalDrag < -50 {
if player.playingFullScreen { player.toggleFullScreenAction()
player.exitFullScreen(showControls: false)
} else {
player.enterFullScreen()
}
disableGestureTemporarily() disableGestureTemporarily()
return return
} }

View File

@ -158,7 +158,7 @@ struct VideoActions: View {
actionButton("PiP", systemImage: player.pipImage, active: player.playingInPictureInPicture, action: player.togglePiPAction) actionButton("PiP", systemImage: player.pipImage, active: player.playingInPictureInPicture, action: player.togglePiPAction)
#if os(iOS) #if os(iOS)
case .lockOrientation: case .lockOrientation:
actionButton("Lock", systemImage: player.lockOrientationImage, active: player.lockedOrientation != nil, action: player.lockOrientationAction) actionButton("Lock", systemImage: player.lockOrientationImage, active: player.isOrientationLocked, action: player.lockOrientationAction)
#endif #endif
case .restart: case .restart:
actionButton("Replay", systemImage: "backward.end.fill", action: player.replayAction) actionButton("Replay", systemImage: "backward.end.fill", action: player.replayAction)

View File

@ -111,9 +111,6 @@ struct VideoPlayerView: View {
.onChange(of: geometry.size) { _ in .onChange(of: geometry.size) { _ in
self.playerSize = geometry.size self.playerSize = geometry.size
} }
.onChange(of: fullScreenDetails) { value in
player.backend.setNeedsDrawing(!value)
}
#if os(iOS) #if os(iOS)
.onChange(of: player.presentingPlayer) { newValue in .onChange(of: player.presentingPlayer) { newValue in
if newValue { if newValue {
@ -127,19 +124,6 @@ struct VideoPlayerView: View {
} }
#endif #endif
viewDragOffset = 0 viewDragOffset = 0
Delay.by(0.2) {
orientationModel.configureOrientationUpdatesBasedOnAccelerometer()
if let orientationMask = player.lockedOrientation {
Orientation.lockOrientation(
orientationMask,
andRotateTo: orientationMask == .landscapeLeft ? .landscapeLeft : orientationMask == .landscapeRight ? .landscapeRight : .portrait
)
} else {
Orientation.lockOrientation(.allButUpsideDown)
}
}
} }
.onAnimationCompleted(for: viewDragOffset) { .onAnimationCompleted(for: viewDragOffset) {
guard !dragGestureState else { return } guard !dragGestureState else { return }
@ -313,11 +297,14 @@ struct VideoPlayerView: View {
playerSize: player.playerSize, playerSize: player.playerSize,
fullScreen: fullScreenDetails fullScreen: fullScreenDetails
)) ))
#if os(macOS)
// TODO: Check whether this is needed on macOS.
.onDisappear { .onDisappear {
if player.presentingPlayer { if player.presentingPlayer {
player.setNeedsDrawing(true) player.setNeedsDrawing(true)
} }
} }
#endif
.id(player.currentVideo?.cacheKey) .id(player.currentVideo?.cacheKey)
.transition(.opacity) .transition(.opacity)
} else { } else {

View File

@ -10,6 +10,7 @@ struct BrowsingSettings: View {
@Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges @Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges
@Default(.keepChannelsWithUnwatchedFeedOnTop) private var keepChannelsWithUnwatchedFeedOnTop @Default(.keepChannelsWithUnwatchedFeedOnTop) private var keepChannelsWithUnwatchedFeedOnTop
#if os(iOS) #if os(iOS)
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
@Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing @Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing
@Default(.showDocuments) private var showDocuments @Default(.showDocuments) private var showDocuments
#endif #endif
@ -161,14 +162,18 @@ struct BrowsingSettings: View {
#if os(iOS) #if os(iOS)
Toggle("Show Documents", isOn: $showDocuments) Toggle("Show Documents", isOn: $showDocuments)
Toggle("Lock portrait mode", isOn: $lockPortraitWhenBrowsing) if Constants.isIPad {
.onChange(of: lockPortraitWhenBrowsing) { lock in Toggle("Lock portrait mode", isOn: $lockPortraitWhenBrowsing)
if lock { .onChange(of: lockPortraitWhenBrowsing) { lock in
Orientation.lockOrientation(.portrait, andRotateTo: .portrait) if lock {
} else { enterFullscreenInLandscape = true
Orientation.lockOrientation(.allButUpsideDown) Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
} else {
enterFullscreenInLandscape = false
Orientation.lockOrientation(.allButUpsideDown)
}
} }
} }
#endif #endif
if !accounts.isEmpty { if !accounts.isEmpty {

View File

@ -18,8 +18,8 @@ struct PlayerSettings: View {
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer @Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
@Default(.closeVideoOnEOF) private var closeVideoOnEOF @Default(.closeVideoOnEOF) private var closeVideoOnEOF
#if os(iOS) #if os(iOS)
@Default(.honorSystemOrientationLock) private var honorSystemOrientationLock
@Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape @Default(.enterFullscreenInLandscape) private var enterFullscreenInLandscape
@Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing
@Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen @Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen
#endif #endif
@Default(.closePiPOnNavigation) private var closePiPOnNavigation @Default(.closePiPOnNavigation) private var closePiPOnNavigation
@ -87,7 +87,7 @@ struct PlayerSettings: View {
} }
pauseOnHidingPlayerToggle pauseOnHidingPlayerToggle
closeVideoOnEOFToggle closeVideoOnEOFToggle
#if !os(tvOS) #if os(macOS)
exitFullscreenOnEOFToggle exitFullscreenOnEOFToggle
#endif #endif
#if !os(macOS) #if !os(macOS)
@ -202,11 +202,12 @@ struct PlayerSettings: View {
#endif #endif
#if os(iOS) #if os(iOS)
Section(header: SettingsHeader(text: "Orientation".localized())) { Section(header: SettingsHeader(text: "Fullscreen".localized())) {
if idiom == .pad { if Constants.isIPad {
enterFullscreenInLandscapeToggle enterFullscreenInLandscapeToggle
} }
honorSystemOrientationLockToggle
exitFullscreenOnEOFToggle
rotateToLandscapeOnEnterFullScreenPicker rotateToLandscapeOnEnterFullScreenPicker
} }
#endif #endif
@ -318,20 +319,15 @@ struct PlayerSettings: View {
#endif #endif
#if os(iOS) #if os(iOS)
private var honorSystemOrientationLockToggle: some View {
Toggle("Honor orientation lock", isOn: $honorSystemOrientationLock)
.disabled(!enterFullscreenInLandscape)
}
private var enterFullscreenInLandscapeToggle: some View { private var enterFullscreenInLandscapeToggle: some View {
Toggle("Enter fullscreen in landscape", isOn: $enterFullscreenInLandscape) Toggle("Enter fullscreen in landscape orientation", isOn: $enterFullscreenInLandscape)
.disabled(lockPortraitWhenBrowsing)
} }
private var rotateToLandscapeOnEnterFullScreenPicker: some View { private var rotateToLandscapeOnEnterFullScreenPicker: some View {
Picker("Rotate when entering fullscreen on landscape video", selection: $rotateToLandscapeOnEnterFullScreen) { Picker("Default orientation", selection: $rotateToLandscapeOnEnterFullScreen) {
Text("Landscape left").tag(FullScreenRotationSetting.landscapeLeft) Text("Landscape left").tag(FullScreenRotationSetting.landscapeLeft)
Text("Landscape right").tag(FullScreenRotationSetting.landscapeRight) Text("Landscape right").tag(FullScreenRotationSetting.landscapeRight)
Text("No rotation").tag(FullScreenRotationSetting.disabled)
} }
.modifier(SettingsPickerModifier()) .modifier(SettingsPickerModifier())
} }

View File

@ -204,9 +204,14 @@ struct YatteeApp: App {
} }
#if os(iOS) #if os(iOS)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
if Defaults[.lockPortraitWhenBrowsing] { if Defaults[.lockPortraitWhenBrowsing] {
Orientation.lockOrientation(.all, andRotateTo: .portrait) Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
} else {
let rotationOrientation =
OrientationTracker.shared.currentDeviceOrientation.rawValue == 4 ? UIInterfaceOrientation.landscapeRight :
(OrientationTracker.shared.currentDeviceOrientation.rawValue == 3 ? UIInterfaceOrientation.landscapeLeft : UIInterfaceOrientation.portrait)
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: rotationOrientation)
} }
} }
#endif #endif
@ -225,6 +230,17 @@ struct YatteeApp: App {
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
self.migrateQualityProfiles() self.migrateQualityProfiles()
} }
#if os(iOS)
DispatchQueue.global(qos: .userInitiated).async {
self.migrateRotateToLandscapeOnEnterFullScreen()
}
DispatchQueue.global(qos: .userInitiated).async {
self.migrateLockPortraitWhenBrowsing()
}
#endif
} }
} }
@ -253,6 +269,22 @@ struct YatteeApp: App {
} }
} }
#if os(iOS)
func migrateRotateToLandscapeOnEnterFullScreen() {
if Defaults[.rotateToLandscapeOnEnterFullScreen] != .landscapeRight || Defaults[.rotateToLandscapeOnEnterFullScreen] != .landscapeLeft {
Defaults[.rotateToLandscapeOnEnterFullScreen] = .landscapeRight
}
}
func migrateLockPortraitWhenBrowsing() {
if Constants.isIPhone {
Defaults[.lockPortraitWhenBrowsing] = true
} else if Constants.isIPad, Defaults[.lockPortraitWhenBrowsing] {
Defaults[.enterFullscreenInLandscape] = true
}
}
#endif
var navigationStyle: NavigationStyle { var navigationStyle: NavigationStyle {
#if os(iOS) #if os(iOS)
return horizontalSizeClass == .compact ? .tab : .sidebar return horizontalSizeClass == .compact ? .tab : .sidebar

View File

@ -4366,7 +4366,9 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIRequiresFullScreen = YES; INFOPLIST_KEY_UIRequiresFullScreen = YES;
INFOPLIST_KEY_UIStatusBarHidden = NO; INFOPLIST_KEY_UIStatusBarHidden = NO;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UIStatusBarStyle = "";
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -4415,7 +4417,9 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIRequiresFullScreen = YES; INFOPLIST_KEY_UIRequiresFullScreen = YES;
INFOPLIST_KEY_UIStatusBarHidden = NO; INFOPLIST_KEY_UIStatusBarHidden = NO;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortraitUpsideDown"; INFOPLIST_KEY_UIStatusBarStyle = "";
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 14.0; IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",

View File

@ -1,16 +1,17 @@
import AVFoundation import AVFoundation
import Defaults
import Foundation import Foundation
import Logging import Logging
import UIKit import UIKit
final class AppDelegate: UIResponder, UIApplicationDelegate { final class AppDelegate: UIResponder, UIApplicationDelegate {
var orientationLock = UIInterfaceOrientationMask.all var orientationLock = UIInterfaceOrientationMask.allButUpsideDown
private var logger = Logger(label: "stream.yattee.app.delegalate") private var logger = Logger(label: "stream.yattee.app.delegate")
private(set) static var instance: AppDelegate! private(set) static var instance: AppDelegate!
func application(_: UIApplication, supportedInterfaceOrientationsFor _: UIWindow?) -> UIInterfaceOrientationMask { func application(_: UIApplication, supportedInterfaceOrientationsFor _: UIWindow?) -> UIInterfaceOrientationMask {
orientationLock return orientationLock
} }
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // swiftlint:disable:this discouraged_optional_collection func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // swiftlint:disable:this discouraged_optional_collection
@ -19,6 +20,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
#if !os(macOS) #if !os(macOS)
UIViewController.swizzleHomeIndicatorProperty() UIViewController.swizzleHomeIndicatorProperty()
OrientationTracker.shared.startDeviceOrientationTracking() OrientationTracker.shared.startDeviceOrientationTracking()
OrientationModel.shared.startOrientationUpdates()
// Configure the audio session for playback // Configure the audio session for playback
do { do {

View File

@ -1,10 +1,12 @@
import Defaults import Defaults
import Foundation import Foundation
import Logging
import Repeat import Repeat
import SwiftUI import SwiftUI
final class OrientationModel { final class OrientationModel {
static var shared = OrientationModel() static var shared = OrientationModel()
let logger = Logger(label: "stream.yattee.orientation.model")
var orientation = UIInterfaceOrientation.portrait var orientation = UIInterfaceOrientation.portrait
var lastOrientation: UIInterfaceOrientation? var lastOrientation: UIInterfaceOrientation?
@ -13,79 +15,69 @@ final class OrientationModel {
private var player = PlayerModel.shared private var player = PlayerModel.shared
func configureOrientationUpdatesBasedOnAccelerometer() { func startOrientationUpdates() {
let currentOrientation = OrientationTracker.shared.currentInterfaceOrientation // Ensure the orientation observer is active
if currentOrientation.isLandscape,
Defaults[.enterFullscreenInLandscape],
!Defaults[.honorSystemOrientationLock],
!player.playingFullScreen,
!player.currentItem.isNil,
player.lockedOrientation.isNil || player.lockedOrientation!.contains(.landscape),
!player.playingInPictureInPicture,
player.presentingPlayer
{
DispatchQueue.main.async {
self.player.controls.presentingControls = false
self.player.enterFullScreen(showControls: false)
}
player.onPresentPlayer.append {
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: currentOrientation)
}
}
orientationObserver = NotificationCenter.default.addObserver( orientationObserver = NotificationCenter.default.addObserver(
forName: OrientationTracker.deviceOrientationChangedNotification, forName: OrientationTracker.deviceOrientationChangedNotification,
object: nil, object: nil,
queue: .main queue: .main
) { _ in ) { _ in
guard !Defaults[.honorSystemOrientationLock], self.logger.info("Notification received: Device orientation changed.")
self.player.presentingPlayer,
!self.player.playingInPictureInPicture, // We only allow .portrait and are not showing the player
self.player.lockedOrientation.isNil guard (!self.player.presentingPlayer && !Defaults[.lockPortraitWhenBrowsing]) || self.player.presentingPlayer
else { else {
return return
} }
let orientation = OrientationTracker.shared.currentInterfaceOrientation let orientation = OrientationTracker.shared.currentInterfaceOrientation
self.logger.info("Current interface orientation: \(orientation)")
guard self.lastOrientation != orientation else { // Always update lastOrientation to keep track of the latest state
if self.lastOrientation != orientation {
self.lastOrientation = orientation
self.logger.info("Orientation changed to: \(orientation)")
} else {
self.logger.info("Orientation has not changed.")
}
// Only take action if the player is active and presenting
guard (!self.player.isOrientationLocked && !self.player.playingInPictureInPicture) || (!Defaults[.lockPortraitWhenBrowsing] && !self.player.presentingPlayer) || (!Defaults[.lockPortraitWhenBrowsing] && self.player.presentingPlayer && !self.player.isOrientationLocked)
else {
self.logger.info("Only updating orientation without actions.")
return return
} }
self.lastOrientation = orientation
DispatchQueue.main.async { DispatchQueue.main.async {
guard Defaults[.enterFullscreenInLandscape],
self.player.presentingPlayer
else {
return
}
self.orientationDebouncer.callback = { self.orientationDebouncer.callback = {
DispatchQueue.main.async { DispatchQueue.main.async {
if orientation.isLandscape { if orientation.isLandscape {
self.player.controls.presentingControls = false if Defaults[.enterFullscreenInLandscape], self.player.presentingPlayer {
self.player.enterFullScreen(showControls: false) self.logger.info("Entering fullscreen because orientation is landscape.")
self.player.controls.presentingControls = false
self.player.enterFullScreen(showControls: false)
}
Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation) Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation)
} else { } else {
self.player.exitFullScreen(showControls: false) self.logger.info("Exiting fullscreen because orientation is portrait.")
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait) if self.player.playingFullScreen {
self.player.exitFullScreen(showControls: false)
}
if Defaults[.lockPortraitWhenBrowsing] {
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
} else {
Orientation.lockOrientation(OrientationTracker.shared.currentInterfaceOrientationMask, andRotateTo: orientation)
}
} }
} }
} }
self.orientationDebouncer.call() self.orientationDebouncer.call()
} }
} }
} }
func stopOrientationUpdates() {
guard let observer = orientationObserver else { return }
NotificationCenter.default.removeObserver(observer)
}
func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation: UIInterfaceOrientation? = nil) { func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation: UIInterfaceOrientation? = nil) {
logger.info("Locking orientation to: \(orientation), rotating to: \(String(describing: rotateOrientation)).")
if let rotateOrientation { if let rotateOrientation {
self.orientation = rotateOrientation self.orientation = rotateOrientation
lastOrientation = rotateOrientation lastOrientation = rotateOrientation