Swift: ползунок аудио трека подлагивает
В моем приложении есть аудиоплеер. И ползунок для перемещения звуковой дорожки. Но, например, в моем приложении на экране блокировки ползунок не подлагивает, когда его скролишь влево вправо. И он не пытается проигрывать аудио в момент перемотки. Когда его зажимаешь он продолжает проигрывает аудио, а проигрывать новое время он начинает только после отпускания ползунка. Поесть он нормально работает как в Apple Music.
А внутри моего приложения тоже слайдер и он зависает, когда я его скроллю. И чем больше размер аудиофайла, тем сильнее зависает ползунок при прокрутке. То есть если я его из начала трека перемещаю в конец, то попутно он цепляет куски аудио трека и пытается проиграть их. А не плавно перемещается как на заблокированном экране. Как это исправить?
@IBAction func slide(_ slider: UISlider) {
musicOperation.cancelAllOperations()
let operation = BlockOperation()
audioPlayer.currentTime = TimeInterval(slider.value)
musicOperation.addOperation(operation)
}
Update
В updateTime() закомментировал slider.value = Float.init(audioPlayer.currentTime) это помогло убрать подергивания и мигание. Но в fastForward() и fastBackward() при нажатии трек перематывался, но слайдер был на месте поэтому закомментировал в этих функциях вызов //updateTime() и добавил вместо slider.value = Float.init(audioPlayer.currentTime). Помогло.
Так же слайдер перестал запоминать свое местоположение при блокировке экрана, перемотке трека на заблокированном экране и следующем возвращении в приложение. Потому что как писал ранее убрал эту строку slider.value = Float.init(audioPlayer.currentTime) в updateTime(). Поэтому решил добавить вызов applicationWillEnterForeground. Правильно ли я все сделал?
Осталась проблема 3. И еще два маленьких вопроса:
Я так понимаю что мне вообще не нужно использовать
musicOperationверно?После того как начал использовать
Touch Up Insideпоявилось странное поведение. Например, если я передвигаю туда сюда ползунок в +- вертикальных рамках слайдера (то есть его размер 30 по стандарту, а палец я держу чуть выше или ниже) то все нормально. Но если я зажму слайдер а палец уведу на верх экрана iPhone или вниз, то я увижу как бы анимацию отжимания ползунка, но все равно смогу им управлять. И если я отожму палец и ползунок остановится, то он просто не выполнит свою функцию. Аудио не переключится на выбранное место. Раньше до использованияTouch Up Insideделал тоже самое уводил палец вверх не отжимая слайдер при этом видел анимацию отжимания, но после отпускания пальца трек играл в месте остановки ползункаvar audioPlayer: AVAudioPlayer! let musicOperation = OperationQueue() override func viewDidLoad() { super.viewDidLoad() NotificationCenter.default.addObserver( self, selector: #selector(ViewController.applicationWillEnterForeground(notification:)), name:NSNotification.Name.UIApplicationWillEnterForeground, object: nil) musicOperation.maxConcurrentOperationCount = 1 try? AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback) let url = Bundle.main.url(forResource: "0", withExtension: "m4a")! do { audioPlayer = try AVAudioPlayer(contentsOf: url) audioPlayer.delegate = self audioPlayer.prepareToPlay() play(sender:AnyObject.self as AnyObject) setupMediaPlayerNotificationView(true) lockScreen() } catch { } } @IBAction func play(sender: AnyObject) { if !audioPlayer.isPlaying{ audioPlayer.play() slider.maximumValue = Float(audioPlayer.duration) timer = Timer(timeInterval: 0.1, target: self, selector: #selector(self.updateTime), userInfo: nil, repeats: true) RunLoop.main.add(timer!, forMode: .commonModes) restorePlayerCurrentTime() playButton.setImage(UIImage(named: "pause.png"), for: UIControlState.normal) } else { audioPlayer.pause() playButton.setImage(UIImage(named: "play.png"), for: UIControlState.normal) timer?.invalidate() } } @IBAction func fastForward(sender: AnyObject) { var time: TimeInterval = audioPlayer.currentTime time += 15.0 // Go Forward by 15 Seconds if time > audioPlayer.duration { audioPlayerDidFinishPlaying(audioPlayer, successfully: true) } else { audioPlayer.currentTime = time slider.value = Float.init(audioPlayer.currentTime) //updateTime() } self.lockScreen() } @IBAction func fastBackward(sender: AnyObject) { var time: TimeInterval = audioPlayer.currentTime time -= 15.0 // Go Back by 15 Seconds if time < 0 { audioPlayer.currentTime = 0 slider.value = Float.init(audioPlayer.currentTime) //updateTime() } else { audioPlayer.currentTime = time slider.value = Float.init(audioPlayer.currentTime) //updateTime() } self.lockScreen() } private func restorePlayerCurrentTime() { let currentTimeFromUserDefaults : Double? = UserDefaults.standard.value(forKey: "currentTime\(masterIndex)\(index)") as! Double? if let currentTimeFromUserDefaultsValue = currentTimeFromUserDefaults { audioPlayer.currentTime = currentTimeFromUserDefaultsValue slider.value = Float.init(audioPlayer.currentTime) } } @objc func updateTime() { let currentTime = Int(audioPlayer.currentTime) let minutes = currentTime/60 let seconds = currentTime - minutes * 60 let durationTime = Int(audioPlayer.duration) - Int(audioPlayer.currentTime) let minutes1 = durationTime/60 let seconds1 = durationTime - minutes1 * 60 timeElapsed.text = NSString(format: "%02d:%02d", minutes,seconds) as String timeDuration.text = NSString(format: "-%02d:%02d", minutes1,seconds1) as String UserDefaults.standard.set(currentTime, forKey: "currentTime\(masterIndex)\(index)") UserDefaults.standard.set(durationTime, forKey: "durationTime\(masterIndex)\(index)") //slider.value = Float.init(audioPlayer.currentTime) } func audioPlayerDidFinishPlaying(_ audioPlayer: AVAudioPlayer, successfully flag: Bool) { playButton.setImage(UIImage(named: "play.png"), for: UIControlState.normal) let currentTime = 0 let durationTime = 0.1 UserDefaults.standard.set(currentTime, forKey: "currentTime\(masterIndex)\(index)") UserDefaults.standard.set(durationTime, forKey: "durationTime\(masterIndex)\(index)") slider.value = Float.init(audioPlayer.currentTime) timer?.invalidate() let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true) let documentDirectoryPath:String = path[0] let fileManager = FileManager() let destinationURLForFile = URL(fileURLWithPath: documentDirectoryPath.appendingFormat("/\(masterIndex)/\(index+1).mp3")) if fileManager.fileExists(atPath: destinationURLForFile.path){ if endOfChapterSleepTimer == true { endOfChapterSleepTimer = false } else { index = index + 1 viewDidLoad() } } else { } } @IBAction func slide(_ slider: UISlider) { musicOperation.cancelAllOperations() let operation = BlockOperation() audioPlayer.currentTime = TimeInterval(slider.value) self.lockScreen() musicOperation.addOperation(operation) } @objc func applicationWillEnterForeground(notification: NSNotification) { slider.value = Float.init(audioPlayer.currentTime) }
Ответы (1 шт):
Назначьте проигрывание после перемотки на событие touchUpInside, то есть на момент отпускания слайдера
slider.addTarget(self, action: #selector(sliderTouchUpInside(_ :)), for: .touchUpInside)
@IBAction func sliderTouchUpInside(_ sender: UISlider) {
musicOperation.cancelAllOperations()
let operation = BlockOperation()
audioPlayer.currentTime = TimeInterval(time)
musicOperation.addOperation(operation)
}
Update
Здесь пример работы с плеером на основе вашего кода, BlockOperation здесь не требуется, касательно события Touch Up Inside - как следует из названия, это события завершения касания внутри контрола, чтобы отработать завершение касания и за его пределами, я добавил обработку Touch Up Outside. Также я уменьшил частоту срабатывания таймера, 0.5 сек вполне достаточно, если вы выводите время с точностью до секунд.
import UIKit
import AVKit
class ViewController: UIViewController {
@IBOutlet var slider: UISlider!
@IBOutlet var timeElapsed: UILabel!
@IBOutlet var timeDuration: UILabel!
@IBOutlet var playButton: UIButton!
var audioPlayer: AVAudioPlayer!
var timer: Timer?
var isRewindMode = false
// MARK: --
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
private func setup() {
NotificationCenter.default.addObserver(
self,
selector: #selector(applicationWillEnterForeground(notification:)),
name:UIApplication.willEnterForegroundNotification,
object: nil)
slider.addTarget(
self,
action: #selector(sliderTouchUp(_ :)),
for: [.touchUpInside, .touchUpOutside])
slider.addTarget(
self,
action: #selector(sliderTouchDown(_ :)),
for: .touchDown)
slider.addTarget(
self,
action: #selector(sliderValueChanged(_ :)),
for: .valueChanged)
setupTestPlayback()
}
// MARK: Playback
private func setupTestPlayback() {
do {
UIApplication.shared.beginReceivingRemoteControlEvents()
let session = AVAudioSession.sharedInstance()
try session.setCategory(AVAudioSession.Category.playback,
mode: .default,
policy: .longFormAudio,
options: [])
try session.setActive(true, options: [])
let url = Bundle.main.url(forResource: "0", withExtension: "mp3")!
audioPlayer = try AVAudioPlayer(contentsOf: url)
audioPlayer.delegate = self
if !audioPlayer.prepareToPlay() {
print("failed to prepare playback")
return
}
restorePlayerCurrentTime()
play()
} catch {
print(error)
}
}
private func setPlayerCurrentTime(_ time: Float) {
audioPlayer.currentTime = TimeInterval(time)
}
private func togglePlayback() {
if audioPlayer.isPlaying {
pause()
} else {
play()
}
}
private func play() {
audioPlayer.play()
playButton.setTitle("Pause", for: .normal)
slider.maximumValue = Float(audioPlayer.duration)
timer = Timer.scheduledTimer(
timeInterval: 0.5, // no need to set too frequent period
target: self,
selector: #selector(updateTime),
userInfo: nil,
repeats: true)
}
private func pause() {
audioPlayer.pause()
playButton.setTitle("Play", for: .normal)
timer?.invalidate()
}
@objc func updateTime() {
let currentTime = Int(audioPlayer.currentTime)
let minutes = currentTime / 60
let seconds = currentTime - minutes * 60
let durationTime = Int(audioPlayer.duration) - Int(audioPlayer.currentTime)
let minutes1 = durationTime / 60
let seconds1 = durationTime - minutes1 * 60
timeElapsed.text = NSString(format: "%02d:%02d", minutes, seconds) as String
timeDuration.text = NSString(format: "-%02d:%02d", minutes1, seconds1) as String
UserDefaults.standard.set(currentTime, forKey: "currentTime")
UserDefaults.standard.set(durationTime, forKey: "durationTime")
if !isRewindMode {
slider.value = Float(audioPlayer.currentTime)
}
}
private func restorePlayerCurrentTime() {
let currentTime = UserDefaults.standard.float(forKey: "currentTime")
setPlayerCurrentTime(currentTime)
slider.value = Float(audioPlayer.currentTime)
}
// MARK: UI Events
@IBAction func playButtonTouchUpInside(_ sender: UIButton) {
togglePlayback()
}
@IBAction func sliderValueChanged(_ sender: UISlider) {
isRewindMode = true
if !audioPlayer.isPlaying {
// rewind time if playback is stopped
setPlayerCurrentTime(sender.value)
updateTime()
}
}
@IBAction func sliderTouchUp(_ sender: UISlider) {
print(#function)
setPlayerCurrentTime(sender.value)
isRewindMode = false
}
@IBAction func sliderTouchDown(_ sender: UISlider) {
print(#function)
isRewindMode = true
}
@objc func applicationWillEnterForeground(notification: NSNotification) {
restorePlayerCurrentTime()
}
}
extension ViewController : AVAudioPlayerDelegate {
// TODO: implement
}