// // LNVideoCompressor.swift // Gami // // Created by OneeChan on 2026/2/28. // import Foundation import AVFoundation class LNVideoCompressor { static func compressVideo(sourceURL: URL, completion: @escaping (URL?) -> Void) { DispatchQueue.global().async { // 检查源文件是否存在 guard FileManager.default.fileExists(atPath: sourceURL.path) else { showToast(.init(key: "B00014")) completion(nil) return } // 获取视频资产 let asset = AVAsset(url: sourceURL) // 计算目标码率(关键:码率决定文件大小) let videoDuration = CMTimeGetSeconds(asset.duration) guard videoDuration > 0, videoDuration <= 1 * 60 else { showToast(.init(key: "A00299")) completion(nil) return } guard let videoTrack = asset.tracks(withMediaType: .video).first else { showToast(.init(key: "A00298", 10001)) completion(nil) return } do { // 创建写入器:指定输出格式为 MP4,并传入流媒体配置 let outputUrl = getTempVideoURL() // 确保临时文件不存在(避免写入冲突) if FileManager.default.fileExists(atPath: outputUrl.path) { try FileManager.default.removeItem(at: outputUrl) } let writer = try AVAssetWriter(outputURL: outputUrl, fileType: .mp4) // 关键配置:启用 interleaved 写入(确保 moov atom 前置) writer.shouldOptimizeForNetworkUse = true // 核心!开启网络优化(流式播放) // 配置视频编码参数(流媒体核心参数) let videoSettings = createCompressionSettings(asset: asset) // 创建视频输入 let videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings) videoInput.expectsMediaDataInRealTime = false videoInput.transform = videoTrack.preferredTransform // 保持原视频方向 guard writer.canAdd(videoInput) else { showToast(.init(key: "A00298", 10002)) completion(nil) return } writer.add(videoInput) // 创建音频输入 var audioInput: AVAssetWriterInput? if asset.tracks(withMediaType: .audio).first != nil { let audioSettings = [ AVFormatIDKey: kAudioFormatMPEG4AAC, AVSampleRateKey: 44100, AVNumberOfChannelsKey: 2, AVEncoderBitRateKey: 128000 // 128Kbps 音频比特率 ] let input = AVAssetWriterInput(mediaType: .audio, outputSettings: audioSettings) input.expectsMediaDataInRealTime = false if writer.canAdd(input) { writer.add(input) audioInput = input } } // 创建 AVAssetReader let reader = try AVAssetReader(asset: asset) // 创建视频读取器 let readerOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: [ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA ]) guard reader.canAdd(readerOutput) else { showToast(.init(key: "A00298", 10003)) completion(nil) return } reader.add(readerOutput) // 处理音频 var audioOutput: AVAssetReaderTrackOutput? if let audioTrack = asset.tracks(withMediaType: .audio).first { // 音频读取器输出 let output = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: [ AVFormatIDKey: kAudioFormatLinearPCM ]) if reader.canAdd(output) { reader.add(output) audioOutput = output } } // 开始写入 guard writer.startWriting() else { showToast(.init(key: "A00298", 10004)) completion(nil) return } writer.startSession(atSourceTime: .zero) guard reader.startReading() else { showToast(.init(key: "A00298", 10005)) completion(nil) return } let dispatchGroup = DispatchGroup() // 逐帧处理并写入 dispatchGroup.enter() videoInput.requestMediaDataWhenReady(on: DispatchQueue(label: "com.jiehe.gami.video.compress")) { while videoInput.isReadyForMoreMediaData { if let sampleBuffer = readerOutput.copyNextSampleBuffer() { videoInput.append(sampleBuffer) } else { videoInput.markAsFinished() dispatchGroup.leave() break } } } // 处理音频 if let audioInput, let audioOutput { dispatchGroup.enter() audioInput.requestMediaDataWhenReady(on: DispatchQueue(label: "com.jiehe.gami.audio.compress")) { while audioInput.isReadyForMoreMediaData { if let sampleBuffer = audioOutput.copyNextSampleBuffer() { audioInput.append(sampleBuffer) } else { audioInput.markAsFinished() dispatchGroup.leave() break } } } } // 等待完成 dispatchGroup.notify(queue: .global()) { writer.finishWriting { switch writer.status { case .completed: completion(outputUrl) case .failed: let err = reader.error ?? writer.error showToast("\(err?.localizedDescription ?? "")") completion(nil) default: break } } } } catch { showToast(error.localizedDescription) completion(nil) } } } } extension LNVideoCompressor { /// 创建压缩设置(视频码率、分辨率) private static func createCompressionSettings(asset: AVAsset) -> [String: Any] { var settings = [String: Any]() // 获取视频轨道 guard let videoTrack = asset.tracks(withMediaType: .video).first else { return settings } // 视频编码类型 settings[AVVideoCodecKey] = AVVideoCodecType.h264 // H.264编码(兼容性好) // 分辨率设置(保持原比例,避免拉伸) let naturalSize = videoTrack.naturalSize let transform = videoTrack.preferredTransform var videoSize = naturalSize // // 处理旋转 // if transform.a == 0 && transform.b == 1.0 && transform.c == -1.0 && transform.d == 0 || // transform.a == 0 && transform.b == -1.0 && transform.c == 1.0 && transform.d == 0 { // videoSize = CGSize(width: naturalSize.height, height: naturalSize.width) // } // 限制最大分辨率(限制为720P) let maxDimension: CGFloat = 720 let scaleFactor = min(maxDimension / videoSize.width, maxDimension / videoSize.height, 1.0) let finalWidth = floor(videoSize.width * scaleFactor) let finalHeight = floor(videoSize.height * scaleFactor) // 确保宽高为偶数(H.264要求) let outputWidth = finalWidth.truncatingRemainder(dividingBy: 2) == 0 ? finalWidth : finalWidth - 1 let outputHeight = finalHeight.truncatingRemainder(dividingBy: 2) == 0 ? finalHeight : finalHeight - 1 settings[AVVideoWidthKey] = outputWidth settings[AVVideoHeightKey] = outputHeight // 流媒体关键配置 settings[AVVideoCompressionPropertiesKey] = [ // 2. 比特率模式:恒定比特率(CBR),适合流媒体 AVVideoAverageBitRateKey: 1000000, // 1Mbps // 3. H.264 配置:兼容流媒体 AVVideoProfileLevelKey: AVVideoProfileLevelH264Baseline31, // Baseline 级别兼容性更好 AVVideoAllowFrameReorderingKey: false // 禁止帧重排序,便于流媒体解析 ] return settings } /// 获取临时视频文件URL private static func getTempVideoURL() -> URL { let tempDir = NSTemporaryDirectory() let fileName = UUID().uuidString + ".mp4" return URL(fileURLWithPath: tempDir).appendingPathComponent(fileName) } }