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