
multipeerconnectivity-p2p-streaming
by co-labs-co
Central skills repository for ContextHarness - curated agent skills and workflows
SKILL.md
name: multipeerconnectivity-p2p-streaming version: 0.1.0 author: cmtzco description: 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: 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
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:
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
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
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:
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
// ❌ 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
// ❌ 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
// ❌ 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
// ❌ 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
スコア
総合スコア
リポジトリの品質指標に基づく評価
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
レビュー
レビュー機能は近日公開予定です