MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift音频与视频处理

2023-07-245.6k 阅读

一、Swift 音频处理基础

1.1 音频框架概述

在 Swift 中进行音频处理,最常用的框架是 AVFoundationAVFoundation 提供了一个全面的、基于对象的框架,用于处理基于时间的视听媒体数据。它涵盖了从简单的音频播放到复杂的音频编辑和录制等广泛功能。例如,AVAudioPlayer 类用于播放音频,而 AVAudioRecorder 类用于录制音频。

1.2 播放音频 - AVAudioPlayer

  1. 初始化与设置 要使用 AVAudioPlayer 播放音频,首先需要获取音频文件的路径并初始化播放器。假设我们有一个名为 example.mp3 的音频文件放在项目的资源目录中:
import AVFoundation

class AudioPlayerViewController: UIViewController {
    var audioPlayer: AVAudioPlayer?

    override func viewDidLoad() {
        super.viewDidLoad()

        guard let url = Bundle.main.url(forResource: "example", withExtension: "mp3") else {
            print("音频文件未找到")
            return
        }

        do {
            audioPlayer = try AVAudioPlayer(contentsOf: url)
            audioPlayer?.prepareToPlay()
        } catch let error {
            print("初始化音频播放器时出错: \(error.localizedDescription)")
        }
    }
}

在上述代码中,我们首先通过 Bundle.main.url(forResource:withExtension:) 方法获取音频文件的 URL。然后使用这个 URL 初始化 AVAudioPlayerprepareToPlay() 方法会在播放音频之前进行一些必要的准备工作,比如分配音频资源。

  1. 控制播放 一旦初始化完成,就可以控制音频的播放了。常见的控制操作包括播放、暂停和停止:
// 播放音频
audioPlayer?.play()

// 暂停音频
audioPlayer?.pause()

// 停止音频
audioPlayer?.stop()

此外,还可以设置音频的音量、播放次数等属性:

// 设置音量,范围是 0.0 到 1.0
audioPlayer?.volume = 0.5

// 设置循环次数,0 表示不循环,-1 表示无限循环
audioPlayer?.numberOfLoops = -1

1.3 录制音频 - AVAudioRecorder

  1. 设置音频会话与录制设置 在录制音频之前,需要配置音频会话,以确保应用程序能够访问音频输入设备。同时,还需要设置录制的参数,如音频格式、采样率等。
import AVFoundation

class AudioRecorderViewController: UIViewController, AVAudioRecorderDelegate {
    var audioRecorder: AVAudioRecorder?

    override func viewDidLoad() {
        super.viewDidLoad()

        let audioSession = AVAudioSession.sharedInstance()
        do {
            try audioSession.setCategory(.record, mode:.default)
            try audioSession.setActive(true)
        } catch let error {
            print("设置音频会话时出错: \(error.localizedDescription)")
        }

        let recordSettings: [String: Any] = [
            AVFormatIDKey: kAudioFormatMPEG4AAC,
            AVSampleRateKey: 44100,
            AVNumberOfChannelsKey: 1,
            AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
        ]

        guard let url = FileManager.default.urls(for:.documentDirectory, in:.userDomainMask).first?.appendingPathComponent("recorded_audio.m4a") else {
            print("无法获取录制文件的路径")
            return
        }

        do {
            audioRecorder = try AVAudioRecorder(url: url, settings: recordSettings)
            audioRecorder?.delegate = self
            audioRecorder?.prepareToRecord()
        } catch let error {
            print("初始化音频录制器时出错: \(error.localizedDescription)")
        }
    }
}

在上述代码中,我们首先设置音频会话的类别为 .record 并激活它。然后定义了录制设置,这里使用了 AAC 音频格式,采样率为 44100Hz,单声道,高质量编码。接着获取了应用程序文档目录中的一个路径,用于保存录制的音频文件。最后初始化 AVAudioRecorder 并设置代理和准备录制。

  1. 控制录制 控制音频录制的操作包括开始录制、暂停录制和停止录制:
// 开始录制
audioRecorder?.record()

// 暂停录制
audioRecorder?.pause()

// 停止录制
audioRecorder?.stop()

当录制完成后,可以通过代理方法获取录制的结果:

extension AudioRecorderViewController {
    func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
        if flag {
            print("音频录制成功,文件路径: \(recorder.url)")
        } else {
            print("音频录制失败")
        }
    }
}

二、Swift 音频处理进阶

2.1 音频编辑

  1. 音频剪辑 音频剪辑是音频编辑中常见的操作之一。在 AVFoundation 中,可以使用 AVMutableComposition 类来实现音频剪辑。假设我们要从一个音频文件中截取一段音频:
import AVFoundation

func clipAudio(sourceURL: URL, start: CMTime, duration: CMTime) -> URL? {
    let composition = AVMutableComposition()
    guard let audioAsset = AVAsset(url: sourceURL),
          let audioTrack = composition.addMutableTrack(withMediaType:.audio, preferredTrackID: kCMPersistentTrackID_Invalid) else {
        return nil
    }

    do {
        try audioTrack.insertTimeRange(CMTimeRange(start: start, duration: duration), of: audioAsset.tracks(withMediaType:.audio)[0], at: CMTime.zero)
    } catch let error {
        print("插入音频片段时出错: \(error.localizedDescription)")
        return nil
    }

    let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("clipped_audio.m4a")
    if FileManager.default.fileExists(atPath: outputURL.path) {
        try? FileManager.default.removeItem(at: outputURL)
    }

    let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetAppleM4A)
    exporter?.outputURL = outputURL
    exporter?.outputFileType =.m4a
    exporter?.exportAsynchronously(completionHandler: {
        switch exporter?.status {
        case.finished:
            print("音频剪辑成功,输出路径: \(outputURL)")
        case.failed:
            print("音频剪辑失败: \(String(describing: exporter?.error))")
        default:
            break
        }
    })

    return outputURL
}

在上述代码中,我们首先创建了一个 AVMutableComposition 对象,然后从源音频文件的 AVAsset 中获取音频轨道,并将指定时间范围的音频片段插入到 AVMutableComposition 的音频轨道中。接着设置输出文件的路径,并使用 AVAssetExportSession 将合成的音频导出为新的文件。

  1. 音频拼接 音频拼接是将多个音频文件连接在一起。同样可以使用 AVMutableComposition 来实现:
func concatenateAudios(urls: [URL]) -> URL? {
    let composition = AVMutableComposition()
    guard let audioTrack = composition.addMutableTrack(withMediaType:.audio, preferredTrackID: kCMPersistentTrackID_Invalid) else {
        return nil
    }

    var currentTime = CMTime.zero
    for url in urls {
        guard let audioAsset = AVAsset(url: url) else {
            continue
        }

        let audioDuration = audioAsset.duration
        do {
            try audioTrack.insertTimeRange(CMTimeRange(start: CMTime.zero, duration: audioDuration), of: audioAsset.tracks(withMediaType:.audio)[0], at: currentTime)
        } catch let error {
            print("插入音频片段时出错: \(error.localizedDescription)")
            return nil
        }

        currentTime = CMTimeAdd(currentTime, audioDuration)
    }

    let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("concatenated_audio.m4a")
    if FileManager.default.fileExists(atPath: outputURL.path) {
        try? FileManager.default.removeItem(at: outputURL)
    }

    let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetAppleM4A)
    exporter?.outputURL = outputURL
    exporter?.outputFileType =.m4a
    exporter?.exportAsynchronously(completionHandler: {
        switch exporter?.status {
        case.finished:
            print("音频拼接成功,输出路径: \(outputURL)")
        case.failed:
            print("音频拼接失败: \(String(describing: exporter?.error))")
        default:
            break
        }
    })

    return outputURL
}

此代码中,我们遍历传入的音频文件 URL 数组,将每个音频文件的音频片段依次插入到 AVMutableComposition 的音频轨道中。插入的起始时间根据上一个音频片段的结束时间来确定。最后同样使用 AVAssetExportSession 导出拼接后的音频文件。

2.2 音频特效处理

  1. 音量调节特效 音量调节是一种基本的音频特效。在 AVFoundation 中,可以通过修改音频轨道的增益来实现音量调节。假设我们有一个 AVMutableComposition 对象,要对其中的音频轨道进行音量调节:
func adjustVolume(composition: AVMutableComposition, gain: Float) {
    guard let audioTrack = composition.tracks(withMediaType:.audio).first else {
        return
    }

    let audioProcessingSettings: [String: Any] = [
        AVVolumeAdjustmentKey: gain
    ]

    let audioEffect = AVAudioUnitEQ()
    audioEffect.loadFactoryPreset(.flat)
    audioEffect.bypass = false
    audioEffect.globalGain = gain

    let audioMixer = AVAudioMixerNode()
    let audioEngine = AVAudioEngine()

    audioEngine.attach(audioEffect)
    audioEngine.attach(audioMixer)

    audioEngine.connect(audioTrack, to: audioEffect, format: audioTrack.format)
    audioEngine.connect(audioEffect, to: audioMixer, format: audioTrack.format)
    audioEngine.connect(audioMixer, to: audioEngine.mainMixerNode, format: audioTrack.format)

    do {
        try audioEngine.start()
    } catch let error {
        print("启动音频引擎时出错: \(error.localizedDescription)")
    }
}

在上述代码中,我们首先获取 AVMutableComposition 中的音频轨道。然后创建了一个 AVAudioUnitEQ 对象,并设置其全局增益为指定的 gain 值。接着构建了一个简单的音频处理链,包括音频轨道、音频效果单元和音频混合器,最后启动音频引擎来应用音量调节效果。

  1. 回声特效 回声特效可以通过 AVAudioUnitReverb 来实现。以下是为音频添加回声特效的代码示例:
func addEcho(composition: AVMutableComposition) {
    guard let audioTrack = composition.tracks(withMediaType:.audio).first else {
        return
    }

    let audioEffect = AVAudioUnitReverb()
    audioEffect.loadFactoryPreset(.cathedral)
    audioEffect.wetDryMix = 50
    audioEffect.density = 100
    audioEffect.decay = 10

    let audioMixer = AVAudioMixerNode()
    let audioEngine = AVAudioEngine()

    audioEngine.attach(audioEffect)
    audioEngine.attach(audioMixer)

    audioEngine.connect(audioTrack, to: audioEffect, format: audioTrack.format)
    audioEngine.connect(audioEffect, to: audioMixer, format: audioTrack.format)
    audioEngine.connect(audioMixer, to: audioEngine.mainMixerNode, format: audioTrack.format)

    do {
        try audioEngine.start()
    } catch let error {
        print("启动音频引擎时出错: \(error.localizedDescription)")
    }
}

在这段代码中,我们创建了 AVAudioUnitReverb 对象,并加载了一个预设的回声效果(这里是“cathedral”预设)。通过调整 wetDryMixdensitydecay 等属性来控制回声的强度、密度和衰减时间。同样构建音频处理链并启动音频引擎来应用回声特效。

三、Swift 视频处理基础

3.1 视频框架简介

在 Swift 中进行视频处理,AVFoundation 依然是核心框架。它不仅能处理音频,还能处理视频的播放、录制、编辑等操作。此外,Core Video 框架提供了底层的视频处理支持,Core Image 框架则用于视频的图像特效处理。

3.2 播放视频 - AVPlayer

  1. 初始化与设置 使用 AVPlayer 播放视频,首先要创建 AVPlayerItem,它包含了视频的源。假设我们有一个名为 example.mp4 的视频文件在项目资源目录中:
import AVFoundation
import AVKit

class VideoPlayerViewController: UIViewController {
    var player: AVPlayer?
    var playerViewController: AVPlayerViewController?

    override func viewDidLoad() {
        super.viewDidLoad()

        guard let url = Bundle.main.url(forResource: "example", withExtension: "mp4") else {
            print("视频文件未找到")
            return
        }

        let playerItem = AVPlayerItem(url: url)
        player = AVPlayer(playerItem: playerItem)
        playerViewController = AVPlayerViewController()
        playerViewController?.player = player
        addChild(playerViewController!)
        view.addSubview(playerViewController!.view)
        playerViewController!.view.frame = view.bounds
        playerViewController!.didMove(toParent: self)
    }
}

在上述代码中,我们获取视频文件的 URL 并创建 AVPlayerItem。然后使用 AVPlayerItem 初始化 AVPlayer,接着创建 AVPlayerViewController 并将 AVPlayer 关联到它。最后将 AVPlayerViewController 的视图添加到当前视图控制器的视图中。

  1. 控制播放 控制视频播放与音频播放类似,有播放、暂停和停止操作:
// 播放视频
player?.play()

// 暂停视频
player?.pause()

// 停止视频
player?.seek(to: CMTime.zero)

此外,还可以监听视频的播放状态,比如是否正在播放、是否播放完成等:

let keyPath = "status"
player?.addObserver(self, forKeyPath: keyPath, options:.new, context: nil)

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == "status", let player = object as? AVPlayer {
        switch player.status {
        case.readyToPlay:
            print("视频准备好播放")
        case.failed:
            print("视频播放失败: \(String(describing: player.error))")
        case.unknown:
            print("视频状态未知")
        }
    }
}

3.3 录制视频 - AVCaptureSession

  1. 配置捕获会话 要录制视频,需要配置 AVCaptureSession,它管理输入设备(如摄像头)和输出(如文件输出)之间的数据流动。
import AVFoundation

class VideoRecorderViewController: UIViewController, AVCaptureFileOutputRecordingDelegate {
    var captureSession: AVCaptureSession?
    var videoOutput: AVCaptureMovieFileOutput?
    var previewLayer: AVCaptureVideoPreviewLayer?

    override func viewDidLoad() {
        super.viewDidLoad()

        captureSession = AVCaptureSession()
        captureSession?.sessionPreset =.high

        guard let videoDevice = AVCaptureDevice.default(for:.video),
              let videoInput = try? AVCaptureDeviceInput(device: videoDevice),
              let audioDevice = AVCaptureDevice.default(for:.audio),
              let audioInput = try? AVCaptureDeviceInput(device: audioDevice) else {
            print("无法获取输入设备")
            return
        }

        if captureSession?.canAddInput(videoInput) == true {
            captureSession?.addInput(videoInput)
        }

        if captureSession?.canAddInput(audioInput) == true {
            captureSession?.addInput(audioInput)
        }

        videoOutput = AVCaptureMovieFileOutput()
        if captureSession?.canAddOutput(videoOutput!) == true {
            captureSession?.addOutput(videoOutput!)
        }

        previewLayer = AVCaptureVideoPreviewLayer(session: captureSession!)
        previewLayer?.frame = view.bounds
        view.layer.addSublayer(previewLayer!)

        captureSession?.startRunning()
    }
}

在上述代码中,我们首先创建了 AVCaptureSession 并设置其预设为高质量。然后获取摄像头和麦克风设备,并创建对应的输入。将视频和音频输入添加到捕获会话中,接着添加 AVCaptureMovieFileOutput 作为输出。创建预览层并将其添加到视图层,最后启动捕获会话。

  1. 控制录制 控制视频录制的操作包括开始录制和停止录制:
func startRecording() {
    guard let output = videoOutput,
          let connection = output.connection(with:.video) else {
        return
    }

    let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("recorded_video.mp4")
    if FileManager.default.fileExists(atPath: outputURL.path) {
        try? FileManager.default.removeItem(at: outputURL)
    }

    output.startRecording(to: outputURL, recordingDelegate: self)
}

func stopRecording() {
    videoOutput?.stopRecording()
}

extension VideoRecorderViewController {
    func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
        if let error = error {
            print("视频录制失败: \(error.localizedDescription)")
        } else {
            print("视频录制成功,文件路径: \(outputFileURL)")
        }
    }
}

四、Swift 视频处理进阶

4.1 视频编辑

  1. 视频剪辑 视频剪辑与音频剪辑类似,使用 AVMutableComposition。假设我们要从一个视频文件中截取一段视频:
import AVFoundation

func clipVideo(sourceURL: URL, start: CMTime, duration: CMTime) -> URL? {
    let composition = AVMutableComposition()
    guard let videoAsset = AVAsset(url: sourceURL),
          let videoTrack = composition.addMutableTrack(withMediaType:.video, preferredTrackID: kCMPersistentTrackID_Invalid),
          let audioTrack = composition.addMutableTrack(withMediaType:.audio, preferredTrackID: kCMPersistentTrackID_Invalid) else {
        return nil
    }

    do {
        try videoTrack.insertTimeRange(CMTimeRange(start: start, duration: duration), of: videoAsset.tracks(withMediaType:.video)[0], at: CMTime.zero)
        try audioTrack.insertTimeRange(CMTimeRange(start: start, duration: duration), of: videoAsset.tracks(withMediaType:.audio)[0], at: CMTime.zero)
    } catch let error {
        print("插入视频和音频片段时出错: \(error.localizedDescription)")
        return nil
    }

    let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("clipped_video.mp4")
    if FileManager.default.fileExists(atPath: outputURL.path) {
        try? FileManager.default.removeItem(at: outputURL)
    }

    let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)
    exporter?.outputURL = outputURL
    exporter?.outputFileType =.mp4
    exporter?.exportAsynchronously(completionHandler: {
        switch exporter?.status {
        case.finished:
            print("视频剪辑成功,输出路径: \(outputURL)")
        case.failed:
            print("视频剪辑失败: \(String(describing: exporter?.error))")
        default:
            break
        }
    })

    return outputURL
}

此代码中,我们创建 AVMutableComposition 并从源视频 AVAsset 中获取视频和音频轨道,将指定时间范围的视频和音频片段插入到 AVMutableComposition 的相应轨道中。最后使用 AVAssetExportSession 导出剪辑后的视频文件。

  1. 视频拼接 视频拼接同样使用 AVMutableComposition。假设我们有多个视频文件要拼接在一起:
func concatenateVideos(urls: [URL]) -> URL? {
    let composition = AVMutableComposition()
    guard let videoTrack = composition.addMutableTrack(withMediaType:.video, preferredTrackID: kCMPersistentTrackID_Invalid),
          let audioTrack = composition.addMutableTrack(withMediaType:.audio, preferredTrackID: kCMPersistentTrackID_Invalid) else {
        return nil
    }

    var currentVideoTime = CMTime.zero
    var currentAudioTime = CMTime.zero
    for url in urls {
        guard let videoAsset = AVAsset(url: url) else {
            continue
        }

        let videoDuration = videoAsset.duration
        let audioDuration = videoAsset.tracks(withMediaType:.audio).first?.duration?? CMTime.zero

        do {
            try videoTrack.insertTimeRange(CMTimeRange(start: CMTime.zero, duration: videoDuration), of: videoAsset.tracks(withMediaType:.video)[0], at: currentVideoTime)
            if let audioTrackOfAsset = videoAsset.tracks(withMediaType:.audio).first {
                try audioTrack.insertTimeRange(CMTimeRange(start: CMTime.zero, duration: audioDuration), of: audioTrackOfAsset, at: currentAudioTime)
            }
        } catch let error {
            print("插入视频和音频片段时出错: \(error.localizedDescription)")
            return nil
        }

        currentVideoTime = CMTimeAdd(currentVideoTime, videoDuration)
        currentAudioTime = CMTimeAdd(currentAudioTime, audioDuration)
    }

    let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("concatenated_video.mp4")
    if FileManager.default.fileExists(atPath: outputURL.path) {
        try? FileManager.default.removeItem(at: outputURL)
    }

    let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)
    exporter?.outputURL = outputURL
    exporter?.outputFileType =.mp4
    exporter?.exportAsynchronously(completionHandler: {
        switch exporter?.status {
        case.finished:
            print("视频拼接成功,输出路径: \(outputURL)")
        case.failed:
            print("视频拼接失败: \(String(describing: exporter?.error))")
        default:
            break
        }
    })

    return outputURL
}

