Как оживить плавный переход камеры между широкими и ультра широкими объективами, такими как приложение для нативного камIOS

Программируем под IOS
Ответить
Anonymous
 Как оживить плавный переход камеры между широкими и ультра широкими объективами, такими как приложение для нативного кам

Сообщение Anonymous »

Я работаю над индивидуальной реализацией камеры для iOS, используя AvcaptureSession и AvcapturedEvice. Я знаю, как справиться с масштабированием и масштабировать, и как переключаться между камерами (широко, ультра широко и т. Д.). Тем не менее, я хочу достичь плавного анимированного эффекта перехода между широкими и ультра широкими объективами, как и нативное приложение для камеры iPhone, когда вы переключаете линзы. Я хочу воспроизвести бесшовную анимацию Zoom и поведение переключения линз, которое можно увидеть в нативном приложении.//
// CameraManager.swift
// Capture Clip
//
// Created by Lucas Sesti on 20/12/24.
//

import UIKit
import SwiftUI
import AVKit
import Observation

/// Camera permissions
enum CameraPermission: String {
case granted = "Permission granted"
case idle = "Not decided"
case denied = "Permission denied"
}

enum CameraError: Error {
case unableToCapturePhoto(error: String)
case permissionDenied
}

@MainActor
@Observable
class Camera: NSObject, AVCaptureSessionControlsDelegate, @preconcurrency AVCapturePhotoCaptureDelegate {
/// Camera properties
private let queue: DispatchSerialQueue = .init(label: "br.com.lejour-capture.Capture.sessionQueue")

/// Camera output
private var photoContinuation: CheckedContinuation?

/// Camera presets
let presets: [AVCaptureSession.Preset] = [
.hd4K3840x2160,
.hd1920x1080,
.hd1280x720,
.vga640x480,
.cif352x288
]

let session: AVCaptureSession = .init()
var cameraPosition: AVCaptureDevice.Position = .back
let cameraOutput: AVCapturePhotoOutput = .init()
var videoGravity: AVLayerVideoGravity = .resizeAspectFill
var permission: CameraPermission = .idle
var zoomFactor: CGFloat = 1.0 {
didSet {
self.setZoom(to: zoomFactor)
}
}
var zoomLevel: Zoom = .oneX {
didSet {
self.handleZoomAction(progress: zoomLevel.rawValue)
}
}

override init() {
super.init()

checkCameraPermission()
}

/// Checking and asking for camera permission
private func checkCameraPermission() {
Task {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
permission = .granted
setupCamera()
case .notDetermined:
if await AVCaptureDevice.requestAccess(for: .video) {
permission = .granted
setupCamera()
}
case .denied, .restricted:
permission = .denied
@unknown default: break
}
}
}

/// Setting up camera
private func setupCamera() {
guard let device = AVCaptureDevice.DiscoverySession(
deviceTypes: [
// /// With 2 lens
// .builtInDualWideCamera,
// /// With 3 lens
// .builtInTripleCamera,
/// Fallback for all iPhone Models
.builtInWideAngleCamera,
],
mediaType: .video,
position: cameraPosition
).devices.first else {
session.commitConfiguration()
print("Couldn't find any background camera")
return
}

self.setCameraDevice(to: device)

startSession()
}

/// Set specific camera
func setCameraDevice(to device: AVCaptureDevice) {
guard permission == .granted else {
print("Permissão para uso da câmera não concedida.")
return
}

do {
try device.lockForConfiguration()

session.beginConfiguration()

session.inputs.forEach { input in
session.removeInput(input)
}

session.outputs.forEach { output in
session.removeOutput(output)
}

let input = try AVCaptureDeviceInput(device: device)

guard session.canAddInput(input), session.canAddOutput(cameraOutput) else {
session.commitConfiguration()
print("Cannot add camera output")
return
}

session.addInput(input)
session.addOutput(cameraOutput)
setupCameraControl(device)

for preset in presets {
if session.canSetSessionPreset(preset) {
session.sessionPreset = preset
print("Preset configurado para: \(preset)")
break
}
}

session.commitConfiguration()

device.unlockForConfiguration()
} catch {
print(error.localizedDescription)
}
}

func toggleCamera() {
cameraPosition = (cameraPosition == .back) ? .front : .back

guard let device = AVCaptureDevice.DiscoverySession(
deviceTypes: [
.builtInWideAngleCamera,
],
mediaType: .video,
position: cameraPosition
).devices.first else {
print("Couldn't find the \(cameraPosition == .back ? "back" : "front") camera")
return
}

setCameraDevice(to: device)

withAnimation {
self.zoomLevel = .oneX
}

print("Switched to \(cameraPosition == .back ? "back" : "front") camera")
}

/// Camera session
func startSession() {
guard !session.isRunning else { return }
/// Starting in background thread, not in the main thread
Task.detached(priority: .background) {
await self.session.startRunning()
}
}

func stopSession() {
guard session.isRunning else { return }

/// Stopping in background thread, not in the main thread
Task.detached(priority: .background) {
await self.session.stopRunning()
}
}

