@capgo/native-audio
Advanced tools
@@ -123,9 +123,20 @@ @preconcurrency import AVFoundation | ||
| func setCurrentTime(time: TimeInterval) { | ||
| owner?.executeOnAudioQueue { [weak self] in | ||
| guard let self else { return } | ||
| guard !channels.isEmpty, playIndex < channels.count else { return } | ||
| func setCurrentTime(time: TimeInterval, completion: (() -> Void)? = nil) { | ||
| guard let owner else { | ||
| completion?() | ||
| return | ||
| } | ||
| owner.executeOnAudioQueue { [weak self] in | ||
| guard let self else { | ||
| completion?() | ||
| return | ||
| } | ||
| guard !channels.isEmpty, playIndex < channels.count else { | ||
| completion?() | ||
| return | ||
| } | ||
| let player = channels[playIndex] | ||
| let validTime = min(max(time, 0), player.duration) | ||
| player.currentTime = validTime | ||
| completion?() | ||
| } | ||
@@ -217,4 +228,3 @@ } | ||
| let player = channels[playIndex] | ||
| let timeOffset = player.deviceCurrentTime + 0.01 | ||
| player.play(atTime: timeOffset) | ||
| player.play() | ||
| startCurrentTimeUpdates() | ||
@@ -253,4 +263,21 @@ } | ||
| let player = channels[playIndex] | ||
| if player.isPlaying && player.volume > 0 { | ||
| fadeOut(audio: player, fadeOutDuration: fadeOutDuration, toPause: toPause) | ||
| if player.isPlaying { | ||
| if toPause { | ||
| if player.volume > 0 { | ||
| fadeOut(audio: player, fadeOutDuration: fadeOutDuration, toPause: true) { [weak self] elapsed, duration in | ||
| guard let self, let owner = self.owner else { return } | ||
| owner.recordPausePositionAfterFade(assetId: self.assetId, elapsedTime: elapsed, duration: duration) | ||
| } | ||
| } else { | ||
| cancelFade() | ||
| schedulePauseWithPositionRecording(audio: player) { [weak self] elapsed, duration in | ||
| guard let self, let owner = self.owner else { return } | ||
| owner.recordPausePositionAfterFade(assetId: self.assetId, elapsedTime: elapsed, duration: duration) | ||
| } | ||
| } | ||
| } else if player.volume > 0 { | ||
| fadeOut(audio: player, fadeOutDuration: fadeOutDuration, toPause: false) | ||
| } else { | ||
| stop() | ||
| } | ||
| } else if !toPause { | ||
@@ -257,0 +284,0 @@ stop() |
@@ -18,2 +18,33 @@ import AVFoundation | ||
| fileprivate func performLocalFadeOutPauseOnMain(audio: AVAudioPlayer, beforePause: ((TimeInterval, TimeInterval) -> Void)?) { | ||
| let elapsed = audio.currentTime | ||
| let duration = audio.duration.isFinite ? audio.duration : 0 | ||
| beforePause?(elapsed, duration) | ||
| audio.pause() | ||
| } | ||
| fileprivate func scheduleLocalFadeOutPauseOnMain(audio: AVAudioPlayer, beforePause: ((TimeInterval, TimeInterval) -> Void)?) { | ||
| DispatchQueue.main.async { [weak self] in | ||
| guard let self else { return } | ||
| self.performLocalFadeOutPauseOnMain(audio: audio, beforePause: beforePause) | ||
| } | ||
| } | ||
| /// Same main-thread pause and `beforePause(elapsed, duration)` as fade-out-to-pause when no fade runs (e.g. volume already zero). | ||
| internal func schedulePauseWithPositionRecording(audio: AVAudioPlayer, beforePause: ((TimeInterval, TimeInterval) -> Void)?) { | ||
| scheduleLocalFadeOutPauseOnMain(audio: audio, beforePause: beforePause) | ||
| } | ||
| fileprivate func scheduleLocalFadeOutStopOnMain(audio: AVAudioPlayer) { | ||
| DispatchQueue.main.async { [weak self] in | ||
| guard let self else { return } | ||
| self.performLocalFadeOutStopOnMain(audio: audio) | ||
| } | ||
| } | ||
| fileprivate func performLocalFadeOutStopOnMain(audio: AVAudioPlayer) { | ||
| audio.stop() | ||
| dispatchComplete() | ||
| } | ||
| func fadeIn(audio: AVAudioPlayer, fadeInDuration: TimeInterval, targetVolume: Float) { | ||
@@ -44,6 +75,20 @@ cancelFade() | ||
| func fadeOut(audio: AVAudioPlayer, fadeOutDuration: TimeInterval, toPause: Bool = false) { | ||
| /// - Parameter beforePause: Called on the main queue immediately before `pause()` when `toPause` is true, | ||
| /// so the plugin can persist `timeBeforePause` and update Now Playing at the actual stop position. | ||
| func fadeOut( | ||
| audio: AVAudioPlayer, | ||
| fadeOutDuration: TimeInterval, | ||
| toPause: Bool = false, | ||
| beforePause: ((TimeInterval, TimeInterval) -> Void)? = nil | ||
| ) { | ||
| cancelFade() | ||
| let steps = Int(fadeOutDuration / TimeInterval(fadeDelaySecs)) | ||
| guard steps > 0 else { return } | ||
| guard steps > 0 else { | ||
| if toPause { | ||
| scheduleLocalFadeOutPauseOnMain(audio: audio, beforePause: beforePause) | ||
| } else { | ||
| scheduleLocalFadeOutStopOnMain(audio: audio) | ||
| } | ||
| return | ||
| } | ||
| var currentVolume = audio.volume | ||
@@ -66,6 +111,5 @@ let fadeStep = currentVolume / Float(steps) | ||
| if toPause { | ||
| audio.pause() | ||
| self.performLocalFadeOutPauseOnMain(audio: audio, beforePause: beforePause) | ||
| } else { | ||
| audio.stop() | ||
| self.dispatchComplete() | ||
| self.performLocalFadeOutStopOnMain(audio: audio) | ||
| } | ||
@@ -72,0 +116,0 @@ } |
@@ -135,2 +135,45 @@ @preconcurrency import AVFoundation | ||
| /// Timescale for seek targets; 600 is a common media default and avoids coarse rounding from timescale 1. | ||
| private static let seekPreferredTimescale: CMTimeScale = 600 | ||
| override func setCurrentTime(time: TimeInterval, completion: (() -> Void)? = nil) { | ||
| guard let owner else { | ||
| completion?() | ||
| return | ||
| } | ||
| owner.executeOnAudioQueue { [weak self] in | ||
| guard let self else { | ||
| completion?() | ||
| return | ||
| } | ||
| guard !players.isEmpty && playIndex < players.count else { | ||
| completion?() | ||
| return | ||
| } | ||
| let player = players[playIndex] | ||
| let lowerBound = max(time, 0) | ||
| let validTime: TimeInterval | ||
| if let item = player.currentItem { | ||
| let d = item.duration | ||
| if d == .indefinite || !d.isValid { | ||
| validTime = lowerBound | ||
| } else { | ||
| let durationSeconds = d.seconds | ||
| if durationSeconds.isFinite && durationSeconds > 0 { | ||
| validTime = min(lowerBound, durationSeconds) | ||
| } else { | ||
| validTime = lowerBound | ||
| } | ||
| } | ||
| } else { | ||
| validTime = lowerBound | ||
| } | ||
| let target = CMTime(seconds: validTime, preferredTimescale: Self.seekPreferredTimescale) | ||
| player.seek(to: target, toleranceBefore: .zero, toleranceAfter: .zero) { finished in | ||
| guard finished else { return } | ||
| completion?() | ||
| } | ||
| } | ||
| } | ||
| override func resume() { | ||
@@ -342,3 +385,10 @@ owner?.executeOnAudioQueue { [weak self] in | ||
| if player.timeControlStatus == .playing { | ||
| fadeOut(player: player, fadeOutDuration: fadeOutDuration, toPause: toPause) | ||
| if toPause { | ||
| fadeOut(player: player, fadeOutDuration: fadeOutDuration, toPause: true) { [weak self] elapsed, duration in | ||
| guard let self, let owner = self.owner else { return } | ||
| owner.recordPausePositionAfterFade(assetId: self.assetId, elapsedTime: elapsed, duration: duration) | ||
| } | ||
| } else { | ||
| fadeOut(player: player, fadeOutDuration: fadeOutDuration, toPause: false) | ||
| } | ||
| } else if !toPause { | ||
@@ -345,0 +395,0 @@ stop() |
@@ -5,2 +5,29 @@ import AVFoundation | ||
| /// Pause after sampling elapsed/duration for Now Playing. Caller must be on the main queue. | ||
| fileprivate func performRemoteFadeOutPauseOnMain(player: AVPlayer, beforePause: ((TimeInterval, TimeInterval) -> Void)?) { | ||
| let elapsed = player.currentTime().seconds | ||
| let rawDuration = player.currentItem?.duration ?? .zero | ||
| let duration = rawDuration.isNumeric && rawDuration.isValid ? rawDuration.seconds : 0 | ||
| beforePause?(elapsed, duration.isFinite ? duration : 0) | ||
| player.pause() | ||
| } | ||
| fileprivate func scheduleRemoteFadeOutPauseOnMain(player: AVPlayer, beforePause: ((TimeInterval, TimeInterval) -> Void)?) { | ||
| DispatchQueue.main.async { [weak self] in | ||
| guard let self else { return } | ||
| self.performRemoteFadeOutPauseOnMain(player: player, beforePause: beforePause) | ||
| } | ||
| } | ||
| /// Pause, seek to start, then emit `complete` on the main queue after the seek finishes. | ||
| fileprivate func pauseSeekToStartThenDispatchComplete(on player: AVPlayer) { | ||
| player.pause() | ||
| player.seek(to: .zero) { [weak self] _ in | ||
| guard let self else { return } | ||
| DispatchQueue.main.async { | ||
| self.dispatchComplete() | ||
| } | ||
| } | ||
| } | ||
| func fadeIn(player: AVPlayer, fadeInDuration: TimeInterval, targetVolume: Float) { | ||
@@ -31,6 +58,22 @@ cancelFade() | ||
| func fadeOut(player: AVPlayer, fadeOutDuration: TimeInterval, toPause: Bool = false) { | ||
| /// - Parameter beforePause: Called on the main queue immediately before `pause()` when `toPause` is true. | ||
| func fadeOut( | ||
| player: AVPlayer, | ||
| fadeOutDuration: TimeInterval, | ||
| toPause: Bool = false, | ||
| beforePause: ((TimeInterval, TimeInterval) -> Void)? = nil | ||
| ) { | ||
| cancelFade() | ||
| let steps = Int(fadeOutDuration / TimeInterval(fadeDelaySecs)) | ||
| guard steps > 0 else { return } | ||
| guard steps > 0 else { | ||
| if toPause { | ||
| scheduleRemoteFadeOutPauseOnMain(player: player, beforePause: beforePause) | ||
| } else { | ||
| DispatchQueue.main.async { [weak self] in | ||
| guard let self else { return } | ||
| self.pauseSeekToStartThenDispatchComplete(on: player) | ||
| } | ||
| } | ||
| return | ||
| } | ||
| let fadeStep = player.volume / Float(steps) | ||
@@ -53,8 +96,5 @@ var currentVolume = player.volume | ||
| if toPause { | ||
| player.pause() | ||
| self.performRemoteFadeOutPauseOnMain(player: player, beforePause: beforePause) | ||
| } else { | ||
| player.pause() | ||
| player.seek(to: .zero) | ||
| self.owner?.notifyListeners("complete", data: ["assetId": self.assetId as Any]) | ||
| self.dispatchedCompleteMap[self.assetId] = true | ||
| self.pauseSeekToStartThenDispatchComplete(on: player) | ||
| } | ||
@@ -61,0 +101,0 @@ } |
+1
-1
| { | ||
| "name": "@capgo/native-audio", | ||
| "version": "8.3.9", | ||
| "version": "8.3.10", | ||
| "description": "A native plugin for native audio engine", | ||
@@ -5,0 +5,0 @@ "license": "MPL-2.0", |
Sorry, the diff of this file is too big to display
690895
1.93%