Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions DualCameraDemo/DualCameraDemo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
4F4460DE2EB94A7C00685CD7 /* Zoomable in Frameworks */ = {isa = PBXBuildFile; productRef = 4F4460DD2EB94A7C00685CD7 /* Zoomable */; };
4F7E4C7F2D6D2B0A006F5609 /* DualCameraKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4F7E4C7E2D6D2B0A006F5609 /* DualCameraKit */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -70,6 +71,7 @@
buildActionMask = 2147483647;
files = (
4F7E4C7F2D6D2B0A006F5609 /* DualCameraKit in Frameworks */,
4F4460DE2EB94A7C00685CD7 /* Zoomable in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -132,6 +134,7 @@
name = DualCameraDemo;
packageProductDependencies = (
4F7E4C7E2D6D2B0A006F5609 /* DualCameraKit */,
4F4460DD2EB94A7C00685CD7 /* Zoomable */,
);
productName = DualCameraDemo;
productReference = 4F4D659B2D66D02C00F40490 /* DualCameraDemo.app */;
Expand Down Expand Up @@ -217,6 +220,7 @@
minimizedProjectReferenceProxies = 1;
packageReferences = (
4F7E4C7D2D6D2B0A006F5609 /* XCLocalSwiftPackageReference "../../DualCameraKit" */,
4F4460DC2EB94A7C00685CD7 /* XCRemoteSwiftPackageReference "Zoomable" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = 4F4D659C2D66D02C00F40490 /* Products */;
Expand Down Expand Up @@ -620,7 +624,23 @@
};
/* End XCLocalSwiftPackageReference section */

/* Begin XCRemoteSwiftPackageReference section */
4F4460DC2EB94A7C00685CD7 /* XCRemoteSwiftPackageReference "Zoomable" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/ryohey/Zoomable";
requirement = {
branch = main;
kind = branch;
};
};
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
4F4460DD2EB94A7C00685CD7 /* Zoomable */ = {
isa = XCSwiftPackageProductDependency;
package = 4F4460DC2EB94A7C00685CD7 /* XCRemoteSwiftPackageReference "Zoomable" */;
productName = Zoomable;
};
4F7E4C7E2D6D2B0A006F5609 /* DualCameraKit */ = {
isa = XCSwiftPackageProductDependency;
productName = DualCameraKit;
Expand Down
5 changes: 5 additions & 0 deletions DualCameraDemo/DualCameraDemo/CapturePreviewOverlay.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import SwiftUI
import Zoomable

struct CapturePreviewOverlay: View {
let image: UIImage
let onDismiss: () -> Void
let onConfirm: () -> Void
@GestureState private var magnifyBy = 1.0

var body: some View {
ZStack {
Expand All @@ -16,6 +18,7 @@ struct CapturePreviewOverlay: View {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 24))
.foregroundColor(.white)

}

Spacer()
Expand All @@ -38,11 +41,13 @@ struct CapturePreviewOverlay: View {
.resizable()
.scaledToFit()
.cornerRadius(12)
.zoomable()
.padding()

Spacer()
}
.padding()
}
}

}
2 changes: 1 addition & 1 deletion DualCameraDemo/DualCameraDemo/ContainerExample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,25 @@
case showing(UIImage)
}
var previewPhase = PreviewPhase.hidden

Check warning on line 21 in DualCameraDemo/DualCameraDemo/ContainerExample.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
func showPreview(_ image: UIImage) {
withAnimation {
previewPhase = .showing(image)
}
}

Check warning on line 27 in DualCameraDemo/DualCameraDemo/ContainerExample.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
func reset() {
withAnimation {
previewPhase = .hidden
}

Check warning on line 32 in DualCameraDemo/DualCameraDemo/ContainerExample.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
}
}

