|
|
|
@ -10,6 +10,7 @@ import AVKit |
|
|
|
struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
// url: URL(string: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8")! |
|
|
|
var player = AVQueuePlayer(items: [AVPlayerItem]()) |
|
|
|
var playerLooper : AVPlayerLooper |
|
|
|
var completionHandler: ((Bool) -> Void)? |
|
|
|
|
|
|
|
@ObservedObject |
|
|
|
@ -18,16 +19,19 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
@State private var lastScaleValue: CGFloat = 1.0 |
|
|
|
@State private var lastDragOffset: CGSize = CGSize.zero |
|
|
|
|
|
|
|
@State var savetext = "save" |
|
|
|
@State var confirmationShown = false |
|
|
|
@State var seekSmoothly = false |
|
|
|
@State var smoothTime = -1.0 |
|
|
|
@State var smoothSeekTime = -1.0 |
|
|
|
@State var timeCounter = 0 |
|
|
|
|
|
|
|
let steps : [Float] = [0.25, 0.5, 1.0, 2.0 ] |
|
|
|
|
|
|
|
init(completionHandler: ((Bool) -> ())?, model: SVideoModel) { |
|
|
|
self.completionHandler = completionHandler |
|
|
|
self.model = model |
|
|
|
|
|
|
|
self.playerLooper = AVPlayerLooper(player: player , templateItem: model.currentPlayerItem()) |
|
|
|
} |
|
|
|
|
|
|
|
func cleanup() { |
|
|
|
@ -62,6 +66,25 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
}, label: { |
|
|
|
Text("\(model.speed, specifier: "%.2f")") |
|
|
|
}).buttonStyle(BorderlessButtonStyle()) |
|
|
|
|
|
|
|
Button(action: { confirmationShown = true }, label: { |
|
|
|
Text(savetext) |
|
|
|
}).buttonStyle(BorderlessButtonStyle()) |
|
|
|
.confirmationDialog("Save to folder", isPresented: $confirmationShown) { |
|
|
|
Button("1") { |
|
|
|
save(currentSnapshot: model.currentSnapshot, name: "1") |
|
|
|
} |
|
|
|
Button("2") { |
|
|
|
save(currentSnapshot: model.currentSnapshot, name: "2") |
|
|
|
} |
|
|
|
Button("3") { |
|
|
|
save(currentSnapshot: model.currentSnapshot, name: "3") |
|
|
|
} |
|
|
|
|
|
|
|
Button("cancel", role: .cancel) {} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Text(model.currentSnapshot.name).foregroundColor(Color.blue) |
|
|
|
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) { |
|
|
|
@ -135,21 +158,34 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
} |
|
|
|
.onAppear() { |
|
|
|
model.observer = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.1, preferredTimescale: 600), queue: nil) { time in |
|
|
|
|
|
|
|
if timeCounter >= 1 { |
|
|
|
timeCounter -= 1 |
|
|
|
} |
|
|
|
if model.loop && model.currentSnapshot.length > 0 { |
|
|
|
if time.seconds > model.currentSnapshot.time + model.currentSnapshot.length { |
|
|
|
seekTimeSmoothly(model.currentSnapshot.time) |
|
|
|
seekTime(model.currentSnapshot.time) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
player.automaticallyWaitsToMinimizeStalling = false |
|
|
|
|
|
|
|
let item = model.currentPlayerItem() |
|
|
|
|
|
|
|
item.preferredForwardBufferDuration = 2.0 |
|
|
|
self.player.replaceCurrentItem(with: item) |
|
|
|
model.currentURL = model.currentSnapshot.playerURL |
|
|
|
|
|
|
|
// player.removeAllItems() |
|
|
|
// player.insert(model.currentPlayerItem(), after: nil) |
|
|
|
// Start the player going, otherwise controls don't appear |
|
|
|
gotoSnapshot(model.currentSnapshot) |
|
|
|
player.play() |
|
|
|
// player.play() |
|
|
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { |
|
|
|
gotoSnapshot(model.currentSnapshot) |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
.onDisappear { |
|
|
|
// When this View isn't being shown anymore stop the player |
|
|
|
@ -160,23 +196,21 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
} |
|
|
|
|
|
|
|
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) |
|
|
|
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) |
|
|
|
showThumbnail(currentItem: model.baseItem, thumbnail: thumbnail, time: time) |
|
|
|
|
|
|
|
} catch let error { |
|
|
|
print("*** Error generating thumbnail: \(error.localizedDescription)") |
|
|
|
} |
|
|
|
// } |
|
|
|
} catch let error { |
|
|
|
print("*** Error generating thumbnail: \(error.localizedDescription)") |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func showThumbnail(currentItem: MediaItem, thumbnail: UIImage, time: CMTime) { |
|
|
|
@ -187,6 +221,7 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
newItem.local = currentItem.local |
|
|
|
newItem.external = currentItem.external |
|
|
|
model.allItems.append(newItem) |
|
|
|
model.currentSnapshot = newItem |
|
|
|
} |
|
|
|
|
|
|
|
private func move(_ dragged: CGSize, start: CGPoint) -> Bool { |
|
|
|
@ -223,22 +258,34 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
return true |
|
|
|
} |
|
|
|
if dragged.width > dragWidth { |
|
|
|
seekTimeSmoothly(time + 5.0) |
|
|
|
seekTime(time + 8.0) |
|
|
|
} else if dragged.width < -dragWidth { |
|
|
|
seekTimeSmoothly(time - 5.0) |
|
|
|
seekTime(time - 10.0) |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
if dragged.width > dragWidth { |
|
|
|
seekTimeSmoothly(time + 30.0) |
|
|
|
seekTime(time + 30.0) |
|
|
|
} else if dragged.width < -dragWidth { |
|
|
|
seekTimeSmoothly(time - 30.0) |
|
|
|
seekTime(time - 30.0) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
model.seeking = sk |
|
|
|
} |
|
|
|
|
|
|
|
if dragged.height > 100 && timeCounter == 0 { |
|
|
|
if let i = model.allItems.index(where: { m in m === model.currentSnapshot }) { |
|
|
|
if i + 1 < model.allItems.count { |
|
|
|
gotoSnapshot(model.allItems[i+1]) |
|
|
|
} |
|
|
|
else { |
|
|
|
gotoSnapshot(model.allItems[0]) |
|
|
|
} |
|
|
|
timeCounter = 20 |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return false |
|
|
|
} |
|
|
|
|
|
|
|
@ -247,8 +294,21 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
} |
|
|
|
|
|
|
|
func seekTime(_ time: Double) { |
|
|
|
// print(time) |
|
|
|
player.seek(to: CMTime(seconds: time, preferredTimescale: CMTimeScale(10000))) |
|
|
|
print("Seek \(time)") |
|
|
|
player.pause() |
|
|
|
// player.cancelPendingPrerolls() |
|
|
|
|
|
|
|
if let item = player.currentItem { |
|
|
|
// item.cancelPendingSeeks() |
|
|
|
} |
|
|
|
|
|
|
|
player.seek(to: CMTime(seconds: time, preferredTimescale: CMTimeScale(10000)), |
|
|
|
toleranceBefore: CMTime.zero, |
|
|
|
toleranceAfter: CMTime(seconds: 1.0, preferredTimescale: CMTimeScale(10000))){ _ in |
|
|
|
player.play() |
|
|
|
player.rate = model.speed |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
func seekTimeSmoothly(_ time: Double) { |
|
|
|
@ -277,11 +337,22 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
} |
|
|
|
|
|
|
|
func gotoSnapshot(_ currentSnapshot: MediaItem) { |
|
|
|
seekTime(currentSnapshot.time) |
|
|
|
// player.forceSeekSmoothlyToTime(newChaseTime: currentSnapshot.time) |
|
|
|
// loopStart = currentSnapshot.time |
|
|
|
// player.loopEnd = loopStart + currentSnapshot.length |
|
|
|
// |
|
|
|
model.currentSnapshot = currentSnapshot |
|
|
|
|
|
|
|
if currentSnapshot.playerURL != model.currentURL { |
|
|
|
model.currentURL = currentSnapshot.playerURL |
|
|
|
player.insert(model.currentPlayerItem(), after: player.currentItem) |
|
|
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { |
|
|
|
player.advanceToNextItem() |
|
|
|
// seekTime(currentSnapshot.time) |
|
|
|
} |
|
|
|
|
|
|
|
// player.replaceCurrentItem(with: model.currentPlayerItem()) |
|
|
|
} |
|
|
|
else { |
|
|
|
seekTime(currentSnapshot.time) |
|
|
|
} |
|
|
|
// if loopMode && currentSnapshot.scale > 0 { |
|
|
|
// player.zoom = Float(currentSnapshot.scale) |
|
|
|
// player.xpos = currentSnapshot.offset.x |
|
|
|
@ -289,14 +360,11 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
// player.transformLayer() |
|
|
|
// } |
|
|
|
|
|
|
|
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() { |
|
|
|
@ -321,6 +389,67 @@ struct SVideoPlayer: View, EditItemDelegate { |
|
|
|
func seek(_ v: Double) { |
|
|
|
seekTimeSmoothly(v) |
|
|
|
} |
|
|
|
|
|
|
|
func save(currentSnapshot c: MediaItem, name: String) { |
|
|
|
do { |
|
|
|
try FileHelper.createDir(name: name) |
|
|
|
|
|
|
|
var file = FileHelper.getDocumentsDirectory().appendingPathComponent(name).appendingPathComponent("\(c.nameWithoutExtension)_\(Int(c.time))") |
|
|
|
|
|
|
|
if file.pathExtension != "mp4" { |
|
|
|
file = file.appendingPathExtension("mp4") |
|
|
|
} |
|
|
|
print (file) |
|
|
|
var dur = c.length |
|
|
|
if (dur < 0) { |
|
|
|
return |
|
|
|
} |
|
|
|
player.pause() |
|
|
|
VideoHelper.export(item: player.currentItem!, clipStart: c.time, clipDuration: dur, file: file, |
|
|
|
progress: { p in |
|
|
|
let percent = Int(p * 100) |
|
|
|
savetext = "\(percent)" |
|
|
|
}) |
|
|
|
{ url in |
|
|
|
savetext = "saved" |
|
|
|
} |
|
|
|
var s : MediaModel; |
|
|
|
if (c.type == ItemType.SNAPSHOT && c.parent != nil) { |
|
|
|
s = c.parent!.toMediaModel() |
|
|
|
do { |
|
|
|
if c.thumbUrlAbsolute != "" { |
|
|
|
let local = try Data(contentsOf: URL(string: c.thumbUrlAbsolute)!) |
|
|
|
let tfile = FileHelper.getDocumentsDirectory() |
|
|
|
.appendingPathComponent(name) |
|
|
|
.appendingPathComponent("\(c.nameWithoutExtension)_\(Int(c.time))") |
|
|
|
.appendingPathExtension("mp4") |
|
|
|
.appendingPathExtension("jpg") |
|
|
|
try local.write(to: tfile) |
|
|
|
} |
|
|
|
} |
|
|
|
catch { |
|
|
|
// ignore |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
s = c.toMediaModel() |
|
|
|
} |
|
|
|
|
|
|
|
let jfile = FileHelper.getDocumentsDirectory() |
|
|
|
.appendingPathComponent(name) |
|
|
|
.appendingPathComponent("\(c.nameWithoutExtension)_\(Int(c.time))") |
|
|
|
.appendingPathExtension("mp4") |
|
|
|
.appendingPathExtension("orig") |
|
|
|
let jsonEncoder = JSONEncoder() |
|
|
|
let jsonData = try! jsonEncoder.encode(s) |
|
|
|
let json = String(data: jsonData, encoding: String.Encoding.utf8) |
|
|
|
print(jfile.absoluteString) |
|
|
|
print(json) |
|
|
|
try json!.write(to: jfile, atomically: true, encoding: .utf8) |
|
|
|
} catch { |
|
|
|
print(error) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
struct SVideoPlayer_Previews: PreviewProvider { |
|
|
|
|