AVPlayer system controls on iOS

This commit is contained in:
Arkadiusz Fal 2023-05-20 22:49:10 +02:00
parent a4fdd50388
commit 5383cf0e90
16 changed files with 405 additions and 69 deletions

View File

@ -0,0 +1,21 @@
import SwiftUI
extension Backport where Content: View {
@ViewBuilder func toolbarBackground(_ color: Color) -> some View {
if #available(iOS 16, *) {
content
.toolbarBackground(color, for: .navigationBar)
} else {
content
}
}
@ViewBuilder func toolbarBackgroundVisibility(_ visible: Bool) -> some View {
if #available(iOS 16, *) {
content
.toolbarBackground(visible ? .visible : .hidden, for: .navigationBar)
} else {
content
}
}
}

View File

@ -0,0 +1,12 @@
import SwiftUI
extension Backport where Content: View {
@ViewBuilder func toolbarColorScheme(_ colorScheme: ColorScheme) -> some View {
if #available(iOS 16, *) {
content
.toolbarColorScheme(colorScheme, for: .navigationBar)
} else {
content
}
}
}

View File

@ -0,0 +1,11 @@
import AVKit
extension AVPlayerViewController {
func enterFullScreen(animated: Bool) {
perform(NSSelectorFromString("enterFullScreenAnimated:completionHandler:"), with: animated, with: nil)
}
func exitFullScreen(animated: Bool) {
perform(NSSelectorFromString("exitFullScreenAnimated:completionHandler:"), with: animated, with: nil)
}
}

View File

