Compare commits

..

30 Commits

Author SHA1 Message Date
Arkadiusz Fal
b0264aaabe Bump build number to 195 2024-09-05 23:01:29 +02:00
Arkadiusz Fal
035f3503c4 Update CHANGELOG 2024-09-05 23:01:19 +02:00
Arkadiusz Fal
e3ac11c172 Merge pull request #786 from stonerl/simplified-fullscreen-and-orientation
iOS: Simplified fullscreen and orientation
2024-09-05 22:59:54 +02:00
Arkadiusz Fal
7aed6ac0d9 Merge pull request #799 from stonerl/controls-background
player controls: add background opacity selection
2024-09-05 22:54:30 +02:00
Arkadiusz Fal
457c0ce7b3 Merge pull request #797 from stonerl/shorts-resolutions
add missing Shorts resolutions
2024-09-05 22:53:42 +02:00
Arkadiusz Fal
747baf3edd Merge pull request #801 from stonerl/O2-for-macOS
use -O1 on macOS
2024-09-05 22:53:26 +02:00
Arkadiusz Fal
cd24a0322f Merge pull request #802 from stonerl/buttons-interfere-with-search
macOS: only apply player shortcuts when window is active
2024-09-05 22:53:16 +02:00
Toni Förster
d525a22215 macOS only apply player shortcuts when window is active 2024-09-05 21:53:25 +02:00
Toni Förster
322a550666 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>
2024-09-05 18:17:14 +02:00
Toni Förster
98fa0b98e5 use -O1 on macOS
On macOS optimisation level -O3 seems to be a bit aggressive and can cause crashes when opening MPV.

- fixes #783

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-05 17:35:52 +02:00
Toni Förster
5313e4ead0 player controls: add background opacity selection
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-05 15:14:39 +02:00
Toni Förster
fa7b897e76 add missing Shorts resolutions
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-04 12:44:43 +02:00
Arkadiusz Fal
9bf3df1a29 Bump build number to 194 2024-09-04 09:38:15 +02:00
Arkadiusz Fal
34a805b986 Fix build issue 2024-09-04 09:37:38 +02:00
Arkadiusz Fal
36f680be62 Update CHANGELOG 2024-09-04 09:36:05 +02:00
Arkadiusz Fal
a27ab02433 Update dependencies 2024-09-04 09:33:23 +02:00
Arkadiusz Fal
59dd0785b3 Merge pull request #778 from stonerl/swipe-up-for-fullscreen
Gestures: swipe up toggles fullscreen
2024-09-04 09:16:23 +02:00
Arkadiusz Fal
d7be915e7e Merge pull request #779 from stonerl/better-audio-ducking
Better audio ducking
2024-09-04 09:15:35 +02:00
Arkadiusz Fal
3752f67630 Merge pull request #780 from stonerl/add-overlay-to-video-context-menu
don’t open video when dismissing context menu
2024-09-04 09:15:03 +02:00
Arkadiusz Fal
dfe7565138 Merge pull request #789 from stonerl/fix-picture-in-picture
fix picture in picture
2024-09-04 09:14:34 +02:00
Arkadiusz Fal
4d02538cb9 Merge pull request #793 from stonerl/mpv-remove-video-layer
mpv: remove video layer when entering background
2024-09-04 09:14:05 +02:00
Arkadiusz Fal
3229528a09 Merge pull request #794 from stonerl/enable-o3-optimization
enable -O3
2024-09-04 09:13:23 +02:00
Arkadiusz Fal
fffc4f4a5f Merge pull request #791 from stonerl/hi-res-invidious-logo
hi-res invidious logos
2024-09-04 09:13:01 +02:00
Toni Förster
e85bfe5007 enable -O3
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-03 21:40:48 +02:00
Toni Förster
b00b733fd5 don’t open video when dismissing context menu
fixes #510

fix tvOS build

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-03 21:21:34 +02:00
Toni Förster
119c663436 Gestures: swipe up toggles fullscreen 2024-09-03 21:20:56 +02:00
Toni Förster
e8fcee23ef make audio ducking and interruption more robust
Signed-off-by: Toni Förster <toni.foerster@gmail.com>

fix audio ducking and bluetooth play/pause

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-03 21:19:30 +02:00
Toni Förster
d56ef74a99 fix picture in picture
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-03 21:17:20 +02:00
Toni Förster
98f5b1a22b mpv: remove video layer when entering background
- fixes #792

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-03 14:58:18 +02:00
Toni Förster
f0b7bd3ab8 hi-res invidious logos
second try

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-09-03 01:01:52 +02:00
30 changed files with 457 additions and 332 deletions

View File

