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

iOS Swift中AVMutableVideoComposition合并视频文字覆盖层不显示问题

iOS Swift视频合并时文字覆盖层不显示问题

我在iOS平台使用Swift开发,拥有一个AVURLAsset数组,通过自定义函数将这些视频资源拼接合并为一个最终视频,目标是为每个视频帧添加居中显示的文字覆盖层。

但输出的视频仅成功完成了视频资源的合并,所有视频帧均未显示文字覆盖层。我尝试参考现有解决方案及单视频添加文字覆盖层的教程,最终结果一致,文字仍无法显示。

以下是我的merge函数代码:

func merge(videos: [AVURLAsset], completion: @escaping (_ url: URL, _ asset: AVAssetExportSession)->()) {
    let videoComposition = AVMutableComposition()
    var lastTime: CMTime = .zero

    var maxVideoSize = CGSize.zero

    guard let videoCompositionTrack = videoComposition.addMutableTrack(withMediaType: .video, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)),
          let audioCompositionTrack = videoComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else { return }

    let mainComposition = AVMutableVideoComposition()

    let mainParentLayer = CALayer()
    let mainVideoLayer = CALayer()
    mainParentLayer.frame = CGRect(x: 0, y: 0, width: maxVideoSize.width, height: maxVideoSize.height)
    mainVideoLayer.frame = CGRect(x: 0, y: 0, width: maxVideoSize.width, height: maxVideoSize.height)

    mainParentLayer.addSublayer(mainVideoLayer)

    var instructions = [AVMutableVideoCompositionInstruction]()
        
    print("Main Parent Layer Frame: \(mainParentLayer.frame)")

    for video in videos {
        
        if let videoTrack = video.tracks(withMediaType: .video)[safe: 0] {
            videoCompositionTrack.preferredTransform = videoTrack.preferredTransform
            print("Video Track Transform: \(videoTrack.preferredTransform)")

            do {
                try videoCompositionTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: video.duration), of: videoTrack, at: lastTime)
                print("Video time range: Start = \(lastTime.seconds), Duration = \(video.duration.seconds) [\(lastTime.seconds + video.duration.seconds > 0 ? "Right" : "Wrong")]")

                if let audioTrack = video.tracks(withMediaType: .audio)[safe: 0] {
                    try audioCompositionTrack.insertTimeRange(CMTimeRangeMake(start: .zero, duration: video.duration), of: audioTrack, at: lastTime)
                }

                lastTime = CMTimeAdd(lastTime, video.duration)

                let videoSize = videoTrack.naturalSize.applying(videoTrack.preferredTransform)
                let videoRect = CGRect(x: 0, y: 0, width: abs(videoSize.width), height: abs(videoSize.height))
                maxVideoSize = CGSize(width: max(maxVideoSize.width, videoRect.width), height: max(maxVideoSize.height, videoRect.height))

                let textLayer = CATextLayer()
                textLayer.string = "TESTING TESTING TESTING"
                textLayer.foregroundColor = UIColor.white.cgColor
                textLayer.font = UIFont(name: "Helvetica-Bold", size: min(videoRect.height / 10, 100))
                textLayer.shadowOpacity = 0.5
                textLayer.alignmentMode = .center
                textLayer.contentsScale = UIScreen.main.scale
                textLayer.isWrapped = true

                let textHeight: CGFloat = min(videoRect.height / 10, 120)
                let textWidth: CGFloat = videoRect.width
                let xPos = (videoRect.width - textWidth) / 2
                let yPos = (videoRect.height - textHeight) / 2
                textLayer.frame = CGRect(x: xPos, y: yPos, width: textWidth, height: textHeight)
                textLayer.zPosition = 1
                print("Text Layer Frame: \(textLayer.frame) [\(textLayer.frame.width > 0 && textLayer.frame.height > 0 ? "Right" : "Wrong")]")

                let parentLayer = CALayer()
                parentLayer.backgroundColor = UIColor.green.cgColor // Temp background color for debugging
                parentLayer.frame = videoRect
                parentLayer.addSublayer(textLayer)
                
                print("Video Layer zPosition: \(mainVideoLayer.zPosition), Text Layer zPosition: \(textLayer.zPosition) [\(textLayer.zPosition > mainVideoLayer.zPosition ? "Right" : "Wrong")]")

                let videoCompositionInstruction = AVMutableVideoCompositionInstruction()
                videoCompositionInstruction.timeRange = CMTimeRangeMake(start: lastTime - video.duration, duration: video.duration)
                let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
                videoCompositionInstruction.layerInstructions = [layerInstruction]
                instructions.append(videoCompositionInstruction)

                parentLayer.zPosition = 0
                mainParentLayer.addSublayer(parentLayer)
                
                print("Parent Layer Frame: \(parentLayer.frame), Background Color: \(parentLayer.backgroundColor.debugDescription)")
                print("Text Layer Frame: \(textLayer.frame)")
                
            } catch {
                print("Failed to insert track: \(error.localizedDescription)")
                return
            }
        }
    }

    mainParentLayer.frame = CGRect(x: 0, y: 0, width: maxVideoSize.width, height: maxVideoSize.height)
    mainVideoLayer.frame = mainParentLayer.frame

    mainComposition.renderSize = maxVideoSize
    mainComposition.instructions = instructions
    mainComposition.frameDuration = CMTime(value: 1, timescale: 30)
    mainComposition.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: mainVideoLayer, in: mainParentLayer)

    print("Final Main Parent Layer Frame: \(mainParentLayer.frame)")
    print("Number of Sublayers in Main Parent Layer: \(mainParentLayer.sublayers?.count ?? 0)")

    let outputUrl = NSURL.fileURL(withPath: NSTemporaryDirectory() + "merged" + ".mp4")

    print("Pre-Export Main Parent Layer Frame: \(mainParentLayer.frame)")
    print("Pre-Export Number of Sublayers in Main Parent Layer: \(mainParentLayer.sublayers?.count ?? 0)")
    if let sublayers = mainParentLayer.sublayers {
        for (index, layer) in sublayers.enumerated() {
            print("Layer \(index): \(layer), Frame: \(layer.frame), zPosition: \(layer.zPosition)")
        }
    }

    guard let exporter = AVAssetExportSession(asset: videoComposition, presetName: AVAssetExportPresetHighestQuality) else { return }

    exporter.videoComposition = mainComposition
    exporter.outputURL = outputUrl
    exporter.outputFileType = .mp4
    exporter.shouldOptimizeForNetworkUse = true

    if let videoComposition = exporter.videoComposition {
        print("Export Video Composition Render Size: \(videoComposition.renderSize)")
        print("Export Video Composition Frame Duration: \(videoComposition.frameDuration)")
        print("Export Video Composition Instructions Count: \(videoComposition.instructions.count)")
    }

    exporter.exportAsynchronously {
        DispatchQueue.main.async {
            if let outputUrl = exporter.outputURL, exporter.status == .completed {
                completion(outputUrl, exporter)
            } else if let error = exporter.error {
                print("Export failed: \(error.localizedDescription)")
            }
        }
    }
    play(video: exporter.asset)
}

