680 lines
26 KiB
Swift
680 lines
26 KiB
Swift
//
|
||
// 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
|
||
}
|
||
}
|