Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ jobs:
- name: Build for multiple platforms
run: make build-all

- name: Clean up cross-platform artifacts
run: |
if [ "$RUNNER_OS" == "macOS" ]; then
rm -f dist/sim-linux-amd64 dist/sim-windows-amd64.exe
else
rm -f dist/sim-darwin-amd64 dist/sim-darwin-arm64
fi
shell: bash

- name: Create checksums (Linux only)
if: matrix.os == 'ubuntu-latest'
run: |
Expand Down
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,17 @@ recording_*.mp4
# Temporary files
/tmp/coverage.out
coverage.html

# MiniSimCam
MiniSimCam/.build/
MiniSimCam/Example/CameraPreviewApp/build/
MiniSimCam/Example/CameraPreviewApp/module_cache/
MiniSimCam/module_cache/
module_cache/
cmd/assets/

# Xcode user state files (never commit)
**/*.xcuserstate

# User Scratch
scratch/
10 changes: 10 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ linters:
- lll
- nestif
path: cmd/
- linters:
- err113
- nlreturn
- gocritic
- tagliatelle
- gocyclo
- testpackage
- cyclop
path: cmd/cam.*\.go
- linters:
- gci
path: cmd/device.go
Expand All @@ -121,6 +130,7 @@ linters:
- revive
- gosec
- unparam
- staticcheck
path: tests/
paths:
- third_party$
Expand Down
30 changes: 28 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: build clean install test test-race test-coverage lint fmt vet check help
.PHONY: build build-all clean install test test-race test-coverage lint fmt vet check help cam-build cam-clean

# Default target
all: build
Expand All @@ -7,24 +7,48 @@ all: build
VERSION := $(shell grep 'version:' config.yaml | awk '{print $$2}' | tr -d '"')
LDFLAGS := -ldflags "-X github.com/annurdien/sim-cli/cmd.Version=$(VERSION)"

# Build the application
UNAME_S := $(shell uname -s)

# Build the application (with embedded cam binaries on macOS)
build:
ifeq ($(UNAME_S),Darwin)
cd MiniSimCam && ./Scripts/build.sh
go build -tags cam_embed $(LDFLAGS) -o sim
else
go build $(LDFLAGS) -o sim
endif

# Build for multiple platforms
build-all:
mkdir -p dist
ifeq ($(UNAME_S),Darwin)
cd MiniSimCam && ./Scripts/build.sh
GOOS=darwin GOARCH=amd64 go build -tags cam_embed $(LDFLAGS) -o dist/sim-darwin-amd64
GOOS=darwin GOARCH=arm64 go build -tags cam_embed $(LDFLAGS) -o dist/sim-darwin-arm64
else
GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o dist/sim-darwin-amd64
GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o dist/sim-darwin-arm64
endif
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o dist/sim-linux-amd64
GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o dist/sim-windows-amd64.exe

# Clean build artifacts
clean:
rm -f sim
rm -rf dist/
rm -rf cmd/assets/
rm -f coverage.out coverage.html

# Build MiniSimCam (FrameHost + MiniCamInject dylib)
cam-build:
cd MiniSimCam && ./Scripts/build.sh

# Clean MiniSimCam build artifacts
cam-clean:
cd MiniSimCam && swift package clean
rm -rf MiniSimCam/.build/injector
rm -rf cmd/assets/

# Install dependencies
deps:
go mod download
Expand Down Expand Up @@ -79,4 +103,6 @@ help:
@echo " vet - Run go vet"
@echo " check - Run all checks (fmt, vet, lint, test-race)"
@echo " install - Install to /usr/local/bin"
@echo " cam-build - Build MiniSimCam (FrameHost + injector dylib)"
@echo " cam-clean - Clean MiniSimCam build artifacts"
@echo " help - Show this help"

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>FrameHost.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>3</integer>
</dict>
<key>MiniCamInject.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>1</integer>
</dict>
<key>MiniSimCam-Package.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
<key>MiniSimCamShared.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>FrameHost</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>FrameTransportTests</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>MiniCamInject</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>MiniSimCamShared</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>ProtocolTests</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
</plist>
146 changes: 146 additions & 0 deletions MiniSimCam/Example/CameraPreviewApp/CameraPreviewApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// CameraPreviewApp.swift
// Minimal SwiftUI example app demonstrating both Stage A (cooperative) and
// Stage B (transparent injection) modes of MiniSimCam.
//
// Stage A: Compile with MINISIMCAM_STAGE_A=1 — uses SharedMemoryCameraSource
// Stage B: Use normal AVFoundation; the injector dylib intercepts it.

import SwiftUI
import AVFoundation
import CoreMedia

@main
struct CameraPreviewApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

// MARK: - Content View

struct ContentView: View {
@StateObject private var camera = CameraController()

var body: some View {
ZStack {
Color.black.ignoresSafeArea()

CameraPreviewView(session: camera.session)
.ignoresSafeArea()

VStack {
Spacer()
HStack {
Image(systemName: "camera.fill")
.foregroundStyle(.white)
Text("MiniSimCam Preview")
.foregroundStyle(.white)
.font(.footnote.bold())
Spacer()
Text("Frame \(camera.frameCount)")
.foregroundStyle(.white.opacity(0.6))
.font(.footnote.monospaced())
}
.padding()
.background(.ultraThinMaterial)
}
}
.onAppear { camera.start() }
.onDisappear { camera.stop() }
}
}

// MARK: - Camera Preview Layer

class PreviewView: UIView {
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
guard let layer = layer as? AVCaptureVideoPreviewLayer else {
fatalError("Expected `AVCaptureVideoPreviewLayer` type for layer. Check PreviewView.layerClass implementation.")
}
return layer
}

var session: AVCaptureSession? {
get { videoPreviewLayer.session }
set { videoPreviewLayer.session = newValue }
}

override class var layerClass: AnyClass {
return AVCaptureVideoPreviewLayer.self
}
}

struct CameraPreviewView: UIViewRepresentable {
let session: AVCaptureSession

func makeUIView(context: Context) -> PreviewView {
let view = PreviewView()
view.backgroundColor = .clear
view.videoPreviewLayer.videoGravity = .resizeAspectFill
view.session = session
return view
}

func updateUIView(_ uiView: PreviewView, context: Context) {
uiView.session = session
}
}

// MARK: - Camera Controller (Stage B: transparent AVFoundation)

@MainActor
final class CameraController: NSObject, ObservableObject {
@Published var frameCount: Int = 0
let session = AVCaptureSession()

private let output = AVCaptureVideoDataOutput()
private let queue = DispatchQueue(label: "com.example.camera.frames")

func start() {
session.sessionPreset = .hd1280x720

if let device = AVCaptureDevice.default(for: .video),
let input = try? AVCaptureDeviceInput(device: device),
session.canAddInput(input) {
session.addInput(input)
}

output.videoSettings = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA
]
output.setSampleBufferDelegate(self, queue: queue)
if session.canAddOutput(output) {
session.addOutput(output)
}

session.startRunning()
}

func stop() {
session.stopRunning()
}
}

extension CameraController: AVCaptureVideoDataOutputSampleBufferDelegate {
nonisolated func captureOutput(
_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
// We only use this delegate to increment the frame counter.
// The actual rendering is handled by AVCaptureVideoPreviewLayer!
Task { @MainActor in
self.frameCount += 1
}
}

nonisolated func captureOutput(
_ output: AVCaptureOutput,
didDrop sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection
) {
// Dropped frames are acceptable in the MVP.
}
}
40 changes: 40 additions & 0 deletions MiniSimCam/Example/CameraPreviewApp/build_app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")"

APP_NAME="CameraPreviewApp"
BUNDLE_ID="com.example.camerapreview"
TARGET_DIR="build/${APP_NAME}.app"

mkdir -p "$TARGET_DIR"

echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\">
<dict>
<key>CFBundleExecutable</key>
<string>${APP_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${BUNDLE_ID}</string>
<key>CFBundleName</key>
<string>${APP_NAME}</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>NSCameraUsageDescription</key>
<string>Camera test</string>
</dict>
</plist>" > "$TARGET_DIR/Info.plist"

xcrun swiftc \
-target arm64-apple-ios17.0-simulator \
-sdk $(xcrun --show-sdk-path --sdk iphonesimulator) \
-parse-as-library \
CameraPreviewApp.swift \
-o "$TARGET_DIR/$APP_NAME"

xcrun simctl install booted "$TARGET_DIR"
echo "Installed $BUNDLE_ID"
8 changes: 8 additions & 0 deletions MiniSimCam/Example/CameraPreviewApp/entitlements.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.get-task-allow</key>
<true/>
</dict>
</plist>
14 changes: 14 additions & 0 deletions MiniSimCam/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading