# Multipeerconnectivity P2p Streaming

> Peer-to-peer video streaming using Apple's MultipeerConnectivity framework. Use this skill when building local mesh networking apps, video streaming between devices (baby monitors, security cameras), local multiplayer games without servers, or AirDrop-like file sharing with custom protocols.

- Skill: `co-labs-co/multipeerconnectivity-p2p-streaming` (Agent Skill)
- Install (CLI): `npx skillmds@latest add co-labs-co/multipeerconnectivity-p2p-streaming`
- Raw SKILL.md: https://api.skillmd.com/api/skills/co-labs-co/multipeerconnectivity-p2p-streaming/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: co-labs-co (https://skillmd.com/u/co-labs-co)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/co-labs-co/multipeerconnectivity-p2p-streaming

---


# Skill: MultipeerConnectivity P2P Streaming

Peer-to-peer video streaming using Apple's MultipeerConnectivity framework with per-viewer session architecture, trust-based authentication, and reliable video frame delivery.

## When to Use

- **Local mesh networking**: Connecting 2-8 devices without internet/server infrastructure
- **Video streaming between devices**: Baby monitors, security cameras, remote displays
- **Game sessions**: Local multiplayer without server requirements
- **File/data sharing**: AirDrop-like functionality with custom protocols
- **Proximity-based features**: Nearby device discovery and communication

**Consider Network.framework instead when**:
- You have a clear client/server model (not true peer-to-peer)
- You need lower latency for real-time applications (per TN3151)
- You require fine-grained control over transport protocols

## Key Concepts

### Core Components

| Component | Role | Description |
|-----------|------|-------------|
| `MCPeerID` | Identity | Unique device identifier (MUST be persisted for reconnection) |
| `MCSession` | Connection | Manages peer connections and data transfer |
| `MCNearbyServiceAdvertiser` | Discovery | Advertises availability to nearby peers |
| `MCNearbyServiceBrowser` | Discovery | Searches for advertising peers |

### Bonjour Service Type Naming (RFC 6763)

```
Format: _<service>._<protocol>
Rules:
- Maximum 15 characters (excluding underscores)
- Lowercase letters, numbers, hyphens only
- Must not start or end with hyphen

Examples:
✅ "carseet-stream"   (valid: 14 chars)
✅ "myapp-share"      (valid: 11 chars)  
❌ "my-really-long-service-name" (invalid: > 15 chars)
```

### Per-Viewer Session Architecture

The recommended pattern uses **separate MCSession per connected peer** rather than a single shared session:

```
┌─────────────────────────────────────────────────────┐
│                   STREAMER (Camera)                  │
│                                                      │
│  ┌──────────────┐  ┌──────────────┐  ┌────────────┐ │
│  │ MCSession A  │  │ MCSession B  │  │ MCSession C│ │
│  │ (Viewer 1)   │  │ (Viewer 2)   │  │ (Viewer 3) │ │
│  └──────┬───────┘  └──────┬───────┘  └──────┬─────┘ │
└─────────┼─────────────────┼─────────────────┼───────┘
          │                 │                 │
          ▼                 ▼                 ▼
     ┌─────────┐       ┌─────────┐       ┌─────────┐
     │ VIEWER 1│       │ VIEWER 2│       │ VIEWER 3│
     └─────────┘       └─────────┘       └─────────┘
```

Benefits:
- **Selective disconnection**: Revoke one viewer without affecting others
- **Per-viewer state tracking**: Independent connection status per device
- **Granular access control**: Different trust levels per connection

### Encryption Options

```swift
MCSession(
    peer: peerID,
    securityIdentity: nil,
    encryptionPreference: .required // .none, .optional, .required
)
```

| Preference | Use Case |
|------------|----------|
| `.required` | Sensitive data (video, personal info) - **Recommended** |
| `.optional` | Balanced (encrypts if peer supports) |
| `.none` | Maximum performance (not recommended) |

## Implementation Guide

### Step 1: MCPeerID Persistence

MCPeerID instances with the same displayName are **NOT equal** unless archived/unarchived:

```swift
import KeychainAccess

class P2PStreamingManager {
    private let keychain = Keychain(service: Bundle.main.bundleIdentifier ?? "com.app")
        .accessibility(.whenUnlockedThisDeviceOnly)
    private let peerIDKey = "p2p_mcpeer_id"
    
    private func setupPeerID() {
        if let persistedPeerID = loadPersistedPeerID() {
            peerID = persistedPeerID
            return
        }
        
        let newPeerID = MCPeerID(displayName: UIDevice.current.name)
        peerID = newPeerID
        persistPeerID(newPeerID)
    }
    
    private func loadPersistedPeerID() -> MCPeerID? {
        guard let data = keychain[data: peerIDKey] else { return nil }
        return try? NSKeyedUnarchiver.unarchivedObject(
            ofClass: MCPeerID.self, 
            from: data
        )
    }
}
```

### Step 2: Streamer (Advertiser) Setup

```swift
class StreamerManager {
    private var peerID: MCPeerID!
    private var advertiser: MCNearbyServiceAdvertiser?
    private var viewerSessions: [String: MCSession] = []  // Per-viewer sessions
    
    private let serviceType = "carseet-stream"  // Max 15 chars
    
    func startAdvertising() {
        let discoveryInfo: [String: String] = [
            "role": "streamer",
            "deviceId": deviceId
        ]
        
        advertiser = MCNearbyServiceAdvertiser(
            peer: peerID,
            discoveryInfo: discoveryInfo,
            serviceType: serviceType
        )
        advertiser?.delegate = self
        advertiser?.startAdvertisingPeer()
    }
}

extension StreamerManager: MCNearbyServiceAdvertiserDelegate {
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser,
                    didReceiveInvitationFromPeer peerID: MCPeerID,
                    withContext context: Data?,
                    invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        
        // Per-viewer session architecture
        let viewerSession = MCSession(
            peer: self.peerID,
            securityIdentity: nil,
            encryptionPreference: .required
        )
        viewerSession.delegate = self
        
        // Store session for selective disconnect
        viewerSessions[credential.deviceId] = viewerSession
        
        invitationHandler(true, viewerSession)
    }
}
```

### Step 3: Reliable Video Frame Streaming

```swift
extension StreamerManager {
    func sendEncodedFrame(_ frame: EncodedVideoFrame) {
        let serializedData = frame.serialize()
        
        for (_, viewerSession) in viewerSessions {
            guard !viewerSession.connectedPeers.isEmpty else { continue }
            
            do {
                // Use .unreliable for video (lower latency, drops OK)
                // Use .reliable for control messages
                try viewerSession.send(
                    serializedData,
                    toPeers: viewerSession.connectedPeers,
                    with: .unreliable
                )
            } catch {
                print("Frame send error: \(error)")
            }
        }
    }
}
```

### Step 4: Background Handling (Critical)

MPC **does not function in background**:

```swift
class P2PManager {
    func setupAppLifecycleObservers() {
        NotificationCenter.default.addObserver(
            forName: UIApplication.didEnterBackgroundNotification,
            object: nil,
            queue: .main
        ) { [weak self] _ in
            // MUST disconnect - MPC fails in background
            for (_, session) in self?.viewerSessions ?? [:] {
                session.disconnect()
            }
            self?.viewerSessions.removeAll()
        }
    }
}
```

## Common Pitfalls

### 1. MCPeerID Equality Trap
```swift
// ❌ WRONG - These are NOT equal even with same name
let peer1 = MCPeerID(displayName: "Device")
let peer2 = MCPeerID(displayName: "Device")
peer1 == peer2  // FALSE!

// ✅ CORRECT - Archive and persist, then unarchive
let archived = try NSKeyedArchiver.archivedData(withRootObject: peer1, requiringSecureCoding: true)
let restored = try NSKeyedUnarchiver.unarchivedObject(ofClass: MCPeerID.self, from: archived)
peer1 == restored  // TRUE!
```

### 2. Service Type Length Violation
```swift
// ❌ WRONG - Too long (18 chars)
let serviceType = "my-awesome-service"  // Will fail silently

// ✅ CORRECT - Max 15 characters
let serviceType = "carseet-stream"  // 14 chars
```

### 3. Background Session Corruption
```swift
// ❌ WRONG - Trying to keep session alive in background
func applicationDidEnterBackground() {
    // MPC WILL cause undefined behavior
}

// ✅ CORRECT - Disconnect immediately on background
func applicationDidEnterBackground() {
    session.disconnect()
}
```

### 4. Shared Session Disconnect Side Effects
```swift
// ❌ WRONG - Shared session affects all peers
session.disconnect()  // Drops ALL connected viewers

// ✅ CORRECT - Per-viewer sessions for selective disconnect
func disconnectViewer(deviceId: String) {
    viewerSessions[deviceId]?.disconnect()
    viewerSessions.removeValue(forKey: deviceId)
}
```

## References

- [Apple MultipeerConnectivity Framework](https://developer.apple.com/documentation/multipeerconnectivity)
- [TN3151: Choosing the right networking API](https://developer.apple.com/documentation/technotes/tn3151-choosing-the-right-networking-api)
- [RFC 6763: DNS-Based Service Discovery](https://datatracker.ietf.org/doc/html/rfc6763)

---
_Derived from CarSeet project - P2PStreamingManager.swift_