在这段代码中,我们遍历视频文件 URL 数组,依次将每个视频的视频和音频片段插入到 AVMutableComposition 的相应轨道中。注意处理音频轨道可能不存在的情况。最后导出拼接后的视频文件。

4.2 视频特效处理

  1. 添加滤镜特效 使用 Core Image 框架可以为视频添加滤镜特效。假设我们要为视频添加一个高斯模糊滤镜:
import AVFoundation
import CoreImage
import CoreVideo

func addBlurEffectToVideo(sourceURL: URL) -> URL? {
    let composition = AVMutableComposition()
    guard let videoAsset = AVAsset(url: sourceURL),
          let videoTrack = composition.addMutableTrack(withMediaType:.video, preferredTrackID: kCMPersistentTrackID_Invalid) else {
        return nil
    }

    let videoDuration = videoAsset.duration
    do {
        try videoTrack.insertTimeRange(CMTimeRange(start: CMTime.zero, duration: videoDuration), of: videoAsset.tracks(withMediaType:.video)[0], at: CMTime.zero)
    } catch let error {
        print("插入视频片段时出错: \(error.localizedDescription)")
        return nil
    }

    let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("blurred_video.mp4")
    if FileManager.default.fileExists(atPath: outputURL.path) {
        try? FileManager.default.removeItem(at: outputURL)
    }

    let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)
    exporter?.outputURL = outputURL
    exporter?.outputFileType =.mp4

    let blurFilter = CIFilter(name: "CIGaussianBlur")!
    blurFilter.setValue(10, forKey: kCIInputRadiusKey)

    let videoComposition = AVMutableVideoComposition(propertiesOf: videoAsset)
    videoComposition.frameDuration = CMTimeMake(1, 30)
    videoComposition.renderSize = CGSize(width: 1280, height: 720)

    videoComposition.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: { (request) -> CALayer in
        let source = CIImage(cvPixelBuffer: request.sourceFrame.imageBuffer)
        blurFilter.setValue(source, forKey: kCIInputImageKey)
        let output = blurFilter.outputImage!
        let ciContext = CIContext()
        let cgImage = ciContext.createCGImage(output, from: output.extent)!
        let layer = CALayer()
        layer.contents = cgImage
        layer.frame = CGRect(x: 0, y: 0, width: request.sourceFrame.displaySize.width, height: request.sourceFrame.displaySize.height)
        return layer
    }, in: { (request) -> AVAsynchronousCIImageFilteringRequestCompletionHandler in
        return { (filterOutput: CIImage?, _: Error?) in
            if let filterOutput = filterOutput {
                request.finish(with: filterOutput, context: nil)
            } else {
                request.finish(with: request.sourceFrame.imageBuffer, context: nil)
            }
        }
    })

    exporter?.videoComposition = videoComposition
    exporter?.exportAsynchronously(completionHandler: {
        switch exporter?.status {
        case.finished:
            print("添加模糊特效成功,输出路径: \(outputURL)")
        case.failed:
            print("添加模糊特效失败: \(String(describing: exporter?.error))")
        default:
            break
        }
    })

    return outputURL
}

在上述代码中,我们首先创建 AVMutableComposition 并插入视频片段。然后创建一个高斯模糊滤镜 CIGaussianBlur 并设置其半径。接着构建 AVMutableVideoComposition,并使用 AVVideoCompositionCoreAnimationTool 来应用滤镜特效。最后使用 AVAssetExportSession 导出添加滤镜后的视频文件。

  1. 添加转场特效 添加转场特效可以通过 AVVideoComposition 来实现。假设我们要在两个视频片段之间添加一个淡入淡出的转场特效:
