You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift视频添加水印速度极慢,求优化方案及第三方库推荐

视频添加水印速度慢的优化思路及第三方库推荐

我用Swift实现了本地视频添加图片和文字水印的功能,但目前处理速度极慢,恳请大家提供优化思路,同时推荐靠谱的第三方水印库。

我的实现代码如下:

public func addWatermark(
    fromVideoAt videoURL: URL,
    watermark: Watermark,
    fileName: String,
    onSuccess: @escaping (URL) -> Void,
    onFailure: @escaping ((Error?) -> Void)
) {
    let asset = AVURLAsset(url: videoURL)
    let composition = AVMutableComposition()

    guard
        let compositionTrack = composition.addMutableTrack(
            withMediaType: .video,
            preferredTrackID: kCMPersistentTrackID_Invalid
        ),
        let assetTrack = asset.tracks(withMediaType: .video).first
    else {
        onFailure(nil)
        return
    }

    do {
        let timeRange = CMTimeRange(start: .zero, duration: assetTrack.timeRange.duration)
        try compositionTrack.insertTimeRange(timeRange, of: assetTrack, at: .zero)

        if let audioAssetTrack = asset.tracks(withMediaType: .audio).first,
           let compositionAudioTrack = composition.addMutableTrack(
               withMediaType: .audio,
               preferredTrackID: kCMPersistentTrackID_Invalid
           ) {
            try compositionAudioTrack.insertTimeRange(
                timeRange,
                of: audioAssetTrack,
                at: .zero
            )
        }
    } catch {
        onFailure(error)
        return
    }

    compositionTrack.preferredTransform = assetTrack.preferredTransform
    let videoInfo = orientation(from: assetTrack.preferredTransform)

    let videoSize: CGSize
    if videoInfo.isPortrait {
        videoSize = CGSize(
            width: assetTrack.naturalSize.height,
            height: assetTrack.naturalSize.width
        )
    } else {
        videoSize = assetTrack.naturalSize
    }

    let videoLayer = CALayer()
    videoLayer.frame = CGRect(origin: .zero, size: videoSize)
    let overlayLayer = CALayer()
    overlayLayer.frame = CGRect(origin: .zero, size: videoSize)

    videoLayer.frame = CGRect(x: 0, y: 0, width: videoSize.width, height: videoSize.height)

    let imageFrame = watermark.calculateImageFrame(parentSize: videoSize)
    addImage(watermark.image, to: overlayLayer, frame: imageFrame)
    let textOrigin = CGPoint(x: imageFrame.minX + 4, y: imageFrame.minY)
    if let text = watermark.text {
        addText(
            text,
            to: overlayLayer,
            origin: textOrigin,
            textAttributes: Watermark.textAttributes(type: watermark.type)
        )
    }

    let outputLayer = CALayer()
    outputLayer.frame = CGRect(origin: .zero, size: videoSize)
    outputLayer.addSublayer(videoLayer)
    outputLayer.addSublayer(overlayLayer)

    let videoComposition = AVMutableVideoComposition()
    videoComposition.renderSize = videoSize
    videoComposition.frameDuration = CMTime(value: 1, timescale: 60)
    videoComposition.animationTool = AVVideoCompositionCoreAnimationTool(
        postProcessingAsVideoLayer: videoLayer,
        in: outputLayer
    )
    videoComposition.colorPrimaries = AVVideoColorPrimaries_ITU_R_709_2
    videoComposition.colorTransferFunction = "sRGB"
    videoComposition.colorYCbCrMatrix = nil

    let instruction = AVMutableVideoCompositionInstruction()
    instruction.timeRange = CMTimeRange(start: .zero, duration: composition.duration)
    videoComposition.instructions = [instruction]
    let layerInstruction = compositionLayerInstruction(
        for: compositionTrack,
        assetTrack: assetTrack
    )
    instruction.layerInstructions = [layerInstruction]

    guard let export = AVAssetExportSession(
        asset: composition,
        presetName: AVAssetExportPresetHighestQuality
    )
    else {
        onFailure(nil)
        return
    }

    let exportURL = URL(fileURLWithPath: NSTemporaryDirectory())
        .appendingPathComponent(fileName)
        .appendingPathExtension("mov")

    export.videoComposition = videoComposition
    export.outputFileType = .mov
    export.outputURL = exportURL

    export.exportAsynchronously {
        DispatchQueue.main.async {
            switch export.status {
            case .completed:
                onSuccess(exportURL)
            default:
                onFailure(export.error)
            }
        }
    }
}

Watermark是自定义结构体,封装了水印的图片/文字内容、文本属性、尺寸位置等信息。

我已经尝试过以下优化方案,但都没有效果:

  • 设置export.shouldOptimizeForNetworkUse = true,无明显提升
  • 用AVAssetExportPresetPassthrough替代AVAssetExportPresetHighestQuality,结果水印直接消失

优化思路

  • 替换CoreAnimation渲染方式:当前使用的AVVideoCompositionCoreAnimationTool依赖CPU渲染,性能瓶颈明显。建议改用自定义AVVideoCompositionInstruction结合AVVideoCompositionRenderContext,或直接用Metal/OpenGL做GPU加速渲染,能大幅提升处理速度。
  • 降低导出编码开销:AVAssetExportPresetHighestQuality会强制全量重新编码,耗时极长。可换成AVAssetExportPresetMediumQuality,或手动设置导出的比特率、分辨率等参数,尽量匹配原视频编码规格,减少编码工作量。
  • 复用原视频轨道参数:如果原视频编码格式支持,尝试保留原视频的编码参数,仅对需要添加水印的帧进行处理,避免整个视频重新编码。
  • 简化图层结构:把图片和文字水印合并成单个图层再渲染,减少图层层级,降低CoreAnimation的渲染负担(如果暂时不换渲染方式的话)。
  • 确保后台处理独立性:虽然代码用了exportAsynchronously,但要确认导出操作在独立的后台队列执行,避免占用主线程资源影响效率。

第三方库推荐

  • GPUImage:基于GPU的多媒体处理库,支持用滤镜快速添加图片/文字水印,GPU加速比CoreAnimation快很多,API友好易上手。
  • FFmpegKit:封装FFmpeg的Swift库,底层用FFmpeg处理视频,性能拉满,适合复杂水印需求(如动态水印、多水印叠加),但学习成本稍高。
  • VideoEditorSDK:功能全面的视频编辑SDK,内置多种水印类型的添加功能,适合快速集成,部分高级功能需付费。

内容的提问来源于stack exchange,提问作者Levan Karanadze

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 06:35:17