16 changed files with 84 additions and 2553 deletions
-
52kplayer.xcodeproj/project.pbxproj
-
68kplayer/core/DatabaseManager.swift
-
2kplayer/core/MediaItem.swift
-
1kplayer/core/MediaModel.swift
-
4kplayer/detail/DetailViewController.swift
-
15kplayer/master/MasterViewController.swift
-
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
-
0kplayer/video/SVideoModel.swift
-
0kplayer/video/SVideoPlayer.swift
-
0kplayer/video/VideoPlayerView.swift
@ -1,126 +0,0 @@ |
|||||
// |
|
||||
// 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 |
|
||||
} |
|
||||
} |
|
||||
@ -1,451 +0,0 @@ |
|||||
// |
|
||||
// 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 |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
@ -1,206 +0,0 @@ |
|||||
// |
|
||||
// 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() |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@ -1,178 +0,0 @@ |
|||||
// |
|
||||
// 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() |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@ -1,460 +0,0 @@ |
|||||
// |
|
||||
// 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 |
|
||||
} |
|
||||
|
|
||||
|
|
||||
} |
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
@ -1,773 +0,0 @@ |
|||||
// |
|
||||
// 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 |
|
||||
} |
|
||||
|
|
||||
|
|
||||
|
|
||||
} |
|
||||
@ -1,301 +0,0 @@ |
|||||
// |
|
||||
// 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