fix(camera): retain capture delegates

This commit is contained in:
Peter Steinberger
2025-12-14 04:30:21 +00:00
parent e0545e2f94
commit 845b26a73b
2 changed files with 35 additions and 10 deletions

View File

@@ -36,6 +36,7 @@ actor CameraController {
height: Int)
{
let facing = params.facing ?? .front
let format = params.format ?? .jpg
// Default to a reasonable max width to keep bridge payload sizes manageable.
// If you need the full-res photo, explicitly request a larger maxWidth.
let maxWidth = params.maxWidth.flatMap { $0 > 0 ? $0 : nil } ?? 1600
@@ -74,9 +75,13 @@ actor CameraController {
}()
settings.photoQualityPrioritization = .quality
var delegate: PhotoCaptureDelegate?
let rawData: Data = try await withCheckedThrowingContinuation { cont in
output.capturePhoto(with: settings, delegate: PhotoCaptureDelegate(cont))
let d = PhotoCaptureDelegate(cont)
delegate = d
output.capturePhoto(with: settings, delegate: d)
}
withExtendedLifetime(delegate) {}
let res = try JPEGTranscoder.transcodeToJPEG(
imageData: rawData,
@@ -84,7 +89,7 @@ actor CameraController {
quality: quality)
return (
format: "jpg",
format: format.rawValue,
base64: res.data.base64EncodedString(),
width: res.widthPx,
height: res.heightPx)
@@ -99,6 +104,7 @@ actor CameraController {
let facing = params.facing ?? .front
let durationMs = Self.clampDurationMs(params.durationMs)
let includeAudio = params.includeAudio ?? true
let format = params.format ?? .mp4
try await self.ensureAccess(for: .video)
if includeAudio {
@@ -149,16 +155,23 @@ actor CameraController {
try? FileManager.default.removeItem(at: mp4URL)
}
var delegate: MovieFileDelegate?
let recordedURL: URL = try await withCheckedThrowingContinuation { cont in
let delegate = MovieFileDelegate(cont)
output.startRecording(to: movURL, recordingDelegate: delegate)
let d = MovieFileDelegate(cont)
delegate = d
output.startRecording(to: movURL, recordingDelegate: d)
}
withExtendedLifetime(delegate) {}
// Transcode .mov -> .mp4 for easier downstream handling.
try await Self.exportToMP4(inputURL: recordedURL, outputURL: mp4URL)
let data = try Data(contentsOf: mp4URL)
return (format: "mp4", base64: data.base64EncodedString(), durationMs: durationMs, hasAudio: includeAudio)
return (
format: format.rawValue,
base64: data.base64EncodedString(),
durationMs: durationMs,
hasAudio: includeAudio)
}
private func ensureAccess(for mediaType: AVMediaType) async throws {
@@ -184,7 +197,11 @@ actor CameraController {
private nonisolated static func pickCamera(facing: ClawdisCameraFacing) -> AVCaptureDevice? {
let position: AVCaptureDevice.Position = (facing == .front) ? .front : .back
return AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: position)
if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: position) {
return device
}
// Fall back to any default camera (e.g. simulator / unusual device configurations).
return AVCaptureDevice.default(for: .video)
}
nonisolated static func clampQuality(_ quality: Double?) -> Double {

View File

@@ -70,9 +70,13 @@ actor CameraCaptureService {
}()
settings.photoQualityPrioritization = .quality
var delegate: PhotoCaptureDelegate?
let rawData: Data = try await withCheckedThrowingContinuation(isolation: nil) { cont in
output.capturePhoto(with: settings, delegate: PhotoCaptureDelegate(cont))
let d = PhotoCaptureDelegate(cont)
delegate = d
output.capturePhoto(with: settings, delegate: d)
}
withExtendedLifetime(delegate) {}
let res = try JPEGTranscoder.transcodeToJPEG(imageData: rawData, maxWidthPx: maxWidth, quality: quality)
return (data: res.data, size: CGSize(width: res.widthPx, height: res.heightPx))
@@ -141,9 +145,13 @@ actor CameraCaptureService {
try? FileManager.default.removeItem(at: outputURL)
let logger = self.logger
var delegate: MovieFileDelegate?
let recordedURL: URL = try await withCheckedThrowingContinuation(isolation: nil) { cont in
output.startRecording(to: tmpMovURL, recordingDelegate: MovieFileDelegate(cont, logger: logger))
let d = MovieFileDelegate(cont, logger: logger)
delegate = d
output.startRecording(to: tmpMovURL, recordingDelegate: d)
}
withExtendedLifetime(delegate) {}
try await Self.exportToMP4(inputURL: recordedURL, outputURL: outputURL)
return (path: outputURL.path, durationMs: durationMs, hasAudio: includeAudio)
@@ -217,9 +225,9 @@ actor CameraCaptureService {
export.outputURL = outputURL
export.outputFileType = .mp4
await withCheckedContinuation { cont in
try await withCheckedThrowingContinuation(isolation: nil) { (cont: CheckedContinuation<Void, Error>) in
export.exportAsynchronously {
cont.resume()
cont.resume(returning: ())
}
}