27 changed files with 3235 additions and 274 deletions
-
8download.js
-
48kplayer.xcodeproj/project.pbxproj
-
5kplayer/AppDelegate.swift
-
263kplayer/core/LocalManager.swift
-
27kplayer/core/MediaItem.swift
-
1kplayer/core/MediaModel.swift
-
238kplayer/core/NetworkManager.swift
-
12kplayer/detail/BrowserController.swift
-
25kplayer/detail/DetailViewController.swift
-
46kplayer/detail/EditItemView.swift
-
2kplayer/detail/ItemCell.swift
-
75kplayer/detail/VideoController.swift
-
4kplayer/master/KSettingsView.swift
-
62kplayer/master/MasterViewController.swift
-
46kplayer/master/NetworkDelegate.swift
-
30kplayer/master/ServerDownloadDelegate.swift
-
3kplayer/photo/PhotoController.swift
-
102kplayer/server/kplayer.js
-
15kplayer/server/links.html
-
126kplayer/timeline/CenterLine.swift
-
451kplayer/timeline/FrameImagesView.swift
-
206kplayer/timeline/TimelineMeasure.swift
-
178kplayer/timeline/TimelineScroller.swift
-
460kplayer/timeline/TimelineView.swift
-
773kplayer/timeline/TrimView.swift
-
301kplayer/timeline/VideoTimelineView.swift
-
2kplayer/video/BMPlayer.swift
@ -0,0 +1,263 @@ |
|||||
|
// |
||||
|
// Created by Marco Schmickler on 13.11.21. |
||||
|
// Copyright (c) 2021 Marco Schmickler. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
import Foundation |
||||
|
|
||||
|
class LocalManager { |
||||
|
static let sharedInstance = LocalManager() |
||||
|
|
||||
|
var settings = KSettings() |
||||
|
|
||||
|
var authenticated = false |
||||
|
|
||||
|
var favorites = MediaItem(name: "fav", path: "", root: "", type: ItemType.FAVROOT) |
||||
|
|
||||
|
var externalURL = "" |
||||
|
|
||||
|
func loadSettings() { |
||||
|
do { |
||||
|
let jsonData = try FileHelper.getData(name: "settings.json") |
||||
|
let model = try JSONDecoder().decode(KSettingsModel.self, from: jsonData) |
||||
|
settings = KSettings(model: model) |
||||
|
} catch { |
||||
|
print(error.localizedDescription) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func saveSettings() { |
||||
|
let json = settings.toJSON() |
||||
|
let url = FileHelper.getDocumentsDirectory().appendingPathComponent("settings.json") |
||||
|
do { |
||||
|
print(url) |
||||
|
try json.write(to: url, atomically: true, encoding: .utf8) |
||||
|
let input = try String(contentsOf: url) |
||||
|
print(input) |
||||
|
} catch { |
||||
|
print(json) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func deleteLocal(selectedItem: MediaItem) { |
||||
|
if (selectedItem.local) { |
||||
|
print(selectedItem.playerURL?.absoluteString) |
||||
|
do { |
||||
|
try FileManager.default.removeItem(at: selectedItem.playerURL!) |
||||
|
} catch { |
||||
|
print(error) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func saveFavDir(url: URL, item: MediaItem) -> Void { |
||||
|
for c in item.children { |
||||
|
let t = c.time |
||||
|
let ms = Int(t * 1000) |
||||
|
let p = url.appendingPathExtension("\(ms).jpg") |
||||
|
let pt = url.appendingPathExtension("\(ms)_thumb.jpg") |
||||
|
print(p) |
||||
|
|
||||
|
if FileManager.default.fileExists(atPath: pt.absoluteString) { |
||||
|
print("contained") |
||||
|
} else { |
||||
|
if let id = c.image, let imageData = id.jpegData(compressionQuality: 1.0) { |
||||
|
do { |
||||
|
try imageData.write(to: pt) |
||||
|
} catch { |
||||
|
print("error") |
||||
|
} |
||||
|
|
||||
|
let thumb = id.scaleToSize(15 * 16, height: 15 * 9) |
||||
|
if let imageDataT = thumb.jpegData(compressionQuality: 1.0) { |
||||
|
do { |
||||
|
try imageDataT.write(to: pt) |
||||
|
} catch { |
||||
|
} |
||||
|
c.image = thumb |
||||
|
c.thumbUrl = pt.absoluteString |
||||
|
} |
||||
|
|
||||
|
c.loaded = true |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let json = item.toJSON() |
||||
|
|
||||
|
do { |
||||
|
print(url) |
||||
|
try json.write(to: url.appendingPathExtension("json"), atomically: true, encoding: .utf8) |
||||
|
let input = try String(contentsOf: url) |
||||
|
print(input) |
||||
|
} catch { |
||||
|
print(json) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func loadFavDirs(_ url: URL, completionHandler: @escaping Weiter) -> Void { |
||||
|
var res = [MediaItem]() |
||||
|
|
||||
|
var loadedJson = [String]() |
||||
|
|
||||
|
var itemsMap = Dictionary<String, MediaItem>() |
||||
|
|
||||
|
let fi = MediaItem(name: "images", path: "", root: "fav", type: ItemType.DETAILS) |
||||
|
fi.local = true |
||||
|
fi.loaded = true |
||||
|
|
||||
|
res.append(fi) |
||||
|
|
||||
|
if let enumerator = FileManager.default.enumerator(at: url, includingPropertiesForKeys: [.isRegularFileKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) { |
||||
|
for case let fileURL as URL in enumerator { |
||||
|
do { |
||||
|
let fileAttributes = try fileURL.resourceValues(forKeys: [.isRegularFileKey]) |
||||
|
if fileAttributes.isRegularFile! { |
||||
|
if (fileURL.pathExtension == "jpg" && fileURL.absoluteString.contains("/images/")) { |
||||
|
let p = fileURL.absoluteString.substringAfter("/Documents") |
||||
|
|
||||
|
var path = p.substringBefore("/") |
||||
|
|
||||
|
let fp = fileURL.absoluteString.substringAfter("/Documents/images/") |
||||
|
|
||||
|
var fpath = fp.substringBefore("/") |
||||
|
|
||||
|
var folder = itemsMap[fpath] |
||||
|
|
||||
|
if folder == nil { |
||||
|
let f = MediaItem(name: fpath, path: "images", root: "fav", type: ItemType.PICS) |
||||
|
f.local = true |
||||
|
f.loaded = true |
||||
|
folder = f |
||||
|
itemsMap[fpath] = folder! |
||||
|
fi.children.append(f) |
||||
|
} |
||||
|
|
||||
|
let m = MediaItem(name: fileURL.lastPathComponent, path: path, root: "fav", type: ItemType.PICS) |
||||
|
m.local = true |
||||
|
m.loaded = true |
||||
|
m.thumbUrl = fileURL.absoluteString |
||||
|
|
||||
|
let jsonURL = fileURL.absoluteString.replacingOccurrences(of: ".jpg", with: ".json") |
||||
|
|
||||
|
do { |
||||
|
let jsonData = try Data(contentsOf: URL(string: jsonURL)!) |
||||
|
let item = try JSONDecoder().decode(MediaModel.self, from: jsonData) |
||||
|
m.scale = item.scale |
||||
|
m.offset = item.offset |
||||
|
m.size = item.size |
||||
|
} catch { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
// var folder = itemsMap[path] |
||||
|
// |
||||
|
// if folder == nil { |
||||
|
// folder = MediaItem(name: item.path, path: item.path, root: item.root, type: ItemType.FOLDER) |
||||
|
// folder!.loaded = true |
||||
|
// folder!.local = item.local |
||||
|
// itemsMap[path] = folder! |
||||
|
// items.append(folder!) |
||||
|
// } |
||||
|
|
||||
|
folder!.children.append(m) |
||||
|
|
||||
|
// print(fileURL.absoluteString) |
||||
|
} |
||||
|
|
||||
|
let p = fileURL.absoluteString.substringAfter("/Documents") |
||||
|
|
||||
|
if (fileURL.pathExtension == "json") { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
if (fileURL.pathExtension == "mp4") { |
||||
|
print(fileURL.absoluteString) |
||||
|
|
||||
|
let jsonURL = fileURL.appendingPathExtension("json") |
||||
|
|
||||
|
do { |
||||
|
let jsonData = try Data(contentsOf: jsonURL) |
||||
|
let item = try JSONDecoder().decode(MediaModel.self, from: jsonData) |
||||
|
let mediaItem = MediaItem(model: item) |
||||
|
mediaItem.externalURL = fileURL.absoluteString |
||||
|
mediaItem.local = true |
||||
|
for i in mediaItem.children { |
||||
|
i.externalURL = fileURL.absoluteString |
||||
|
i.local = true |
||||
|
} |
||||
|
|
||||
|
res.append(mediaItem) |
||||
|
} catch { |
||||
|
var path = p.substringBefore("/") |
||||
|
|
||||
|
let m = MediaItem(name: fileURL.lastPathComponent, path: path, root: "fav", type: ItemType.VIDEO) |
||||
|
m.local = true |
||||
|
m.loaded = true |
||||
|
m.thumbUrl = fileURL.appendingPathExtension("jpg").absoluteString |
||||
|
m.externalURL = fileURL.absoluteString |
||||
|
|
||||
|
res.append(m) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} catch { |
||||
|
print(error, fileURL) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let m = MediaItem(name: "new", path: "new", root: "fav", type: ItemType.FOLDER) |
||||
|
m.local = true |
||||
|
res.append(m) |
||||
|
|
||||
|
completionHandler(res) |
||||
|
} |
||||
|
|
||||
|
// old |
||||
|
func loadFav2Dirs(_ rootParam: String, completionHandler: @escaping Weiter) -> Void { |
||||
|
let files = FileHelper.listFiles(name: "1") |
||||
|
var res = [MediaItem]() |
||||
|
for f in files { |
||||
|
if f.pathExtension == "mp4" { |
||||
|
let m = MediaItem(name: f.lastPathComponent, path: "", root: "1", type: ItemType.VIDEO) |
||||
|
|
||||
|
m.local = true |
||||
|
res.append(m) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
completionHandler(res) |
||||
|
} |
||||
|
|
||||
|
//old |
||||
|
func loadFav1Dirs(_ rootParam: String, completionHandler: @escaping Weiter) -> Void { |
||||
|
var res = [MediaItem]() |
||||
|
|
||||
|
do { |
||||
|
let jsonData = try FileHelper.getData(name: "fav.json") |
||||
|
let items = try JSONDecoder().decode(MediaModel.self, from: jsonData) |
||||
|
let loaded = MediaItem(model: items) |
||||
|
res = loaded.children |
||||
|
} catch { |
||||
|
print(error.localizedDescription) |
||||
|
} |
||||
|
|
||||
|
// res.append(currentFav) |
||||
|
completionHandler(res) |
||||
|
} |
||||
|
|
||||
|
func fixDocumentURL(_ path: String?) -> String? { |
||||
|
if path == nil { |
||||
|
return path |
||||
|
} |
||||
|
|
||||
|
if path!.contains("file://") { |
||||
|
let p = path!.substringAfter("/Documents/") |
||||
|
let fixed = FileHelper.getDocumentsDirectory().appendingPathComponent(p) |
||||
|
return fixed.absoluteString |
||||
|
} |
||||
|
return path |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
// |
||||
|
// Created by Marco Schmickler on 12.11.21. |
||||
|
// Copyright (c) 2021 Marco Schmickler. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
import Foundation |
||||
|
import SwiftUI |
||||
|
|
||||
|
struct EditItemView : View { |
||||
|
@ObservedObject |
||||
|
var item : MediaItem |
||||
|
|
||||
|
var body: some View { |
||||
|
Form { |
||||
|
Section(header: Text("K Settings")) { |
||||
|
VStack { |
||||
|
Text("Size") |
||||
|
Slider(value: $item.time, in: 0...1000) |
||||
|
Slider(value: $item.length, in: 0...1000) |
||||
|
Toggle(isOn: $item.loop, label: { |
||||
|
Text("Loop") |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
Button(action: { |
||||
|
|
||||
|
}, label: { |
||||
|
Text("ok") |
||||
|
}); |
||||
|
Button(action: { |
||||
|
|
||||
|
}, label: { |
||||
|
Text("cancel") |
||||
|
}) |
||||
|
} |
||||
|
.onAppear { |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
struct EditItemView_Previews: PreviewProvider { |
||||
|
static var previews: some View { |
||||
|
EditItemView(item: MediaItem(name: "extern", path:"", root: "", type: ItemType.FAVROOT)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,30 @@ |
|||||
|
// |
||||
|
// Created by Marco Schmickler on 13.11.21. |
||||
|
// Copyright (c) 2021 Marco Schmickler. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
import Foundation |
||||
|
|
||||
|
class ServerDownloadDelegate : DownloadDelegate { |
||||
|
func killFFMPEG() { |
||||
|
NetworkManager.sharedInstance.killFFMPEG() |
||||
|
print("killffmpeg") |
||||
|
} |
||||
|
|
||||
|
func dlserverlen(result: @escaping (String) -> ()) { |
||||
|
NetworkManager.sharedInstance.dlserverlen(result: result) |
||||
|
} |
||||
|
|
||||
|
func download(url: URL, path: String, result: @escaping (URL) -> ()) { |
||||
|
NetworkManager.sharedInstance.download(url: url, path: path, result: result) |
||||
|
} |
||||
|
|
||||
|
func downloadToServer(path: String, url: URL, result: @escaping (String) -> ()) { |
||||
|
NetworkManager.sharedInstance.downloadToServer(path: path, url: url, result: result) |
||||
|
} |
||||
|
|
||||
|
func inProgress() -> Int { |
||||
|
NetworkManager.sharedInstance.currentDownloads.count |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
<html> |
||||
|
<body> |
||||
|
<a href="https://www.google.de">Google</a> |
||||
|
<a href="https://www.xvideos.com">XVID</a> |
||||
|
<a href="https://www.xhamster.com">XVID</a> |
||||
|
<a href="https://www.chaturbate.com">gustav29 veberle12a</a> |
||||
|
<a href="https://www.stripchat.com">gustav29 ms39084st</a> |
||||
|
<a href="https://www.kink.com">XVID</a> |
||||
|
<a href="https://www.hegre.com">XVID</a> |
||||
|
<a href="https://www.facialabuse.com">XVID</a> |
||||
|
<a href="https://www.ghettogaggers.com">XVID</a> |
||||
|
|
||||
|
</body> |
||||
|
</html> |
||||
|
|
||||
@ -0,0 +1,126 @@ |
|||||
|
// |
||||
|
// CenterLine.swift |
||||
|
// Examplay |
||||
|
// |
||||
|
// Created by Tomohiro Yamashita on 2020/03/09. |
||||
|
// Copyright © 2020 Tom. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
import UIKit |
||||
|
import AVFoundation |
||||
|
|
||||
|
class CenterLine: UIView { |
||||
|
|
||||
|
var mainView:VideoTimelineView! |
||||
|
|
||||
|
let timeLabel = UILabel() |
||||
|
var parentView:TimelineView? = nil |
||||
|
var duration:Float64 = 0 |
||||
|
var currentTime:Float64 = 0 |
||||
|
let margin:CGFloat = 6 |
||||
|
|
||||
|
override init (frame: CGRect) { |
||||
|
super.init(frame: frame) |
||||
|
|
||||
|
self.backgroundColor = .clear |
||||
|
self.isUserInteractionEnabled = false |
||||
|
} |
||||
|
|
||||
|
required init(coder aDecoder: NSCoder) { |
||||
|
fatalError("CenterLine init(coder:) has not been implemented") |
||||
|
} |
||||
|
|
||||
|
func configure(parent:TimelineView) { |
||||
|
parentView = parent |
||||
|
self.setNeedsDisplay() |
||||
|
|
||||
|
self.timeLabel.adjustsFontSizeToFitWidth = true |
||||
|
self.timeLabel.textAlignment = .center |
||||
|
|
||||
|
self.timeLabel.text = String("00:00.00") |
||||
|
|
||||
|
|
||||
|
self.addSubview(timeLabel) |
||||
|
} |
||||
|
|
||||
|
func update() { |
||||
|
self.setNeedsDisplay() |
||||
|
let textMargin = margin + 3 |
||||
|
self.timeLabel.frame.size.width = self.bounds.size.width - textMargin * 2 |
||||
|
self.timeLabel.frame.origin = CGPoint(x:textMargin,y:0) |
||||
|
self.timeLabel.textColor = UIColor(hue: 0.94, saturation:0.68, brightness:0.95, alpha: 0.94) |
||||
|
setTimeText() |
||||
|
timeLabel.font = UIFont(name:"HelveticaNeue-CondensedBold" ,size:timeLabel.frame.size.height * 0.9) |
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
override func draw(_ rect: CGRect) { |
||||
|
|
||||
|
|
||||
|
gradient() |
||||
|
|
||||
|
let context = UIGraphicsGetCurrentContext()! |
||||
|
context.saveGState() |
||||
|
context.setShadow(offset: CGSize(width: 0,height: 0), blur: 6, color: UIColor(hue: 0, saturation:0, brightness:0.0, alpha: 0.30).cgColor) |
||||
|
|
||||
|
UIColor(hue: 0, saturation:0, brightness:1, alpha: 0.92).setFill() |
||||
|
|
||||
|
let labelRect = CGRect(x: margin,y: 0.3,width: self.frame.size.width - margin * 2,height: timeLabel.frame.size.height + 1) |
||||
|
let rectPath = UIBezierPath(roundedRect:labelRect, cornerRadius:timeLabel.frame.size.height) |
||||
|
rectPath.fill() |
||||
|
context.restoreGState() |
||||
|
|
||||
|
|
||||
|
|
||||
|
let path = UIBezierPath() |
||||
|
path.move(to: CGPoint(x: self.frame.size.width / 2 , y:timeLabel.frame.size.height)) |
||||
|
path.addLine(to: CGPoint(x:self.frame.size.width / 2 , y:self.frame.size.height)) |
||||
|
path.lineWidth = 1.4 |
||||
|
UIColor(hue: 0, saturation:0, brightness:1.0, alpha: 0.7).setStroke() |
||||
|
path.stroke() |
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
func gradient() { |
||||
|
let width = self.frame.size.width |
||||
|
|
||||
|
|
||||
|
let context = UIGraphicsGetCurrentContext()! |
||||
|
|
||||
|
let startColor = UIColor(hue: 0, saturation:0, brightness:0.0, alpha: 0.06).cgColor |
||||
|
let endColor = UIColor.clear.cgColor |
||||
|
let colors = [startColor, endColor] as CFArray |
||||
|
|
||||
|
let locations = [0, 1] as [CGFloat] |
||||
|
|
||||
|
let space = CGColorSpaceCreateDeviceRGB() |
||||
|
|
||||
|
let gradient = CGGradient(colorsSpace: space, colors: colors, locations: locations)! |
||||
|
context.drawLinearGradient(gradient, start: CGPoint(x:(width / 2) - 0.7, y:0), end: CGPoint(x: (width / 2) - 4, y: 0), options: []) |
||||
|
context.drawLinearGradient(gradient, start: CGPoint(x:(width / 2) + 0.7, y:0), end: CGPoint(x: (width / 2) + 4, y: 0), options: []) |
||||
|
} |
||||
|
|
||||
|
var ignoreSendScrollToParent = false |
||||
|
func setScrollPoint(_ scrollPoint:CGFloat) { |
||||
|
currentTime = Float64(scrollPoint) * duration |
||||
|
setTimeText() |
||||
|
if !ignoreSendScrollToParent { |
||||
|
if let parent = parentView { |
||||
|
parent.moved(currentTime) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
mainView!.currentTime = currentTime |
||||
|
ignoreSendScrollToParent = false |
||||
|
} |
||||
|
|
||||
|
func setTimeText() { |
||||
|
let minute = Int(currentTime / 60) |
||||
|
let second = (currentTime - Float64(minute) * 60) |
||||
|
let milliSec = Int((second - Float64(Int(second))) * 100) |
||||
|
let text = String(format: "%02d:%02d.%02d", minute, (Int(second)), milliSec) |
||||
|
timeLabel.text = text |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,451 @@ |
|||||
|
// |
||||
|
// FrameImagesView.swift |
||||
|
// Examplay |
||||
|
// |
||||
|
// Created by Tomohiro Yamashita on 2020/03/01. |
||||
|
// Copyright © 2020 Tom. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
import Foundation |
||||
|
import UIKit |
||||
|
import AVFoundation |
||||
|
|
||||
|
|
||||
|
|
||||
|
class FrameImage: UIImageView { |
||||
|
var tolerance:Float64? = nil |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// MARK: - FrameImagesView |
||||
|
class FrameImagesView: UIScrollView { |
||||
|
|
||||
|
|
||||
|
var mainView:VideoTimelineView! |
||||
|
|
||||
|
var frameImagesArray:[FrameImage] = [] |
||||
|
|
||||
|
|
||||
|
var thumbnailFrameSize:CGSize = CGSize(width: 640,height: 480) |
||||
|
let preferredTimescale:Int32 = 100 |
||||
|
var timeTolerance = CMTimeMakeWithSeconds(10 , preferredTimescale:100) |
||||
|
|
||||
|
var maxWidth:CGFloat = 0 |
||||
|
var minWidth:CGFloat = 0 |
||||
|
|
||||
|
var parentScroller:TimelineScroller? = nil |
||||
|
|
||||
|
override init (frame: CGRect) { |
||||
|
super.init(frame: frame) |
||||
|
self.isScrollEnabled = false |
||||
|
self.isUserInteractionEnabled = false |
||||
|
self.backgroundColor = UIColor(hue: 0, saturation:0, brightness:0.0, alpha: 0.02) |
||||
|
} |
||||
|
|
||||
|
required init(coder aDecoder: NSCoder) { |
||||
|
fatalError("FrameImagesView init(coder:) has not been implemented") |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
func reset() { |
||||
|
discardAllFrameImages() |
||||
|
cancelImageGenerator() |
||||
|
|
||||
|
prepareFrameViews() |
||||
|
layout() |
||||
|
requestVisible(depth:0, wide:2, direction:0) |
||||
|
} |
||||
|
|
||||
|
//MARK: - timer for animation |
||||
|
var animationTimer = Timer() |
||||
|
var animating = false |
||||
|
func startAnimation() { |
||||
|
animating = true |
||||
|
displayFrames() |
||||
|
animationTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.animate(_:)), userInfo: nil, repeats: true) |
||||
|
RunLoop.main.add(animationTimer, forMode:RunLoop.Mode.common) |
||||
|
|
||||
|
} |
||||
|
|
||||
|
func stopAnimation() { |
||||
|
animationTimer.invalidate() |
||||
|
animating = false |
||||
|
|
||||
|
|
||||
|
if let parent = parentScroller { |
||||
|
frame.origin = CGPoint(x: parent.frame.size.width / 2, y: parent.measureHeight) |
||||
|
parent.addSubview(self) |
||||
|
} |
||||
|
displayFrames() |
||||
|
} |
||||
|
|
||||
|
@objc func animate(_ timer:Timer) { |
||||
|
if animating == false { |
||||
|
return |
||||
|
} |
||||
|
displayFrames() |
||||
|
} |
||||
|
|
||||
|
//MARK: - layout |
||||
|
|
||||
|
|
||||
|
var uponFrames = Set<Int>() |
||||
|
var belowFrames = Set<Int>() |
||||
|
var deepFrames = Set<Int>() |
||||
|
var hiddenFrames = Set<Int>() |
||||
|
|
||||
|
|
||||
|
func layout() { |
||||
|
setThumnailFrameSize() |
||||
|
let coordinated = coordinateFrames() |
||||
|
uponFrames = coordinated.upon |
||||
|
belowFrames = coordinated.below |
||||
|
hiddenFrames = coordinated.hidden |
||||
|
deepFrames = coordinated.deep |
||||
|
displayFrames() |
||||
|
} |
||||
|
|
||||
|
func displayFrames() { |
||||
|
|
||||
|
var baseView:UIView = self |
||||
|
var offset:CGPoint = CGPoint.zero |
||||
|
|
||||
|
if let parent = parentScroller { |
||||
|
let visibleHalf = parent.frame.size.width / 2 |
||||
|
|
||||
|
if animating { |
||||
|
if let layer = parent.layer.presentation() { |
||||
|
offset.x = visibleHalf - layer.bounds.origin.x |
||||
|
offset.y = parent.frame.origin.y + parent.measureHeight |
||||
|
frame.origin = offset |
||||
|
mainView!.timelineView.viewForAnimate.addSubview(self) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let visibleSide = indexOfVisibleSide() |
||||
|
func locate(_ index:Int, visible:Bool) { |
||||
|
if frameImagesArray.count > index && index >= 0 { |
||||
|
let frameImg = frameImagesArray[index] |
||||
|
if index >= visibleSide.left && index <= visibleSide.right { |
||||
|
let position = positionWithIndex(index) |
||||
|
frameImg.frame = CGRect(x: position, y:0, width: thumbnailFrameSize.width, height: thumbnailFrameSize.height) |
||||
|
if visible { |
||||
|
self.addSubview(frameImg) |
||||
|
} else { |
||||
|
frameImg.removeFromSuperview() |
||||
|
} |
||||
|
} else { |
||||
|
frameImg.removeFromSuperview() |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
for element in belowFrames {//do addSubview under the upon |
||||
|
locate(element, visible:true) |
||||
|
} |
||||
|
for element in uponFrames { |
||||
|
locate(element, visible:true) |
||||
|
} |
||||
|
for element in hiddenFrames {// includes deep |
||||
|
locate(element, visible:false) |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
func positionWithIndex(_ index:Int) -> CGFloat { |
||||
|
let position = frame.size.width * (CGFloat(index) / CGFloat(thumbnailCountFloat())) |
||||
|
return position |
||||
|
} |
||||
|
|
||||
|
func coordinateFrames() -> (upon:Set<Int>, below:Set<Int>, hidden:Set<Int>, deep:Set<Int>) { |
||||
|
|
||||
|
let keyDivision = Int(pow(2,Double(Int(log2(maxWidth * 2 / (self.frame.size.width + 1)))))) |
||||
|
if keyDivision <= 0 { |
||||
|
return (Set<Int>(), Set<Int>(), Set<Int>(), Set<Int>()) |
||||
|
} |
||||
|
let keyCount = ((frameImagesArray.count - 1) / keyDivision) + 1 |
||||
|
|
||||
|
|
||||
|
var visibleIndexes = Set<Int>() |
||||
|
var uponElements = Set<Int>() |
||||
|
for index in 0 ... (keyCount - 1) { |
||||
|
let uponIndex = index * keyDivision |
||||
|
uponElements.insert(uponIndex) |
||||
|
visibleIndexes.insert(uponIndex) |
||||
|
} |
||||
|
var belowElements = Set<Int>() |
||||
|
let belowDivision = keyDivision / 2 |
||||
|
if belowDivision >= 1 { |
||||
|
for index in 0 ..< (keyCount - 1) { |
||||
|
let belowIndex = (index * keyDivision) + (keyDivision / 2) |
||||
|
belowElements.insert(belowIndex) |
||||
|
visibleIndexes.insert(belowIndex) |
||||
|
} |
||||
|
} |
||||
|
var deepElements = Set<Int>() |
||||
|
let deepDivision = belowDivision / 2 |
||||
|
if deepDivision >= 1 { |
||||
|
if let max = visibleIndexes.max() { |
||||
|
for index in visibleIndexes { |
||||
|
if max > index { |
||||
|
deepElements.insert(index + deepDivision) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
var hiddenElements = Set<Int>() |
||||
|
for index in 0 ..< frameImagesArray.count { |
||||
|
if !visibleIndexes.contains(index) { |
||||
|
let hiddenIndex = index |
||||
|
hiddenElements.insert(hiddenIndex) |
||||
|
} |
||||
|
} |
||||
|
return (uponElements,belowElements,hiddenElements, deepElements) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func setThumnailFrameSize() { |
||||
|
if let asset = mainView!.asset { |
||||
|
var frameSize = CGSize(width: 640,height: 480) |
||||
|
let tracks:Array = asset.tracks(withMediaType:AVMediaType.video) |
||||
|
if tracks.count > 0 { |
||||
|
let track = tracks[0] |
||||
|
frameSize = track.naturalSize.applying(track.preferredTransform) |
||||
|
frameSize.width = abs(frameSize.width) |
||||
|
frameSize.height = abs(frameSize.height) |
||||
|
} |
||||
|
thumbnailFrameSize = mainView!.resizeHeightKeepRatio(frameSize, height:self.frame.size.height) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func assetDuration() -> Float64? { |
||||
|
if let asset = mainView!.asset { |
||||
|
return CMTimeGetSeconds(asset.duration) |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func prepareFrameViews() { |
||||
|
frameImagesArray = [] |
||||
|
for _ in 0 ... Int(thumbnailCountFloat()) { |
||||
|
let view = FrameImage() |
||||
|
view.alpha = 0 |
||||
|
frameImagesArray += [view] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
func thumbnailCountFloat() -> CGFloat { |
||||
|
return maxWidth / thumbnailFrameSize.width |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func indexWithTime(_ time:Float64) -> Int? { |
||||
|
if let assetDuration = assetDuration() { |
||||
|
let value = time / (assetDuration / Float64(thumbnailCountFloat())) |
||||
|
var intValue = Int(value) |
||||
|
if value - Float64(intValue) >= 0.5 { |
||||
|
intValue += 1 |
||||
|
} |
||||
|
return intValue |
||||
|
} |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func timeWithIndex(_ index:Int) -> Float64 { |
||||
|
if let assetDuration = assetDuration() { |
||||
|
return assetDuration * ((Float64(index)) / Float64(thumbnailCountFloat())) |
||||
|
} |
||||
|
return 0 |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
//MARK: - frame images |
||||
|
func cancelImageGenerator() { |
||||
|
if let asset = mainView!.asset { |
||||
|
let assetImgGenerate : AVAssetImageGenerator = AVAssetImageGenerator(asset: asset) |
||||
|
assetImgGenerate.cancelAllCGImageGeneration() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func discardAllFrameImages() { |
||||
|
|
||||
|
for index in 0 ..< frameImagesArray.count { |
||||
|
let view = frameImagesArray[index] |
||||
|
UIView.animate(withDuration: 0.5,delay:Double(0.0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in |
||||
|
|
||||
|
view.alpha = 0 |
||||
|
|
||||
|
},completion: { finished in |
||||
|
|
||||
|
view.image = nil |
||||
|
view.removeFromSuperview() |
||||
|
view.tolerance = nil |
||||
|
}) |
||||
|
} |
||||
|
frameImagesArray = [] |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
func requestAll(){ |
||||
|
|
||||
|
var timesArray = [NSValue]() |
||||
|
for index in 0 ..< frameImagesArray.count { |
||||
|
timesArray += [NSValue(time:CMTimeMakeWithSeconds(timeWithIndex(index) , preferredTimescale:preferredTimescale))] |
||||
|
} |
||||
|
requestImageGeneration(timesArray:timesArray) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
var requesting = Set<Int>() |
||||
|
func requestVisible(depth:Int, wide:Float, direction:Float) { |
||||
|
var timesArray = [NSValue]() |
||||
|
func request(_ index:Int) { |
||||
|
|
||||
|
if self.requesting.count > 0 && self.requesting.contains(index) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if frameImagesArray.count > index { |
||||
|
let imageView = frameImagesArray[index] |
||||
|
var needsUpdate = false |
||||
|
if let tolerance = imageView.tolerance { |
||||
|
if tolerance > CMTimeGetSeconds(timeTolerance) * 1.2 { |
||||
|
needsUpdate = true |
||||
|
} |
||||
|
} |
||||
|
if imageView.image == nil || needsUpdate { |
||||
|
timesArray += [NSValue(time:CMTimeMakeWithSeconds(timeWithIndex(index) , preferredTimescale:preferredTimescale))] |
||||
|
self.requesting.insert(index) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
let visibleSide = indexOfVisibleSide() |
||||
|
let width = visibleSide.right - visibleSide.left |
||||
|
var additionLeft = Int(Float(width) * wide) |
||||
|
var additionRight = additionLeft |
||||
|
if direction > 0 { |
||||
|
additionLeft = Int(Float(-width) * direction) |
||||
|
} else if direction < 0 { |
||||
|
additionRight = Int(Float(width) * direction) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
for index in uponFrames { |
||||
|
if index >= visibleSide.left - additionLeft && index <= visibleSide.right + additionRight { |
||||
|
request(index) |
||||
|
} |
||||
|
} |
||||
|
let belowAddLeft = Int(Float(additionLeft) * 0.5) - Int(Float(width) * 0.7) |
||||
|
let belowAddRight = Int(Float(additionRight) * 0.5) - Int(Float(width) * 0.7) |
||||
|
if depth > 0 { |
||||
|
for index in belowFrames { |
||||
|
if index >= visibleSide.left - belowAddLeft && index <= visibleSide.right + belowAddRight { |
||||
|
request(index) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
let deepAddLeft = Int(Float(additionLeft) * 0.2) - Int(Float(width) * 0.4) |
||||
|
let deepAddRight = Int(Float(additionRight) * 0.2) - Int(Float(width) * 0.4) |
||||
|
if depth > 1 { |
||||
|
for index in deepFrames { |
||||
|
if index >= visibleSide.left - deepAddLeft && index <= visibleSide.right + deepAddRight { |
||||
|
request(index) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
if timesArray.count > 0 { |
||||
|
requestImageGeneration(timesArray:timesArray) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func indexOfVisibleSide() -> (left:Int, right:Int) { |
||||
|
func indexWithPosition(_ position:CGFloat) -> Int{ |
||||
|
let index = position * CGFloat(thumbnailCountFloat()) / frame.size.width |
||||
|
var indexInt = Int(index) |
||||
|
if index - CGFloat(indexInt) > 0.5 { |
||||
|
indexInt += 1 |
||||
|
} |
||||
|
return indexInt |
||||
|
} |
||||
|
var visibleLeft = indexWithPosition(-(parentScroller!.frame.width / 2) + parentScroller!.contentOffset.x - thumbnailFrameSize.width) - 1 |
||||
|
var visibleRight = indexWithPosition(parentScroller!.contentOffset.x + (parentScroller!.frame.size.width * 0.5) + thumbnailFrameSize.width) |
||||
|
if visibleLeft < 0 { |
||||
|
visibleLeft = 0 |
||||
|
} |
||||
|
let max = frameImagesArray.count - 1 |
||||
|
if visibleRight > max { |
||||
|
visibleRight = max |
||||
|
} |
||||
|
return (visibleLeft, visibleRight) |
||||
|
} |
||||
|
|
||||
|
func updateTolerance() { |
||||
|
if mainView!.asset == nil { |
||||
|
return |
||||
|
} |
||||
|
let thumbDuration = Float64(thumbnailFrameSize.width / self.frame.size.width) * mainView!.duration * 2 |
||||
|
timeTolerance = CMTimeMakeWithSeconds(thumbDuration , preferredTimescale:100) |
||||
|
|
||||
|
|
||||
|
} |
||||
|
|
||||
|
func requestImageGeneration(timesArray:[NSValue]) { |
||||
|
|
||||
|
if let asset = mainView!.asset { |
||||
|
let assetImgGenerate : AVAssetImageGenerator = AVAssetImageGenerator(asset: asset) |
||||
|
assetImgGenerate.appliesPreferredTrackTransform = true |
||||
|
let maxsize = CGSize(width: thumbnailFrameSize.width * 1.5,height: thumbnailFrameSize.height * 1.5) |
||||
|
assetImgGenerate.maximumSize = maxsize |
||||
|
assetImgGenerate.requestedTimeToleranceAfter = timeTolerance |
||||
|
assetImgGenerate.requestedTimeToleranceBefore = timeTolerance |
||||
|
|
||||
|
assetImgGenerate.generateCGImagesAsynchronously(forTimes: timesArray, |
||||
|
completionHandler: |
||||
|
{ time,resultImage,actualTime,result,error in |
||||
|
|
||||
|
let timeValue = CMTimeGetSeconds(time) |
||||
|
if let image = resultImage { |
||||
|
DispatchQueue.main.async { |
||||
|
self.setFrameImage(image:UIImage(cgImage:image), time:timeValue) |
||||
|
} |
||||
|
} |
||||
|
if let index = self.indexWithTime(timeValue) { |
||||
|
self.requesting.remove(index) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func setFrameImage(image:UIImage, time:Float64) { |
||||
|
|
||||
|
if let index = indexWithTime(time) { |
||||
|
if frameImagesArray.count > index && index >= 0 { |
||||
|
let imageView = frameImagesArray[index] |
||||
|
|
||||
|
imageView.image = image |
||||
|
imageView.tolerance = CMTimeGetSeconds(timeTolerance) |
||||
|
UIView.animate(withDuration: 0.2,delay:Double(0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in |
||||
|
|
||||
|
imageView.alpha = 1 |
||||
|
|
||||
|
},completion: { finished in |
||||
|
|
||||
|
|
||||
|
}) |
||||
|
imageView.alpha = 1 |
||||
|
imageView.backgroundColor = .red |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
@ -0,0 +1,206 @@ |
|||||
|
// |
||||
|
// TimelineMeasure.swift |
||||
|
// Examplay |
||||
|
// |
||||
|
// Created by Tomohiro Yamashita on 2020/03/08. |
||||
|
// Copyright © 2020 Tom. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
import UIKit |
||||
|
|
||||
|
class TimelineMeasure: UIView { |
||||
|
|
||||
|
var unitSize:CGFloat = 100 |
||||
|
var frameImagesView:FrameImagesView? = nil |
||||
|
var parentScroller:TimelineScroller? = nil |
||||
|
var parentView:TimelineView? = nil |
||||
|
var stringColor:UIColor = UIColor(hue: 0.0, saturation:0.0, brightness:0.35, alpha: 1) |
||||
|
var animating = false |
||||
|
|
||||
|
override init (frame: CGRect) { |
||||
|
super.init(frame: frame) |
||||
|
|
||||
|
self.isMultipleTouchEnabled = true |
||||
|
self.isUserInteractionEnabled = true |
||||
|
} |
||||
|
|
||||
|
required init(coder aDecoder: NSCoder) { |
||||
|
fatalError("TimelineMeasure init(coder:) has not been implemented") |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
override func draw(_ rect: CGRect) { |
||||
|
|
||||
|
if frameImagesView == nil { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let max = frameImagesView!.maxWidth |
||||
|
var width = frameImagesView!.frame.size.width |
||||
|
if animating { |
||||
|
if let layer = frameImagesView!.layer.presentation() { |
||||
|
width = layer.frame.size.width |
||||
|
} |
||||
|
} |
||||
|
if width == 0 { |
||||
|
return |
||||
|
} |
||||
|
var unit = (width / max) * unitSize |
||||
|
|
||||
|
let unitWidth = CGFloat(80) |
||||
|
var division = 0 |
||||
|
while (unit <= unitWidth) { |
||||
|
unit *= 2 |
||||
|
division += 1 |
||||
|
if division > 1000 { |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
let unitLength = pow(2,Double(division + 1)) / 2 |
||||
|
func index(_ position:CGFloat) -> Int { |
||||
|
return Int(position / unit) |
||||
|
} |
||||
|
|
||||
|
let paragraphStyle = NSMutableParagraphStyle() |
||||
|
paragraphStyle.alignment = .center |
||||
|
|
||||
|
let attributes = [NSAttributedString.Key.font: UIFont(name: "HelveticaNeue", size: self.frame.size.height * 0.7)!, NSAttributedString.Key.paragraphStyle: paragraphStyle, NSAttributedString.Key.foregroundColor: stringColor] |
||||
|
|
||||
|
var string = "" |
||||
|
|
||||
|
var visibleRect = rect |
||||
|
if let parent = parentScroller { |
||||
|
|
||||
|
if animating { |
||||
|
if let layer = parent.layer.presentation() { |
||||
|
let offset = layer.bounds.origin |
||||
|
visibleRect.origin = offset |
||||
|
visibleRect.size = parent.frame.size |
||||
|
} else { |
||||
|
visibleRect = parent.visibleRect() |
||||
|
} |
||||
|
} else { |
||||
|
visibleRect = parent.visibleRect() |
||||
|
} |
||||
|
} |
||||
|
let startIndex = index(visibleRect.origin.x) - 10 * (division + 1) |
||||
|
let endIndex = index(visibleRect.origin.x + visibleRect.size.width) |
||||
|
|
||||
|
for index in startIndex ... endIndex { |
||||
|
if index < 0 { |
||||
|
continue |
||||
|
} |
||||
|
let position = CGFloat(index) * unit - visibleRect.origin.x + (visibleRect.size.width / 2) |
||||
|
if division == 0 { |
||||
|
let path = UIBezierPath() |
||||
|
path.move(to: CGPoint(x: position + (unit / 2 ) - (unitWidth / 4) , y:(self.frame.size.height / 2))) |
||||
|
path.addLine(to: CGPoint(x: position + (unit / 2) + (unitWidth / 4), y:(self.frame.size.height / 2))) |
||||
|
path.lineWidth = 1 |
||||
|
stringColor.setStroke() |
||||
|
path.stroke() |
||||
|
} else { |
||||
|
for i in 1 ... 3 { |
||||
|
let point = position + ((unit / 4 ) * CGFloat(i)) |
||||
|
let r:CGFloat = 0.8 |
||||
|
let pointRect = CGRect(x: point - r, y: (self.frame.size.height / 2) - r, width: r * 2,height: r * 2) |
||||
|
let path = UIBezierPath(ovalIn:pointRect) |
||||
|
stringColor.setFill() |
||||
|
path.fill() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let length = unitLength * Double(index) |
||||
|
let minute = Int(length / 60) |
||||
|
let second = Int(length - Double(minute * 60)) |
||||
|
|
||||
|
string = String(format: "%02d:%02d", minute, (Int(second))) |
||||
|
|
||||
|
string.draw(with: CGRect(x: position - (unitWidth / 2), y:0, width: unitWidth, height: self.frame.size.height), options: .usesLineFragmentOrigin, attributes: attributes, context: nil) |
||||
|
|
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//MARK: - timer for animation |
||||
|
var animationTimer = Timer() |
||||
|
func startAnimation() { |
||||
|
animationTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.animate(_:)), userInfo: nil, repeats: true) |
||||
|
RunLoop.main.add(animationTimer, forMode:RunLoop.Mode.common) |
||||
|
animating = true |
||||
|
} |
||||
|
|
||||
|
func stopAnimation() { |
||||
|
animating = false |
||||
|
self.setNeedsDisplay() |
||||
|
animationTimer.invalidate() |
||||
|
} |
||||
|
|
||||
|
@objc func animate(_ timer:Timer) { |
||||
|
if animating == false { |
||||
|
return |
||||
|
} |
||||
|
self.setNeedsDisplay() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
//MARK: - Touch Events |
||||
|
var allTouches = [UITouch]() |
||||
|
|
||||
|
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) |
||||
|
{ |
||||
|
for touch in touches { |
||||
|
if !allTouches.contains(touch) { |
||||
|
allTouches += [touch] |
||||
|
} |
||||
|
if !parentView!.allTouches.contains(touch) { |
||||
|
parentView!.allTouches += [touch] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) |
||||
|
{ |
||||
|
if parentView!.allTouches.count == 2 { |
||||
|
if parentView!.pinching { |
||||
|
parentView!.updatePinch() |
||||
|
} else { |
||||
|
parentView!.startPinch() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) |
||||
|
{ |
||||
|
for touch in touches { |
||||
|
if let index = allTouches.firstIndex(of:touch) { |
||||
|
allTouches.remove(at: index) |
||||
|
} |
||||
|
if let index = parentView!.allTouches.firstIndex(of:touch) { |
||||
|
parentView!.allTouches.remove(at: index) |
||||
|
} |
||||
|
} |
||||
|
if parentView!.pinching && parentView!.allTouches.count < 2 { |
||||
|
parentView!.endPinch() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
override open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) |
||||
|
{ |
||||
|
for touch in touches { |
||||
|
if let index = allTouches.firstIndex(of:touch) { |
||||
|
allTouches.remove(at: index) |
||||
|
} |
||||
|
if let index = parentView!.allTouches.firstIndex(of:touch) { |
||||
|
parentView!.allTouches.remove(at: index) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if parentView!.pinching { |
||||
|
parentView!.endPinch() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,178 @@ |
|||||
|
// |
||||
|
// TimelineScroller.swift |
||||
|
// Examplay |
||||
|
// |
||||
|
// Created by Tomohiro Yamashita on 2020/03/01. |
||||
|
// Copyright © 2020 Tom. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
import UIKit |
||||
|
|
||||
|
class TimelineScroller: UIScrollView { |
||||
|
|
||||
|
var parentView:TimelineView? = nil |
||||
|
let frameImagesView = FrameImagesView() |
||||
|
let measure = TimelineMeasure() |
||||
|
let trimView = TrimView() |
||||
|
|
||||
|
|
||||
|
override init (frame: CGRect) { |
||||
|
super.init(frame: frame) |
||||
|
|
||||
|
self.isScrollEnabled = true |
||||
|
self.isDirectionalLockEnabled = true |
||||
|
self.showsHorizontalScrollIndicator = false |
||||
|
self.showsVerticalScrollIndicator = false |
||||
|
self.bounces = false |
||||
|
self.decelerationRate = .fast |
||||
|
self.isMultipleTouchEnabled = true |
||||
|
self.delaysContentTouches = false |
||||
|
self.frameImagesView.parentScroller = self |
||||
|
self.addSubview(frameImagesView) |
||||
|
self.measure.parentScroller = self |
||||
|
self.measure.frameImagesView = self.frameImagesView |
||||
|
self.measure.backgroundColor = .clear |
||||
|
} |
||||
|
|
||||
|
required init(coder aDecoder: NSCoder) { |
||||
|
fatalError("TimelineScroller init(coder:) has not been implemented") |
||||
|
} |
||||
|
|
||||
|
func configure(parent:TimelineView) { |
||||
|
parentView = parent |
||||
|
trimView.configure(parent, scroller:self) |
||||
|
} |
||||
|
|
||||
|
func reset() { |
||||
|
frameImagesView.reset() |
||||
|
} |
||||
|
|
||||
|
var ignoreScrollViewDidScroll:Bool = false |
||||
|
func setContentWidth(_ width:CGFloat) { |
||||
|
setContentWidth(width, setOrigin:true) |
||||
|
} |
||||
|
|
||||
|
func setContentWidth(_ width: CGFloat, setOrigin:Bool) { |
||||
|
ignoreScrollViewDidScroll = true |
||||
|
self.contentSize = CGSize(width:width + self.frame.size.width, height:self.frame.size.height) |
||||
|
frameImagesView.frame.size.width = width |
||||
|
//measure.frame.size.width = width |
||||
|
|
||||
|
if setOrigin { |
||||
|
let halfVisibleWidth = self.frame.size.width / 2 |
||||
|
//frameImagesView.frame.size.height = self.frame.size.height |
||||
|
frameImagesView.frame.origin.x = halfVisibleWidth |
||||
|
} |
||||
|
//measure.frame.origin.x = halfVisibleWidth |
||||
|
measure.setNeedsDisplay() |
||||
|
frameImagesView.displayFrames() |
||||
|
} |
||||
|
|
||||
|
var measureHeight:CGFloat = 5 |
||||
|
func coordinate() { |
||||
|
|
||||
|
let measureMin:CGFloat = 10 |
||||
|
|
||||
|
let wholeHeight = self.frame.size.height |
||||
|
measureHeight = wholeHeight * 0.2 |
||||
|
if measureHeight < measureMin { |
||||
|
measureHeight = measureMin |
||||
|
} |
||||
|
frameImagesView.frame.size.height = wholeHeight - measureHeight |
||||
|
if frameImagesView.animating == false { |
||||
|
frameImagesView.frame.origin = CGPoint(x: self.frame.size.width / 2,y: measureHeight) |
||||
|
} |
||||
|
measure.frame.size.height = measureHeight |
||||
|
//frameImagesView.layout() |
||||
|
|
||||
|
trimView.frame = self.frame |
||||
|
|
||||
|
} |
||||
|
|
||||
|
func visibleRect() -> CGRect { |
||||
|
var visibleRect = frame |
||||
|
visibleRect.origin = contentOffset |
||||
|
if contentSize.width < frame.size.width { |
||||
|
visibleRect.size.width = contentSize.width |
||||
|
} |
||||
|
if contentSize.height < frame.size.height { |
||||
|
visibleRect.size.height = contentSize.height |
||||
|
} |
||||
|
if zoomScale != 1 { |
||||
|
let theScale = 1.0 / zoomScale; |
||||
|
visibleRect.origin.x *= theScale; |
||||
|
visibleRect.origin.y *= theScale; |
||||
|
visibleRect.size.width *= theScale; |
||||
|
visibleRect.size.height *= theScale; |
||||
|
} |
||||
|
return visibleRect |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//MARK: - scroll |
||||
|
func setScrollPoint(_ scrollPoint:CGFloat) { |
||||
|
let offset = (scrollPoint * frameImagesView.frame.size.width) + (self.frame.size.width / 2) |
||||
|
|
||||
|
self.contentOffset.x = offset - (self.frame.size.width / 2) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//MARK: - Touch Events |
||||
|
var allTouches = [UITouch]() |
||||
|
|
||||
|
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) |
||||
|
{ |
||||
|
for touch in touches { |
||||
|
if !allTouches.contains(touch) { |
||||
|
allTouches += [touch] |
||||
|
} |
||||
|
if !parentView!.allTouches.contains(touch) { |
||||
|
parentView!.allTouches += [touch] |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) |
||||
|
{ |
||||
|
if parentView!.allTouches.count == 2 { |
||||
|
if parentView!.pinching { |
||||
|
parentView!.updatePinch() |
||||
|
} else { |
||||
|
parentView!.startPinch() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) |
||||
|
{ |
||||
|
for touch in touches { |
||||
|
if let index = allTouches.firstIndex(of:touch) { |
||||
|
allTouches.remove(at: index) |
||||
|
} |
||||
|
if let index = parentView!.allTouches.firstIndex(of:touch) { |
||||
|
parentView!.allTouches.remove(at: index) |
||||
|
} |
||||
|
} |
||||
|
if parentView!.pinching && parentView!.allTouches.count < 2 { |
||||
|
parentView!.endPinch() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
override open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) |
||||
|
{ |
||||
|
for touch in touches { |
||||
|
if let index = allTouches.firstIndex(of:touch) { |
||||
|
allTouches.remove(at: index) |
||||
|
} |
||||
|
if let index = parentView!.allTouches.firstIndex(of:touch) { |
||||
|
parentView!.allTouches.remove(at: index) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if parentView!.pinching { |
||||
|
parentView!.endPinch() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,460 @@ |
|||||
|
// |
||||
|
// TimelineView.swift |
||||
|
// Examplay |
||||
|
// |
||||
|
// Created by Tomohiro Yamashita on 2020/02/18. |
||||
|
// Copyright © 2020 Tom. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
import Foundation |
||||
|
import UIKit |
||||
|
import AVFoundation |
||||
|
|
||||
|
class TimelineView: UIView, UIScrollViewDelegate { |
||||
|
var mainView:VideoTimelineView? = nil |
||||
|
let scroller = TimelineScroller() |
||||
|
let centerLine = CenterLine() |
||||
|
let viewForAnimate = UIScrollView() |
||||
|
|
||||
|
let durationPerHeight:Float64 = 0.35 |
||||
|
var animating = false |
||||
|
|
||||
|
override init (frame: CGRect) { |
||||
|
|
||||
|
super.init(frame: frame) |
||||
|
|
||||
|
self.backgroundColor = UIColor(hue: 0, saturation:0, brightness:0.0, alpha: 0.05) |
||||
|
|
||||
|
viewForAnimate.frame.origin = CGPoint.zero |
||||
|
viewForAnimate.isScrollEnabled = false |
||||
|
viewForAnimate.isUserInteractionEnabled = false |
||||
|
self.addSubview(viewForAnimate) |
||||
|
|
||||
|
self.addSubview(scroller) |
||||
|
scroller.delegate = self |
||||
|
|
||||
|
self.addSubview(scroller.measure) |
||||
|
scroller.configure(parent: self) |
||||
|
centerLine.configure(parent:self) |
||||
|
coordinate() |
||||
|
|
||||
|
self.addSubview(scroller.trimView) |
||||
|
self.addSubview(centerLine) |
||||
|
|
||||
|
scroller.measure.parentView = self |
||||
|
} |
||||
|
|
||||
|
required init(coder aDecoder: NSCoder) { |
||||
|
fatalError("TimelineView init(coder:) has not been implemented") |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//MARK: - coordinate |
||||
|
func coordinate() { |
||||
|
if mainView == nil { |
||||
|
return |
||||
|
} |
||||
|
frame = mainView!.bounds |
||||
|
viewForAnimate.frame.size = self.frame.size |
||||
|
|
||||
|
scroller.frame = self.bounds |
||||
|
scroller.frameImagesView.frame.size.height = scroller.frame.size.height |
||||
|
scroller.measure.frame = scroller.frame |
||||
|
scroller.measure.frame.size.height = 20 |
||||
|
scroller.coordinate() |
||||
|
|
||||
|
centerLine.timeLabel.frame.size.height = scroller.measure.frame.size.height - 2 |
||||
|
let centerLineWidth:CGFloat = scroller.measure.frame.size.height * 5 |
||||
|
centerLine.frame = CGRect(x: (self.frame.size.width - centerLineWidth) / 2,y: 0,width: centerLineWidth,height: self.frame.size.height) |
||||
|
|
||||
|
centerLine.update() |
||||
|
|
||||
|
guard let view = (mainView) else { return } |
||||
|
guard let _ = (view.asset) else { return } |
||||
|
if scroller.frameImagesView.frame.size.width <= 0 { |
||||
|
return |
||||
|
} |
||||
|
let previousThumbSize = scroller.frameImagesView.thumbnailFrameSize |
||||
|
scroller.frameImagesView.setThumnailFrameSize() |
||||
|
let thumbSize = scroller.frameImagesView.thumbnailFrameSize |
||||
|
let unit = (thumbSize.height / CGFloat(durationPerHeight)) |
||||
|
scroller.measure.unitSize = unit |
||||
|
let contentMaxWidth = (unit * CGFloat(centerLine.duration)) |
||||
|
scroller.frameImagesView.maxWidth = contentMaxWidth |
||||
|
let defineMin = scroller.frame.size.width * 0.8 |
||||
|
var contentMinWidth:CGFloat |
||||
|
|
||||
|
if scroller.frameImagesView.maxWidth <= defineMin { |
||||
|
contentMinWidth = scroller.frameImagesView.maxWidth |
||||
|
} else { |
||||
|
contentMinWidth = snapWidth(defineMin, max:scroller.frameImagesView.maxWidth) |
||||
|
} |
||||
|
|
||||
|
scroller.frameImagesView.minWidth = contentMinWidth |
||||
|
|
||||
|
var currentWidth = snapWidth((scroller.frameImagesView.thumbnailFrameSize.width / previousThumbSize.width) * scroller.frameImagesView.frame.size.width, max:scroller.frameImagesView.maxWidth) |
||||
|
if currentWidth < scroller.frameImagesView.minWidth { |
||||
|
currentWidth = scroller.frameImagesView.minWidth |
||||
|
} else if currentWidth > scroller.frameImagesView.maxWidth { |
||||
|
currentWidth = scroller.frameImagesView.maxWidth |
||||
|
} |
||||
|
|
||||
|
scroller.setContentWidth(currentWidth) |
||||
|
|
||||
|
scroller.reset() |
||||
|
|
||||
|
scroller.trimView.layout() |
||||
|
if view.currentTime <= view.duration { |
||||
|
setCurrentTime(view.currentTime, force:false) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func snapWidth(_ width:CGFloat, max:CGFloat) -> CGFloat { |
||||
|
let n = log2((2 * max) / width) |
||||
|
var intN = CGFloat(Int(n)) |
||||
|
if n - intN >= 0.5 { |
||||
|
intN += 1 |
||||
|
} |
||||
|
let result = (2 * max) / (pow(2,intN)) |
||||
|
return result |
||||
|
} |
||||
|
|
||||
|
func scrollPoint() -> CGFloat { |
||||
|
return scroller.contentOffset.x / scroller.frameImagesView.frame.size.width |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
//MARK: new movie set |
||||
|
func newMovieSet() { |
||||
|
|
||||
|
coordinate() |
||||
|
if let asset = mainView!.asset{ |
||||
|
scroller.frameImagesView.setThumnailFrameSize() |
||||
|
|
||||
|
let duration = asset.duration |
||||
|
let durationFloat = CMTimeGetSeconds(duration) |
||||
|
centerLine.duration = durationFloat |
||||
|
|
||||
|
let detailThumbSize = scroller.frameImagesView.thumbnailFrameSize |
||||
|
|
||||
|
let unit = (detailThumbSize.height / CGFloat(durationPerHeight)) |
||||
|
scroller.measure.unitSize = unit |
||||
|
|
||||
|
|
||||
|
let contentMaxWidth = (unit * CGFloat(centerLine.duration)) |
||||
|
scroller.frameImagesView.maxWidth = contentMaxWidth |
||||
|
|
||||
|
|
||||
|
let defineMin = scroller.frame.size.width * 0.8 |
||||
|
var contentMinWidth:CGFloat |
||||
|
|
||||
|
if scroller.frameImagesView.maxWidth <= defineMin { |
||||
|
contentMinWidth = scroller.frameImagesView.maxWidth |
||||
|
} else { |
||||
|
contentMinWidth = snapWidth(defineMin, max:scroller.frameImagesView.maxWidth) |
||||
|
} |
||||
|
scroller.frameImagesView.minWidth = contentMinWidth |
||||
|
scroller.setContentWidth(scroller.frameImagesView.minWidth) |
||||
|
|
||||
|
scroller.reset() |
||||
|
scroller.trimView.reset(duration:durationFloat) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//MARK: - currentTime |
||||
|
func setCurrentTime(_ currentTime:Float64, force:Bool) { |
||||
|
if inAction() && force == false { |
||||
|
return |
||||
|
} |
||||
|
if mainView!.asset == nil { |
||||
|
return |
||||
|
} |
||||
|
var scrollPoint:CGFloat = 0 |
||||
|
scrollPoint = CGFloat(currentTime / mainView!.duration) |
||||
|
|
||||
|
centerLine.ignoreSendScrollToParent = true |
||||
|
centerLine.setScrollPoint(scrollPoint) |
||||
|
scroller.ignoreScrollViewDidScroll = true |
||||
|
scroller.setScrollPoint(scrollPoint) |
||||
|
|
||||
|
scroller.frameImagesView.requestVisible(depth:0, wide:0, direction:0) |
||||
|
|
||||
|
scroller.frameImagesView.displayFrames() |
||||
|
scroller.measure.setNeedsDisplay() |
||||
|
} |
||||
|
|
||||
|
func moved(_ currentTime:Float64) { |
||||
|
mainView!.timelineIsMoved(currentTime, scrub:true) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//MARK: - TrimViews |
||||
|
func setTrimmerStatus(enabled:Bool) { |
||||
|
if enabled { |
||||
|
scroller.trimView.alpha = 1 |
||||
|
scroller.trimView.startKnob.isUserInteractionEnabled = true |
||||
|
scroller.trimView.alpha = 1 |
||||
|
scroller.trimView.endKnob.isUserInteractionEnabled = true |
||||
|
|
||||
|
} else { |
||||
|
scroller.trimView.alpha = 0.5 |
||||
|
scroller.trimView.startKnob.isUserInteractionEnabled = false |
||||
|
scroller.trimView.alpha = 0.5 |
||||
|
scroller.trimView.endKnob.isUserInteractionEnabled = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func setTrimmerVisible(_ visible:Bool) { |
||||
|
scroller.trimView.isHidden = !visible |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func setTrim(start:Float64?, end:Float64?) { |
||||
|
var changed = false |
||||
|
if start != nil { |
||||
|
scroller.trimView.startKnob.knobTimePoint = start! |
||||
|
changed = true |
||||
|
} |
||||
|
if end != nil { |
||||
|
scroller.trimView.endKnob.knobTimePoint = end! |
||||
|
changed = true |
||||
|
} |
||||
|
if changed { |
||||
|
scroller.trimView.layout() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func setTrimWithAnimation(trim:VideoTimelineTrim, time:Float64) { |
||||
|
scroller.trimView.moveToTimeAndTrimWithAnimation(time, trim:trim) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
var manualScrolledAfterEnd = false |
||||
|
func setTrimViewInteraction(_ active:Bool) { |
||||
|
if mainView!.trimEnabled == false && active { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
scroller.trimView.startKnob.isUserInteractionEnabled = active |
||||
|
scroller.trimView.endKnob.isUserInteractionEnabled = active |
||||
|
|
||||
|
if active { |
||||
|
setManualScrolledAfterEnd() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func setManualScrolledAfterEnd() { |
||||
|
let trim = currentTrim() |
||||
|
if mainView!.asset != nil { |
||||
|
let currentTime = mainView!.currentTime |
||||
|
if currentTime >= trim.end { |
||||
|
manualScrolledAfterEnd = true |
||||
|
} else { |
||||
|
manualScrolledAfterEnd = false |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func currentTrim() -> (start:Float64, end:Float64) { |
||||
|
var start = scroller.trimView.startKnob.knobTimePoint |
||||
|
var end = scroller.trimView.endKnob.knobTimePoint |
||||
|
if mainView!.asset != nil { |
||||
|
if end > mainView!.duration { |
||||
|
end = mainView!.duration |
||||
|
} |
||||
|
if start < 0 { |
||||
|
start = 0 |
||||
|
} |
||||
|
} |
||||
|
return (start, end) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func swapTrimKnobs() { |
||||
|
let knob = scroller.trimView.endKnob |
||||
|
scroller.trimView.endKnob = scroller.trimView.startKnob |
||||
|
scroller.trimView.startKnob = knob |
||||
|
} |
||||
|
|
||||
|
//MARK: - animation |
||||
|
func startAnimation() { |
||||
|
scroller.frameImagesView.startAnimation() |
||||
|
scroller.measure.startAnimation() |
||||
|
scroller.trimView.startAnimation() |
||||
|
} |
||||
|
|
||||
|
func stopAnimation() { |
||||
|
scroller.frameImagesView.stopAnimation() |
||||
|
scroller.measure.stopAnimation() |
||||
|
scroller.trimView.stopAnimation() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
//MARK: - Gestures |
||||
|
func inAction() -> Bool { |
||||
|
if allTouches.count > 0 || scroller.isTracking || scroller.isDecelerating { |
||||
|
return true |
||||
|
} else { |
||||
|
return false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//MARK: - Scrolling |
||||
|
|
||||
|
var allTouches = [UITouch]() |
||||
|
var pinching:Bool = false |
||||
|
|
||||
|
func scrollViewDidScroll(_ scrollView:UIScrollView) { |
||||
|
scroller.trimView.layout() |
||||
|
if scroller.ignoreScrollViewDidScroll { |
||||
|
scroller.ignoreScrollViewDidScroll = false |
||||
|
return |
||||
|
} |
||||
|
scroller.measure.setNeedsDisplay() |
||||
|
scroller.frameImagesView.displayFrames() |
||||
|
setTrimViewInteraction(false) |
||||
|
let scrollPoint = scroller.contentOffset.x / scroller.frameImagesView.frame.size.width |
||||
|
self.centerLine.setScrollPoint(scrollPoint) |
||||
|
|
||||
|
guard let mView = (mainView) else { return } |
||||
|
if let receiver = mView.playStatusReceiver { |
||||
|
receiver.videoTimelineMoved() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { |
||||
|
scroller.frameImagesView.requestVisible(depth:0, wide:0, direction:0) |
||||
|
setTrimViewInteraction(true) |
||||
|
let scrollPoint = scroller.contentOffset.x / scroller.frameImagesView.frame.size.width |
||||
|
self.centerLine.setScrollPoint(scrollPoint) |
||||
|
} |
||||
|
|
||||
|
func scrollViewDidEndDragging(_ scrollView: UIScrollView, |
||||
|
willDecelerate decelerate: Bool) { |
||||
|
scroller.frameImagesView.requestVisible(depth:0, wide:0, direction:0) |
||||
|
if decelerate == false { |
||||
|
setTrimViewInteraction(true) |
||||
|
} |
||||
|
let scrollPoint = scroller.contentOffset.x / scroller.frameImagesView.frame.size.width |
||||
|
self.centerLine.setScrollPoint(scrollPoint) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
//MARK: - Zooming |
||||
|
|
||||
|
var pinchCenterInContent:CGFloat = 0 |
||||
|
var pinchStartDistance:CGFloat = 0 |
||||
|
var pinchStartContent:(x:CGFloat,width:CGFloat) = (0,0) |
||||
|
func pinchCenter(_ pointA:CGPoint, pointB:CGPoint) -> CGPoint { |
||||
|
return CGPoint(x: (pointA.x + pointB.x) / 2, y: (pointA.y + pointB.y) / 2) |
||||
|
} |
||||
|
func pinchDistance(_ pointA:CGPoint, pointB:CGPoint) -> CGFloat { |
||||
|
return sqrt(pow((pointA.x - pointB.x),2) + pow((pointA.y - pointB.y),2)); |
||||
|
} |
||||
|
func startPinch() { |
||||
|
pinching = true |
||||
|
scroller.isScrollEnabled = false |
||||
|
|
||||
|
let touch1 = allTouches[0] |
||||
|
let touch2 = allTouches[1] |
||||
|
let center = pinchCenter(touch1.location(in: self),pointB: touch2.location(in: self)) |
||||
|
|
||||
|
pinchStartDistance = pinchDistance(touch1.location(in: self),pointB: touch2.location(in: self)) |
||||
|
let framewidth = scroller.frame.size.width |
||||
|
pinchStartContent = ((framewidth / 2) - scroller.contentOffset.x,scroller.contentSize.width - framewidth) |
||||
|
pinchCenterInContent = (center.x - pinchStartContent.x) / pinchStartContent.width |
||||
|
} |
||||
|
|
||||
|
func updatePinch() { |
||||
|
let touch1 = allTouches[0] |
||||
|
let touch2 = allTouches[1] |
||||
|
let center = pinchCenter(touch1.location(in: self), pointB:touch2.location(in: self)) |
||||
|
var sizeChange = (1 * pinchDistance(touch1.location(in: self), pointB: touch2.location(in: self))) / pinchStartDistance |
||||
|
|
||||
|
var contentWidth = pinchStartContent.width * sizeChange |
||||
|
|
||||
|
let sizeMin = scroller.frameImagesView.minWidth |
||||
|
let sizeMax = scroller.frameImagesView.maxWidth |
||||
|
|
||||
|
if contentWidth < sizeMin { |
||||
|
let sizeUnit = sizeMin / pinchStartContent.width |
||||
|
sizeChange = ((pow(sizeChange/sizeUnit,2)/4) + 0.75) * sizeUnit |
||||
|
contentWidth = pinchStartContent.width * sizeChange |
||||
|
contentWidth = sizeMin |
||||
|
} else if contentWidth > sizeMax { |
||||
|
sizeChange = sizeMax |
||||
|
contentWidth = sizeMax |
||||
|
} else { |
||||
|
let startRatio = pinchStartContent.width / sizeMax |
||||
|
let currentRatio = startRatio * sizeChange |
||||
|
let effect = ((sin(CGFloat.pi * 2 * log2(2/currentRatio)) * 0.108) - (sin(CGFloat.pi * 6 * log2(2/currentRatio)) * 0.009)) * currentRatio |
||||
|
let resultWidth = sizeMax * (currentRatio + effect) |
||||
|
contentWidth = resultWidth |
||||
|
} |
||||
|
let contentOrigin = center.x - (contentWidth * pinchCenterInContent) |
||||
|
scroller.contentOffset.x = (scroller.frame.size.width / 2) - contentOrigin |
||||
|
scroller.setContentWidth(contentWidth) |
||||
|
scroller.frameImagesView.layout() |
||||
|
|
||||
|
scroller.frameImagesView.requestVisible(depth:2, wide:0, direction:0) |
||||
|
self.centerLine.setScrollPoint(scroller.contentOffset.x / scroller.frameImagesView.frame.size.width) |
||||
|
scroller.trimView.layout() |
||||
|
} |
||||
|
|
||||
|
func endPinch() { |
||||
|
scroller.frameImagesView.requestVisible(depth:0, wide:1, direction:0) |
||||
|
|
||||
|
let width = snapWidth(scroller.frameImagesView.frame.size.width, max:scroller.frameImagesView.maxWidth) |
||||
|
|
||||
|
let offset = self.resizedPositionWithKeepOrigin(width:scroller.frameImagesView.frame.size.width, origin:scroller.contentOffset.x, destinationWidth:width) |
||||
|
//startAnimation() |
||||
|
|
||||
|
UIView.animate(withDuration: 0.1,delay:Double(0.0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in |
||||
|
|
||||
|
self.scroller.setContentWidth(width, setOrigin:false) |
||||
|
self.scroller.contentOffset.x = offset |
||||
|
self.scroller.frameImagesView.layout() |
||||
|
self.scroller.trimView.layout() |
||||
|
},completion: { finished in |
||||
|
self.pinching = false |
||||
|
self.scroller.isScrollEnabled = true |
||||
|
}) |
||||
|
|
||||
|
self.scroller.frameImagesView.updateTolerance() |
||||
|
|
||||
|
self.centerLine.setScrollPoint(scroller.contentOffset.x / scroller.frameImagesView.frame.size.width) |
||||
|
|
||||
|
setTrimViewInteraction(true) |
||||
|
|
||||
|
guard let mView = (mainView) else { return } |
||||
|
if let receiver = mView.playStatusReceiver { |
||||
|
receiver.videoTimelineMoved() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func resizedPositionWithKeepOrigin(width:CGFloat, origin:CGFloat, destinationWidth:CGFloat) -> CGFloat { |
||||
|
let originPoint = origin / width |
||||
|
let result = originPoint * destinationWidth |
||||
|
return result |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
@ -0,0 +1,773 @@ |
|||||
|
// |
||||
|
// TrimView.swift |
||||
|
// Examplay |
||||
|
// |
||||
|
// Created by Tomohiro Yamashita on 2020/03/12. |
||||
|
// Copyright © 2020 Tom. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
import UIKit |
||||
|
|
||||
|
class TrimView: UIView { |
||||
|
var mainView:VideoTimelineView! |
||||
|
|
||||
|
var timelineView:TimelineView! |
||||
|
var parentScroller:TimelineScroller! |
||||
|
var startKnob = TrimKnob() |
||||
|
var endKnob = TrimKnob() |
||||
|
var movieDuration:Float64 = 0 |
||||
|
|
||||
|
let canPassThroughEachKnobs = true |
||||
|
|
||||
|
override init (frame: CGRect) { |
||||
|
super.init(frame: frame) |
||||
|
self.isUserInteractionEnabled = false |
||||
|
self.backgroundColor = .clear |
||||
|
|
||||
|
} |
||||
|
|
||||
|
required init(coder aDecoder: NSCoder) { |
||||
|
fatalError("TrimView init(coder:) has not been implemented") |
||||
|
} |
||||
|
|
||||
|
func configure(_ timeline:TimelineView, scroller:TimelineScroller) { |
||||
|
timelineView = timeline |
||||
|
parentScroller = scroller |
||||
|
self.frame = timeline.frame |
||||
|
startKnob.configure(timeline, trimmer:self) |
||||
|
endKnob.configure(timeline, trimmer:self) |
||||
|
|
||||
|
timelineView.addSubview(startKnob) |
||||
|
timelineView.addSubview(endKnob) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func reset(duration:Float64) { |
||||
|
movieDuration = duration |
||||
|
startKnob.knobTimePoint = 0 |
||||
|
endKnob.knobTimePoint = 3 |
||||
|
if duration < endKnob.knobTimePoint { |
||||
|
endKnob.knobTimePoint = duration |
||||
|
} |
||||
|
layout() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
let knobWidth:CGFloat = 20 |
||||
|
let knobWidthExtend:CGFloat = 5 |
||||
|
func layout() { |
||||
|
if self.isHidden { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
swapKnobs() |
||||
|
|
||||
|
let knobPositions = knobPositionsAsVisible() |
||||
|
let startPosition = knobPositions.start |
||||
|
let endPosition = knobPositions.end |
||||
|
|
||||
|
startKnob.knobPositionOnScreen = startPosition |
||||
|
endKnob.knobPositionOnScreen = endPosition |
||||
|
startKnob.isOutOfScreen = knobPositions.startFixed |
||||
|
endKnob.isOutOfScreen = knobPositions.endFixed |
||||
|
|
||||
|
startKnob.frame = CGRect(x:startPosition - knobWidth - knobWidthExtend, y:self.frame.origin.y, width:knobWidth + knobWidthExtend * 2, height:self.frame.size.height) |
||||
|
endKnob.frame = CGRect(x:endPosition - knobWidthExtend, y:self.frame.origin.y, width:knobWidth + knobWidthExtend * 2, height:self.frame.size.height) |
||||
|
|
||||
|
self.setNeedsDisplay() |
||||
|
} |
||||
|
|
||||
|
//MARK: - draw |
||||
|
|
||||
|
override func draw(_ rect: CGRect) { |
||||
|
|
||||
|
|
||||
|
var startRect = startKnob.frame |
||||
|
var endRect = endKnob.frame |
||||
|
if startRect.size.height <= 0 { |
||||
|
return |
||||
|
} |
||||
|
if animating { |
||||
|
if let layer = startKnob.layer.presentation() { |
||||
|
startRect = layer.frame |
||||
|
} |
||||
|
if let layer = endKnob.layer.presentation() { |
||||
|
endRect = layer.frame |
||||
|
} |
||||
|
} |
||||
|
startRect.origin.x += knobWidthExtend |
||||
|
startRect.size.width -= knobWidthExtend * 2 |
||||
|
endRect.origin.x += knobWidthExtend |
||||
|
endRect.size.width -= knobWidthExtend * 2 |
||||
|
if startRect.origin.x > endRect.origin.x + endRect.size.width { |
||||
|
let swapRect = startRect |
||||
|
startRect = endRect |
||||
|
endRect = swapRect |
||||
|
} |
||||
|
|
||||
|
|
||||
|
let beamWidth:CGFloat = 3 |
||||
|
var outerRect = CGRect(x: startRect.origin.x,y: 0,width: endRect.origin.x + endRect.size.width - startRect.origin.x,height:startRect.size.height) |
||||
|
var innerRect = CGRect(x:startRect.origin.x + startRect.size.width,y:beamWidth,width: endRect.origin.x - startRect.origin.x - startRect.size.width,height:startRect.size.height - (beamWidth * 2)) |
||||
|
|
||||
|
let screenLeft = cgToTime(screenToTimelinePosition(0)) |
||||
|
let screenRight = cgToTime(screenToTimelinePosition(timelineView.frame.size.width)) |
||||
|
var color = UIColor(hue: 0.1, saturation:0.8, brightness:1, alpha: 1) |
||||
|
let outColor = UIColor(hue: 0.1, saturation:0.8, brightness:1, alpha: 0.3) |
||||
|
if (endKnob.knobTimePoint < screenLeft || startKnob.knobTimePoint > screenRight) { |
||||
|
color = outColor |
||||
|
} else { |
||||
|
let addition = knobWidth + 10 |
||||
|
let knobWidthTime = cgToTime(knobWidth) |
||||
|
if endKnob.knobTimePoint + knobWidthTime * 0.9 > screenRight { |
||||
|
outerRect.size.width += addition |
||||
|
innerRect.size.width += addition |
||||
|
let outRect = CGRect(x: timelineView.frame.size.width - knobWidth, y: beamWidth,width: knobWidth, height: endRect.size.height - beamWidth * 2) |
||||
|
let path = UIBezierPath(rect:outRect) |
||||
|
outColor.setFill() |
||||
|
path.fill() |
||||
|
} |
||||
|
if startKnob.knobTimePoint - knobWidthTime * 0.9 < screenLeft { |
||||
|
outerRect.origin.x -= addition |
||||
|
innerRect.origin.x -= addition |
||||
|
outerRect.size.width += addition |
||||
|
innerRect.size.width += addition |
||||
|
let outRect = CGRect(x: 0, y: beamWidth,width: knobWidth, height: endRect.size.height - beamWidth * 2) |
||||
|
let path = UIBezierPath(rect:outRect) |
||||
|
outColor.setFill() |
||||
|
path.fill() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let path = UIBezierPath(roundedRect:outerRect, cornerRadius:5) |
||||
|
path.usesEvenOddFillRule = true |
||||
|
let innerPath = UIBezierPath(roundedRect:innerRect, cornerRadius:2) |
||||
|
path.append(innerPath) |
||||
|
color.setFill() |
||||
|
path.fill() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
//MARK: - timer for animation |
||||
|
var animationTimer = Timer() |
||||
|
var animating = false |
||||
|
func startAnimation() { |
||||
|
animationTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.animate(_:)), userInfo: nil, repeats: true) |
||||
|
RunLoop.main.add(animationTimer, forMode:RunLoop.Mode.common) |
||||
|
animating = true |
||||
|
} |
||||
|
|
||||
|
func stopAnimation() { |
||||
|
animating = false |
||||
|
self.setNeedsDisplay() |
||||
|
animationTimer.invalidate() |
||||
|
} |
||||
|
|
||||
|
@objc func animate(_ timer:Timer) { |
||||
|
if animating == false { |
||||
|
return |
||||
|
} |
||||
|
self.setNeedsDisplay() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//MARK: - positioning |
||||
|
func swapKnobs() { |
||||
|
if startKnob.knobTimePoint > endKnob.knobTimePoint { |
||||
|
if canPassThroughEachKnobs { |
||||
|
//timelineView.swapTrimKnobs() |
||||
|
} else { |
||||
|
let start = endKnob.knobTimePoint |
||||
|
endKnob.knobTimePoint = startKnob.knobTimePoint |
||||
|
startKnob.knobTimePoint = start |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func anotherKnob(_ knob:TrimKnob) -> TrimKnob { |
||||
|
if knob == startKnob { |
||||
|
return endKnob |
||||
|
} |
||||
|
return startKnob |
||||
|
} |
||||
|
|
||||
|
func knobOnScreen(_ knob:TrimKnob) -> CGFloat { |
||||
|
let maxWidth = scrollMaxWidth() |
||||
|
let offset = scrollOffset() |
||||
|
let position = CGFloat(knob.knobTimePoint / movieDuration) * maxWidth |
||||
|
return position - offset + (timelineView.frame.size.width / 2) |
||||
|
} |
||||
|
|
||||
|
func knobsMinDistanceTime() -> Float64 { |
||||
|
return Float64(0.1) |
||||
|
} |
||||
|
|
||||
|
func knobsMinDistanceFloat() -> CGFloat { |
||||
|
return timeToCG(knobsMinDistanceTime()) |
||||
|
} |
||||
|
|
||||
|
func timeToCG(_ time:Float64) -> CGFloat { |
||||
|
return CGFloat(time / movieDuration) * scrollMaxWidth() |
||||
|
} |
||||
|
|
||||
|
func cgToTime(_ cgFloat:CGFloat) -> Float64 { |
||||
|
return Float64(cgFloat / scrollMaxWidth()) * movieDuration |
||||
|
} |
||||
|
|
||||
|
func screenToTimelinePosition(_ onScreen:CGFloat) -> CGFloat { |
||||
|
return onScreen + scrollOffset() - (timelineView.frame.size.width / 2) |
||||
|
} |
||||
|
|
||||
|
func timelineToScreenPosition(_ position:CGFloat) -> CGFloat { |
||||
|
return position - scrollOffset() + (timelineView.frame.size.width / 2) |
||||
|
} |
||||
|
|
||||
|
func knobPositionsAsVisible() -> (start:CGFloat, end:CGFloat, startFixed:Bool, endFixed:Bool) { |
||||
|
|
||||
|
let positionStart = knobOnScreen(startKnob) |
||||
|
let positionEnd = knobOnScreen(endKnob) |
||||
|
var resultStart = positionStart |
||||
|
var resultEnd = positionEnd |
||||
|
var startFixed:Bool = false |
||||
|
var endFixed:Bool = false |
||||
|
let screenRight = timelineView.frame.size.width// + offset |
||||
|
let minDistance = knobsMinDistanceFloat() |
||||
|
|
||||
|
if positionStart < knobWidth { |
||||
|
if (positionEnd - minDistance - knobWidth) < 0 { |
||||
|
resultStart = positionEnd - minDistance |
||||
|
} else { |
||||
|
resultStart = knobWidth |
||||
|
} |
||||
|
startFixed = true |
||||
|
} else if positionStart > screenRight { |
||||
|
resultStart = screenRight |
||||
|
startFixed = true |
||||
|
} |
||||
|
if positionEnd < 0 { |
||||
|
resultEnd = 0 |
||||
|
endFixed = true |
||||
|
} else if positionEnd + knobWidth > screenRight { |
||||
|
if positionStart + minDistance + knobWidth > screenRight { |
||||
|
resultEnd = positionStart + minDistance |
||||
|
} else { |
||||
|
resultEnd = screenRight - knobWidth |
||||
|
} |
||||
|
} |
||||
|
if true { |
||||
|
let distance = abs(resultStart - resultEnd) |
||||
|
if distance < minDistance { |
||||
|
let value = (minDistance - distance) / 2 |
||||
|
resultStart -= value |
||||
|
resultEnd += value |
||||
|
startFixed = true |
||||
|
endFixed = true |
||||
|
} |
||||
|
} |
||||
|
return (resultStart, resultEnd, startFixed, endFixed) |
||||
|
} |
||||
|
|
||||
|
func scrollOffset() -> CGFloat { |
||||
|
return parentScroller.contentOffset.x |
||||
|
} |
||||
|
|
||||
|
func scrollMaxWidth() -> CGFloat { |
||||
|
return parentScroller.frameImagesView.frame.size.width |
||||
|
} |
||||
|
|
||||
|
func knobTimeOnScreen(_ knob:TrimKnob) -> Float64 { |
||||
|
let offset = scrollOffset() |
||||
|
let position = offset + knob.knobPositionOnScreen - (timelineView.frame.size.width / 2) |
||||
|
let maxWidth = scrollMaxWidth() |
||||
|
let time = Float64(position / maxWidth) * movieDuration |
||||
|
return time |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func knobMoveRange(_ knob:TrimKnob) -> (min:Float64, max:Float64) { |
||||
|
var min:Float64 = 0 |
||||
|
var max:Float64 = movieDuration |
||||
|
let minDistance = knobsMinDistanceTime() |
||||
|
if canPassThroughEachKnobs { |
||||
|
if knob == startKnob { |
||||
|
max -= minDistance |
||||
|
} else if knob == endKnob { |
||||
|
min += minDistance |
||||
|
} |
||||
|
} else { |
||||
|
if knob == startKnob { |
||||
|
max = endKnob.knobTimePoint - minDistance |
||||
|
} else if knob == endKnob { |
||||
|
min = startKnob.knobTimePoint + minDistance |
||||
|
} |
||||
|
} |
||||
|
return (min, max) |
||||
|
} |
||||
|
|
||||
|
func visibleKnobMoveLimit(_ knob:TrimKnob, margin:CGFloat) -> (min:Bool, max:Bool) { |
||||
|
let range = knobMoveRange(knob) |
||||
|
let minOnScreen = timelineToScreenPosition(timeToCG(range.min)) |
||||
|
let maxOnScreen = timelineToScreenPosition(timeToCG(range.max)) |
||||
|
var resultMin = false |
||||
|
var resultMax = false |
||||
|
if minOnScreen >= margin && minOnScreen <= timelineView.frame.width - margin { |
||||
|
resultMin = true |
||||
|
} |
||||
|
if maxOnScreen >= margin && maxOnScreen <= timelineView.frame.width - margin { |
||||
|
resultMax = true |
||||
|
} |
||||
|
return (resultMin , resultMax ) |
||||
|
} |
||||
|
|
||||
|
func directionReachesEnd(_ knob:TrimKnob, direction:CGFloat) -> Bool { |
||||
|
let visibleLimit = visibleKnobMoveLimit(knob, margin:knobWidth) |
||||
|
if direction > 0 { |
||||
|
if visibleLimit.max { |
||||
|
return true |
||||
|
} |
||||
|
} else if direction < 0 { |
||||
|
if visibleLimit.min { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
func fixKnobPoint(_ knob:TrimKnob, move:CGFloat, startKnobPoint:Float64) -> (knobPoint:Float64, fixed:Bool) { |
||||
|
|
||||
|
var timePoint = startKnobPoint + cgToTime(move) |
||||
|
|
||||
|
let moveRange = knobMoveRange(knob) |
||||
|
var fixed = false |
||||
|
if timePoint < moveRange.min { |
||||
|
timePoint = moveRange.min |
||||
|
fixed = true |
||||
|
} |
||||
|
if timePoint > moveRange.max { |
||||
|
timePoint = moveRange.max |
||||
|
fixed = true |
||||
|
} |
||||
|
return (timePoint, fixed) |
||||
|
} |
||||
|
|
||||
|
func updateKnob(_ knob:TrimKnob, timePoint:Float64) { |
||||
|
knob.knobTimePoint = timePoint |
||||
|
layout() |
||||
|
|
||||
|
timelineView.moved(knob.knobTimePoint) |
||||
|
} |
||||
|
|
||||
|
func resetSeek(_ time:Float64) { |
||||
|
if edgeScrolled { |
||||
|
moveToTimeWithAnimation(time) |
||||
|
} else { |
||||
|
mainView!.accurateSeek(time, scrub:true) |
||||
|
} |
||||
|
edgeScrolled = false |
||||
|
} |
||||
|
|
||||
|
func moveToTimeWithAnimation(_ time:Float64) { |
||||
|
moveToTimeAndTrimWithAnimation(time, trim:nil) |
||||
|
} |
||||
|
|
||||
|
func moveToTimeAndTrimWithAnimation(_ time:Float64, trim:VideoTimelineTrim?) { |
||||
|
let player = mainView!.player |
||||
|
if player == nil { |
||||
|
return |
||||
|
} |
||||
|
if player!.timeControlStatus == .playing { |
||||
|
player!.pause() |
||||
|
} |
||||
|
timelineView.animating = true |
||||
|
mainView!.isUserInteractionEnabled = false |
||||
|
timelineView.startAnimation() |
||||
|
|
||||
|
UIView.animate(withDuration: 0.2,delay:Double(0.0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in |
||||
|
if let pinnedTrim = trim { |
||||
|
self.timelineView.setTrim(start:pinnedTrim.start,end:pinnedTrim.end) |
||||
|
} |
||||
|
self.timelineView.setCurrentTime(time,force:false) |
||||
|
},completion: { finished in |
||||
|
|
||||
|
self.mainView!.isUserInteractionEnabled = true |
||||
|
if self.mainView!.playing { |
||||
|
self.mainView!.accurateSeek(time, scrub:false) |
||||
|
player!.play() |
||||
|
} else { |
||||
|
self.mainView!.accurateSeek(time, scrub:true) |
||||
|
} |
||||
|
self.timelineView.animating = false |
||||
|
self.timelineView.setManualScrolledAfterEnd() |
||||
|
self.timelineView.stopAnimation() |
||||
|
if let receiver = self.mainView!.playStatusReceiver { |
||||
|
receiver.videoTimelineMoved() |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
//MARK: - edgeScroll |
||||
|
var edgeScrollTimer = Timer() |
||||
|
var edgeScrolled = false |
||||
|
var edgeScrollingKnob:TrimKnob? = nil |
||||
|
var edgeScrollStrength:CGFloat = 0 |
||||
|
var edgeScrollingKnobPosition:CGFloat = 0 |
||||
|
var edgeScrollLastChangedTime:Date = Date() |
||||
|
var edgeScrollLastChangedPosition:CGFloat = 0 |
||||
|
|
||||
|
|
||||
|
func updateEdgeScroll(_ knob:TrimKnob, strength:CGFloat, position:CGFloat) { |
||||
|
var changed = false |
||||
|
if edgeScrollStrength != strength { |
||||
|
edgeScrollStrength = strength |
||||
|
changed = true |
||||
|
} |
||||
|
if edgeScrolling() == false { |
||||
|
startEdgeScroll(knob) |
||||
|
edgeScrolled = true |
||||
|
} else if changed { |
||||
|
edgeScrollLastChangedPosition = scrollOffset() |
||||
|
edgeScrollLastChangedTime = Date() |
||||
|
} |
||||
|
edgeScrollingKnobPosition = position |
||||
|
edgeScrollingKnob = knob |
||||
|
} |
||||
|
|
||||
|
func startEdgeScroll(_ knob:TrimKnob) { |
||||
|
edgeScrollTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.edgeScrollTimer(_:)), userInfo: nil, repeats: true) |
||||
|
RunLoop.main.add(edgeScrollTimer, forMode:RunLoop.Mode.common) |
||||
|
edgeScrollLastChangedTime = Date() |
||||
|
edgeScrollLastChangedPosition = scrollOffset() |
||||
|
} |
||||
|
|
||||
|
@objc func edgeScrollTimer(_ timer:Timer) { |
||||
|
if let knob = edgeScrollingKnob { |
||||
|
let movedPosition = currentEdgeScrollMovedPosition() |
||||
|
let destination = currentEdgeScrollPosition(movedPosition) |
||||
|
let moveRange = knobMoveRange(knob) |
||||
|
var knobPoint = cgToTime(edgeScrollingKnobPosition - (timelineView.frame.size.width / 2) + destination) |
||||
|
|
||||
|
var overLimit:Float64 = 0 |
||||
|
if knobPoint > moveRange.max { |
||||
|
overLimit = knobPoint - moveRange.max |
||||
|
knobPoint = moveRange.max |
||||
|
|
||||
|
} else if knobPoint < moveRange.min { |
||||
|
overLimit = knobPoint - moveRange.min |
||||
|
knobPoint = moveRange.min |
||||
|
|
||||
|
} |
||||
|
updateKnob(knob, timePoint:knobPoint) |
||||
|
if (knob == startKnob && knobPoint > endKnob.knobTimePoint) || (knob == endKnob && knobPoint < startKnob.knobTimePoint) { |
||||
|
timelineView.swapTrimKnobs() |
||||
|
} |
||||
|
timelineView.setCurrentTime(cgToTime(destination) - overLimit,force:true) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func stopEdgeScrollTimer() { |
||||
|
edgeScrollTimer.invalidate() |
||||
|
edgeScrollStrength = 0 |
||||
|
edgeScrollingKnob = nil |
||||
|
} |
||||
|
|
||||
|
func edgeScrolling() -> Bool { |
||||
|
return edgeScrollTimer.isValid |
||||
|
} |
||||
|
|
||||
|
func currentEdgeScrollPosition(_ moved:CGFloat) -> CGFloat { |
||||
|
var result:CGFloat = 0 |
||||
|
|
||||
|
let maxWidth = scrollMaxWidth() |
||||
|
result = edgeScrollLastChangedPosition + moved |
||||
|
if result < 0 { |
||||
|
result = 0 |
||||
|
} else if result > maxWidth { |
||||
|
result = maxWidth |
||||
|
} |
||||
|
return result |
||||
|
} |
||||
|
|
||||
|
func currentEdgeScrollMovedPosition() -> CGFloat { |
||||
|
let pastTime = -edgeScrollLastChangedTime.timeIntervalSinceNow |
||||
|
return CGFloat(pastTime) * edgeScrollStrength * 5 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//MARK: - TrimKnob |
||||
|
class TrimKnob:UIView { |
||||
|
var timelineView:TimelineView! |
||||
|
var knobPositionOnScreen:CGFloat = 0 |
||||
|
var trimView:TrimView! |
||||
|
var knobTimePoint:Float64 = 0 |
||||
|
var isOutOfScreen:Bool = false |
||||
|
|
||||
|
|
||||
|
override init (frame: CGRect) { |
||||
|
super.init(frame: frame) |
||||
|
|
||||
|
self.isMultipleTouchEnabled = true |
||||
|
self.isUserInteractionEnabled = true |
||||
|
|
||||
|
} |
||||
|
|
||||
|
required init(coder aDecoder: NSCoder) { |
||||
|
fatalError("TrimKnob init(coder:) has not been implemented") |
||||
|
} |
||||
|
|
||||
|
func configure(_ timeline:TimelineView, trimmer:TrimView) { |
||||
|
timelineView = timeline |
||||
|
trimView = trimmer |
||||
|
} |
||||
|
|
||||
|
//MARK: - Touch Events |
||||
|
var allTouches = [UITouch]() |
||||
|
|
||||
|
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) |
||||
|
{ |
||||
|
for touch in touches { |
||||
|
if !allTouches.contains(touch) { |
||||
|
allTouches += [touch] |
||||
|
} |
||||
|
if !timelineView!.allTouches.contains(touch) { |
||||
|
timelineView!.allTouches += [touch] |
||||
|
} |
||||
|
} |
||||
|
if timelineView!.allTouches.count == 1 && allTouches.count == 1 { |
||||
|
if dragging == false { |
||||
|
startDrag() |
||||
|
} |
||||
|
evaluateTap = true |
||||
|
} else { |
||||
|
evaluateTap = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
override open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) |
||||
|
{ |
||||
|
if timelineView!.allTouches.count > 1 { |
||||
|
if dragging { |
||||
|
cancelDrag() |
||||
|
} |
||||
|
} |
||||
|
if timelineView!.allTouches.count == 2 { |
||||
|
if timelineView!.pinching { |
||||
|
timelineView!.updatePinch() |
||||
|
} else { |
||||
|
timelineView!.startPinch() |
||||
|
} |
||||
|
} |
||||
|
if dragging && timelineView!.allTouches.count == 1 && allTouches.count == 1 { |
||||
|
updateDrag() |
||||
|
} |
||||
|
evaluateTap = false |
||||
|
} |
||||
|
|
||||
|
override open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) |
||||
|
{ |
||||
|
for touch in touches { |
||||
|
if let index = allTouches.firstIndex(of:touch) { |
||||
|
allTouches.remove(at: index) |
||||
|
} |
||||
|
if let index = timelineView!.allTouches.firstIndex(of:touch) { |
||||
|
timelineView!.allTouches.remove(at: index) |
||||
|
} |
||||
|
} |
||||
|
if timelineView!.pinching && timelineView!.allTouches.count < 2 { |
||||
|
timelineView!.endPinch() |
||||
|
} |
||||
|
if dragging { |
||||
|
endDrag() |
||||
|
} |
||||
|
|
||||
|
if evaluateTap && timelineView!.allTouches.count == 0 { |
||||
|
tapped() |
||||
|
} |
||||
|
evaluateTap = false |
||||
|
} |
||||
|
|
||||
|
override open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) |
||||
|
{ |
||||
|
for touch in touches { |
||||
|
if let index = allTouches.firstIndex(of:touch) { |
||||
|
allTouches.remove(at: index) |
||||
|
} |
||||
|
if let index = timelineView!.allTouches.firstIndex(of:touch) { |
||||
|
timelineView!.allTouches.remove(at: index) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if timelineView!.pinching { |
||||
|
timelineView!.endPinch() |
||||
|
} |
||||
|
if dragging { |
||||
|
endDrag() |
||||
|
} |
||||
|
evaluateTap = false |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//MARK: - actions |
||||
|
|
||||
|
var dragging:Bool = false |
||||
|
var dragStartPoint = CGPoint.zero |
||||
|
var startKnobTimePoint:Float64 = 0 |
||||
|
var dragStartOffset:CGFloat = 0 |
||||
|
var startTimeOutOfScreen:Float64 = 0 |
||||
|
var scrolling = false |
||||
|
var startCurrentTime:Float64 = 0 |
||||
|
var evaluateTap:Bool = false |
||||
|
var ignoreEdgeScroll = false |
||||
|
|
||||
|
func startDrag() { |
||||
|
|
||||
|
dragging = true |
||||
|
let touch = allTouches[0] |
||||
|
dragStartPoint = touch.location(in: timelineView) |
||||
|
startKnobTimePoint = knobTimePoint |
||||
|
dragStartOffset = trimView.scrollOffset() |
||||
|
startTimeOutOfScreen = trimView.knobTimeOnScreen(self) - startKnobTimePoint |
||||
|
|
||||
|
|
||||
|
startCurrentTime = timelineView.mainView!.currentTime |
||||
|
|
||||
|
if edgeScrollStrength(dragStartPoint.x) != 0 { |
||||
|
ignoreEdgeScroll = true |
||||
|
} else { |
||||
|
ignoreEdgeScroll = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func updateDrag() { |
||||
|
let touch = allTouches[0] |
||||
|
let currentPoint = touch.location(in: timelineView) |
||||
|
let scrolled = trimView.scrollOffset() - dragStartOffset |
||||
|
let move = currentPoint.x - dragStartPoint.x + scrolled |
||||
|
let startKnobPoint = startKnobTimePoint + startTimeOutOfScreen |
||||
|
|
||||
|
let timePoint = startKnobPoint + trimView.cgToTime(move) |
||||
|
let dragPoint = trimView.cgToTime(trimView.screenToTimelinePosition(currentPoint.x)) |
||||
|
let onKnob = trimView.timeToCG(timePoint - dragPoint) |
||||
|
let anotherKnob = trimView.anotherKnob(self) |
||||
|
let anotherTimePoint = anotherKnob.knobTimePoint |
||||
|
let knobWidth = trimView.knobWidth |
||||
|
var swapping:CGFloat = 0 |
||||
|
|
||||
|
|
||||
|
if anotherTimePoint < timePoint && startKnobPoint < anotherTimePoint |
||||
|
{ |
||||
|
swapping = trimView.timeToCG(anotherTimePoint - timePoint) |
||||
|
if -swapping > knobWidth { |
||||
|
swapping = -knobWidth |
||||
|
} |
||||
|
} else if anotherTimePoint > timePoint && startKnobPoint > anotherTimePoint |
||||
|
{ |
||||
|
swapping = trimView.timeToCG(anotherTimePoint - timePoint) |
||||
|
if swapping > knobWidth { |
||||
|
swapping = knobWidth |
||||
|
} |
||||
|
} |
||||
|
var rangeOut = false |
||||
|
let range = trimView.knobMoveRange(self) |
||||
|
if timePoint > range.max || timePoint < range.min { |
||||
|
rangeOut = true |
||||
|
} |
||||
|
|
||||
|
if rangeOut == false && ((self == trimView.startKnob && dragPoint > anotherTimePoint) || (self == trimView.endKnob && dragPoint < anotherTimePoint)) { |
||||
|
timelineView.swapTrimKnobs() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
let strength = edgeScrollStrength(currentPoint.x) |
||||
|
let reachedEnd = trimView.directionReachesEnd(self, direction:strength) |
||||
|
if strength != 0 && ignoreEdgeScroll == false && reachedEnd == false { |
||||
|
trimView.updateEdgeScroll(self, strength:strength, position:currentPoint.x + onKnob + swapping) |
||||
|
} else { |
||||
|
let fixedKnobPoint = trimView.fixKnobPoint(self, move:move + swapping, startKnobPoint:startKnobPoint) |
||||
|
|
||||
|
trimView.updateKnob(self, timePoint:fixedKnobPoint.knobPoint) |
||||
|
if strength == 0 { |
||||
|
ignoreEdgeScroll = false |
||||
|
|
||||
|
} |
||||
|
if strength == 0 || reachedEnd { |
||||
|
if trimView.edgeScrolling() { |
||||
|
trimView.stopEdgeScrollTimer() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
guard let mainView = (timelineView.mainView) else { return } |
||||
|
if let receiver = mainView.playStatusReceiver { |
||||
|
receiver.videoTimelineTrimChanged() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func endDrag() { |
||||
|
let anotherKnob = trimView.anotherKnob(self) |
||||
|
let distance = abs(anotherKnob.knobTimePoint - knobTimePoint) |
||||
|
let minDistance = trimView.knobsMinDistanceTime() |
||||
|
if distance < minDistance { |
||||
|
if self == trimView.startKnob { |
||||
|
knobTimePoint -= (minDistance - distance) |
||||
|
} else if self == trimView.endKnob { |
||||
|
knobTimePoint += (minDistance - distance) |
||||
|
} |
||||
|
|
||||
|
trimView.timelineView.animating = true |
||||
|
trimView.startAnimation() |
||||
|
UIView.animate(withDuration: 0.2,delay:Double(0.0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in |
||||
|
|
||||
|
self.trimView.layout() |
||||
|
|
||||
|
},completion: { finished in |
||||
|
self.trimView.stopAnimation() |
||||
|
self.timelineView.animating = false |
||||
|
}) |
||||
|
} |
||||
|
timelineView.setManualScrolledAfterEnd() |
||||
|
trimView.resetSeek(startCurrentTime) |
||||
|
dragging = false |
||||
|
trimView.stopEdgeScrollTimer() |
||||
|
|
||||
|
if evaluateTap == false { |
||||
|
guard let mainView = (timelineView.mainView) else { return } |
||||
|
if let receiver = mainView.playStatusReceiver { |
||||
|
receiver.videoTimelineTrimChanged() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func cancelDrag() { |
||||
|
knobTimePoint = startKnobTimePoint |
||||
|
|
||||
|
timelineView.setManualScrolledAfterEnd() |
||||
|
dragging = false |
||||
|
trimView.layout() |
||||
|
trimView.resetSeek(startCurrentTime) |
||||
|
trimView.stopEdgeScrollTimer() |
||||
|
} |
||||
|
|
||||
|
func tapped() { |
||||
|
trimView.moveToTimeWithAnimation(knobTimePoint) |
||||
|
timelineView.setManualScrolledAfterEnd() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func edgeScrollStrength(_ position:CGFloat) -> CGFloat { |
||||
|
var strength:CGFloat = 0 |
||||
|
let edgeWidth:CGFloat = 40 |
||||
|
if position >= timelineView.frame.size.width - edgeWidth { |
||||
|
strength = position + edgeWidth - timelineView.frame.size.width |
||||
|
} else if position <= edgeWidth { |
||||
|
strength = position - edgeWidth |
||||
|
} |
||||
|
return strength |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
} |
||||
@ -0,0 +1,301 @@ |
|||||
|
// |
||||
|
// VideoTimelineView.swift |
||||
|
// VideoTimelineView |
||||
|
// |
||||
|
// Created by Tomohiro Yamashita on 2020/03/28. |
||||
|
// Copyright © 2020 Tom. All rights reserved. |
||||
|
// |
||||
|
|
||||
|
import UIKit |
||||
|
import AVFoundation |
||||
|
|
||||
|
protocol TimelinePlayStatusReceiver: class { |
||||
|
func videoTimelineStopped() |
||||
|
func videoTimelineMoved() |
||||
|
func videoTimelineTrimChanged() |
||||
|
} |
||||
|
|
||||
|
|
||||
|
struct VideoTimelineTrim { |
||||
|
var start:Float64 |
||||
|
var end:Float64 |
||||
|
} |
||||
|
|
||||
|
class VideoTimelineView: UIView { |
||||
|
|
||||
|
public private(set) var asset:AVAsset? = nil |
||||
|
var player:AVPlayer? = nil |
||||
|
|
||||
|
weak var playStatusReceiver:TimelinePlayStatusReceiver? = nil |
||||
|
|
||||
|
var repeatOn:Bool = false |
||||
|
|
||||
|
public private(set) var trimEnabled:Bool = false |
||||
|
|
||||
|
|
||||
|
|
||||
|
var currentTime:Float64 = 0 |
||||
|
public private(set) var duration:Float64 = 0 |
||||
|
|
||||
|
public private(set) var audioPlayer:AVPlayer! |
||||
|
public private(set) var audioPlayer2:AVPlayer! |
||||
|
|
||||
|
let timelineView = TimelineView() |
||||
|
|
||||
|
override init (frame: CGRect) { |
||||
|
super.init(frame: frame) |
||||
|
timelineView.mainView = self |
||||
|
timelineView.centerLine.mainView = self |
||||
|
timelineView.scroller.frameImagesView.mainView = self |
||||
|
timelineView.scroller.trimView.mainView = self |
||||
|
|
||||
|
self.addSubview(timelineView) |
||||
|
} |
||||
|
|
||||
|
required init(coder aDecoder: NSCoder) { |
||||
|
fatalError("MainView init(coder:) has not been implemented") |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func viewDidLayoutSubviews() { |
||||
|
coordinate() |
||||
|
} |
||||
|
|
||||
|
func coordinate() { |
||||
|
timelineView.coordinate() |
||||
|
} |
||||
|
|
||||
|
func new(asset newAsset:AVAsset?) { |
||||
|
if let new = newAsset { |
||||
|
asset = new |
||||
|
duration = CMTimeGetSeconds(new.duration) |
||||
|
player = AVPlayer(playerItem: AVPlayerItem(asset: asset!)) |
||||
|
audioPlayer = AVPlayer(playerItem: AVPlayerItem(asset: asset!)) |
||||
|
audioPlayer.volume = 1.0 |
||||
|
audioPlayer2 = AVPlayer(playerItem: AVPlayerItem(asset: asset!)) |
||||
|
audioPlayer2.volume = 1.0 |
||||
|
timelineView.newMovieSet() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
func setTrim(start:Float64, end:Float64, seek:Float64?, animate:Bool) { |
||||
|
|
||||
|
var seekTime = currentTime |
||||
|
if let time = seek { |
||||
|
seekTime = time |
||||
|
} |
||||
|
if animate { |
||||
|
timelineView.setTrimWithAnimation(trim:VideoTimelineTrim(start:start, end:end), time:seekTime) |
||||
|
} else { |
||||
|
timelineView.setTrim(start:start, end:end) |
||||
|
if seek != nil { |
||||
|
moveTo(seek!, animate:animate) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func setTrimIsEnabled(_ enabled:Bool) { |
||||
|
trimEnabled = enabled |
||||
|
timelineView.setTrimmerStatus(enabled:enabled) |
||||
|
} |
||||
|
|
||||
|
func setTrimmerIsHidden(_ hide:Bool) { |
||||
|
timelineView.setTrimmerVisible(!hide) |
||||
|
} |
||||
|
|
||||
|
func currentTrim() -> (start:Float64, end:Float64) { |
||||
|
let trim = timelineView.currentTrim() |
||||
|
return (trim.start,trim.end) |
||||
|
} |
||||
|
|
||||
|
func moveTo(_ time:Float64, animate:Bool) { |
||||
|
if animate { |
||||
|
|
||||
|
} else { |
||||
|
accurateSeek(time, scrub:false) |
||||
|
timelineView.setCurrentTime(time, force:true) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//MARK: - seeking |
||||
|
var previousSeektime:Float64 = 0 |
||||
|
func timelineIsMoved(_ currentTime:Float64, scrub:Bool) { |
||||
|
let move = abs(currentTime - previousSeektime) |
||||
|
let seekTolerance = CMTimeMakeWithSeconds(move, preferredTimescale:100) |
||||
|
|
||||
|
if player != nil { |
||||
|
player!.seek(to:CMTimeMakeWithSeconds(currentTime , preferredTimescale:100), toleranceBefore:seekTolerance,toleranceAfter:seekTolerance) |
||||
|
} |
||||
|
previousSeektime = currentTime |
||||
|
if scrub { |
||||
|
audioScrub() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func accurateSeek(_ currentTime:Float64, scrub:Bool) { |
||||
|
previousSeektime = currentTime |
||||
|
timelineIsMoved(currentTime, scrub:scrub) |
||||
|
} |
||||
|
|
||||
|
var scrubed1 = Date() |
||||
|
var scrubed2 = Date() |
||||
|
var canScrub1 = true |
||||
|
var canScrub2 = true |
||||
|
func audioScrub() { |
||||
|
if player == nil { |
||||
|
return |
||||
|
} |
||||
|
if scrubed2.timeIntervalSinceNow < -0.16 && canScrub1 { |
||||
|
canScrub1 = false |
||||
|
self.scrubed1 = Date() |
||||
|
DispatchQueue.main.async { |
||||
|
if self.audioPlayer.timeControlStatus == .playing { |
||||
|
self.audioPlayer.pause() |
||||
|
self.canScrub1 = true |
||||
|
} else { |
||||
|
self.audioPlayer.seek(to: self.player!.currentTime()) |
||||
|
self.audioPlayer.play() |
||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.22) { |
||||
|
self.audioPlayer.pause() |
||||
|
self.audioPlayer.seek(to: self.player!.currentTime()) |
||||
|
self.canScrub1 = true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
if scrubed1.timeIntervalSinceNow < -0.16 && canScrub2 { |
||||
|
canScrub2 = false |
||||
|
self.scrubed2 = Date() |
||||
|
DispatchQueue.main.async { |
||||
|
if self.audioPlayer2.timeControlStatus == .playing { |
||||
|
self.audioPlayer2.pause() |
||||
|
self.canScrub2 = true |
||||
|
} else { |
||||
|
self.audioPlayer2.seek(to: self.player!.currentTime()) |
||||
|
self.audioPlayer2.play() |
||||
|
|
||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.22) { |
||||
|
self.audioPlayer2.pause() |
||||
|
self.audioPlayer2.seek(to: self.player!.currentTime()) |
||||
|
self.canScrub2 = true |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//MARK: - play |
||||
|
var playerTimer = Timer() |
||||
|
@objc dynamic var playing = false |
||||
|
func play() { |
||||
|
if asset == nil { |
||||
|
return |
||||
|
} |
||||
|
let currentTime = timelineView.centerLine.currentTime |
||||
|
let reached = timeReachesEnd(currentTime) |
||||
|
|
||||
|
if reached.trimEnd { |
||||
|
accurateSeek(timelineView.currentTrim().start, scrub:false) |
||||
|
timelineView.manualScrolledAfterEnd = false |
||||
|
} else if reached.movieEnd { |
||||
|
accurateSeek(0, scrub:false) |
||||
|
timelineView.manualScrolledAfterEnd = false |
||||
|
} |
||||
|
if player != nil { |
||||
|
player!.play() |
||||
|
} |
||||
|
playerTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.playerTimerAction(_:)), userInfo: nil, repeats: true) |
||||
|
RunLoop.main.add(playerTimer, forMode:RunLoop.Mode.common) |
||||
|
playing = true |
||||
|
} |
||||
|
|
||||
|
func stop() { |
||||
|
playing = false |
||||
|
if asset == nil || player == nil { |
||||
|
return |
||||
|
} |
||||
|
player!.pause() |
||||
|
playerTimer.invalidate() |
||||
|
|
||||
|
if let receiver = playStatusReceiver { |
||||
|
receiver.videoTimelineStopped() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var reachFlg = false |
||||
|
@objc func playerTimerAction(_ timer:Timer) { |
||||
|
if player == nil { |
||||
|
return |
||||
|
} |
||||
|
var currentPlayerTime = CMTimeGetSeconds(player!.currentTime()) |
||||
|
|
||||
|
let trim = timelineView.currentTrim() |
||||
|
let reached = timeReachesEnd(currentPlayerTime) |
||||
|
if timelineView.inAction() { |
||||
|
if player!.timeControlStatus == .playing { |
||||
|
player!.pause() |
||||
|
} |
||||
|
} else if reached.reached { |
||||
|
if repeatOn && reached.trimEnd { |
||||
|
|
||||
|
if player!.timeControlStatus == .playing { |
||||
|
player!.pause() |
||||
|
} |
||||
|
currentPlayerTime = trim.start |
||||
|
accurateSeek(currentPlayerTime, scrub:false) |
||||
|
reachFlg = true |
||||
|
|
||||
|
} else { |
||||
|
stop() |
||||
|
} |
||||
|
timelineView.setCurrentTime(currentPlayerTime,force:false) |
||||
|
timelineView.manualScrolledAfterEnd = false |
||||
|
} else if timelineView.animating == false { |
||||
|
timelineView.setCurrentTime(currentPlayerTime,force:false) |
||||
|
if player!.timeControlStatus == .paused { |
||||
|
player!.play() |
||||
|
} |
||||
|
if reachFlg { |
||||
|
if let receiver = playStatusReceiver { |
||||
|
receiver.videoTimelineMoved() |
||||
|
} |
||||
|
reachFlg = false |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func timeReachesEnd(_ time:Float64) -> (reached:Bool, trimEnd:Bool, movieEnd:Bool) { |
||||
|
var reached = false |
||||
|
var trimEnd = false |
||||
|
var movieEnd = false |
||||
|
if asset != nil { |
||||
|
let duration = CMTimeGetSeconds(asset!.duration) |
||||
|
let trimTimeEnd = timelineView.currentTrim().end |
||||
|
if (time >= trimTimeEnd && timelineView.manualScrolledAfterEnd == false && trimEnabled) { |
||||
|
trimEnd = true |
||||
|
reached = true |
||||
|
} |
||||
|
if time >= duration { |
||||
|
if trimTimeEnd < duration { |
||||
|
trimEnd = false |
||||
|
} |
||||
|
movieEnd = true |
||||
|
reached = true |
||||
|
} |
||||
|
} |
||||
|
return (reached, trimEnd, movieEnd) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
//MARK: - |
||||
|
func resizeHeightKeepRatio(_ size:CGSize, height:CGFloat) -> CGSize { |
||||
|
var result = size |
||||
|
let ratio = size.width / size.height |
||||
|
result.height = height |
||||
|
result.width = height * ratio |
||||
|
return result |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue