How to control AVPlayer in SwiftUI?

旧时模样 提交于 2020-04-17 23:01:48

问题


In my new SwiftUI project I have an AVPlayer for streaming music from url. Now I need to control the current time of playing track and the volume through sliders, here is the part of design:

now I can control the player with UserData final class and its @Published vars, like 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()
    }

}

glad to know if there is better decision for this part 🧐

The problem is that properties currentTime, duration I can take only from player or player?.currentItem, so I can't make slider like this:

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

How can I control these things?


回答1:


I didn't find any solution, so tried to do it on my own. I learned Combine framework a little, inherited AVPlayer class and signed it under the protocol ObservableObject and used KVO. May be it's not the best solution, but it works, hope somebody will give me advices for improving the code in future. Here are some code snippets:

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)

        }

    }

}


update for VolumeView For volume control I made new 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)

    }

}


来源:https://stackoverflow.com/questions/58779184/how-to-control-avplayer-in-swiftui

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!