问题分析与修复方案

1. 图层层级覆盖问题

你设置了parentLayer.zPosition = 0,而mainVideoLayer默认zPosition也是0,导致文字所在的parentLayer被视频层遮挡。需要将parentLayer的zPosition设为大于mainVideoLayer的值:

parentLayer.zPosition = 1 // 改为1,确保在视频层上方

2. 初始图层尺寸错误

一开始maxVideoSize是CGSize.zero,导致mainParentLayer和mainVideoLayer初始帧为空,虽然后续修改了,但可能影响CoreAnimationTool的绑定逻辑。建议在循环结束确定maxVideoSize后再初始化这两个图层:

// 移到循环结束后初始化图层
let mainParentLayer = CALayer()
let mainVideoLayer = CALayer()
mainParentLayer.frame = CGRect(x: 0, y: 0, width: maxVideoSize.width, height: maxVideoSize.height)
mainVideoLayer.frame = mainParentLayer.frame
mainParentLayer.addSublayer(mainVideoLayer)

3. 视频轨道变换设置错误

直接给videoCompositionTrack.preferredTransform赋值会导致后续视频的变换被覆盖,应该在AVMutableVideoCompositionLayerInstruction中设置变换:

let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoCompositionTrack)
layerInstruction.setTransform(videoTrack.preferredTransform, at: lastTime - video.duration)

4. CATextLayer字体设置问题

UIFont直接赋值给CATextLayer的font属性可能失效,改用CTFont确保字体生效:

let fontSize = min(videoRect.height / 10, 100)
textLayer.font = CTFontCreateWithName("Helvetica-Bold" as CFString, fontSize, nil)
textLayer.fontSize = fontSize // 额外设置fontSize保证显示正常

简化修复后的核心代码片段

