Initial commit: SimVision tvOS streaming app
Features: - VOD library with movie grouping and version detection - TV show library with season/episode organization - TMDB integration for trending shows and recently aired episodes - Recent releases section with TMDB release date sorting - Watch history tracking with continue watching - Playlist caching (12-hour TTL) for offline support - M3U playlist parsing with XStream API support - Authentication with credential storage Technical: - SwiftUI for tvOS - Actor-based services for thread safety - Persistent caching for playlists, TMDB data, and watch history - KSPlayer integration for video playback Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
707
KSPlayer-main/Sources/KSPlayer/AVPlayer/KSPlayerLayer.swift
Normal file
707
KSPlayer-main/Sources/KSPlayer/AVPlayer/KSPlayerLayer.swift
Normal file
@@ -0,0 +1,707 @@
|
||||
//
|
||||
// KSPlayerLayerView.swift
|
||||
// Pods
|
||||
//
|
||||
// Created by kintan on 16/4/28.
|
||||
//
|
||||
//
|
||||
import AVFoundation
|
||||
import AVKit
|
||||
import MediaPlayer
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
/**
|
||||
Player status emun
|
||||
- setURL: set url
|
||||
- readyToPlay: player ready to play
|
||||
- buffering: player buffering
|
||||
- bufferFinished: buffer finished
|
||||
- playedToTheEnd: played to the End
|
||||
- error: error with playing
|
||||
*/
|
||||
public enum KSPlayerState: CustomStringConvertible {
|
||||
case initialized
|
||||
case preparing
|
||||
case readyToPlay
|
||||
case buffering
|
||||
case bufferFinished
|
||||
case paused
|
||||
case playedToTheEnd
|
||||
case error
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .initialized:
|
||||
return "initialized"
|
||||
case .preparing:
|
||||
return "preparing"
|
||||
case .readyToPlay:
|
||||
return "readyToPlay"
|
||||
case .buffering:
|
||||
return "buffering"
|
||||
case .bufferFinished:
|
||||
return "bufferFinished"
|
||||
case .paused:
|
||||
return "paused"
|
||||
case .playedToTheEnd:
|
||||
return "playedToTheEnd"
|
||||
case .error:
|
||||
return "error"
|
||||
}
|
||||
}
|
||||
|
||||
public var isPlaying: Bool { self == .buffering || self == .bufferFinished }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public protocol KSPlayerLayerDelegate: AnyObject {
|
||||
func player(layer: KSPlayerLayer, state: KSPlayerState)
|
||||
func player(layer: KSPlayerLayer, currentTime: TimeInterval, totalTime: TimeInterval)
|
||||
func player(layer: KSPlayerLayer, finish error: Error?)
|
||||
func player(layer: KSPlayerLayer, bufferedCount: Int, consumeTime: TimeInterval)
|
||||
}
|
||||
|
||||
open class KSPlayerLayer: NSObject {
|
||||
public weak var delegate: KSPlayerLayerDelegate?
|
||||
@Published
|
||||
public var bufferingProgress: Int = 0
|
||||
@Published
|
||||
public var loopCount: Int = 0
|
||||
@Published
|
||||
public var isPipActive = false {
|
||||
didSet {
|
||||
if #available(tvOS 14.0, *) {
|
||||
guard let pipController = player.pipController else {
|
||||
return
|
||||
}
|
||||
|
||||
if isPipActive {
|
||||
// 一定要async才不会pip之后就暂停播放
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
pipController.start(view: self)
|
||||
}
|
||||
} else {
|
||||
pipController.stop(restoreUserInterface: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public private(set) var options: KSOptions
|
||||
|
||||
public var player: MediaPlayerProtocol {
|
||||
didSet {
|
||||
KSLog("player is \(player)")
|
||||
state = .initialized
|
||||
runOnMainThread { [weak self] in
|
||||
guard let self else { return }
|
||||
if let oldView = oldValue.view, let superview = oldView.superview, let view = player.view {
|
||||
#if canImport(UIKit)
|
||||
superview.insertSubview(view, belowSubview: oldView)
|
||||
#else
|
||||
superview.addSubview(view, positioned: .below, relativeTo: oldView)
|
||||
#endif
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
view.topAnchor.constraint(equalTo: superview.topAnchor),
|
||||
view.leadingAnchor.constraint(equalTo: superview.leadingAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: superview.bottomAnchor),
|
||||
view.trailingAnchor.constraint(equalTo: superview.trailingAnchor),
|
||||
])
|
||||
}
|
||||
oldValue.view?.removeFromSuperview()
|
||||
}
|
||||
player.playbackRate = oldValue.playbackRate
|
||||
player.playbackVolume = oldValue.playbackVolume
|
||||
player.delegate = self
|
||||
player.contentMode = .scaleAspectFit
|
||||
if isAutoPlay {
|
||||
prepareToPlay()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public private(set) var url: URL {
|
||||
didSet {
|
||||
let firstPlayerType: MediaPlayerProtocol.Type
|
||||
if isWirelessRouteActive {
|
||||
// airplay的话,默认使用KSAVPlayer
|
||||
firstPlayerType = KSAVPlayer.self
|
||||
} else if options.display != .plane {
|
||||
// AR模式只能用KSMEPlayer
|
||||
// swiftlint:disable force_cast
|
||||
firstPlayerType = NSClassFromString("KSPlayer.KSMEPlayer") as! MediaPlayerProtocol.Type
|
||||
// swiftlint:enable force_cast
|
||||
} else {
|
||||
firstPlayerType = KSOptions.firstPlayerType
|
||||
}
|
||||
if type(of: player) == firstPlayerType {
|
||||
if url == oldValue {
|
||||
if isAutoPlay {
|
||||
play()
|
||||
}
|
||||
} else {
|
||||
stop()
|
||||
player.replace(url: url, options: options)
|
||||
if isAutoPlay {
|
||||
prepareToPlay()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stop()
|
||||
player = firstPlayerType.init(url: url, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 播发器的几种状态
|
||||
|
||||
public private(set) var state = KSPlayerState.initialized {
|
||||
willSet {
|
||||
if state != newValue {
|
||||
runOnMainThread { [weak self] in
|
||||
guard let self else { return }
|
||||
KSLog("playerStateDidChange - \(newValue)")
|
||||
self.delegate?.player(layer: self, state: newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var timer: Timer = .scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
|
||||
guard let self, self.player.isReadyToPlay else {
|
||||
return
|
||||
}
|
||||
self.delegate?.player(layer: self, currentTime: self.player.currentPlaybackTime, totalTime: self.player.duration)
|
||||
if self.player.playbackState == .playing, self.player.loadState == .playable, self.state == .buffering {
|
||||
// 一个兜底保护,正常不能走到这里
|
||||
self.state = .bufferFinished
|
||||
}
|
||||
if self.player.isPlaying {
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyElapsedPlaybackTime] = self.player.currentPlaybackTime
|
||||
}
|
||||
}
|
||||
|
||||
private var urls = [URL]()
|
||||
private var isAutoPlay: Bool
|
||||
private var isWirelessRouteActive = false
|
||||
private var bufferedCount = 0
|
||||
private var shouldSeekTo: TimeInterval = 0
|
||||
private var startTime: TimeInterval = 0
|
||||
public init(url: URL, isAutoPlay: Bool = KSOptions.isAutoPlay, options: KSOptions, delegate: KSPlayerLayerDelegate? = nil) {
|
||||
self.url = url
|
||||
self.options = options
|
||||
self.delegate = delegate
|
||||
let firstPlayerType: MediaPlayerProtocol.Type
|
||||
if options.display != .plane {
|
||||
// AR模式只能用KSMEPlayer
|
||||
// swiftlint:disable force_cast
|
||||
firstPlayerType = NSClassFromString("KSPlayer.KSMEPlayer") as! MediaPlayerProtocol.Type
|
||||
// swiftlint:enable force_cast
|
||||
} else {
|
||||
firstPlayerType = KSOptions.firstPlayerType
|
||||
}
|
||||
player = firstPlayerType.init(url: url, options: options)
|
||||
self.isAutoPlay = isAutoPlay
|
||||
super.init()
|
||||
player.playbackRate = options.startPlayRate
|
||||
if options.registerRemoteControll {
|
||||
registerRemoteControllEvent()
|
||||
}
|
||||
player.delegate = self
|
||||
player.contentMode = .scaleAspectFit
|
||||
if isAutoPlay {
|
||||
prepareToPlay()
|
||||
}
|
||||
#if canImport(UIKit)
|
||||
runOnMainThread { [weak self] in
|
||||
guard let self else { return }
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(enterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(enterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
}
|
||||
#if !os(xrOS)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(wirelessRouteActiveDidChange(notification:)), name: .MPVolumeViewWirelessRouteActiveDidChange, object: nil)
|
||||
#endif
|
||||
#endif
|
||||
#if !os(macOS)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(audioInterrupted), name: AVAudioSession.interruptionNotification, object: nil)
|
||||
#endif
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
public required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
if #available(iOS 15.0, tvOS 15.0, macOS 12.0, *) {
|
||||
player.pipController?.contentSource = nil
|
||||
}
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
||||
MPRemoteCommandCenter.shared().playCommand.removeTarget(nil)
|
||||
MPRemoteCommandCenter.shared().pauseCommand.removeTarget(nil)
|
||||
MPRemoteCommandCenter.shared().togglePlayPauseCommand.removeTarget(nil)
|
||||
MPRemoteCommandCenter.shared().stopCommand.removeTarget(nil)
|
||||
MPRemoteCommandCenter.shared().nextTrackCommand.removeTarget(nil)
|
||||
MPRemoteCommandCenter.shared().previousTrackCommand.removeTarget(nil)
|
||||
MPRemoteCommandCenter.shared().changeRepeatModeCommand.removeTarget(nil)
|
||||
MPRemoteCommandCenter.shared().changePlaybackRateCommand.removeTarget(nil)
|
||||
MPRemoteCommandCenter.shared().skipForwardCommand.removeTarget(nil)
|
||||
MPRemoteCommandCenter.shared().skipBackwardCommand.removeTarget(nil)
|
||||
MPRemoteCommandCenter.shared().changePlaybackPositionCommand.removeTarget(nil)
|
||||
MPRemoteCommandCenter.shared().enableLanguageOptionCommand.removeTarget(nil)
|
||||
options.playerLayerDeinit()
|
||||
}
|
||||
|
||||
public func set(url: URL, options: KSOptions) {
|
||||
self.options = options
|
||||
runOnMainThread {
|
||||
self.url = url
|
||||
}
|
||||
}
|
||||
|
||||
public func set(urls: [URL], options: KSOptions) {
|
||||
self.options = options
|
||||
self.urls.removeAll()
|
||||
self.urls.append(contentsOf: urls)
|
||||
if let first = urls.first {
|
||||
runOnMainThread {
|
||||
self.url = first
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open func play() {
|
||||
runOnMainThread {
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
}
|
||||
isAutoPlay = true
|
||||
if state == .error || state == .initialized {
|
||||
prepareToPlay()
|
||||
}
|
||||
if player.isReadyToPlay {
|
||||
if state == .playedToTheEnd {
|
||||
player.seek(time: 0) { [weak self] finished in
|
||||
guard let self else { return }
|
||||
if finished {
|
||||
self.player.play()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
player.play()
|
||||
}
|
||||
timer.fireDate = Date.distantPast
|
||||
}
|
||||
state = player.loadState == .playable ? .bufferFinished : .buffering
|
||||
MPNowPlayingInfoCenter.default().playbackState = .playing
|
||||
if #available(tvOS 14.0, *) {
|
||||
KSPictureInPictureController.mute()
|
||||
}
|
||||
}
|
||||
|
||||
open func pause() {
|
||||
isAutoPlay = false
|
||||
player.pause()
|
||||
timer.fireDate = Date.distantFuture
|
||||
state = .paused
|
||||
MPNowPlayingInfoCenter.default().playbackState = .paused
|
||||
runOnMainThread {
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
}
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
KSLog("stop Player")
|
||||
state = .initialized
|
||||
player.shutdown()
|
||||
bufferedCount = 0
|
||||
shouldSeekTo = 0
|
||||
player.playbackRate = 1
|
||||
player.playbackVolume = 1
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
||||
runOnMainThread {
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
}
|
||||
}
|
||||
|
||||
open func seek(time: TimeInterval, autoPlay: Bool, completion: @escaping ((Bool) -> Void)) {
|
||||
if time.isInfinite || time.isNaN {
|
||||
completion(false)
|
||||
}
|
||||
if player.isReadyToPlay, player.seekable {
|
||||
player.seek(time: time) { [weak self] finished in
|
||||
guard let self else { return }
|
||||
if finished, autoPlay {
|
||||
self.play()
|
||||
}
|
||||
completion(finished)
|
||||
}
|
||||
} else {
|
||||
isAutoPlay = autoPlay
|
||||
shouldSeekTo = time
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - MediaPlayerDelegate
|
||||
|
||||
extension KSPlayerLayer: MediaPlayerDelegate {
|
||||
public func readyToPlay(player: some MediaPlayerProtocol) {
|
||||
state = .readyToPlay
|
||||
#if os(macOS)
|
||||
runOnMainThread { [weak self] in
|
||||
guard let self else { return }
|
||||
if let window = player.view?.window {
|
||||
window.isMovableByWindowBackground = true
|
||||
if options.automaticWindowResize {
|
||||
let naturalSize = player.naturalSize
|
||||
if naturalSize.width > 0, naturalSize.height > 0 {
|
||||
window.aspectRatio = naturalSize
|
||||
var frame = window.frame
|
||||
frame.size.height = frame.width * naturalSize.height / naturalSize.width
|
||||
window.setFrame(frame, display: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if !os(macOS) && !os(tvOS)
|
||||
if #available(iOS 14.2, *) {
|
||||
if options.canStartPictureInPictureAutomaticallyFromInline {
|
||||
player.pipController?.canStartPictureInPictureAutomaticallyFromInline = true
|
||||
}
|
||||
}
|
||||
#endif
|
||||
updateNowPlayingInfo()
|
||||
if isAutoPlay {
|
||||
if shouldSeekTo > 0 {
|
||||
seek(time: shouldSeekTo, autoPlay: true) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.shouldSeekTo = 0
|
||||
}
|
||||
|
||||
} else {
|
||||
play()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func changeLoadState(player: some MediaPlayerProtocol) {
|
||||
guard player.playbackState != .seeking else { return }
|
||||
if player.loadState == .playable, startTime > 0 {
|
||||
let diff = CACurrentMediaTime() - startTime
|
||||
runOnMainThread { [weak self] in
|
||||
guard let self else { return }
|
||||
delegate?.player(layer: self, bufferedCount: bufferedCount, consumeTime: diff)
|
||||
}
|
||||
if bufferedCount == 0 {
|
||||
var dic = ["firstTime": diff]
|
||||
if options.tcpConnectedTime > 0 {
|
||||
dic["initTime"] = options.dnsStartTime - startTime
|
||||
dic["dnsTime"] = options.tcpStartTime - options.dnsStartTime
|
||||
dic["tcpTime"] = options.tcpConnectedTime - options.tcpStartTime
|
||||
dic["openTime"] = options.openTime - options.tcpConnectedTime
|
||||
dic["findTime"] = options.findTime - options.openTime
|
||||
} else {
|
||||
dic["openTime"] = options.openTime - startTime
|
||||
}
|
||||
dic["findTime"] = options.findTime - options.openTime
|
||||
dic["readyTime"] = options.readyTime - options.findTime
|
||||
dic["readVideoTime"] = options.readVideoTime - options.readyTime
|
||||
dic["readAudioTime"] = options.readAudioTime - options.readyTime
|
||||
dic["decodeVideoTime"] = options.decodeVideoTime - options.readVideoTime
|
||||
dic["decodeAudioTime"] = options.decodeAudioTime - options.readAudioTime
|
||||
KSLog(dic)
|
||||
}
|
||||
bufferedCount += 1
|
||||
startTime = 0
|
||||
}
|
||||
guard state.isPlaying else { return }
|
||||
if player.loadState == .playable {
|
||||
state = .bufferFinished
|
||||
} else {
|
||||
if state == .bufferFinished {
|
||||
startTime = CACurrentMediaTime()
|
||||
}
|
||||
state = .buffering
|
||||
}
|
||||
}
|
||||
|
||||
public func changeBuffering(player _: some MediaPlayerProtocol, progress: Int) {
|
||||
bufferingProgress = progress
|
||||
}
|
||||
|
||||
public func playBack(player _: some MediaPlayerProtocol, loopCount: Int) {
|
||||
self.loopCount = loopCount
|
||||
}
|
||||
|
||||
public func finish(player: some MediaPlayerProtocol, error: Error?) {
|
||||
if let error {
|
||||
if type(of: player) != KSOptions.secondPlayerType, let secondPlayerType = KSOptions.secondPlayerType {
|
||||
self.player = secondPlayerType.init(url: url, options: options)
|
||||
return
|
||||
}
|
||||
state = .error
|
||||
KSLog(error as CustomStringConvertible)
|
||||
} else {
|
||||
let duration = player.duration
|
||||
runOnMainThread { [weak self] in
|
||||
guard let self else { return }
|
||||
delegate?.player(layer: self, currentTime: duration, totalTime: duration)
|
||||
}
|
||||
state = .playedToTheEnd
|
||||
}
|
||||
timer.fireDate = Date.distantFuture
|
||||
bufferedCount = 1
|
||||
runOnMainThread { [weak self] in
|
||||
guard let self else { return }
|
||||
delegate?.player(layer: self, finish: error)
|
||||
}
|
||||
if error == nil {
|
||||
nextPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AVPictureInPictureControllerDelegate
|
||||
|
||||
@available(tvOS 14.0, *)
|
||||
extension KSPlayerLayer: AVPictureInPictureControllerDelegate {
|
||||
public func pictureInPictureControllerDidStopPictureInPicture(_: AVPictureInPictureController) {
|
||||
player.pipController?.stop(restoreUserInterface: false)
|
||||
}
|
||||
|
||||
public func pictureInPictureController(_: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler _: @escaping (Bool) -> Void) {
|
||||
isPipActive = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - private functions
|
||||
|
||||
extension KSPlayerLayer {
|
||||
open func prepareToPlay() {
|
||||
state = .preparing
|
||||
startTime = CACurrentMediaTime()
|
||||
bufferedCount = 0
|
||||
player.prepareToPlay()
|
||||
}
|
||||
|
||||
private func updateNowPlayingInfo() {
|
||||
if MPNowPlayingInfoCenter.default().nowPlayingInfo == nil {
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = [MPMediaItemPropertyPlaybackDuration: player.duration]
|
||||
} else {
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyPlaybackDuration] = player.duration
|
||||
}
|
||||
if MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyTitle] == nil, let title = player.dynamicInfo?.metadata["title"] {
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyTitle] = title
|
||||
}
|
||||
if MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyArtist] == nil, let artist = player.dynamicInfo?.metadata["artist"] {
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPMediaItemPropertyArtist] = artist
|
||||
}
|
||||
var current: [MPNowPlayingInfoLanguageOption] = []
|
||||
var langs: [MPNowPlayingInfoLanguageOptionGroup] = []
|
||||
for track in player.tracks(mediaType: .audio) {
|
||||
if let lang = track.language {
|
||||
let audioLang = MPNowPlayingInfoLanguageOption(type: .audible, languageTag: lang, characteristics: nil, displayName: track.name, identifier: track.name)
|
||||
let audioGroup = MPNowPlayingInfoLanguageOptionGroup(languageOptions: [audioLang], defaultLanguageOption: nil, allowEmptySelection: false)
|
||||
langs.append(audioGroup)
|
||||
if track.isEnabled {
|
||||
current.append(audioLang)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !langs.isEmpty {
|
||||
MPRemoteCommandCenter.shared().enableLanguageOptionCommand.isEnabled = true
|
||||
}
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyAvailableLanguageOptions] = langs
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo?[MPNowPlayingInfoPropertyCurrentLanguageOptions] = current
|
||||
}
|
||||
|
||||
private func nextPlayer() {
|
||||
if urls.count > 1, let index = urls.firstIndex(of: url), index < urls.count - 1 {
|
||||
isAutoPlay = true
|
||||
url = urls[index + 1]
|
||||
}
|
||||
}
|
||||
|
||||
private func previousPlayer() {
|
||||
if urls.count > 1, let index = urls.firstIndex(of: url), index > 0 {
|
||||
isAutoPlay = true
|
||||
url = urls[index - 1]
|
||||
}
|
||||
}
|
||||
|
||||
func seek(time: TimeInterval) {
|
||||
seek(time: time, autoPlay: options.isSeekedAutoPlay) { _ in
|
||||
}
|
||||
}
|
||||
|
||||
public func registerRemoteControllEvent() {
|
||||
let remoteCommand = MPRemoteCommandCenter.shared()
|
||||
remoteCommand.playCommand.addTarget { [weak self] _ in
|
||||
guard let self else {
|
||||
return .commandFailed
|
||||
}
|
||||
self.play()
|
||||
return .success
|
||||
}
|
||||
remoteCommand.pauseCommand.addTarget { [weak self] _ in
|
||||
guard let self else {
|
||||
return .commandFailed
|
||||
}
|
||||
self.pause()
|
||||
return .success
|
||||
}
|
||||
remoteCommand.togglePlayPauseCommand.addTarget { [weak self] _ in
|
||||
guard let self else {
|
||||
return .commandFailed
|
||||
}
|
||||
if self.state.isPlaying {
|
||||
self.pause()
|
||||
} else {
|
||||
self.play()
|
||||
}
|
||||
return .success
|
||||
}
|
||||
remoteCommand.stopCommand.addTarget { [weak self] _ in
|
||||
guard let self else {
|
||||
return .commandFailed
|
||||
}
|
||||
self.player.shutdown()
|
||||
return .success
|
||||
}
|
||||
remoteCommand.nextTrackCommand.addTarget { [weak self] _ in
|
||||
guard let self else {
|
||||
return .commandFailed
|
||||
}
|
||||
self.nextPlayer()
|
||||
return .success
|
||||
}
|
||||
remoteCommand.previousTrackCommand.addTarget { [weak self] _ in
|
||||
guard let self else {
|
||||
return .commandFailed
|
||||
}
|
||||
self.previousPlayer()
|
||||
return .success
|
||||
}
|
||||
remoteCommand.changeRepeatModeCommand.addTarget { [weak self] event in
|
||||
guard let self, let event = event as? MPChangeRepeatModeCommandEvent else {
|
||||
return .commandFailed
|
||||
}
|
||||
self.options.isLoopPlay = event.repeatType != .off
|
||||
return .success
|
||||
}
|
||||
remoteCommand.changeShuffleModeCommand.isEnabled = false
|
||||
// remoteCommand.changeShuffleModeCommand.addTarget {})
|
||||
remoteCommand.changePlaybackRateCommand.supportedPlaybackRates = [0.5, 1, 1.5, 2]
|
||||
remoteCommand.changePlaybackRateCommand.addTarget { [weak self] event in
|
||||
guard let self, let event = event as? MPChangePlaybackRateCommandEvent else {
|
||||
return .commandFailed
|
||||
}
|
||||
self.player.playbackRate = event.playbackRate
|
||||
return .success
|
||||
}
|
||||
remoteCommand.skipForwardCommand.preferredIntervals = [15]
|
||||
remoteCommand.skipForwardCommand.addTarget { [weak self] event in
|
||||
guard let self, let event = event as? MPSkipIntervalCommandEvent else {
|
||||
return .commandFailed
|
||||
}
|
||||
self.seek(time: self.player.currentPlaybackTime + event.interval)
|
||||
return .success
|
||||
}
|
||||
remoteCommand.skipBackwardCommand.preferredIntervals = [15]
|
||||
remoteCommand.skipBackwardCommand.addTarget { [weak self] event in
|
||||
guard let self, let event = event as? MPSkipIntervalCommandEvent else {
|
||||
return .commandFailed
|
||||
}
|
||||
self.seek(time: self.player.currentPlaybackTime - event.interval)
|
||||
return .success
|
||||
}
|
||||
remoteCommand.changePlaybackPositionCommand.addTarget { [weak self] event in
|
||||
guard let self, let event = event as? MPChangePlaybackPositionCommandEvent else {
|
||||
return .commandFailed
|
||||
}
|
||||
self.seek(time: event.positionTime)
|
||||
return .success
|
||||
}
|
||||
remoteCommand.enableLanguageOptionCommand.addTarget { [weak self] event in
|
||||
guard let self, let event = event as? MPChangeLanguageOptionCommandEvent else {
|
||||
return .commandFailed
|
||||
}
|
||||
let selectLang = event.languageOption
|
||||
if selectLang.languageOptionType == .audible,
|
||||
let trackToSelect = self.player.tracks(mediaType: .audio).first(where: { $0.name == selectLang.displayName })
|
||||
{
|
||||
self.player.select(track: trackToSelect)
|
||||
}
|
||||
return .success
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func enterBackground() {
|
||||
guard state.isPlaying, !player.isExternalPlaybackActive else {
|
||||
return
|
||||
}
|
||||
if #available(tvOS 14.0, *), player.pipController?.isPictureInPictureActive == true {
|
||||
return
|
||||
}
|
||||
|
||||
if KSOptions.canBackgroundPlay {
|
||||
player.enterBackground()
|
||||
return
|
||||
}
|
||||
pause()
|
||||
}
|
||||
|
||||
@objc private func enterForeground() {
|
||||
if KSOptions.canBackgroundPlay {
|
||||
player.enterForeground()
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(UIKit) && !os(xrOS)
|
||||
@MainActor
|
||||
@objc private func wirelessRouteActiveDidChange(notification: Notification) {
|
||||
guard let volumeView = notification.object as? MPVolumeView, isWirelessRouteActive != volumeView.isWirelessRouteActive else { return }
|
||||
if volumeView.isWirelessRouteActive {
|
||||
if !player.allowsExternalPlayback {
|
||||
isWirelessRouteActive = true
|
||||
}
|
||||
player.usesExternalPlaybackWhileExternalScreenIsActive = true
|
||||
}
|
||||
isWirelessRouteActive = volumeView.isWirelessRouteActive
|
||||
}
|
||||
#endif
|
||||
#if !os(macOS)
|
||||
@objc private func audioInterrupted(notification: Notification) {
|
||||
guard let userInfo = notification.userInfo,
|
||||
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
|
||||
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
|
||||
else {
|
||||
return
|
||||
}
|
||||
switch type {
|
||||
case .began:
|
||||
pause()
|
||||
|
||||
case .ended:
|
||||
// An interruption ended. Resume playback, if appropriate.
|
||||
|
||||
guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
|
||||
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
|
||||
if options.contains(.shouldResume) {
|
||||
play()
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Reference in New Issue
Block a user