@ -1,4 +1,4 @@
import AVFoundation
import AVKit
import Defaults
import Foundation
import Logging
@ -6,6 +6,7 @@ import MediaPlayer
#if !os(macOS)
import UIKit
#endif
import SwiftUI
final class AVPlayerBackend: PlayerBackend {
static let assetKeysToLoad = ["tracks", "playable", "duration"]
@ -84,6 +85,10 @@ final class AVPlayerBackend: PlayerBackend {
private(set) var playerLayer = AVPlayerLayer()
#if os(tvOS)
var controller: AppleAVPlayerViewController?
#elseif os(iOS)
var controller = AVPlayerViewController() { didSet {
controller.player = avPlayer
}}
#endif
var startPictureInPictureOnPlay = false
var startPictureInPictureOnSwitch = false
@ -108,6 +113,9 @@ final class AVPlayerBackend: PlayerBackend {
addPlayerTimeControlStatusObserver()
playerLayer.player = avPlayer
#if os(iOS)
controller.player = avPlayer
#endif
}
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream? {
@ -469,10 +477,6 @@ final class AVPlayerBackend: PlayerBackend {
switch playerItem.status {
case .readyToPlay:
if self.model.playingInPictureInPicture {
self.startPictureInPictureOnSwitch = false
self.startPictureInPictureOnPlay = false
}
if self.model.activeBackend == .appleAVPlayer,
self.isAutoplaying(playerItem)
{
@ -487,17 +491,21 @@ final class AVPlayerBackend: PlayerBackend {
self.model.play()
}
} else if self.startPictureInPictureOnPlay {
self.startPictureInPictureOnPlay = false
self.model.stream = self.stream
self.model.streamSelection = self.stream
if self.model.activeBackend != .appleAVPlayer {
self.startPictureInPictureOnSwitch = true
let seconds = self.model.mpvBackend.currentTime?.seconds ?? 0
self.seek(to: seconds, seekType: .backendSync) { _ in
self.seek(to: seconds, seekType: .backendSync) { finished in
guard finished else { return }
DispatchQueue.main.async {
self.model.pause()
self.model.changeActiveBackend(from: .mpv, to: .appleAVPlayer, changingStream: false)
Delay.by(3) {
self.startPictureInPictureOnPlay = false
}
}
}
}
@ -688,7 +696,6 @@ final class AVPlayerBackend: PlayerBackend {
func didChangeTo() {
if startPictureInPictureOnSwitch {
startPictureInPictureOnSwitch = false
tryStartingPictureInPicture()
} else if model.musicMode {
startMusicMode()
@ -697,6 +704,10 @@ final class AVPlayerBackend: PlayerBackend {
}
}
var isStartingPiP: Bool {
startPictureInPictureOnPlay || startPictureInPictureOnSwitch
}
func tryStartingPictureInPicture() {
guard let controller = model.pipController else { return }
@ -712,6 +723,32 @@ final class AVPlayerBackend: PlayerBackend {
}
}
}
Delay.by(5) {
self.startPictureInPictureOnSwitch = false
}
}
func setPlayerInLayer(_ playerIsPresented: Bool) {
if playerIsPresented {
bindPlayerToLayer()
} else {
removePlayerFromLayer()
}
}
func removePlayerFromLayer() {
playerLayer.player = nil
#if os(iOS)
controller.player = nil
#endif
}
func bindPlayerToLayer() {
playerLayer.player = avPlayer
#if os(iOS)
controller.player = avPlayer
#endif
}
func getTimeUpdates() {}

View File

@ -4,7 +4,7 @@ import Foundation
import SwiftUI
final class PiPDelegate: NSObject, AVPictureInPictureControllerDelegate {
var player: PlayerModel!
var player: PlayerModel { .shared }
func pictureInPictureController(
_: AVPictureInPictureController,
@ -16,19 +16,17 @@ final class PiPDelegate: NSObject, AVPictureInPictureControllerDelegate {
func pictureInPictureControllerWillStartPictureInPicture(_: AVPictureInPictureController) {}
func pictureInPictureControllerDidStartPictureInPicture(_: AVPictureInPictureController) {
guard let player else { return }
player.play()
player.playingInPictureInPicture = true
player.avPlayerBackend.startPictureInPictureOnPlay = false
player.avPlayerBackend.startPictureInPictureOnSwitch = false
player.controls.objectWillChange.send()
if Defaults[.closePlayerOnOpeningPiP] { Delay.by(0.1) { player.hide() } }
if Defaults[.closePlayerOnOpeningPiP] { Delay.by(0.1) { self.player.hide() } }
}
func pictureInPictureControllerDidStopPictureInPicture(_: AVPictureInPictureController) {
guard let player else { return }
player.playingInPictureInPicture = false
player.controls.objectWillChange.send()
}
@ -39,6 +37,8 @@ final class PiPDelegate: NSObject, AVPictureInPictureControllerDelegate {
_: AVPictureInPictureController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
) {
let wasPlaying = player.isPlaying
var delay = 0.0
#if os(iOS)
if !player.presentingPlayer {
@ -50,7 +50,7 @@ final class PiPDelegate: NSObject, AVPictureInPictureControllerDelegate {
#endif
if !player.currentItem.isNil, !player.musicMode {
player?.show()
player.show()
}
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
@ -58,6 +58,11 @@ final class PiPDelegate: NSObject, AVPictureInPictureControllerDelegate {
self?.player.playingInPictureInPicture = false
}
if wasPlaying {
Delay.by(1) {
self?.player.play()
}
}
completionHandler(true)
}
}

View File

@ -49,7 +49,6 @@ final class PlayerModel: ObservableObject {
let logger = Logger(label: "stream.yattee.app")
var avPlayerView = AppleAVPlayerView()
var playerItem: AVPlayerItem?
var mpvPlayerView = MPVPlayerView()
@ -153,6 +152,9 @@ final class PlayerModel: ObservableObject {
@Published var playingInPictureInPicture = false
var pipController: AVPictureInPictureController?
var pipDelegate = PiPDelegate()
#if !os(macOS)
var appleAVPlayerViewControllerDelegate = AppleAVPlayerViewControllerDelegate()
#endif
var playerError: Error? { didSet {
if let error = playerError {
@ -164,6 +166,7 @@ final class PlayerModel: ObservableObject {
@Default(.saveLastPlayed) var saveLastPlayed
@Default(.lastPlayed) var lastPlayed
@Default(.qualityProfiles) var qualityProfiles
@Default(.avPlayerUsesSystemControls) var avPlayerUsesSystemControls
@Default(.forceAVPlayerForLiveStreams) var forceAVPlayerForLiveStreams
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
@Default(.closePiPOnNavigation) var closePiPOnNavigation
@ -187,16 +190,17 @@ final class PlayerModel: ObservableObject {
mpvBackend.client = mpvController.client
#endif
Defaults[.activeBackend] = .mpv
playbackMode = Defaults[.playbackMode]
guard pipController.isNil else { return }
pipController = .init(playerLayer: avPlayerBackend.playerLayer)
let pipDelegate = PiPDelegate()
pipDelegate.player = self
self.pipDelegate = pipDelegate
pipController = .init(playerLayer: avPlayerBackend.playerLayer)
pipController?.delegate = pipDelegate
#if os(iOS)
if #available(iOS 14.2, *) {
pipController?.canStartPictureInPictureAutomaticallyFromInline = true
}
#endif
currentRate = playerRate
}
@ -475,6 +479,12 @@ final class PlayerModel: ObservableObject {
private func handlePresentationChange() {
backend.setNeedsDrawing(presentingPlayer)
#if os(iOS)
if presentingPlayer, activeBackend == .appleAVPlayer, avPlayerUsesSystemControls, Constants.isIPhone {
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
}
#endif
controls.hide()
#if !os(macOS)
@ -542,10 +552,15 @@ final class PlayerModel: ObservableObject {
self.stream = stream
streamSelection = stream
self.upgradeToStream(stream, force: true)
return
}
if !backend.canPlay(stream) || (to == .mpv && !stream.hlsURL.isNil) {
if !backend.canPlay(stream) ||
(to == .mpv && stream.isHLS) ||
(to == .appleAVPlayer && !stream.isHLS)
{
guard let preferredStream = streamByQualityProfile else {
return
}
@ -631,8 +646,8 @@ final class PlayerModel: ObservableObject {
if avPlayerBackend.video == video {
if activeBackend != .appleAVPlayer {
avPlayerBackend.startPictureInPictureOnSwitch = true
changeActiveBackend(from: activeBackend, to: .appleAVPlayer)
}
changeActiveBackend(from: activeBackend, to: .appleAVPlayer)
} else {
avPlayerBackend.startPictureInPictureOnPlay = true
playStream(stream, of: video, preservingTime: true, upgrading: true, withBackend: avPlayerBackend)
@ -882,7 +897,7 @@ final class PlayerModel: ObservableObject {
#else
func handleEnterForeground() {
setNeedsDrawing(presentingPlayer)
avPlayerBackend.playerLayer.player = avPlayerBackend.avPlayer
avPlayerBackend.bindPlayerToLayer()
guard closePiPAndOpenPlayerOnEnteringForeground, playingInPictureInPicture else {
return
@ -896,7 +911,7 @@ final class PlayerModel: ObservableObject {
if Defaults[.pauseOnEnteringBackground], !playingInPictureInPicture, !musicMode {
pause()
} else if !playingInPictureInPicture {
avPlayerBackend.playerLayer.player = nil
avPlayerBackend.removePlayerFromLayer()
}
}
#endif
@ -919,6 +934,13 @@ final class PlayerModel: ObservableObject {
#if os(tvOS)
guard activeBackend == .mpv else { return }
#endif
#if os(iOS)
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
return
}
#endif
guard let video = currentItem?.video else {
MPNowPlayingInfoCenter.default().nowPlayingInfo = .none
return
@ -986,13 +1008,23 @@ final class PlayerModel: ObservableObject {
#if os(iOS)
if playingFullScreen {
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
avPlayerBackend.controller.enterFullScreen(animated: true)
}
guard rotateToLandscapeOnEnterFullScreen.isRotating else { return }
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: rotateToLandscapeOnEnterFullScreen.interaceOrientation)
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: orientation)
}
}
} else {
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
avPlayerBackend.controller.exitFullScreen(animated: true)
}
let rotationOrientation = rotateToPortraitOnExitFullScreen ? UIInterfaceOrientation.portrait : nil
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: rotationOrientation)
}

View File

@ -176,6 +176,10 @@ class Stream: Equatable, Hashable, Identifiable {
localURL != nil
}
var isHLS: Bool {
hlsURL != nil
}
var quality: String {
guard localURL.isNil else { return "Opened File" }
return kind == .hls ? "adaptive (HLS)" : "\(resolution.name)\(kind == .stream ? " (\(kind.rawValue))" : "")"
@ -229,8 +233,14 @@ class Stream: Equatable, Hashable, Identifiable {
}
func hash(into hasher: inout Hasher) {
hasher.combine(videoAsset?.url)
hasher.combine(audioAsset?.url)
hasher.combine(hlsURL)
if let url = videoAsset?.url {
hasher.combine(url)
}
if let url = audioAsset?.url {
hasher.combine(url)
}
if let url = hlsURL {
hasher.combine(url)
}
}
}

View File

@ -132,6 +132,7 @@ extension Defaults.Keys {
static let playerControlsLayout = Key<PlayerControlsLayout>("playerControlsLayout", default: playerControlsLayoutDefault)
static let fullScreenPlayerControlsLayout = Key<PlayerControlsLayout>("fullScreenPlayerControlsLayout", default: fullScreenPlayerControlsLayoutDefault)
static let avPlayerUsesSystemControls = Key<Bool>("avPlayerUsesSystemControls", default: true)
static let horizontalPlayerGestureEnabled = Key<Bool>("horizontalPlayerGestureEnabled", default: true)
static let seekGestureSpeed = Key<Double>("seekGestureSpeed", default: 0.5)
static let seekGestureSensitivity = Key<Double>("seekGestureSensitivity", default: 30.0)

View File

@ -15,6 +15,8 @@ struct ContentView: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
#endif
@Default(.avPlayerUsesSystemControls) private var avPlayerUsesSystemControls
var body: some View {
Group {
#if os(iOS)
@ -133,6 +135,7 @@ struct ContentView: View {
)
#endif
.alert(isPresented: $navigation.presentingAlert) { navigation.alert }
.statusBarHidden(player.playingFullScreen)
}
var navigationStyle: NavigationStyle {
@ -150,9 +153,11 @@ struct ContentView: View {
playerView
.transition(.asymmetric(insertion: .identity, removal: .opacity))
.zIndex(3)
} else if player.activeBackend == .appleAVPlayer {
} else if player.activeBackend == .appleAVPlayer,
avPlayerUsesSystemControls || player.avPlayerBackend.isStartingPiP
{
#if os(iOS)
playerView.offset(y: UIScreen.main.bounds.height)
AppleAVPlayerLayerView().offset(y: UIScreen.main.bounds.height)
#endif
}
}

View File

@ -2,15 +2,119 @@ import AVKit
import Defaults
import SwiftUI
#if !os(macOS)
final class AppleAVPlayerViewControllerDelegate: NSObject, AVPlayerViewControllerDelegate {
var player: PlayerModel { .shared }
func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_: AVPlayerViewController) -> Bool {
false
}
func playerViewController(_: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator _: UIViewControllerTransitionCoordinator) {
Delay.by(0.5) { [weak self] in
self?.player.playingFullScreen = true
}
}
func playerViewController(_: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
let wasPlaying = player.isPlaying
coordinator.animate(alongsideTransition: nil) { context in
#if os(iOS)
struct AppleAVPlayerView: UIViewRepresentable {
if wasPlaying {
self.player.play()
}
#endif
if !context.isCancelled {
#if os(iOS)
self.player.lockedOrientation = nil
if Defaults[.rotateToPortraitOnExitFullScreen] {
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: .portrait)
}
if wasPlaying {
self.player.play()
}
self.player.playingFullScreen = false
#endif
}
}
}
func playerViewControllerWillStartPictureInPicture(_: AVPlayerViewController) {}
func playerViewControllerWillStopPictureInPicture(_: AVPlayerViewController) {}
func playerViewControllerDidStartPictureInPicture(_: AVPlayerViewController) {
player.playingInPictureInPicture = true
player.avPlayerBackend.startPictureInPictureOnPlay = false
player.avPlayerBackend.startPictureInPictureOnSwitch = false
player.controls.objectWillChange.send()
if Defaults[.closePlayerOnOpeningPiP] { Delay.by(0.1) { self.player.hide() } }
}
func playerViewControllerDidStopPictureInPicture(_: AVPlayerViewController) {
player.playingInPictureInPicture = false
player.controls.objectWillChange.send()
}
func playerViewController(_: AVPlayerViewController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
player.presentingPlayer = true
withAnimation(.linear(duration: 0.3)) {
self.player.playingInPictureInPicture = false
Delay.by(0.5) {
completionHandler(true)
Delay.by(0.2) {
self.player.play()
}
}
}
}
func playerViewController(_: AVPlayerViewController, restoreUserInterfaceForFullScreenExitWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
withAnimation(nil) {
player.presentingPlayer = true
}
completionHandler(true)
}
}
#endif
#if os(iOS)
struct AppleAVPlayerView: UIViewControllerRepresentable {
@State private var controller = AVPlayerViewController()
func makeUIViewController(context _: Context) -> AVPlayerViewController {
setupController()
return controller
}
func updateUIViewController(_: AVPlayerViewController, context _: Context) {
setupController()
}
func setupController() {
controller.delegate = PlayerModel.shared.appleAVPlayerViewControllerDelegate
controller.allowsPictureInPicturePlayback = true
if #available(iOS 14.2, *) {
controller.canStartPictureInPictureAutomaticallyFromInline = true
}
PlayerModel.shared.avPlayerBackend.controller = controller
}
}
struct AppleAVPlayerLayerView: UIViewRepresentable {
func makeUIView(context _: Context) -> some UIView {
PlayerLayerView()
PlayerLayerView(frame: .zero)
}
func updateUIView(_: UIViewType, context _: Context) {}
}
#else
#elseif os(tvOS)
struct AppleAVPlayerView: UIViewControllerRepresentable {
func makeUIViewController(context _: Context) -> AppleAVPlayerViewController {
let controller = AppleAVPlayerViewController()
@ -23,4 +127,27 @@ import SwiftUI
PlayerModel.shared.rebuildTVMenu()
}
}
#else
struct AppleAVPlayerView: NSViewRepresentable {
@State private var pictureInPictureDelegate = MacOSPiPDelegate()
func makeNSView(context _: Context) -> some NSView {
let view = AVPlayerView()
view.player = PlayerModel.shared.avPlayerBackend.avPlayer
view.showsFullScreenToggleButton = true
view.allowsPictureInPicturePlayback = true
view.pictureInPictureDelegate = pictureInPictureDelegate
return view
}
func updateNSView(_: NSViewType, context _: Context) {}
}
struct AppleAVPlayerLayerView: NSViewRepresentable {
func makeNSView(context _: Context) -> some NSView {
PlayerLayerView(frame: .zero)
}
func updateNSView(_: NSViewType, context _: Context) {}
}
#endif

View File

@ -1,3 +1,4 @@
import Defaults
import SwiftUI
struct PlayerBackendView: View {
@ -7,6 +8,8 @@ struct PlayerBackendView: View {
@ObservedObject private var player = PlayerModel.shared
@ObservedObject private var safeAreaModel = SafeAreaModel.shared
@Default(.avPlayerUsesSystemControls) private var avPlayerUsesSystemControls
var body: some View {
ZStack(alignment: .top) {
Group {
@ -16,7 +19,20 @@ struct PlayerBackendView: View {
case .mpv:
player.mpvPlayerView
case .appleAVPlayer:
player.avPlayerView
#if os(tvOS)
AppleAVPlayerView()
#else
if avPlayerUsesSystemControls,
!player.playingInPictureInPicture,
!player.avPlayerBackend.isStartingPiP
{
AppleAVPlayerView()
} else if !avPlayerUsesSystemControls ||
player.avPlayerBackend.isStartingPiP
{
AppleAVPlayerLayerView()
}
#endif
}
}
.zIndex(0)
@ -31,17 +47,16 @@ struct PlayerBackendView: View {
.onChange(of: proxy.size) { _ in player.playerSize = proxy.size }
.onChange(of: player.controls.presentingOverlays) { _ in player.playerSize = proxy.size }
})
#if os(iOS)
.padding(.top, player.playingFullScreen && verticalSizeClass == .regular ? 20 : 0)
#endif
#if !os(tvOS)
if player.activeBackend == .mpv || !avPlayerUsesSystemControls {
PlayerGestures()
PlayerControls()
#if os(iOS)
.padding(.top, controlsTopPadding)
.padding(.bottom, controlsBottomPadding)
#endif
}
#else
hiddenControlsButton
#endif

View File

@ -3,7 +3,7 @@ import SwiftUI
extension VideoPlayerView {
var playerDragGesture: some Gesture {
DragGesture(minimumDistance: 0, coordinateSpace: .global)
DragGesture(minimumDistance: 30, coordinateSpace: .global)
#if os(iOS)
.updating($dragGestureOffset) { value, state, _ in
guard isVerticalDrag else { return }
@ -36,7 +36,12 @@ extension VideoPlayerView {
}
#endif
if !isVerticalDrag, horizontalPlayerGestureEnabled, abs(horizontalDrag) > seekGestureSensitivity, !isHorizontalDrag {
if !isVerticalDrag,
horizontalPlayerGestureEnabled,
abs(horizontalDrag) > seekGestureSensitivity,
!isHorizontalDrag,
player.activeBackend == .mpv || !avPlayerUsesSystemControls
{
isHorizontalDrag = true
player.seek.onSeekGestureStart()
viewDragOffset = 0
@ -80,6 +85,16 @@ 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,

View File

@ -4,8 +4,8 @@ import SwiftUI
struct VideoPlayerSizeModifier: ViewModifier {
let geometry: GeometryProxy
let aspectRatio: Double?
let minimumHeightLeft: Double
let fullScreen: Bool
var detailsHiddenInFullScreen = true
#if os(iOS)
@Environment(\.verticalSizeClass) private var verticalSizeClass
@ -14,26 +14,31 @@ struct VideoPlayerSizeModifier: ViewModifier {
init(
geometry: GeometryProxy,
aspectRatio: Double? = nil,
minimumHeightLeft: Double? = nil,
fullScreen: Bool = false
fullScreen: Bool = false,
detailsHiddenInFullScreen: Bool = false
) {
self.geometry = geometry
self.aspectRatio = aspectRatio ?? VideoPlayerView.defaultAspectRatio
self.minimumHeightLeft = minimumHeightLeft ?? VideoPlayerView.defaultMinimumHeightLeft
self.fullScreen = fullScreen
self.detailsHiddenInFullScreen = detailsHiddenInFullScreen
}
func body(content: Content) -> some View {
content
.frame(width: geometry.size.width)
.frame(maxWidth: geometry.size.width)
.frame(maxHeight: maxHeight)
#if !os(macOS)
.aspectRatio(fullScreen ? nil : usedAspectRatio, contentMode: usedAspectRatioContentMode)
.aspectRatio(ratio, contentMode: usedAspectRatioContentMode)
#endif
}
var ratio: CGFloat? {
fullScreen ? detailsHiddenInFullScreen ? nil : usedAspectRatio : usedAspectRatio
}
var usedAspectRatio: Double {
guard let aspectRatio, aspectRatio > 0, aspectRatio < VideoPlayerView.defaultAspectRatio else {
guard let aspectRatio, aspectRatio > 0 else {
return VideoPlayerView.defaultAspectRatio
}
@ -50,15 +55,13 @@ struct VideoPlayerSizeModifier: ViewModifier {
var maxHeight: Double {
guard !fullScreen else {
return .infinity
if detailsHiddenInFullScreen {
return geometry.size.height
} else {
return geometry.size.width / usedAspectRatio
}
}
#if os(iOS)
let height = verticalSizeClass == .regular ? geometry.size.height - minimumHeightLeft : .infinity
#else
let height = geometry.size.height - minimumHeightLeft
#endif
return [height, 0].max()!
return max(geometry.size.height - VideoPlayerView.defaultMinimumHeightLeft, 0)
}
}

View File

@ -64,6 +64,7 @@ struct VideoPlayerView: View {
@Default(.playerSidebar) var playerSidebar
@Default(.gestureBackwardSeekDuration) private var gestureBackwardSeekDuration
@Default(.gestureForwardSeekDuration) private var gestureForwardSeekDuration
@Default(.avPlayerUsesSystemControls) internal var avPlayerUsesSystemControls
@ObservedObject internal var controlsOverlayModel = ControlOverlaysModel.shared
@ -98,12 +99,12 @@ struct VideoPlayerView: View {
return GeometryReader { geometry in
HStack(spacing: 0) {
content
.ignoresSafeArea(.all, edges: .bottom)
.frame(height: playerHeight.isNil ? nil : Double(playerHeight!))
.onAppear {
playerSize = geometry.size
}
}
.ignoresSafeArea(.all, edges: .bottom)
.frame(height: playerHeight.isNil ? nil : Double(playerHeight!))
.onChange(of: geometry.size) { _ in
self.playerSize = geometry.size
}
@ -279,7 +280,8 @@ struct VideoPlayerView: View {
VideoPlayerSizeModifier(
geometry: geometry,
aspectRatio: player.aspectRatio,
fullScreen: fullScreenPlayer
fullScreen: fullScreenPlayer,
detailsHiddenInFullScreen: detailsHiddenInFullScreen
)
)
.onHover { hovering in
@ -303,15 +305,12 @@ struct VideoPlayerView: View {
.background(Color.black)
if !fullScreenPlayer {
if !detailsHiddenInFullScreen {
VideoDetails(
video: player.videoForDisplay,
fullScreen: $fullScreenDetails,
sidebarQueue: $sidebarQueue
)
#if os(iOS)
.ignoresSafeArea(.all, edges: .bottom)
#endif
.modifier(VideoDetailsPaddingModifier(
playerSize: player.playerSize,
fullScreen: fullScreenDetails
@ -369,7 +368,7 @@ struct VideoPlayerView: View {
}
}
#endif
if !fullScreenPlayer {
if !detailsHiddenInFullScreen {
#if os(iOS)
if sidebarQueue {
List {
@ -404,6 +403,12 @@ struct VideoPlayerView: View {
}
#if os(iOS)
.statusBar(hidden: fullScreenPlayer)
.backport
.toolbarBackground(colorScheme == .light ? .white : .black)
.backport
.toolbarBackgroundVisibility(true)
.backport
.toolbarColorScheme(colorScheme)
#endif
#if os(macOS)
.background(
@ -414,6 +419,16 @@ struct VideoPlayerView: View {
#endif
}
var detailsHiddenInFullScreen: Bool {
guard fullScreenPlayer else { return false }
if player.activeBackend == .mpv {
return true
}
return !avPlayerUsesSystemControls || verticalSizeClass == .compact
}
var fullScreenPlayer: Bool {
#if os(iOS)
player.playingFullScreen || verticalSizeClass == .compact

View File

@ -2,6 +2,8 @@ import Defaults
import SwiftUI
struct PlayerControlsSettings: View {
@Default(.avPlayerUsesSystemControls) private var avPlayerUsesSystemControls
@Default(.systemControlsCommands) private var systemControlsCommands
@Default(.playerControlsLayout) private var playerControlsLayout
@Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout
@ -61,6 +63,9 @@ struct PlayerControlsSettings: View {
@ViewBuilder var sections: some View {
#if !os(tvOS)
Section(header: SettingsHeader(text: "Controls".localized()), footer: controlsLayoutFooter) {
#if !os(tvOS)
avPlayerUsesSystemControlsToggle
#endif
horizontalPlayerGestureEnabledToggle
SettingsHeader(text: "Seek gesture sensitivity".localized(), secondary: true)
seekGestureSensitivityPicker
@ -143,6 +148,10 @@ struct PlayerControlsSettings: View {
Toggle("Seek with horizontal swipe on video", isOn: $horizontalPlayerGestureEnabled)
}
private var avPlayerUsesSystemControlsToggle: some View {
Toggle("Use system controls with AVPlayer", isOn: $avPlayerUsesSystemControls)
}
private var seekGestureSensitivityPicker: some View {
Picker("Seek gesture sensitivity", selection: $seekGestureSensitivity) {
Text("Highest").tag(1.0)

View File

@ -723,6 +723,8 @@
37B767DD2677C3CA0098BAA8 /* PlayerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */; };
37B795902771DAE0001CF27B /* OpenURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */; };
37B795912771DAE0001CF27B /* OpenURLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */; };
37B7CFE92A19603B001B0564 /* ToolbarBackground+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B7CFE82A19603B001B0564 /* ToolbarBackground+Backport.swift */; };
37B7CFEB2A1960EC001B0564 /* ToolbarColorScheme+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B7CFEA2A1960EC001B0564 /* ToolbarColorScheme+Backport.swift */; };
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */; };
37B81AFA26D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */; };
37B81AFC26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */; };
@ -885,6 +887,10 @@
37D9BA0829507F69002586BD /* PlayerControlsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D9BA0529507F69002586BD /* PlayerControlsSettings.swift */; };
37DCD3112A18E8150059A470 /* OrientationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DCD3102A18E8150059A470 /* OrientationModel.swift */; };
37DCD3152A18F7630059A470 /* SafeAreaModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DCD3142A18F7630059A470 /* SafeAreaModel.swift */; };
37DCD3172A191A180059A470 /* AVPlayerViewController+FullScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DCD3162A191A180059A470 /* AVPlayerViewController+FullScreen.swift */; };
37DCD3182A191A180059A470 /* AVPlayerViewController+FullScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DCD3162A191A180059A470 /* AVPlayerViewController+FullScreen.swift */; };
37DCD3192A191A180059A470 /* AVPlayerViewController+FullScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DCD3162A191A180059A470 /* AVPlayerViewController+FullScreen.swift */; };
37DCD31A2A191A180059A470 /* AVPlayerViewController+FullScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DCD3162A191A180059A470 /* AVPlayerViewController+FullScreen.swift */; };
37DD87C7271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
37DD87C8271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */; };
@ -1379,6 +1385,8 @@
37B4E804277D0AB4004BF56A /* Orientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Orientation.swift; sourceTree = "<group>"; };
37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerModel.swift; sourceTree = "<group>"; };
37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenURLHandler.swift; sourceTree = "<group>"; };
37B7CFE82A19603B001B0564 /* ToolbarBackground+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ToolbarBackground+Backport.swift"; sourceTree = "<group>"; };
37B7CFEA2A1960EC001B0564 /* ToolbarColorScheme+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ToolbarColorScheme+Backport.swift"; sourceTree = "<group>"; };
37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSizeModifier.swift; sourceTree = "<group>"; };
37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsPaddingModifier.swift; sourceTree = "<group>"; };
37B81AFE26D2CA3700675966 /* VideoDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetails.swift; sourceTree = "<group>"; };
@ -1458,6 +1466,7 @@
37D9BA0529507F69002586BD /* PlayerControlsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerControlsSettings.swift; sourceTree = "<group>"; };
37DCD3102A18E8150059A470 /* OrientationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrientationModel.swift; sourceTree = "<group>"; };
37DCD3142A18F7630059A470 /* SafeAreaModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeAreaModel.swift; sourceTree = "<group>"; };
37DCD3162A191A180059A470 /* AVPlayerViewController+FullScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVPlayerViewController+FullScreen.swift"; sourceTree = "<group>"; };
37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStreams.swift; sourceTree = "<group>"; };
37DD9DA22785BBC900539416 /* NoCommentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoCommentsView.swift; sourceTree = "<group>"; };
37DD9DAF2785D58D00539416 /* RefreshControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshControl.swift; sourceTree = "<group>"; };
@ -1897,6 +1906,8 @@
37E80F3F287B472300561799 /* ScrollContentBackground+Backport.swift */,
376E331128AD3B320070E30C /* ScrollDismissesKeyboard+Backport.swift */,
3722AEBF274DAEB8005EA4D6 /* Tint+Backport.swift */,
37B7CFE82A19603B001B0564 /* ToolbarBackground+Backport.swift */,
37B7CFEA2A1960EC001B0564 /* ToolbarColorScheme+Backport.swift */,
3727B74927872A920021C15E /* VisualEffectBlur-iOS.swift */,
3727B74727872A500021C15E /* VisualEffectBlur-macOS.swift */,
);
@ -2228,6 +2239,7 @@
isa = PBXGroup;
children = (
379775922689365600DD52A8 /* Array+Next.swift */,
37DCD3162A191A180059A470 /* AVPlayerViewController+FullScreen.swift */,
376578842685429C00D4EA09 /* CaseIterable+Next.swift */,
37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */,
378AE942274EF00A006A4EE1 /* Color+Background.swift */,
@ -3296,6 +3308,8 @@
37BE0BCF26A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
377692562946476F0055EC18 /* ChannelPlaylistsCacheModel.swift in Sources */,
3769C02E2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */,
37B7CFE92A19603B001B0564 /* ToolbarBackground+Backport.swift in Sources */,
37DCD3172A191A180059A470 /* AVPlayerViewController+FullScreen.swift in Sources */,
379EF9E029AA585F009FE6C6 /* HideShortsButtons.swift in Sources */,
37F5E8B6291BE9D0006C15F5 /* URLBookmarkModel.swift in Sources */,
37579D5D27864F5F00FD0B98 /* Help.swift in Sources */,
@ -3305,6 +3319,7 @@
375EC972289F2ABF00751258 /* MultiselectRow.swift in Sources */,
37001563271B1F250049C794 /* AccountsModel.swift in Sources */,
3795593627B08538007FF8F4 /* StreamControl.swift in Sources */,
37B7CFEB2A1960EC001B0564 /* ToolbarColorScheme+Backport.swift in Sources */,
37A2B346294723850050933E /* CacheModel.swift in Sources */,
37CC3F50270D010D00608308 /* VideoBanner.swift in Sources */,
378E50FB26FE8B9F00F49626 /* Instance.swift in Sources */,
@ -3558,6 +3573,7 @@
3711404026B206A6005B3555 /* SearchModel.swift in Sources */,
37484C2A26FC83FF00287258 /* AccountForm.swift in Sources */,
37BE0BD026A0E2D50092E2DB /* VideoPlayerView.swift in Sources */,
37DCD3182A191A180059A470 /* AVPlayerViewController+FullScreen.swift in Sources */,
373CFAEC26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */,
37D2E0D528B67EFC00F64D52 /* Delay.swift in Sources */,
37977584268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
@ -3615,6 +3631,7 @@
3774124D27387D2300423605 /* PlaylistsModel.swift in Sources */,
3774124B27387D2300423605 /* ThumbnailsModel.swift in Sources */,
3774125427387D2300423605 /* Store.swift in Sources */,
37DCD31A2A191A180059A470 /* AVPlayerViewController+FullScreen.swift in Sources */,
3774125027387D2300423605 /* Video.swift in Sources */,
37EF9A79275BEB8E0043B585 /* CommentView.swift in Sources */,
3774125327387D2300423605 /* Country.swift in Sources */,
@ -3813,6 +3830,7 @@
372D85E0283842EE00FF3C7D /* PlayerLayerView.swift in Sources */,
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */,
37F64FE626FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
37DCD3192A191A180059A470 /* AVPlayerViewController+FullScreen.swift in Sources */,
37B2631C2735EAAB00FE0D40 /* FavoriteResourceObserver.swift in Sources */,
37484C2B26FC83FF00287258 /* AccountForm.swift in Sources */,
37FB2860272225E800A57617 /* ContentItemView.swift in Sources */,