@@ -1,13 +1,9 @@
## Build 193
* Invidious: propper HTTP basic auth support by @stonerl in https://github.com/yattee/yattee/pull/762
* Apply correct orientation by @stonerl in https://github.com/yattee/yattee/pull/770
* Circular Invidious logo by @stonerl in https://github.com/yattee/yattee/pull/769
* Video Thumbnails: retry 3 times fetching from URL by @stonerl in https://github.com/yattee/yattee/pull/768
* Make thumbnail fill the view in music mode by @stonerl in https://github.com/yattee/yattee/pull/766
* Changes to defaults by @stonerl in https://github.com/yattee/yattee/pull/767
* Fixed fullscreen handling for backgrounding by @stonerl in https://github.com/yattee/yattee/pull/772
* Update now playing info when using system controls Partial fix for 503 by @stonerl in https://github.com/yattee/yattee/pull/765
* Fix crash on HLS live playback by @stonerl in https://github.com/yattee/yattee/pull/775
## Build 195
* iOS: Simplified fullscreen and orientation by @stonerl in https://github.com/yattee/yattee/pull/786
* macOS: only apply player shortcuts when window is active by @stonerl in https://github.com/yattee/yattee/pull/802
* player controls: add background opacity selection by @stonerl in https://github.com/yattee/yattee/pull/799
* add missing Shorts resolutions by @stonerl in https://github.com/yattee/yattee/pull/797
* use -O1 on macOS by @stonerl in https://github.com/yattee/yattee/pull/801
## Previous builds
* Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein)
@@ -26,6 +22,22 @@
* Add import export of missing settings
* macOS: Fix settings windows layout
* Fix seek OSD layout on tvOS, revert OSD position
* Gestures: swipe up toggles fullscreen by @stonerl in https://github.com/yattee/yattee/pull/778
* dont open video when dismissing context menu by @stonerl in https://github.com/yattee/yattee/pull/780
* mpv: remove video layer when entering background by @stonerl in https://github.com/yattee/yattee/pull/793
* hi-res invidious logos by @stonerl in https://github.com/yattee/yattee/pull/791
* enable -O3 by @stonerl in https://github.com/yattee/yattee/pull/794
* Better audio ducking by @stonerl in https://github.com/yattee/yattee/pull/779
* fix picture in picture by @stonerl in https://github.com/yattee/yattee/pull/789
* Invidious: propper HTTP basic auth support by @stonerl in https://github.com/yattee/yattee/pull/762
* Apply correct orientation by @stonerl in https://github.com/yattee/yattee/pull/770
* Circular Invidious logo by @stonerl in https://github.com/yattee/yattee/pull/769
* Video Thumbnails: retry 3 times fetching from URL by @stonerl in https://github.com/yattee/yattee/pull/768
* Make thumbnail fill the view in music mode by @stonerl in https://github.com/yattee/yattee/pull/766
* Changes to defaults by @stonerl in https://github.com/yattee/yattee/pull/767
* Fixed fullscreen handling for backgrounding by @stonerl in https://github.com/yattee/yattee/pull/772
* Update now playing info when using system controls Partial fix for 503 by @stonerl in https://github.com/yattee/yattee/pull/765
* Fix crash on HLS live playback by @stonerl in https://github.com/yattee/yattee/pull/775
* Fix mpv crashing on macOS by @stonerl in https://github.com/yattee/yattee/pull/754
* Refreshed icons for iOS and macOS by @stonerl in https://github.com/yattee/yattee/pull/752
* Add new MPVKit repo by @stonerl in https://github.com/yattee/yattee/pull/753

View File

@@ -11,16 +11,16 @@ GEM
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.970.0)
aws-sdk-core (3.202.2)
aws-sdk-core (3.203.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.88.0)
aws-sdk-core (~> 3, >= 3.201.0)
aws-sdk-kms (1.89.0)
aws-sdk-core (~> 3, >= 3.203.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.159.0)
aws-sdk-core (~> 3, >= 3.201.0)
aws-sdk-s3 (1.160.0)
aws-sdk-core (~> 3, >= 3.203.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.9.1)
@@ -171,8 +171,7 @@ GEM
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.3.6)
strscan
rexml (3.3.7)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
@@ -185,7 +184,6 @@ GEM
simctl (1.6.10)
CFPropertyList
naturally
strscan (3.1.0)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)

View File

@@ -10,6 +10,7 @@ final class ConstrolsSettingsGroupExporter: SettingsGroupExporter {
"seekGestureSpeed": Defaults[.seekGestureSpeed],
"playerControlsLayout": Defaults[.playerControlsLayout].rawValue,
"fullScreenPlayerControlsLayout": Defaults[.fullScreenPlayerControlsLayout].rawValue,
"playerControlsBackgroundOpacity": Defaults[.playerControlsBackgroundOpacity],
"systemControlsCommands": Defaults[.systemControlsCommands].rawValue,
"buttonBackwardSeekDuration": Defaults[.buttonBackwardSeekDuration],
"buttonForwardSeekDuration": Defaults[.buttonForwardSeekDuration],

View File

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

View File

@@ -33,6 +33,10 @@ struct ConstrolsSettingsGroupImporter {
Defaults[.fullScreenPlayerControlsLayout] = fullScreenPlayerControlsLayout
}
if let playerControlsBackgroundOpacity = json["playerControlsBackgroundOpacity"].double {
Defaults[.playerControlsBackgroundOpacity] = playerControlsBackgroundOpacity
}
if let systemControlsCommandsString = json["systemControlsCommands"].string,
let systemControlsCommands = SystemControlsCommands(rawValue: systemControlsCommandsString)
{

View File

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

View File

@@ -364,7 +364,11 @@ final class AVPlayerBackend: PlayerBackend {
let startPlaying = {
#if !os(macOS)
try? AVAudioSession.sharedInstance().setActive(true)
do {
try AVAudioSession.sharedInstance().setActive(true)
} catch {
self.logger.error("Error setting up audio session: \(error)")
}
#endif
self.setRate(self.model.currentRate)

View File

@@ -248,13 +248,6 @@ final class MPVBackend: PlayerBackend {
#if !os(macOS)
do {
try AVAudioSession.sharedInstance().setActive(true)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handleAudioSessionInterruption(_:)),
name: AVAudioSession.interruptionNotification,
object: nil
)
} catch {
self.logger.error("Error setting up audio session: \(error)")
}
@@ -649,33 +642,4 @@ final class MPVBackend: PlayerBackend {
logger.info("MPV backend received unhandled property: \(name)")
}
}
#if !os(macOS)
@objc func handleAudioSessionInterruption(_ notification: Notification) {
logger.info("Audio session interruption received.")
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt
else {
logger.info("AVAudioSessionInterruptionTypeKey is missing or not a UInt in userInfo.")
return
}
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
logger.info("Interruption type received: \(String(describing: type))")
switch type {
case .began:
pause()
logger.info("Audio session interrupted.")
default:
break
}
}
deinit {
NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
}
#endif
}

View File

@@ -14,6 +14,8 @@ final class MPVClient: ObservableObject {
}
private var logger = Logger(label: "mpv-client")
private var needsDrawingCooldown = false
private var needsDrawingWorkItem: DispatchWorkItem?
var mpv: OpaquePointer!
var mpvGL: OpaquePointer!
@@ -389,10 +391,30 @@ final class MPVClient: ObservableObject {
}
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)")
// Set the cooldown flag to true and cancel any existing work item
needsDrawingCooldown = true
needsDrawingWorkItem?.cancel()
#if !os(macOS)
glView?.needsDrawing = needsDrawing
#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(

View File

@@ -47,7 +47,7 @@ final class PlayerModel: ObservableObject {
static var shared = PlayerModel()
let logger = Logger(label: "stream.yattee.app")
let logger = Logger(label: "stream.yattee.player.model")
var playerItem: AVPlayerItem?
@@ -56,7 +56,6 @@ final class PlayerModel: ObservableObject {
@Published var presentingPlayer = false { didSet { handlePresentationChange() } }
@Published var activeBackend = PlayerBackendType.mpv
@Published var forceBackendOnPlay: PlayerBackendType?
@Published var wasFullscreen = false
var avPlayerBackend = AVPlayerBackend()
var mpvBackend = MPVBackend()
@@ -131,6 +130,12 @@ final class PlayerModel: ObservableObject {
#if os(iOS)
@Published var lockedOrientation: UIInterfaceOrientationMask?
@Published var isOrientationLocked: Bool {
didSet {
Defaults[.isOrientationLocked] = isOrientationLocked
}
}
@Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen
#endif
@@ -201,9 +206,27 @@ final class PlayerModel: ObservableObject {
#endif
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)
mpvBackend.controller = mpvController
mpvBackend.client = mpvController.client
// Register for audio session interruption notifications
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioSessionInterruption(_:)),
name: AVAudioSession.interruptionNotification,
object: nil
)
#endif
playbackMode = Defaults[.playbackMode]
@@ -220,6 +243,12 @@ final class PlayerModel: ObservableObject {
currentRate = playerRate
}
#if !os(macOS)
deinit {
NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
}
#endif
func show() {
#if os(macOS)
if presentingPlayer {
@@ -503,7 +532,10 @@ final class PlayerModel: ObservableObject {
}
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 presentingPlayer, activeBackend == .appleAVPlayer, avPlayerUsesSystemControls, Constants.isIPhone {
@@ -537,8 +569,6 @@ final class PlayerModel: ObservableObject {
} else {
Orientation.lockOrientation(.allButUpsideDown)
}
OrientationModel.shared.stopOrientationUpdates()
#endif
}
}
@@ -645,32 +675,37 @@ final class PlayerModel: ObservableObject {
}
func closeCurrentItem(finished: Bool = false) {
pause()
videoBeingOpened = nil
advancing = false
forceBackendOnPlay = nil
guard !closing else { return }
closing = true
controls.presentingControls = false
self.prepareCurrentItemForHistory(finished: finished)
if playingFullScreen { exitFullScreen() }
self.hide()
Delay.by(0.8) { [weak self] in
Delay.by(0.3) { [weak self] in
guard let self else { return }
self.closePiP()
pause()
videoBeingOpened = nil
advancing = false
forceBackendOnPlay = nil
withAnimation {
self.currentItem = nil
controls.presentingControls = false
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
}
}
@@ -679,38 +714,24 @@ final class PlayerModel: ObservableObject {
avPlayerBackend.startPictureInPictureOnPlay = false
avPlayerBackend.startPictureInPictureOnSwitch = false
if activeBackend == .appleAVPlayer {
guard activeBackend != .appleAVPlayer else {
avPlayerBackend.tryStartingPictureInPicture()
return
}
// First, we need to create an array with supported formats.
let formatOrderPiP: [QualityProfile.Format] = [.stream, .hls]
avPlayerBackend.startPictureInPictureOnSwitch = true
guard let video = currentVideo else { return }
guard let stream = avPlayerBackend.bestPlayable(availableStreams, maxResolution: .hd720p30, formatOrder: formatOrderPiP) else { return }
if avPlayerBackend.video == video {
if activeBackend != .appleAVPlayer {
avPlayerBackend.startPictureInPictureOnSwitch = true
}
changeActiveBackend(from: activeBackend, to: .appleAVPlayer)
} else {
avPlayerBackend.startPictureInPictureOnPlay = true
playStream(stream, of: video, preservingTime: true, upgrading: true, withBackend: avPlayerBackend)
}
var retryCount = 0
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
if let pipController = self?.pipController, pipController.isPictureInPictureActive, self?.avPlayerBackend.isPlaying == true {
self?.exitFullScreen()
self?.controls.objectWillChange.send()
timer.invalidate()
} else if retryCount < 3, self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.startPictureInPictureOnSwitch == false {
// If PiP didn't start, try starting it again up to 3 times,
self?.avPlayerBackend.startPictureInPictureOnSwitch = true
self?.avPlayerBackend.tryStartingPictureInPicture()
retryCount += 1
saveTime {
self.changeActiveBackend(from: .mpv, to: .appleAVPlayer)
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
if let pipController = self?.pipController, pipController.isPictureInPictureActive, self?.avPlayerBackend.isPlaying == true {
self?.exitFullScreen()
self?.controls.objectWillChange.send()
timer.invalidate()
} else if self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.startPictureInPictureOnSwitch == false {
self?.avPlayerBackend.startPictureInPictureOnSwitch = true
self?.avPlayerBackend.tryStartingPictureInPicture()
}
}
}
}
@@ -740,19 +761,27 @@ final class PlayerModel: ObservableObject {
show()
#endif
if previousActiveBackend == .mpv {
saveTime {
self.changeActiveBackend(from: self.activeBackend, to: .mpv, isInClosePip: true)
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
if self?.activeBackend == .mpv, self?.mpvBackend.isPlaying == true {
self?.backend.closePiP()
self?.controls.resetTimer()
timer.invalidate()
}
avPlayerBackend.closePiP()
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
if self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.isPlaying == true, self?.playingInPictureInPicture == false {
timer.invalidate()
}
}
guard previousActiveBackend == .mpv else { return }
saveTime {
self.changeActiveBackend(from: .appleAVPlayer, to: .mpv, isInClosePip: true)
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
if self?.activeBackend == .mpv, self?.mpvBackend.isPlaying == true {
timer.invalidate()
}
}
} else {
backend.closePiP()
}
// We need to remove the itme from the player, if not it will be displayed when next video goe to PiP.
Delay.by(1.0) {
self.avPlayerBackend.closeItem()
}
}
@@ -765,7 +794,7 @@ final class PlayerModel: ObservableObject {
}
func toggleFullScreenAction() {
toggleFullscreen(playingFullScreen, showControls: false)
toggleFullscreen(playingFullScreen, showControls: false, initiatedByButton: true)
}
func togglePiPAction() {
@@ -778,20 +807,21 @@ final class PlayerModel: ObservableObject {
#if os(iOS)
var lockOrientationImage: String {
lockedOrientation.isNil ? "lock.rotation.open" : "lock.rotation"
isOrientationLocked ? "lock.rotation" : "lock.rotation.open"
}
func lockOrientationAction() {
if lockedOrientation.isNil {
// This makes toggling orientation lock more robust
if lockedOrientation.isNil || !isOrientationLocked {
isOrientationLocked = true
let orientationMask = OrientationTracker.shared.currentInterfaceOrientationMask
lockedOrientation = orientationMask
let orientation = OrientationTracker.shared.currentInterfaceOrientation
Orientation.lockOrientation(orientationMask, andRotateTo: .landscapeLeft)
// iOS 16 workaround
Orientation.lockOrientation(orientationMask, andRotateTo: orientation)
Orientation.lockOrientation(orientationMask, andRotateTo: playingFullScreen ? nil : orientation)
} else {
isOrientationLocked = false
lockedOrientation = nil
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation)
Orientation.lockOrientation(.allButUpsideDown)
}
}
#endif
@@ -977,46 +1007,46 @@ final class PlayerModel: ObservableObject {
}
#else
func handleEnterForeground() {
setNeedsDrawing(presentingPlayer)
#if os(iOS)
OrientationTracker.shared.startDeviceOrientationTracking()
#endif
if !musicMode, activeBackend == .appleAVPlayer {
#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 {
mpvBackend.addVideoTrackFromStream()
mpvBackend.setVideoToAuto()
mpvBackend.controls.resetTimer()
} else if !musicMode, activeBackend == .appleAVPlayer {
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 {
return
}
show()
closePiP()
// Needs to be delayed a bit, otherwise the PiP windows stays open
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.closePiP()
}
}
func handleEnterBackground() {
#if os(iOS)
OrientationTracker.shared.stopDeviceOrientationTracking()
#endif
if Defaults[.pauseOnEnteringBackground], !playingInPictureInPicture, !musicMode {
pause()
} else if !playingInPictureInPicture {
} else if !playingInPictureInPicture, activeBackend == .appleAVPlayer {
avPlayerBackend.removePlayerFromLayer()
} else if activeBackend == .mpv, !musicMode {
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
@@ -1107,7 +1137,7 @@ final class PlayerModel: ObservableObject {
task.resume()
}
func toggleFullscreen(_ isFullScreen: Bool, showControls: Bool = true) {
func toggleFullscreen(_ isFullScreen: Bool, showControls: Bool = true, initiatedByButton: Bool = false) {
controls.presentingControls = showControls && isFullScreen
#if os(macOS)
@@ -1122,15 +1152,13 @@ final class PlayerModel: ObservableObject {
avPlayerBackend.controller.enterFullScreen(animated: true)
return
}
guard rotateToLandscapeOnEnterFullScreen.isRotating else { return }
let lockOrientation = rotateToLandscapeOnEnterFullScreen.interfaceOrientation
if currentVideoIsLandscape {
let delay = activeBackend == .appleAVPlayer && avPlayerUsesSystemControls ? 0.8 : 0
// not sure why but first rotation call is ignore so doing rotate to same orientation first
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)
if initiatedByButton {
Orientation.lockOrientation(self.isOrientationLocked ? (lockOrientation == .landscapeRight ? .landscapeRight : .landscapeLeft) : .landscape)
}
let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape ? OrientationTracker.shared.currentInterfaceOrientation : self.rotateToLandscapeOnEnterFullScreen.interfaceOrientation
Orientation.lockOrientation(self.isOrientationLocked ? (lockOrientation == .landscapeRight ? .landscapeRight : .landscapeLeft) : .landscape, andRotateTo: orientation)
}
} else {
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
@@ -1138,10 +1166,12 @@ final class PlayerModel: ObservableObject {
avPlayerBackend.controller.dismiss(animated: true)
return
}
let rotationOrientation = Constants.isIPhone ? UIInterfaceOrientation.portrait : nil
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: rotationOrientation)
if Defaults[.lockPortraitWhenBrowsing] {
lockedOrientation = UIInterfaceOrientationMask.portrait
}
let rotationOrientation = Defaults[.lockPortraitWhenBrowsing] ? UIInterfaceOrientation.portrait : nil
Orientation.lockOrientation(Defaults[.lockPortraitWhenBrowsing] ? .portrait : .allButUpsideDown, andRotateTo: rotationOrientation)
}
#endif
}
@@ -1231,9 +1261,48 @@ final class PlayerModel: ObservableObject {
return nil
}
#if !os(macOS)
@objc func handleAudioSessionInterruption(_ notification: Notification) {
logger.info("Audio session interruption received.")
logger.info("Notification received: \(notification)")
guard let info = notification.userInfo,
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
else {
logger.info("AVAudioSessionInterruptionTypeKey is missing or not a UInt in userInfo.")
return
}
logger.info("Interruption type received: \(type)")
switch type {
case .began:
logger.info("Audio session interrupted.")
// We need to call pause() to set all variables correctly, and play()
// directly afterwards, because the .began interrupt is sent after audio
// ducking ended and playback would pause. Audio ducking usually happens
// when using headphones.
pause()
play()
case .ended:
logger.info("Audio session interruption ended.")
// We need to call pause() to set all variables correctly.
// Otherwise, playback does not resume when the interruption ends.
pause()
play()
default:
break
}
}
#endif
#if os(macOS)
private func assignKeyPressMonitor() {
keyPressMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { keyEvent -> NSEvent? in
keyPressMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] keyEvent -> NSEvent? in
// Check if the player window is the key window
guard let self, let window = Windows.playerWindow, window.isKeyWindow else { return keyEvent }
switch keyEvent.keyCode {
case 124:
if !self.liveStreamInAVPlayer {

View File

@@ -139,10 +139,14 @@ class Stream: Equatable, Hashable, Identifiable {
case sd428p30
case sd428p25
case sd426p30
case sd426p25
case sd360p30
case sd360p25
case sd320p30
case sd320p25
case sd256p30
case sd256p25
case sd240p30
case sd240p25
case sd214p30
@@ -253,7 +257,7 @@ class Stream: Equatable, Hashable, Identifiable {
case .sd480p30, .sd480p25:
return 2_500_000 // 2.5 Mbit/s
case .sd428p30, .sd428p25:
case .sd428p30, .sd428p25, .sd426p30, .sd426p25:
return 2_000_000 // 2 Mbit/s
case .sd360p30, .sd360p25:
@@ -262,7 +266,7 @@ class Stream: Equatable, Hashable, Identifiable {
case .sd320p30, .sd320p25:
return 1_200_000 // 1.2 Mbit/s
case .sd240p30, .sd240p25:
case .sd256p30, .sd256p25, .sd240p30, .sd240p25:
return 1_000_000 // 1 Mbit/s
case .sd214p30, .sd214p25:

View File

@@ -1,15 +1,17 @@
{
"images" : [
{
"filename" : "Invidious.svg",
"filename" : "Invidious_512x512@1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Invidious_512x512@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "Invidious_512x512@3x.png",
"idiom" : "universal",
"scale" : "3x"
}

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.6.7 -->
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<g id="Group">
<path id="Path" fill="#f0f0f0" stroke="none" d="M 244.186371 511.752167 C 219.045975 510.71109 195.004303 506.137482 171.587616 497.941071 C 94.144188 470.833344 33.538929 407.477814 10.268302 329.279663 C 0.239193 295.592224 -2.512759 258.122925 2.318441 221.024231 C 7.031626 184.829193 19.597385 150.432068 39.58955 118.998993 C 54.919968 94.894897 76.601517 71.145599 99.579987 53.286163 C 146.440094 16.865601 208.748688 -2.762817 267.733124 0.314728 C 300.60672 2.029694 331.167175 9.238464 360.594604 22.219849 C 371.003937 26.811676 386.029724 34.994751 395.774933 41.379883 C 413.748718 53.155853 424.186218 61.823517 439.575043 77.75174 C 456.410675 95.178497 467.682678 109.774475 478.1875 127.753906 C 487.343475 143.423645 496.096527 163.56778 501.34256 181.042023 C 503.374359 187.809723 506.984924 202.749298 508.564056 210.923828 C 511.600952 226.643677 511.993439 231.662842 511.999939 254.866028 C 512.007507 279.289337 511.412323 287.069458 508.295135 303.353882 C 496.447205 365.24649 463.100311 419.655823 413.19043 458.533966 C 384.211426 481.106567 349.644592 497.493866 313.417664 505.834595 C 292.186981 510.723083 268.424774 512.753723 244.192581 511.750305 Z M 199.601273 407.824738 C 199.600616 407.13028 199.507141 405.112122 199.394073 403.339905 L 199.188583 400.117706 L 193.216202 399.771149 C 188.074692 399.472839 187.123169 399.331085 186.376404 398.752106 C 183.806091 396.759216 184.51181 390.745789 189.233658 374.405304 C 190.33078 370.608765 193.472549 359.471619 196.215607 349.656189 C 198.958557 339.840759 202.82106 326.12854 204.798935 319.183411 C 206.776825 312.238525 210.127289 300.343872 212.2444 292.751038 C 214.361496 285.15802 216.835648 276.394104 217.742447 273.275696 C 218.649307 270.157227 221.881256 258.716736 224.924591 247.853851 C 231.209076 225.419739 235.292999 211.284149 236.285294 208.529846 C 236.943924 206.701843 236.981201 206.664764 237.55249 207.272522 C 237.876221 207.616882 242.438049 216.990021 247.689819 228.101257 C 252.941574 239.212921 264.315857 263.153992 272.964874 281.302307 C 294.797607 327.11499 321.04184 382.317078 327.916321 396.885345 L 333.677551 409.096344 L 348.10614 408.978271 C 356.041901 408.913391 362.859833 408.719421 363.258698 408.547302 C 363.971802 408.238831 363.946777 408.156982 361.515564 402.851898 C 360.158997 399.891571 351.171295 380.953369 341.54248 360.767029 C 279.69873 231.107727 263.778931 197.38205 255.30777 178.09668 C 249.3349 164.497955 246.53923 158.564606 245.509338 157.30484 C 244.455933 156.015533 243.436447 155.901581 242.498398 156.96814 C 240.974991 158.700165 237.284607 170.24234 230.574875 194.259399 C 227.962112 203.611725 222.271103 223.840454 217.928177 239.210693 C 209.49437 269.060883 207.108093 277.513733 199.725769 303.692749 C 197.14035 312.859924 193.631577 325.285278 191.928467 331.303101 C 190.225357 337.321899 186.805634 349.519958 184.329178 358.409424 C 178.862122 378.033875 176.535034 385.964355 174.94397 390.397858 C 172.229355 397.960846 171.676529 398.746796 168.692398 399.28656 C 167.563736 399.490662 165.63089 399.658478 164.39711 399.659515 C 161.603485 399.663513 159.888535 400.138885 159.245316 401.092468 C 158.709564 401.88678 158.528641 407.530029 159.013474 408.322784 C 159.274811 408.750031 162.147385 408.816345 188.66066 409.00708 L 199.603806 409.085815 L 199.602936 407.82312 Z M 246.283508 136.628906 C 251.781326 135.410889 257.030548 130.108551 258.271179 124.519989 C 258.735718 122.427612 258.68457 117.95636 258.17337 115.97229 C 257.092316 111.775818 254.02124 107.673767 250.502441 105.726105 C 245.661484 103.0466 238.49118 103.04895 233.643967 105.732697 C 226.044434 109.939087 223.284454 120.360321 227.562363 128.69577 C 230.991348 135.376801 238.182877 138.424713 246.28302 136.630219 Z"/>
<path id="Circle" fill="#575757" stroke="none" d="M 256 0 C 114.61525 0 0 114.615257 0 256 C 0 397.384735 114.61525 512 256 512 C 397.384735 512 512 397.384735 512 256 C 512 114.615257 397.384735 0 256 0 Z M 256 4 C 395.175446 4 508 116.824524 508 256 C 508 395.175446 395.175446 508 256 508 C 116.824524 508 4 395.175446 4 256 C 4 116.824524 116.824524 4 256 4 Z"/>
</g>
<g id="g1">
<path id="path1" fill="#00b6f0" stroke="#00b6f0" stroke-width="0.297331" d="M 234.067764 106.178009 C 223.288239 112.003052 223.375183 129.030151 234.328568 134.765594 C 241.804688 138.70871 251.367157 136.199432 255.800674 129.209381 C 260.842682 121.41275 258.060883 110.300354 249.976257 106.088379 C 245.54274 103.758362 238.501282 103.758362 234.067764 106.178009 Z"/>
<path id="path2" fill="#575757" stroke="none" d="M 242.34436 157.257843 C 241.282883 158.735199 236.77153 172.585571 233.321655 185.235535 C 230.667953 194.83847 224.387421 217.55304 218.72612 237.405212 C 216.956955 243.776398 213.595551 255.779999 211.207184 264.182556 C 208.907288 272.585114 205.545883 284.588745 203.688263 290.9599 C 201.919098 297.331055 198.557724 309.334686 196.169357 317.737244 C 193.869431 326.139801 190.508026 338.143433 188.650406 344.514587 C 186.881271 350.885742 183.608307 362.52005 181.485321 370.368591 C 176.266296 389.482056 173.258743 397.976929 171.312653 398.992645 C 170.428085 399.546631 168.216629 399.915985 166.359024 399.915985 C 159.901581 399.915985 158.928543 400.654663 159.193924 404.9021 L 159.459305 408.687897 L 179.627701 408.964874 L 199.796112 409.149567 L 199.530731 404.809784 L 199.265381 400.377686 L 192.807953 400.100647 C 186.969711 399.823669 186.262039 399.638977 185.377472 397.607605 C 184.227524 395.022217 185.377472 388.0047 188.650406 376.832092 C 189.800354 373.046326 192.807953 362.427704 195.28476 353.286499 C 197.761581 344.145264 201.122986 332.049255 202.803696 326.509125 C 204.395935 320.876648 207.757339 308.872986 210.322601 299.731781 C 212.799438 290.590576 216.160843 278.494598 217.841522 272.954437 C 219.433777 267.32196 222.795181 255.318329 225.360458 246.177094 C 232.879379 218.753387 236.240784 207.488464 236.948441 206.565094 C 237.390732 206.103394 238.45224 207.58078 239.425278 209.796844 C 240.309845 212.012909 256.40918 246.084747 275.073822 285.419769 C 293.738434 324.754761 314.614532 368.706543 321.337311 383.110901 L 333.632965 409.149567 L 348.493896 409.149567 C 356.632019 409.149567 363.354828 408.780212 363.354828 408.410889 C 363.354828 408.041534 356.72049 393.821838 348.670807 376.832092 C 296.657532 267.598999 262.955078 196.038818 257.293793 182.927185 C 254.728485 177.110016 251.19017 168.984467 249.421021 164.921692 C 245.52887 156.149841 244.290451 154.764771 242.34436 157.257843 Z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

View File

@@ -93,12 +93,9 @@ extension Defaults.Keys {
static let enableReturnYouTubeDislike = Key<Bool>("enableReturnYouTubeDislike", default: false)
#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 rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>(
"rotateToLandscapeOnEnterFullScreen",
default: Constants.isIPhone ? .landscapeRight : .disabled
)
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>("rotateToLandscapeOnEnterFullScreen", default: .landscapeRight)
#endif
static let closePiPOnNavigation = Key<Bool>("closePiPOnNavigation", default: false)
@@ -134,6 +131,7 @@ extension Defaults.Keys {
static let playerControlsLayout = Key<PlayerControlsLayout>("playerControlsLayout", default: playerControlsLayoutDefault)
static let fullScreenPlayerControlsLayout = Key<PlayerControlsLayout>("fullScreenPlayerControlsLayout", default: fullScreenPlayerControlsLayoutDefault)
static let playerControlsBackgroundOpacity = Key<Double>("playerControlsBackgroundOpacity", default: 0.2)
static let systemControlsCommands = Key<SystemControlsCommands>("systemControlsCommands", default: .restartAndAdvanceToNext)
@@ -612,26 +610,19 @@ enum PlayerTapGestureAction: String, CaseIterable, Defaults.Serializable {
}
enum FullScreenRotationSetting: String, CaseIterable, Defaults.Serializable {
case disabled
case landscapeLeft
case landscapeRight
#if os(iOS)
var interaceOrientation: UIInterfaceOrientation {
var interfaceOrientation: UIInterfaceOrientation {
switch self {
case .landscapeLeft:
return .landscapeLeft
case .landscapeRight:
return .landscapeRight
default:
return .portrait
}
}
#endif
var isRotating: Bool {
self != .disabled
}
}
struct WidgetSettings: Defaults.Serializable {

View File

@@ -17,12 +17,11 @@ import SwiftUI
#if os(iOS)
func playerViewController(_: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator) {
guard rotateToLandscapeOnEnterFullScreen.isRotating else { return }
if PlayerModel.shared.currentVideoIsLandscape {
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
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: orientation)
}
@@ -37,8 +36,6 @@ import SwiftUI
}
if !context.isCancelled {
#if os(iOS)
self.player.lockedOrientation = nil
if Constants.isIPhone {
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
}

View File

@@ -29,6 +29,7 @@ struct PlayerControls: View {
@Default(.playerControlsLayout) private var regularPlayerControlsLayout
@Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout
@Default(.playerControlsBackgroundOpacity) private var playerControlsBackgroundOpacity
@Default(.buttonBackwardSeekDuration) private var buttonBackwardSeekDuration
@Default(.buttonForwardSeekDuration) private var buttonForwardSeekDuration
@@ -270,6 +271,9 @@ struct PlayerControls: View {
}
} else if player.videoForDisplay == nil {
Color.black
} else if model.presentingControls {
Color.black.opacity(playerControlsBackgroundOpacity)
.edgesIgnoringSafeArea(.all)
}
}
}
@@ -383,13 +387,13 @@ struct PlayerControls: View {
}
private var pipButton: some View {
button("PiP", systemImage: player.pipImage, action: player.togglePiPAction)
button("PiP", systemImage: player.pipImage, active: player.playingInPictureInPicture, action: player.togglePiPAction)
.disabled(!player.pipPossible)
}
#if os(iOS)
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

View File

@@ -8,7 +8,7 @@ extension VideoPlayerView {
.updating($dragGestureOffset) { value, state, _ in
guard isVerticalDrag else { return }
var translation = value.translation
translation.height = max(0, translation.height)
translation.height = max(-translation.height, translation.height)
state = translation
}
#endif
@@ -18,7 +18,8 @@ extension VideoPlayerView {
.onChanged { value in
guard player.presentingPlayer,
!controlsOverlayModel.presenting,
dragGestureState else { return }
dragGestureState,
!disableToggleGesture else { return }
if player.controls.presentingControls, !player.musicMode {
player.controls.presentingControls = false
@@ -61,19 +62,18 @@ extension VideoPlayerView {
return
}
guard verticalDrag > 0 else { return }
viewDragOffset = verticalDrag
if verticalDrag > 60,
player.playingFullScreen
{
player.exitFullScreen(showControls: false)
#if os(iOS)
if Constants.isIPhone {
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
}
#endif
// Toggle fullscreen on upward drag only when not disabled
if verticalDrag < -50 {
player.toggleFullScreenAction()
disableGestureTemporarily()
return
}
// Ignore downward swipes when in fullscreen
guard verticalDrag > 0 && !player.playingFullScreen else {
return
}
viewDragOffset = verticalDrag
}
.onEnded { _ in
onPlayerDragGestureEnded()
@@ -86,16 +86,6 @@ 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,
@@ -117,4 +107,12 @@ extension VideoPlayerView {
}
}
}
// Function to temporarily disable the toggle gesture after a fullscreen change
private func disableGestureTemporarily() {
disableToggleGesture = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
disableToggleGesture = false
}
}
}

View File

@@ -155,10 +155,10 @@ struct VideoActions: View {
case .fullScreen:
actionButton("Fullscreen", systemImage: player.fullscreenImage, action: player.toggleFullScreenAction)
case .pip:
actionButton("PiP", systemImage: player.pipImage, action: player.togglePiPAction)
actionButton("PiP", systemImage: player.pipImage, active: player.playingInPictureInPicture, action: player.togglePiPAction)
#if os(iOS)
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
case .restart:
actionButton("Replay", systemImage: "backward.end.fill", action: player.replayAction)

View File

@@ -47,11 +47,18 @@ struct VideoPlayerView: View {
#if !os(tvOS)
@GestureState var dragGestureState = false
@GestureState var dragGestureOffset = CGSize.zero
@State var isHorizontalDrag = false // swiftlint:disable:this swiftui_state_private
@State var isVerticalDrag = false // swiftlint:disable:this swiftui_state_private
@State var viewDragOffset = Self.hiddenOffset // swiftlint:disable:this swiftui_state_private
// swiftlint:disable private_swiftui_state
@State var isHorizontalDrag = false
@State var isVerticalDrag = false
@State var viewDragOffset = Self.hiddenOffset
// swiftlint:enable private_swiftui_state
#endif
// swiftlint:disable private_swiftui_state
@State var disableToggleGesture = false
// swiftlint:enable private_swiftui_state
@ObservedObject var player = PlayerModel.shared // swiftlint:disable:this swiftui_state_private
#if os(macOS)
@@ -104,9 +111,6 @@ struct VideoPlayerView: View {
.onChange(of: geometry.size) { _ in
self.playerSize = geometry.size
}
.onChange(of: fullScreenDetails) { value in
player.backend.setNeedsDrawing(!value)
}
#if os(iOS)
.onChange(of: player.presentingPlayer) { newValue in
if newValue {
@@ -120,19 +124,6 @@ struct VideoPlayerView: View {
}
#endif
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) {
guard !dragGestureState else { return }
@@ -306,11 +297,14 @@ struct VideoPlayerView: View {
playerSize: player.playerSize,
fullScreen: fullScreenDetails
))
#if os(macOS)
// TODO: Check whether this is needed on macOS.
.onDisappear {
if player.presentingPlayer {
player.setNeedsDrawing(true)
}
}
#endif
.id(player.currentVideo?.cacheKey)
.transition(.opacity)
} else {

View File

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

View File

@@ -38,6 +38,7 @@ struct PlayerControlsSettings: View {
@Default(.playerControlsAdvanceToNextEnabled) private var playerControlsAdvanceToNextEnabled
@Default(.playerControlsPlaybackModeEnabled) private var playerControlsPlaybackModeEnabled
@Default(.playerControlsMusicModeEnabled) private var playerControlsMusicModeEnabled
@Default(.playerControlsBackgroundOpacity) private var playerControlsBackgroundOpacity
private var player = PlayerModel.shared
@@ -76,6 +77,8 @@ struct PlayerControlsSettings: View {
playerControlsLayoutPicker
SettingsHeader(text: "Fullscreen size".localized(), secondary: true)
fullScreenPlayerControlsLayoutPicker
SettingsHeader(text: "Background opacity".localized(), secondary: true)
playerControlsBackgroundOpacityPicker
}
#endif
@@ -202,6 +205,15 @@ struct PlayerControlsSettings: View {
.modifier(SettingsPickerModifier())
}
private var playerControlsBackgroundOpacityPicker: some View {
Picker("Background opacity", selection: $playerControlsBackgroundOpacity) {
ForEach(Array(stride(from: 0.0, through: 1.0, by: 0.1)), id: \.self) { value in
Text("\(Int(value * 100))%").tag(value)
}
}
.modifier(SettingsPickerModifier())
}
@ViewBuilder private var seekingSection: some View {
seekingDurationSetting("System controls", $systemControlsSeekDuration)
.foregroundColor(systemControlsCommands == .restartAndAdvanceToNext ? .secondary : .primary)

View File

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

View File

@@ -24,14 +24,42 @@ struct VideoContextMenuView: View {
private var backgroundContext = PersistenceController.shared.container.newBackgroundContext()
@State private var isOverlayVisible = false
init(video: Video) {
self.video = video
_watchRequest = video.watchFetchRequest
}
var body: some View {
if video.videoID != Video.fixtureID {
contextMenu
ZStack {
// Conditional overlay to block taps on underlying views
if isOverlayVisible {
Color.clear
.contentShape(Rectangle())
#if !os(tvOS)
// This is not available on tvOS < 16 so we leave out.
// TODO: remove #if when setting the minimum deployment target to >= 16
.onTapGesture {
// Dismiss overlay without triggering other interactions
isOverlayVisible = false
}
#endif
.ignoresSafeArea() // Ensure overlay covers the entire screen
.accessibilityLabel("Dismiss context menu")
.accessibilityHint("Tap to close the context")
.accessibilityAddTraits(.isButton)
}
if video.videoID != Video.fixtureID {
contextMenu
.onAppear {
isOverlayVisible = true
}
.onDisappear {
isOverlayVisible = false
}
}
}
}

View File

@@ -204,9 +204,14 @@ struct YatteeApp: App {
}
#if os(iOS)
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
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
@@ -225,6 +230,17 @@ struct YatteeApp: App {
DispatchQueue.global(qos: .userInitiated).async {
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 {
#if os(iOS)
return horizontalSizeClass == .compact ? .tab : .sidebar

View File

@@ -4103,7 +4103,7 @@
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 193;
CURRENT_PROJECT_VERSION = 195;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
@@ -4134,7 +4134,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 193;
CURRENT_PROJECT_VERSION = 195;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist";
@@ -4165,7 +4165,7 @@
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 193;
CURRENT_PROJECT_VERSION = 195;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 11.0;
@@ -4185,7 +4185,7 @@
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 193;
CURRENT_PROJECT_VERSION = 195;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 11.0;
@@ -4326,6 +4326,7 @@
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 3;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
@@ -4348,7 +4349,7 @@
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 193;
CURRENT_PROJECT_VERSION = 195;
ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
@@ -4365,7 +4366,9 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIRequiresFullScreen = YES;
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;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -4400,7 +4403,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 193;
CURRENT_PROJECT_VERSION = 195;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
@@ -4414,7 +4417,9 @@
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UIRequiresFullScreen = YES;
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;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -4452,7 +4457,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 193;
CURRENT_PROJECT_VERSION = 195;
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
@@ -4491,13 +4496,14 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 193;
CURRENT_PROJECT_VERSION = 195;
DEAD_CODE_STRIPPING = YES;
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
ENABLE_USER_SELECTED_FILES = readonly;
GCC_OPTIMIZATION_LEVEL = 1;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = macOS/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.video";
@@ -4525,7 +4531,7 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 193;
CURRENT_PROJECT_VERSION = 195;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4548,7 +4554,7 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 193;
CURRENT_PROJECT_VERSION = 195;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4573,7 +4579,7 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 193;
CURRENT_PROJECT_VERSION = 195;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4597,7 +4603,7 @@
buildSettings = {
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 193;
CURRENT_PROJECT_VERSION = 195;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4623,7 +4629,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 193;
CURRENT_PROJECT_VERSION = 195;
DEVELOPMENT_ASSET_PATHS = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -4663,7 +4669,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 193;
CURRENT_PROJECT_VERSION = 195;
DEVELOPMENT_ASSET_PATHS = "";
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
ENABLE_PREVIEWS = YES;
@@ -4703,7 +4709,7 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 193;
CURRENT_PROJECT_VERSION = 195;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -4726,7 +4732,7 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 193;
CURRENT_PROJECT_VERSION = 195;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",

View File

@@ -1,16 +1,17 @@
import AVFoundation
import Defaults
import Foundation
import Logging
import UIKit
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!
func application(_: UIApplication, supportedInterfaceOrientationsFor _: UIWindow?) -> UIInterfaceOrientationMask {
orientationLock
return orientationLock
}
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)
UIViewController.swizzleHomeIndicatorProperty()
OrientationTracker.shared.startDeviceOrientationTracking()
OrientationModel.shared.startOrientationUpdates()
// Configure the audio session for playback
do {

View File

@@ -1,10 +1,12 @@
import Defaults
import Foundation
import Logging
import Repeat
import SwiftUI
final class OrientationModel {
static var shared = OrientationModel()
let logger = Logger(label: "stream.yattee.orientation.model")
var orientation = UIInterfaceOrientation.portrait
var lastOrientation: UIInterfaceOrientation?
@@ -13,79 +15,69 @@ final class OrientationModel {
private var player = PlayerModel.shared
func configureOrientationUpdatesBasedOnAccelerometer() {
let currentOrientation = OrientationTracker.shared.currentInterfaceOrientation
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)
}
}
func startOrientationUpdates() {
// Ensure the orientation observer is active
orientationObserver = NotificationCenter.default.addObserver(
forName: OrientationTracker.deviceOrientationChangedNotification,
object: nil,
queue: .main
) { _ in
guard !Defaults[.honorSystemOrientationLock],
self.player.presentingPlayer,
!self.player.playingInPictureInPicture,
self.player.lockedOrientation.isNil
self.logger.info("Notification received: Device orientation changed.")
// We only allow .portrait and are not showing the player
guard (!self.player.presentingPlayer && !Defaults[.lockPortraitWhenBrowsing]) || self.player.presentingPlayer
else {
return
}
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
}
self.lastOrientation = orientation
DispatchQueue.main.async {
guard Defaults[.enterFullscreenInLandscape],
self.player.presentingPlayer
else {
return
}
self.orientationDebouncer.callback = {
DispatchQueue.main.async {
if orientation.isLandscape {
self.player.controls.presentingControls = false
self.player.enterFullScreen(showControls: false)
if Defaults[.enterFullscreenInLandscape], self.player.presentingPlayer {
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)
} else {
self.player.exitFullScreen(showControls: false)
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
self.logger.info("Exiting fullscreen because orientation is 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()
}
}
}
func stopOrientationUpdates() {
guard let observer = orientationObserver else { return }
NotificationCenter.default.removeObserver(observer)
}
func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation: UIInterfaceOrientation? = nil) {
logger.info("Locking orientation to: \(orientation), rotating to: \(String(describing: rotateOrientation)).")
if let rotateOrientation {
self.orientation = rotateOrientation
lastOrientation = rotateOrientation