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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 06:41:59