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
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)
|
|
}
|
|
}
|
|
}
|
|
}
|