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 ## Build 195
* Invidious: propper HTTP basic auth support by @stonerl in https://github.com/yattee/yattee/pull/762 * iOS: Simplified fullscreen and orientation by @stonerl in https://github.com/yattee/yattee/pull/786
* Apply correct orientation by @stonerl in https://github.com/yattee/yattee/pull/770 * macOS: only apply player shortcuts when window is active by @stonerl in https://github.com/yattee/yattee/pull/802
* Circular Invidious logo by @stonerl in https://github.com/yattee/yattee/pull/769 * player controls: add background opacity selection by @stonerl in https://github.com/yattee/yattee/pull/799
* Video Thumbnails: retry 3 times fetching from URL by @stonerl in https://github.com/yattee/yattee/pull/768 * add missing Shorts resolutions by @stonerl in https://github.com/yattee/yattee/pull/797
* Make thumbnail fill the view in music mode by @stonerl in https://github.com/yattee/yattee/pull/766 * use -O1 on macOS by @stonerl in https://github.com/yattee/yattee/pull/801
* 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
## Previous builds ## Previous builds
* Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein) * Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein)
@@ -26,6 +22,22 @@
* Add import export of missing settings * Add import export of missing settings
* macOS: Fix settings windows layout * macOS: Fix settings windows layout
* Fix seek OSD layout on tvOS, revert OSD position * 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 * 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 * 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 * 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) atomos (0.1.3)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.970.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-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.9) aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.88.0) aws-sdk-kms (1.89.0)
aws-sdk-core (~> 3, >= 3.201.0) aws-sdk-core (~> 3, >= 3.203.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.159.0) aws-sdk-s3 (1.160.0)
aws-sdk-core (~> 3, >= 3.201.0) aws-sdk-core (~> 3, >= 3.203.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sigv4 (1.9.1) aws-sigv4 (1.9.1)
@@ -171,8 +171,7 @@ GEM
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
retriable (3.1.2) retriable (3.1.2)
rexml (3.3.6) rexml (3.3.7)
strscan
rouge (2.0.7) rouge (2.0.7)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.3.2) rubyzip (2.3.2)
@@ -185,7 +184,6 @@ GEM
simctl (1.6.10) simctl (1.6.10)
CFPropertyList CFPropertyList
naturally naturally
strscan (3.1.0)
terminal-notifier (2.0.0) terminal-notifier (2.0.0)
terminal-table (3.0.2) terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)

View File

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

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

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

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

@@ -364,7 +364,11 @@ final class AVPlayerBackend: PlayerBackend {
let startPlaying = { let startPlaying = {
#if !os(macOS) #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 #endif
self.setRate(self.model.currentRate) self.setRate(self.model.currentRate)

View File

@@ -248,13 +248,6 @@ final class MPVBackend: PlayerBackend {
#if !os(macOS) #if !os(macOS)
do { do {
try AVAudioSession.sharedInstance().setActive(true) try AVAudioSession.sharedInstance().setActive(true)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handleAudioSessionInterruption(_:)),
name: AVAudioSession.interruptionNotification,
object: nil
)
} catch { } catch {
self.logger.error("Error setting up audio session: \(error)") 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)") 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 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

@@ -47,7 +47,7 @@ final class PlayerModel: ObservableObject {
static var shared = PlayerModel() static var shared = PlayerModel()
let logger = Logger(label: "stream.yattee.app") let logger = Logger(label: "stream.yattee.player.model")
var playerItem: AVPlayerItem? var playerItem: AVPlayerItem?
@@ -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,9 +206,27 @@ 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
// Register for audio session interruption notifications
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioSessionInterruption(_:)),
name: AVAudioSession.interruptionNotification,
object: nil
)
#endif #endif
playbackMode = Defaults[.playbackMode] playbackMode = Defaults[.playbackMode]
@@ -220,6 +243,12 @@ final class PlayerModel: ObservableObject {
currentRate = playerRate currentRate = playerRate
} }
#if !os(macOS)
deinit {
NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
}
#endif
func show() { func show() {
#if os(macOS) #if os(macOS)
if presentingPlayer { if presentingPlayer {
@@ -503,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 {
@@ -537,8 +569,6 @@ final class PlayerModel: ObservableObject {
} else { } else {
Orientation.lockOrientation(.allButUpsideDown) Orientation.lockOrientation(.allButUpsideDown)
} }
OrientationModel.shared.stopOrientationUpdates()
#endif #endif
} }
} }
@@ -645,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
} }
} }
@@ -679,38 +714,24 @@ final class PlayerModel: ObservableObject {
avPlayerBackend.startPictureInPictureOnPlay = false avPlayerBackend.startPictureInPictureOnPlay = false
avPlayerBackend.startPictureInPictureOnSwitch = false avPlayerBackend.startPictureInPictureOnSwitch = false
if activeBackend == .appleAVPlayer { guard activeBackend != .appleAVPlayer else {
avPlayerBackend.tryStartingPictureInPicture() avPlayerBackend.tryStartingPictureInPicture()
return return
} }
// First, we need to create an array with supported formats. avPlayerBackend.startPictureInPictureOnSwitch = true
let formatOrderPiP: [QualityProfile.Format] = [.stream, .hls]
guard let video = currentVideo else { return } saveTime {
guard let stream = avPlayerBackend.bestPlayable(availableStreams, maxResolution: .hd720p30, formatOrder: formatOrderPiP) else { return } self.changeActiveBackend(from: .mpv, to: .appleAVPlayer)
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
if avPlayerBackend.video == video { if let pipController = self?.pipController, pipController.isPictureInPictureActive, self?.avPlayerBackend.isPlaying == true {
if activeBackend != .appleAVPlayer { self?.exitFullScreen()
avPlayerBackend.startPictureInPictureOnSwitch = true self?.controls.objectWillChange.send()
} timer.invalidate()
changeActiveBackend(from: activeBackend, to: .appleAVPlayer) } else if self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.startPictureInPictureOnSwitch == false {
} else { self?.avPlayerBackend.startPictureInPictureOnSwitch = true
avPlayerBackend.startPictureInPictureOnPlay = true self?.avPlayerBackend.tryStartingPictureInPicture()
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
} }
} }
} }
@@ -740,19 +761,27 @@ final class PlayerModel: ObservableObject {
show() show()
#endif #endif
if previousActiveBackend == .mpv { avPlayerBackend.closePiP()
saveTime { _ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
self.changeActiveBackend(from: self.activeBackend, to: .mpv, isInClosePip: true) if self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.isPlaying == true, self?.playingInPictureInPicture == false {
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in timer.invalidate()
if self?.activeBackend == .mpv, self?.mpvBackend.isPlaying == true { }
self?.backend.closePiP() }
self?.controls.resetTimer()
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() { func toggleFullScreenAction() {
toggleFullscreen(playingFullScreen, showControls: false) toggleFullscreen(playingFullScreen, showControls: false, initiatedByButton: true)
} }
func togglePiPAction() { func togglePiPAction() {
@@ -778,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
@@ -977,46 +1007,46 @@ final class PlayerModel: ObservableObject {
} }
#else #else
func handleEnterForeground() { 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() 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
} }
show() 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() { func handleEnterBackground() {
#if os(iOS)
OrientationTracker.shared.stopDeviceOrientationTracking()
#endif
if Defaults[.pauseOnEnteringBackground], !playingInPictureInPicture, !musicMode { if Defaults[.pauseOnEnteringBackground], !playingInPictureInPicture, !musicMode {
pause() pause()
} else if !playingInPictureInPicture { } else if !playingInPictureInPicture, activeBackend == .appleAVPlayer {
avPlayerBackend.removePlayerFromLayer() 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 #endif
@@ -1107,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)
@@ -1122,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 {
@@ -1138,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
} }
@@ -1231,9 +1261,48 @@ final class PlayerModel: ObservableObject {
return nil 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) #if os(macOS)
private func assignKeyPressMonitor() { 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 { switch keyEvent.keyCode {
case 124: case 124:
if !self.liveStreamInAVPlayer { if !self.liveStreamInAVPlayer {

View File

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

View File

@@ -1,15 +1,17 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "Invidious.svg", "filename" : "Invidious_512x512@1x.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "1x" "scale" : "1x"
}, },
{ {
"filename" : "Invidious_512x512@2x.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "2x" "scale" : "2x"
}, },
{ {
"filename" : "Invidious_512x512@3x.png",
"idiom" : "universal", "idiom" : "universal",
"scale" : "3x" "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) 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)
@@ -134,6 +131,7 @@ extension Defaults.Keys {
static let playerControlsLayout = Key<PlayerControlsLayout>("playerControlsLayout", default: playerControlsLayoutDefault) static let playerControlsLayout = Key<PlayerControlsLayout>("playerControlsLayout", default: playerControlsLayoutDefault)
static let fullScreenPlayerControlsLayout = Key<PlayerControlsLayout>("fullScreenPlayerControlsLayout", default: fullScreenPlayerControlsLayoutDefault) 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) static let systemControlsCommands = Key<SystemControlsCommands>("systemControlsCommands", default: .restartAndAdvanceToNext)
@@ -612,26 +610,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

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

@@ -8,7 +8,7 @@ extension VideoPlayerView {
.updating($dragGestureOffset) { value, state, _ in .updating($dragGestureOffset) { value, state, _ in
guard isVerticalDrag else { return } guard isVerticalDrag else { return }
var translation = value.translation var translation = value.translation
translation.height = max(0, translation.height) translation.height = max(-translation.height, translation.height)
state = translation state = translation
} }
#endif #endif
@@ -18,7 +18,8 @@ extension VideoPlayerView {
.onChanged { value in .onChanged { value in
guard player.presentingPlayer, guard player.presentingPlayer,
!controlsOverlayModel.presenting, !controlsOverlayModel.presenting,
dragGestureState else { return } dragGestureState,
!disableToggleGesture else { return }
if player.controls.presentingControls, !player.musicMode { if player.controls.presentingControls, !player.musicMode {
player.controls.presentingControls = false player.controls.presentingControls = false
@@ -61,19 +62,18 @@ extension VideoPlayerView {
return return
} }
guard verticalDrag > 0 else { return } // Toggle fullscreen on upward drag only when not disabled
viewDragOffset = verticalDrag if verticalDrag < -50 {
player.toggleFullScreenAction()
if verticalDrag > 60, disableGestureTemporarily()
player.playingFullScreen return
{
player.exitFullScreen(showControls: false)
#if os(iOS)
if Constants.isIPhone {
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
}
#endif
} }
// Ignore downward swipes when in fullscreen
guard verticalDrag > 0 && !player.playingFullScreen else {
return
}
viewDragOffset = verticalDrag
} }
.onEnded { _ in .onEnded { _ in
onPlayerDragGestureEnded() onPlayerDragGestureEnded()
@@ -86,16 +86,6 @@ extension VideoPlayerView {
player.seek.onSeekGestureEnd() player.seek.onSeekGestureEnd()
} }
if viewDragOffset > 60,
player.playingFullScreen
{
#if os(iOS)
player.lockedOrientation = nil
#endif
player.exitFullScreen(showControls: false)
viewDragOffset = 0
return
}
isVerticalDrag = false isVerticalDrag = false
guard player.presentingPlayer, 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: case .fullScreen:
actionButton("Fullscreen", systemImage: player.fullscreenImage, action: player.toggleFullScreenAction) actionButton("Fullscreen", systemImage: player.fullscreenImage, action: player.toggleFullScreenAction)
case .pip: case .pip:
actionButton("PiP", systemImage: player.pipImage, 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

@@ -47,11 +47,18 @@ struct VideoPlayerView: View {
#if !os(tvOS) #if !os(tvOS)
@GestureState var dragGestureState = false @GestureState var dragGestureState = false
@GestureState var dragGestureOffset = CGSize.zero @GestureState var dragGestureOffset = CGSize.zero
@State var isHorizontalDrag = false // swiftlint:disable:this swiftui_state_private // swiftlint:disable private_swiftui_state
@State var isVerticalDrag = false // swiftlint:disable:this swiftui_state_private @State var isHorizontalDrag = false
@State var viewDragOffset = Self.hiddenOffset // swiftlint:disable:this swiftui_state_private @State var isVerticalDrag = false
@State var viewDragOffset = Self.hiddenOffset
// swiftlint:enable private_swiftui_state
#endif #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 @ObservedObject var player = PlayerModel.shared // swiftlint:disable:this swiftui_state_private
#if os(macOS) #if os(macOS)
@@ -104,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 {
@@ -120,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 }
@@ -306,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

@@ -38,6 +38,7 @@ struct PlayerControlsSettings: View {
@Default(.playerControlsAdvanceToNextEnabled) private var playerControlsAdvanceToNextEnabled @Default(.playerControlsAdvanceToNextEnabled) private var playerControlsAdvanceToNextEnabled
@Default(.playerControlsPlaybackModeEnabled) private var playerControlsPlaybackModeEnabled @Default(.playerControlsPlaybackModeEnabled) private var playerControlsPlaybackModeEnabled
@Default(.playerControlsMusicModeEnabled) private var playerControlsMusicModeEnabled @Default(.playerControlsMusicModeEnabled) private var playerControlsMusicModeEnabled
@Default(.playerControlsBackgroundOpacity) private var playerControlsBackgroundOpacity
private var player = PlayerModel.shared private var player = PlayerModel.shared
@@ -76,6 +77,8 @@ struct PlayerControlsSettings: View {
playerControlsLayoutPicker playerControlsLayoutPicker
SettingsHeader(text: "Fullscreen size".localized(), secondary: true) SettingsHeader(text: "Fullscreen size".localized(), secondary: true)
fullScreenPlayerControlsLayoutPicker fullScreenPlayerControlsLayoutPicker
SettingsHeader(text: "Background opacity".localized(), secondary: true)
playerControlsBackgroundOpacityPicker
} }
#endif #endif
@@ -202,6 +205,15 @@ struct PlayerControlsSettings: View {
.modifier(SettingsPickerModifier()) .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 { @ViewBuilder private var seekingSection: some View {
seekingDurationSetting("System controls", $systemControlsSeekDuration) seekingDurationSetting("System controls", $systemControlsSeekDuration)
.foregroundColor(systemControlsCommands == .restartAndAdvanceToNext ? .secondary : .primary) .foregroundColor(systemControlsCommands == .restartAndAdvanceToNext ? .secondary : .primary)

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

@@ -24,14 +24,42 @@ struct VideoContextMenuView: View {
private var backgroundContext = PersistenceController.shared.container.newBackgroundContext() private var backgroundContext = PersistenceController.shared.container.newBackgroundContext()
@State private var isOverlayVisible = false
init(video: Video) { init(video: Video) {
self.video = video self.video = video
_watchRequest = video.watchFetchRequest _watchRequest = video.watchFetchRequest
} }
var body: some View { var body: some View {
if video.videoID != Video.fixtureID { ZStack {
contextMenu // 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) #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

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