Swift实现多帧转视频求助及FFmpeg命令优化咨询
iOS Swift 本地生成视频方案:AVAssetWriter 原生实现 + FFmpeg 集成优化
针对你的需求——用APP本地采集的n帧图片,按指定帧率生成15秒视频,我整理了两种可行方案,优先推荐系统原生的AVAssetWriter(不用依赖第三方,适配性好),再补充FFmpeg的集成和命令优化思路:
一、AVAssetWriter 原生实现(优先推荐)
AVAssetWriter是苹果提供的原生API,专门用于生成视频文件,完全适配Swift,不需要额外依赖,性能也能匹配iOS设备的硬件能力。下面是一个可直接修改适配的示例代码,已经结合你的dft(单帧显示时长)、15秒时长需求:
import AVFoundation func generateVideo(from frames: [UIImage], dft: Double, outputURL: URL) async throws { // 计算帧率:帧率 = 1 / dft(比如dft=0.13秒,帧率就是100/13≈7.69fps) let frameRate = CMTimeMake(value: 1, timescale: Int32(dft * 1000)) // 用毫秒级 timescale 避免浮点精度问题 let totalDuration = CMTimeMakeWithSeconds(15, preferredTimescale: 600) let frameCount = frames.count // 初始化AVAssetWriter guard let writer = try? AVAssetWriter(outputURL: outputURL, fileType: .mp4) else { throw NSError(domain: "VideoGenerator", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to create AVAssetWriter"]) } // 配置视频输入:取第一张图片的尺寸作为视频分辨率 guard let firstFrame = frames.first else { throw NSError(domain: "VideoGenerator", code: -2, userInfo: [NSLocalizedDescriptionKey: "No frames provided"]) } let videoSettings: [String: Any] = [ AVVideoCodecKey: AVVideoCodecType.h264, AVVideoWidthKey: firstFrame.size.width, AVVideoHeightKey: firstFrame.size.height, AVVideoCompressionPropertiesKey: [ AVVideoAverageBitRateKey: Int(firstFrame.size.width * firstFrame.size.height * 10), // 合理比特率,保证画质 AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel ] ] let input = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings) input.expectsMediaDataInRealTime = false // 非实时写入,适合批量处理帧 input.transform = firstFrame.imageOrientation.transformForOrientation() // 处理图片方向 if writer.canAdd(input) { writer.add(input) } else { throw NSError(domain: "VideoGenerator", code: -3, userInfo: [NSLocalizedDescriptionKey: "Failed to add input to writer"]) } // 开始写入 writer.startWriting() writer.startSession(atSourceTime: .zero) // 创建帧缓存队列 let dispatchQueue = DispatchQueue(label: "com.yourapp.video.writer") input.requestMediaDataWhenReady(on: dispatchQueue) { var currentFrameIndex = 0 var currentTime = CMTime.zero while input.isReadyForMoreMediaData { guard currentTime < totalDuration else { input.markAsFinished() writer.finishWriting { // 写入完成后的回调 if writer.status == .completed { print("Video generated successfully at \(outputURL)") } else if let error = writer.error { print("Video generation failed: \(error.localizedDescription)") } } return } // 获取当前帧,转为CMSampleBuffer let frame = frames[currentFrameIndex] guard let buffer = frame.toCMSampleBuffer(with: input) else { input.markAsFinished() writer.cancelWriting() return } // 写入帧 input.append(buffer) // 更新时间和帧索引:如果帧不足,循环使用(对应你FFmpeg命令里的-loop 1) currentTime = CMTimeAdd(currentTime, frameRate) currentFrameIndex = (currentFrameIndex + 1) % frameCount } } } // 扩展UIImage,用于转换为CMSampleBuffer和处理方向 extension UIImage { func toCMSampleBuffer(with input: AVAssetWriterInput) -> CMSampleBuffer? { guard let pixelBufferPool = input.pixelBufferAdaptor?.pixelBufferPool else { return nil } var pixelBuffer: CVPixelBuffer? CVPixelBufferCreate(kCFAllocatorDefault, Int(size.width), Int(size.height), kCVPixelFormatType_32ARGB, nil, &pixelBuffer) guard let buffer = pixelBuffer else { return nil } CVPixelBufferLockBaseAddress(buffer, []) let context = CGContext(data: CVPixelBufferGetBaseAddress(buffer), width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(buffer), space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue) context?.draw(cgImage!, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) CVPixelBufferUnlockBaseAddress(buffer, []) let sampleTime = CMSampleTimingInfo(duration: .invalid, presentationTimeStamp: .invalid, decodeTimeStamp: .invalid) var sampleBuffer: CMSampleBuffer? CMSampleBufferCreateReadyWithImageBuffer(kCFAllocatorDefault, buffer, nil, &sampleTime, nil, nil, &sampleBuffer) return sampleBuffer } func transformForOrientation() -> CGAffineTransform { switch imageOrientation { case .up: return CGAffineTransform.identity case .down: return CGAffineTransform(rotationAngle: .pi) case .left: return CGAffineTransform(rotationAngle: -.pi/2) case .right: return CGAffineTransform(rotationAngle: .pi/2) case .upMirrored: return CGAffineTransform(scaleX: -1, y: 1) case .downMirrored: return CGAffineTransform(rotationAngle: .pi).scaledBy(x: -1, y: 1) case .leftMirrored: return CGAffineTransform(rotationAngle: -.pi/2).scaledBy(x: -1, y: 1) case .rightMirrored: return CGAffineTransform(rotationAngle: .pi/2).scaledBy(x: -1, y: 1) @unknown default: return CGAffineTransform.identity } } }
代码说明:
- 自动根据
dft计算帧率,用CMTime避免浮点精度问题 - 支持循环帧(对应你FFmpeg命令的
-loop 1),直到达到15秒时长 - 配置了合理的H264编码参数,保证画质的同时兼顾速度
- 处理了图片方向问题,避免生成的视频画面旋转
二、FFmpeg 集成与命令优化
如果你更倾向于用FFmpeg,下面是Swift端的集成思路和命令优化方案:
1. Swift 集成 FFmpeg
最简便的方式是用预编译的FFmpeg iOS框架:
- 手动下载预编译的
.framework文件,导入到Xcode项目中,配置Build Phases的Link Binary With Libraries - 或者用CocoaPods,选择维护活跃的FFmpeg iOS pod库
集成后,你可以用Process(Swift 3+)来执行FFmpeg命令,比如:
import Foundation func runFFmpegCommand(framesPath: String, dft: Double, outputPath: String) { // 计算帧率:100/(dft*100) 等价于 1/dft,比如dft=0.13,就是100/13 let frameRateNumerator = 100 let frameRateDenominator = Int(dft * 100) let ffmpegPath = Bundle.main.path(forResource: "ffmpeg", ofType: nil)! // 假设ffmpeg二进制文件在Bundle中 let process = Process() process.executableURL = URL(fileURLWithPath: ffmpegPath) process.arguments = [ "-framerate", "\(frameRateNumerator)/\(frameRateDenominator)", "-loop", "1", "-i", "\(framesPath)/frame%02d.png", "-c:v", "libx264", "-preset", "fast", // 优化速度的预设 "-hwaccel", "videotoolbox", // 启用iOS硬件加速 "-pix_fmt", "yuv420p", "-t", "0:15", outputPath ] let pipe = Pipe() process.standardOutput = pipe process.standardError = pipe do { try process.run() let data = pipe.fileHandleForReading.readDataToEndOfFile() print(String(data: data, encoding: .utf8)!) } catch { print("FFmpeg command failed: \(error.localizedDescription)") } }
2. 命令优化(不损失画质前提下加快速度)
针对你的原命令,做以下优化:
- 启用硬件加速:添加
-hwaccel videotoolbox,利用苹果的VideoToolbox硬件编码,速度能提升3-5倍,完全不损失画质 - 选择更快的编码预设:原命令默认用
medium预设,换成fast或veryfast,压缩率几乎不变,但编码速度大幅提升;极致速度可选ultrafast,画质损失极小 - 简化参数:输入输出帧率一致时,可省略输出端的
-r,FFmpeg会自动继承输入帧率 - 优化后的命令示例:
ffmpeg -framerate 100/13 -loop 1 -i frame%02d.png -c:v libx264 -preset fast -hwaccel videotoolbox -pix_fmt yuv420p -t 0:15 instagram.mp4
方案选择建议
- 优先用AVAssetWriter:原生API,无需额外依赖,苹果官方维护,稳定性和兼容性最好,性能完全满足需求
- 选FFmpeg的场景:如果需要更复杂的视频处理(比如滤镜、转场、多轨道合成),再考虑集成FFmpeg,毕竟需要额外引入框架,体积会增加一些
内容的提问来源于stack exchange,提问作者James Woodrow
相关产品推荐
相关产品推荐

