スキル一覧に戻る
co-labs-co

multipeerconnectivity-p2p-streaming

by co-labs-co

Central skills repository for ContextHarness - curated agent skills and workflows

0🍴 0📅 2026年1月12日
GitHubで見るManusで実行

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

ComponentRoleDescription
MCPeerIDIdentityUnique device identifier (MUST be persisted for reconnection)
MCSessionConnectionManages peer connections and data transfer
MCNearbyServiceAdvertiserDiscoveryAdvertises availability to nearby peers
MCNearbyServiceBrowserDiscoverySearches 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
)
PreferenceUse Case
.requiredSensitive data (video, personal info) - Recommended
.optionalBalanced (encrypts if peer supports)
.noneMaximum 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


Derived from CarSeet project - P2PStreamingManager.swift

スコア

総合スコア

60/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

レビュー

💬

レビュー機能は近日公開予定です