SpriteKit开发实时视频编辑器:如何将SKScene转换为视频文件
实现SKScene导出视频方案
你的技术方案是可行的,SpriteKit场景可以通过逐帧渲染+AVFoundation写入的方式导出为本地视频文件,以下是完整实现流程:
核心实现步骤
- 配置AVAssetWriter导出参数:指定导出路径、视频编码格式、分辨率、帧率等参数
- 创建离线SKView用来渲染场景,不需要添加到当前视图层级
- 手动逐帧控制SKScene的动画进度,将每帧内容渲染为CMSampleBuffer写入视频
- 所有帧写入完成后结束写入,拿到导出的视频文件
完整代码示例
首先你的SKScene代码不需要做太大改动,只需要对外暴露动画总时长,方便导出时使用:
class MyScene: SKScene { // 动画总时长,对外暴露方便导出时调用 let animationDuration: TimeInterval = 3.0 override func didMove(to view: SKView) { let box2 = SKLabelNode(text: "HELLOO") box2.fontSize = 60 box2.color = .white box2.position = CGPoint(x: size.width / 2, y: size.height / 2 - 80) addChild(box2) let actionMove: SKAction = SKAction.move(to: CGPoint(x: 200, y: 100), duration: animationDuration) box2.run(actionMove) } }
然后是导出工具的核心代码:
import AVFoundation import SpriteKit import UIKit func exportSceneToVideo(sceneSize: CGSize, fps: Int = 30, completion: @escaping (URL?, Error?) -> Void) { // 1. 配置导出路径 let outputPath = NSTemporaryDirectory().appending("exportedVideo.mp4") let outputURL = URL(fileURLWithPath: outputPath) // 先删除已存在的旧文件 try? FileManager.default.removeItem(at: outputURL) // 2. 初始化AVAssetWriter guard let writer = try? AVAssetWriter(outputURL: outputURL, fileType: .mp4) else { completion(nil, NSError(domain: "ExportError", code: 0, userInfo: [NSLocalizedDescriptionKey: "创建视频写入器失败"])) return } // 3. 配置视频写入参数 let videoSettings: [String: Any] = [ AVVideoCodecKey: AVVideoCodecType.h264, AVVideoWidthKey: sceneSize.width, AVVideoHeightKey: sceneSize.height ] let writerInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings) let pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: writerInput, sourcePixelBufferAttributes: [ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB, kCVPixelBufferWidthKey as String: sceneSize.width, kCVPixelBufferHeightKey as String: sceneSize.height ] as [String : Any]) writer.add(writerInput) writer.startWriting() writer.startSession(atSourceTime: .zero) // 4. 创建离线SKView和场景 let offscreenView = SKView(frame: CGRect(origin: .zero, size: sceneSize)) let scene = MyScene(size: sceneSize) scene.scaleMode = .aspectFit offscreenView.presentScene(scene) // 5. 逐帧渲染写入 let totalFrames = Int(scene.animationDuration * Double(fps)) let frameDuration = CMTimeMake(value: 1, timescale: Int32(fps)) var currentFrame = 0 let queue = DispatchQueue(label: "videoExportQueue") writerInput.requestMediaDataWhenReady(on: queue) { while writerInput.isReadyForMoreMediaData, currentFrame < totalFrames { let presentationTime = CMTimeMultiply(frameDuration, multiplier: Int32(currentFrame)) // 手动更新场景时间,控制动画进度 scene.update(CGFloat(CMTimeGetSeconds(presentationTime))) scene.simulatePhysics() scene.applyEffects() scene.speed = 1.0 // 渲染当前帧为像素缓冲 var pixelBuffer: CVPixelBuffer? CVPixelBufferCreate(kCFAllocatorDefault, Int(sceneSize.width), Int(sceneSize.height), kCVPixelFormatType_32ARGB, nil, &pixelBuffer) guard let buffer = pixelBuffer else { currentFrame += 1 continue } CVPixelBufferLockBaseAddress(buffer, []) offscreenView.drawHierarchy(in: offscreenView.bounds, afterScreenUpdates: true) let context = CGContext(data: CVPixelBufferGetBaseAddress(buffer), width: Int(sceneSize.width), height: Int(sceneSize.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(buffer), space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue) if let ctx = context { UIGraphicsPushContext(ctx) offscreenView.layer.render(in: ctx) UIGraphicsPopContext() } CVPixelBufferUnlockBaseAddress(buffer, []) // 写入像素缓冲 pixelBufferAdaptor.append(buffer, withPresentationTime: presentationTime) CVPixelBufferRelease(buffer) currentFrame += 1 } if currentFrame >= totalFrames { writerInput.markAsFinished() writer.finishWriting { DispatchQueue.main.async { if writer.status == .completed { completion(outputURL, nil) } else { completion(nil, writer.error) } } } } } }
调用方法
直接调用上面的导出方法即可,传入你需要的导出分辨率:
// 可自定义导出分辨率,建议和场景的size比例一致避免拉伸 let exportSize = CGSize(width: 1080, height: 1920) exportSceneToVideo(sceneSize: exportSize) { outputURL, error in if let url = outputURL { print("导出成功,路径:\(url)") // 可在此处添加保存到相册等后续逻辑 } else if let err = error { print("导出失败:\(err.localizedDescription)") } }
注意事项
- 如果你的场景包含粒子效果、物理引擎模拟等内容,需要在逐帧更新时额外调用对应的模拟方法,确保导出内容和实际运行效果一致
- 若需要导出带音频的视频,额外添加音频轨道的AVAssetWriterInput,同步写入音频数据即可
- 导出过程建议放在子线程执行,避免阻塞主线程UI
内容的提问来源于stack exchange,提问作者TuracBey
相关产品推荐
相关产品推荐

