Skip to content

Instantly share code, notes, and snippets.

@AKosmachyov
Last active February 10, 2024 18:21
Show Gist options
  • Save AKosmachyov/2b9327545d4b538ec50ca3f3757c6cc7 to your computer and use it in GitHub Desktop.
Save AKosmachyov/2b9327545d4b538ec50ca3f3757c6cc7 to your computer and use it in GitHub Desktop.
SwiftUI + Combine. Sync Slider & AVPlayer states. MVVM
/// 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