private struct AppTabView: View {
@State private var selectedTab: Tab = .camera
@State private var captureReviewState = CaptureReviewState()
private var vm: DualCameraViewModel

Check warning on line 39 in DualCameraDemo/DualCameraDemo/ContainerExample.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Identifier Name Violation: Variable name 'vm' should be between 3 and 40 characters long (identifier_name)

init() {
vm = DualCameraViewModel(
Expand All @@ -45,7 +45,7 @@
saveToLibrary: false
)
}

Check warning on line 48 in DualCameraDemo/DualCameraDemo/ContainerExample.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
var body: some View {
ZStack {
VStack {
Expand All @@ -60,7 +60,7 @@
tabBar
.edgesIgnoringSafeArea(.all)
}

Check warning on line 63 in DualCameraDemo/DualCameraDemo/ContainerExample.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
switch captureReviewState.previewPhase {
case .hidden:
EmptyView()
Expand All @@ -77,14 +77,14 @@
.transition(.scale.combined(with: .opacity))
}
}
.background(.primary)
.background(.black)
.onChange(of: vm.capturedPhoto) { oldValue, newValue in
if let image = newValue {
captureReviewState.showPreview(image)
}
}
}

Check warning on line 87 in DualCameraDemo/DualCameraDemo/ContainerExample.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
private var cameraCapture: some View {
DualCameraScreen(
viewModel: vm
Expand Down Expand Up @@ -125,7 +125,7 @@
private func tabBarButton(tab: Tab, image: String, isCenter: Bool = false) -> some View {
Button(action: {
selectedTab = tab
}) {

Check warning on line 128 in DualCameraDemo/DualCameraDemo/ContainerExample.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Multiple Closures with Trailing Closure Violation: Trailing closure syntax should not be used when passing more than one closure argument (multiple_closures_with_trailing_closure)
if isCenter {
ZStack {
Circle()
Expand Down
42 changes: 31 additions & 11 deletions Sources/DualCameraKit/CameraRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import UIKit
public protocol CameraRenderer: AnyObject {
/// Update renderer with new camera frame.
func update(with buffer: CVPixelBuffer)

/// Capture current frame as UIImage.
func captureCurrentFrame() async throws -> UIImage

/// Capture current frame as raw pixel buffer (for high-quality composition).
func captureCurrentBuffer() async throws -> CVPixelBuffer
}

enum MetalRendererError: Error {
Expand All @@ -32,6 +35,10 @@ public final class MetalCameraRenderer: MTKView, CameraRenderer, MTKViewDelegate
private var textureCache: CVMetalTextureCache?
private var renderPipelineState: MTLRenderPipelineState?
private var currentTexture: MTLTexture?

// MARK: - Buffer Storage
/// Stores the most recent pixel buffer for high-quality capture
private var currentBuffer: CVPixelBuffer?

// MARK: - Initialization
public required init(coder: NSCoder) {
Expand Down Expand Up @@ -69,15 +76,15 @@ public final class MetalCameraRenderer: MTKView, CameraRenderer, MTKViewDelegate
/// Initializes Metal components and sets up the render pipeline.
private func initializeMetal() throws {
guard let device = self.device else {
DualCameraLogger.errors.error("❌ Metal not supported on this device")
DualCameraLogger.log("❌ Metal not supported on this device", category: .errors, level: .error)
throw MetalRendererError.metalNotSupported
}

commandQueue = device.makeCommandQueue()

let status = CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, device, nil, &textureCache)
if status != kCVReturnSuccess {
DualCameraLogger.errors.error("❌ Failed to create Metal texture cache")
DualCameraLogger.log("❌ Failed to create Metal texture cache", category: .errors, level: .error)
throw MetalRendererError.textureCreationFailed
}

Expand All @@ -99,7 +106,7 @@ public final class MetalCameraRenderer: MTKView, CameraRenderer, MTKViewDelegate
/// Sets up the Metal render pipeline.
private func setupRenderPipeline() throws {
guard let device = device else {
DualCameraLogger.errors.error("❌ No metal device found")
DualCameraLogger.log("❌ No metal device found", category: .errors, level: .error)
throw MetalRendererError.metalLibraryLoadFailed
}

Expand All @@ -109,13 +116,13 @@ public final class MetalCameraRenderer: MTKView, CameraRenderer, MTKViewDelegate
do {
library = try device.makeDefaultLibrary(bundle: spmBundle)
} catch {
DualCameraLogger.errors.error("❌ Failed to load Metal library: \(error.localizedDescription)")
DualCameraLogger.log("❌ Failed to load Metal library: \(error.localizedDescription)", category: .errors, level: .error)
throw MetalRendererError.metalLibraryLoadFailed
}

guard let vertexFunction = library.makeFunction(name: MetalLibFunctionName.vertexShader),
let fragmentFunction = library.makeFunction(name: MetalLibFunctionName.fragmentShader) else {
DualCameraLogger.errors.error("❌ Metal functions not found in library")
DualCameraLogger.log("❌ Metal functions not found in library", category: .errors, level: .error)
throw MetalRendererError.metalFunctionNotFound
}

Expand All @@ -126,9 +133,10 @@ public final class MetalCameraRenderer: MTKView, CameraRenderer, MTKViewDelegate

do {
renderPipelineState = try device.makeRenderPipelineState(descriptor: pipelineDescriptor)
DualCameraLogger.session.info("✅ Metal render pipeline initialized successfully")
DualCameraLogger.log("✅ Metal render pipeline initialized successfully", category: .session)

} catch {
DualCameraLogger.errors.error("❌ Failed to create render pipeline: \(error.localizedDescription)")
DualCameraLogger.log("❌ Failed to create render pipeline: \(error.localizedDescription)", category: .errors, level: .error)
throw MetalRendererError.renderPipelineCreationFailed(error)
}
}
Expand Down Expand Up @@ -160,6 +168,9 @@ public final class MetalCameraRenderer: MTKView, CameraRenderer, MTKViewDelegate
extension MetalCameraRenderer {

public func update(with buffer: CVPixelBuffer) {
// Store the buffer for high-quality capture
self.currentBuffer = buffer

let bufferWrapper = PixelBufferWrapper(buffer: buffer)
createAndUpdateTexture(from: bufferWrapper)
}
Expand Down Expand Up @@ -247,11 +258,20 @@ extension MetalCameraRenderer {
free(rawData)

// Create and return the UIImage
// TODO: should this scale be dynamic?
// TODO: should this scale be dynamic?
return UIImage(cgImage: cgImage, scale: 1.0, orientation: .up)
}



/// Captures the current frame as a raw pixel buffer at native camera resolution.
/// This is preferred over captureCurrentFrame() for high-quality photo/video composition.
public func captureCurrentBuffer() async throws -> CVPixelBuffer {
guard let buffer = self.currentBuffer else {
throw DualCameraError.captureFailure(.noFrameAvailable)
}
return buffer
}


// MARK: - Private Helpers

/// Creates a texture from the given buffer and updates the view.
Expand Down
5 changes: 3 additions & 2 deletions Sources/DualCameraKit/DualCameraCameraStreamSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -252,10 +252,11 @@ public final class DualCameraMockCameraStreamSource: DualCameraCameraStreamSourc
public init() { }

public func startSession() async throws {
let purpleBuffer: CVPixelBuffer = UIColor.purple.asImage().pixelBuffer()!
let mockSize = CGSize(width: 1080, height: 1920)
let purpleBuffer: CVPixelBuffer = UIColor.purple.asImage(mockSize).pixelBuffer()!
let purpleBufferWrapper = PixelBufferWrapper(buffer: purpleBuffer)

let yellowBuffer = UIColor.yellow.asImage().pixelBuffer()!
let yellowBuffer = UIColor.yellow.asImage(mockSize).pixelBuffer()!
let yellowBufferWrapper = PixelBufferWrapper(buffer: yellowBuffer)
await frontBroadcaster.broadcast(yellowBufferWrapper)
await backBroadcaster.broadcast(purpleBufferWrapper)
Expand Down
Loading
Loading