357 lines
13 KiB
Swift
357 lines
13 KiB
Swift
//
|
||
// ViewController.swift
|
||
// SimpleVideoEditor
|
||
//
|
||
// Created by RenZhu Macro on 2020/7/2.
|
||
// Copyright © 2020 RenZhu Macro. All rights reserved.
|
||
//
|
||
|
||
import UIKit
|
||
import AVFoundation
|
||
import MobileCoreServices
|
||
import MetalVideoProcess
|
||
|
||
class ViewController: UIViewController {
|
||
|
||
@IBOutlet weak var renderView: MetalVideoProcessRenderView!
|
||
@IBOutlet weak var mainEditButton: UIButton!
|
||
@IBOutlet weak var mainDeleteButton: UIButton!
|
||
|
||
@IBOutlet weak var subEditButton: UIButton!
|
||
@IBOutlet weak var subDeleteButton: UIButton!
|
||
|
||
var mainPicker: UIImagePickerController?
|
||
var subPicker: UIImagePickerController?
|
||
|
||
var mainLayer: MetalVideoProcessBlendFilter?
|
||
var player: MetalVideoProcessPlayer?
|
||
var movieWriter: MetalVideoProcessMovieWriter?
|
||
var videoBackground: MetalVideoProcessBackground = MetalVideoProcessBackground(trackID: 0)
|
||
var pop: PopupDialog?
|
||
var progressHUD: MBProgressHUD?
|
||
|
||
var mainResources: [ResourceItem] = []
|
||
var lastMainLayerItem: ResourceItem?
|
||
|
||
@IBOutlet weak var mainTableView: UITableView!
|
||
var mainDragger: TableViewDragger?
|
||
var videoEditor: MetalVideoEditor?
|
||
var mainSelectedItem: ResourceItem?
|
||
|
||
var subResources: [ResourceItem] = []
|
||
@IBOutlet weak var subTableView: UITableView!
|
||
var subDragger: TableViewDragger?
|
||
var subSelectedItem: ResourceItem?
|
||
|
||
@IBOutlet weak var progress: UISlider!
|
||
|
||
private var rotGes: UIRotationGestureRecognizer?
|
||
private var pinchGes: UIPinchGestureRecognizer?
|
||
var currentPostion: Position = Position(0.0, 0.0)
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
self.navigationController?.setNavigationBarHidden(true, animated: false)
|
||
|
||
|
||
self.mainTableView.register(ResourceItemTableViewCell.self, forCellReuseIdentifier: "mainCell")
|
||
self.mainTableView.allowsSelection = true
|
||
self.mainTableView.dataSource = self
|
||
self.mainTableView.delegate = self
|
||
|
||
self.subTableView.register(ResourceItemTableViewCell.self, forCellReuseIdentifier: "pipCell")
|
||
self.subTableView.allowsSelection = true
|
||
self.subTableView.dataSource = self
|
||
self.subTableView.delegate = self
|
||
|
||
self.mainDragger = TableViewDragger(tableView: self.mainTableView)
|
||
self.mainDragger?.delegate = self
|
||
self.subDragger = TableViewDragger(tableView: self.subTableView)
|
||
self.subDragger?.delegate = self
|
||
|
||
let asset1 = AVAsset(url: Bundle.main.url(forResource: "cute", withExtension: "mp4")!)
|
||
let asset2 = AVAsset(url: Bundle.main.url(forResource: "movie", withExtension: "mov")!)
|
||
let sampleMainItem = ResourceItem(asset: asset1)
|
||
let sampleSubItem = ResourceItem(asset: asset2)
|
||
self.lastMainLayerItem = sampleMainItem
|
||
|
||
|
||
let backgroundFrame = self.renderView.frame
|
||
let backgroundSize = CGSize(width: backgroundFrame.size.width * 2.0,
|
||
height: backgroundFrame.size.height * 2.0)
|
||
MetalVideoProcessBackground.canvasSize = backgroundSize
|
||
|
||
videoBackground.canvasSizeType = .Type720p
|
||
videoBackground.aspectRatioType = .Ratio16_9
|
||
videoBackground.setBackgroundType(type: .Blur)
|
||
|
||
self.mainResources.append(sampleMainItem)
|
||
self.subResources.append(sampleSubItem)
|
||
|
||
|
||
|
||
self.progress.minimumValue = 0.0
|
||
|
||
sampleMainItem.fillType = .aspectToFit
|
||
sampleMainItem.roi = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)
|
||
sampleSubItem.fillType = .aspectToFit
|
||
sampleSubItem.roi = CGRect(x: 0.5, y: 0.5, width: 0.5, height: 0.5)
|
||
self.rebuildPipeline()
|
||
self.mainTableView.reloadData()
|
||
self.subTableView.reloadData()
|
||
|
||
self.renderView.isUserInteractionEnabled = true
|
||
|
||
rotGes = UIRotationGestureRecognizer(target: self,
|
||
action: #selector(self.rotateAction))
|
||
rotGes!.delegate = self;
|
||
pinchGes = UIPinchGestureRecognizer(target: self, action: #selector(self.pinchAction))
|
||
pinchGes!.delegate = self;
|
||
let panGes = UIPanGestureRecognizer(target: self, action: #selector(self.panAction))
|
||
panGes.delegate = self
|
||
self.renderView.addGestureRecognizer(rotGes!)
|
||
self.renderView.addGestureRecognizer(pinchGes!)
|
||
self.renderView.addGestureRecognizer(panGes)
|
||
}
|
||
|
||
@IBAction func changeFillType(_ sender: UIButton) {
|
||
let tag = sender.tag
|
||
switch tag {
|
||
case 0:
|
||
self.videoBackground.aspectRatioType = .Ratio16_9
|
||
break
|
||
case 1:
|
||
self.videoBackground.aspectRatioType = .Ratio9_16
|
||
case 2:
|
||
self.videoBackground.aspectRatioType = .Ratio1_1
|
||
case 3:
|
||
self.videoBackground.aspectRatioType = .Ratio4_3
|
||
case 4:
|
||
self.videoBackground.aspectRatioType = .Ratio3_4
|
||
default:
|
||
break
|
||
}
|
||
}
|
||
|
||
@IBAction func play(_ sender: Any) {
|
||
self.player?.play()
|
||
}
|
||
|
||
@IBAction func pause(_ sender: Any) {
|
||
self.player?.pause()
|
||
}
|
||
|
||
|
||
@IBAction func importMain(_ sender: Any) {
|
||
let picker = UIImagePickerController()
|
||
|
||
picker.delegate = self
|
||
picker.sourceType = .photoLibrary
|
||
|
||
var pickerTypes: [String] = []
|
||
|
||
pickerTypes.append(kUTTypeMovie as String)
|
||
picker.mediaTypes = pickerTypes
|
||
|
||
picker.allowsEditing = true
|
||
self.navigationController?.present(picker, animated: true, completion: nil)
|
||
self.mainPicker = picker
|
||
}
|
||
|
||
@IBAction func importPIP(_ sender: Any) {
|
||
let picker = UIImagePickerController()
|
||
|
||
picker.delegate = self
|
||
picker.sourceType = .photoLibrary
|
||
|
||
var pickerTypes: [String] = []
|
||
|
||
pickerTypes.append(kUTTypeMovie as String)
|
||
picker.mediaTypes = pickerTypes
|
||
|
||
picker.allowsEditing = true
|
||
self.navigationController?.present(picker, animated: true, completion: nil)
|
||
self.subPicker = picker
|
||
}
|
||
|
||
@IBAction func progressChanged(_ sender: UISlider) {
|
||
let value = sender.value
|
||
self.player?.seekTo(time: Float64(value))
|
||
}
|
||
|
||
@IBAction func editItem(_ sender: UIButton) {
|
||
|
||
let resourceEditView = ResourceItemEditView(nibName: "ResourceItemEditView", bundle: nil)
|
||
self.pop = PopupDialog(viewController: resourceEditView, buttonAlignment: .horizontal, transitionStyle: .bounceDown, tapGestureDismissal: true, panGestureDismissal: false)
|
||
|
||
let btnCancel = CancelButton(title: "Cancel", height: 60) {
|
||
self.pop?.viewController.removeFromParent()
|
||
self.pop?.removeFromParent()
|
||
self.pop = nil
|
||
return
|
||
}
|
||
|
||
let btnOK = DefaultButton(title: "OK", height: 60) {
|
||
self.rebuildPipeline()
|
||
self.mainTableView.reloadData()
|
||
self.subTableView.reloadData()
|
||
self.pop?.viewController.removeFromParent()
|
||
self.pop?.removeFromParent()
|
||
self.pop = nil
|
||
return
|
||
}
|
||
|
||
pop?.addButtons([btnOK, btnCancel])
|
||
if sender == self.mainEditButton {
|
||
guard let item = self.mainSelectedItem else {
|
||
return
|
||
}
|
||
self.present(pop!
|
||
, animated: true) {
|
||
resourceEditView.loadResourceItem(item)
|
||
}
|
||
} else {
|
||
guard let item = self.subSelectedItem else {
|
||
return
|
||
}
|
||
self.present(pop!
|
||
, animated: true) {
|
||
resourceEditView.loadResourceItem(item, isPipItem: true)
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
@IBAction func deleteItem(_ sender: UIButton) {
|
||
|
||
if sender == self.mainDeleteButton {
|
||
self.mainResources.removeAll { $0.trackID == self.mainSelectedItem?.trackID ?? 0 }
|
||
} else {
|
||
self.subResources.removeAll { $0.trackID == self.subSelectedItem?.trackID ?? 0 }
|
||
}
|
||
self.rebuildPipeline()
|
||
self.mainTableView.reloadData()
|
||
self.subTableView.reloadData()
|
||
self.subEditButton.isEnabled = self.subResources.count > 0
|
||
self.subDeleteButton.isEnabled = self.subResources.count > 0
|
||
|
||
self.mainEditButton.isEnabled = self.mainResources.count > 0
|
||
self.mainDeleteButton.isEnabled = self.mainResources.count > 0
|
||
}
|
||
|
||
func rebuildPipeline() {
|
||
|
||
self.videoEditor = nil
|
||
|
||
//rebuild player
|
||
self.player?.suspend()
|
||
self.player?.dispose()
|
||
self.player?.removeAllTargets()
|
||
self.player = nil
|
||
|
||
//clear all sources
|
||
renderView.sources.sources.removeAll()
|
||
|
||
do {
|
||
let currentEditor = try MetalVideoEditor(videoItems: self.mainResources, overlayItems: self.subResources)
|
||
self.videoEditor = currentEditor
|
||
try currentEditor.updateChannel()
|
||
print("before build PlayerItem mainItems:", currentEditor.editorItems)
|
||
print("before build PlayerItem overlayItems:", currentEditor.overlayItems)
|
||
let playerItem = currentEditor.buildPlayerItem()
|
||
print("after build PlayerItem mainItems:", currentEditor.editorItems)
|
||
print("after build PlayerItem overlayItems:", currentEditor.overlayItems)
|
||
|
||
|
||
|
||
self.progress.maximumValue = Float(playerItem.duration.seconds)
|
||
let player = try MetalVideoProcessPlayer(playerItem: playerItem)
|
||
|
||
let mainLayer = MetalVideoProcessBlendFilter()
|
||
mainLayer.debugName = "mainLayer"
|
||
var lastTransform: MetalVideoProcessTransformFilter? = nil
|
||
for mainItem in self.mainResources {
|
||
let transform = MetalVideoProcessTransformFilter()
|
||
transform.saveUniformSettings(forTimelineRange: mainItem.timeRange, trackID: mainItem.trackID)
|
||
transform.roi = mainItem.roi
|
||
transform.debugName = "main transform trackId:\(mainItem.trackID)"
|
||
//render blur background with every main track
|
||
player.addTarget(videoBackground, atTargetIndex: nil, trackID: mainItem.trackID, targetTrackId: 0)
|
||
|
||
mainItem.transformFilter = transform
|
||
transform.rotate = mainItem.rotate
|
||
transform.translation = mainItem.translation
|
||
transform.scale = mainItem.scale
|
||
|
||
|
||
mainItem.currentLayer = mainLayer
|
||
if let lt = lastTransform {
|
||
//link every main transform in mainLayer
|
||
lt --> transform
|
||
}
|
||
lastTransform = transform
|
||
}
|
||
|
||
// make sure mapping every item‘s trackID on main track to main trackContainer with trackID 0,
|
||
// if you don't do this, player can't render every item's texutre to item's target
|
||
if let firstTransform = self.mainResources.first?.transformFilter {
|
||
self.mainResources.forEach { (item) in
|
||
player.addTarget(firstTransform, atTargetIndex: nil, trackID: item.trackID, targetTrackId: 0)
|
||
}
|
||
}
|
||
|
||
|
||
guard let lt = lastTransform else {
|
||
// zero items
|
||
self.player?.suspend()
|
||
self.player?.dispose()
|
||
self.player?.removeAllTargets()
|
||
self.player = nil
|
||
return
|
||
}
|
||
|
||
videoBackground --> mainLayer
|
||
// place lt on videoBackground, secondary source to mainLayer
|
||
lt --> mainLayer
|
||
|
||
var lastLayer: MetalVideoProcessBlendFilter = mainLayer
|
||
|
||
for subItem in self.subResources {
|
||
let pipLayer = MetalVideoProcessBlendFilter()
|
||
|
||
// render pipLayer in subItem.timeRange
|
||
pipLayer.saveUniformSettings(forTimelineRange: subItem.timeRange, trackID: subItem.trackID)
|
||
|
||
let transform = MetalVideoProcessTransformFilter()
|
||
// transform subItem's texture in subItem.timeRange
|
||
transform.saveUniformSettings(forTimelineRange: subItem.timeRange, trackID: subItem.trackID)
|
||
transform.roi = subItem.roi
|
||
|
||
// create a new targetContainer for subItem & pipLayer
|
||
player.addTarget(transform, atTargetIndex: nil, trackID: subItem.trackID, targetTrackId: subItem.trackID)
|
||
|
||
subItem.currentLayer = pipLayer
|
||
subItem.transformFilter = transform
|
||
transform.rotate = subItem.rotate
|
||
transform.translation = subItem.translation
|
||
transform.scale = subItem.scale
|
||
//compositing two layer (warning!!! place transform to secondary source to pipLayer)
|
||
lastLayer --> pipLayer
|
||
transform --> pipLayer
|
||
|
||
|
||
lastLayer = pipLayer
|
||
}
|
||
|
||
//render the final layer
|
||
lastLayer --> renderView
|
||
player.playerDelegate = self
|
||
self.player = player
|
||
|
||
} catch {
|
||
|
||
}
|
||
}
|
||
}
|
||
|