Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Sign in / Register
Toggle navigation
P
PhoneManager
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Packages
Packages
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Yang
PhoneManager
Commits
66ed2451
Commit
66ed2451
authored
May 07, 2025
by
CZ1004
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
【修改】修改压缩方法
parent
4b92a5f0
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
379 additions
and
249 deletions
+379
-249
CompressQualityController.swift
...ssion/Compress/Controller/CompressQualityController.swift
+4
-5
CompressViewModel.swift
.../Class/Session/Compress/ViewModel/CompressViewModel.swift
+7
-244
VideoCompressor.swift
...er/Class/Session/Compress/ViewModel/VideoCompressor.swift
+368
-0
No files found.
PhoneManager/Class/Session/Compress/Controller/CompressQualityController.swift
View file @
66ed2451
...
@@ -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
)
}
}
}
}
...
...
PhoneManager/Class/Session/Compress/ViewModel/CompressViewModel.swift
View file @
66ed2451
...
@@ -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
}
}
}
PhoneManager/Class/Session/Compress/ViewModel/VideoCompressor.swift
0 → 100644
View file @
66ed2451
@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
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment