9 changed files with 374 additions and 38 deletions
-
12kplayer.xcodeproj/project.pbxproj
-
3kplayer/core/KSettings.swift
-
1kplayer/core/MediaItem.swift
-
13kplayer/detail/DetailViewController.swift
-
3kplayer/master/KSettingsView.swift
-
103kplayer/svideo/SVideoPlayer.swift
-
170kplayer/svideo/VideoPlayerView.swift
-
51kplayer/util/AsyncImage.swift
-
30kplayer/util/Utility.swift
@ -0,0 +1,170 @@ |
|||
// |
|||
// 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 |
|||
private 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 |
|||
} |
|||
|
|||
self.videoDuration.wrappedValue = self.player.currentItem!.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 { |
|||
@Binding private(set) var videoPos: Double |
|||
@Binding private(set) var videoDuration: Double |
|||
@Binding private(set) var seeking: Bool |
|||
|
|||
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: $videoPos, |
|||
videoDuration: $videoDuration, |
|||
seeking: $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 { |
|||
@Binding private(set) var videoPos: Double |
|||
@Binding private(set) var videoDuration: Double |
|||
@Binding private(set) var seeking: Bool |
|||
|
|||
let player: AVPlayer |
|||
|
|||
@State var playerPaused = true |
|||
|
|||
var body: some View { |
|||
HStack { |
|||
// Play/pause button |
|||
Button(action: togglePlayPause) { |
|||
Image(systemName: playerPaused ? "play" : "pause") |
|||
.padding(.trailing, 10) |
|||
} |
|||
// Current video time |
|||
Text("\(Utility.formatSecondsToHMS(videoPos * videoDuration))") |
|||
// Slider for seeking / showing video progress |
|||
Slider(value: $videoPos, in: 0...1, onEditingChanged: sliderEditingChanged) |
|||
// Video duration |
|||
Text("\(Utility.formatSecondsToHMS(videoDuration))") |
|||
} |
|||
.padding(.leading, 10) |
|||
.padding(.trailing, 10) |
|||
} |
|||
|
|||
private func togglePlayPause() { |
|||
pausePlayer(!playerPaused) |
|||
} |
|||
|
|||
private func pausePlayer(_ pause: Bool) { |
|||
playerPaused = pause |
|||
if playerPaused { |
|||
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 |
|||
seeking = true |
|||
pausePlayer(true) |
|||
} |
|||
|
|||
// Do the seek if we're finished |
|||
if !editingStarted { |
|||
let targetTime = CMTime(seconds: videoPos * videoDuration, |
|||
preferredTimescale: 600) |
|||
player.seek(to: targetTime) { _ in |
|||
// Now the seek is finished, resume normal operation |
|||
self.seeking = false |
|||
self.pausePlayer(false) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
import SwiftUI |
|||
import Haneke |
|||
|
|||
struct AsyncImage<Placeholder: View>: View { |
|||
@StateObject private var item: MediaItem |
|||
private let placeholder: Placeholder |
|||
private let image: (UIImage) -> Image |
|||
|
|||
init( |
|||
item: MediaItem, |
|||
@ViewBuilder placeholder: () -> Placeholder, |
|||
@ViewBuilder image: @escaping (UIImage) -> Image = Image.init(uiImage:) |
|||
) { |
|||
self.placeholder = placeholder() |
|||
self.image = image |
|||
_item = StateObject(wrappedValue: item) |
|||
|
|||
setImage(newItem: item) |
|||
} |
|||
|
|||
var body: some View { |
|||
content |
|||
// .onAppear(perform: loader.load) |
|||
} |
|||
|
|||
private var content: some View { |
|||
Group { |
|||
if item.image != nil { |
|||
image(item.image!) |
|||
} else { |
|||
placeholder |
|||
} |
|||
} |
|||
} |
|||
|
|||
func setImage(newItem: MediaItem) { |
|||
if newItem.image != nil { |
|||
let icon = newItem.image!.scaleToSize(66.0, height: 44.0) |
|||
} else { |
|||
if newItem.thumbUrl != nil { |
|||
let URL = Foundation.URL(string: newItem.thumbUrlAbsolute)! |
|||
|
|||
Shared.imageCache.fetch(URL: URL).onSuccess { |
|||
i in |
|||
newItem.image = i.scaleToSize(66.0, height: 44.0) |
|||
|
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
// |
|||
// Utility.swift |
|||
// AVPlayer-SwiftUI |
|||
// |
|||
// Created by Chris Mash on 11/09/2019. |
|||
// Copyright © 2019 Chris Mash. All rights reserved. |
|||
// |
|||
|
|||
import Foundation |
|||
|
|||
class Utility: NSObject { |
|||
|
|||
private static var timeHMSFormatter: DateComponentsFormatter = { |
|||
let formatter = DateComponentsFormatter() |
|||
formatter.unitsStyle = .positional |
|||
formatter.allowedUnits = [.minute, .second] |
|||
formatter.zeroFormattingBehavior = [.pad] |
|||
return formatter |
|||
}() |
|||
|
|||
static func formatSecondsToHMS(_ seconds: Double) -> String { |
|||
guard !seconds.isNaN, |
|||
let text = timeHMSFormatter.string(from: seconds) else { |
|||
return "00:00" |
|||
} |
|||
|
|||
return text |
|||
} |
|||
|
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue