diff --git a/example/src/views/CaptureButton.tsx b/example/src/views/CaptureButton.tsx index 2476d8bdcb..716d4bae8e 100644 --- a/example/src/views/CaptureButton.tsx +++ b/example/src/views/CaptureButton.tsx @@ -92,6 +92,14 @@ const _CaptureButton: React.FC = ({ console.log('calling startRecording()...') camera.current.startRecording({ flash: flash, + width: 1024, // Output width after crop + height: 1024, // Output height after crop + crop: { + left: 0.1, // Start crop at 10% from left + top: 0.1, // Start crop at 10% from top + width: 0.9, // Crop to 90% width + height: 0.9 // Crop to 90% height + }, onRecordingError: (error) => { console.error('Recording failed!', error) onStoppedRecording() diff --git a/package/ios/Core/CameraSession+Video.swift b/package/ios/Core/CameraSession+Video.swift index 8e57710f24..6aba107327 100644 --- a/package/ios/Core/CameraSession+Video.swift +++ b/package/ios/Core/CameraSession+Video.swift @@ -91,13 +91,24 @@ extension CameraSession { // Orientation is relative to our current output orientation let orientation = self.outputOrientation.relativeTo(orientation: videoOutput.orientation) + // Create crop rect if needed + var cropRect: CGRect? + if let crop = options.crop { + cropRect = CGRect(x: crop.x, + y: crop.y, + width: crop.width, + height: crop.height) + VisionLogger.log(level: .info, message: "Creating recording with crop rect: \(cropRect!)") + } + // Create RecordingSession for the temp file let recordingSession = try RecordingSession(url: options.path, - fileType: options.fileType, - metadataProvider: self.metadataProvider, - clock: self.captureSession.clock, - orientation: orientation, - completion: onFinish) + fileType: options.fileType, + metadataProvider: self.metadataProvider, + clock: self.captureSession.clock, + orientation: orientation, + cropRect: cropRect, + completion: onFinish) // Init Audio + Activate Audio Session (optional) if enableAudio, diff --git a/package/ios/Core/Extensions/AVCaptureVideoDataOutput+recommendedVideoSettings.swift b/package/ios/Core/Extensions/AVCaptureVideoDataOutput+recommendedVideoSettings.swift index f985f2c737..c809a07af3 100644 --- a/package/ios/Core/Extensions/AVCaptureVideoDataOutput+recommendedVideoSettings.swift +++ b/package/ios/Core/Extensions/AVCaptureVideoDataOutput+recommendedVideoSettings.swift @@ -39,6 +39,13 @@ extension AVCaptureVideoDataOutput { guard var settings else { throw CameraError.capture(.createRecorderError(message: "Failed to get video settings!")) } + + // Apply custom width/height if provided + if let width = options.width, let height = options.height { + VisionLogger.log(level: .info, message: "Setting custom video dimensions: \(width)x\(height)") + settings[AVVideoWidthKey] = NSNumber(value: width) + settings[AVVideoHeightKey] = NSNumber(value: height) + } if let bitRateOverride = options.bitRateOverride { // Convert from Mbps -> bps diff --git a/package/ios/Core/RecordingSession.swift b/package/ios/Core/RecordingSession.swift index adee189013..af874a3203 100644 --- a/package/ios/Core/RecordingSession.swift +++ b/package/ios/Core/RecordingSession.swift @@ -26,6 +26,10 @@ final class RecordingSession { private var audioTrack: Track? private let completionHandler: (RecordingSession, AVAssetWriter.Status, Error?) -> Void private var isFinishing = false + + // Crop properties + private var cropRect: CGRect? + private var inputSize: CGSize? private let lock = DispatchSemaphore(value: 1) @@ -69,7 +73,10 @@ final class RecordingSession { metadataProvider: MetadataProvider, clock: CMClock, orientation: Orientation, + cropRect: CGRect? = nil, completion: @escaping (RecordingSession, AVAssetWriter.Status, Error?) -> Void) throws { + + self.cropRect = cropRect completionHandler = completion self.clock = clock videoOrientation = orientation @@ -107,9 +114,49 @@ final class RecordingSession { guard assetWriter.canApply(outputSettings: settings, forMediaType: .video) else { throw CameraError.capture(.createRecorderError(message: "The given output settings are not supported!")) } + + // Store the input size for later use in crop calculations + if let width = settings[AVVideoWidthKey] as? NSNumber, + let height = settings[AVVideoHeightKey] as? NSNumber { + inputSize = CGSize(width: width.doubleValue, height: height.doubleValue) + } VisionLogger.log(level: .info, message: "Initializing Video AssetWriter with settings: \(settings.description)") - let videoWriter = AVAssetWriterInput(mediaType: .video, outputSettings: settings) + + // Create video writer input with initial settings + var videoWriter: AVAssetWriterInput + + if let cropRect = cropRect, let inputSize = inputSize { + // Calculate the actual crop rectangle based on input size + let renderSize = inputSize + let cropRect = CGRect(x: cropRect.origin.x * renderSize.width, + y: cropRect.origin.y * renderSize.height, + width: cropRect.width * renderSize.width, + height: cropRect.height * renderSize.height) + + // Create a transform to apply the crop + let transform = CGAffineTransform( + translationX: -cropRect.origin.x, + y: -cropRect.origin.y + ) + + // Create a new settings dictionary with the cropped size + var croppedSettings = settings + croppedSettings[AVVideoWidthKey] = NSNumber(value: Int(cropRect.width)) + croppedSettings[AVVideoHeightKey] = NSNumber(value: Int(cropRect.height)) + + // Create the video writer with cropped settings + videoWriter = AVAssetWriterInput(mediaType: .video, outputSettings: croppedSettings) + + // Apply the transform (combine with orientation transform) + videoWriter.transform = transform.concatenating(videoOrientation.affineTransform) + + VisionLogger.log(level: .info, message: "Applied video crop: \(cropRect)") + } else { + // No crop, use original settings + videoWriter = AVAssetWriterInput(mediaType: .video, outputSettings: settings) + } + videoWriter.expectsMediaDataInRealTime = true videoWriter.transform = videoOrientation.affineTransform assetWriter.add(videoWriter) diff --git a/package/ios/Core/Types/RecordVideoOptions.swift b/package/ios/Core/Types/RecordVideoOptions.swift index d12e05eb79..f707c67c1c 100644 --- a/package/ios/Core/Types/RecordVideoOptions.swift +++ b/package/ios/Core/Types/RecordVideoOptions.swift @@ -23,6 +23,18 @@ struct RecordVideoOptions { * or set via bitRate, in Megabits per second (Mbps) */ var bitRateMultiplier: Double? + var width: Int? + var height: Int? + + struct CropRect { + var x: CGFloat + var y: CGFloat + var width: CGFloat + var height: CGFloat + + static let `default` = CropRect(x: 0, y: 0, width: 1, height: 1) + } + var crop: CropRect? init(fromJSValue dictionary: NSDictionary, bitRateOverride: Double? = nil, bitRateMultiplier: Double? = nil) throws { // File Type (.mov or .mp4) @@ -41,6 +53,28 @@ struct RecordVideoOptions { self.bitRateOverride = bitRateOverride // BitRate Multiplier self.bitRateMultiplier = bitRateMultiplier + // Width + if let width = dictionary["width"] as? NSNumber { + self.width = width.intValue + } + // Height + if let height = dictionary["height"] as? NSNumber { + self.height = height.intValue + } + // Parse crop region if provided + if let cropDict = dictionary["crop"] as? [String: Any] { + let x = cropDict["left"] as? NSNumber ?? 0 + let y = cropDict["top"] as? NSNumber ?? 0 + let width = cropDict["width"] as? NSNumber ?? 1 + let height = cropDict["height"] as? NSNumber ?? 1 + + self.crop = CropRect( + x: CGFloat(truncating: x), + y: CGFloat(truncating: y), + width: CGFloat(truncating: width), + height: CGFloat(truncating: height) + ) + } // Custom Path let fileExtension = fileType.descriptor ?? "mov" if let customPath = dictionary["path"] as? String { diff --git a/package/src/types/VideoFile.ts b/package/src/types/VideoFile.ts index 6391df76fa..7e196ce8b0 100644 --- a/package/src/types/VideoFile.ts +++ b/package/src/types/VideoFile.ts @@ -10,6 +10,43 @@ export interface RecordVideoOptions { * Specifies the output file type to record videos into. */ fileType?: 'mov' | 'mp4' + /** + * The width of the video in pixels. + * If not specified, the native sensor resolution is used. + */ + width?: number + /** + * The height of the video in pixels. + * If not specified, the native sensor resolution is used. + */ + height?: number + /** + * The crop region of the video. + * All values are in the range 0 to 1, relative to the video dimensions. + * @default { left: 0, top: 0, width: 1, height: 1 } + */ + crop?: { + /** + * The x-coordinate of the top-left corner of the crop region (0-1) + * @default 0 + */ + left?: number + /** + * The y-coordinate of the top-left corner of the crop region (0-1) + * @default 0 + */ + top?: number + /** + * The width of the crop region (0-1) + * @default 1 + */ + width?: number + /** + * The height of the crop region (0-1) + * @default 1 + */ + height?: number + } /** * A custom `path` where the video will be saved to. *