Replace MultipeerConnectivity with a custom P2P implementation using Network.framework (NWListener/NWConnection) and CoreBluetooth for discovery. - Add P2PConnectionManager, BLEDiscoveryManager, and NetworkFraming. - Add ConnectedPeer and DiscoveredHost models. - Update Info.plist with local network and bluetooth permissions. - Refactor Views to use P2PConnectionManager. - Add implementation plan and transition docs.
7.1 KiB
BeamScribe: MPC to Network.framework Transition Plan
Overview
Transition from MultipeerConnectivity to Network.framework while preserving all existing functionality: live transcription broadcast, late-joiner history sync, alerts, and multi-guest support.
Phase 1: Create P2PConnectionManager (Network.framework Core)
1.1 Define the Protocol Interface
Create a protocol that mirrors what MultipeerManager currently provides to the UI:
protocol P2PConnectionDelegate: AnyObject {
func didDiscoverHost(_ host: DiscoveredHost)
func didLoseHost(_ host: DiscoveredHost)
func didConnectGuest(_ guest: ConnectedPeer)
func didDisconnectGuest(_ guest: ConnectedPeer)
func didReceiveData(_ data: Data, from peer: ConnectedPeer)
func connectionStateChanged(_ state: P2PConnectionState)
}
1.2 Host Side: NWListener
Replace MCNearbyServiceAdvertiser with NWListener:
// Key configuration
let parameters = NWParameters.tcp
parameters.includePeerToPeer = true // Enables AWDL
parameters.serviceClass = .responsiveData // Low-latency priority
// Advertise with service type
let listener = try NWListener(using: parameters)
listener.service = NWListener.Service(
name: eventName, // Your event name (currently in discoveryInfo)
type: "_beamscribe._tcp"
)
Connection Handling:
- Store accepted connections in
[UUID: NWConnection]dictionary - Implement same 4-second "traffic gate" stabilization logic
- Track unstable connections (disconnect within 5 seconds)
1.3 Guest Side: NWBrowser + NWConnection
Replace MCNearbyServiceBrowser with NWBrowser:
let parameters = NWParameters.tcp
parameters.includePeerToPeer = true
let browser = NWBrowser(for: .bonjour(type: "_beamscribe._tcp", domain: nil), using: parameters)
Discovery Results:
NWBrowser.Resultincludes the service name (your event name)- Create
NWConnectionto connect to discovered host - Implement retry logic (1s, 2s, 4s backoff - same as current)
Phase 2: BLE Fast-Discovery Layer (CoreBluetooth)
2.1 Why BLE?
Standard Bonjour discovery over AWDL can take 2-8 seconds due to channel hopping. BLE advertisement is near-instant and "wakes" the AWDL interface.
2.2 BLEDiscoveryManager - Host (Peripheral)
let serviceUUID = CBUUID(string: "YOUR-BEAMSCRIBE-UUID")
// Advertise when hosting
peripheralManager.startAdvertising([
CBAdvertisementDataServiceUUIDsKey: [serviceUUID],
CBAdvertisementDataLocalNameKey: eventName.prefix(8) // BLE has 28-byte limit
])
2.3 BLEDiscoveryManager - Guest (Central)
// Scan for BeamScribe hosts
centralManager.scanForPeripherals(withServices: [serviceUUID])
// On discovery, trigger Network.framework browser
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, ...) {
delegate?.bleDidDiscoverHost(name: peripheral.name)
// Start NWBrowser immediately - AWDL is now primed
}
2.4 BLE Lifecycle
- Start BLE advertising when
startHosting()called - Start BLE scanning when
startBrowsing()called - Stop BLE once TCP connection established (saves battery)
- Resume BLE if connection lost (for reconnection)
Phase 3: Data Transmission Layer
3.1 Framing Protocol
Network.framework uses raw TCP streams. Implement length-prefixed framing:
// Sending
func send(_ packet: TranscriptPacket, to connection: NWConnection) {
let data = try JSONEncoder().encode(packet)
var length = UInt32(data.count).bigEndian
let header = Data(bytes: &length, count: 4)
connection.send(content: header + data, completion: .contentProcessed { ... })
}
// Receiving
func receiveNextPacket(on connection: NWConnection) {
// Read 4-byte header first, then read that many bytes for payload
}
3.2 Broadcast to All Guests (Host)
Replace MCSession.send(_:toPeers:with:):
func broadcastPacket(_ packet: TranscriptPacket) {
let data = encode(packet)
for (_, connection) in stableConnections {
connection.send(content: data, completion: ...)
}
}
3.3 Packet Types (Keep Existing)
Your current TranscriptPacket model works unchanged:
.fullHistory- Late-joiner sync.liveChunk- Real-time transcription (partial/final).alert- Host disconnected, battery low, session resumed
Phase 4: Integration with Existing Code
4.1 File Changes Summary
| Current File | Changes |
|---|---|
MultipeerManager.swift |
Deprecate, keep as fallback initially |
P2PConnectionManager.swift |
NEW - Main networking logic |
BLEDiscoveryManager.swift |
NEW - CoreBluetooth layer |
NetworkFraming.swift |
NEW - TCP framing utilities |
SessionState.swift |
Update peer tracking types |
ContentView.swift |
Swap manager reference |
GuestBrowserView.swift |
Use new discovery delegate |
Info.plist |
Add BLE background modes |
4.2 Info.plist Additions
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
</array>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>BeamScribe uses Bluetooth to quickly discover nearby sessions.</string>
4.3 Fallback Strategy
Keep MultipeerManager available initially:
// In SessionState or AppConfig
var useNetworkFramework: Bool = true
var connectionManager: any P2PConnectionDelegate {
useNetworkFramework ? p2pManager : legacyMultipeerManager
}
Phase 5: Testing & Verification
5.1 Terminal Checks
# Verify AWDL activates during connection
ifconfig awdl0
# Check for active peer-to-peer interface
netstat -rn | grep awdl
5.2 Latency Testing
// Add to packet for round-trip measurement
struct TranscriptPacket {
// existing fields...
var sentTimestamp: TimeInterval?
}
5.3 Test Scenarios
- Host starts, 1 guest joins
- Host starts, 7 guests join simultaneously
- Guest joins late, receives full history
- Host disconnects, guests receive alert
- App backgrounded, BLE keeps discovery alive
- Same Wi-Fi network (should prefer infrastructure)
- Different Wi-Fi / no Wi-Fi (should use AWDL)
Implementation Order
- P2PConnectionManager - NWListener + NWBrowser (no BLE yet)
- NetworkFraming - Length-prefixed TCP framing
- Integration - Wire up to ContentView/GuestBrowserView
- Testing - Verify feature parity with MPC
- BLEDiscoveryManager - Add fast-discovery layer
- Optimization - Infrastructure vs AWDL preference logic
- Cleanup - Remove MultipeerManager fallback
Estimated New Files
BeamScribe/
├── Managers/
│ ├── MultipeerManager.swift (existing - deprecate later)
│ ├── P2PConnectionManager.swift (NEW - ~400 lines)
│ ├── BLEDiscoveryManager.swift (NEW - ~150 lines)
│ └── NetworkFraming.swift (NEW - ~80 lines)
├── Models/
│ ├── DiscoveredHost.swift (NEW - ~20 lines)
│ └── ConnectedPeer.swift (NEW - ~20 lines)
Rollback Plan
If issues arise:
git reset --hard backup-before-networkframework