Skip to content
Open
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
4 changes: 2 additions & 2 deletions RemoteCam/CamStates.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ extension RemoteCamSession {
^{ [weak self] in
if let h = alertHandle { self?.alertPresenter.dismissAlert(h) }
}
if c.peer?.displayName == peer.displayName && self.connectedPeers.count == 0 {
if c.peer?.displayName == peer.displayName {
self.popAndStartScanning()
}

Expand Down Expand Up @@ -309,7 +309,7 @@ extension RemoteCamSession {
msg: RemoteCmd.SetAspectRatioResp(aspectRatio: result, error: nil))

case let c as DisconnectPeer:
if c.peer?.displayName == peer.displayName && self.connectedPeers.count == 0 {
if c.peer?.displayName == peer.displayName {
debugLog("🔍 DEBUG: Camera disconnecting peer - going to scanning")
self.popAndStartScanning()
}
Expand Down
90 changes: 90 additions & 0 deletions RemoteCam/CameraRegistry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// CameraRegistry.swift
// RemoteShutter
//
// Tracks connected camera peers for multi-camera support.
// Owned by RemoteCamSession — only accessed from the actor's serial mailbox.
//

import Foundation
import MultipeerConnectivity

enum CameraStatus {
case connected
case ready
case capturing
case disconnected
}

struct CameraState {
let peerId: MCPeerID
let displayName: String
var status: CameraStatus
}

class CameraRegistry {

static let maxCameras = 7

// MARK: - Connected cameras

private(set) var cameras: [MCPeerID: CameraState] = [:]
var selectedCamera: MCPeerID?

var allPeers: [MCPeerID] { Array(cameras.keys) }
var count: Int { cameras.count }
var isEmpty: Bool { cameras.isEmpty }
var isFull: Bool { cameras.count >= Self.maxCameras }

// MARK: - Discovered but not-yet-connected cameras

private(set) var availableCameras: [MCPeerID] = []

func addAvailable(peer: MCPeerID) {
guard !availableCameras.contains(peer), !contains(peer: peer) else { return }
availableCameras.append(peer)
}

func removeAvailable(peer: MCPeerID) {
availableCameras.removeAll { $0 == peer }
}

// MARK: - Connected cameras

@discardableResult
func add(peer: MCPeerID) -> Bool {
guard cameras[peer] == nil, !isFull else { return false }
cameras[peer] = CameraState(
peerId: peer,
displayName: peer.displayName,
status: .connected
)
removeAvailable(peer: peer)
if selectedCamera == nil {
selectedCamera = peer
}
return true
}

func remove(peer: MCPeerID?) {
guard let peer = peer else { return }
cameras.removeValue(forKey: peer)
if selectedCamera == peer {
selectedCamera = cameras.keys.first
}
}

func updateStatus(peer: MCPeerID, status: CameraStatus) {
cameras[peer]?.status = status
}

func contains(peer: MCPeerID) -> Bool {
cameras[peer] != nil
}

func reset() {
cameras.removeAll()
availableCameras.removeAll()
selectedCamera = nil
}
}
4 changes: 2 additions & 2 deletions RemoteCam/CameraVideoStates.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ extension RemoteCamSession {
)

case let c as DisconnectPeer:
if c.peer?.displayName == peer.displayName && self.connectedPeers.count == 0 {
if c.peer?.displayName == peer.displayName {
self.popAndStartScanning()
ctrl.stopRecordingVideo(false)
}
Expand Down Expand Up @@ -123,7 +123,7 @@ extension RemoteCamSession {
}

case let c as DisconnectPeer:
if c.peer?.displayName == peer.displayName && self.connectedPeers.count == 0 {
if c.peer?.displayName == peer.displayName {
// Progress UI handled by SwiftUI - no alert to dismiss
self.mailbox.addOperation {
self.popAndStartScanning()
Expand Down
12 changes: 6 additions & 6 deletions RemoteCam/FeatureFlags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ struct FeatureFlags {
/// Enable video/photo quality controls overlay on monitor
static let ENABLE_QUALITY_CONTROLS = true

// MARK: - Future Feature Flags
// Add new feature flags here as needed
// Example:
// static let ENABLE_AI_FILTERS = false
// static let ENABLE_CLOUD_SYNC = false
}
// MARK: - Multi-Camera

/// Enable multi-camera support (1 monitor : N cameras)
/// Set to true when multi-camera implementation is complete
static let ENABLE_MULTI_CAMERA = true
}
51 changes: 46 additions & 5 deletions RemoteCam/MonitorPhotoStates.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,24 @@ extension RemoteCamSession {
switch msg {
case is OnEnter:
monitor ! UICmd.RenderPhotoMode()
self.requestFrame([peer])
// Request frames from all connected cameras
for cameraPeer in self.cameraRegistry.allPeers {
self.requestFrame([cameraPeer])
}
if self.cameraRegistry.isEmpty {
self.requestFrame([peer])
}
// Re-enable browsing so additional cameras can be discovered
self.multipeerService?.startBrowsingOnly()
// Notify ViewModel about all cameras already in registry
for cam in self.cameraRegistry.cameras.values {
monitor ! UICmd.CameraConnected(peer: cam.peerId)
}

case is RemoteCmd.OnFrame:
case let frame as RemoteCmd.OnFrame:
monitor ! msg
self.requestFrame([peer])
// Request next frame from the camera that sent this one
self.requestFrame([frame.peerId])

case is UICmd.UnbecomeMonitor:
self.popToState(name: .connected)
Expand Down Expand Up @@ -190,11 +203,38 @@ extension RemoteCamSession {
discardOld: true)
}

// MARK: - Multi-Camera Connection Handling

case let w as OnConnectToDevice:
if self.cameraRegistry.add(peer: w.peer) {
monitor ! UICmd.CameraConnected(peer: w.peer)
// Start requesting frames from the new camera
self.requestFrame([w.peer])
}

case let w as ConnectToDevice:
self.multipeerService?.invitePeer(w.peer, timeout: 5)

case let cmd as UICmd.ConnectToCamera:
self.multipeerService?.invitePeer(cmd.peer, timeout: 5)

case let m as UICmd.BrowserFoundPeer:
self.cameraRegistry.addAvailable(peer: m.peer)
monitor ! UICmd.AvailableCameraFound(peer: m.peer)

case let m as UICmd.BrowserLostPeer:
self.cameraRegistry.removeAvailable(peer: m.peer)
monitor ! UICmd.AvailableCameraLost(peer: m.peer)

case is Disconnect:
self.popAndStartScanning()

case let c as DisconnectPeer:
if c.peer?.displayName == peer.displayName && self.connectedPeers.count == 0 {
if let peer = c.peer {
self.cameraRegistry.remove(peer: peer)
monitor ! UICmd.CameraDisconnected(peer: peer)
}
if self.cameraRegistry.isEmpty {
self.popAndStartScanning()
}

Expand Down Expand Up @@ -256,7 +296,8 @@ extension RemoteCamSession {
self.popToState(name: .connected)

case let c as DisconnectPeer:
if c.peer?.displayName == peer.displayName && self.connectedPeers.count == 0 {
self.cameraRegistry.remove(peer: c.peer)
if self.cameraRegistry.isEmpty {
^{ [weak self] in
if let h = alertHandle { self?.alertPresenter.dismissAlert(h) }
}
Expand Down
9 changes: 6 additions & 3 deletions RemoteCam/MonitorStates.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ extension RemoteCamSession {
^{ [weak self] in
if let h = alertHandle { self?.alertPresenter.dismissAlert(h) }
}
if c.peer?.displayName == peer.displayName && self.connectedPeers.count == 0 {
self.cameraRegistry.remove(peer: c.peer)
if self.cameraRegistry.isEmpty {
self.popAndStartScanning()
}

Expand Down Expand Up @@ -150,7 +151,8 @@ extension RemoteCamSession {
^{ [weak self] in
if let h = alertHandle { self?.alertPresenter.dismissAlert(h) }
}
if c.peer?.displayName == peer.displayName && self.connectedPeers.count == 0 {
self.cameraRegistry.remove(peer: c.peer)
if self.cameraRegistry.isEmpty {
self.popAndStartScanning()
}

Expand Down Expand Up @@ -230,7 +232,8 @@ extension RemoteCamSession {
^{ [weak self] in
if let h = alertHandle { self?.alertPresenter.dismissAlert(h) }
}
if c.peer?.displayName == peer.displayName && self.connectedPeers.count == 0 {
self.cameraRegistry.remove(peer: c.peer)
if self.cameraRegistry.isEmpty {
self.popAndStartScanning()
}

Expand Down
55 changes: 48 additions & 7 deletions RemoteCam/MonitorVideoStates.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,24 @@ extension MonitorVideoStates {
switch msg {
case is OnEnter:
monitor ! UICmd.RenderVideoMode()
self.requestFrame([peer])
// Request frames from all connected cameras
for cameraPeer in self.cameraRegistry.allPeers {
self.requestFrame([cameraPeer])
}
if self.cameraRegistry.isEmpty {
self.requestFrame([peer])
}
// Re-enable browsing so additional cameras can be discovered
self.multipeerService?.startBrowsingOnly()
// Notify ViewModel about all cameras already in registry
for cam in self.cameraRegistry.cameras.values {
monitor ! UICmd.CameraConnected(peer: cam.peerId)
}

case is RemoteCmd.OnFrame:
case let frame as RemoteCmd.OnFrame:
monitor ! msg
self.requestFrame([peer])
// Request next frame from the camera that sent this one
self.requestFrame([frame.peerId])

case is UICmd.UnbecomeMonitor:
self.popToState(name: .connected)
Expand Down Expand Up @@ -156,8 +169,35 @@ extension MonitorVideoStates {
debugLog("🔍 DEBUG: Monitor detected peer became camera - requesting fresh capabilities")
self.sendCommandOrGoToScanning(peer: [peer], msg: RemoteCmd.RequestCameraCapabilities())

// MARK: - Multi-Camera Connection Handling

case let w as OnConnectToDevice:
if self.cameraRegistry.add(peer: w.peer) {
monitor ! UICmd.CameraConnected(peer: w.peer)
// Start requesting frames from the new camera
self.requestFrame([w.peer])
}

case let w as ConnectToDevice:
self.multipeerService?.invitePeer(w.peer, timeout: 5)

case let cmd as UICmd.ConnectToCamera:
self.multipeerService?.invitePeer(cmd.peer, timeout: 5)

case let m as UICmd.BrowserFoundPeer:
self.cameraRegistry.addAvailable(peer: m.peer)
monitor ! UICmd.AvailableCameraFound(peer: m.peer)

case let m as UICmd.BrowserLostPeer:
self.cameraRegistry.removeAvailable(peer: m.peer)
monitor ! UICmd.AvailableCameraLost(peer: m.peer)

case let c as DisconnectPeer:
if c.peer?.displayName == peer.displayName && self.connectedPeers.count == 0 {
if let peer = c.peer {
self.cameraRegistry.remove(peer: peer)
monitor ! UICmd.CameraDisconnected(peer: peer)
}
if self.cameraRegistry.isEmpty {
self.popAndStartScanning()
}

Expand Down Expand Up @@ -250,7 +290,8 @@ extension MonitorVideoStates {
self.popAndStartScanning()

case let c as DisconnectPeer:
if c.peer?.displayName == peer.displayName && self.connectedPeers.count == 0 {
self.cameraRegistry.remove(peer: c.peer)
if self.cameraRegistry.isEmpty {
self.popAndStartScanning()
}

Expand Down Expand Up @@ -284,8 +325,8 @@ extension MonitorVideoStates {
self.popToState(name: .connected)

case let c as DisconnectPeer:
if c.peer?.displayName == peer.displayName && self.connectedPeers.count == 0 {
// Progress UI handled by SwiftUI - no alert to dismiss
self.cameraRegistry.remove(peer: c.peer)
if self.cameraRegistry.isEmpty {
self.popAndStartScanning()
}

Expand Down
Loading
Loading