@@ -5,6 +5,57 @@ private struct ExportSessionReference: @unchecked Sendable {
55 let session : AVAssetExportSession
66}
77
8+ private func videoCompressionFileSizeBytes( at url: URL ) -> Int64 ? {
9+ guard let fileSize = try ? url. resourceValues ( forKeys: [ . fileSizeKey] ) . fileSize else {
10+ return nil
11+ }
12+ return Int64 ( fileSize)
13+ }
14+
15+ private func videoCompressionFormatBytes( _ value: Int64 ? ) -> String {
16+ guard let value else {
17+ return " nil "
18+ }
19+ return " \( value) "
20+ }
21+
22+ private func videoCompressionFormatSeconds( _ time: CMTime ) -> String {
23+ let seconds = CMTimeGetSeconds ( time)
24+ guard seconds. isFinite else {
25+ return " indefinite "
26+ }
27+ return String ( format: " %.3f " , seconds)
28+ }
29+
30+ private func videoCompressionFormatSize( _ size: CGSize ) -> String {
31+ String ( format: " %.0fx%.0f " , size. width, size. height)
32+ }
33+
34+ private func videoCompressionFormatTransform( _ transform: CGAffineTransform ) -> String {
35+ String (
36+ format: " [a=%.3f,b=%.3f,c=%.3f,d=%.3f,tx=%.3f,ty=%.3f] " ,
37+ transform. a,
38+ transform. b,
39+ transform. c,
40+ transform. d,
41+ transform. tx,
42+ transform. ty
43+ )
44+ }
45+
46+ private func videoCompressionFormatFileType( _ fileType: AVFileType ? ) -> String {
47+ fileType? . rawValue ?? " nil "
48+ }
49+
50+ private func videoCompressionFormatString( _ value: String ? ) -> String {
51+ value ?? " nil "
52+ }
53+
54+ private func videoCompressionDescribeError( _ error: Error ) -> String {
55+ let nsError = error as NSError
56+ return " domain= \( nsError. domain) code= \( nsError. code) description= \( nsError. localizedDescription) "
57+ }
58+
859struct VideoCompressionJob {
960 private struct SourceColorProperties {
1061 let colorPrimaries : String ?
@@ -37,6 +88,10 @@ struct VideoCompressionJob {
3788 videoComposition. colorTransferFunction = colorTransferFunction
3889 videoComposition. colorYCbCrMatrix = colorYCbCrMatrix
3990 }
91+
92+ var debugDescription : String {
93+ " containsHDR= \( containsHDR) colorPrimaries= \( videoCompressionFormatString ( colorPrimaries) ) colorTransferFunction= \( videoCompressionFormatString ( colorTransferFunction) ) colorYCbCrMatrix= \( videoCompressionFormatString ( colorYCbCrMatrix) ) "
94+ }
4095 }
4196
4297 enum Option : String , CaseIterable , Identifiable {
@@ -125,6 +180,10 @@ struct VideoCompressionJob {
125180 let roundedDown = floor ( value / 2 ) * 2
126181 return max ( 2 , roundedDown)
127182 }
183+
184+ var debugDescription : String {
185+ " id= \( id) title= \( title) preset= \( exportPresetName) usesHEVC= \( usesHEVC) boundingSize= \( videoCompressionFormatSize ( landscapeBoundingSize) ) "
186+ }
128187 }
129188
130189 struct PreparedExport {
@@ -163,36 +222,73 @@ struct VideoCompressionJob {
163222 let option : Option
164223
165224 func prepareExport( ) async throws -> PreparedExport {
166- let asset = AVURLAsset ( url: sourceURL)
167- guard let session = AVAssetExportSession ( asset: asset, presetName: option. exportPresetName) else {
168- throw CompressionError . exportSessionUnavailable
169- }
225+ VideoLogger . log (
226+ " [VideoCompressionJob] prepareExport start source= \( sourceURL. path) sourceSizeBytes= \( videoCompressionFormatBytes ( videoCompressionFileSizeBytes ( at: sourceURL) ) ) option={ \( option. debugDescription) } "
227+ )
170228
171- let outputFileType = try preferredOutputFileType ( for: session)
172- let outputURL = try makeOutputURL ( for: outputFileType)
173- session. outputURL = outputURL
174- session. outputFileType = outputFileType
175- session. shouldOptimizeForNetworkUse = true
176- session. videoComposition = try await makeVideoComposition ( for: asset)
229+ do {
230+ let asset = AVURLAsset ( url: sourceURL)
231+ guard let session = AVAssetExportSession ( asset: asset, presetName: option. exportPresetName) else {
232+ VideoLogger . log (
233+ " [VideoCompressionJob] prepareExport could not create AVAssetExportSession source= \( sourceURL. path) preset= \( option. exportPresetName) "
234+ )
235+ throw CompressionError . exportSessionUnavailable
236+ }
237+
238+ VideoLogger . log (
239+ " [VideoCompressionJob] export session created preset= \( option. exportPresetName) supportedFileTypes= \( session. supportedFileTypes. map ( \. rawValue) . joined ( separator: " , " ) ) "
240+ )
241+
242+ let outputFileType = try preferredOutputFileType ( for: session)
243+ let outputURL = try makeOutputURL ( for: outputFileType)
244+ session. outputURL = outputURL
245+ session. outputFileType = outputFileType
246+ session. shouldOptimizeForNetworkUse = true
247+ session. videoComposition = try await makeVideoComposition ( for: asset)
177248
178- return PreparedExport ( session: session, outputURL: outputURL)
249+ VideoLogger . log (
250+ " [VideoCompressionJob] prepareExport ready outputURL= \( outputURL. path) outputFileType= \( outputFileType. rawValue) renderSize= \( videoCompressionFormatSize ( session. videoComposition? . renderSize ?? . zero) ) frameDurationSeconds= \( videoCompressionFormatSeconds ( session. videoComposition? . frameDuration ?? . invalid) ) optimizeForNetworkUse= \( session. shouldOptimizeForNetworkUse) "
251+ )
252+
253+ return PreparedExport ( session: session, outputURL: outputURL)
254+ } catch {
255+ VideoLogger . log (
256+ " [VideoCompressionJob] prepareExport failed source= \( sourceURL. path) option= \( option. id) error= \( videoCompressionDescribeError ( error) ) "
257+ )
258+ throw error
259+ }
179260 }
180261
181262 static func export( _ session: AVAssetExportSession ) async throws {
263+ VideoLogger . log (
264+ " [VideoCompressionJob] export start outputURL= \( session. outputURL? . path ?? " nil " ) outputFileType= \( videoCompressionFormatFileType ( session. outputFileType) ) progress= \( String ( format: " %.3f " , session. progress) ) "
265+ )
182266 let reference = ExportSessionReference ( session: session)
183267 try await withCheckedThrowingContinuation { continuation in
184268 reference. session. exportAsynchronously {
185269 switch reference. session. status {
186270 case . completed:
271+ VideoLogger . log (
272+ " [VideoCompressionJob] export completed outputURL= \( reference. session. outputURL? . path ?? " nil " ) progress= \( String ( format: " %.3f " , reference. session. progress) ) "
273+ )
187274 continuation. resume ( )
188275 case . cancelled:
276+ VideoLogger . log (
277+ " [VideoCompressionJob] export cancelled outputURL= \( reference. session. outputURL? . path ?? " nil " ) progress= \( String ( format: " %.3f " , reference. session. progress) ) "
278+ )
189279 continuation. resume ( throwing: CancellationError ( ) )
190280 case . failed:
281+ VideoLogger . log (
282+ " [VideoCompressionJob] export failed outputURL= \( reference. session. outputURL? . path ?? " nil " ) progress= \( String ( format: " %.3f " , reference. session. progress) ) error= \( videoCompressionDescribeError ( reference. session. error ?? CompressionError . exportFailed ( nil ) ) ) "
283+ )
191284 continuation. resume (
192285 throwing: reference. session. error
193286 ?? CompressionError . exportFailed ( nil )
194287 )
195288 default :
289+ VideoLogger . log (
290+ " [VideoCompressionJob] export ended unexpectedly status= \( reference. session. status. rawValue) outputURL= \( reference. session. outputURL? . path ?? " nil " ) progress= \( String ( format: " %.3f " , reference. session. progress) ) error= \( videoCompressionDescribeError ( reference. session. error ?? CompressionError . exportFailed ( nil ) ) ) "
291+ )
196292 continuation. resume (
197293 throwing: reference. session. error
198294 ?? CompressionError . exportFailed ( nil )
@@ -204,6 +300,9 @@ struct VideoCompressionJob {
204300
205301 private func makeVideoComposition( for asset: AVAsset ) async throws -> AVMutableVideoComposition {
206302 guard let videoTrack = try await asset. loadTracks ( withMediaType: . video) . first else {
303+ VideoLogger . log (
304+ " [VideoCompressionJob] asset has no readable video track source= \( sourceURL. path) "
305+ )
207306 throw CompressionError . invalidVideoTrack
208307 }
209308
@@ -213,6 +312,9 @@ struct VideoCompressionJob {
213312 formatDescriptions: formatDescriptions
214313 )
215314 if sourceColorProperties. containsHDR && !option. usesHEVC {
315+ VideoLogger . log (
316+ " [VideoCompressionJob] rejecting non-HEVC preset for HDR source source= \( sourceURL. path) option= \( option. id) "
317+ )
216318 throw CompressionError . hdrRequiresHEVC
217319 }
218320
@@ -222,12 +324,19 @@ struct VideoCompressionJob {
222324 let sourceBounds = CGRect ( origin: . zero, size: naturalSize) . applying ( preferredTransform)
223325 let sourceSize = CGSize ( width: abs ( sourceBounds. width) , height: abs ( sourceBounds. height) )
224326 guard sourceSize. width > 0 , sourceSize. height > 0 else {
327+ VideoLogger . log (
328+ " [VideoCompressionJob] invalid transformed video size source= \( sourceURL. path) naturalSize= \( videoCompressionFormatSize ( naturalSize) ) preferredTransform= \( videoCompressionFormatTransform ( preferredTransform) ) "
329+ )
225330 throw CompressionError . invalidVideoTrack
226331 }
227332
228333 let renderSize = option. renderSize ( for: sourceSize)
229334 let frameRate = ( try ? await videoTrack. load ( . nominalFrameRate) ) ?? 0
230335
336+ VideoLogger . log (
337+ " [VideoCompressionJob] source track details source= \( sourceURL. path) durationSeconds= \( videoCompressionFormatSeconds ( duration) ) naturalSize= \( videoCompressionFormatSize ( naturalSize) ) transformedSize= \( videoCompressionFormatSize ( sourceSize) ) nominalFrameRate= \( String ( format: " %.3f " , frameRate) ) preferredTransform= \( videoCompressionFormatTransform ( preferredTransform) ) colorProperties={ \( sourceColorProperties. debugDescription) } option= \( option. id) "
338+ )
339+
231340 let videoComposition = AVMutableVideoComposition ( )
232341 videoComposition. renderSize = renderSize
233342 videoComposition. frameDuration = CMTime (
@@ -236,20 +345,24 @@ struct VideoCompressionJob {
236345 )
237346 sourceColorProperties. apply ( to: videoComposition)
238347
348+ let transform = scaledTransform (
349+ naturalSize: naturalSize,
350+ preferredTransform: preferredTransform,
351+ renderSize: renderSize
352+ )
239353 let layerInstruction = AVMutableVideoCompositionLayerInstruction ( assetTrack: videoTrack)
240354 layerInstruction. setTransform (
241- scaledTransform (
242- naturalSize: naturalSize,
243- preferredTransform: preferredTransform,
244- renderSize: renderSize
245- ) ,
355+ transform,
246356 at: . zero
247357 )
248358
249359 let instruction = AVMutableVideoCompositionInstruction ( )
250360 instruction. timeRange = CMTimeRange ( start: . zero, duration: duration)
251361 instruction. layerInstructions = [ layerInstruction]
252362 videoComposition. instructions = [ instruction]
363+ VideoLogger . log (
364+ " [VideoCompressionJob] video composition prepared renderSize= \( videoCompressionFormatSize ( renderSize) ) frameDurationSeconds= \( videoCompressionFormatSeconds ( videoComposition. frameDuration) ) transform= \( videoCompressionFormatTransform ( transform) ) instructionDurationSeconds= \( videoCompressionFormatSeconds ( duration) ) "
365+ )
253366 return videoComposition
254367 }
255368
@@ -264,12 +377,23 @@ struct VideoCompressionJob {
264377 preferredTypes = [ . mov, . mp4, . m4v]
265378 }
266379
380+ VideoLogger . log (
381+ " [VideoCompressionJob] resolving output file type sourceExtension= \( sourceURL. pathExtension. lowercased ( ) ) preferredTypes= \( preferredTypes. map ( \. rawValue) . joined ( separator: " , " ) ) supportedFileTypes= \( session. supportedFileTypes. map ( \. rawValue) . joined ( separator: " , " ) ) "
382+ )
383+
267384 if let outputFileType = preferredTypes. first ( where: session. supportedFileTypes. contains) {
385+ VideoLogger . log (
386+ " [VideoCompressionJob] selected output file type= \( outputFileType. rawValue) "
387+ )
268388 return outputFileType
269389 }
270390 if let outputFileType = session. supportedFileTypes. first {
391+ VideoLogger . log (
392+ " [VideoCompressionJob] selected fallback output file type= \( outputFileType. rawValue) "
393+ )
271394 return outputFileType
272395 }
396+ VideoLogger . log ( " [VideoCompressionJob] output file type unavailable " )
273397 throw CompressionError . outputFileTypeUnavailable
274398 }
275399
@@ -281,10 +405,14 @@ struct VideoCompressionJob {
281405 withIntermediateDirectories: true
282406 )
283407
284- let fileName = sourceURL . deletingPathExtension ( ) . lastPathComponent
285- return temporaryDirectory
408+ let fileName = UUID ( ) . uuidString . lowercased ( )
409+ let outputURL = temporaryDirectory
286410 . appendingPathComponent ( fileName, isDirectory: false )
287411 . appendingPathExtension ( fileExtension ( for: fileType) )
412+ VideoLogger . log (
413+ " [VideoCompressionJob] created temporary export directory= \( temporaryDirectory. path) outputURL= \( outputURL. path) "
414+ )
415+ return outputURL
288416 }
289417
290418 private func fileExtension( for fileType: AVFileType ) -> String {
0 commit comments