Commit 66ed2451 authored by CZ1004's avatar CZ1004

【修改】修改压缩方法

parent 4b92a5f0
...@@ -258,7 +258,7 @@ class CompressQualityController : BaseViewController{ ...@@ -258,7 +258,7 @@ class CompressQualityController : BaseViewController{
}else{ }else{
// 压缩视频 // 压缩视频
var compressAllSize : Double = 0.0 var compressAllSize : Double = 0.0
CompressViewModel.compressVideos(models: self.model!, quality: Float(currentQulity)) { (identifier, progress) in CompressViewModel.compressVideos(models: self.model!, quality: Float(currentQulity)) { progress in
compressingView.animationView.setProgress(CGFloat(progress), animated: false, duration: 0.1) compressingView.animationView.setProgress(CGFloat(progress), animated: false, duration: 0.1)
} completion: { (outputURLs, errors) in } completion: { (outputURLs, errors) in
for (index, outputURL) in outputURLs.enumerated() { for (index, outputURL) in outputURLs.enumerated() {
...@@ -268,17 +268,16 @@ class CompressQualityController : BaseViewController{ ...@@ -268,17 +268,16 @@ class CompressQualityController : BaseViewController{
if let fileSize = attributes[.size] as? Int64 { if let fileSize = attributes[.size] as? Int64 {
compressAllSize = compressAllSize + Double(fileSize) compressAllSize = compressAllSize + Double(fileSize)
} }
Print("---------压缩后的大小:\(compressAllSize)")
} catch { } catch {
Print("获取视频文件大小失败") Print("获取视频文件大小失败")
} }
print("Compressed video \(index) saved at: \(outputURL)")
} else if let error = errors[index] { } else if let error = errors[index] {
print("Error compressing video \(index): \(error.localizedDescription)") print("Error compressing video \(index): \(error.localizedDescription)")
} }
// 不管成功失败都跳转下一页
self.updateNextView(compressAllSize,compressingView,[],outputURLs)
} }
Print("---------压缩后的大小:\(compressAllSize)")
// 不管成功失败都跳转下一页
self.updateNextView(compressAllSize,compressingView,[],outputURLs)
} }
} }
......
...@@ -153,160 +153,15 @@ class CompressViewModel{ ...@@ -153,160 +153,15 @@ class CompressViewModel{
/// - quality: 压缩质量 /// - quality: 压缩质量
/// - progress: 进度回调 /// - progress: 进度回调
/// - completion: 完成回调 /// - completion: 完成回调
static func compressVideos(models: [AssetModel], quality: Float, static func compressVideos(
progress: @escaping (String, Float) -> Void, models: [AssetModel],
completion: @escaping ([URL?], [Error?]) -> Void) { quality: Float,
progress: @escaping (Float) -> Void,
var outputURLs = [URL?](repeating: nil, count: models.count) completion: @escaping ([URL?], [Error?]) -> Void
var errors = [Error?](repeating: nil, count: models.count) ) {
let group = DispatchGroup() VideoCompressor.compressVideos(models: models, quality: quality, progress: progress, completion: completion)
for (index, model) in models.enumerated() {
group.enter()
let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [model.localIdentifier], options: nil)
guard let asset = fetchResult.firstObject else {
errors[index] = NSError(domain: "VideoCompression", code: 404, userInfo: [NSLocalizedDescriptionKey: "Asset not found"])
group.leave()
continue
}
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { (avAsset, _, _) in
guard let avAsset = avAsset else {
errors[index] = NSError(domain: "VideoCompression", code: 500, userInfo: [NSLocalizedDescriptionKey: "Could not load asset"])
group.leave()
return
}
// 获取原始视频信息
guard let videoTrack = avAsset.tracks(withMediaType: .video).first else {
errors[index] = NSError(domain: "VideoCompression", code: 501, userInfo: [NSLocalizedDescriptionKey: "No video track found"])
group.leave()
return
}
let originalBitrate = videoTrack.estimatedDataRate
let originalSize = model.assetSize
Print("---------原始大小:\(originalSize)")
// 压缩设置
let targetBitrate: Float
if originalBitrate > 0 {
// 强制压缩到原始比特率的quality比例,最低不低于500kbps
if(originalSize <= 100000){
// 当大小已经没有100KB时,按照0.95去压缩
targetBitrate = min(originalBitrate * quality, originalBitrate * 0.95)
}else{
// 最低500kbps
targetBitrate = max(originalBitrate * quality, 500_000)
}
} else {
// 无法获取原始比特率时的默认值
// 1Mbps为基准
targetBitrate = quality * 1_000_000
}
// 创建输出URL
let outputURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("mp4")
// 压缩参数
let compressionProperties: [String: Any] = [
AVVideoAverageBitRateKey: targetBitrate,
// 关键帧间隔加大到4秒(30fps)
AVVideoMaxKeyFrameIntervalKey: 120,
// 使用基线配置
AVVideoProfileLevelKey: AVVideoProfileLevelH264Baseline30,
// 禁用B帧
AVVideoAllowFrameReorderingKey: false,
// 降低帧率到15fps
AVVideoExpectedSourceFrameRateKey: 15
]
let videoSettings: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.h264,
// 分辨率减半
AVVideoWidthKey: videoTrack.naturalSize.width * 2 / 3,
AVVideoHeightKey: videoTrack.naturalSize.height * 2 / 3,
AVVideoScalingModeKey: AVVideoScalingModeResizeAspect,
AVVideoCompressionPropertiesKey: compressionProperties
]
// 音频设置也进行压缩
let audioSettings: [String: Any] = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
// 单声道
AVNumberOfChannelsKey: 1,
// 降低采样率
AVSampleRateKey: 22050,
// 64kbps音频比特率
AVEncoderBitRateKey: 64_000
]
// 创建导出会话
guard let exportSession = AVAssetExportSession(asset: avAsset, presetName: AVAssetExportPresetLowQuality) else {
errors[index] = NSError(domain: "VideoCompression", code: 502, userInfo: [NSLocalizedDescriptionKey: "Could not create export session"])
group.leave()
return
}
exportSession.outputURL = outputURL
exportSession.outputFileType = .mp4
exportSession.shouldOptimizeForNetworkUse = true
exportSession.videoComposition = self.aggressiveVideoComposition(for: avAsset, settings: videoSettings)
exportSession.audioMix = self.aggressiveAudioMix(for: avAsset, settings: audioSettings)
exportSession.exportAsynchronously {
defer { group.leave() }
guard exportSession.status == .completed else {
errors[index] = exportSession.error ?? NSError(domain: "VideoCompression", code: 503, userInfo: [NSLocalizedDescriptionKey: "Export failed"])
try? FileManager.default.removeItem(at: outputURL)
return
}
// 强制验证文件大小
if let attributes = try? FileManager.default.attributesOfItem(atPath: outputURL.path),
let compressedSize = attributes[.size] as? NSNumber {
if compressedSize.doubleValue >= originalSize {
// 如果压缩后大于等于原始大小,使用更激进的设置重新压缩
try? FileManager.default.removeItem(at: outputURL)
self.recompressWithMoreAggressiveSettings(avAsset: avAsset, originalSize: originalSize, index: index) { url, error in
outputURLs[index] = url
errors[index] = error
}
} else {
outputURLs[index] = outputURL
}
} else {
outputURLs[index] = outputURL
}
}
// 进度监控
DispatchQueue.global().async {
while exportSession.status == .waiting || exportSession.status == .exporting {
DispatchQueue.main.async {
progress(model.localIdentifier, exportSession.progress)
}
Thread.sleep(forTimeInterval: 0.1)
}
}
}
}
group.notify(queue: .main) {
completion(outputURLs, errors)
}
} }
/// 压缩多张图片 /// 压缩多张图片
/// - Parameters: /// - Parameters:
/// - assets: 图片集合 /// - assets: 图片集合
...@@ -409,97 +264,5 @@ class CompressViewModel{ ...@@ -409,97 +264,5 @@ class CompressViewModel{
} }
} }
private static func recompressWithMoreAggressiveSettings(avAsset: AVAsset, originalSize: Double, index: Int, completion: @escaping (URL?, Error?) -> Void) {
let outputURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("mp4")
guard let videoTrack = avAsset.tracks(withMediaType: .video).first else {
completion(nil, NSError(domain: "VideoCompression", code: 504, userInfo: [NSLocalizedDescriptionKey: "No video track found"]))
return
}
// 更激进的设置
let compressionProperties: [String: Any] = [
// 固定250kbps
AVVideoAverageBitRateKey: 250_000,
// 关键帧间隔8秒
AVVideoMaxKeyFrameIntervalKey: 240,
AVVideoProfileLevelKey: AVVideoProfileLevelH264Baseline30,
AVVideoAllowFrameReorderingKey: false,
// 帧率降到10fps
AVVideoExpectedSourceFrameRateKey: 10
]
let videoSettings: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: videoTrack.naturalSize.width / 2,
AVVideoHeightKey: videoTrack.naturalSize.height / 2,
AVVideoScalingModeKey: AVVideoScalingModeResizeAspect,
AVVideoCompressionPropertiesKey: compressionProperties
]
let audioSettings: [String: Any] = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVNumberOfChannelsKey: 1,
// 16kHz采样率
AVSampleRateKey: 16000,
// 32kbps音频比特率
AVEncoderBitRateKey: 32_000
]
guard let exportSession = AVAssetExportSession(asset: avAsset, presetName: AVAssetExportPresetLowQuality) else {
completion(nil, NSError(domain: "VideoCompression", code: 505, userInfo: [NSLocalizedDescriptionKey: "Could not create export session"]))
return
}
exportSession.outputURL = outputURL
exportSession.outputFileType = .mp4
exportSession.shouldOptimizeForNetworkUse = true
exportSession.videoComposition = self.aggressiveVideoComposition(for: avAsset, settings: videoSettings)
exportSession.audioMix = self.aggressiveAudioMix(for: avAsset, settings: audioSettings)
exportSession.exportAsynchronously {
if exportSession.status == .completed {
completion(outputURL, nil)
} else {
try? FileManager.default.removeItem(at: outputURL)
completion(nil, exportSession.error ?? NSError(domain: "VideoCompression", code: 506, userInfo: [NSLocalizedDescriptionKey: "Recompression failed"]))
}
}
}
private static func aggressiveVideoComposition(for asset: AVAsset, settings: [String: Any]) -> AVVideoComposition? {
guard let videoTrack = asset.tracks(withMediaType: .video).first else { return nil }
let composition = AVMutableVideoComposition()
composition.renderSize = CGSize(
width: (settings[AVVideoWidthKey] as? CGFloat) ?? videoTrack.naturalSize.width,
height: (settings[AVVideoHeightKey] as? CGFloat) ?? videoTrack.naturalSize.height
)
composition.frameDuration = CMTime(value: 1, timescale: Int32(settings[AVVideoExpectedSourceFrameRateKey] as? Int ?? 15))
let instruction = AVMutableVideoCompositionInstruction()
instruction.timeRange = CMTimeRange(start: .zero, duration: asset.duration)
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
instruction.layerInstructions = [layerInstruction]
composition.instructions = [instruction]
return composition
}
private static func aggressiveAudioMix(for asset: AVAsset, settings: [String: Any]) -> AVAudioMix? {
guard let audioTrack = asset.tracks(withMediaType: .audio).first else { return nil }
let audioMix = AVMutableAudioMix()
let audioInputParams = AVMutableAudioMixInputParameters(track: audioTrack)
// 保持音频处理简单
audioInputParams.audioTimePitchAlgorithm = .timeDomain
audioMix.inputParameters = [audioInputParams]
return audioMix
}
} }
@preconcurrency import AVFoundation
import Photos
class VideoCompressor {
private static let compressionQueue = DispatchQueue(label: "com.compression.queue", qos: .userInitiated)
static func compressVideos(
models: [AssetModel],
quality: Float,
progress: @escaping (Float) -> Void,
completion: @escaping ([URL?], [Error?]) -> Void
) {
var results = [URL?](repeating: nil, count: models.count)
var errors = [Error?](repeating: nil, count: models.count)
let group = DispatchGroup()
var totalProgress: Float = 0
let totalVideos = Float(models.count)
var individualProgress = [Float](repeating: 0, count: models.count)
for (index, model) in models.enumerated() {
group.enter()
fetchAVAsset(for: model) { asset, error in
guard let asset = asset else {
errors[index] = error ?? NSError(domain: "AssetError", code: -1, userInfo: nil)
group.leave()
return
}
// 这里再添加一个逻辑 当原大小小于等于为100时压缩比例调成0.98
var tempScale = quality
if model.assetSize <= 102400 {
tempScale = 0.98
}
compressionQueue.async {
compressSingleVideo(
asset: asset,
quality: tempScale,
progress: { p in
DispatchQueue.main.async {
// 更新单个视频的进度
individualProgress[index] = p
// 重新计算总进度
totalProgress = individualProgress.reduce(0, +) / totalVideos
progress(totalProgress)
}
},
completion: { url, error in
results[index] = url
errors[index] = error
group.leave()
}
)
}
}
}
group.notify(queue: .main) {
completion(results, errors)
}
}
// MARK: - 核心压缩方法(闭包版本)
private static func compressSingleVideo(
asset: AVAsset,
quality: Float,
progress: @escaping (Float) -> Void,
completion: @escaping (URL?, Error?) -> Void
) {
let outputURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("mp4")
// 清理已存在的文件
try? FileManager.default.removeItem(at: outputURL)
// 异步加载视频轨道
loadTracksAsync(asset: asset) { videoTrack, audioTrack, error in
guard let videoTrack = videoTrack else {
completion(nil, error ?? CompressionError.invalidVideoTrack)
return
}
do {
let (reader, videoOutput, writer, videoInput) = try setupVideoComponents(
videoTrack: videoTrack,
outputURL: outputURL,
quality: quality
)
let (audioOutput, audioInput) = try setupAudioComponents(audioTrack: audioTrack)
// 配置读写器
if let audioOutput = audioOutput, let audioInput = audioInput {
reader.add(audioOutput)
writer.add(audioInput)
}
// 开始处理
processSamples(
reader: reader,
writer: writer,
videoOutput: videoOutput,
videoInput: videoInput,
audioOutput: audioOutput,
audioInput: audioInput,
duration: CMTimeGetSeconds(asset.duration),
progress: progress,
completion: { result in
switch result {
case .success:
completion(outputURL, nil)
case .failure(let error):
completion(nil, error)
}
}
)
} catch {
completion(nil, error)
}
}
}
// MARK: - 异步加载轨道(兼容iOS 14)
private static func loadTracksAsync(
asset: AVAsset,
completion: @escaping (AVAssetTrack?, AVAssetTrack?, Error?) -> Void
) {
let keys: [String] = [
#keyPath(AVAsset.tracks),
#keyPath(AVAsset.duration)
]
asset.loadValuesAsynchronously(forKeys: keys) {
var error: NSError?
let status = asset.statusOfValue(forKey: #keyPath(AVAsset.tracks), error: &error)
guard status == .loaded else {
completion(nil, nil, error ?? CompressionError.trackLoadingFailed)
return
}
let videoTrack = asset.tracks(withMediaType: .video).first
let audioTrack = asset.tracks(withMediaType: .audio).first
completion(videoTrack, audioTrack, nil)
}
}
// MARK: - 视频组件配置
private static func setupVideoComponents(
videoTrack: AVAssetTrack,
outputURL: URL,
quality: Float
) throws -> (AVAssetReader, AVAssetReaderTrackOutput, AVAssetWriter, AVAssetWriterInput) {
// 视频读取配置
let readerOutputSettings: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
]
guard let reader = try? AVAssetReader(asset: videoTrack.asset!) else {
throw CompressionError.readerInitializationFailed
}
let videoOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: readerOutputSettings)
guard reader.canAdd(videoOutput) else {
throw CompressionError.readerInitializationFailed
}
reader.add(videoOutput)
// 视频写入配置
guard let writer = try? AVAssetWriter(outputURL: outputURL, fileType: .mp4) else {
throw CompressionError.writerInitializationFailed
}
let originalSize = videoTrack.naturalSize
let originalBitrate = Double(videoTrack.estimatedDataRate)
let targetSize = calculateTargetSize(originalSize: originalSize, quality: quality)
let videoInput = AVAssetWriterInput(
mediaType: .video,
outputSettings: [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: targetSize.width,
AVVideoHeightKey: targetSize.height,
AVVideoCompressionPropertiesKey: [
AVVideoAverageBitRateKey: quality * Float(originalBitrate),
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
AVVideoMaxKeyFrameIntervalKey: 30
]
]
)
videoInput.transform = videoTrack.preferredTransform
guard writer.canAdd(videoInput) else {
throw CompressionError.writerInitializationFailed
}
writer.add(videoInput)
return (reader, videoOutput, writer, videoInput)
}
// MARK: - 音频组件配置
private static func setupAudioComponents(
audioTrack: AVAssetTrack?
) throws -> (AVAssetReaderTrackOutput?, AVAssetWriterInput?) {
guard let audioTrack = audioTrack else {
return (nil, nil)
}
// 音频读取配置
let audioOutput = AVAssetReaderTrackOutput(
track: audioTrack,
outputSettings: [
AVFormatIDKey: kAudioFormatLinearPCM
]
)
// 音频写入配置
let audioInput = AVAssetWriterInput(
mediaType: .audio,
outputSettings: [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVSampleRateKey: 44100,
AVEncoderBitRateKey: 128000,
AVNumberOfChannelsKey: 2
]
)
return (audioOutput, audioInput)
}
// MARK: - 处理样本数据
// 修改后的 processSamples 方法
private static func processSamples(
reader: AVAssetReader,
writer: AVAssetWriter,
videoOutput: AVAssetReaderTrackOutput,
videoInput: AVAssetWriterInput,
audioOutput: AVAssetReaderTrackOutput?,
audioInput: AVAssetWriterInput?,
duration: Float64,
progress: @escaping (Float) -> Void,
completion: @escaping (Result<Void, Error>) -> Void
) {
let videoQueue = DispatchQueue(label: "video.queue")
let audioQueue = DispatchQueue(label: "audio.queue")
let stateQueue = DispatchQueue(label: "state.queue")
// 使用线程安全的状态管理
var _videoFinished = false
var _audioFinished = false
func updateVideoFinished(_ value: Bool) {
stateQueue.async {
_videoFinished = value
checkCompletion()
}
}
func updateAudioFinished(_ value: Bool) {
stateQueue.async {
_audioFinished = value
checkCompletion()
}
}
func checkCompletion() {
stateQueue.async {
guard _videoFinished && _audioFinished else { return }
writer.finishWriting {
// 切换到主队列处理完成回调
DispatchQueue.main.async {
switch writer.status {
case .completed:
completion(.success(()))
case .failed, .cancelled:
completion(.failure(writer.error ?? CompressionError.exportFailed))
default:
completion(.failure(CompressionError.exportFailed))
}
}
}
}
}
writer.startWriting()
reader.startReading()
writer.startSession(atSourceTime: .zero)
// 处理视频样本
videoInput.requestMediaDataWhenReady(on: videoQueue) {
while videoInput.isReadyForMoreMediaData {
guard reader.status == .reading,
let sampleBuffer = videoOutput.copyNextSampleBuffer() else {
videoInput.markAsFinished()
updateVideoFinished(true)
break
}
let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
let currentProgress = Float(time.seconds / duration)
DispatchQueue.main.async {
progress(currentProgress)
}
videoInput.append(sampleBuffer)
}
}
// 处理音频样本
if let audioInput = audioInput, let audioOutput = audioOutput {
audioInput.requestMediaDataWhenReady(on: audioQueue) {
while audioInput.isReadyForMoreMediaData {
guard let sampleBuffer = audioOutput.copyNextSampleBuffer() else {
audioInput.markAsFinished()
updateAudioFinished(true)
break
}
audioInput.append(sampleBuffer)
}
}
} else {
updateAudioFinished(true)
}
}
// MARK: - 其他工具方法
private static func calculateTargetSize(originalSize: CGSize, quality: Float) -> CGSize {
let scale = max(0.1, min(1.0, quality))
return CGSize(
width: round(originalSize.width * CGFloat(scale)),
height: round(originalSize.height * CGFloat(scale))
)
}
private static func fetchAVAsset(
for model: AssetModel,
completion: @escaping (AVAsset?, Error?) -> Void
) {
let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [model.localIdentifier], options: nil)
guard let phAsset = fetchResult.firstObject else {
completion(nil, CompressionError.assetNotFound)
return
}
let options = PHVideoRequestOptions()
options.isNetworkAccessAllowed = true
PHImageManager.default().requestAVAsset(
forVideo: phAsset,
options: options
) { asset, _, info in
completion(asset, info?[PHImageErrorKey] as? Error)
}
}
enum CompressionError: Error {
case readerInitializationFailed
case writerInitializationFailed
case invalidVideoTrack
case trackLoadingFailed
case exportFailed
case assetNotFound
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment