【问题标题】:How to control AVPlayer in SwiftUI?如何在 SwiftUI 中控制 AVPlayer?
【发布时间】:2020-03-05 19:57:26
【问题描述】:

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

现在我可以使用 UserData final 类及其 @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, duration 我只能从 playerplayer?.currentItem 获取,所以我不能像这样制作滑块:

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

我该如何控制这些东西?

【问题讨论】:

    标签: binding avplayer swiftui


    【解决方案1】:

    我没有找到任何解决方案,所以尝试自己解决。我学了一点Combine框架,继承了AVPlayer类并在协议ObservableObject下签名,使用KVO。可能它不是最好的解决方案,但它有效,希望有人能给我建议以改进未来的代码。下面是一些sn-ps的代码:

    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 struct:

    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, green: 0.813, blue: 0.837, 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)
    
        }
    
    }
    

    【讨论】:

      【解决方案2】:

      我建议将 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 boolean 的原因

      【讨论】:

      【解决方案3】:

      使用 'UIViewRepresentable' 根据您的要求创建一个 'MPVolumeView',如下所示

      import SwiftUI
      import MediaPlayer
      import UIKit
      
      struct VolumeSlider: UIViewRepresentable {
         func makeUIView(context: Context) -> MPVolumeView {
            MPVolumeView(frame: .zero)
         }
      
         func updateUIView(_ view: MPVolumeView, context: Context) {}
      }
      

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

      VolumeSlider()
         .frame(height: 40)
         .padding(.horizontal)
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-09-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2022-08-11
        • 2021-03-24
        • 2020-06-14
        相关资源
        最近更新 更多