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

@@ -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
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)
}

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)
}
}
}