Commit c3c18b7a authored by CZ1004's avatar CZ1004

【优化】优化视频压缩逻辑

parent 4a1b6582
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "btn_add_contact.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "btn_add_contact@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "btn_add_contact@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "ic_beifen.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ic_beifen@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_beifen@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "ic_close_similar.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ic_close_similar@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_close_similar@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "ic_delete_duplicates.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ic_delete_duplicates@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_delete_duplicates@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "ic_delete_email.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ic_delete_email@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_delete_email@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "ic_hebing.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ic_hebing@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_hebing@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "ic_no_duolicates.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ic_no_duolicates@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_no_duolicates@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "ic_ok_duolicates.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ic_ok_duolicates@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_ok_duolicates@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "ic_unsel_com_red.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ic_unsel_com_red@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic_unsel_com_red@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "icon_return_setting_nor.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "icon_return_setting_nor@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "icon_return_setting_nor@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
{
"images" : [
{
"filename" : "img_contacts.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "img_contacts@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "img_contacts@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
...@@ -140,10 +140,7 @@ extension BaseNavViewController:UIViewControllerTransitioningDelegate { ...@@ -140,10 +140,7 @@ extension BaseNavViewController:UIViewControllerTransitioningDelegate {
} }
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return nil return nil
} }
} }
......
...@@ -146,7 +146,20 @@ class CompressSelectCell : UICollectionViewCell { ...@@ -146,7 +146,20 @@ class CompressSelectCell : UICollectionViewCell {
@objc func selectClick(){ @objc func selectClick(){
let vc = PMShowImgVideoController()
vc.getVideoURLFromLocalIdentifier(localIdentifier: self.model?.localIdentifier ?? "") {[weak self] url, error in
guard let self else {return}
if url != nil{
self.choose = !self.choose self.choose = !self.choose
}else{
let alert = UIAlertController(title: nil, message: "ICloud video cannot be compressed", preferredStyle: .alert)
self.responderViewController()?.present(alert, animated: true, completion: nil)
// 1 秒后关闭弹窗
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
alert.dismiss(animated: true, completion: nil)
}
}
}
} }
...@@ -204,8 +217,6 @@ class CompressSelectCell : UICollectionViewCell { ...@@ -204,8 +217,6 @@ class CompressSelectCell : UICollectionViewCell {
make.height.width.equalTo(80) make.height.width.equalTo(80)
} }
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
...@@ -219,10 +230,6 @@ class CompressSelectCell : UICollectionViewCell { ...@@ -219,10 +230,6 @@ class CompressSelectCell : UICollectionViewCell {
if self.currentMediaType == .compressPhoto { if self.currentMediaType == .compressPhoto {
// // 如果是图片
// let vc : PreViewController = PreViewController()
// vc.imageIdent = self.model!.localIdentifier
// self.responderViewController()?.navigationController?.pushViewController(vc, animated: true)
// 点击之后跳转详情页面 // 点击之后跳转详情页面
if let tempModel = self.model { if let tempModel = self.model {
let vc = PMShowImgVideoController() let vc = PMShowImgVideoController()
...@@ -239,9 +246,6 @@ class CompressSelectCell : UICollectionViewCell { ...@@ -239,9 +246,6 @@ class CompressSelectCell : UICollectionViewCell {
}else{ }else{
// 如果是视频 // 如果是视频
// let vc : PreVideoController = PreVideoController(localIdentifier: self.model!.localIdentifier)
// self.responderViewController()?.navigationController?.pushViewController(vc, animated: true)
if let tempModel = self.model { if let tempModel = self.model {
// 获取视频的图片 // 获取视频的图片
PhotoAndVideoMananger.mananger.getVideoImageByIdent(ident: tempModel) { image in PhotoAndVideoMananger.mananger.getVideoImageByIdent(ident: tempModel) { image in
...@@ -271,7 +275,6 @@ class CompressSelectCell : UICollectionViewCell { ...@@ -271,7 +275,6 @@ class CompressSelectCell : UICollectionViewCell {
} }
} }
} }
} errorHandler: { } errorHandler: {
DispatchQueue.main.async { DispatchQueue.main.async {
let alert = UIAlertController(title: nil, message: "Get Video image failure", preferredStyle: .alert) let alert = UIAlertController(title: nil, message: "Get Video image failure", preferredStyle: .alert)
......
...@@ -83,7 +83,7 @@ class CompressCompletedViewController : BaseViewController{ ...@@ -83,7 +83,7 @@ class CompressCompletedViewController : BaseViewController{
let label = UILabel() let label = UILabel()
label.text = "511MB" label.text = "511MB"
label.textColor = UIColor(red: 0, green: 0.51, blue: 1, alpha: 1) label.textColor = UIColor(red: 0, green: 0.51, blue: 1, alpha: 1)
label.textAlignment = .center label.textAlignment = .right
label.font = UIFont.systemFont(ofSize: 14, weight: .bold) label.font = UIFont.systemFont(ofSize: 14, weight: .bold)
label.layer.borderColor = UIColor(red: 0, green: 0.51, blue: 1, alpha: 1).cgColor label.layer.borderColor = UIColor(red: 0, green: 0.51, blue: 1, alpha: 1).cgColor
label.backgroundColor = .clear label.backgroundColor = .clear
...@@ -119,7 +119,7 @@ class CompressCompletedViewController : BaseViewController{ ...@@ -119,7 +119,7 @@ class CompressCompletedViewController : BaseViewController{
let label = UILabel() let label = UILabel()
label.text = "1.0%" label.text = "1.0%"
label.textColor = UIColor(red: 0, green: 0.51, blue: 1, alpha: 1) label.textColor = UIColor(red: 0, green: 0.51, blue: 1, alpha: 1)
label.textAlignment = .center label.textAlignment = .right
label.font = UIFont.systemFont(ofSize: 14, weight: .bold) label.font = UIFont.systemFont(ofSize: 14, weight: .bold)
label.layer.borderColor = UIColor(red: 0, green: 0.51, blue: 1, alpha: 1).cgColor label.layer.borderColor = UIColor(red: 0, green: 0.51, blue: 1, alpha: 1).cgColor
label.backgroundColor = .clear label.backgroundColor = .clear
...@@ -200,19 +200,19 @@ class CompressCompletedViewController : BaseViewController{ ...@@ -200,19 +200,19 @@ class CompressCompletedViewController : BaseViewController{
self.tipTopLabel.snp.makeConstraints { make in self.tipTopLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(20) make.top.equalToSuperview().offset(20)
make.width.equalTo(198) make.width.equalTo(120)
make.height.equalTo(22) make.height.equalTo(22)
make.left.equalTo(self.topImageView.snp.right).offset(8) make.left.equalTo(self.topImageView.snp.right).offset(8)
} }
self.detailTipToplabel.snp.makeConstraints { make in self.detailTipToplabel.snp.makeConstraints { make in
make.top.equalTo(self.tipTopLabel.snp.bottom).offset(0) make.top.equalTo(self.tipTopLabel.snp.bottom).offset(0)
make.width.equalTo(198) make.width.equalTo(120)
make.height.equalTo(17) make.height.equalTo(17)
make.left.equalTo(self.topImageView.snp.right).offset(8) make.left.equalTo(self.topImageView.snp.right).offset(8)
} }
self.sizeToplabel.snp.makeConstraints { make in self.sizeToplabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(26) make.top.equalToSuperview().offset(26)
make.width.equalTo(64) make.left.equalTo(self.detailTipToplabel.snp.right).offset(5)
make.height.equalTo(28) make.height.equalTo(28)
make.right.equalToSuperview().offset(-20) make.right.equalToSuperview().offset(-20)
} }
...@@ -224,19 +224,19 @@ class CompressCompletedViewController : BaseViewController{ ...@@ -224,19 +224,19 @@ class CompressCompletedViewController : BaseViewController{
self.tipBottomLabel.snp.makeConstraints { make in self.tipBottomLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(75) make.top.equalToSuperview().offset(75)
make.width.equalTo(198) make.width.equalTo(120)
make.height.equalTo(22) make.height.equalTo(22)
make.left.equalTo(self.bottomImageView.snp.right).offset(8) make.left.equalTo(self.bottomImageView.snp.right).offset(8)
} }
self.detailTipBottomlabel.snp.makeConstraints { make in self.detailTipBottomlabel.snp.makeConstraints { make in
make.top.equalTo(self.tipBottomLabel.snp.bottom).offset(0) make.top.equalTo(self.tipBottomLabel.snp.bottom).offset(0)
make.width.equalTo(198) make.width.equalTo(120)
make.height.equalTo(17) make.height.equalTo(17)
make.left.equalTo(self.bottomImageView.snp.right).offset(8) make.left.equalTo(self.bottomImageView.snp.right).offset(8)
} }
self.sizeBottomlabel.snp.makeConstraints { make in self.sizeBottomlabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(81) make.top.equalToSuperview().offset(81)
make.width.equalTo(64) make.left.equalTo(self.detailTipBottomlabel.snp.right).offset(5)
make.height.equalTo(28) make.height.equalTo(28)
make.right.equalToSuperview().offset(-20) make.right.equalToSuperview().offset(-20)
} }
......
...@@ -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
manager.compressVideos(models: self.model!, quality: Float(currentQulity)) { (identifier, progress) in CompressViewModel.compressVideos(models: self.model!, quality: Float(currentQulity)) { (identifier, 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,6 +268,7 @@ class CompressQualityController : BaseViewController{ ...@@ -268,6 +268,7 @@ 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("获取视频文件大小失败")
} }
...@@ -275,12 +276,12 @@ class CompressQualityController : BaseViewController{ ...@@ -275,12 +276,12 @@ class CompressQualityController : BaseViewController{
} 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) self.updateNextView(compressAllSize,compressingView,[],outputURLs)
} }
} }
}
} }
actionBlock() actionBlock()
......
...@@ -150,158 +150,211 @@ class CompressViewModel{ ...@@ -150,158 +150,211 @@ class CompressViewModel{
/// 视频压缩 /// 视频压缩
/// - Parameters: /// - Parameters:
/// - assetIdentifiers: 视频文件标识 /// - assetIdentifiers: 视频文件标识
/// - quality: 要锁质量 /// - quality: 压缩质量
/// - progress: 进度回调 /// - progress: 进度回调
/// - completion: 完成回调 /// - completion: 完成回调
func compressVideos(models: [AssetModel], quality: Float, static func compressVideos(models: [AssetModel], quality: Float,
progress: @escaping (String, Float) -> Void, progress: @escaping (String, Float) -> Void,
completion: @escaping ([URL?], [Error?]) -> Void) { completion: @escaping ([URL?], [Error?]) -> Void) {
var outputURLs: [URL?] = Array(repeating: nil, count: models.count)
var errors: [Error?] = Array(repeating: nil, count: models.count)
var completedCount = 0
func processNextVideo(index: Int) { var outputURLs = [URL?](repeating: nil, count: models.count)
guard index < models.count else { var errors = [Error?](repeating: nil, count: models.count)
completion(outputURLs, errors) let group = DispatchGroup()
return
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
} }
let model = models[index] PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { (avAsset, _, _) in
let fetchOptions = PHFetchOptions() guard let avAsset = avAsset else {
let assets = PHAsset.fetchAssets(withLocalIdentifiers: [model.localIdentifier], options: fetchOptions) errors[index] = NSError(domain: "VideoCompression", code: 500, userInfo: [NSLocalizedDescriptionKey: "Could not load asset"])
guard let asset = assets.firstObject else { group.leave()
errors[index] = NSError(domain: "VideoCompressor", code: 1, userInfo: [NSLocalizedDescriptionKey: "Asset not found"])
completedCount += 1
processNextVideo(index: index + 1)
return return
} }
let options = PHVideoRequestOptions() // 获取原始视频信息
options.version = .current guard let videoTrack = avAsset.tracks(withMediaType: .video).first else {
options.deliveryMode = .highQualityFormat errors[index] = NSError(domain: "VideoCompression", code: 501, userInfo: [NSLocalizedDescriptionKey: "No video track found"])
group.leave()
PHImageManager.default().requestAVAsset(forVideo: asset, options: options) { (avAsset, audioMix, info) in
guard let avAsset = avAsset as? AVURLAsset else {
errors[index] = NSError(domain: "VideoCompressor", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to get AVURLAsset"])
completedCount += 1
processNextVideo(index: index + 1)
return return
} }
let composition = AVMutableComposition() let originalBitrate = videoTrack.estimatedDataRate
guard let videoTrack = avAsset.tracks(withMediaType: .video).first else { let originalSize = model.assetSize
errors[index] = NSError(domain: "VideoCompressor", code: 6, userInfo: [NSLocalizedDescriptionKey: "No video track found"])
completedCount += 1 Print("---------原始大小:\(originalSize)")
processNextVideo(index: index + 1)
// 激进压缩设置
let targetBitrate: Float
if originalBitrate > 0 {
// 强制压缩到原始比特率的quality比例,最低不低于500kbps
targetBitrate = max(originalBitrate * quality, 500_000) // 最低500kbps
} else {
// 无法获取原始比特率时的默认值
targetBitrate = quality * 1_000_000 // 1Mbps为基准
}
// 创建输出URL
let outputURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("mp4")
// 激进压缩参数
let compressionProperties: [String: Any] = [
AVVideoAverageBitRateKey: targetBitrate,
AVVideoMaxKeyFrameIntervalKey: 120, // 关键帧间隔加大到4秒(30fps)
AVVideoProfileLevelKey: AVVideoProfileLevelH264Baseline30, // 使用基线配置
AVVideoAllowFrameReorderingKey: false, // 禁用B帧
AVVideoExpectedSourceFrameRateKey: 15 // 降低帧率到15fps
]
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, // 单声道
AVSampleRateKey: 22050, // 降低采样率
AVEncoderBitRateKey: 64_000 // 64kbps音频比特率
]
// 创建导出会话
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 return
} }
let compositionVideoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) exportSession.outputURL = outputURL
do { exportSession.outputFileType = .mp4
try compositionVideoTrack?.insertTimeRange(CMTimeRangeMake(start: .zero, duration: avAsset.duration), of: videoTrack, at: .zero) exportSession.shouldOptimizeForNetworkUse = true
} catch { exportSession.videoComposition = self.aggressiveVideoComposition(for: avAsset, settings: videoSettings)
errors[index] = error exportSession.audioMix = self.aggressiveAudioMix(for: avAsset, settings: audioSettings)
completedCount += 1
processNextVideo(index: index + 1) 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 return
} }
let audioTracks = avAsset.tracks(withMediaType: .audio) // 强制验证文件大小
if let audioTrack = audioTracks.first { if let attributes = try? FileManager.default.attributesOfItem(atPath: outputURL.path),
let compositionAudioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) let compressedSize = attributes[.size] as? NSNumber {
do {
try compositionAudioTrack?.insertTimeRange(CMTimeRangeMake(start: .zero, duration: avAsset.duration), of: audioTrack, at: .zero) if compressedSize.doubleValue >= originalSize {
} catch { // 如果压缩后仍然大于等于原始大小,使用更激进的设置重新压缩
try? FileManager.default.removeItem(at: outputURL)
self.recompressWithMoreAggressiveSettings(avAsset: avAsset, originalSize: originalSize, index: index) { url, error in
outputURLs[index] = url
errors[index] = error errors[index] = error
completedCount += 1 }
processNextVideo(index: index + 1) } else {
return outputURLs[index] = outputURL
}
} else {
outputURLs[index] = outputURL
} }
} }
let outputURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("compressed_video_\(index).mp4") // 进度监控
if FileManager.default.fileExists(atPath: outputURL.path) { DispatchQueue.global().async {
do { while exportSession.status == .waiting || exportSession.status == .exporting {
try FileManager.default.removeItem(at: outputURL) DispatchQueue.main.async {
} catch { progress(model.localIdentifier, exportSession.progress)
errors[index] = error }
completedCount += 1 Thread.sleep(forTimeInterval: 0.1)
processNextVideo(index: index + 1) }
return }
} }
} }
let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality) group.notify(queue: .main) {
guard let exportSession = exportSession else { completion(outputURLs, errors)
errors[index] = NSError(domain: "VideoCompressor", code: 3, userInfo: [NSLocalizedDescriptionKey: "Failed to create export session"])
completedCount += 1
processNextVideo(index: index + 1)
return
} }
exportSession.outputURL = outputURL }
exportSession.outputFileType = .mp4 private static func videoComposition(for asset: AVAsset, quality: Float) -> AVVideoComposition? {
exportSession.shouldOptimizeForNetworkUse = true guard let videoTrack = asset.tracks(withMediaType: .video).first else {
return nil
}
let compressionSettings: [String: Any] = [ // 1. 获取原始视频的比特率作为参考
AVVideoAverageBitRateKey: Int(videoTrack.estimatedDataRate) * Int(quality), let originalBitrate = videoTrack.estimatedDataRate
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel let targetBitrate: Float
]
let _: [String: Any] = [ // 2. 根据质量参数和原始比特率计算目标比特率
AVVideoCodecKey: AVVideoCodecType.h264, if originalBitrate > 0 {
AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill, // 确保压缩后的比特率不超过原始比特率的quality比例
AVVideoCompressionPropertiesKey: compressionSettings // 例如quality=0.7表示压缩到原始比特率的70%
// 最多压缩到95%,避免质量损失太小
targetBitrate = min(originalBitrate * quality, originalBitrate * 0.95)
} else {
// 无法获取原始比特率时的默认值
targetBitrate = quality * 5_000_000 // 5Mbps为基准
}
// 3. 设置更高效的关键帧间隔(从默认的10秒改为30秒)
let videoCompressionProperties: [String: Any] = [
AVVideoAverageBitRateKey: targetBitrate,
// 关键帧间隔(帧数),30fps时约为3秒
AVVideoMaxKeyFrameIntervalKey: 90,
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
// 禁用B帧可减小文件但可能降低质量
AVVideoAllowFrameReorderingKey: false
] ]
let _: [String: Any] = [ // 4. 保持原始分辨率
AVFormatIDKey: kAudioFormatMPEG4AAC, let naturalSize = videoTrack.naturalSize
AVNumberOfChannelsKey: 2, let videoSettings: [String: Any] = [
AVSampleRateKey: 44100, AVVideoCodecKey: AVVideoCodecType.h264,
AVEncoderBitRateKey: 128000 AVVideoWidthKey: naturalSize.width,
AVVideoHeightKey: naturalSize.height,
AVVideoScalingModeKey: AVVideoScalingModeResizeAspect,
AVVideoCompressionPropertiesKey: videoCompressionProperties
] ]
let videoComposition = AVMutableVideoComposition() let composition = AVMutableVideoComposition()
videoComposition.renderSize = videoTrack.naturalSize composition.renderSize = naturalSize
videoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30) composition.frameDuration = CMTime(value: 1, timescale: 30)
let instruction = AVMutableVideoCompositionInstruction() let instruction = AVMutableVideoCompositionInstruction()
instruction.timeRange = CMTimeRangeMake(start: .zero, duration: avAsset.duration) instruction.timeRange = CMTimeRange(start: .zero, duration: asset.duration)
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack) let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
instruction.layerInstructions = [layerInstruction] instruction.layerInstructions = [layerInstruction]
videoComposition.instructions = [instruction] composition.instructions = [instruction]
exportSession.videoComposition = videoComposition
let progressTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
progress(model.localIdentifier, exportSession.progress)
}
exportSession.exportAsynchronously { return composition
progressTimer.invalidate()
switch exportSession.status {
case .completed:
outputURLs[index] = outputURL
errors[index] = nil
case .failed:
outputURLs[index] = nil
errors[index] = exportSession.error
case .cancelled:
outputURLs[index] = nil
errors[index] = NSError(domain: "VideoCompressor", code: 4, userInfo: [NSLocalizedDescriptionKey: "Export cancelled"])
default:
outputURLs[index] = nil
errors[index] = NSError(domain: "VideoCompressor", code: 5, userInfo: [NSLocalizedDescriptionKey: "Unknown export status"])
}
completedCount += 1
processNextVideo(index: index + 1)
} }
private static func presetName(for quality: Float) -> String {
switch quality {
case 0.8...1.0: return AVAssetExportPresetHighestQuality
case 0.6..<0.8: return AVAssetExportPreset1280x720
case 0.4..<0.6: return AVAssetExportPreset960x540
case 0.2..<0.4: return AVAssetExportPreset640x480
default: return AVAssetExportPresetLowQuality
} }
} }
processNextVideo(index: 0)
}
...@@ -408,4 +461,91 @@ class CompressViewModel{ ...@@ -408,4 +461,91 @@ 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] = [
AVVideoAverageBitRateKey: 250_000, // 固定250kbps
AVVideoMaxKeyFrameIntervalKey: 240, // 关键帧间隔8秒
AVVideoProfileLevelKey: AVVideoProfileLevelH264Baseline30,
AVVideoAllowFrameReorderingKey: false,
AVVideoExpectedSourceFrameRateKey: 10 // 帧率降到10fps
]
let videoSettings: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: videoTrack.naturalSize.width / 4, // 分辨率降到1/4
AVVideoHeightKey: videoTrack.naturalSize.height / 4,
AVVideoScalingModeKey: AVVideoScalingModeResizeAspect,
AVVideoCompressionPropertiesKey: compressionProperties
]
let audioSettings: [String: Any] = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVNumberOfChannelsKey: 1,
AVSampleRateKey: 16000, // 16kHz采样率
AVEncoderBitRateKey: 32_000 // 32kbps音频比特率
]
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
}
} }
...@@ -9,6 +9,16 @@ ...@@ -9,6 +9,16 @@
"heightImage": "tabbar_secret_hight", "heightImage": "tabbar_secret_hight",
"text":"Secret Space", "text":"Secret Space",
}, },
{
"normalImage": "ic_contacts_home_pre",
"heightImage": "tabbar_contacts_hight",
"text":"Contacts",
},
{
"normalImage": "ic_email_home_pre",
"heightImage": "tabbar_email_hight",
"text":"Email Cleaner",
},
{ {
"normalImage": "ic_cmpress_home_pre", "normalImage": "ic_cmpress_home_pre",
"heightImage": "tabbar_cmpress_high", "heightImage": "tabbar_cmpress_high",
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>NSContactsUsageDescription</key>
<string>Phone Manager needs access to your contacts to find and merge duplicate contacts</string>
<key>NSUserTrackingUsageDescription</key> <key>NSUserTrackingUsageDescription</key>
<string>We need your permission to track your usage habits in order to provide a more personalized advertising experience</string> <string>We need your permission to track your usage habits in order to provide a more personalized advertising experience</string>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
......
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