expo-pro-video-editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierrc +8 -0
- package/LICENSE +67 -0
- package/NOTICE.md +250 -0
- package/README.md +70 -0
- package/android/build.gradle +35 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/provideoeditor/ExpoProVideoEditorModule.kt +163 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/core/constants/LoggingConstants.kt +4 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/EffectsProcessor.kt +66 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/RenderVideo.kt +1089 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ApplyAnimation.kt +290 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ApplyBitrate.kt +110 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ApplyBlur.kt +27 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ApplyChromaKey.kt +58 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ApplyClipTransition.kt +88 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ApplyColorMatrix.kt +144 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ApplyComposition.kt +86 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ApplyCrop.kt +104 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ApplyFlip.kt +27 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ApplyFrameRate.kt +28 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ApplyImageLayer.kt +397 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ApplyOpacity.kt +24 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ApplyPlaybackSpeed.kt +35 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ApplyRotation.kt +26 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ApplyScale.kt +29 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/AudioPreRenderer.kt +492 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/AudioSequenceBuilder.kt +144 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/BitrateCapPolicy.kt +45 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ChromaKeyEffect.kt +321 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ChromaKeyMath.kt +90 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ClipTransitionGeometry.kt +160 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ClipTransitionRenderer.kt +1002 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/CompositionBuilder.kt +166 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ConfigurableInAppMp4Muxer.kt +137 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/EncoderFailureClassifier.kt +71 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/GifDecoder.kt +76 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/LayeredCompositionBuilder.kt +556 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/MediaInfoExtractor.kt +364 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/ResilientVideoEncoderFactory.kt +307 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/VideoCompositionTransformation.kt +192 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/VideoEncoderConfig.kt +179 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/VideoGlobalTrimCalculator.kt +147 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/VideoReverser.kt +1044 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/VideoSequenceBuilder.kt +905 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/VideoTimelineDurationCalculator.kt +19 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/VideoTranscoder.kt +242 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/VolumeAudioProcessor.kt +82 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/helpers/VolumeControlAudioMixer.kt +182 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/models/CodecResourceExhaustedException.kt +24 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/models/RenderConfig.kt +572 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/models/RenderJobHandle.kt +45 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/models/VideoEncoderConfigurationException.kt +20 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/utils/RotatedVideoDimensions.kt +68 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/features/render/utils/VideoMimeUtils.kt +36 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/shared/concurrency/ExportGate.kt +102 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/shared/logging/PluginLog.kt +112 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/shared/media/EncodedImage.kt +101 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/shared/media/ImageOrientation.kt +265 -0
- package/android/src/main/java/expo/modules/provideoeditor/src/shared/media/PcmRangeDecoder.kt +239 -0
- package/build/ExpoProVideoEditor.types.d.ts +125 -0
- package/build/ExpoProVideoEditor.types.d.ts.map +1 -0
- package/build/ExpoProVideoEditor.types.js +8 -0
- package/build/ExpoProVideoEditor.types.js.map +1 -0
- package/build/ExpoProVideoEditorModule.d.ts +20 -0
- package/build/ExpoProVideoEditorModule.d.ts.map +1 -0
- package/build/ExpoProVideoEditorModule.js +3 -0
- package/build/ExpoProVideoEditorModule.js.map +1 -0
- package/build/ExpoProVideoEditorModule.web.d.ts +9 -0
- package/build/ExpoProVideoEditorModule.web.d.ts.map +1 -0
- package/build/ExpoProVideoEditorModule.web.js +16 -0
- package/build/ExpoProVideoEditorModule.web.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/bun.lock +2132 -0
- package/eslint.config.cjs +5 -0
- package/expo-module.config.json +9 -0
- package/ios/ExpoProVideoEditor.podspec +24 -0
- package/ios/ExpoProVideoEditorModule.swift +131 -0
- package/ios/src/features/render/RenderVideo.swift +1087 -0
- package/ios/src/features/render/helpers/ApplyAnimation.swift +198 -0
- package/ios/src/features/render/helpers/ApplyBitrate.swift +112 -0
- package/ios/src/features/render/helpers/ApplyBlur.swift +23 -0
- package/ios/src/features/render/helpers/ApplyChromaKey.swift +175 -0
- package/ios/src/features/render/helpers/ApplyColorMatrix.swift +100 -0
- package/ios/src/features/render/helpers/ApplyComposition.swift +45 -0
- package/ios/src/features/render/helpers/ApplyCrop.swift +53 -0
- package/ios/src/features/render/helpers/ApplyFlip.swift +24 -0
- package/ios/src/features/render/helpers/ApplyImageLayer.swift +65 -0
- package/ios/src/features/render/helpers/ApplyPlaybackSpeed.swift +57 -0
- package/ios/src/features/render/helpers/ApplyRotation.swift +32 -0
- package/ios/src/features/render/helpers/ApplyScale.swift +30 -0
- package/ios/src/features/render/helpers/AudioPreRenderer.swift +352 -0
- package/ios/src/features/render/helpers/AudioReverser.swift +308 -0
- package/ios/src/features/render/helpers/AudioSequenceBuilder.swift +162 -0
- package/ios/src/features/render/helpers/BitrateCapPolicy.swift +77 -0
- package/ios/src/features/render/helpers/BitrateCappedExporter.swift +501 -0
- package/ios/src/features/render/helpers/ClipTransitionGeometry.swift +111 -0
- package/ios/src/features/render/helpers/ClipTransitionRenderer.swift +397 -0
- package/ios/src/features/render/helpers/CompositionBuilder.swift +464 -0
- package/ios/src/features/render/helpers/DecodeOrientedImage.swift +60 -0
- package/ios/src/features/render/helpers/LayeredCompositionBuilder.swift +371 -0
- package/ios/src/features/render/helpers/MediaInfoExtractor.swift +381 -0
- package/ios/src/features/render/helpers/TrackEndTrimmer.swift +112 -0
- package/ios/src/features/render/helpers/VideoSequenceBuilder.swift +555 -0
- package/ios/src/features/render/helpers/VideoTranscoder.swift +283 -0
- package/ios/src/features/render/models/RenderConfig.swift +586 -0
- package/ios/src/features/render/models/VideoClip.swift +81 -0
- package/ios/src/features/render/models/VideoCompositorConfig.swift +78 -0
- package/ios/src/features/render/utils/VideoCompositor.swift +1014 -0
- package/ios/src/features/render/utils/VideoMimeUtils.swift +24 -0
- package/ios/src/shared/EncodedImage.swift +75 -0
- package/ios/src/shared/ExportGate.swift +79 -0
- package/ios/src/shared/ExportSessionDriver.swift +185 -0
- package/ios/src/shared/ExportSessionGuard.swift +85 -0
- package/ios/src/shared/ExportWatchdog.swift +198 -0
- package/ios/src/shared/LoggingConstants.swift +7 -0
- package/ios/src/shared/PluginLog.swift +107 -0
- package/package.json +59 -0
- package/src/ExpoProVideoEditor.types.ts +148 -0
- package/src/ExpoProVideoEditorModule.ts +22 -0
- package/src/ExpoProVideoEditorModule.web.ts +22 -0
- package/src/index.ts +4 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import CoreImage
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
/// Utility for pre-transcoding HEVC 10-bit HDR videos to H.264 8-bit SDR.
|
|
6
|
+
///
|
|
7
|
+
/// This is necessary because GPU-based effect pipelines have compatibility
|
|
8
|
+
/// issues with HEVC Main 10 Profile videos. By transcoding to H.264 first,
|
|
9
|
+
/// we can then safely apply effects like ColorMatrix, Blur, or Overlay.
|
|
10
|
+
internal class VideoTranscoder {
|
|
11
|
+
|
|
12
|
+
// MARK: - Result Types
|
|
13
|
+
|
|
14
|
+
/// Result of a transcoding operation.
|
|
15
|
+
enum TranscodeResult {
|
|
16
|
+
/// Transcoding succeeded, contains path to transcoded file
|
|
17
|
+
case success(outputPath: String)
|
|
18
|
+
|
|
19
|
+
/// No transcoding needed, original file is compatible
|
|
20
|
+
case notNeeded(originalPath: String)
|
|
21
|
+
|
|
22
|
+
/// Transcoding failed with error
|
|
23
|
+
case error(Error)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// MARK: - Public Methods
|
|
27
|
+
|
|
28
|
+
/// Checks if a video needs transcoding for effect compatibility.
|
|
29
|
+
///
|
|
30
|
+
/// - Parameter videoPath: Path to the video file
|
|
31
|
+
/// - Returns: True if transcoding is needed
|
|
32
|
+
static func needsTranscoding(_ videoPath: String) async -> Bool {
|
|
33
|
+
let formatInfo = await MediaInfoExtractor.getVideoFormatInfo(videoPath)
|
|
34
|
+
let needsTranscode = formatInfo.needsTranscodingForEffects()
|
|
35
|
+
|
|
36
|
+
PluginLog.print(
|
|
37
|
+
"🔍 Video transcoding check: path=\(videoPath), "
|
|
38
|
+
+ "isHevc=\(formatInfo.isHevc), bitDepth=\(formatInfo.bitDepth), "
|
|
39
|
+
+ "isHdr=\(formatInfo.isHdr), needsTranscoding=\(needsTranscode)")
|
|
40
|
+
|
|
41
|
+
return needsTranscode
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Transcodes a video to H.264 8-bit SDR format for effect compatibility.
|
|
45
|
+
///
|
|
46
|
+
/// Uses HDR → SDR tonemapping to convert 10-bit HDR to 8-bit SDR,
|
|
47
|
+
/// which allows proper GPU effect processing.
|
|
48
|
+
///
|
|
49
|
+
/// - Parameter videoPath: Path to the input video
|
|
50
|
+
/// - Returns: TranscodeResult indicating success, not-needed, or error
|
|
51
|
+
static func transcodeToH264(_ videoPath: String) async -> TranscodeResult {
|
|
52
|
+
// Check if transcoding is needed
|
|
53
|
+
guard await needsTranscoding(videoPath) else {
|
|
54
|
+
PluginLog.print("✅ No transcoding needed for: \(videoPath)")
|
|
55
|
+
return .notNeeded(originalPath: videoPath)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
PluginLog.print(
|
|
59
|
+
"🎬 Starting HEVC 10-bit HDR → H.264 8-bit SDR transcoding for: \(videoPath)")
|
|
60
|
+
|
|
61
|
+
let inputURL = URL(fileURLWithPath: videoPath)
|
|
62
|
+
let outputURL = FileManager.default.temporaryDirectory
|
|
63
|
+
.appendingPathComponent("transcoded_\(Int(Date().timeIntervalSince1970 * 1000)).mp4")
|
|
64
|
+
|
|
65
|
+
do {
|
|
66
|
+
try await transcodeVideo(from: inputURL, to: outputURL)
|
|
67
|
+
|
|
68
|
+
// Verify output
|
|
69
|
+
let outputInfo = await MediaInfoExtractor.getVideoFormatInfo(outputURL.path)
|
|
70
|
+
PluginLog.print("✅ Transcoding completed: \(outputURL.path)")
|
|
71
|
+
PluginLog.print(
|
|
72
|
+
" Output: isHevc=\(outputInfo.isHevc), bitDepth=\(outputInfo.bitDepth), isHdr=\(outputInfo.isHdr)"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return .success(outputPath: outputURL.path)
|
|
76
|
+
|
|
77
|
+
} catch {
|
|
78
|
+
PluginLog.print("❌ Transcoding failed: \(error.localizedDescription)")
|
|
79
|
+
try? FileManager.default.removeItem(at: outputURL)
|
|
80
|
+
return .error(error)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Transcodes multiple video clips if needed.
|
|
85
|
+
///
|
|
86
|
+
/// A clip that cannot be transcoded keeps its original path: the render then
|
|
87
|
+
/// runs on the HDR source, which is worse than a transcode but better than no
|
|
88
|
+
/// render at all. A *cancelled* job is the one failure that must not degrade
|
|
89
|
+
/// that way — it would build the whole composition on exactly the source the
|
|
90
|
+
/// pre-transcode exists to avoid, only to be thrown away — so it throws
|
|
91
|
+
/// instead, after removing the files already written.
|
|
92
|
+
///
|
|
93
|
+
/// - Parameter inputPaths: List of input video paths
|
|
94
|
+
/// - Returns: Dictionary mapping original path to transcoded path (or original if no transcoding needed)
|
|
95
|
+
/// - Throws: `CancellationError` if the job was cancelled mid-transcode.
|
|
96
|
+
static func transcodeClipsIfNeeded(_ inputPaths: [String]) async throws -> [String: String] {
|
|
97
|
+
var result: [String: String] = [:]
|
|
98
|
+
var produced: [String] = []
|
|
99
|
+
|
|
100
|
+
for inputPath in inputPaths {
|
|
101
|
+
switch await transcodeToH264(inputPath) {
|
|
102
|
+
case .success(let outputPath):
|
|
103
|
+
result[inputPath] = outputPath
|
|
104
|
+
produced.append(outputPath)
|
|
105
|
+
case .notNeeded(let originalPath):
|
|
106
|
+
result[inputPath] = originalPath
|
|
107
|
+
case .error(let error):
|
|
108
|
+
if error is CancellationError || Task.isCancelled {
|
|
109
|
+
cleanupTranscodedFiles(produced)
|
|
110
|
+
throw CancellationError()
|
|
111
|
+
}
|
|
112
|
+
PluginLog.print("⚠️ Transcoding failed for \(inputPath), using original")
|
|
113
|
+
result[inputPath] = inputPath
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Cleans up transcoded temporary files.
|
|
121
|
+
///
|
|
122
|
+
/// - Parameter transcodedPaths: Collection of transcoded file paths to delete
|
|
123
|
+
static func cleanupTranscodedFiles(_ transcodedPaths: [String]) {
|
|
124
|
+
for path in transcodedPaths {
|
|
125
|
+
if path.contains("transcoded_") {
|
|
126
|
+
do {
|
|
127
|
+
try FileManager.default.removeItem(atPath: path)
|
|
128
|
+
PluginLog.print("🗑️ Cleaned up transcoded file: \(path)")
|
|
129
|
+
} catch {
|
|
130
|
+
PluginLog.print("⚠️ Failed to clean up \(path): \(error.localizedDescription)")
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// MARK: - Private Methods
|
|
137
|
+
|
|
138
|
+
/// Performs the actual video transcoding using AVAssetExportSession.
|
|
139
|
+
/// This is simpler and more reliable than AVAssetWriter for basic transcoding.
|
|
140
|
+
private static func transcodeVideo(from inputURL: URL, to outputURL: URL) async throws {
|
|
141
|
+
let asset = AVURLAsset(
|
|
142
|
+
url: inputURL,
|
|
143
|
+
options: [
|
|
144
|
+
AVURLAssetPreferPreciseDurationAndTimingKey: true
|
|
145
|
+
])
|
|
146
|
+
|
|
147
|
+
guard
|
|
148
|
+
let exportSession = AVAssetExportSession(
|
|
149
|
+
asset: asset, presetName: AVAssetExportPresetHighestQuality)
|
|
150
|
+
else {
|
|
151
|
+
throw NSError(
|
|
152
|
+
domain: "VideoTranscoder", code: 1,
|
|
153
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to create export session"])
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
exportSession.shouldOptimizeForNetworkUse = true
|
|
157
|
+
|
|
158
|
+
// Create video composition for HDR → SDR conversion
|
|
159
|
+
let videoTrack: AVAssetTrack
|
|
160
|
+
|
|
161
|
+
if #available(iOS 15.0, macOS 13.0, *) {
|
|
162
|
+
let videoTracks = try await asset.loadTracks(withMediaType: .video)
|
|
163
|
+
guard let vTrack = videoTracks.first else {
|
|
164
|
+
throw NSError(
|
|
165
|
+
domain: "VideoTranscoder", code: 2,
|
|
166
|
+
userInfo: [NSLocalizedDescriptionKey: "No video track found"]
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
videoTrack = vTrack
|
|
170
|
+
} else {
|
|
171
|
+
guard let vTrack = asset.tracks(withMediaType: .video).first else {
|
|
172
|
+
throw NSError(
|
|
173
|
+
domain: "VideoTranscoder", code: 2,
|
|
174
|
+
userInfo: [NSLocalizedDescriptionKey: "No video track found"]
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
videoTrack = vTrack
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Get video properties
|
|
181
|
+
let naturalSize: CGSize
|
|
182
|
+
let preferredTransform: CGAffineTransform
|
|
183
|
+
let nominalFrameRate: Float
|
|
184
|
+
|
|
185
|
+
if #available(iOS 15.0, macOS 13.0, *) {
|
|
186
|
+
naturalSize = try await videoTrack.load(.naturalSize)
|
|
187
|
+
preferredTransform = try await videoTrack.load(.preferredTransform)
|
|
188
|
+
nominalFrameRate = try await videoTrack.load(.nominalFrameRate)
|
|
189
|
+
} else {
|
|
190
|
+
naturalSize = videoTrack.naturalSize
|
|
191
|
+
preferredTransform = videoTrack.preferredTransform
|
|
192
|
+
nominalFrameRate = videoTrack.nominalFrameRate
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Calculate render size accounting for rotation
|
|
196
|
+
let renderSize = calculateOutputSize(
|
|
197
|
+
naturalSize: naturalSize, transform: preferredTransform)
|
|
198
|
+
|
|
199
|
+
let processImage: (CIImage) -> CIImage = { sourceImage in
|
|
200
|
+
var image = sourceImage.clampedToExtent()
|
|
201
|
+
|
|
202
|
+
if let colorMatrix = CIFilter(name: "CIColorMatrix") {
|
|
203
|
+
colorMatrix.setValue(image, forKey: kCIInputImageKey)
|
|
204
|
+
colorMatrix.setValue(CIVector(x: 1, y: 0, z: 0, w: 0), forKey: "inputRVector")
|
|
205
|
+
colorMatrix.setValue(CIVector(x: 0, y: 1, z: 0, w: 0), forKey: "inputGVector")
|
|
206
|
+
colorMatrix.setValue(CIVector(x: 0, y: 1, z: 0, w: 0), forKey: "inputBVector")
|
|
207
|
+
colorMatrix.setValue(CIVector(x: 0, y: 0, z: 0, w: 1), forKey: "inputAVector")
|
|
208
|
+
|
|
209
|
+
if let output = colorMatrix.outputImage {
|
|
210
|
+
image = output
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return image.cropped(to: CGRect(origin: .zero, size: renderSize))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if #available(iOS 26.0, macOS 26.0, *) {
|
|
218
|
+
let videoComposition = try await AVVideoComposition(applyingFiltersTo: asset) {
|
|
219
|
+
params in
|
|
220
|
+
let cropped = processImage(params.sourceImage)
|
|
221
|
+
return AVCIImageFilteringResult(resultImage: cropped, ciContext: nil)
|
|
222
|
+
}
|
|
223
|
+
exportSession.videoComposition = videoComposition
|
|
224
|
+
|
|
225
|
+
} else {
|
|
226
|
+
// Fallback block for older OS versions
|
|
227
|
+
let videoComposition = AVMutableVideoComposition(asset: asset) { request in
|
|
228
|
+
let cropped = processImage(request.sourceImage)
|
|
229
|
+
request.finish(with: cropped, context: nil)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
videoComposition.renderSize = renderSize
|
|
233
|
+
videoComposition.frameDuration = CMTime(
|
|
234
|
+
value: 1, timescale: CMTimeScale(nominalFrameRate))
|
|
235
|
+
|
|
236
|
+
#if os(iOS)
|
|
237
|
+
if #available(iOS 14.0, *) {
|
|
238
|
+
videoComposition.colorPrimaries = AVVideoColorPrimaries_ITU_R_709_2
|
|
239
|
+
videoComposition.colorTransferFunction = AVVideoTransferFunction_ITU_R_709_2
|
|
240
|
+
videoComposition.colorYCbCrMatrix = AVVideoYCbCrMatrix_ITU_R_709_2
|
|
241
|
+
}
|
|
242
|
+
#else // macOS branch
|
|
243
|
+
videoComposition.colorPrimaries = AVVideoColorPrimaries_ITU_R_709_2
|
|
244
|
+
videoComposition.colorTransferFunction = AVVideoTransferFunction_ITU_R_709_2
|
|
245
|
+
videoComposition.colorYCbCrMatrix = AVVideoYCbCrMatrix_ITU_R_709_2
|
|
246
|
+
#endif
|
|
247
|
+
|
|
248
|
+
exportSession.videoComposition = videoComposition
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
PluginLog.print("🎬 Transcoding with AVAssetExportSession...")
|
|
252
|
+
PluginLog.print(" Input size: \(naturalSize), Output size: \(renderSize)")
|
|
253
|
+
|
|
254
|
+
try await ExportSessionDriver.run(
|
|
255
|
+
exportSession, to: outputURL, as: .mp4, label: "Transcode",
|
|
256
|
+
failureDomain: "VideoTranscoder")
|
|
257
|
+
|
|
258
|
+
PluginLog.print("✅ Transcoding completed successfully")
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/// Calculates output size accounting for rotation.
|
|
262
|
+
private static func calculateOutputSize(naturalSize: CGSize, transform: CGAffineTransform)
|
|
263
|
+
-> CGSize
|
|
264
|
+
{
|
|
265
|
+
let rotationAngle = atan2(transform.b, transform.a)
|
|
266
|
+
let radians = abs(rotationAngle)
|
|
267
|
+
|
|
268
|
+
// Check if rotation is ~90° or ~270°
|
|
269
|
+
if radians > .pi / 4 && radians < 3 * .pi / 4 {
|
|
270
|
+
return CGSize(width: naturalSize.height, height: naturalSize.width)
|
|
271
|
+
}
|
|
272
|
+
return naturalSize
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// Calculates appropriate bitrate based on resolution and frame rate (Mac optimization legacy).
|
|
276
|
+
private static func calculateBitrate(size: CGSize, frameRate: Float) -> Int {
|
|
277
|
+
let pixels = Int(size.width * size.height)
|
|
278
|
+
let fps = max(24, min(60, Int(frameRate)))
|
|
279
|
+
|
|
280
|
+
// Roughly 0.1 bits per pixel per frame for H.264 High profile
|
|
281
|
+
return max(2_000_000, pixels * fps / 10)
|
|
282
|
+
}
|
|
283
|
+
}
|