func merge(videos: [AVURLAsset], completion: @escaping (_ url: URL, _ asset: AVAssetExportSession)->()) {
    let videoComposition = AVMutableComposition()
    var lastTime: CMTime = .zero
    var maxVideoSize = CGSize.zero

    guard let videoCompositionTrack = videoComposition.addMutableTrack(withMediaType: .video, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)),
          let audioCompositionTrack = videoComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: Int32(kCMPersistentTrackID_Invalid)) else { return }

    var instructions = [AVMutableVideoCompositionInstruction]()
    var textParentLayers = [CALayer]() // 保存所有文字父图层

    for video in videos {
        if let videoTrack = video.tracks(withMediaType: .video).first {
            do {
                try videoCompositionTrack.insertTimeRange(CMTimeRange(start: .zero, duration: video.duration), of: videoTrack, at: lastTime)
                
                if let audioTrack = video.tracks(withMediaType: .audio).first {
                    try audioCompositionTrack.insertTimeRange(CMTimeRange(start: .zero, duration: video.duration), of: audioTrack, at: lastTime)
                }

                let videoSize = videoTrack.naturalSize.applying(videoTrack.preferredTransform)
                let videoRect = CGRect(x: 0, y: 0, width: abs(videoSize.width), height: abs(videoSize.height))
                maxVideoSize = CGSize(width: max(maxVideoSize.width, videoRect.width), height: max(maxVideoSize.height, videoRect.height))

                // 创建文字层
                let textLayer = CATextLayer()
                textLayer.string = "TESTING TESTING TESTING"
                textLayer.foregroundColor = UIColor.white.cgColor
                let fontSize = min(videoRect.height / 10, 100)
                textLayer.font = CTFontCreateWithName("Helvetica-Bold" as CFString, fontSize, nil)
                textLayer.fontSize = fontSize
                textLayer.shadowOpacity = 0.5
                textLayer.alignmentMode = .center
                textLayer.contentsScale = UIScreen.main.scale
                textLayer.isWrapped = true
                textLayer.frame = CGRect(x: 0, y: (videoRect.height - fontSize) / 2, width: videoRect.width, height: fontSize)

                let parentLayer = CALayer()
                parentLayer.backgroundColor = UIColor.green.cgColor
                parentLayer.frame = videoRect
                parentLayer.addSublayer(textLayer)
                parentLayer.zPosition = 1 // 确保在视频层上方
                textParentLayers.append(parentLayer)

                // 创建图层指令
                let instruction = AVMutableVideoCompositionInstruction()
                instruction.timeRange = CMTimeRange(start: lastTime, duration: video.duration)
                let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoCompositionTrack)
                layerInstruction.setTransform(videoTrack.preferredTransform, at: lastTime)
                instruction.layerInstructions = [layerInstruction]
                instructions.append(instruction)

                lastTime = CMTimeAdd(lastTime, video.duration)
            } catch {
                print("Failed to insert track: \(error.localizedDescription)")
                return
            }
        }
    }

    // 初始化合成图层
    let mainComposition = AVMutableVideoComposition()
    let mainParentLayer = CALayer()
    let mainVideoLayer = CALayer()
    mainParentLayer.frame = CGRect(x: 0, y: 0, width: maxVideoSize.width, height: maxVideoSize.height)
    mainVideoLayer.frame = mainParentLayer.frame
    mainParentLayer.addSublayer(mainVideoLayer)
    
    // 添加所有文字父图层
    textParentLayers.forEach { mainParentLayer.addSublayer($0) }

    mainComposition.renderSize = maxVideoSize
    mainComposition.instructions = instructions
    mainComposition.frameDuration = CMTime(value: 1, timescale: 30)
    mainComposition.animationTool = AVVideoCompositionCoreAnimationTool(postProcessingAsVideoLayer: mainVideoLayer, in: mainParentLayer)

    // 导出逻辑
    let outputUrl = URL(fileURLWithPath: NSTemporaryDirectory() + "merged.mp4")
    guard let exporter = AVAssetExportSession(asset: videoComposition, presetName: AVAssetExportPresetHighestQuality) else { return }
    
    exporter.videoComposition = mainComposition
    exporter.outputURL = outputUrl
    exporter.outputFileType = .mp4
    exporter.shouldOptimizeForNetworkUse = true

    exporter.exportAsynchronously {
        DispatchQueue.main.async {
            if let outputUrl = exporter.outputURL, exporter.status == .completed {
                completion(outputUrl, exporter)
            } else if let error = exporter.error {
                print("Export failed: \(error.localizedDescription)")
            }
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 03:54:51