Я работаю над проектом, похожим на редактирование фото / видео в Instagram Story (с возможностью добавления стикеров и т. Д.).Мой первоначальный подход заключался в использовании
videoCompositionInstructions! .AnimationTool = AVVideoCompositionCoreAnimationTool (postProcessingAsVideoLayer: videoLayer, in: containerLayer)
, но я понял, что с этим методом связано много проблем.Во-первых, если входные данные представляют собой альбомное видео, я не могу восстановить цвет градиента фона - он становится полностью черным (https://imgur.com/a/wYpknE4). Не говоря уже о проблемах обрезки - если пользователь перемещает видео за границы, оно должно бытьобрезается, но с моим текущим подходом это было бы сложно. Кроме того, если я добавляю стикеры, мне нужно масштабировать x и y, чтобы соответствовать размеру рендеринга видео.
Что действительно будет лучшим подходомк этому? Конечно, есть более простой способ? Интуитивно, было бы разумно начать с представления контейнера, и пользователь может добавить к нему наклейки, видео и т. д., и было бы проще всего просто экспортировать представление контейнера с помощью clipsToBounds= true (не нужно масштабировать x / y, обрезать видео, создавать проблемы с альбомной ориентацией и т. д.).
Если кто-то работал над аналогичным проектом или имеет какие-либо входные данные, это будет оценено.
class AVFoundationClient {
var selectedVideoURL: URL?
var mutableComposition: AVMutableComposition?
var videoCompositionInstructions: AVMutableVideoComposition?
var videoTrack: AVMutableCompositionTrack?
var sourceAsset: AVURLAsset?
var insertTime = CMTime.zero
var sourceVideoAsset: AVAsset?
var sourceVideoTrack: AVAssetTrack?
var sourceRange: CMTimeRange?
var renderWidth: CGFloat?
var renderHeight: CGFloat?
var endTime: CMTime?
var videoBounds: CGRect?
var stickerLayers = [CALayer]()
func exportVideoFileFromStickersAndOriginalVideo(_ stickers: [Int:Sticker], sourceURL: URL) {
createNewMutableCompositionAndTrack()
getSourceAssetFromURL(sourceURL)
getVideoParamsAndAppendTracks()
createVideoCompositionInstructions()
for (_, sticker) in stickers {
createStickerLayer(sticker.image!, x: sticker.x!, y: sticker.y!, width: sticker.width!, height: sticker.height!, scale: sticker.scale!)
}
mergeStickerLayersAndFinalizeInstructions()
export(mutableComposition!)
}
func createStickerLayer(_ image: UIImage, x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat, scale: CGFloat) {
let scaleRatio = renderWidth!/UIScreen.main.bounds.width
let stickerX = x*scaleRatio
let stickerY = y*scaleRatio
let imageLayer = CALayer()
imageLayer.frame = CGRect(x: stickerX, y: stickerY, width: width*scaleRatio, height: height*scaleRatio)
imageLayer.contents = image.cgImage
imageLayer.contentsGravity = CALayerContentsGravity.resize
imageLayer.masksToBounds = true
stickerLayers.append(imageLayer)
}
func mergeStickerLayersAndFinalizeInstructions() {
let videoLayer = CALayer()
videoLayer.frame = CGRect(x: 0, y: 0, width: renderWidth!, height: renderWidth!*16/9)
videoLayer.contentsGravity = .resizeAspectFill
let containerLayer = CALayer()
containerLayer.backgroundColor = UIColor.mainBlue().cgColor
containerLayer.isGeometryFlipped = true
containerLayer.frame = CGRect(x: 0, y: 0, width: renderWidth!, height: renderWidth!*16/9)
containerLayer.addSublayer(videoLayer)
for stickerLayer in stickerLayers {
containerLayer.addSublayer(stickerLayer)
}
videoCompositionInstructions!.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: videoLayer, in: containerLayer)
}
func createNewMutableCompositionAndTrack() {
mutableComposition = AVMutableComposition()
videoTrack = mutableComposition!.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: CMPersistentTrackID())
}
func getSourceAssetFromURL(_ fileURL: URL) {
sourceAsset = AVURLAsset(url: fileURL, options: nil)
}
func getVideoParamsAndAppendTracks() {
let sourceDuration = CMTimeRangeMake(start: CMTime.zero, duration: sourceAsset!.duration)
sourceVideoTrack = sourceAsset!.tracks(withMediaType: AVMediaType.video)[0]
renderWidth = sourceVideoTrack!.renderSize().width
renderHeight = sourceVideoTrack!.renderSize().height
endTime = sourceAsset!.duration
sourceRange = sourceDuration
do {
try videoTrack!.insertTimeRange(sourceDuration, of: sourceVideoTrack!, at: insertTime)
}catch {
print("error inserting time range")
}
}
func createVideoCompositionInstructions() {
let mainInstruction = AVMutableVideoCompositionInstruction()
mainInstruction.timeRange = sourceRange!
let videolayerInstruction = videoCompositionInstruction(videoTrack!, asset: sourceAsset!)
videolayerInstruction.setOpacity(0.0, at: endTime!)
//Add instructions
mainInstruction.layerInstructions = [videolayerInstruction]
videoCompositionInstructions = AVMutableVideoComposition()
videoCompositionInstructions!.renderScale = 1.0
videoCompositionInstructions!.renderSize = CGSize(width: renderWidth!, height: renderWidth!*16/9)
videoCompositionInstructions!.frameDuration = CMTimeMake(value: 1, timescale: 30)
videoCompositionInstructions!.instructions = [mainInstruction]
}
func videoCompositionInstruction(_ track: AVCompositionTrack, asset: AVAsset)
-> AVMutableVideoCompositionLayerInstruction {
let instruction = AVMutableVideoCompositionLayerInstruction(assetTrack: track)
let assetTrack = asset.tracks(withMediaType: .video)[0]
instruction.setTransform(assetTrack.preferredTransform.concatenating(CGAffineTransform(translationX: 0, y: -(renderHeight! - renderWidth!*16/9)/2)), at: CMTime.zero)
return instruction
}
}
extension AVFoundationClient {
//Export the AV Mutable Composition
func export(_ mutableComposition: AVMutableComposition) {
// Set up exporter
guard let exporter = AVAssetExportSession(asset: mutableComposition, presetName: AVAssetExportPreset1920x1080) else { return }
exporter.outputURL = generateExportUrl()
exporter.outputFileType = AVFileType.mov
exporter.shouldOptimizeForNetworkUse = false
exporter.videoComposition = videoCompositionInstructions
exporter.exportAsynchronously() {
DispatchQueue.main.async {
self.exportDidComplete(exportURL: exporter.outputURL!, doneEditing: false)
}
}
}
func generateExportUrl() -> URL {
// Create a custom URL using curernt date-time to prevent conflicted URL in the future.
let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let dateFormat = DateFormatter()
dateFormat.dateStyle = .long
dateFormat.timeStyle = .short
let dateString = dateFormat.string(from: Date())
let exportPath = (documentDirectory as NSString).strings(byAppendingPaths: ["edited-video-\(dateString).mp4"])[0]
//erase old
let fileManager = FileManager.default
do {
try fileManager.removeItem(at: URL(fileURLWithPath: exportPath))
} catch {
print("Unable to remove item at \(URL(fileURLWithPath: exportPath))")
}
return URL(fileURLWithPath: exportPath)
}
//Export Finish Handler
func exportDidComplete(exportURL: URL, doneEditing: Bool) {
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: exportURL)
}) { saved, error in
if saved {print("successful saving")}
else {
print("error saving")
}
}
}
}