Swift Media Player
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

171 lines
5.7 KiB

//
// Created by Marco Schmickler on 28.11.21.
// Copyright (c) 2021 Marco Schmickler. All rights reserved.
//
import Foundation
import SwiftUI
import AVFoundation
// This is the UIView that contains the AVPlayerLayer for rendering the video
class VideoPlayerUIView: UIView {
private let player: AVPlayer
public let playerLayer = AVPlayerLayer()
private let videoPos: Binding<Double>
private let videoDuration: Binding<Double>
private let seeking: Binding<Bool>
private var durationObservation: NSKeyValueObservation?
private var timeObservation: Any?
init(player: AVPlayer, videoPos: Binding<Double>, videoDuration: Binding<Double>, seeking: Binding<Bool>) {
self.player = player
self.videoDuration = videoDuration
self.videoPos = videoPos
self.seeking = seeking
super.init(frame: .zero)
// backgroundColor = .black
playerLayer.player = player
layer.addSublayer(playerLayer)
// Observe the duration of the player's item so we can display it
// and use it for updating the seek bar's position
durationObservation = player.currentItem?.observe(\.duration, changeHandler: { [weak self] item, change in
guard let self = self else { return }
self.videoDuration.wrappedValue = item.duration.seconds
})
// Observe the player's time periodically so we can update the seek bar's
// position as we progress through playback
timeObservation = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.5, preferredTimescale: 600), queue: nil) { [weak self] time in
guard let self = self else { return }
// If we're not seeking currently (don't want to override the slider
// position if the user is interacting)
guard !self.seeking.wrappedValue else {
return
}
if let citem = self.player.currentItem {
self.videoDuration.wrappedValue = citem.duration.seconds
// update videoPos with the new video time (as a percentage)
self.videoPos.wrappedValue = time.seconds / self.videoDuration.wrappedValue
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
func cleanUp() {
// Remove observers we setup in init
durationObservation?.invalidate()
durationObservation = nil
if let observation = timeObservation {
player.removeTimeObserver(observation)
timeObservation = nil
}
}
}
// This is the SwiftUI view which wraps the UIKit-based PlayerUIView above
struct VideoPlayerView: UIViewRepresentable {
@ObservedObject var model: SVideoModel
let player: AVPlayer
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<VideoPlayerView>) {
// This function gets called if the bindings change, which could be useful if
// you need to respond to external changes, but we don't in this example
}
func makeUIView(context: UIViewRepresentableContext<VideoPlayerView>) -> UIView {
let uiView = VideoPlayerUIView(player: player,
videoPos: $model.videoPos,
videoDuration: $model.videoDuration,
seeking: $model.seeking)
return uiView
}
static func dismantleUIView(_ uiView: UIView, coordinator: ()) {
guard let playerUIView = uiView as? VideoPlayerUIView else {
return
}
playerUIView.cleanUp()
}
}
// This is the SwiftUI view that contains the controls for the player
struct VideoPlayerControlsView : View {
@ObservedObject var model: SVideoModel
let player: AVPlayer
var body: some View {
HStack {
// Play/pause button
Button(action: togglePlayPause) {
Image(systemName: model.paused ? "play" : "pause")
.padding(.trailing, 30)
}
// Current video time
let postime = model.videoPos * model.videoDuration
let postext = Utility.formatSecondsToHMS(postime)
let tens = postime.isNaN ? 0 : Int((postime*10.0).truncatingRemainder(dividingBy: 10))
Text("\(postext):\(tens)").foregroundColor(Color.white)
// Slider for seeking / showing video progress
Slider(value: $model.videoPos, in: 0...1, onEditingChanged: sliderEditingChanged)
// Video duration
Text("\(Utility.formatSecondsToHMS(model.videoDuration))").foregroundColor(Color.white)
}
.padding(.leading, 10)
.padding(.trailing, 10)
}
private func togglePlayPause() {
pausePlayer(!model.paused)
}
private func pausePlayer(_ pause: Bool) {
model.paused = pause
if pause {
player.pause()
}
else {
player.play()
}
}
private func sliderEditingChanged(editingStarted: Bool) {
if editingStarted {
// Set a flag stating that we're seeking so the slider doesn't
// get updated by the periodic time observer on the player
self.model.seeking = true
pausePlayer(true)
}
// Do the seek if we're finished
if !editingStarted {
let targetTime = CMTime(seconds: model.videoPos * model.videoDuration,
preferredTimescale: 600)
player.seek(to: targetTime) { _ in
// Now the seek is finished, resume normal operation
self.model.seeking = false
self.pausePlayer(false)
}
}
}
}