如何在SwiftUI中控制AVPlayer?

在我的新SwiftUI项目中,我有一个AVPlayer用于从url流音乐。现在,我需要通过滑块控制播放曲目的当前时间和音量,这是设计的一部分:

如何在SwiftUI中控制AVPlayer?

现在我可以使用UserData最后一个类及其@Published变量来控制播放器,例如 isPlaying

final class UserData: ObservableObject {
    // ...
    @Published var player: AVPlayer? = nil
    @Published var isPlaying: Bool = false
    //...

    func playPausePlayer(withSong song: Song,forPlaylist playlist: [Song]?) {
        //...
        if isPlaying {
            player?.pause()
        } else {
            player?.play()
        }

        isPlaying.toggle()
    }

}

很高兴知道这部分是否有更好的决定?

问题是属性 currentTime 持续时间我只能从playerplayer?.currentitem中获取,所以我无法像这个:

@EnvironmentObject var userData: UserData
// ...
Slider(value: userData.player?.currentitem?.currentTime()!,in: 0...userData.player?.currentitem?.duration as! Double,step: 1)

我该如何控制这些东西?

xuzhipeng52076 回答:如何在SwiftUI中控制AVPlayer?

我没有找到任何解决方案,因此尝试自行解决。我稍微学习了Combine框架,继承了AVPlayer类,并根据协议ObservableObject对其进行了签名,并使用了KVO。也许这不是最好的解决方案,但它确实有效,希望将来有人能给我一些改进代码的建议。以下是一些代码段:

import Foundation
import AVKit
import Combine

final class AudioPlayer: AVPlayer,ObservableObject {

    @Published var currentTimeInSeconds: Double = 0.0
    private var timeObserverToken: Any?
    // ... some other staff

    // MARK: Publishers
    var currentTimeInSecondsPass: AnyPublisher<Double,Never>  {
        return $currentTimeInSeconds
            .eraseToAnyPublisher()
    }

    // in init() method I add observer,which update time in seconds
    override init() {
        super.init()
        registerObserves()
    }

    private func registerObserves() {

        let interval = CMTime(seconds: 1,preferredTimescale: CMTimeScale(NSEC_PER_SEC))
        timeObserverToken = self.addPeriodicTimeObserver(forInterval: interval,queue: .main) {
            [weak self] _ in
            self?.currentTimeInSeconds = self?.currentTime().seconds ?? 0.0
        }

    } 

    // func for rewind song time
    func rewindTime(to seconds: Double) {
        let timeCM = CMTime(seconds: seconds,preferredTimescale: CMTimeScale(NSEC_PER_SEC))
        self.seek(to: timeCM)
    }

    // sure I need to remove observer:
    deinit {

        if let token = timeObserverToken {
            self.removeTimeObserver(token)
            timeObserverToken = nil
        }

    }

}

// simplified slider 

import SwiftUI

struct PlayerSlider: View {

    @EnvironmentObject var player: AudioPlayer
    @State private var currentPlayerTime: Double = 0.0
    var song: Song // struct which contains the song length as Int

    var body: some View {

        HStack {

            GeometryReader { geometry in
                Slider(value: self.$currentPlayerTime,in: 0.0...Double(self.song.songLength))
                    .onReceive(self.player.currentTimeInSecondsPass) { _ in
                    // here I changed the value every second
                        self.currentPlayerTime = self.player.currentTimeInSeconds
                }
                // controlling rewind
                .gesture(DragGesture(minimumDistance: 0)
                .onChanged({ value in
                    let coefficient = abs(Double(self.song.songLength) / Double(geometry.size.width))
                    self.player.rewindTime(to: Double(value.location.x) * coefficient)
                }))
            }
            .frame(height: 30)

        }

    }

}


VolumeView的更新 对于音量控制,我制作了新的UIViewRepresentable结构:

import SwiftUI
import UIKit
import MediaPlayer

struct MPVolumeViewRepresenter: UIViewRepresentable {


    func makeUIView(context: Context) -> MPVolumeView {

        let volumeView = MPVolumeView()
        volumeView.showsRouteButton = false // TODO: 'showsRouteButton' was deprecated in iOS 13.0: Use AVRoutePickerView instead.
        if let sliderView = volumeView.subviews.first as? UISlider {
        // custom design colors
            sliderView.minimumTrackTintColor = UIColor(red: 0.805,green: 0.813,blue: 0.837,alpha: 1)
            sliderView.thumbTintColor = UIColor(red: 0.805,alpha: 1)
            sliderView.maximumTrackTintColor = UIColor(red: 0.906,green: 0.91,blue: 0.929,alpha: 1)
        }

        return volumeView

    }

    func updateUIView(_ uiView: MPVolumeView,context: UIViewRepresentableContext<MPVolumeViewRepresenter>) {
        // nothing here. really,nothing
    }

}

// and you can use it like:
struct VolumeView: View {

    var body: some View {

        HStack(alignment: .center) {
            Image("volumeDown")
                .renderingMode(.original)
                .resizable()
                .frame(width: 24,height: 24)

                MPVolumeViewRepresenter()
                    .frame(height: 24)
                    .offset(y: 2) // centering

            Image("volumeUp")
                .renderingMode(.original)
                .resizable()
                .frame(width: 24,height: 24)

        }.padding(.horizontal)

    }

}
,

使用如下所示的“ UIViewRepresentable”作为您的要求来创建“ MPVolumeView”

<div class="entry-content">
  <h2>My title</h2>
  <div class="mnk-block">
    <h2>My block title</h2>
  </div>
</div>

然后在如下所示的“查看”文件中使用它

import SwiftUI
import MediaPlayer
import UIKit

struct VolumeSlider: UIViewRepresentable {
   func makeUIView(context: Context) -> MPVolumeView {
      MPVolumeView(frame: .zero)
   }

   func updateUIView(_ view: MPVolumeView,context: Context) {}
}
,

我建议将Combine注入您的代码中。例如,为了使滑块更新。

class MediaPlayer {

    var player: AVPlayer!
    var currentTimePublisher: PassthroughSubject<Double,Never> = .init() 
    var currentProgressPublisher: PassthroughSubject<Float,Never> = .init() 
    ... 

    private func setupPeriodicObservation(for player: AVPlayer) {
        let timeScale = CMTimeScale(NSEC_PER_SEC)
        let time = CMTime(seconds: 0.5,preferredTimescale: timeScale)
        
        playerPeriodicObserver = player.addPeriodicTimeObserver(forInterval: time,queue: .main) { [weak self] (time) in
            guard let `self` = self else { return }
            let progress = self.calculateProgress(currentTime: time.seconds)
            currentProgressPublisher.send(progress)
            currentTimePublisher.send(time.seconds)
        }
    }

    private func calculateProgress(currentTime: Double) -> Float {
        let duration = player.currentItem!.duration.seconds
        return Float(currentTime / duration)
    }

    func play() {
        player.play()
    }
    
    func pause() {
        player.pause()
    }
    
    func seek(to time: CMTime) {
        player.seek(to: time)
    }
}


class PlayerSliderViewModel: ObservableObject {
    @Published currentTime: String 
    @Published progressValue: Float 
    
    var player: MediaPlayer!
    var invalidateProgress: Bool = false 
    var subscriptions: Set<AnyCancellable> = .init()
    
    func listenToProgress {
        player.currentProgressPublisher.sink { [weak self] progress in 
            guard !invalidateProgress else { return }
            self?.progressValue = progress 
        }.store(in: &subscriptions)
    }
}

struct SliderView: View {
    @ObservedObject var viewModel: PlayerSliderViewModel

    init(player: MediaPlayer) {
        viewModel = .init(player: player)
    } 

    var body: some View {
        Slider(value: $viewModel.progressValue,onEditingChanged: { didChange in
                    self.viewModel.invalidateProgress = didChange
                    if didChange {
                        self.viewModel.player.pause()
                    }
                    if !didChange {
                        let percentage = self.viewModel.progressValue
                        let time = self.convertFloatToCMTime(progress: percentage)
                        self.viewModel.player.seek(to: time)
                        self.viewModel.player.play()
                    }
                })
    }

    func convertFloatToCMTime(progress: Float) -> CMTime {
        ...
    } 
}

您可以执行类似的操作来获取当前时间。此外,我添加了使用滑块的内置onEditingChanged闭包来查找曲目的代码。

为了防止发布者在移动结果时将结果写入滑块,必须停止发布值,这就是为什么我包含了invalidateProgress布尔值

本文链接:https://www.f2er.com/3132939.html

大家都在问