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

使用AVAssetExportPresetPassthrough裁剪视频失败,如何保留原分辨率修复?

问题出错原因

AVAssetExportPresetPassthrough 属于透传预设,不会对音视频进行重新编码,直接拷贝原文件的编码数据,你的问题主要由以下几个原因导致:

  • 裁剪时间未对齐原视频关键帧:透传模式只能在原视频的关键帧(I帧)位置进行裁剪,如果设置的timeRange起始/结束位置落在非关键帧区间,导出会直接失败。
  • 输出路径已存在文件:代码中没有提前清理outputUrl路径下的已有文件,AVAssetExportSession默认不会覆盖已有文件,会直接返回失败。
  • 时间范围不合法:缺少对startTime、endTime的合法性校验,如果出现startTime >= endTime、endTime超过视频总时长的情况也会导出失败。
  • 业务逻辑潜在问题:导出成功后直接返回了原file实例,没有关联新生成的outputUrl,哪怕导出成功业务层拿到的还是原视频文件,会误以为导出失败。
修复方案(保留原视频分辨率)

方案1:继续使用透传模式(无重新编码,速度最快)

适合可以接受裁剪时间对齐关键帧的场景:

  1. 导出前先删除输出路径的已有文件:
try? FileManager.default.removeItem(at: outputUrl)
  1. 将裁剪起始时间对齐到最近的前向关键帧:通过视频轨道的segments属性获取所有关键帧位置,找到小于等于startTime的最近关键帧时间作为新的裁剪起始时间。
  2. 增加时间范围合法性校验,提前判断startTime < endTime且endTime不超过视频总时长。
  3. 导出成功后将outputUrl关联到返回的File实例中。

方案2:使用质量优先预设(支持精确裁剪,保留原分辨率)

如果需要精确控制裁剪位置,不要使用AVAssetExportPresetPassthrough,改用AVAssetExportPresetHighestQuality预设,该预设会自动匹配原视频的分辨率、码率等参数,仅对裁剪涉及的片段进行重新编码,导出的视频画质和分辨率和原视频完全一致。

使用AVAssetReader+AVAssetWriter实现裁剪的方案

这种方式可以同时满足精确裁剪、保留原分辨率、自定义编码参数的需求,执行效率也比通用导出会话更高,实现步骤如下:

  1. 前置校验:校验裁剪时间合法性,删除输出路径已有文件,加载原asset的音视频轨道
let asset = AVAsset(url: originUrl)
guard let videoTrack = try await asset.loadTracks(withMediaType: .video).first else {
    handleFailure(error: .mediaSavingError, completion: completion)
    return
}
let audioTrack = try await asset.loadTracks(withMediaType: .audio).first
let totalDuration = try await asset.load(.duration).seconds
guard startTime < endTime, endTime <= totalDuration else {
    handleFailure(error: .mediaSavingError, completion: completion)
    return
}
let timeRange = CMTimeRange(start: CMTime(seconds: startTime, preferredTimescale: 900),
                            end: CMTime(seconds: endTime, preferredTimescale: 900))
// 清理已有文件
try? FileManager.default.removeItem(at: outputUrl)
  1. 初始化AVAssetReader和AVAssetWriter
guard let reader = try? AVAssetReader(asset: asset),
      let writer = try? AVAssetWriter(outputURL: outputUrl, fileType: .mp4) else {
    handleFailure(error: .mediaSavingError, completion: completion)
    return
}
reader.timeRange = timeRange
  1. 配置读写输入输出,参数完全匹配原视频保证分辨率不变
// 视频写入配置
let videoSize = try await videoTrack.load(.naturalSize)
let videoFormatHint = try await videoTrack.load(.formatDescriptions).first as? CMFormatDescription
let videoWriterInput = AVAssetWriterInput(mediaType: .video, 
                                          outputSettings: [
                                            AVVideoCodecKey: AVVideoCodecType.h264,
                                            AVVideoWidthKey: videoSize.width,
                                            AVVideoHeightKey: videoSize.height
                                          ],
                                          sourceFormatHint: videoFormatHint)
writer.add(videoWriterInput)
let videoReaderOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: nil)
reader.add(videoReaderOutput)

// 音频写入配置(存在音频轨道时添加)
var audioWriterInput: AVAssetWriterInput?
if let audioTrack = audioTrack {
    let audioFormatHint = try await audioTrack.load(.formatDescriptions).first as? CMFormatDescription
    audioWriterInput = AVAssetWriterInput(mediaType: .audio,
                                          outputSettings: [
                                            AVFormatIDKey: kAudioFormatMPEG4AAC,
                                            AVSampleRateKey: 44100,
                                            AVNumberOfChannelsKey: 2
                                          ],
                                          sourceFormatHint: audioFormatHint)
    writer.add(audioWriterInput!)
    let audioReaderOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: nil)
    reader.add(audioReaderOutput)
}
  1. 启动读写流程
reader.startReading()
writer.startWriting()
writer.startSession(atSourceTime: timeRange.start)

let processingQueue = DispatchQueue(label: "video.trim.queue")
let group = DispatchGroup()

// 处理视频流
group.enter()
videoWriterInput.requestMediaDataWhenReady(on: processingQueue) {
    while videoWriterInput.isReadyForMoreMediaData {
        if let buffer = videoReaderOutput.copyNextSampleBuffer() {
            videoWriterInput.append(buffer)
        } else {
            videoWriterInput.markAsFinished()
            group.leave()
            break
        }
    }
}

// 处理音频流
if let audioWriterInput = audioWriterInput, let audioReaderOutput = reader.outputs.first(where: { $0.mediaType == .audio }) as? AVAssetReaderTrackOutput {
    group.enter()
    audioWriterInput.requestMediaDataWhenReady(on: processingQueue) {
        while audioWriterInput.isReadyForMoreMediaData {
            if let buffer = audioReaderOutput.copyNextSampleBuffer() {
                audioWriterInput.append(buffer)
            } else {
                audioWriterInput.markAsFinished()
                group.leave()
                break
            }
        }
    }
}

// 结果回调
group.notify(queue: .main) {
    if reader.status == .completed {
        writer.finishWriting {
            if writer.status == .completed {
                // 关联新文件路径后返回
                let newFile = File(localUrl: outputUrl) // 替换为你自己的File初始化逻辑
                completion(.success(newFile))
            } else {
                handleFailure(error: .mediaSavingError, completion: completion)
            }
        }
    } else {
        handleFailure(error: .mediaSavingError, completion: completion)
    }
}

内容的提问来源于stack exchange,提问作者DevSemenuk

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 05:06:08