mirror of
https://github.com/yattee/yattee.git
synced 2025-08-09 20:24:06 +00:00
AVPlayer system controls on iOS
This commit is contained in:
@@ -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() {}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation)
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: rotateToLandscapeOnEnterFullScreen.interaceOrientation)
|
||||
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)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
|
||||
avPlayerBackend.controller.exitFullScreen(animated: true)
|
||||
}
|
||||
let rotationOrientation = rotateToPortraitOnExitFullScreen ? UIInterfaceOrientation.portrait : nil
|
||||
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: rotationOrientation)
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user