AVFoundationをがっつり使ってすっかりAV芸人になった回

畑田です。  
ある開発においてがっつりAVFoundation芸人にならねばならなかったのでAppleのAVFoundationのドキュメントを読んで分かったことをまとめてみます。  
# captureについて
AVFoundationのcaptureというサブシステムはビデオ、写真、音声を撮影、録音するためのものであり、以下のときに使用されうる。
- アプリに自前のカメラ機能を実装するとき
- 動画や写真を撮影する際にフォーカスや露光、安定性などについて、ユーザーにより直接的に操作させたいとき
- 撮影した写真やdepth maps(なにこれ訳せやん)の保存形式を指定したり、ビデオのメタデータをカスタムしたりしたいとき
- 撮影、あるいは録音デバイスから直接ストリーミングさせたいとき

**もし、ユーザーにネイティブのカメラアプリを使用させたい場合は`UIImagePickerController`を用いれば良い点に注意すること。**   
captureの主要なパーツはsession, inputs, outputsであり、sessionは**一つ以上のinputと一つ以上のoutput**をつなげてくれる。  
![AVFoundation_capture.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496198/3b12200e-966b-5ab0-0018-b425d789a484.png)


## AVCaptureSession
`AVCaptureSession`はcaptureにおけるinputからoutputへのデータの流れを管理するためのクラスである。   
`AVCaptureSession`をインスタンス化し、適当なinputとoutputを追加すると、`startRunning()`でcaptureを開始でき、`stopRunning()`でcaptureを終了させられる。   
画質などの設定は`sessionPreset`というpropertyを用いることで可能であるが、フレームレートの高度な調整などより詳しい設定に関しては`AVCaptureDevice`に直接指定してやる必要がある。   
[これ](https://developer.apple.com/documentation/avfoundation/cameras_and_media_capture/avcam_building_a_camera_app)を参考に色々なプラクティスを実装してみて練習する余地はありそうかもしれない。   

## AVCaptureConnection
`AVCaptureConnection`はsessionにおいて特定のinputとoutputをつなげる役割を果たすクラスである。   
`AVCaptureSession`の`addInput(_:)`や`addOutput(_:)`を使えば、自動でinputとoutputの接続は形成される。   
しかし、接続したくないinputとoutputがある場合や接続ごとの有効化などを設定したい場合、またはビデオの状態を取得したり設定したりしたい場合はこのクラスを用いることが必要となる。   
`videoOrientation`に値を指定すれば、ビデオの表示方向を指定できる。   
このクラスはインスタンス化したのち、`AVCaptureSession`に追加することでsessionに反映される。

## AVCaptureMovieFileOutput
`AVCaptureMovieFileOutput`はcaptureした映像と音声をMOV形式で記録するためのクラスである。   
**このクラスはMOV形式への保存にのみ対応しており、MPEG-4などその他のファイル形式で記録することはできない。**   
`setOutputSettings([String : Any]?, for: AVCaptureConnection)`を用いて設定を追加できるようである。この引数の辞書型のものには`[AVVideoCodecKey: AVVideoCodecType.h264]`などが指定できる。詳しくは[こちら](https://developer.apple.com/documentation/avfoundation/avassetwriterinput/video_settings_dictionaries)を参照されたい。   

# 動画再生について
## AVAsset
`AVAsset`は映像トラックや音声トラックなどのデジタル素材を定義するためのクラスであり、そのメディアの所在を示すurlを用いて初期化する。   
(トラックとは、、、磁気テープやディスクなどの記録メディアに映像や音声の信号を記録する帯状の領域のこと。実際にはデータが断続しているディジタル信号の記録でも、イメージとしては各信号ごとに帯状の領域となっている。)   
ただし、`AVAsset`は抽象的なクラスであり、インスタンス化すると実際にはサブクラスであるAVURLAssetが返される。ちなみに`AVAsset`の直接のサブクラスは`AVURLAsset`と`AVComposition`の2つである。   

```swift
let url: URL = // Local or Remote Asset URL
let asset = AVAsset(url: url)
print(type(of: asset)) // AVURLAsset
```
`AVURLAsset`は`AVAsset`をインスタンス化するのではなく、直接インスタンス化でき、その際には、live streamingやセルラーデータを使っては通信しないなどの詳しいoptionsを辞書型で与えることができる。

```swift
let url: URL = // Remote Asset URL
let options = [AVURLAssetAllowsCellularAccessKey: false]
let asset = AVURLAsset(url: url, options: options)
```
メディアを再生するには`AVPlayerItem`に`AVURLAsset`を渡してインスタンス化し、再生時間制限などの設定をして、それを渡すことで`AVPlayer`をインスタンス化する。   

## AVPlayerItem
`AVPlayerItem`は`AVAsset`インスタンスをへの参照を保持する、つまり再生したいメディアを管理するオブジェクトへのアクセスをする役割がある。   
初期化時に必要なアセットデータを記述した配列を渡すと自動で必要なアセットを読み込んで使えるようにしてくれる。   
`AVPlayerItem`のpropertyは変更できるし、read-only propertyでさえ`AVPlayer`からは自動で変更されるが、その中でも`status` propertyは重要である。このpropertyの値が`AVPlayerItem.Status.readyToPlay`になったときに再生できるため、これを観察する手を打つべきである。   
`player`に`playerItem`を渡す前に`addObserver(_:forKeyPath:options:context:)`を呼び、その変更を`observeValue(forKeyPath:of:change:context:)`によって取得するのが良い。   
なお、`status`propertyが`failed`の場合は`AVPlayerItem`の`error`に詳細があるはずである。   

```swift
func prepareToPlay() {
    let url = <#Asset URL#>
    // Create asset to be played
    asset = AVAsset(url: url)
    
    let assetKeys = [
        "playable",
        "hasProtectedContent"
    ]
    // Create a new AVPlayerItem with the asset and an
    // array of asset keys to be automatically loaded
    playerItem = AVPlayerItem(asset: asset,
                              automaticallyLoadedAssetKeys: assetKeys)
    
    // Register as an observer of the player item's status property
    playerItem.addObserver(self,
                           forKeyPath: #keyPath(AVPlayerItem.status),
                           options: [.old, .new],
                           context: &playerItemContext)
    
    // Associate the player item with the player
    player = AVPlayer(playerItem: playerItem)
}
```

## AVPlayer
`AVPlayer`はメディアファイルを再生するためのクラスであり、同時に一つの素材(media asset)しか扱えない。   
異なる素材を連続して再生したい場合は、`NotificationCenter`に再生の終わりを通知させて`replaceCurrentItem(with:)`メソッドを用いてアイテムを挿げ替えるか、`AVQueuePlayer`で代用することを検討する。   
`timeControlObserver` propertyで再生中か一時停止中華などの情報を得られる。   
`AVAsset`は作成時刻やトラックの長さなど静的な情報しか持っていないため、動的な状態を保持できる`AVPlayerItem`を渡すことで再生できる。   
`AVPlayer`もまた動的なオブジェクトであり、その変わりゆく状態を取得するには以下の二つの方法を併用することが必要である。   
- KVO(key-value observing)を使って`currentItem`や`rate`などの変化を取得するというもの
- KVOは再生時間などの絶えず変化している値の取得には向いていないため、`addPeriodicTimeObserver(forInterval:queue:using:)`や`addBoundaryTimeObserver(forTimes:queue:using:)`を用いてその変化を取得するというもの

後者では、値をpriodicallyまたはby boundaryに観察し、値が変化するごとにコールバック関数が発火しインターフェイスを更新するなどの記述が可能である。

# 動画編集について

## AVComposition
`AVComposition`は複数のソースからなるデータを重ね合わせるためのクラスであり、`AVAsset`のサブクラスです。   
`AVComposition`は音声や映像といったそれぞれのメディアを意味するトラック(`AVCompositionTrack`という`AVAssetTrack`を継承したクラスで表される)という単位の集合です。やっぱり`AVAsset`の友達。   
またそれぞれのトラックはsegment(`AVCompositionTrackSegment`)によって構成されており、これはURL、track identifier、time mappingなどでコンテナに保存されている各メディアデータを表しています。   
`AVComposition`は`AVAsset`のサブクラスであることからなんとなくわかるかもしれませんが、`AVPlayer`などに渡して再生することができます。   
また、ファイルで表せる全てのAVデータはコンテナ形式によらず、合成することができます。   
time mappingはメディアの長さを管理しており、元データと保存先(合成先)データのタイムレンジが等しければそのまま保存し、異なれば等倍で伸ばして保存するなどするということです。   
こういったtrackやsegmentには簡単にアクセスできて書き出すこともできるし、`AVMutableCompositionTrack`を用いれば`AVMutableComposition`を新しくインスタンス化することで、compositionを再構築、新規作成することもできます。   
`AVMutableCompositionTrack`や`AVMutableComposition`を用いれば、メディアの挿入や削除、スケーリングなどの操作を高レイヤーにおいて行うことができます。   
特に、動画編集、compositionの新規作成については以下の図表を参照してください。

| name | class | description |
| :-: | :-: | :-: |
| Composition | `AVMutableComposition` | 一つの映像作品 / プロジェクト |
| Track | `AVMutableCompositionTrack` | Compositionを構成する映像/音声トラック|
| Asset | `AVAsset` | Compositionの各トラックにのせるトラックを持つ素材 |

少しソースコード書いてみます。

```swift
// create an empty composition
let mutableComposition = AVMutableComposition()

// add an empty video track to the composition
guard let compositionVideoTrack = mutableComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) else {
    print("failed to add video track")
    return
}

// add an empty audio track to the composition
guard let compositionAudioTrack = mutableComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) else {
    print("failed to add audio track")
    return
}

// add a video track of asset to the composition video track
let videoAsset = AVURLAsset(url: someURL)
guard let _ = videoAsset.tracks(withMediaType: .video).first else { return }
let sourceVideoTrack = videoAsset.tracks(withMediaType: .video)[0]
do {
    try compositionVideoTrack.insertTimeRange(sourceVideoTrack.timeRange, of: sourceVideoTrack, at: .zero)
} catch {
    print(error)
}

// add a audio track of asset to the composition audio track
let audioAsset = AVURLAsset(url: someURL)
guard let _ = audioAsset.tracks(withMediaType: .audio).first else { return }
let sourceAudioTrack = audioAsset.tracks(withMediaType: .audio)[0]
do {
    try compositionAudioTrack.insertTimeRange(sourceAudioTrack.timeRange, of: sourceAudioTrack, at: .zero)
} catch {
    print(error)
}

// create export session
guard let session = AVAssetExportSession(asset: mutableComposition, presetName: AVAssetExportPresetPassthrough) else {
    print("failed to prepare session")
    return
}

// set up output file
session.outputURL = urlToWriteTo
session.outputFileType = .mp4

// export the composition
session.exportAsynchronously {
    switch session.status {
    case .completed:
        print("completed")
    case .failed:
        print("export error: \(session.error!.localizedDescription)")
    default:
        break
    }
}
```
composition trackにassetのtrackを載せるコードのdo構文の中を以下のようにしてあげると、時間の設定を変えられます。

```swift
let firstFiveSeconds = CMTimeRange(start: .zero, end: CMTime(seconds: 5.0, preferredTimescale: sourceVideoTrack.naturalTimeScale))
try compositionVideoTrack.insertTimeRange(firstFiveSeconds, of: sourceVideoTrack, at: .zero)
```

## AVMutableVideoCompositionInstruction, AVMutableVideoCompositionLayerInstruction
実際にトラックの操作(透過、移動、クロップ)を設定するにはこちらのクラスを利用します。   
それらの操作の開始時間、継続時間も同時に設定します。   
動画を撮影したときの向きを取得して、それに合わせてビデオトラックを回転させることもできます。(下のソースコードの`layerInstruction.setTransform(sourceVideoTrack.preferredTransform, at: .zero)`の部分を参照してください。)   
上の図では1つの`AVMutableCompositionTrack`に対して1つの`AVMutableVideoCompositionLayerInstruction`を使用しているイメージとなります。   
しかし、1つの`AVMutableCompositionTrack`に対して複数の`AVMutableVideoCompositionLayerInstruction`を構築することも可能ですので、より複雑な編集も効率よく行えます。   

## AVMutableVideoComposition
`AVMutableComposition`と名前は似ていますが、こちらのクラスでは`AVMutableComposition`に対する付加情報を設定していきます。   
具体的には、フレームの長さやレンダリングサイズ、そして先ほどでてきたトラックの操作(`AVMutableVideoCompositionInstruction`)です。   
この`AVMutableVideoComposition`を、`AVMutableComposition`とともに`AVPlayer`や`AVAssetExportSession`のクラスに渡す事でビデオに付加情報がセットされます。  
このように説明だけしても、使い方が不明です、、、となりそうなのでまたソースコードを書いてみます。   
先ほどのソースコードで生成したexport sessionである`session`で出力を実行する前に、`videoComposition`インスタンスを渡しています。

```swift
// create mutable video composition which mainly decides frame duration, render size and instruction
let videoComposition: AVMutableVideoComposition = AVMutableVideoComposition()

// set up frame size and render size
videoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
videoComposition.renderSize = sourceVideoTrack.naturalSize

// create mutable video composition instruction
let instruction: AVMutableVideoCompositionInstruction = AVMutableVideoCompositionInstruction()

// set time range in which this instruction is active
instruction.timeRange = CMTimeRangeMake(start: CMTime.zero, duration: mutableComposition.duration)

// create and set up layer instruction
let layerInstruction: AVMutableVideoCompositionLayerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionVideoTrack)
layerInstruction.setTransform(sourceVideoTrack.preferredTransform, at: .zero) // rotate video here
instruction.layerInstructions = [layerInstruction]
videoComposition.instructions = [instruction]

// set mutable video composition in export session
session.videoComposition = videoComposition

// export below...
```

## 背景を挿入してみる
`AVMutableVideoComposition`では動画の上にアニメーションや字幕、動画と画像、動画と動画の重ね合わせを設定することもできます。   
`CALayer`を用いることで映像に階層構造を作っています。   
以下では映像の上に背景画像(静止画)を載せています。   
これを上のソースコードの、sessionにvideo compositionを渡す前に書いてあげれば良いという感じです。

```swift
// get video size
let videoSize: CGSize = videoTrack.naturalSize
        
// create parent layer
let parentLayer: CALayer = CALayer()
parentLayer.frame = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)

// create videp layer which will be associated with composition video track
let videoLayer: CALayer = CALayer()
videoLayer.frame = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)

// add video layer to parent layer and attach video contents to video layer
parentLayer.addSublayer(videoLayer)
videoComposition.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: videoLayer, in: parentLayer)

// if original video selected, `backgroundImage` is nil and background layer shouldn't be created or added to parent layer
if let _ = backgroundImage {
    // create background layer
    let backgroundLayer: CALayer = CALayer()
    backgroundLayer.frame = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)
    backgroundLayer.opacity = 1.0
    backgroundLayer.masksToBounds = true
    backgroundLayer.backgroundColor = UIColor.clear.cgColor
    backgroundLayer.contentsGravity = CALayerContentsGravity.resizeAspectFill
    
    // add contents to background layer
    backgroundLayer.contents = backgroundImage!.cgImage
    
    // add background layer over video layer
    parentLayer.addSublayer(backgroundLayer)
    
    // make video layer clear to lighten video
    videoLayer.opacity = 0
}
```

## 自作の字幕を挿入してみる
開発中の製品の売りは歌詞入れ機能！ということで歌詞の入れ方も記録しておきます。   
実際のプロダクトのコードです。   
`mergeMovie()`メソッドを呼ぶと歌詞のついた動画がアプリのtmpディレクトリとフォトライブラリに保存されるようになっています。   
ここで`lyricData`プロパティは前の画面から引き継いでいるもので、型は`Dictionary<String, Any>`ですが、実際には`["id": Int, "lyric": String, "start_at": CMTime, "end_at": CMTime]`というような形式です。   
`getFromTmp(file:)`メソッドは独自に定義したものなので完全コピペでは走りません。
<details><summary>ソースコード</summary><div>

```swift
private func mergeMovie() {
    // confirm source video asset is not nil
    let asset = AVURLAsset(url: movieURL)
    print(movieURL.path)
    
    // extract video track from asset
    guard let _ = asset.tracks(withMediaType: .video).first else { return print("video track not found") }
    let videoTrack = asset.tracks(withMediaType: AVMediaType.video)[0]
    
    // extract audio track from asset
    guard let _ = asset.tracks(withMediaType: .audio).first else { return print("audio track not found") }
    let audioTrack = asset.tracks(withMediaType: AVMediaType.audio)[0]
    
    // create empty base composition
    let mutableComposition: AVMutableComposition = AVMutableComposition()
    
    // create empty composition video and audio tracks
    let compositionVideoTrack: AVMutableCompositionTrack! = mutableComposition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
    let compositionAudioTrack: AVMutableCompositionTrack! = mutableComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
    
    // insert source video track to the composition video track
    do {
        try compositionVideoTrack.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: asset.duration), of: videoTrack, at: CMTime.zero)
    } catch {
        print("insert video track error:", error)
    }
    
    // insert source audio track to the composition audio track
    do {
        try compositionAudioTrack.insertTimeRange(CMTimeRangeMake(start: CMTime.zero, duration: asset.duration), of: audioTrack, at: CMTime.zero)
    } catch {
        print("insert audio track error:", error)
    }
    // 回転方向の設定
    let preferredTransform = videoTrack.preferredTransform
    //        compositionVideoTrack.preferredTransform = preferredTransform // not effective
    
    // create eport session with base composition
    _assetExportSession = AVAssetExportSession(asset: mutableComposition, presetName: AVAssetExportPresetMediumQuality)
    
    // create mutable video composition which mainly decides duration, render size and instruction
    let videoComposition: AVMutableVideoComposition = AVMutableVideoComposition()
    videoComposition.renderSize = videoTrack.naturalSize
    videoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
    
    let instruction: AVMutableVideoCompositionInstruction = AVMutableVideoCompositionInstruction()
    instruction.timeRange = CMTimeRangeMake(start: CMTime.zero, duration: mutableComposition.duration)
    let layerInstruction: AVMutableVideoCompositionLayerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionVideoTrack)
    layerInstruction.setTransform(preferredTransform, at: .zero) // required!
    instruction.layerInstructions = [layerInstruction]
    videoComposition.instructions = [instruction]
    
    let videoSize: CGSize = mutableComposition.naturalSize
    
    // create lyric layer
    let lyricLayer = self.makeLyricLayer(for: mutableComposition)
    
    // create parent layer
    let parentLayer: CALayer = CALayer()
    parentLayer.frame = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)
    
    let videoLayer: CALayer = CALayer()
    videoLayer.frame = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)
    
    parentLayer.addSublayer(videoLayer)
    parentLayer.addSublayer(lyricLayer)
    videoComposition.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: videoLayer, in: parentLayer)
    
    // set mutable video composition in export session
    _assetExportSession?.videoComposition = videoComposition
    
    // set up export session
    exportURL = FileManager.getFromTmp(file: "completion_movie.mov")
    _assetExportSession?.outputFileType = AVFileType.mov
    _assetExportSession?.outputURL = exportURL
    _assetExportSession?.shouldOptimizeForNetworkUse = true
    
    // export
    _assetExportSession?.exportAsynchronously(completionHandler: {() -> Void in
        if self._assetExportSession?.status == AVAssetExportSession.Status.failed {
            print("failed:", self._assetExportSession?.error ?? "error")
        }
        if self._assetExportSession?.status == AVAssetExportSession.Status.completed {
            // save to photo library
            PHPhotoLibrary.shared().performChanges({
                PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: self.exportURL)
            })
            print("saved to \(self._assetExportSession!.outputURL!.path)")
        }
    })
}

private func makeLyricLayer(for mutableComposition: AVMutableComposition) -> CALayer {
    let videoSize = mutableComposition.naturalSize
    
    // create parent layer
    let lyricLayer: CALayer = CALayer()
    lyricLayer.frame = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)
    lyricLayer.opacity = 1.0
    lyricLayer.backgroundColor = UIColor.clear.cgColor
    lyricLayer.masksToBounds = true
    
    // prepare animation
    let frameAnimation: CAKeyframeAnimation = CAKeyframeAnimation(keyPath: "contents")
    frameAnimation.beginTime = AVCoreAnimationBeginTimeAtZero // attention! apple recommends
    frameAnimation.duration = CMTimeGetSeconds(mutableComposition.duration)
    frameAnimation.repeatCount = 1
    frameAnimation.autoreverses = false
    frameAnimation.isRemovedOnCompletion = false // apple recommends
    frameAnimation.fillMode = CAMediaTimingFillMode.forwards
    frameAnimation.calculationMode = CAAnimationCalculationMode.discrete
    
    // set up key times
    var imageKeyTimes: Array<NSNumber> = []
    let frameCount = Int(frameAnimation.duration * 30) // duration [s] * frame rate [/s] = total frame count
    for i in 0 ... frameCount {
        imageKeyTimes.append((Double(i)/Double(frameCount)) as NSNumber)
    }
    frameAnimation.keyTimes = imageKeyTimes
    
    // set up values
    var imageValues: Array<CGImage> = []
    for currentFrame in 0 ... frameCount {
        // begin rendering setting
        UIGraphicsBeginImageContext(videoSize)
        // get position of crrent frame in total video length
        let ratio = Double(currentFrame) / Double(frameCount)
        
        for (index, lyricDatum) in lyricData.enumerated() {
            let startTime = lyricDatum["start_at"] as! CMTime // time at which the lyric appears
            let endTime = lyricDatum["end_at"] as! CMTime // time at which lyric disappears
            // if current frame is between start time and end time, render lyric label
            if CMTimeGetSeconds(startTime) / CMTimeGetSeconds(mutableComposition.duration) < ratio && ratio < CMTimeGetSeconds(endTime) / CMTimeGetSeconds(mutableComposition.duration) {
                let lyric = lyricDatum["lyric"] as! String
                let attributedString = NSMutableAttributedString(string: lyric, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 36, weight: UIFont.Weight(rawValue: 1)), NSAttributedString.Key.foregroundColor: UIColor.white])
                let label = UILabel()
                label.frame.size = videoSize
                label.textAlignment = .center
                label.numberOfLines = 2
                label.clipsToBounds = true
                label.allowsDefaultTighteningForTruncation = true
                label.attributedText = attributedString
                label.drawText(in: CGRect(x: lyricLayer.bounds.origin.x, y: lyricLayer.bounds.maxY * 2 / 3, width: videoSize.width, height: videoSize.height / 3))
            }
            // else render nothing
        }
        
        let lyricImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        
        guard let _ = lyricImage?.cgImage else { continue }
        imageValues.append(lyricImage!.cgImage!)
    }
    frameAnimation.values = imageValues
    
    // add animation to lyric layer
    lyricLayer.add(frameAnimation, forKey: nil)
    
    return lyricLayer
}
```
</div></details>
このコードだと重すぎたので、滑らかに動くアニメーションを挿入するのであれば別ですが、字幕を入れるだけであれば、下のコードの方が良いです。
<details><summary>追記ソースコード</summary><div>

```swift
private func makeLyricLayer(for mutableComposition: AVMutableComposition) -> CALayer {
    let videoSize = mutableComposition.naturalSize
    
    // create parent layer
    let lyricLayer: CALayer = CALayer()
    lyricLayer.frame = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)
    lyricLayer.opacity = 1.0
    lyricLayer.backgroundColor = UIColor.clear.cgColor
    lyricLayer.masksToBounds = true
    
    // prepare animation
    let frameAnimation: CAKeyframeAnimation = CAKeyframeAnimation(keyPath: "contents")
    frameAnimation.beginTime = AVCoreAnimationBeginTimeAtZero // attention! apple recommends
    frameAnimation.duration = CMTimeGetSeconds(mutableComposition.duration)
    frameAnimation.repeatCount = 1
    frameAnimation.autoreverses = false
    frameAnimation.isRemovedOnCompletion = false // apple recommends
    frameAnimation.fillMode = CAMediaTimingFillMode.forwards
    frameAnimation.calculationMode = CAAnimationCalculationMode.discrete
    
    // set up key times
    var imageKeyTimes: Array<NSNumber> = []
    
    // set up values
    var imageValues: Array<CGImage> = []
    
    // create transparent image
    UIGraphicsBeginImageContext(videoSize)
    guard let cgEmptyImage = UIGraphicsGetImageFromCurrentImageContext()?.cgImage else { return lyricLayer }
    UIGraphicsEndImageContext()
    
    lyricData.sort() { d0, d1 in
        let startTime0 = d0["start_at"] as! CMTime
        let startTime1 = d1["start_at"] as! CMTime
        return startTime0 < startTime1
    }
    
    for (index, lyricDatum) in lyricData.enumerated() {
        let startTime = lyricDatum["start_at"] as! CMTime // time at which the lyric appears
        let endTime = lyricDatum["end_at"] as! CMTime // time at which lyric disappears
        
        if startTime == endTime { continue }
        
        if index == 0, startTime != .zero {
            imageKeyTimes.append(0)
            imageValues.append(cgEmptyImage)
        }
        
        // if current frame is between start time and end time, render lyric label
        imageKeyTimes.append(NSNumber(value: CMTimeGetSeconds(startTime) / CMTimeGetSeconds(mutableComposition.duration)))
        imageKeyTimes.append(NSNumber(value: CMTimeGetSeconds(endTime) / CMTimeGetSeconds(mutableComposition.duration)))
        
        // begin rendering
        UIGraphicsBeginImageContext(videoSize)
                    
        let lyric = lyricDatum["lyric"] as! String
        let attributedString = NSMutableAttributedString(string: lyric, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 24), NSAttributedString.Key.foregroundColor: UIColor.white])
        let label = UILabel()
        label.frame.size = videoSize
        label.textAlignment = .center
        label.numberOfLines = 2
        label.clipsToBounds = true
        label.allowsDefaultTighteningForTruncation = true
        label.attributedText = attributedString
        label.drawText(in: CGRect(x: lyricLayer.bounds.origin.x, y: lyricLayer.bounds.maxY * 2 / 3, width: videoSize.width, height: videoSize.height / 3))
        
        guard let cgLyricImage = UIGraphicsGetImageFromCurrentImageContext()?.cgImage else {
            UIGraphicsEndImageContext()
            imageValues.append(cgEmptyImage)
            imageValues.append(cgEmptyImage)
            continue
        }
        
        // end rendering
        UIGraphicsEndImageContext()
        
        imageValues.append(cgLyricImage)
        imageValues.append(cgEmptyImage)
    }
    
    // set the last key time and the last value
    imageKeyTimes.append(1)
    imageValues.append(cgEmptyImage)
    
    // set key times
    frameAnimation.keyTimes = imageKeyTimes
    print(imageKeyTimes)
    
    // set values
    frameAnimation.values = imageValues
    print(imageValues)
    
    // add animation to lyric layer
    lyricLayer.add(frameAnimation, forKey: nil)
    
    return lyricLayer
}
```
</div></details>