func addFadeTransitionVideos(urls: [URL]) -> URL? {
    let composition = AVMutableComposition()
    guard let videoTrack = composition.addMutableTrack(withMediaType:.video, preferredTrackID: kCMPersistentTrackID_Invalid),
          let audioTrack = composition.addMutableTrack(withMediaType:.audio, preferredTrackID: kCMPersistentTrackID_Invalid) else {
        return nil
    }

    var currentVideoTime = CMTime.zero
    var currentAudioTime = CMTime.zero
    for (index, url) in urls.enumerated() {
        guard let videoAsset = AVAsset(url: url) else {
            continue
        }

        let videoDuration = videoAsset.duration
        let audioDuration = videoAsset.tracks(withMediaType:.audio).first?.duration?? CMTime.zero

        if index > 0 {
            let transitionDuration = CMTimeMake(1, 2)
            let startOfNextClip = currentVideoTime
            let endOfPreviousClip = CMTimeSubtract(startOfNextClip, transitionDuration)

            let fadeOutVideoInstruction = AVMutableVideoCompositionInstruction()
            fadeOutVideoInstruction.timeRange = CMTimeRange(start: endOfPreviousClip, duration: transitionDuration)
            fadeOutVideoInstruction.layerInstructions = [AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)]
            fadeOutVideoInstruction.layerInstructions[0].setOpacityRamp(fromStart: 1.0, toEnd: 0.0, timeRange: fadeOutVideoInstruction.timeRange)

            let fadeInVideoInstruction = AVMutableVideoCompositionInstruction()
            fadeInVideoInstruction.timeRange = CMTimeRange(start: startOfNextClip, duration: transitionDuration)
            fadeInVideoInstruction.layerInstructions = [AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)]
            fadeInVideoInstruction.layerInstructions[0].setOpacityRamp(fromStart: 0.0, toEnd: 1.0, timeRange: fadeInVideoInstruction.timeRange)

            let videoComposition = AVMutableVideoComposition(propertiesOf: videoAsset)
            videoComposition.frameDuration = CMTimeMake(1, 30)
            videoComposition.renderSize = CGSize(width: 1280, height: 720)
            videoComposition.instructions = [fadeOutVideoInstruction, fadeInVideoInstruction]

            let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)
            exporter?.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("transition_video.mp4")
            exporter?.outputFileType =.mp4
            exporter?.videoComposition = videoComposition
            exporter?.exportAsynchronously(completionHandler: {
                switch exporter?.status {
                case.finished:
                    print("添加转场特效成功,输出路径: \(exporter?.outputURL?? URL(fileURLWithPath: ""))")
                case.failed:
                    print("添加转场特效失败: \(String(describing: exporter?.error))")
                default:
                    break
                }
            })
        }

        do {
            try videoTrack.insertTimeRange(CMTimeRange(start: CMTime.zero, duration: videoDuration), of: videoAsset.tracks(withMediaType:.video)[0], at: currentVideoTime)
            if let audioTrackOfAsset = videoAsset.tracks(withMediaType:.audio).first {
                try audioTrack.insertTimeRange(CMTimeRange(start: CMTime.zero, duration: audioDuration), of: audioTrackOfAsset, at: currentAudioTime)
            }
        } catch let error {
            print("插入视频和音频片段时出错: \(error.localizedDescription)")
            return nil
        }

        currentVideoTime = CMTimeAdd(currentVideoTime, videoDuration)
        currentAudioTime = CMTimeAdd(currentAudioTime, audioDuration)
    }

    return FileManager.default.temporaryDirectory.appendingPathComponent("transition_video.mp4")
}

在这段代码中,我们遍历视频文件 URL 数组,当处理第二个及以后的视频片段时,创建淡入淡出的转场指令 AVMutableVideoCompositionInstruction。通过设置透明度渐变来实现淡入淡出效果。然后构建 AVMutableVideoComposition 并将转场指令添加进去,最后使用 AVAssetExportSession 导出添加转场特效后的视频文件。