使用AVAssetExportPresetPassthrough裁剪视频失败,如何保留原分辨率修复?
问题出错原因
AVAssetExportPresetPassthrough 属于透传预设,不会对音视频进行重新编码,直接拷贝原文件的编码数据,你的问题主要由以下几个原因导致:
- 裁剪时间未对齐原视频关键帧:透传模式只能在原视频的关键帧(I帧)位置进行裁剪,如果设置的
timeRange起始/结束位置落在非关键帧区间,导出会直接失败。 - 输出路径已存在文件:代码中没有提前清理
outputUrl路径下的已有文件,AVAssetExportSession默认不会覆盖已有文件,会直接返回失败。 - 时间范围不合法:缺少对
startTime、endTime的合法性校验,如果出现startTime >= endTime、endTime超过视频总时长的情况也会导出失败。 - 业务逻辑潜在问题:导出成功后直接返回了原
file实例,没有关联新生成的outputUrl,哪怕导出成功业务层拿到的还是原视频文件,会误以为导出失败。
修复方案(保留原视频分辨率)
方案1:继续使用透传模式(无重新编码,速度最快)
适合可以接受裁剪时间对齐关键帧的场景:
- 导出前先删除输出路径的已有文件:
try? FileManager.default.removeItem(at: outputUrl)
- 将裁剪起始时间对齐到最近的前向关键帧:通过视频轨道的
segments属性获取所有关键帧位置,找到小于等于startTime的最近关键帧时间作为新的裁剪起始时间。 - 增加时间范围合法性校验,提前判断
startTime < endTime且endTime不超过视频总时长。 - 导出成功后将
outputUrl关联到返回的File实例中。
方案2:使用质量优先预设(支持精确裁剪,保留原分辨率)
如果需要精确控制裁剪位置,不要使用AVAssetExportPresetPassthrough,改用AVAssetExportPresetHighestQuality预设,该预设会自动匹配原视频的分辨率、码率等参数,仅对裁剪涉及的片段进行重新编码,导出的视频画质和分辨率和原视频完全一致。
使用AVAssetReader+AVAssetWriter实现裁剪的方案
这种方式可以同时满足精确裁剪、保留原分辨率、自定义编码参数的需求,执行效率也比通用导出会话更高,实现步骤如下:
- 前置校验:校验裁剪时间合法性,删除输出路径已有文件,加载原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)
- 初始化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
- 配置读写输入输出,参数完全匹配原视频保证分辨率不变
// 视频写入配置 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) }
- 启动读写流程
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
相关产品推荐
相关产品推荐

