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)
code
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:
code
┌─────────────────────────────────────────────────────┐
│ 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
- •TN3151: Choosing the right networking API
- •RFC 6763: DNS-Based Service Discovery
Derived from CarSeet project - P2PStreamingManager.swift