Files
BeamScribe/IMPLEMENTATION_PLAN_NETWORK_FRAMEWORK.md
jared 0c1e3d6fff Refactor networking to use Network.framework
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.
2026-01-20 23:48:14 -05:00

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.Result includes the service name (your event name)
  • Create NWConnection to 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

  1. P2PConnectionManager - NWListener + NWBrowser (no BLE yet)
  2. NetworkFraming - Length-prefixed TCP framing
  3. Integration - Wire up to ContentView/GuestBrowserView
  4. Testing - Verify feature parity with MPC
  5. BLEDiscoveryManager - Add fast-discovery layer
  6. Optimization - Infrastructure vs AWDL preference logic
  7. 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