/// Setting up camera controls actions for iPhone 16+ models
private func setupCameraControl(_ device: AVCaptureDevice) {
if #available(iOS 18.0, *) {
guard session.supportsControls else { return }

session.setControlsDelegate(self, queue: queue)

for control in session.controls {
session.removeControl(control)
}

let zoomControl = AVCaptureSlider("Zoom", symbolName: "", in: 0.5...5, step: 0.5)
zoomControl.value = 1.0

zoomControl.setActionQueue(queue) { progress in
self.handleZoomAction(progress: CGFloat(progress))

if let closestZoom = Zoom.allCases.min(by: { abs($0.rawValue - CGFloat(progress)) < abs($1.rawValue - CGFloat(progress)) }) {
withAnimation {
self.zoomLevel = closestZoom
}
}
}

if session.canAddControl(zoomControl) {
session.addControl(zoomControl)
} else {
print("Couldn't add zoom control")
}

} else {
print("Not available")
}
}

/// Camera control protocols
nonisolated func sessionControlsDidBecomeActive(_ session: AVCaptureSession) {

}

nonisolated func sessionControlsWillEnterFullscreenAppearance(_ session: AVCaptureSession) {

}

nonisolated func sessionControlsWillExitFullscreenAppearance(_ session: AVCaptureSession) {

}

nonisolated func sessionControlsDidBecomeInactive(_ session: AVCaptureSession) {

}

/// Camera photo output
func capturePhoto() async throws -> Image {
guard permission == .granted else {
print("Permissão para uso da câmera não concedida.")
throw CameraError.permissionDenied
}

let photoSettings = AVCapturePhotoSettings()
photoSettings.flashMode = .off
photoSettings.photoQualityPrioritization = .balanced

return try await withCheckedThrowingContinuation { continuation in
self.photoContinuation = continuation
cameraOutput.capturePhoto(with: photoSettings, delegate: self)
}
}

func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
if let error = error {
photoContinuation?.resume(throwing: error)
return
}

guard let imageData = photo.fileDataRepresentation(),
let uiImage = UIImage(data: imageData) else {
photoContinuation?.resume(throwing: CameraError.unableToCapturePhoto(error: "Não foi possível processar a imagem capturada."))

return
}

var finalUIImage = uiImage

/// Mirroring the image if is in front camera
if cameraPosition == .front {
finalUIImage = mirrorImage(uiImage)
}

let swiftUIImage = Image(uiImage: finalUIImage)

photoContinuation?.resume(returning: swiftUIImage)
}

/// Mirror an image horizontally
private func mirrorImage(_ image: UIImage) -> UIImage {
guard let cgImage = image.cgImage else { return image }

let mirroredOrientation: UIImage.Orientation

switch image.imageOrientation {
case .up:
mirroredOrientation = .upMirrored
case .down:
mirroredOrientation = .downMirrored
case .left:
mirroredOrientation = .rightMirrored
case .right:
mirroredOrientation = .leftMirrored
default:
mirroredOrientation = .upMirrored
}

return UIImage(cgImage: cgImage, scale: image.scale, orientation: mirroredOrientation)
}

/// Camera zoom control
func setZoom(to zoomFactor: CGFloat) {
guard let activeDevice = (session.inputs.first as? AVCaptureDeviceInput)?.device else {
print("No active video input device found.")
return
}

let clampedZoomFactor = max(
activeDevice.minAvailableVideoZoomFactor,
min(
zoomFactor,
activeDevice.maxAvailableVideoZoomFactor
)
)

do {
try activeDevice.lockForConfiguration()

activeDevice.ramp(toVideoZoomFactor: clampedZoomFactor, withRate: 3.3)

activeDevice.unlockForConfiguration()
} catch {
print("Failed to set zoom: \(error.localizedDescription)")
}
}

func setZoomLevel(_ zoom: Zoom?) {
if zoom != nil {
self.zoomLevel = zoom!
} else {
self.zoomLevel = self.zoomLevel.next()
}
}

func handleZoomAction(progress: CGFloat) {
guard let activeDevice = (self.session.inputs.first as? AVCaptureDeviceInput)?.device else {
print("No active video input device found.")
return
}

if progress < 1.0 {
if activeDevice.deviceType == .builtInUltraWideCamera {
return
}

let ultraWideDevices = AVCaptureDevice.DiscoverySession(
deviceTypes: [
/// For iPhone 11+ models,
.builtInUltraWideCamera
],
mediaType: .video,
position: self.cameraPosition
)

guard let ultraWideDevice = ultraWideDevices.devices.first else {
print("Couldn't find any ultra wide camera")
return
}

self.setCameraDevice(to: ultraWideDevice)

return
} else {
if activeDevice.deviceType != .builtInWideAngleCamera {
let wideCamera = AVCaptureDevice.DiscoverySession(
deviceTypes: [
/// For all iPhone models
.builtInWideAngleCamera
],
mediaType: .video,
position: self.cameraPosition
)

guard let device = wideCamera.devices.first else {
print("Couldn't find any wide camera")
return
}

self.setCameraDevice(to: device)
}
}

self.zoomFactor = CGFloat(progress)
}
}
< /code>
Я попытался переключить входы avcapturedevice, когда фактор масштабирования пересекает определенные пороговые значения (например, менее 1x использует Ultra Wide, 1x и выше используется широко), но переход не анимирован плавно. /> Существует ли рекомендуемый подход или Appi Appi для использования, который обрабатывает этот вид бесшовного многокамерного ZooM? код!

Подробнее здесь: https://stackoverflow.com/questions/796 ... enses-like
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «IOS»