| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 |
- //
- // 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)
- }
- }
|