fix(camera): retain capture delegates
This commit is contained in:
@@ -36,6 +36,7 @@ actor CameraController {
|
|||||||
height: Int)
|
height: Int)
|
||||||
{
|
{
|
||||||
let facing = params.facing ?? .front
|
let facing = params.facing ?? .front
|
||||||
|
let format = params.format ?? .jpg
|
||||||
// Default to a reasonable max width to keep bridge payload sizes manageable.
|
// Default to a reasonable max width to keep bridge payload sizes manageable.
|
||||||
// If you need the full-res photo, explicitly request a larger maxWidth.
|
// If you need the full-res photo, explicitly request a larger maxWidth.
|
||||||
let maxWidth = params.maxWidth.flatMap { $0 > 0 ? $0 : nil } ?? 1600
|
let maxWidth = params.maxWidth.flatMap { $0 > 0 ? $0 : nil } ?? 1600
|
||||||
@@ -74,9 +75,13 @@ actor CameraController {
|
|||||||
}()
|
}()
|
||||||
settings.photoQualityPrioritization = .quality
|
settings.photoQualityPrioritization = .quality
|
||||||
|
|
||||||
|
var delegate: PhotoCaptureDelegate?
|
||||||
let rawData: Data = try await withCheckedThrowingContinuation { cont in
|
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(
|
let res = try JPEGTranscoder.transcodeToJPEG(
|
||||||
imageData: rawData,
|
imageData: rawData,
|
||||||
@@ -84,7 +89,7 @@ actor CameraController {
|
|||||||
quality: quality)
|
quality: quality)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
format: "jpg",
|
format: format.rawValue,
|
||||||
base64: res.data.base64EncodedString(),
|
base64: res.data.base64EncodedString(),
|
||||||
width: res.widthPx,
|
width: res.widthPx,
|
||||||
height: res.heightPx)
|
height: res.heightPx)
|
||||||
@@ -99,6 +104,7 @@ actor CameraController {
|
|||||||
let facing = params.facing ?? .front
|
let facing = params.facing ?? .front
|
||||||
let durationMs = Self.clampDurationMs(params.durationMs)
|
let durationMs = Self.clampDurationMs(params.durationMs)
|
||||||
let includeAudio = params.includeAudio ?? true
|
let includeAudio = params.includeAudio ?? true
|
||||||
|
let format = params.format ?? .mp4
|
||||||
|
|
||||||
try await self.ensureAccess(for: .video)
|
try await self.ensureAccess(for: .video)
|
||||||
if includeAudio {
|
if includeAudio {
|
||||||
@@ -149,16 +155,23 @@ actor CameraController {
|
|||||||
try? FileManager.default.removeItem(at: mp4URL)
|
try? FileManager.default.removeItem(at: mp4URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var delegate: MovieFileDelegate?
|
||||||
let recordedURL: URL = try await withCheckedThrowingContinuation { cont in
|
let recordedURL: URL = try await withCheckedThrowingContinuation { cont in
|
||||||
let delegate = MovieFileDelegate(cont)
|
let d = MovieFileDelegate(cont)
|
||||||
output.startRecording(to: movURL, recordingDelegate: delegate)
|
delegate = d
|
||||||
|
output.startRecording(to: movURL, recordingDelegate: d)
|
||||||
}
|
}
|
||||||
|
withExtendedLifetime(delegate) {}
|
||||||
|
|
||||||
// Transcode .mov -> .mp4 for easier downstream handling.
|
// Transcode .mov -> .mp4 for easier downstream handling.
|
||||||
try await Self.exportToMP4(inputURL: recordedURL, outputURL: mp4URL)
|
try await Self.exportToMP4(inputURL: recordedURL, outputURL: mp4URL)
|
||||||
|
|
||||||
let data = try Data(contentsOf: 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 {
|
private func ensureAccess(for mediaType: AVMediaType) async throws {
|
||||||
@@ -184,7 +197,11 @@ actor CameraController {
|
|||||||
|
|
||||||
private nonisolated static func pickCamera(facing: ClawdisCameraFacing) -> AVCaptureDevice? {
|
private nonisolated static func pickCamera(facing: ClawdisCameraFacing) -> AVCaptureDevice? {
|
||||||
let position: AVCaptureDevice.Position = (facing == .front) ? .front : .back
|
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 {
|
nonisolated static func clampQuality(_ quality: Double?) -> Double {
|
||||||
|
|||||||
@@ -70,9 +70,13 @@ actor CameraCaptureService {
|
|||||||
}()
|
}()
|
||||||
settings.photoQualityPrioritization = .quality
|
settings.photoQualityPrioritization = .quality
|
||||||
|
|
||||||
|
var delegate: PhotoCaptureDelegate?
|
||||||
let rawData: Data = try await withCheckedThrowingContinuation(isolation: nil) { cont in
|
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)
|
let res = try JPEGTranscoder.transcodeToJPEG(imageData: rawData, maxWidthPx: maxWidth, quality: quality)
|
||||||
return (data: res.data, size: CGSize(width: res.widthPx, height: res.heightPx))
|
return (data: res.data, size: CGSize(width: res.widthPx, height: res.heightPx))
|
||||||
@@ -141,9 +145,13 @@ actor CameraCaptureService {
|
|||||||
try? FileManager.default.removeItem(at: outputURL)
|
try? FileManager.default.removeItem(at: outputURL)
|
||||||
|
|
||||||
let logger = self.logger
|
let logger = self.logger
|
||||||
|
var delegate: MovieFileDelegate?
|
||||||
let recordedURL: URL = try await withCheckedThrowingContinuation(isolation: nil) { cont in
|
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)
|
try await Self.exportToMP4(inputURL: recordedURL, outputURL: outputURL)
|
||||||
return (path: outputURL.path, durationMs: durationMs, hasAudio: includeAudio)
|
return (path: outputURL.path, durationMs: durationMs, hasAudio: includeAudio)
|
||||||
@@ -217,9 +225,9 @@ actor CameraCaptureService {
|
|||||||
export.outputURL = outputURL
|
export.outputURL = outputURL
|
||||||
export.outputFileType = .mp4
|
export.outputFileType = .mp4
|
||||||
|
|
||||||
await withCheckedContinuation { cont in
|
try await withCheckedThrowingContinuation(isolation: nil) { (cont: CheckedContinuation<Void, Error>) in
|
||||||
export.exportAsynchronously {
|
export.exportAsynchronously {
|
||||||
cont.resume()
|
cont.resume(returning: ())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user