Video from jsturgis gist
Last active
February 10, 2024 18:21
-
-
Save AKosmachyov/2b9327545d4b538ec50ca3f3757c6cc7 to your computer and use it in GitHub Desktop.
SwiftUI + Combine. Sync Slider & AVPlayer states. MVVM
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// https://stackoverflow.com/questions/58779184/how-to-control-avplayer-in-swiftui | |
import SwiftUI | |
import AVKit | |
import Combine | |
struct TestView: View { | |
private var player = MediaPlayer(url: Bundle.main.url(forResource: "FileName", withExtension: "mp4")!) | |
var body: some View { | |
VStack { | |
VideoPlayer(player: player.player) | |
SliderView(player: player) | |
} | |
} | |
} | |
struct TestView_Previews: PreviewProvider { | |
static var previews: some View { | |
TestView() | |
} | |
} | |
class MediaPlayer { | |
var player: AVPlayer | |
var currentTimePublisher: PassthroughSubject<Double, Never> = .init() | |
var currentProgressPublisher: PassthroughSubject<Float, Never> = .init() | |
private var playerPeriodicObserver: Any? | |
init(url: URL) { | |
player = AVPlayer(url: url) | |
setupPeriodicObservation(for: player) | |
} | |
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) | |
self.currentProgressPublisher.send(progress) | |
self.currentTimePublisher.send(time.seconds) | |
} | |
} | |
private func calculateProgress(currentTime: Double) -> Float { | |
return Float(currentTime / duration) | |
} | |
private var duration: Double { | |
return player.currentItem?.duration.seconds ?? 0 | |
} | |
func play() { | |
player.play() | |
} | |
func pause() { | |
player.pause() | |
} | |
func seek(to time: CMTime) { | |
player.seek(to: time) | |
} | |
func seek(to percentage: Float) { | |
let time = convertFloatToCMTime(percentage) | |
player.seek(to: time) | |
} | |
private func convertFloatToCMTime(_ percentage: Float) -> CMTime { | |
return CMTime(seconds: duration * Double(percentage), preferredTimescale: CMTimeScale(NSEC_PER_SEC)) | |
} | |
} | |
class PlayerSliderViewModel: ObservableObject { | |
@Published var progressValue: Float = 0 | |
var player: MediaPlayer | |
var acceptProgressUpdates = true | |
var subscriptions: Set<AnyCancellable> = .init() | |
init(player: MediaPlayer) { | |
self.player = player | |
listenToProgress() | |
} | |
private func listenToProgress() { | |
player.currentProgressPublisher.sink { [weak self] progress in | |
guard let self = self, | |
self.acceptProgressUpdates else { return } | |
self.progressValue = progress | |
}.store(in: &subscriptions) | |
} | |
func didSliderChanged(_ didChange: Bool) { | |
acceptProgressUpdates = !didChange | |
if didChange { | |
player.pause() | |
} else { | |
player.seek(to: progressValue) | |
player.play() | |
} | |
} | |
} | |
struct SliderView: View { | |
@ObservedObject var viewModel: PlayerSliderViewModel | |
init(player: MediaPlayer) { | |
viewModel = .init(player: player) | |
} | |
var body: some View { | |
Slider(value: $viewModel.progressValue) { didChange in | |
viewModel.didSliderChanged(didChange) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment