Browse Source

Swift Video 2

master
marcoschmickler 4 years ago
parent
commit
7289a66d2b
  1. 2
      kplayer/core/LocalManager.swift
  2. 22
      kplayer/core/MediaItem.swift
  3. 67
      kplayer/detail/DetailViewController.swift
  4. 6
      kplayer/detail/EditItemView.swift
  5. 29
      kplayer/svideo/SVideoModel.swift
  6. 316
      kplayer/svideo/SVideoPlayer.swift
  7. 46
      kplayer/svideo/VideoPlayerView.swift
  8. 13
      kplayer/util/AsyncImage.swift

2
kplayer/core/LocalManager.swift

@ -63,7 +63,7 @@ class LocalManager {
} else {
if let id = c.image, let imageData = id.jpegData(compressionQuality: 1.0) {
do {
try imageData.write(to: pt)
try imageData.write(to: p)
} catch {
print("error")
}

22
kplayer/core/MediaItem.swift

@ -40,6 +40,9 @@ class MediaItem: CustomDebugStringConvertible, ObservableObject, Identifiable {
@Published
var image: UIImage?
@Published
var thumbImage: UIImage?
// let didChange = PassthroughSubject<MediaItem,Never>()
@Published
@ -68,8 +71,11 @@ class MediaItem: CustomDebugStringConvertible, ObservableObject, Identifiable {
var leaf = false
var cancelled = false
@Published
var scale = 1.0
@Published
var offset = CGPoint()
var size = CGSize()
convenience init(model: MediaModel) {
@ -84,12 +90,14 @@ class MediaItem: CustomDebugStringConvertible, ObservableObject, Identifiable {
self.offset = model.offset
self.size = model.size
self.loaded = true
self.local = true
for m in model.children {
let item = MediaItem(model: m)
item.index = children.count
item.parent = self
item.loaded = true
item.local = true
children.append(item)
}
}
@ -277,22 +285,26 @@ class MediaItem: CustomDebugStringConvertible, ObservableObject, Identifiable {
}
func isWeb() -> Bool {
return type == ItemType.WEB
type == ItemType.WEB
}
func isPic() -> Bool {
return type == ItemType.PICS || type == ItemType.DETAILS
type == ItemType.PICS || type == ItemType.DETAILS
}
func isVideo() -> Bool {
return type == ItemType.VIDEO || type == ItemType.SNAPSHOT
type == ItemType.VIDEO || type == ItemType.SNAPSHOT
}
func isDetails() -> Bool {
return isWeb() || isPic() || isVideo()
isWeb() || isPic() || isVideo()
}
func isFolder() -> Bool {
return type == ItemType.REMOTEROOT || type == ItemType.FOLDER || type == ItemType.WEBROOT || type == ItemType.FAVROOT
type == ItemType.REMOTEROOT || type == ItemType.FOLDER || type == ItemType.WEBROOT || type == ItemType.FAVROOT
}
func clone() -> MediaItem {
MediaItem(model: toMediaModel())
}
}

67
kplayer/detail/DetailViewController.swift

@ -397,7 +397,12 @@ class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout
}
if sectionItem.isVideo() {
self.showVideo(selectedItem: selectedItem)
if self.delegate!.settings().newPlayer {
self.showNewVideo(selectedItem: selectedItem)
}
else {
self.showVideo(selectedItem: selectedItem)
}
} else if sectionItem.isPic() {
self.showPhotos(sectionItem.children)
} else if sectionItem.isWeb() {
@ -460,26 +465,56 @@ class DetailViewController: UIViewController, UICollectionViewDelegateFlowLayout
self.present(navController, animated: false, completion: nil)
}
func showVideo(selectedItem: MediaItem) {
func getWindow() -> UIWindow {
let delegate2 = UIApplication.shared.delegate!
return delegate2.window as! UIWindow
}
func showNewVideo(selectedItem: MediaItem) {
var se = selectedItem
var children = selectedItem.parent!.children
if delegate!.settings().newPlayer {
let model = SVideoModel(allItems: children, currentSnapshot: se)
var children = [MediaItem]()
var clonedChildren = [MediaItem]()
var baseItem = selectedItem
let player = SVideoPlayer(completionHandler: {
self.dismiss(animated: true, completion: nil);
}, model: model)
player
if baseItem.type == ItemType.SNAPSHOT {
baseItem = selectedItem.parent!
}
let pc = UIHostingController(rootView: player)
pc.view.backgroundColor = .black
let navController = UINavigationController(rootViewController: pc) // Creating a navigation controller with pc at the root of the navigation stack.
navController.modalPresentationStyle = .fullScreen
navController.setNavigationBarHidden(true, animated: false)
children = baseItem.children
clonedChildren = baseItem.clone().children
present(navController, animated: false, completion: nil)
let model = SVideoModel(allItems: children, currentSnapshot: se, baseItem: baseItem)
return
let player = SVideoPlayer(completionHandler: { saved in
if saved {
baseItem.children = model.allItems
self.delegate!.saveItem(selectedItem: baseItem)
}
else {
baseItem.children = clonedChildren
}
self.collectionView.reloadData()
self.collectionView.collectionViewLayout.invalidateLayout()
self.dismiss(animated: true, completion: nil);
}, model: model)
player
let pc = UIHostingController(rootView: player)
pc.view.backgroundColor = .black
getWindow().rootViewController!.definesPresentationContext = true
pc.modalPresentationStyle = .overCurrentContext
getWindow().rootViewController!.present(pc, animated: true)
}
func showVideo(selectedItem: MediaItem) {
var se = selectedItem
var children = [MediaItem]()
if selectedItem.parent != nil {
children = selectedItem.parent!.children
}
var pc: VideoController

6
kplayer/detail/EditItemView.swift

@ -72,19 +72,19 @@ struct EditItemView: View {
HStack {
Button(action: delegate.captureZoom, label: {
Text("Zoom")
}).buttonStyle(BorderlessButtonStyle());
}).padding(10).buttonStyle(BorderlessButtonStyle());
Button(action: {
item.scale = 1.0
item.offset = CGPoint(x: 0,y: 0)
item.objectWillChange.send()
}, label: {
Text("Reset")
}).buttonStyle(BorderlessButtonStyle());
}).padding(10).buttonStyle(BorderlessButtonStyle());
Button(action: {
delegate.cancelEdit()
}, label: {
Text("cancel")
}).buttonStyle(BorderlessButtonStyle());
}).padding(10).buttonStyle(BorderlessButtonStyle());
}
}.background(Color.clear)
}.background(Color.clear)

