自定义Video Compositor导致视频左旋90度的问题求助
解决方案:自定义Compositor中手动应用视频变换并匹配字幕层尺寸
当启用自定义AVVideoCompositing时,系统不会自动执行AVMutableVideoCompositionLayerInstruction中设置的transform,这是导致视频旋转问题的核心原因。你需要在自定义Compositor中手动处理视频帧的变换,同时确保字幕层的尺寸与最终输出尺寸匹配,避免字幕错位。
步骤1:调整合成代码,传递变换信息并匹配字幕层尺寸
首先在合成代码中计算正确的输出尺寸,将视频变换信息传递给自定义指令,并设置字幕层的尺寸为最终输出尺寸:
let subtitleLayer = VideoSubtitleLayer() // 计算应用transform后的正确输出尺寸 let transformedSize = videoTrack.naturalSize.applying(videoTrack.preferredTransform) let outputSize = CGSize(width: abs(transformedSize.width), height: abs(transformedSize.height)) subtitleLayer.frame = CGRect(origin: .zero, size: outputSize) // 匹配输出尺寸 let videoComposition = AVMutableVideoComposition() videoComposition.frameDuration = CMTime(value: 1, timescale: 30) videoComposition.renderSize = outputSize // 使用正确的输出尺寸,无需手动交换宽高 let defaultPosition = CGPoint(x: outputSize.width * 0.5, y: outputSize.height * 0.9) let instruction = VideoSubtitleInstruction( timeRange: CMTimeRange(start: .zero, duration: videoAsset.duration), subtitleLayer: subtitleLayer, savedSubtitlePosition: savedSubtitlePosition ?? defaultPosition ) instruction.preferredTransform = videoTrack.preferredTransform // 传递变换信息 let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: compositionVideoTrack) // 自定义Compositor会手动处理transform,这里可以移除setTransform调用,或者保留不影响 // layerInstruction.setTransform(videoTrack.preferredTransform, at: .zero) instruction.layerInstructions = [layerInstruction] videoComposition.instructions = [instruction] videoComposition.customVideoCompositorClass = SubtitleCompositor.self
同时需要给VideoSubtitleInstruction新增存储变换的属性:
class VideoSubtitleInstruction: AVMutableVideoCompositionInstruction { var subtitleLayer: VideoSubtitleLayer? var savedSubtitlePosition: CGPoint? var preferredTransform: CGAffineTransform? // 新增属性 }
步骤2:修改自定义Compositor,手动应用视频变换
替换原来的copyPixelBuffer方法,改为通过CIImage应用变换并绘制源视频帧,确保视频方向正确:
class SubtitleCompositor: NSObject, AVVideoCompositing { let subtitles: [Subtitle] override init() { self.subtitles = SubtitleStore.shared.subtitles super.init() } var requiredPixelBufferAttributesForRenderContext: [String : Any] = [ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB ] var sourcePixelBufferAttributes: [String : Any]? = [ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB ] private var renderContext: AVVideoCompositionRenderContext? private let ciContext = CIContext(options: [.useSoftwareRenderer: false]) // 复用CIContext提升性能 func renderContextChanged(_ newRenderContext: AVVideoCompositionRenderContext) { renderContext = newRenderContext } func startRequest(_ request: AVAsynchronousVideoCompositionRequest) { guard let renderContext = renderContext else { print("Render context is nil.") request.finish(with: NSError(domain: "SubtitleCompositor", code: -1, userInfo: nil)) return } guard let sourcePixelBuffer = request.sourceFrame(byTrackID: request.sourceTrackIDs[0].int32Value) else { print("Source pixel buffer is nil.") request.finish(with: NSError(domain: "SubtitleCompositor", code: -2, userInfo: nil)) return } guard let destinationPixelBuffer = renderContext.newPixelBuffer() else { print("Failed to create destination pixel buffer.") request.finish(with: NSError(domain: "SubtitleCompositor", code: -3, userInfo: nil)) return } guard let videoInstruction = request.videoCompositionInstruction as? VideoSubtitleInstruction, let subtitleLayer = videoInstruction.subtitleLayer, let transform = videoInstruction.preferredTransform else { print("Video instruction, subtitle layer or transform is nil.") request.finish(withComposedVideoFrame: destinationPixelBuffer) return } // 应用变换绘制源视频到目标Buffer drawSourcePixelBuffer(sourcePixelBuffer, to: destinationPixelBuffer, transform: transform) DispatchQueue.main.sync { let currentTime = request.compositionTime.seconds if let subtitle = subtitles.first(where: { $0.startTime <= currentTime && $0.endTime > currentTime }) { subtitleLayer.updateSubtitle(subtitle: subtitle, time: CFTimeInterval(currentTime)) } else { subtitleLayer.updateSubtitle(subtitle: nil, time: CFTimeInterval(currentTime)) } if let position = videoInstruction.savedSubtitlePosition { subtitleLayer.setInitialPosition(position) } renderLayer(subtitleLayer, to: destinationPixelBuffer) } request.finish(withComposedVideoFrame: destinationPixelBuffer) } // 替换原copyPixelBuffer方法,手动应用变换绘制源视频 private func drawSourcePixelBuffer(_ sourcePixelBuffer: CVPixelBuffer, to destinationPixelBuffer: CVPixelBuffer, transform: CGAffineTransform) { CVPixelBufferLockBaseAddress(sourcePixelBuffer, .readOnly) CVPixelBufferLockBaseAddress(destinationPixelBuffer, []) defer { CVPixelBufferUnlockBaseAddress(sourcePixelBuffer, .readOnly) CVPixelBufferUnlockBaseAddress(destinationPixelBuffer, []) } guard let context = CGContext(data: CVPixelBufferGetBaseAddress(destinationPixelBuffer), width: CVPixelBufferGetWidth(destinationPixelBuffer), height: CVPixelBufferGetHeight(destinationPixelBuffer), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(destinationPixelBuffer), space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue) else { return } // 清空目标Buffer背景 context.clear(context.bounds) // 将源像素Buffer转为CIImage并应用变换 let sourceImage = CIImage(cvPixelBuffer: sourcePixelBuffer) let transformedImage = sourceImage.transformed(by: transform) // 绘制变换后的视频帧到目标Buffer ciContext.draw(transformedImage, in: context.bounds, from: transformedImage.extent) } private func renderLayer(_ layer: CALayer, to pixelBuffer: CVPixelBuffer) { CVPixelBufferLockBaseAddress(pixelBuffer, []) guard let context = CGContext(data: CVPixelBufferGetBaseAddress(pixelBuffer), width: CVPixelBufferGetWidth(pixelBuffer), height: CVPixelBufferGetHeight(pixelBuffer), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer), space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue) else { CVPixelBufferUnlockBaseAddress(pixelBuffer, []) return } // 翻转上下文匹配CALayer坐标系 context.translateBy(x: 0, y: CGFloat(CVPixelBufferGetHeight(pixelBuffer))) context.scaleBy(x: 1.0, y: -1.0) // 渲染字幕层 if !layer.bounds.isEmpty && layer.sublayers?.isEmpty == false { layer.render(in: context) } CVPixelBufferUnlockBaseAddress(pixelBuffer, []) } }
关键说明
- 自定义Compositor的独立性:系统默认的合成逻辑会自动处理
layerInstruction中的变换,但自定义Compositor完全接管了渲染流程,所以必须手动应用视频的preferredTransform。 - 尺寸匹配:字幕层的尺寸必须与最终输出的
renderSize一致,否则会出现字幕位置偏移或拉伸的问题。 - 性能优化:复用
CIContext避免每次渲染都创建新实例,提升合成效率。
内容的提问来源于stack exchange,提问作者Azat
相关产品推荐
相关产品推荐

