This commit is contained in:
DDIsFriend
2023-08-18 17:28:57 +08:00
commit f0e8a1709d
4282 changed files with 192396 additions and 0 deletions

View File

@@ -0,0 +1,679 @@
//
// ZLEditVideoViewController.swift
// ZLPhotoBrowser
//
// Created by long on 2020/8/30.
//
// Copyright (c) 2020 Long Zhang <495181165@qq.com>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import UIKit
import Photos
public class ZLEditVideoViewController: UIViewController {
private static let frameImageSize = CGSize(width: CGFloat(round(50.0 * 2.0 / 3.0)), height: 50.0)
private let avAsset: AVAsset
private let animateDismiss: Bool
private lazy var cancelBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle(localLanguageTextValue(.cancel), for: .normal)
btn.setTitleColor(.zl.bottomToolViewBtnNormalTitleColor, for: .normal)
btn.titleLabel?.font = ZLLayout.bottomToolTitleFont
btn.addTarget(self, action: #selector(cancelBtnClick), for: .touchUpInside)
return btn
}()
private lazy var doneBtn: UIButton = {
let btn = UIButton(type: .custom)
btn.setTitle(localLanguageTextValue(.done), for: .normal)
btn.setTitleColor(.zl.bottomToolViewBtnNormalTitleColor, for: .normal)
btn.titleLabel?.font = ZLLayout.bottomToolTitleFont
btn.addTarget(self, action: #selector(doneBtnClick), for: .touchUpInside)
btn.backgroundColor = .zl.bottomToolViewBtnNormalBgColor
btn.layer.masksToBounds = true
btn.layer.cornerRadius = ZLLayout.bottomToolBtnCornerRadius
return btn
}()
private var timer: Timer?
private lazy var playerLayer: AVPlayerLayer = {
let layer = AVPlayerLayer()
layer.videoGravity = .resizeAspect
return layer
}()
private lazy var collectionView: UICollectionView = {
let layout = ZLCollectionViewFlowLayout()
layout.itemSize = ZLEditVideoViewController.frameImageSize
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
layout.scrollDirection = .horizontal
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.backgroundColor = .clear
view.delegate = self
view.dataSource = self
view.showsHorizontalScrollIndicator = false
ZLEditVideoFrameImageCell.zl.register(view)
return view
}()
private lazy var frameImageBorderView: ZLEditVideoFrameImageBorderView = {
let view = ZLEditVideoFrameImageBorderView()
view.isUserInteractionEnabled = false
return view
}()
private lazy var leftSideView: UIImageView = {
let view = UIImageView(image: .zl.getImage("zl_ic_left"))
view.isUserInteractionEnabled = true
return view
}()
private lazy var rightSideView: UIImageView = {
let view = UIImageView(image: .zl.getImage("zl_ic_right"))
view.isUserInteractionEnabled = true
return view
}()
private lazy var leftSidePan: UIPanGestureRecognizer = {
let pan = UIPanGestureRecognizer(target: self, action: #selector(leftSidePanAction(_:)))
pan.delegate = self
return pan
}()
private lazy var rightSidePan: UIPanGestureRecognizer = {
let pan = UIPanGestureRecognizer(target: self, action: #selector(rightSidePanAction(_:)))
pan.delegate = self
return pan
}()
private lazy var indicator: UIView = {
let view = UIView()
view.backgroundColor = UIColor.white.withAlphaComponent(0.7)
return view
}()
private var measureCount = 0
private lazy var interval: TimeInterval = {
let assetDuration = round(self.avAsset.duration.seconds)
return min(assetDuration, TimeInterval(ZLPhotoConfiguration.default().maxEditVideoTime)) / 10
}()
private lazy var requestFrameImageQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 10
return queue
}()
private lazy var avAssetRequestID = PHInvalidImageRequestID
private lazy var videoRequestID = PHInvalidImageRequestID
private var frameImageCache: [Int: UIImage] = [:]
private var requestFailedFrameImageIndex: [Int] = []
private var shouldLayout = true
private lazy var generator: AVAssetImageGenerator = {
let g = AVAssetImageGenerator(asset: self.avAsset)
g.maximumSize = CGSize(width: ZLEditVideoViewController.frameImageSize.width * 3, height: ZLEditVideoViewController.frameImageSize.height * 3)
g.appliesPreferredTrackTransform = true
g.requestedTimeToleranceBefore = .zero
g.requestedTimeToleranceAfter = .zero
g.apertureMode = .productionAperture
return g
}()
@objc public var editFinishBlock: ((URL?) -> Void)?
override public var prefersStatusBarHidden: Bool {
return true
}
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
deinit {
zl_debugPrint("ZLEditVideoViewController deinit")
cleanTimer()
requestFrameImageQueue.cancelAllOperations()
if avAssetRequestID > PHInvalidImageRequestID {
PHImageManager.default().cancelImageRequest(avAssetRequestID)
}
if videoRequestID > PHInvalidImageRequestID {
PHImageManager.default().cancelImageRequest(videoRequestID)
}
}
/// initialize
/// - Parameters:
/// - avAsset: AVAsset
/// - animateDismiss: 退dismiss
@objc public init(avAsset: AVAsset, animateDismiss: Bool = false) {
self.avAsset = avAsset
self.animateDismiss = animateDismiss
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func viewDidLoad() {
super.viewDidLoad()
setupUI()
NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive), name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
analysisAssetImages()
}
override public func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard shouldLayout else {
return
}
shouldLayout = false
zl_debugPrint("edit video layout subviews")
var insets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
if #available(iOS 11.0, *) {
insets = self.view.safeAreaInsets
}
let btnH = ZLLayout.bottomToolBtnH
let bottomBtnAndColSpacing: CGFloat = 20
let playerLayerY = insets.top + 20
let diffBottom = btnH + ZLEditVideoViewController.frameImageSize.height + bottomBtnAndColSpacing + insets.bottom + 30
playerLayer.frame = CGRect(x: 15, y: insets.top + 20, width: view.bounds.width - 30, height: view.bounds.height - playerLayerY - diffBottom)
let cancelBtnW = localLanguageTextValue(.cancel).zl.boundingRect(font: ZLLayout.bottomToolTitleFont, limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: btnH)).width
cancelBtn.frame = CGRect(x: 20, y: view.bounds.height - insets.bottom - btnH, width: cancelBtnW, height: btnH)
let doneBtnW = localLanguageTextValue(.done).zl.boundingRect(font: ZLLayout.bottomToolTitleFont, limitSize: CGSize(width: CGFloat.greatestFiniteMagnitude, height: btnH)).width + 20
doneBtn.frame = CGRect(x: view.bounds.width - doneBtnW - 20, y: view.bounds.height - insets.bottom - btnH, width: doneBtnW, height: btnH)
collectionView.frame = CGRect(x: 0, y: doneBtn.frame.minY - bottomBtnAndColSpacing - ZLEditVideoViewController.frameImageSize.height, width: view.bounds.width, height: ZLEditVideoViewController.frameImageSize.height)
let frameViewW = ZLEditVideoViewController.frameImageSize.width * 10
frameImageBorderView.frame = CGRect(x: (view.bounds.width - frameViewW) / 2, y: collectionView.frame.minY, width: frameViewW, height: ZLEditVideoViewController.frameImageSize.height)
// view
let leftRightSideViewW = ZLEditVideoViewController.frameImageSize.width / 2
leftSideView.frame = CGRect(x: frameImageBorderView.frame.minX, y: collectionView.frame.minY, width: leftRightSideViewW, height: ZLEditVideoViewController.frameImageSize.height)
let rightSideViewX = view.bounds.width - frameImageBorderView.frame.minX - leftRightSideViewW
rightSideView.frame = CGRect(x: rightSideViewX, y: collectionView.frame.minY, width: leftRightSideViewW, height: ZLEditVideoViewController.frameImageSize.height)
frameImageBorderView.validRect = frameImageBorderView.convert(clipRect(), from: view)
}
private func setupUI() {
view.backgroundColor = .black
view.layer.addSublayer(playerLayer)
view.addSubview(collectionView)
view.addSubview(frameImageBorderView)
view.addSubview(indicator)
view.addSubview(leftSideView)
view.addSubview(rightSideView)
view.addGestureRecognizer(leftSidePan)
view.addGestureRecognizer(rightSidePan)
collectionView.panGestureRecognizer.require(toFail: leftSidePan)
collectionView.panGestureRecognizer.require(toFail: rightSidePan)
rightSidePan.require(toFail: leftSidePan)
view.addSubview(cancelBtn)
view.addSubview(doneBtn)
}
@objc private func cancelBtnClick() {
dismiss(animated: animateDismiss, completion: nil)
}
@objc private func doneBtnClick() {
cleanTimer()
let d = CGFloat(interval) * clipRect().width / ZLEditVideoViewController.frameImageSize.width
if ZLPhotoConfiguration.Second(round(d)) < ZLPhotoConfiguration.default().minSelectVideoDuration {
let message = String(format: localLanguageTextValue(.shorterThanMinVideoDuration), ZLPhotoConfiguration.default().minSelectVideoDuration)
showAlertView(message, self)
return
}
if ZLPhotoConfiguration.Second(round(d)) > ZLPhotoConfiguration.default().maxSelectVideoDuration {
let message = String(format: localLanguageTextValue(.longerThanMaxVideoDuration), ZLPhotoConfiguration.default().maxSelectVideoDuration)
showAlertView(message, self)
return
}
// Max deviation is 0.01
if abs(d - round(CGFloat(avAsset.duration.seconds))) <= 0.01 {
dismiss(animated: animateDismiss) {
self.editFinishBlock?(nil)
}
return
}
let hud = ZLProgressHUD.show()
ZLVideoManager.exportEditVideo(for: avAsset, range: getTimeRange()) { [weak self] url, error in
hud.hide()
if let er = error {
showAlertView(er.localizedDescription, self)
} else if url != nil {
self?.dismiss(animated: self?.animateDismiss ?? false) {
self?.editFinishBlock?(url)
}
}
}
}
@objc private func leftSidePanAction(_ pan: UIPanGestureRecognizer) {
let point = pan.location(in: view)
if pan.state == .began {
frameImageBorderView.layer.borderColor = UIColor(white: 1, alpha: 0.4).cgColor
cleanTimer()
} else if pan.state == .changed {
let minX = frameImageBorderView.frame.minX
let maxX = rightSideView.frame.minX - leftSideView.frame.width
var frame = leftSideView.frame
frame.origin.x = min(maxX, max(minX, point.x))
leftSideView.frame = frame
frameImageBorderView.validRect = frameImageBorderView.convert(clipRect(), from: view)
playerLayer.player?.seek(to: getStartTime(), toleranceBefore: .zero, toleranceAfter: .zero)
} else if pan.state == .ended || pan.state == .cancelled {
frameImageBorderView.layer.borderColor = UIColor.clear.cgColor
startTimer()
}
}
@objc private func rightSidePanAction(_ pan: UIPanGestureRecognizer) {
let point = pan.location(in: view)
if pan.state == .began {
frameImageBorderView.layer.borderColor = UIColor(white: 1, alpha: 0.4).cgColor
cleanTimer()
} else if pan.state == .changed {
let minX = leftSideView.frame.maxX
let maxX = frameImageBorderView.frame.maxX - rightSideView.frame.width
var frame = rightSideView.frame
frame.origin.x = min(maxX, max(minX, point.x))
rightSideView.frame = frame
frameImageBorderView.validRect = frameImageBorderView.convert(clipRect(), from: view)
playerLayer.player?.seek(to: getStartTime(), toleranceBefore: .zero, toleranceAfter: .zero)
} else if pan.state == .ended || pan.state == .cancelled {
frameImageBorderView.layer.borderColor = UIColor.clear.cgColor
startTimer()
}
}
@objc private func appWillResignActive() {
cleanTimer()
indicator.layer.removeAllAnimations()
}
@objc private func appDidBecomeActive() {
startTimer()
}
private func analysisAssetImages() {
let duration = round(avAsset.duration.seconds)
guard duration > 0 else {
showFetchFailedAlert()
return
}
let item = AVPlayerItem(asset: avAsset)
let player = AVPlayer(playerItem: item)
playerLayer.player = player
startTimer()
measureCount = Int(duration / interval)
collectionView.reloadData()
requestVideoMeasureFrameImage()
}
private func requestVideoMeasureFrameImage() {
for i in 0..<measureCount {
let mes = TimeInterval(i) * interval
let time = CMTimeMakeWithSeconds(Float64(mes), preferredTimescale: avAsset.duration.timescale)
let operation = ZLEditVideoFetchFrameImageOperation(generator: generator, time: time) { [weak self] image, _ in
self?.frameImageCache[Int(i)] = image
let cell = self?.collectionView.cellForItem(at: IndexPath(row: Int(i), section: 0)) as? ZLEditVideoFrameImageCell
cell?.imageView.image = image
if image == nil {
self?.requestFailedFrameImageIndex.append(i)
}
}
requestFrameImageQueue.addOperation(operation)
}
}
@objc private func playPartVideo() {
playerLayer.player?.seek(to: getStartTime(), toleranceBefore: .zero, toleranceAfter: .zero)
if (playerLayer.player?.rate ?? 0) == 0 {
playerLayer.player?.play()
}
}
private func startTimer() {
cleanTimer()
let duration = interval * TimeInterval(clipRect().width / ZLEditVideoViewController.frameImageSize.width)
timer = Timer.scheduledTimer(timeInterval: duration, target: ZLWeakProxy(target: self), selector: #selector(playPartVideo), userInfo: nil, repeats: true)
timer?.fire()
RunLoop.main.add(timer!, forMode: .common)
indicator.isHidden = false
let indicatorW: CGFloat = 2
let indicatorH = leftSideView.zl.height
let indicatorY = leftSideView.zl.top
var indicatorFromX = leftSideView.zl.left
var indicatorToX = rightSideView.zl.right - indicatorW
if isRTL() {
swap(&indicatorFromX, &indicatorToX)
}
let fromFrame = CGRect(x: indicatorFromX, y: indicatorY, width: indicatorW, height: indicatorH)
indicator.frame = fromFrame
var toFrame = fromFrame
toFrame.origin.x = indicatorToX
indicator.layer.removeAllAnimations()
UIView.animate(withDuration: duration, delay: 0, options: [.allowUserInteraction, .curveLinear, .repeat], animations: {
self.indicator.frame = toFrame
}, completion: nil)
}
private func cleanTimer() {
timer?.invalidate()
timer = nil
indicator.layer.removeAllAnimations()
indicator.isHidden = true
playerLayer.player?.pause()
}
private func getStartTime() -> CMTime {
var rect = collectionView.convert(clipRect(), from: view)
rect.origin.x -= frameImageBorderView.frame.minX
let second = max(0, CGFloat(interval) * rect.minX / ZLEditVideoViewController.frameImageSize.width)
return CMTimeMakeWithSeconds(Float64(second), preferredTimescale: avAsset.duration.timescale)
}
private func getTimeRange() -> CMTimeRange {
let start = getStartTime()
let d = CGFloat(interval) * clipRect().width / ZLEditVideoViewController.frameImageSize.width
let duration = CMTimeMakeWithSeconds(Float64(d), preferredTimescale: avAsset.duration.timescale)
return CMTimeRangeMake(start: start, duration: duration)
}
private func clipRect() -> CGRect {
var frame = CGRect.zero
frame.origin.x = leftSideView.frame.minX
frame.origin.y = leftSideView.frame.minY
frame.size.width = rightSideView.frame.maxX - frame.minX
frame.size.height = leftSideView.frame.height
return frame
}
private func showFetchFailedAlert() {
let action = ZLCustomAlertAction(title: localLanguageTextValue(.ok), style: .default) { [weak self] _ in
self?.dismiss(animated: false)
}
showAlertController(title: nil, message: localLanguageTextValue(.iCloudVideoLoadFaild), style: .alert, actions: [action], sender: self)
}
}
extension ZLEditVideoViewController: UIGestureRecognizerDelegate {
public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer == leftSidePan {
let point = gestureRecognizer.location(in: view)
let frame = leftSideView.frame
let outerFrame = frame.inset(by: UIEdgeInsets(top: -20, left: -40, bottom: -20, right: -20))
return outerFrame.contains(point)
} else if gestureRecognizer == rightSidePan {
let point = gestureRecognizer.location(in: view)
let frame = rightSideView.frame
let outerFrame = frame.inset(by: UIEdgeInsets(top: -20, left: -20, bottom: -20, right: -40))
return outerFrame.contains(point)
}
return true
}
}
extension ZLEditVideoViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
cleanTimer()
playerLayer.player?.seek(to: getStartTime(), toleranceBefore: .zero, toleranceAfter: .zero)
}
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
startTimer()
}
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
startTimer()
}
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
let w = ZLEditVideoViewController.frameImageSize.width * 10
let leftRight = (collectionView.frame.width - w) / 2
return UIEdgeInsets(top: 0, left: leftRight, bottom: 0, right: leftRight)
}
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return measureCount
}
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ZLEditVideoFrameImageCell.zl.identifier, for: indexPath) as! ZLEditVideoFrameImageCell
if let image = frameImageCache[indexPath.row] {
cell.imageView.image = image
}
return cell
}
public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if requestFailedFrameImageIndex.contains(indexPath.row) {
let mes = TimeInterval(indexPath.row) * interval
let time = CMTimeMakeWithSeconds(Float64(mes), preferredTimescale: avAsset.duration.timescale)
let operation = ZLEditVideoFetchFrameImageOperation(generator: generator, time: time) { [weak self] image, _ in
self?.frameImageCache[indexPath.row] = image
let cell = self?.collectionView.cellForItem(at: IndexPath(row: indexPath.row, section: 0)) as? ZLEditVideoFrameImageCell
cell?.imageView.image = image
if image != nil {
self?.requestFailedFrameImageIndex.removeAll { $0 == indexPath.row }
}
}
requestFrameImageQueue.addOperation(operation)
}
}
}
class ZLEditVideoFrameImageBorderView: UIView {
var validRect: CGRect = .zero {
didSet {
self.setNeedsDisplay()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
layer.borderWidth = 2
layer.borderColor = UIColor.clear.cgColor
backgroundColor = .clear
isOpaque = false
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
context?.setStrokeColor(UIColor.white.cgColor)
context?.setLineWidth(4)
context?.move(to: CGPoint(x: validRect.minX, y: 0))
context?.addLine(to: CGPoint(x: validRect.minX + validRect.width, y: 0))
context?.move(to: CGPoint(x: validRect.minX, y: rect.height))
context?.addLine(to: CGPoint(x: validRect.minX + validRect.width, y: rect.height))
context?.strokePath()
}
}
class ZLEditVideoFrameImageCell: UICollectionViewCell {
lazy var imageView: UIImageView = {
let view = UIImageView()
view.contentMode = .scaleAspectFill
view.clipsToBounds = true
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(imageView)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
imageView.frame = bounds
}
}
class ZLEditVideoFetchFrameImageOperation: Operation {
private let generator: AVAssetImageGenerator
private let time: CMTime
let completion: (UIImage?, CMTime) -> Void
var pri_isExecuting = false {
willSet {
self.willChangeValue(forKey: "isExecuting")
}
didSet {
self.didChangeValue(forKey: "isExecuting")
}
}
override var isExecuting: Bool {
return pri_isExecuting
}
var pri_isFinished = false {
willSet {
self.willChangeValue(forKey: "isFinished")
}
didSet {
self.didChangeValue(forKey: "isFinished")
}
}
override var isFinished: Bool {
return pri_isFinished
}
var pri_isCancelled = false {
willSet {
self.willChangeValue(forKey: "isCancelled")
}
didSet {
self.didChangeValue(forKey: "isCancelled")
}
}
override var isCancelled: Bool {
return pri_isCancelled
}
init(generator: AVAssetImageGenerator, time: CMTime, completion: @escaping ((UIImage?, CMTime) -> Void)) {
self.generator = generator
self.time = time
self.completion = completion
super.init()
}
override func start() {
if isCancelled {
fetchFinish()
return
}
pri_isExecuting = true
generator.generateCGImagesAsynchronously(forTimes: [NSValue(time: time)]) { _, cgImage, _, result, _ in
if result == .succeeded, let cg = cgImage {
let image = UIImage(cgImage: cg)
ZLMainAsync {
self.completion(image, self.time)
}
self.fetchFinish()
} else {
self.fetchFinish()
}
}
}
override func cancel() {
super.cancel()
pri_isCancelled = true
}
private func fetchFinish() {
pri_isExecuting = false
pri_isFinished = true
}
}