29
kplayer/svideo/SVideoModel.swift

@ -7,17 +7,36 @@ import Foundation
import AVKit
class SVideoModel : ObservableObject {
var allItems : [MediaItem]
var currentSnapshot: MediaItem
@Published var allItems : [MediaItem]
@Published var currentSnapshot: MediaItem
@Published var baseItem: MediaItem
init(allItems: [MediaItem], currentSnapshot: MediaItem) {
// The progress through the video, as a percentage (from 0 to 1)
@Published var videoPos: Double = 0
// The duration of the video in seconds
@Published var videoDuration: Double = 0
// Whether we're currently interacting with the seek bar or doing a seek
@Published var seeking = false
@Published var paused = false
@Published var edit = true
@Published var loop = false
@Published var speed: Float = 1.0
@Published var scale: CGFloat = 1.0
@Published var dragOffset: CGSize = CGSize.zero
var observer: Any?
init(allItems: [MediaItem], currentSnapshot: MediaItem, baseItem: MediaItem) {
self.allItems = allItems
self.currentSnapshot = currentSnapshot
self.baseItem = baseItem
}
func currentPlayerItem() -> AVPlayerItem {
AVPlayerItem(asset: AVAsset(url: currentSnapshot.playerURL!))
}
}

316
kplayer/svideo/SVideoPlayer.swift

@ -7,34 +7,62 @@ import Foundation
import SwiftUI
import AVKit
struct SVideoPlayer: View {
struct SVideoPlayer: View, EditItemDelegate {
// url: URL(string: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8")!
var player = AVQueuePlayer(items: [AVPlayerItem]())
var completionHandler: (() -> Void)?
var completionHandler: ((Bool) -> Void)?
@ObservedObject
var model: SVideoModel
// The progress through the video, as a percentage (from 0 to 1)
@State private var videoPos: Double = 0
// The duration of the video in seconds
@State private var videoDuration: Double = 0
// Whether we're currently interacting with the seek bar or doing a seek
@State private var seeking = false
@State private var scale: CGFloat = 1.0
@State private var lastScaleValue: CGFloat = 1.0
@State private var lastDragOffset: CGSize = CGSize.zero
@State private var dragOffset: CGSize = CGSize.zero
@State var seekSmoothly = false
@State var smoothTime = -1.0
@State var smoothSeekTime = -1.0
let steps : [Float] = [0.25, 0.5, 1.0, 2.0 ]
init(completionHandler: ((Bool) -> ())?, model: SVideoModel) {
self.completionHandler = completionHandler
self.model = model
}
func cleanup() {
player.removeTimeObserver(model.observer)
}
var body: some View {
VStack {
HStack {
Button(action: {completionHandler!(model.edit)}, label: {
Text("cancel")
}).buttonStyle(BorderlessButtonStyle())
Button(action: {
completionHandler!()
model.loop.toggle()
}, label: {
Text("cancel")
}).buttonStyle(BorderlessButtonStyle());
Text("loop")
}).foregroundColor(model.loop ? Color.yellow : Color.blue).buttonStyle(BorderlessButtonStyle())
Button(action: {
for s in steps {
if model.speed < s {
model.speed = s
break
}
else {
if s == 2.0 {
model.speed = steps[0]
}
}
}
player.rate = model.speed
}, label: {
Text("\(model.speed, specifier: "%.2f")")
}).buttonStyle(BorderlessButtonStyle())
Text(model.currentSnapshot.name).foregroundColor(Color.blue)
ScrollView(.horizontal, showsIndicators: false) {
HStack {
@ -47,72 +75,209 @@ struct SVideoPlayer: View {
}
}
}
}.frame(height: 50)
}
Spacer()
}
VideoPlayerView(videoPos: $videoPos,
videoDuration: $videoDuration,
seeking: $seeking,
player: player)
.scaleEffect(scale)
.offset(dragOffset)
.gesture(
DragGesture()
.onChanged { gesture in
let dragged = gesture.translation
if move(dragged) {
// lastDragOffset = gesture.translation
dragOffset = CGSize(width: dragged.width + lastDragOffset.width, height: dragged.height + lastDragOffset.height)
}
}
.onEnded { gesture in
lastDragOffset = dragOffset
}
)
.gesture(MagnificationGesture().onChanged { val in
let delta = val / self.lastScaleValue
self.lastScaleValue = val
self.scale = self.scale * delta
Button(action: { model.edit = true }, label: {
Text("edit")
}).buttonStyle(BorderlessButtonStyle());
Button(action: doSnapshot, label: {
Text("snap")
}).buttonStyle(BorderlessButtonStyle());
}.frame(height: 50)
VStack {
let v = VideoPlayerView(model: model,
player: player)
.scaleEffect(model.scale)
.offset(model.dragOffset)
.gesture(
DragGesture()
.onChanged { gesture in
let dragged = gesture.translation
if move(dragged, start: gesture.startLocation) {
model.dragOffset = CGSize(width: dragged.width + lastDragOffset.width, height: dragged.height + lastDragOffset.height)
}
}
.onEnded { gesture in
lastDragOffset = model.dragOffset
smoothTime = -1.0
}
)
.gesture(MagnificationGesture()
.onChanged { val in
let delta = val / self.lastScaleValue
self.lastScaleValue = val
self.model.scale = self.model.scale * delta
//... anything else e.g. clamping the newScale
}.onEnded { val in
// without this the next gesture will be broken
self.lastScaleValue = 1.0
}).clipped()
VideoPlayerControlsView(videoPos: $videoPos,
videoDuration: $videoDuration,
seeking: $seeking,
player: player)
}.onEnded { val in
// without this the next gesture will be broken
self.lastScaleValue = 1.0
}).contentShape(Rectangle())
if model.edit && model.videoDuration != Double.nan && model.videoDuration > 0.0 {
v.overlay(EditItemView(item: model.currentSnapshot, len: model.videoDuration, delegate: self)
.frame(width: 400, alignment: .top).offset(x: 0, y: -50), alignment: .topTrailing)
}
else {
v
}
VideoPlayerControlsView(model: model,
player: player)
}.clipped()
}
.onAppear() {
model.observer = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.1, preferredTimescale: 600), queue: nil) { time in
if model.loop && model.currentSnapshot.length > 0 {
if time.seconds > model.currentSnapshot.time + model.currentSnapshot.length {
seekTimeSmoothly(model.currentSnapshot.time)
}
}
}
let item = model.currentPlayerItem()
self.player.replaceCurrentItem(with: item)
// player.removeAllItems()
// player.insert(model.currentPlayerItem(), after: nil)
// Start the player going, otherwise controls don't appear
gotoSnapshot(model.currentSnapshot)
player.play()
}
.onDisappear {
// When this View isn't being shown anymore stop the player
player.pause()
cleanup()
self.player.replaceCurrentItem(with: nil)
}
}
func doSnapshot() {
// if edit {
let currentItem = player.currentItem!
let asset = currentItem.asset
do {
let imgGenerator = AVAssetImageGenerator(asset: asset)
imgGenerator.appliesPreferredTrackTransform = true
let time = currentItem.currentTime()
let cgImage = try imgGenerator.copyCGImage(at: time, actualTime: nil)
let thumbnail = UIImage(cgImage: cgImage)
showThumbnail(currentItem: model.baseItem, thumbnail: thumbnail, time: time)
private func move(_ dragged: CGSize) -> Bool {
// if player.status == .playing {
return true
//}
} catch let error {
print("*** Error generating thumbnail: \(error.localizedDescription)")
}
// }
}
func gotoSnapshot(_ currentSnapshot: MediaItem) {
print(currentSnapshot.time)
func showThumbnail(currentItem: MediaItem, thumbnail: UIImage, time: CMTime) {
let newItem = MediaItem(name: currentItem.name, path: currentItem.path, root: currentItem.root, type: ItemType.SNAPSHOT)
newItem.image = thumbnail
newItem.time = time.seconds
newItem.parent = currentItem
newItem.local = currentItem.local
newItem.external = currentItem.external
model.allItems.append(newItem)
}
private func move(_ dragged: CGSize, start: CGPoint) -> Bool {
if model.paused {
if let time = getCurrentTime() {
if smoothTime < 0 {
smoothTime = time
}
player.seek(to: CMTime(seconds: currentSnapshot.time, preferredTimescale: CMTimeScale(10000)))
if (start.y < 130) {
let delta = dragged.width / 1000.0
// print(delta)
seekTimeSmoothly(smoothTime + delta)
return false
} else {
return true
}
}
else {
return true
}
}
if let time = getCurrentTime() {
let sk = model.seeking
model.seeking = false
// print("Start \(start.y) Drag \(dragged.width)))")
let dragWidth = 20.0
if (start.y < 130) {
if model.scale > 1.0 {
return true
}
if dragged.width > dragWidth {
seekTimeSmoothly(time + 5.0)
} else if dragged.width < -dragWidth {
seekTimeSmoothly(time - 5.0)
}
}
else {
if dragged.width > dragWidth {
seekTimeSmoothly(time + 30.0)
} else if dragged.width < -dragWidth {
seekTimeSmoothly(time - 30.0)
}
}
model.seeking = sk
}
return false
}
private func getCurrentTime() -> Double? {
player.currentItem?.currentTime().seconds
}
func seekTime(_ time: Double) {
// print(time)
player.seek(to: CMTime(seconds: time, preferredTimescale: CMTimeScale(10000)))
}
func seekTimeSmoothly(_ time: Double) {
print("smooth \(time)")
if seekSmoothly {
smoothSeekTime = time
print("delay \(smoothSeekTime)")
return
}
if smoothSeekTime == time {
print("finished \(smoothSeekTime)")
smoothSeekTime = -1.0
return
}
seekSmoothly = true
print("seek \(time)")
player.seek(to: CMTime(seconds: time, preferredTimescale: CMTimeScale(10000)), toleranceBefore: CMTime.zero, toleranceAfter: CMTime.zero) { _ in
seekSmoothly = false
if smoothSeekTime > 0.0 {
print("next level")
seekTimeSmoothly(smoothSeekTime)
}
}
}
func gotoSnapshot(_ currentSnapshot: MediaItem) {
seekTime(currentSnapshot.time)
// player.forceSeekSmoothlyToTime(newChaseTime: currentSnapshot.time)
// loopStart = currentSnapshot.time
// player.loopEnd = loopStart + currentSnapshot.length
@ -126,10 +291,43 @@ struct SVideoPlayer: View {
model.currentSnapshot = currentSnapshot
}
func captureZoom() {
model.currentSnapshot.scale = model.scale
model.currentSnapshot.offset = CGPoint(x: model.dragOffset.width, y: model.dragOffset.height)
//model.objectWillChange()
}
func setStart() {
if let time = getCurrentTime() {
model.currentSnapshot.time = time
}
}
func setEnd() {
if let time = getCurrentTime() {
let snapTime = model.currentSnapshot.time
if time > snapTime {
model.currentSnapshot.length = time - snapTime
}
}
}
func cancelEdit() {
model.edit = false
}
func seek(_ v: Double) {
seekTimeSmoothly(v)
}
}
struct SVideoPlayer_Previews: PreviewProvider {
static var previews: some View {
SVideoPlayer(model: SVideoModel(allItems: [MediaItem](), currentSnapshot: MediaItem(name: "extern", path: "", root: "", type: ItemType.FAVROOT)))
SVideoPlayer(completionHandler: {b in}, model: SVideoModel(allItems: [MediaItem](),
currentSnapshot: MediaItem(name: "extern", path: "", root: "", type: ItemType.FAVROOT),
baseItem: MediaItem(name: "extern", path: "", root: "", type: ItemType.FAVROOT)
))
}
}

46
kplayer/svideo/VideoPlayerView.swift

@ -47,9 +47,11 @@ class VideoPlayerUIView: UIView {
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
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
}
}
}
@ -78,9 +80,7 @@ class VideoPlayerUIView: UIView {
// 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
@ObservedObject var model: SVideoModel
let player: AVPlayer
@ -91,9 +91,9 @@ struct VideoPlayerView: UIViewRepresentable {
func makeUIView(context: UIViewRepresentableContext<VideoPlayerView>) -> UIView {
let uiView = VideoPlayerUIView(player: player,
videoPos: $videoPos,
videoDuration: $videoDuration,
seeking: $seeking)
videoPos: $model.videoPos,
videoDuration: $model.videoDuration,
seeking: $model.seeking)
return uiView
}
@ -108,39 +108,35 @@ struct VideoPlayerView: UIViewRepresentable {
// 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
@ObservedObject var model: SVideoModel
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)
Image(systemName: model.paused ? "play" : "pause")
.padding(.trailing, 30)
}
// Current video time
Text("\(Utility.formatSecondsToHMS(videoPos * videoDuration))")
Text("\(Utility.formatSecondsToHMS(model.videoPos * model.videoDuration))").foregroundColor(Color.white)
// Slider for seeking / showing video progress
Slider(value: $videoPos, in: 0...1, onEditingChanged: sliderEditingChanged)
Slider(value: $model.videoPos, in: 0...1, onEditingChanged: sliderEditingChanged)
// Video duration
Text("\(Utility.formatSecondsToHMS(videoDuration))")
Text("\(Utility.formatSecondsToHMS(model.videoDuration))").foregroundColor(Color.white)
}
.padding(.leading, 10)
.padding(.trailing, 10)
}
private func togglePlayPause() {
pausePlayer(!playerPaused)
pausePlayer(!model.paused)
}
private func pausePlayer(_ pause: Bool) {
playerPaused = pause
if playerPaused {
model.paused = pause
if pause {
player.pause()
}
else {
@ -152,17 +148,17 @@ struct VideoPlayerControlsView : View {
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
self.model.seeking = true
pausePlayer(true)
}
// Do the seek if we're finished
if !editingStarted {
let targetTime = CMTime(seconds: videoPos * videoDuration,
let targetTime = CMTime(seconds: model.videoPos * model.videoDuration,
preferredTimescale: 600)
player.seek(to: targetTime) { _ in
// Now the seek is finished, resume normal operation
self.seeking = false
self.model.seeking = false
self.pausePlayer(false)
}
}

13
kplayer/util/AsyncImage.swift

@ -25,8 +25,8 @@ struct AsyncImage<Placeholder: View>: View {
private var content: some View {
Group {
if item.image != nil {
image(item.image!)
if item.thumbImage != nil {
image(item.thumbImage!)
} else {
placeholder
}
@ -34,16 +34,21 @@ struct AsyncImage<Placeholder: View>: View {
}
func setImage(newItem: MediaItem) {
if newItem.thumbImage != nil {
return
}
if newItem.image != nil {
let icon = newItem.image!.scaleToSize(66.0, height: 44.0)
newItem.thumbImage = 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)
newItem.image = i
newItem.thumbImage = newItem.image!.scaleToSize(66.0, height: 44.0)
}
}
}

Loading…
Cancel
Save