<commit_before>//
//  MovieEditor.swift
//  VideoDecode
//
//  Created by larryhou on 09/01/2018.
//  Copyright © 2018 larryhou. All rights reserved.
//

import Foundation
import AVFoundation
import GameplayKit

extension AVAssetExportSessionStatus
{
    var description:String
    {
        switch self
        {
            case .cancelled:return "cancelled"
            case .exporting:return "exporting"
            case .completed:return "completed"
            case .waiting:return "waiting"
            case .unknown:return "unknown"
            case .failed:return "failed"
        }
    }
}

extension CMTimeRange
{
    var description:String
    {
        return "f:\(start.seconds) t:\(end.seconds) d:\(duration.seconds)"
    }
}

enum TransitionDirection
{
    case right, left, top, bottom
}

enum VideoTransition
{
    case DISSOLVE, PUSH_RIGHT, PUSH_LEFT, PUSH_TOP, PUSH_BOTTOM, ERASE_RIGHT, ERASE_LEFT, ERASE_TOP, ERASE_BOTTOM, RANDOM
}

class MovieEditor
{
    var asset:AVAsset!
    var insertClips:[CMTimeRange]!
    
    var assetComposition:AVMutableComposition!
    var transitionClips:[CMTimeRange]!
    var passClips:[CMTimeRange]!
    
    var videoTransition:VideoTransition
    var transitionDuration:TimeInterval
    
    init(transition:VideoTransition = .DISSOLVE, transitionDuration duration:TimeInterval = 1.0)
    {
        self.videoTransition = transition
        self.transitionDuration = duration
    }
    
    func cut(asset:AVAsset, withClips clips:[CMTimeRange], transition:VideoTransition? = nil)->AVAssetExportSession?
    {
        guard asset.isReadable else { return nil }
        let insertClips:[CMTimeRange] = clips.filter({ !$0.duration.seconds.isNaN })
        guard insertClips.count > 0 else { return nil }
        
        if let transition = transition
        {
            self.videoTransition = transition
        }
        
        self.asset = asset
        self.insertClips = insertClips
        
        self.assetComposition = AVMutableComposition()
        let exporter = AVAssetExportSession(asset: assetComposition, presetName: AVAssetExportPreset1280x720)
        if let (videoComposition, mix) = composeMovieTracks()
        {
            exporter?.videoComposition = videoComposition
            exporter?.audioMix = mix
        }
        exporter?.outputFileType = AVFileType.mp4
        return exporter
    }
    
