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
相关产品推荐
相关产品推荐

