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 {
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return nil
}
}
......
......@@ -146,7 +146,20 @@ class CompressSelectCell : UICollectionViewCell {
@objc func selectClick(){
self.choose = !self.choose
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
}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 {
make.height.width.equalTo(80)
}
}
required init?(coder: NSCoder) {
......@@ -219,10 +230,6 @@ class CompressSelectCell : UICollectionViewCell {
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 {
let vc = PMShowImgVideoController()
......@@ -236,12 +243,9 @@ class CompressSelectCell : UICollectionViewCell {
vc.homeDataSource = [dataSource]
self.responderViewController()?.navigationController?.pushViewController(vc, animated: true)
}
}else{
// 如果是视频
// let vc : PreVideoController = PreVideoController(localIdentifier: self.model!.localIdentifier)
// self.responderViewController()?.navigationController?.pushViewController(vc, animated: true)
if let tempModel = self.model {
// 获取视频的图片
PhotoAndVideoMananger.mananger.getVideoImageByIdent(ident: tempModel) { image in
......@@ -271,7 +275,6 @@ class CompressSelectCell : UICollectionViewCell {
}
}
}
} errorHandler: {
DispatchQueue.main.async {
let alert = UIAlertController(title: nil, message: "Get Video image failure", preferredStyle: .alert)
......@@ -285,8 +288,8 @@ class CompressSelectCell : UICollectionViewCell {
}
}
}
}
}
}
}
......@@ -83,7 +83,7 @@ class CompressCompletedViewController : BaseViewController{
let label = UILabel()
label.text = "511MB"
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.layer.borderColor = UIColor(red: 0, green: 0.51, blue: 1, alpha: 1).cgColor
label.backgroundColor = .clear
......@@ -119,7 +119,7 @@ class CompressCompletedViewController : BaseViewController{
let label = UILabel()
label.text = "1.0%"
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.layer.borderColor = UIColor(red: 0, green: 0.51, blue: 1, alpha: 1).cgColor
label.backgroundColor = .clear
......@@ -200,19 +200,19 @@ class CompressCompletedViewController : BaseViewController{
self.tipTopLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(20)
make.width.equalTo(198)
make.width.equalTo(120)
make.height.equalTo(22)
make.left.equalTo(self.topImageView.snp.right).offset(8)
}
self.detailTipToplabel.snp.makeConstraints { make in
make.top.equalTo(self.tipTopLabel.snp.bottom).offset(0)
make.width.equalTo(198)
make.width.equalTo(120)
make.height.equalTo(17)
make.left.equalTo(self.topImageView.snp.right).offset(8)
}
self.sizeToplabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(26)
make.width.equalTo(64)
make.left.equalTo(self.detailTipToplabel.snp.right).offset(5)
make.height.equalTo(28)
make.right.equalToSuperview().offset(-20)
}
......@@ -224,19 +224,19 @@ class CompressCompletedViewController : BaseViewController{
self.tipBottomLabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(75)
make.width.equalTo(198)
make.width.equalTo(120)
make.height.equalTo(22)
make.left.equalTo(self.bottomImageView.snp.right).offset(8)
}
self.detailTipBottomlabel.snp.makeConstraints { make in
make.top.equalTo(self.tipBottomLabel.snp.bottom).offset(0)
make.width.equalTo(198)
make.width.equalTo(120)
make.height.equalTo(17)
make.left.equalTo(self.bottomImageView.snp.right).offset(8)
}
self.sizeBottomlabel.snp.makeConstraints { make in
make.top.equalToSuperview().offset(81)
make.width.equalTo(64)
make.left.equalTo(self.detailTipBottomlabel.snp.right).offset(5)
make.height.equalTo(28)
make.right.equalToSuperview().offset(-20)
}
......
......@@ -258,7 +258,7 @@ class CompressQualityController : BaseViewController{
}else{
// 压缩视频
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)
} completion: { (outputURLs, errors) in
for (index, outputURL) in outputURLs.enumerated() {
......@@ -268,6 +268,7 @@ class CompressQualityController : BaseViewController{
if let fileSize = attributes[.size] as? Int64 {
compressAllSize = compressAllSize + Double(fileSize)
}
Print("---------压缩后的大小:\(compressAllSize)")
} catch {
Print("获取视频文件大小失败")
}
......@@ -275,12 +276,12 @@ class CompressQualityController : BaseViewController{
} else if let error = errors[index] {
print("Error compressing video \(index): \(error.localizedDescription)")
}
// 不管成功失败都跳转下一页
self.updateNextView(compressAllSize,compressingView,[],outputURLs)
}
self.updateNextView(compressAllSize,compressingView,[],outputURLs)
}
}
}
actionBlock()
......
......@@ -150,162 +150,215 @@ class CompressViewModel{
/// 视频压缩
/// - Parameters:
/// - assetIdentifiers: 视频文件标识
/// - quality: 要锁质量
/// - quality: 压缩质量
/// - progress: 进度回调
/// - completion: 完成回调
func compressVideos(models: [AssetModel], quality: Float,
progress: @escaping (String, Float) -> 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
static func compressVideos(models: [AssetModel], quality: Float,
progress: @escaping (String, Float) -> Void,
completion: @escaping ([URL?], [Error?]) -> Void) {
func processNextVideo(index: Int) {
guard index < models.count else {
completion(outputURLs, errors)
return
}
var outputURLs = [URL?](repeating: nil, count: models.count)
var errors = [Error?](repeating: nil, count: models.count)
let group = DispatchGroup()
for (index, model) in models.enumerated() {
group.enter()
let model = models[index]
let fetchOptions = PHFetchOptions()
let assets = PHAsset.fetchAssets(withLocalIdentifiers: [model.localIdentifier], options: fetchOptions)
guard let asset = assets.firstObject else {
errors[index] = NSError(domain: "VideoCompressor", code: 1, userInfo: [NSLocalizedDescriptionKey: "Asset not found"])
completedCount += 1
processNextVideo(index: index + 1)
return
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 options = PHVideoRequestOptions()
options.version = .current
options.deliveryMode = .highQualityFormat
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)
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
}
let composition = AVMutableComposition()
// 获取原始视频信息
guard let videoTrack = avAsset.tracks(withMediaType: .video).first else {
errors[index] = NSError(domain: "VideoCompressor", code: 6, userInfo: [NSLocalizedDescriptionKey: "No video track found"])
completedCount += 1
processNextVideo(index: index + 1)
errors[index] = NSError(domain: "VideoCompression", code: 501, userInfo: [NSLocalizedDescriptionKey: "No video track found"])
group.leave()
return
}
let compositionVideoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid)
do {
try compositionVideoTrack?.insertTimeRange(CMTimeRangeMake(start: .zero, duration: avAsset.duration), of: videoTrack, at: .zero)
} catch {
errors[index] = error
completedCount += 1
processNextVideo(index: index + 1)
return
}
let originalBitrate = videoTrack.estimatedDataRate
let originalSize = model.assetSize
let audioTracks = avAsset.tracks(withMediaType: .audio)
if let audioTrack = audioTracks.first {
let compositionAudioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
do {
try compositionAudioTrack?.insertTimeRange(CMTimeRangeMake(start: .zero, duration: avAsset.duration), of: audioTrack, at: .zero)
} catch {
errors[index] = error
completedCount += 1
processNextVideo(index: index + 1)
return
}
}
Print("---------原始大小:\(originalSize)")
let outputURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("compressed_video_\(index).mp4")
if FileManager.default.fileExists(atPath: outputURL.path) {
do {
try FileManager.default.removeItem(at: outputURL)
} catch {
errors[index] = error
completedCount += 1
processNextVideo(index: index + 1)
return
}
// 激进压缩设置
let targetBitrate: Float
if originalBitrate > 0 {
// 强制压缩到原始比特率的quality比例,最低不低于500kbps
targetBitrate = max(originalBitrate * quality, 500_000) // 最低500kbps
} else {
// 无法获取原始比特率时的默认值
targetBitrate = quality * 1_000_000 // 1Mbps为基准
}
let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)
guard let exportSession = exportSession else {
errors[index] = NSError(domain: "VideoCompressor", code: 3, userInfo: [NSLocalizedDescriptionKey: "Failed to create export session"])
completedCount += 1
processNextVideo(index: index + 1)
return
}
// 创建输出URL
let outputURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("mp4")
exportSession.outputURL = outputURL
exportSession.outputFileType = .mp4
exportSession.shouldOptimizeForNetworkUse = true
let compressionSettings: [String: Any] = [
AVVideoAverageBitRateKey: Int(videoTrack.estimatedDataRate) * Int(quality),
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel
// 激进压缩参数
let compressionProperties: [String: Any] = [
AVVideoAverageBitRateKey: targetBitrate,
AVVideoMaxKeyFrameIntervalKey: 120, // 关键帧间隔加大到4秒(30fps)
AVVideoProfileLevelKey: AVVideoProfileLevelH264Baseline30, // 使用基线配置
AVVideoAllowFrameReorderingKey: false, // 禁用B帧
AVVideoExpectedSourceFrameRateKey: 15 // 降低帧率到15fps
]
let _: [String: Any] = [
let videoSettings: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoScalingModeKey: AVVideoScalingModeResizeAspectFill,
AVVideoCompressionPropertiesKey: compressionSettings
AVVideoWidthKey: videoTrack.naturalSize.width / 2, // 分辨率减半
AVVideoHeightKey: videoTrack.naturalSize.height / 2,
AVVideoScalingModeKey: AVVideoScalingModeResizeAspect,
AVVideoCompressionPropertiesKey: compressionProperties
]
let _: [String: Any] = [
// 音频设置也进行压缩
let audioSettings: [String: Any] = [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVNumberOfChannelsKey: 2,
AVSampleRateKey: 44100,
AVEncoderBitRateKey: 128000
AVNumberOfChannelsKey: 1, // 单声道
AVSampleRateKey: 22050, // 降低采样率
AVEncoderBitRateKey: 64_000 // 64kbps音频比特率
]
let videoComposition = AVMutableVideoComposition()
videoComposition.renderSize = videoTrack.naturalSize
videoComposition.frameDuration = CMTimeMake(value: 1, timescale: 30)
let instruction = AVMutableVideoCompositionInstruction()
instruction.timeRange = CMTimeRangeMake(start: .zero, duration: avAsset.duration)
let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack)
instruction.layerInstructions = [layerInstruction]
videoComposition.instructions = [instruction]
exportSession.videoComposition = videoComposition
let progressTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
progress(model.localIdentifier, exportSession.progress)
// 创建导出会话
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 {
progressTimer.invalidate()
switch exportSession.status {
case .completed:
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
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)
}
// 进度监控
DispatchQueue.global().async {
while exportSession.status == .waiting || exportSession.status == .exporting {
DispatchQueue.main.async {
progress(model.localIdentifier, exportSession.progress)
}
Thread.sleep(forTimeInterval: 0.1)
}
}
}
}
processNextVideo(index: 0)
group.notify(queue: .main) {
completion(outputURLs, errors)
}
}
private static func videoComposition(for asset: AVAsset, quality: Float) -> AVVideoComposition? {
guard let videoTrack = asset.tracks(withMediaType: .video).first else {
return nil
}
// 1. 获取原始视频的比特率作为参考
let originalBitrate = videoTrack.estimatedDataRate
let targetBitrate: Float
// 2. 根据质量参数和原始比特率计算目标比特率
if originalBitrate > 0 {
// 确保压缩后的比特率不超过原始比特率的quality比例
// 例如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
]
// 4. 保持原始分辨率
let naturalSize = videoTrack.naturalSize
let videoSettings: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: naturalSize.width,
AVVideoHeightKey: naturalSize.height,
AVVideoScalingModeKey: AVVideoScalingModeResizeAspect,
AVVideoCompressionPropertiesKey: videoCompressionProperties
]
let composition = AVMutableVideoComposition()
composition.renderSize = naturalSize
composition.frameDuration = CMTime(value: 1, timescale: 30)
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 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
}
}
/// 压缩多张图片
/// - Parameters:
/// - assets: 图片集合
......@@ -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 @@
"heightImage": "tabbar_secret_hight",
"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",
"heightImage": "tabbar_cmpress_high",
......
......@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSContactsUsageDescription</key>
<string>Phone Manager needs access to your contacts to find and merge duplicate contacts</string>
<key>NSUserTrackingUsageDescription</key>
<string>We need your permission to track your usage habits in order to provide a more personalized advertising experience</string>
<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