-
Notifications
You must be signed in to change notification settings - Fork 42
Open
Description
import AVFoundation
import MultipeerConnectivity
import Photos
import SwiftUI
// MARK: - 1. THE STREAM ENGINE (Multipeer & Camera)
class UniversalStreamManager: NSObject, ObservableObject, MCSessionDelegate, MCNearbyServiceAdvertiserDelegate, MCNearbyServiceBrowserDelegate, AVCaptureVideoDataOutputSampleBufferDelegate {
@Published var connectedPeer: MCPeerID?
@Published var receivedImage: UIImage?
@Published var isStreaming = false
// Multipeer Properties
private let peerID = MCPeerID(displayName: UIDevice.current.name)
private let serviceType = "v-stream"
private var session: MCSession!
private var advertiser: MCNearbyServiceAdvertiser!
private var browser: MCNearbyServiceBrowser!
// Camera Properties
private let captureSession = AVCaptureSession()
private let videoDataOutput = AVCaptureVideoDataOutput()
private let context = CIContext()
override init() {
super.init()
session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .required)
session.delegate = self
advertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
advertiser.delegate = self
browser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType)
browser.delegate = self
}
// Role Selection
func startAsCamera() {
setupCamera()
advertiser.startAdvertisingPeer()
isStreaming = true
}
func startAsMonitor() {
browser.startBrowsingForPeers()
isStreaming = true
}
// MARK: - Camera Capture Logic (iPhone 11+)
private func setupCamera() {
captureSession.beginConfiguration()
defer { captureSession.commitConfiguration() }
guard let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back),
let videoInput = try? AVCaptureDeviceInput(device: videoDevice)
else {
print("Failed to set up the video device.")
return
}
if captureSession.canAddInput(videoInput) {
captureSession.addInput(videoInput)
}
videoDataOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "videoQueue"))
if captureSession.canAddOutput(videoDataOutput) {
captureSession.addOutput(videoDataOutput)
}
DispatchQueue.global(qos: .userInitiated).async {
self.captureSession.startRunning()
}
}
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
let ciImage = CIImage(cvImageBuffer: imageBuffer)
// Optimize: Downsample and compress for zero-latency peer-to-peer
if let cgImage = context.createCGImage(ciImage, from: ciImage.extent) {
let uiImage = UIImage(cgImage: cgImage)
if let data = uiImage.jpegData(compressionQuality: 0.2) {
sendData(data)
}
}
}
private func sendData(_ data: Data) {
if !session.connectedPeers.isEmpty {
try? session.send(data, toPeers: session.connectedPeers, with: .unreliable)
}
}
// MARK: - Receiver Logic (iPad 2020+)
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
DispatchQueue.main.async {
self.receivedImage = UIImage(data: data)
}
}
// MARK: - Multipeer Delegates (Required)
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
invitationHandler(true, session)
}
func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String: String]?) {
browser.invitePeer(peerID, to: session, withContext: nil, timeout: 10)
}
func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
DispatchQueue.main.async {
self.connectedPeer = (state == .connected) ? peerID : nil
}
}
// Unused MCSession Delegates
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {}
func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, progress: Progress) {}
func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {}
func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {}
}
// MARK: - 2. THE USER INTERFACE
struct ContentView: View {
@StateObject var manager = UniversalStreamManager()
@State private var isCameraMode = false
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
if !manager.isStreaming {
VStack(spacing: 30) {
Text("P2P Video Vault")
.font(.system(size: 32, weight: .bold, design: .rounded))
.foregroundColor(.white)
Button(action: {
isCameraMode = true
manager.startAsCamera()
}) {
Label("iPhone: Start Camera", systemImage: "camera.fill")
.frame(maxWidth: .infinity).padding().background(Color.blue).cornerRadius(15)
}
Button(action: {
isCameraMode = false
manager.startAsMonitor()
}) {
Label("iPad: Start Monitor", systemImage: "desktopcomputer")
.frame(maxWidth: .infinity).padding().background(Color.green).cornerRadius(15)
}
}
.foregroundColor(.white)
.padding()
} else {
if isCameraMode {
VStack {
Text("Broadcasting to iPad...")
.foregroundColor(.red)
.font(.headline)
ProgressView().tint(.white)
}
} else {
MonitorView(manager: manager)
}
}
}
}
}
struct MonitorView: View {
@ObservedObject var manager: UniversalStreamManager
var body: some View {
VStack {
if let image = manager.receivedImage {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(20)
Button(action: { saveImage(image) }) {
Label("Capture to Photos", systemImage: "arrow.down.circle.fill")
.padding().background(Color.white).foregroundColor(.black).cornerRadius(12)
}
.padding()
} else {
Text("Waiting for iPhone Feed...")
.foregroundColor(.gray)
}
}
}
func saveImage(_ image: UIImage) {
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAsset(from: image)
})
}
}
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels