From c3b6a631ce22836f648fdb9bc3eb08baaec1889d Mon Sep 17 00:00:00 2001 From: Mohammed Elnaggar Date: Mon, 15 Mar 2021 19:27:43 +0200 Subject: [PATCH 01/10] add setRate func to Player Protocol --- Sources/Player.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/Player.swift b/Sources/Player.swift index 9df3beb..cedc0d3 100644 --- a/Sources/Player.swift +++ b/Sources/Player.swift @@ -76,6 +76,9 @@ public enum PlayerError: Int { /// Play the video func play() + /// Set Rate of video + func setRate(_ rate: Float) + /// Pause the video func pause() } From 457752a1d3ddc4fd2ace809cf3fc8e3841301de9 Mon Sep 17 00:00:00 2001 From: Mohammed Elnaggar Date: Mon, 15 Mar 2021 19:29:38 +0200 Subject: [PATCH 02/10] implement setRate func --- Sources/RegularPlayer.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/RegularPlayer.swift b/Sources/RegularPlayer.swift index 33de027..961f5b6 100644 --- a/Sources/RegularPlayer.swift +++ b/Sources/RegularPlayer.swift @@ -136,6 +136,10 @@ extension AVMediaSelectionOption: TextTrackMetadata { self.player.play() } + open func setRate(_ rate: Float) { + self.player.rate = rate + } + open func pause() { self.player.pause() } From 17b4efbdf2d0d4d3bec9ba66b9fffc4640cbdfd9 Mon Sep 17 00:00:00 2001 From: Mohammad Elnaggar Date: Tue, 16 Mar 2021 19:07:38 +0200 Subject: [PATCH 03/10] add scaleToFit gravity --- Sources/Player.swift | 1 + Sources/RegularPlayer.swift | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Sources/Player.swift b/Sources/Player.swift index cedc0d3..eff3ac6 100644 --- a/Sources/Player.swift +++ b/Sources/Player.swift @@ -110,6 +110,7 @@ public enum PlayerError: Int { @objc public enum FillMode: Int { case fit case fill + case scaleToFit } /// The metadata that should be attached to any type of text track. diff --git a/Sources/RegularPlayer.swift b/Sources/RegularPlayer.swift index 961f5b6..6ec8dc6 100644 --- a/Sources/RegularPlayer.swift +++ b/Sources/RegularPlayer.swift @@ -428,6 +428,9 @@ extension RegularPlayer: FillModeCapable { case .fill: gravity = .resizeAspectFill + case .scaleToFit: + + gravity = .resize } (self.view.layer as! AVPlayerLayer).videoGravity = gravity From 7f10a708d86bf742f843d2bb5da0d6da93bb13df Mon Sep 17 00:00:00 2001 From: Mohammed Elnaggar Date: Mon, 19 Sep 2022 15:31:27 +0200 Subject: [PATCH 04/10] Update RegularPlayer.swift --- Sources/RegularPlayer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/RegularPlayer.swift b/Sources/RegularPlayer.swift index 6ec8dc6..db133e2 100644 --- a/Sources/RegularPlayer.swift +++ b/Sources/RegularPlayer.swift @@ -35,7 +35,7 @@ extension AVMediaSelectionOption: TextTrackMetadata { private var seekTolerance: CMTime? private var seekTarget: CMTime = CMTime.invalid - private var isSeekInProgress: Bool = false + public var isSeekInProgress: Bool = false // MARK: - Public API From 9b3c0e568dbbfb09ac6647aaac1434d567b11659 Mon Sep 17 00:00:00 2001 From: Mohammed Elnaggar Date: Mon, 19 Sep 2022 15:32:29 +0200 Subject: [PATCH 05/10] Update Player.swift --- Sources/Player.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Player.swift b/Sources/Player.swift index eff3ac6..2fa7831 100644 --- a/Sources/Player.swift +++ b/Sources/Player.swift @@ -62,6 +62,8 @@ public enum PlayerError: Int { var bufferedTime: TimeInterval { get } + var isSeekInProgress: Bool { get } + var playing: Bool { get } var ended: Bool { get } From 229b6653a0d5017f1a9d1a2f394861e9d1a29c05 Mon Sep 17 00:00:00 2001 From: Mohammad Elnaggar Date: Sat, 1 Oct 2022 20:11:47 +0200 Subject: [PATCH 06/10] add isMuted property --- Sources/Player.swift | 2 ++ Sources/RegularPlayer.swift | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/Sources/Player.swift b/Sources/Player.swift index 2fa7831..a0be890 100644 --- a/Sources/Player.swift +++ b/Sources/Player.swift @@ -62,6 +62,8 @@ public enum PlayerError: Int { var bufferedTime: TimeInterval { get } + var isMuted: Bool { get set } + var isSeekInProgress: Bool { get } var playing: Bool { get } diff --git a/Sources/RegularPlayer.swift b/Sources/RegularPlayer.swift index db133e2..ef12d55 100644 --- a/Sources/RegularPlayer.swift +++ b/Sources/RegularPlayer.swift @@ -18,6 +18,7 @@ extension AVMediaSelectionOption: TextTrackMetadata { /// A RegularPlayer is used to play regular videos. @objc open class RegularPlayer: NSObject, Player, ProvidesView { + public struct Constants { public static let TimeUpdateInterval: TimeInterval = 0.1 } @@ -115,6 +116,12 @@ extension AVMediaSelectionOption: TextTrackMetadata { } } + public var isMuted: Bool = true { + didSet { + player.isMuted = isMuted + } + } + public var playing: Bool { return self.player.rate > 0 } From 790f5699102dc8ac12d9f7934d1e00b198522297 Mon Sep 17 00:00:00 2001 From: Mohammed Elnaggar Date: Sun, 2 Mar 2025 02:06:51 +0200 Subject: [PATCH 07/10] Update RegularPlayer.swift --- Sources/RegularPlayer.swift | 297 +++++++++++++++++++++++++----------- 1 file changed, 209 insertions(+), 88 deletions(-) diff --git a/Sources/RegularPlayer.swift b/Sources/RegularPlayer.swift index ef12d55..3d84973 100644 --- a/Sources/RegularPlayer.swift +++ b/Sources/RegularPlayer.swift @@ -18,13 +18,13 @@ extension AVMediaSelectionOption: TextTrackMetadata { /// A RegularPlayer is used to play regular videos. @objc open class RegularPlayer: NSObject, Player, ProvidesView { - + public struct Constants { public static let TimeUpdateInterval: TimeInterval = 0.1 } - + // MARK: - Private Properties - + fileprivate var player = AVPlayer() private var regularPlayerView: RegularPlayerView @@ -37,14 +37,32 @@ extension AVMediaSelectionOption: TextTrackMetadata { private var seekTarget: CMTime = CMTime.invalid public var isSeekInProgress: Bool = false - + + private var lastStateChange: Date = Date() + private var stateChanges: [String: Int] = [:] + // MARK: - Public API - + /// Sets an AVAsset on the player. /// /// - Parameter asset: The AVAsset @objc open func set(_ asset: AVAsset) { + // Create playback options for faster loading + let options: [String: Any] = [ + AVURLAssetPreferPreciseDurationAndTimingKey: true + ] + + // Create a specialized playerItem with custom options let playerItem = AVPlayerItem(asset: asset) + + // Optimize for high-speed playback by setting appropriate values + if #available(iOS 10.0, tvOS 10.0, macOS 10.12, *) { + playerItem.preferredForwardBufferDuration = 10.0 // Start with a good buffer + + // This helps maintain higher playback rates by giving a quality/speed tradeoff hint + playerItem.preferredPeakBitRate = 0 // 0 means no limit + } + self.set(playerItem: playerItem) } @@ -58,19 +76,19 @@ extension AVMediaSelectionOption: TextTrackMetadata { self.addPlayerItemObservers(toPlayerItem: playerItem) self.player.replaceCurrentItem(with: playerItem) } - + // MARK: - ProvidesView - + private class RegularPlayerView: PlayerView { var playerLayer: AVPlayerLayer { return self.layer as! AVPlayerLayer } - #if canImport(UIKit) +#if canImport(UIKit) override class var layerClass: AnyClass { return AVPlayerLayer.self } - #elseif canImport(AppKit) +#elseif canImport(AppKit) override init(frame frameRect: NSRect) { super.init(frame: frameRect) self.layer = AVPlayerLayer() @@ -79,61 +97,63 @@ extension AVMediaSelectionOption: TextTrackMetadata { required init?(coder decoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - #endif - +#endif + func configureForPlayer(player: AVPlayer) { (self.layer as! AVPlayerLayer).player = player } } - + open var view: UIView { return self.regularPlayerView } - + // MARK: - Player - + weak public var delegate: PlayerDelegate? - + public private(set) var state: PlayerState = .ready { didSet { + // Track performance metrics + self.trackPerformance(oldState: oldValue, newState: state) self.delegate?.playerDidUpdateState(player: self, previousState: oldValue) } } - + public var duration: TimeInterval { return self.player.currentItem?.duration.timeInterval ?? 0 } - + public private(set) var time: TimeInterval = 0 { didSet { self.delegate?.playerDidUpdateTime(player: self) } } - + public private(set) var bufferedTime: TimeInterval = 0 { didSet { self.delegate?.playerDidUpdateBufferedTime(player: self) } } - + public var isMuted: Bool = true { didSet { player.isMuted = isMuted } } - + public var playing: Bool { return self.player.rate > 0 } - + public var ended: Bool { return self.time >= self.duration } - + public var error: NSError? { return self.player.errorForPlayerOrItem } - + open func seek(to time: TimeInterval) { let cmTime = CMTimeMakeWithSeconds(time, preferredTimescale: Int32(NSEC_PER_SEC)) self.smoothSeek(to: cmTime) @@ -142,21 +162,45 @@ extension AVMediaSelectionOption: TextTrackMetadata { open func play() { self.player.play() } - + open func setRate(_ rate: Float) { self.player.rate = rate + + if let currentItem = self.player.currentItem { + // Progressive buffer scaling based on rate + if #available(iOS 10.0, tvOS 10.0, macOS 10.12, *) { + // Scale buffer with rate - higher rates need more buffer + let bufferDuration = min(30.0, rate * 5.0) // Cap at reasonable maximum + currentItem.preferredForwardBufferDuration = TimeInterval(bufferDuration) + + // Lower values make playback start faster but might cause more rebuffering + if rate > 2.0 { + currentItem.canUseNetworkResourcesForLiveStreamingWhilePaused = true + self.player.automaticallyWaitsToMinimizeStalling = false + } else { + self.player.automaticallyWaitsToMinimizeStalling = true + } + } + } + + // Apply network and memory optimizations for high rates + if rate > 2.0 { + optimizeForHighRatePlayback(true) + } else { + optimizeForHighRatePlayback(false) + } } - + open func pause() { self.player.pause() } - + // MARK: - Lifecycle override public convenience init() { self.init(seekTolerance: nil) } - + public init(seekTolerance: TimeInterval?) { self.regularPlayerView = RegularPlayerView(frame: .zero) self.seekTolerance = seekTolerance.map { @@ -164,20 +208,20 @@ extension AVMediaSelectionOption: TextTrackMetadata { } super.init() - + self.addPlayerObservers() self.regularPlayerView.configureForPlayer(player: self.player) self.setupAirplay() } - + deinit { if let playerItem = self.player.currentItem { self.removePlayerItemObservers(fromPlayerItem: playerItem) } - + self.removePlayerObservers() } - + // MARK: - Setup @available(iOS 10.0, tvOS 10.0, macOS 10.12, *) @@ -189,11 +233,44 @@ extension AVMediaSelectionOption: TextTrackMetadata { self.player.automaticallyWaitsToMinimizeStalling = newValue } } - + private func setupAirplay() { - #if os(iOS) || os(tvOS) - self.player.usesExternalPlaybackWhileExternalScreenIsActive = true - #endif +#if os(iOS) || os(tvOS) + self.player.usesExternalPlaybackWhileExternalScreenIsActive = true +#endif + } + + // MARK: - Performance Optimization + + private func optimizeForHighRatePlayback(_ enabled: Bool) { + if enabled { + // Reduce concurrent operations that might compete with playback + URLSession.shared.configuration.httpMaximumConnectionsPerHost = 6 + + if #available(iOS 10.0, tvOS 10.0, macOS 10.12, *) { + // Prioritize media loading on the network + URLSession.shared.configuration.networkServiceType = .video + } + } + } + + private func trackPerformance(oldState: PlayerState, newState: PlayerState) { + // Track frequency of state changes to detect flapping + let now = Date() + let timeInState = now.timeIntervalSince(self.lastStateChange) + let key = "\(oldState)->\(newState)" + + stateChanges[key] = (stateChanges[key] ?? 0) + 1 + + // Log if we're seeing rapid state transitions (potential flapping) + if timeInState < 0.5 { + print("Warning: Rapid state transition: \(key) after \(timeInState)s") + if stateChanges[key] ?? 0 > 5 { + print("Performance Warning: Frequent state changes detected: \(stateChanges)") + } + } + + self.lastStateChange = now } // MARK: - Smooth Seeking @@ -216,6 +293,17 @@ extension AVMediaSelectionOption: TextTrackMetadata { assert(CMTIME_IS_VALID(self.seekTarget)) let inProgressSeekTarget = self.seekTarget + // Special handling for high rate playback - seek ahead of target position + var seekAdjustment: CMTime = CMTime.zero + if self.player.rate > 2.0 { + // When playing fast, seek further ahead to give more buffer time + let adjustment = Double(self.player.rate) * 0.5 + seekAdjustment = CMTimeMakeWithSeconds(adjustment, preferredTimescale: Int32(NSEC_PER_SEC)) + } + + // Adjusted target with additional buffer for high rates + let adjustedTarget = CMTimeAdd(inProgressSeekTarget, seekAdjustment) + let completion: (Bool) -> Void = { [weak self] _ in guard let self = self else { return } @@ -229,73 +317,72 @@ extension AVMediaSelectionOption: TextTrackMetadata { if let tolerance = self.seekTolerance { self.player.seek( - to: inProgressSeekTarget, + to: adjustedTarget, toleranceBefore: tolerance, toleranceAfter: tolerance, completionHandler: completion ) } else { - self.player.seek(to: inProgressSeekTarget, completionHandler: completion) + self.player.seek(to: adjustedTarget, completionHandler: completion) } } - // MARK: - Observers - + private struct KeyPath { struct Player { static let Rate = "rate" } - + struct PlayerItem { static let Status = "status" static let PlaybackLikelyToKeepUp = "playbackLikelyToKeepUp" static let LoadedTimeRanges = "loadedTimeRanges" } } - + private var playerTimeObserver: Any? - + private func addPlayerItemObservers(toPlayerItem playerItem: AVPlayerItem) { playerItem.addObserver(self, forKeyPath: KeyPath.PlayerItem.Status, options: [.initial, .new], context: nil) playerItem.addObserver(self, forKeyPath: KeyPath.PlayerItem.PlaybackLikelyToKeepUp, options: [.initial, .new], context: nil) playerItem.addObserver(self, forKeyPath: KeyPath.PlayerItem.LoadedTimeRanges, options: [.initial, .new], context: nil) } - + private func removePlayerItemObservers(fromPlayerItem playerItem: AVPlayerItem) { playerItem.removeObserver(self, forKeyPath: KeyPath.PlayerItem.Status, context: nil) playerItem.removeObserver(self, forKeyPath: KeyPath.PlayerItem.PlaybackLikelyToKeepUp, context: nil) playerItem.removeObserver(self, forKeyPath: KeyPath.PlayerItem.LoadedTimeRanges, context: nil) } - + private func addPlayerObservers() { self.player.addObserver(self, forKeyPath: KeyPath.Player.Rate, options: [.initial, .new], context: nil) - + let interval = CMTimeMakeWithSeconds(Constants.TimeUpdateInterval, preferredTimescale: Int32(NSEC_PER_SEC)) - + self.playerTimeObserver = self.player.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { [weak self] (cmTime) in - + if let strongSelf = self, let time = cmTime.timeInterval { strongSelf.time = time } }) } - + private func removePlayerObservers() { self.player.removeObserver(self, forKeyPath: KeyPath.Player.Rate, context: nil) - + if let playerTimeObserver = self.playerTimeObserver { self.player.removeTimeObserver(playerTimeObserver) - + self.playerTimeObserver = nil } } - + // MARK: Observation - + override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { // Player Item Observers - + if keyPath == KeyPath.PlayerItem.Status { if let statusInt = change?[.newKey] as? Int, let status = AVPlayerItem.Status(rawValue: statusInt) { self.playerItemStatusDidChange(status: status) @@ -311,75 +398,109 @@ extension AVMediaSelectionOption: TextTrackMetadata { self.playerItemLoadedTimeRangesDidChange(loadedTimeRanges: loadedTimeRanges) } } - + // Player Observers - + else if keyPath == KeyPath.Player.Rate { if let rate = change?[.newKey] as? Float { self.playerRateDidChange(rate: rate) } } - + // Fall Through Observers - + else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) } } - + // MARK: Observation Helpers private func playerItemStatusDidChange(status: AVPlayerItem.Status) { switch status { case .unknown: - + self.state = .loading - + case .readyToPlay: - + self.state = .ready // If we tried to seek before the video was ready to play, resume seeking now. if self.isSeekInProgress { self.seekToTarget() } - + case .failed: - + self.state = .failed - + @unknown default: - + self.state = .failed } } - + private func playerRateDidChange(rate: Float) { self.delegate?.playerDidUpdatePlaying(player: self) } - + private func playerItemPlaybackLikelyToKeepUpDidChange(playbackLikelyToKeepUp: Bool) { - let state: PlayerState = playbackLikelyToKeepUp ? .ready : .loading - - self.state = state + let currentRate = self.player.rate + let currentItem = self.player.currentItem + + // More sophisticated state determination based on multiple factors + if currentRate > 2.0 { + // For high rates, evaluate more buffer metrics + if let currentItem = currentItem { + let isBufferEmpty = currentItem.isPlaybackBufferEmpty + let isBufferFull = currentItem.isPlaybackBufferFull + + if isBufferEmpty { + self.state = .loading + } else if playbackLikelyToKeepUp || isBufferFull || self.bufferedTime > 5.0 { + // If we have good buffer or system reports likely to keep up + self.state = .ready + } else { + // Implement a brief delay before showing loading state at high speeds + // This prevents flickering between states on minor buffer issues + let capturedItem = currentItem // Create a strong reference + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + guard let self = self, + self.player.rate > 2.0, + let currentItem = self.player.currentItem, // Safely get the current item + !(currentItem.isPlaybackLikelyToKeepUp), + !(currentItem.isPlaybackBufferFull) else { + return + } + self.state = .loading + } + } + } else { + self.state = playbackLikelyToKeepUp ? .ready : .loading + } + } else { + // Standard behavior for normal playback rates + self.state = playbackLikelyToKeepUp ? .ready : .loading + } } - + private func playerItemLoadedTimeRangesDidChange(loadedTimeRanges: [NSValue]) { guard let bufferedCMTime = loadedTimeRanges.first?.timeRangeValue.end, let bufferedTime = bufferedCMTime.timeInterval else { return } - + self.bufferedTime = bufferedTime } - + // MARK: - Capability Protocol Helpers - - #if os(iOS) + +#if os(iOS) @available(iOS 9.0, *) fileprivate lazy var _pictureInPictureController: AVPictureInPictureController? = { AVPictureInPictureController(playerLayer: self.regularPlayerView.playerLayer) }() - #endif +#endif } // MARK: Capability Protocols @@ -421,22 +542,22 @@ extension RegularPlayer: FillModeCapable { public var fillMode: FillMode { get { let gravity = (self.view.layer as! AVPlayerLayer).videoGravity - + return gravity == .resizeAspect ? .fit : .fill } set { let gravity: AVLayerVideoGravity - + switch newValue { case .fit: - + gravity = .resizeAspect - + case .fill: - + gravity = .resizeAspectFill case .scaleToFit: - + gravity = .resize } @@ -450,7 +571,7 @@ extension RegularPlayer: TextTrackCapable { guard let group = self.player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else { return nil } - + if #available(iOS 9.0, *) { return self.player.currentItem?.currentMediaSelection.selectedMediaOption(in: group) } @@ -458,14 +579,14 @@ extension RegularPlayer: TextTrackCapable { return self.player.currentItem?.selectedMediaOption(in: group) } } - + public var availableTextTracks: [TextTrackMetadata] { guard let group = self.player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else { return [] } return group.options } - + public func fetchTextTracks(completion: @escaping ([TextTrackMetadata], TextTrackMetadata?) -> Void) { self.player.currentItem?.asset.loadValuesAsynchronously(forKeys: [#keyPath(AVAsset.availableMediaCharacteristicsWithMediaSelectionOptions)]) { [weak self] in guard let strongSelf = self, let group = strongSelf.player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else { @@ -480,17 +601,17 @@ extension RegularPlayer: TextTrackCapable { } } } - + public func select(_ textTrack: TextTrackMetadata?) { guard let group = self.player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else { return } - + guard let track = textTrack else { self.player.currentItem?.select(nil, in: group) return } - + let option = group.options.first(where: { option in track.matches(option) }) From 8367323dad0d32c65c35e9d51a2dbad6a9631934 Mon Sep 17 00:00:00 2001 From: Mohammed Elnaggar Date: Sun, 2 Mar 2025 02:08:30 +0200 Subject: [PATCH 08/10] Update RegularPlayer.swift --- Sources/RegularPlayer.swift | 40 +++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/Sources/RegularPlayer.swift b/Sources/RegularPlayer.swift index 3d84973..7009b3b 100644 --- a/Sources/RegularPlayer.swift +++ b/Sources/RegularPlayer.swift @@ -47,23 +47,37 @@ extension AVMediaSelectionOption: TextTrackMetadata { /// /// - Parameter asset: The AVAsset @objc open func set(_ asset: AVAsset) { - // Create playback options for faster loading - let options: [String: Any] = [ - AVURLAssetPreferPreciseDurationAndTimingKey: true - ] + // If the asset is already a URL asset, we should create a new one with our options + if let urlAsset = asset as? AVURLAsset { + // Create playback options for faster loading + let options: [String: Any] = [ + AVURLAssetPreferPreciseDurationAndTimingKey: true + ] - // Create a specialized playerItem with custom options - let playerItem = AVPlayerItem(asset: asset) + // Create a new URL asset with our performance options + let optimizedAsset = AVURLAsset(url: urlAsset.url, options: options) - // Optimize for high-speed playback by setting appropriate values - if #available(iOS 10.0, tvOS 10.0, macOS 10.12, *) { - playerItem.preferredForwardBufferDuration = 10.0 // Start with a good buffer + // Create a player item from the optimized asset + let playerItem = AVPlayerItem(asset: optimizedAsset) - // This helps maintain higher playback rates by giving a quality/speed tradeoff hint - playerItem.preferredPeakBitRate = 0 // 0 means no limit - } + // Apply high-speed playback optimizations + if #available(iOS 10.0, tvOS 10.0, macOS 10.12, *) { + playerItem.preferredForwardBufferDuration = 10.0 + playerItem.preferredPeakBitRate = 0 + } + + self.set(playerItem: playerItem) + } else { + // For non-URL assets, we can't apply URL options, but we can still optimize the player item + let playerItem = AVPlayerItem(asset: asset) - self.set(playerItem: playerItem) + if #available(iOS 10.0, tvOS 10.0, macOS 10.12, *) { + playerItem.preferredForwardBufferDuration = 10.0 + playerItem.preferredPeakBitRate = 0 + } + + self.set(playerItem: playerItem) + } } @objc open func set(playerItem: AVPlayerItem) { From 002740ca5fb040ec0a4bc34a9d185d914e2b875e Mon Sep 17 00:00:00 2001 From: Mohammed Elnaggar Date: Sun, 2 Mar 2025 03:37:06 +0200 Subject: [PATCH 09/10] Update RegularPlayer.swift --- Sources/RegularPlayer.swift | 315 +++++++++++------------------------- 1 file changed, 90 insertions(+), 225 deletions(-) diff --git a/Sources/RegularPlayer.swift b/Sources/RegularPlayer.swift index 7009b3b..ef12d55 100644 --- a/Sources/RegularPlayer.swift +++ b/Sources/RegularPlayer.swift @@ -18,13 +18,13 @@ extension AVMediaSelectionOption: TextTrackMetadata { /// A RegularPlayer is used to play regular videos. @objc open class RegularPlayer: NSObject, Player, ProvidesView { - + public struct Constants { public static let TimeUpdateInterval: TimeInterval = 0.1 } - + // MARK: - Private Properties - + fileprivate var player = AVPlayer() private var regularPlayerView: RegularPlayerView @@ -37,47 +37,15 @@ extension AVMediaSelectionOption: TextTrackMetadata { private var seekTarget: CMTime = CMTime.invalid public var isSeekInProgress: Bool = false - - private var lastStateChange: Date = Date() - private var stateChanges: [String: Int] = [:] - + // MARK: - Public API - + /// Sets an AVAsset on the player. /// /// - Parameter asset: The AVAsset @objc open func set(_ asset: AVAsset) { - // If the asset is already a URL asset, we should create a new one with our options - if let urlAsset = asset as? AVURLAsset { - // Create playback options for faster loading - let options: [String: Any] = [ - AVURLAssetPreferPreciseDurationAndTimingKey: true - ] - - // Create a new URL asset with our performance options - let optimizedAsset = AVURLAsset(url: urlAsset.url, options: options) - - // Create a player item from the optimized asset - let playerItem = AVPlayerItem(asset: optimizedAsset) - - // Apply high-speed playback optimizations - if #available(iOS 10.0, tvOS 10.0, macOS 10.12, *) { - playerItem.preferredForwardBufferDuration = 10.0 - playerItem.preferredPeakBitRate = 0 - } - - self.set(playerItem: playerItem) - } else { - // For non-URL assets, we can't apply URL options, but we can still optimize the player item - let playerItem = AVPlayerItem(asset: asset) - - if #available(iOS 10.0, tvOS 10.0, macOS 10.12, *) { - playerItem.preferredForwardBufferDuration = 10.0 - playerItem.preferredPeakBitRate = 0 - } - - self.set(playerItem: playerItem) - } + let playerItem = AVPlayerItem(asset: asset) + self.set(playerItem: playerItem) } @objc open func set(playerItem: AVPlayerItem) { @@ -90,19 +58,19 @@ extension AVMediaSelectionOption: TextTrackMetadata { self.addPlayerItemObservers(toPlayerItem: playerItem) self.player.replaceCurrentItem(with: playerItem) } - + // MARK: - ProvidesView - + private class RegularPlayerView: PlayerView { var playerLayer: AVPlayerLayer { return self.layer as! AVPlayerLayer } -#if canImport(UIKit) + #if canImport(UIKit) override class var layerClass: AnyClass { return AVPlayerLayer.self } -#elseif canImport(AppKit) + #elseif canImport(AppKit) override init(frame frameRect: NSRect) { super.init(frame: frameRect) self.layer = AVPlayerLayer() @@ -111,63 +79,61 @@ extension AVMediaSelectionOption: TextTrackMetadata { required init?(coder decoder: NSCoder) { fatalError("init(coder:) has not been implemented") } -#endif - + #endif + func configureForPlayer(player: AVPlayer) { (self.layer as! AVPlayerLayer).player = player } } - + open var view: UIView { return self.regularPlayerView } - + // MARK: - Player - + weak public var delegate: PlayerDelegate? - + public private(set) var state: PlayerState = .ready { didSet { - // Track performance metrics - self.trackPerformance(oldState: oldValue, newState: state) self.delegate?.playerDidUpdateState(player: self, previousState: oldValue) } } - + public var duration: TimeInterval { return self.player.currentItem?.duration.timeInterval ?? 0 } - + public private(set) var time: TimeInterval = 0 { didSet { self.delegate?.playerDidUpdateTime(player: self) } } - + public private(set) var bufferedTime: TimeInterval = 0 { didSet { self.delegate?.playerDidUpdateBufferedTime(player: self) } } - + public var isMuted: Bool = true { didSet { player.isMuted = isMuted } } - + public var playing: Bool { return self.player.rate > 0 } - + public var ended: Bool { return self.time >= self.duration } - + public var error: NSError? { return self.player.errorForPlayerOrItem } - + open func seek(to time: TimeInterval) { let cmTime = CMTimeMakeWithSeconds(time, preferredTimescale: Int32(NSEC_PER_SEC)) self.smoothSeek(to: cmTime) @@ -176,45 +142,21 @@ extension AVMediaSelectionOption: TextTrackMetadata { open func play() { self.player.play() } - + open func setRate(_ rate: Float) { self.player.rate = rate - - if let currentItem = self.player.currentItem { - // Progressive buffer scaling based on rate - if #available(iOS 10.0, tvOS 10.0, macOS 10.12, *) { - // Scale buffer with rate - higher rates need more buffer - let bufferDuration = min(30.0, rate * 5.0) // Cap at reasonable maximum - currentItem.preferredForwardBufferDuration = TimeInterval(bufferDuration) - - // Lower values make playback start faster but might cause more rebuffering - if rate > 2.0 { - currentItem.canUseNetworkResourcesForLiveStreamingWhilePaused = true - self.player.automaticallyWaitsToMinimizeStalling = false - } else { - self.player.automaticallyWaitsToMinimizeStalling = true - } - } - } - - // Apply network and memory optimizations for high rates - if rate > 2.0 { - optimizeForHighRatePlayback(true) - } else { - optimizeForHighRatePlayback(false) - } } - + open func pause() { self.player.pause() } - + // MARK: - Lifecycle override public convenience init() { self.init(seekTolerance: nil) } - + public init(seekTolerance: TimeInterval?) { self.regularPlayerView = RegularPlayerView(frame: .zero) self.seekTolerance = seekTolerance.map { @@ -222,20 +164,20 @@ extension AVMediaSelectionOption: TextTrackMetadata { } super.init() - + self.addPlayerObservers() self.regularPlayerView.configureForPlayer(player: self.player) self.setupAirplay() } - + deinit { if let playerItem = self.player.currentItem { self.removePlayerItemObservers(fromPlayerItem: playerItem) } - + self.removePlayerObservers() } - + // MARK: - Setup @available(iOS 10.0, tvOS 10.0, macOS 10.12, *) @@ -247,44 +189,11 @@ extension AVMediaSelectionOption: TextTrackMetadata { self.player.automaticallyWaitsToMinimizeStalling = newValue } } - + private func setupAirplay() { -#if os(iOS) || os(tvOS) - self.player.usesExternalPlaybackWhileExternalScreenIsActive = true -#endif - } - - // MARK: - Performance Optimization - - private func optimizeForHighRatePlayback(_ enabled: Bool) { - if enabled { - // Reduce concurrent operations that might compete with playback - URLSession.shared.configuration.httpMaximumConnectionsPerHost = 6 - - if #available(iOS 10.0, tvOS 10.0, macOS 10.12, *) { - // Prioritize media loading on the network - URLSession.shared.configuration.networkServiceType = .video - } - } - } - - private func trackPerformance(oldState: PlayerState, newState: PlayerState) { - // Track frequency of state changes to detect flapping - let now = Date() - let timeInState = now.timeIntervalSince(self.lastStateChange) - let key = "\(oldState)->\(newState)" - - stateChanges[key] = (stateChanges[key] ?? 0) + 1 - - // Log if we're seeing rapid state transitions (potential flapping) - if timeInState < 0.5 { - print("Warning: Rapid state transition: \(key) after \(timeInState)s") - if stateChanges[key] ?? 0 > 5 { - print("Performance Warning: Frequent state changes detected: \(stateChanges)") - } - } - - self.lastStateChange = now + #if os(iOS) || os(tvOS) + self.player.usesExternalPlaybackWhileExternalScreenIsActive = true + #endif } // MARK: - Smooth Seeking @@ -307,17 +216,6 @@ extension AVMediaSelectionOption: TextTrackMetadata { assert(CMTIME_IS_VALID(self.seekTarget)) let inProgressSeekTarget = self.seekTarget - // Special handling for high rate playback - seek ahead of target position - var seekAdjustment: CMTime = CMTime.zero - if self.player.rate > 2.0 { - // When playing fast, seek further ahead to give more buffer time - let adjustment = Double(self.player.rate) * 0.5 - seekAdjustment = CMTimeMakeWithSeconds(adjustment, preferredTimescale: Int32(NSEC_PER_SEC)) - } - - // Adjusted target with additional buffer for high rates - let adjustedTarget = CMTimeAdd(inProgressSeekTarget, seekAdjustment) - let completion: (Bool) -> Void = { [weak self] _ in guard let self = self else { return } @@ -331,72 +229,73 @@ extension AVMediaSelectionOption: TextTrackMetadata { if let tolerance = self.seekTolerance { self.player.seek( - to: adjustedTarget, + to: inProgressSeekTarget, toleranceBefore: tolerance, toleranceAfter: tolerance, completionHandler: completion ) } else { - self.player.seek(to: adjustedTarget, completionHandler: completion) + self.player.seek(to: inProgressSeekTarget, completionHandler: completion) } } + // MARK: - Observers - + private struct KeyPath { struct Player { static let Rate = "rate" } - + struct PlayerItem { static let Status = "status" static let PlaybackLikelyToKeepUp = "playbackLikelyToKeepUp" static let LoadedTimeRanges = "loadedTimeRanges" } } - + private var playerTimeObserver: Any? - + private func addPlayerItemObservers(toPlayerItem playerItem: AVPlayerItem) { playerItem.addObserver(self, forKeyPath: KeyPath.PlayerItem.Status, options: [.initial, .new], context: nil) playerItem.addObserver(self, forKeyPath: KeyPath.PlayerItem.PlaybackLikelyToKeepUp, options: [.initial, .new], context: nil) playerItem.addObserver(self, forKeyPath: KeyPath.PlayerItem.LoadedTimeRanges, options: [.initial, .new], context: nil) } - + private func removePlayerItemObservers(fromPlayerItem playerItem: AVPlayerItem) { playerItem.removeObserver(self, forKeyPath: KeyPath.PlayerItem.Status, context: nil) playerItem.removeObserver(self, forKeyPath: KeyPath.PlayerItem.PlaybackLikelyToKeepUp, context: nil) playerItem.removeObserver(self, forKeyPath: KeyPath.PlayerItem.LoadedTimeRanges, context: nil) } - + private func addPlayerObservers() { self.player.addObserver(self, forKeyPath: KeyPath.Player.Rate, options: [.initial, .new], context: nil) - + let interval = CMTimeMakeWithSeconds(Constants.TimeUpdateInterval, preferredTimescale: Int32(NSEC_PER_SEC)) - + self.playerTimeObserver = self.player.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { [weak self] (cmTime) in - + if let strongSelf = self, let time = cmTime.timeInterval { strongSelf.time = time } }) } - + private func removePlayerObservers() { self.player.removeObserver(self, forKeyPath: KeyPath.Player.Rate, context: nil) - + if let playerTimeObserver = self.playerTimeObserver { self.player.removeTimeObserver(playerTimeObserver) - + self.playerTimeObserver = nil } } - + // MARK: Observation - + override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { // Player Item Observers - + if keyPath == KeyPath.PlayerItem.Status { if let statusInt = change?[.newKey] as? Int, let status = AVPlayerItem.Status(rawValue: statusInt) { self.playerItemStatusDidChange(status: status) @@ -412,109 +311,75 @@ extension AVMediaSelectionOption: TextTrackMetadata { self.playerItemLoadedTimeRangesDidChange(loadedTimeRanges: loadedTimeRanges) } } - + // Player Observers - + else if keyPath == KeyPath.Player.Rate { if let rate = change?[.newKey] as? Float { self.playerRateDidChange(rate: rate) } } - + // Fall Through Observers - + else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) } } - + // MARK: Observation Helpers private func playerItemStatusDidChange(status: AVPlayerItem.Status) { switch status { case .unknown: - + self.state = .loading - + case .readyToPlay: - + self.state = .ready // If we tried to seek before the video was ready to play, resume seeking now. if self.isSeekInProgress { self.seekToTarget() } - + case .failed: - + self.state = .failed - + @unknown default: - + self.state = .failed } } - + private func playerRateDidChange(rate: Float) { self.delegate?.playerDidUpdatePlaying(player: self) } - + private func playerItemPlaybackLikelyToKeepUpDidChange(playbackLikelyToKeepUp: Bool) { - let currentRate = self.player.rate - let currentItem = self.player.currentItem - - // More sophisticated state determination based on multiple factors - if currentRate > 2.0 { - // For high rates, evaluate more buffer metrics - if let currentItem = currentItem { - let isBufferEmpty = currentItem.isPlaybackBufferEmpty - let isBufferFull = currentItem.isPlaybackBufferFull - - if isBufferEmpty { - self.state = .loading - } else if playbackLikelyToKeepUp || isBufferFull || self.bufferedTime > 5.0 { - // If we have good buffer or system reports likely to keep up - self.state = .ready - } else { - // Implement a brief delay before showing loading state at high speeds - // This prevents flickering between states on minor buffer issues - let capturedItem = currentItem // Create a strong reference - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - guard let self = self, - self.player.rate > 2.0, - let currentItem = self.player.currentItem, // Safely get the current item - !(currentItem.isPlaybackLikelyToKeepUp), - !(currentItem.isPlaybackBufferFull) else { - return - } - self.state = .loading - } - } - } else { - self.state = playbackLikelyToKeepUp ? .ready : .loading - } - } else { - // Standard behavior for normal playback rates - self.state = playbackLikelyToKeepUp ? .ready : .loading - } + let state: PlayerState = playbackLikelyToKeepUp ? .ready : .loading + + self.state = state } - + private func playerItemLoadedTimeRangesDidChange(loadedTimeRanges: [NSValue]) { guard let bufferedCMTime = loadedTimeRanges.first?.timeRangeValue.end, let bufferedTime = bufferedCMTime.timeInterval else { return } - + self.bufferedTime = bufferedTime } - + // MARK: - Capability Protocol Helpers - -#if os(iOS) + + #if os(iOS) @available(iOS 9.0, *) fileprivate lazy var _pictureInPictureController: AVPictureInPictureController? = { AVPictureInPictureController(playerLayer: self.regularPlayerView.playerLayer) }() -#endif + #endif } // MARK: Capability Protocols @@ -556,22 +421,22 @@ extension RegularPlayer: FillModeCapable { public var fillMode: FillMode { get { let gravity = (self.view.layer as! AVPlayerLayer).videoGravity - + return gravity == .resizeAspect ? .fit : .fill } set { let gravity: AVLayerVideoGravity - + switch newValue { case .fit: - + gravity = .resizeAspect - + case .fill: - + gravity = .resizeAspectFill case .scaleToFit: - + gravity = .resize } @@ -585,7 +450,7 @@ extension RegularPlayer: TextTrackCapable { guard let group = self.player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else { return nil } - + if #available(iOS 9.0, *) { return self.player.currentItem?.currentMediaSelection.selectedMediaOption(in: group) } @@ -593,14 +458,14 @@ extension RegularPlayer: TextTrackCapable { return self.player.currentItem?.selectedMediaOption(in: group) } } - + public var availableTextTracks: [TextTrackMetadata] { guard let group = self.player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else { return [] } return group.options } - + public func fetchTextTracks(completion: @escaping ([TextTrackMetadata], TextTrackMetadata?) -> Void) { self.player.currentItem?.asset.loadValuesAsynchronously(forKeys: [#keyPath(AVAsset.availableMediaCharacteristicsWithMediaSelectionOptions)]) { [weak self] in guard let strongSelf = self, let group = strongSelf.player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else { @@ -615,17 +480,17 @@ extension RegularPlayer: TextTrackCapable { } } } - + public func select(_ textTrack: TextTrackMetadata?) { guard let group = self.player.currentItem?.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else { return } - + guard let track = textTrack else { self.player.currentItem?.select(nil, in: group) return } - + let option = group.options.first(where: { option in track.matches(option) }) From 24146fa4428352ad1ecfe9dc44499da1601c4b3f Mon Sep 17 00:00:00 2001 From: Mohammed Elnaggar Date: Wed, 4 Feb 2026 23:59:42 +0200 Subject: [PATCH 10/10] Add Package.swift for SPM support --- Package.swift | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Package.swift diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..17869e6 --- /dev/null +++ b/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version:5.7 +import PackageDescription + +let package = Package( + name: "PlayerKit", + platforms: [ + .iOS(.v13) + ], + products: [ + .library( + name: "PlayerKit", + targets: ["PlayerKit"] + ) + ], + targets: [ + .target( + name: "PlayerKit", + path: "Sources" + ) + ] +)