    private func composeMovieTracks()->(AVMutableVideoComposition, AVMutableAudioMix)?
    {
        let videoTracks = [
            assetComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)!,
            assetComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)!]
        let audioTracks = [
            assetComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)!,
            assetComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)!]
        
        self.transitionClips = []
        self.passClips = []
        
        let assetVideoTrack = asset.tracks(withMediaType: .video)[0]
        let assetAudioTrack = asset.tracks(withMediaType: .audio)[0]
        
        var anchor = kCMTimeZero
        let overlap = CMTime(seconds: transitionDuration, preferredTimescale: 100)
        for i in 0..<insertClips.count
        {
            let index = i % 2
            let range = insertClips[i]
            try? videoTracks[index].insertTimeRange(range, of: assetVideoTrack, at: anchor)
            try? audioTracks[index].insertTimeRange(range, of: assetAudioTrack, at: anchor)
            
            var passClip = CMTimeRange(start: anchor, duration: range.duration)
            if i > 0
            {
                passClip.start = passClip.start + overlap
                passClip.duration = passClip.duration - overlap
            }
            
            if i + 1 < insertClips.count
            {
                passClip.duration = passClip.duration - overlap
            }
            
            passClips.append(passClip)
            
            anchor = anchor + range.duration - overlap
            if i + 1 < insertClips.count
            {
                transitionClips.append(CMTimeRange(start: anchor, duration: overlap))
            }
        }
        
        var mixParameters:[AVMutableAudioMixInputParameters] = []
        var videoInstuctions:[AVMutableVideoCompositionInstruction] = []
        for i in 0..<passClips.count
        {
            let index = i % 2
            let instruction = AVMutableVideoCompositionInstruction()
            let layer = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTracks[index])
            instruction.layerInstructions = [layer]
            instruction.timeRange = passClips[i]
            videoInstuctions.append(instruction)
            
            if i < transitionClips.count
            {
                let range = transitionClips[i]
                let instruction = AVMutableVideoCompositionInstruction()
                instruction.layerInstructions = getTransitionInstuctions(srcTrack: videoTracks[index], dstTrack: videoTracks[1-index], range: range, transition: videoTransition)
                instruction.timeRange = range
                videoInstuctions.append(instruction)
                
                var parameter = AVMutableAudioMixInputParameters(track: audioTracks[index])
                parameter.setVolumeRamp(fromStartVolume: 1.0, toEndVolume: 0.0, timeRange: transitionClips[i])
                parameter.setVolume(1.0, at: transitionClips[i].end)
                mixParameters.append(parameter)
                
                parameter = AVMutableAudioMixInputParameters(track: audioTracks[1 - index])
                parameter.setVolumeRamp(fromStartVolume: 0.0, toEndVolume: 1.0, timeRange: transitionClips[i])
                mixParameters.append(parameter)
            }
        }
        
        let videoComposition = AVMutableVideoComposition()
        videoComposition.renderSize = assetVideoTrack.naturalSize
        videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
        videoComposition.instructions = videoInstuctions
        
        let mix = AVMutableAudioMix()
        mix.inputParameters = mixParameters
        return (videoComposition, mix)
    }
    
    func getTransitionInstuctions(srcTrack:AVMutableCompositionTrack, dstTrack:AVMutableCompositionTrack, range:CMTimeRange, transition:VideoTransition)->[AVMutableVideoCompositionLayerInstruction]
    {
        switch transition
        {
            case .DISSOLVE:
                return dissolve(srcTrack: srcTrack, dstTrack: dstTrack, range: range)
            case .PUSH_RIGHT:
                return push(srcTrack: srcTrack, dstTrack: dstTrack, range: range, direction: .right)
            case .PUSH_LEFT:
                return push(srcTrack: srcTrack, dstTrack: dstTrack, range: range, direction: .left)
            case .PUSH_TOP:
                return push(srcTrack: srcTrack, dstTrack: dstTrack, range: range, direction: .top)
            case .PUSH_BOTTOM:
                return push(srcTrack: srcTrack, dstTrack: dstTrack, range: range, direction: .bottom)
            case .ERASE_RIGHT:
                return erase(srcTrack: srcTrack, dstTrack: dstTrack, range: range, direction: .right)
            case .ERASE_LEFT:
                return erase(srcTrack: srcTrack, dstTrack: dstTrack, range: range, direction: .right)
            case .ERASE_TOP:
                return erase(srcTrack: srcTrack, dstTrack: dstTrack, range: range, direction: .right)
            case .ERASE_BOTTOM:
                return erase(srcTrack: srcTrack, dstTrack: dstTrack, range: range, direction: .right)
            case .RANDOM:
                let transitions:[VideoTransition] = [.DISSOLVE, .PUSH_TOP, .PUSH_BOTTOM, .PUSH_LEFT, .PUSH_RIGHT, .ERASE_TOP, .ERASE_BOTTOM, .ERASE_RIGHT, .ERASE_LEFT]
                let index = Int(GKRandomSource.sharedRandom().nextUniform() * Float(transitions.count))
                return getTransitionInstuctions(srcTrack: srcTrack, dstTrack: dstTrack, range: range, transition: transitions[index])
        }
    }
    
    func dissolve(srcTrack:AVMutableCompositionTrack, dstTrack:AVMutableCompositionTrack, range:CMTimeRange)->[AVMutableVideoCompositionLayerInstruction]
    {
        let src = AVMutableVideoCompositionLayerInstruction(assetTrack: srcTrack)
        src.setOpacityRamp(fromStartOpacity: 1.0, toEndOpacity: 0.0, timeRange: range)
        let dst = AVMutableVideoCompositionLayerInstruction(assetTrack: dstTrack)
        dst.setOpacityRamp(fromStartOpacity: 0.0, toEndOpacity: 1.0, timeRange: range)
        return [src, dst]
    }
    
    //MARK: push transition effect
    func push(srcTrack:AVMutableCompositionTrack, dstTrack:AVMutableCompositionTrack, range:CMTimeRange, direction:TransitionDirection)->[AVMutableVideoCompositionLayerInstruction]
    {
        let size = srcTrack.naturalSize
        let center = CGAffineTransform.identity
        let left = center.translatedBy(x: -size.width, y: 0)
        let right = center.translatedBy(x: size.width, y: 0)
        let top = center.translatedBy(x: 0, y: -size.height)<commit_msg>transition effetcs<commit_after>//
//  MovieEditor.swift
//  VideoDecode
//
//  Created by larryhou on 09/01/2018.
//  Copyright © 2018 larryhou. All rights reserved.
//

import Foundation
import AVFoundation
import GameplayKit

extension AVAssetExportSessionStatus
{
    var description:String
    {
        switch self
        {
            case .cancelled:return "cancelled"
            case .exporting:return "exporting"
            case .completed:return "completed"
            case .waiting:return "waiting"
            case .unknown:return "unknown"
            case .failed:return "failed"
        }
    }
}

extension CMTimeRange
{
    var description:String
    {
        return "f:\(start.seconds) t:\(end.seconds) d:\(duration.seconds)"
    }
}

enum TransitionDirection
{
    case right, left, top, bottom
}

enum VideoTransition
{
    case DISSOLVE, PUSH_RIGHT, PUSH_LEFT, PUSH_TOP, PUSH_BOTTOM, ERASE_RIGHT, ERASE_LEFT, ERASE_TOP, ERASE_BOTTOM, RANDOM
}

class MovieEditor
{
    var asset:AVAsset!
    var insertClips:[CMTimeRange]!
    
    var assetComposition:AVMutableComposition!
    var transitionClips:[CMTimeRange]!
    var passClips:[CMTimeRange]!
    
    var videoTransition:VideoTransition
    var transitionDuration:TimeInterval
    
    init(transition:VideoTransition = .DISSOLVE, transitionDuration duration:TimeInterval = 1.0)
    {
        self.videoTransition = transition
        self.transitionDuration = duration
    }
    
    func cut(asset:AVAsset, withClips clips:[CMTimeRange], transition:VideoTransition? = nil)->AVAssetExportSession?
    {
        guard asset.isReadable else { return nil }
        let insertClips:[CMTimeRange] = clips.filter({ !$0.duration.seconds.isNaN })
        guard insertClips.count > 0 else { return nil }
        
        if let transition = transition
        {
            self.videoTransition = transition
        }
        
        self.asset = asset
        self.insertClips = insertClips
        
        self.assetComposition = AVMutableComposition()
        let exporter = AVAssetExportSession(asset: assetComposition, presetName: AVAssetExportPreset1280x720)
        if let (videoComposition, mix) = composeMovieTracks()
        {
            exporter?.videoComposition = videoComposition
            exporter?.audioMix = mix
        }
        exporter?.outputFileType = AVFileType.mp4
        return exporter
    }
    
    private func composeMovieTracks()->(AVMutableVideoComposition, AVMutableAudioMix)?
    {
        let videoTracks = [
            assetComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)!,
            assetComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)!]
        let audioTracks = [
            assetComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)!,
            assetComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)!]
        
        self.transitionClips = []
        self.passClips = []
        
        let assetVideoTrack = asset.tracks(withMediaType: .video)[0]
        let assetAudioTrack = asset.tracks(withMediaType: .audio)[0]
        
        var anchor = kCMTimeZero
        let overlap = CMTime(seconds: transitionDuration, preferredTimescale: 100)
        for i in 0..<insertClips.count
        {
            let index = i % 2
            let range = insertClips[i]
            try? videoTracks[index].insertTimeRange(range, of: assetVideoTrack, at: anchor)
            try? audioTracks[index].insertTimeRange(range, of: assetAudioTrack, at: anchor)
            
            var passClip = CMTimeRange(start: anchor, duration: range.duration)
            if i > 0
            {
                passClip.start = passClip.start + overlap
                passClip.duration = passClip.duration - overlap
            }
            
            if i + 1 < insertClips.count
            {
                passClip.duration = passClip.duration - overlap
            }
            
            passClips.append(passClip)
            
            anchor = anchor + range.duration - overlap
            if i + 1 < insertClips.count
            {
                transitionClips.append(CMTimeRange(start: anchor, duration: overlap))
            }
        }
        
        var mixParameters:[AVMutableAudioMixInputParameters] = []
        var videoInstuctions:[AVMutableVideoCompositionInstruction] = []
        for i in 0..<passClips.count
        {
            let index = i % 2
            let instruction = AVMutableVideoCompositionInstruction()
            let layer = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTracks[index])
            instruction.layerInstructions = [layer]
            instruction.timeRange = passClips[i]
            videoInstuctions.append(instruction)
            
            if i < transitionClips.count
            {
                let range = transitionClips[i]
                let instruction = AVMutableVideoCompositionInstruction()
                instruction.layerInstructions = getTransitionInstuctions(srcTrack: videoTracks[index], dstTrack: videoTracks[1-index], range: range, transition: videoTransition)
                instruction.timeRange = range
                videoInstuctions.append(instruction)
                
                var parameter = AVMutableAudioMixInputParameters(track: audioTracks[index])
                parameter.setVolumeRamp(fromStartVolume: 1.0, toEndVolume: 0.0, timeRange: transitionClips[i])
                parameter.setVolume(1.0, at: transitionClips[i].end)
                mixParameters.append(parameter)
                
                parameter = AVMutableAudioMixInputParameters(track: audioTracks[1 - index])
                parameter.setVolumeRamp(fromStartVolume: 0.0, toEndVolume: 1.0, timeRange: transitionClips[i])
                mixParameters.append(parameter)
            }
        }
        
        let videoComposition = AVMutableVideoComposition()
        videoComposition.renderSize = assetVideoTrack.naturalSize
        videoComposition.frameDuration = CMTime(value: 1, timescale: 30)
        videoComposition.instructions = videoInstuctions
        
        let mix = AVMutableAudioMix()
        mix.inputParameters = mixParameters
        return (videoComposition, mix)
    }
    
    func getTransitionInstuctions(srcTrack:AVMutableCompositionTrack, dstTrack:AVMutableCompositionTrack, range:CMTimeRange, transition:VideoTransition)->[AVMutableVideoCompositionLayerInstruction]
    {
        switch transition
        {
            case .DISSOLVE:
                return dissolve(srcTrack: srcTrack, dstTrack: dstTrack, range: range)
            case .PUSH_RIGHT:
                return push(srcTrack: srcTrack, dstTrack: dstTrack, range: range, direction: .right)
            case .PUSH_LEFT:
                return push(srcTrack: srcTrack, dstTrack: dstTrack, range: range, direction: .left)
            case .PUSH_TOP:
                return push(srcTrack: srcTrack, dstTrack: dstTrack, range: range, direction: .top)
            case .PUSH_BOTTOM:
                return push(srcTrack: srcTrack, dstTrack: dstTrack, range: range, direction: .bottom)
            case .ERASE_RIGHT:
                return erase(srcTrack: srcTrack, dstTrack: dstTrack, range: range, direction: .right)
            case .ERASE_LEFT:
                return erase(srcTrack: srcTrack, dstTrack: dstTrack, range: range, direction: .left)
            case .ERASE_TOP:
                return erase(srcTrack: srcTrack, dstTrack: dstTrack, range: range, direction: .top)
            case .ERASE_BOTTOM:
                return erase(srcTrack: srcTrack, dstTrack: dstTrack, range: range, direction: .bottom)
            case .RANDOM:
                let transitions:[VideoTransition] = [.DISSOLVE, .PUSH_TOP, .PUSH_BOTTOM, .PUSH_LEFT, .PUSH_RIGHT, .ERASE_TOP, .ERASE_BOTTOM, .ERASE_RIGHT, .ERASE_LEFT]
                let index = Int(GKRandomSource.sharedRandom().nextUniform() * Float(transitions.count))
                return getTransitionInstuctions(srcTrack: srcTrack, dstTrack: dstTrack, range: range, transition: transitions[index])
        }
    }
    
    func dissolve(srcTrack:AVMutableCompositionTrack, dstTrack:AVMutableCompositionTrack, range:CMTimeRange)->[AVMutableVideoCompositionLayerInstruction]
    {
        let src = AVMutableVideoCompositionLayerInstruction(assetTrack: srcTrack)
        src.setOpacityRamp(fromStartOpacity: 1.0, toEndOpacity: 0.0, timeRange: range)
        let dst = AVMutableVideoCompositionLayerInstruction(assetTrack: dstTrack)
        dst.setOpacityRamp(fromStartOpacity: 0.0, toEndOpacity: 1.0, timeRange: range)
        return [src, dst]
    }
    
    //MARK: push transition effect
    func push(srcTrack:AVMutableCompositionTrack, dstTrack:AVMutableCompositionTrack, range:CMTimeRange, direction:TransitionDirection)->[AVMutableVideoCompositionLayerInstruction]
    {
        let size = srcTrack.naturalSize
        let center = CGAffineTransform.identity
        let left = center.translatedBy(x: -size.width, y: 0)
        let right = center.translatedBy(x: size.width, y: 0)
        let top = center.translatedBy(x: 0, y: -size.height)
{'fraction_non_alphanumeric': 0.08271850934352953, 'fraction_numerical': 0.00667779632721202, 'mean_word_length': 2.4723261032161554, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 17, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1078950', 'n_tokens_mistral': 5288, 'n_tokens_neox': 4992, 'n_words': 1062}