iOS Swift Реализация редактора Instagram Story - PullRequest
0 голосов
/ 05 февраля 2019

Я работаю над проектом, похожим на редактирование фото / видео в 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")
        }
    }
}
}
...