Files
run/RunPlus/PeerSession.swift
T

163 lines
6.0 KiB
Swift

import Foundation
import MultipeerConnectivity
import UIKit
final class PeerSession: NSObject, ObservableObject {
@Published private(set) var peers: [PeerInfo] = []
@Published private(set) var isConnected: Bool = false
@Published private(set) var statusText: String = "Idle"
var onMessage: ((SessionMessage) -> Void)?
var onReceiveResource: ((String, URL) -> Void)?
private let serviceType = "runplus-sync"
private let myPeerID = MCPeerID(displayName: UIDevice.current.name)
private var session: MCSession!
private var advertiser: MCNearbyServiceAdvertiser?
private var browser: MCNearbyServiceBrowser?
// Host keeps per-peer offsets
private var peerOffsets: [MCPeerID: TimeInterval] = [:]
override init() {
super.init()
session = MCSession(peer: myPeerID, securityIdentity: nil, encryptionPreference: .required)
session.delegate = self
}
func startHosting() {
stop()
statusText = "Hosting"
advertiser = MCNearbyServiceAdvertiser(peer: myPeerID, discoveryInfo: nil, serviceType: serviceType)
advertiser?.delegate = self
advertiser?.startAdvertisingPeer()
}
func startJoining() {
stop()
statusText = "Browsing"
browser = MCNearbyServiceBrowser(peer: myPeerID, serviceType: serviceType)
browser?.delegate = self
browser?.startBrowsingForPeers()
}
func stop() {
advertiser?.stopAdvertisingPeer()
advertiser = nil
browser?.stopBrowsingForPeers()
browser = nil
session.disconnect()
peers = []
isConnected = false
statusText = "Idle"
peerOffsets = [:]
}
func broadcast(_ message: SessionMessage) {
guard !session.connectedPeers.isEmpty else { return }
send(message, to: session.connectedPeers)
}
func reply(_ message: SessionMessage) {
guard !session.connectedPeers.isEmpty else { return }
send(message, to: session.connectedPeers)
}
func updateOffset(hostUptime: TimeInterval, peerUptime: TimeInterval) {
for peer in session.connectedPeers {
let offset = hostUptime - peerUptime
peerOffsets[peer] = offset
}
}
func sendTrack(_ track: LocalTrack) {
guard !session.connectedPeers.isEmpty else { return }
let info = TrackInfoPayload(trackID: track.id.uuidString, displayName: track.displayName)
send(.trackInfo(info), to: session.connectedPeers)
for peer in session.connectedPeers {
let resourceName = "track:\(track.id.uuidString)"
session.sendResource(at: track.url, withName: resourceName, toPeer: peer) { [weak self] error in
if let error {
DispatchQueue.main.async {
self?.statusText = "Send track failed: \(error.localizedDescription)"
}
}
}
}
}
private func send(_ message: SessionMessage, to peers: [MCPeerID]) {
do {
let data = try JSONEncoder().encode(message)
try session.send(data, toPeers: peers, with: .reliable)
} catch {
statusText = "Send failed: \(error.localizedDescription)"
}
}
private func handle(_ data: Data, from peerID: MCPeerID) {
do {
let message = try JSONDecoder().decode(SessionMessage.self, from: data)
if case .syncRequest(let hostUptime) = message {
let peerUptime = SyncClock.uptime()
SyncClock.setHostOffset(hostUptime: hostUptime, peerUptime: peerUptime)
}
onMessage?(message)
} catch {
statusText = "Decode failed: \(error.localizedDescription)"
}
}
}
extension PeerSession: MCSessionDelegate {
func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
DispatchQueue.main.async {
self.peers = session.connectedPeers.map { PeerInfo(id: $0.displayName, name: $0.displayName) }
self.isConnected = !session.connectedPeers.isEmpty
switch state {
case .connected:
self.statusText = "Connected to \(peerID.displayName)"
self.send(.hello, to: [peerID])
case .connecting:
self.statusText = "Connecting to \(peerID.displayName)"
case .notConnected:
self.statusText = "Disconnected"
@unknown default:
self.statusText = "Unknown state"
}
}
}
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
DispatchQueue.main.async {
self.handle(data, from: peerID)
}
}
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {}
func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {}
func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
guard error == nil, let localURL else { return }
DispatchQueue.main.async {
self.onReceiveResource?(resourceName, localURL)
}
}
}
extension PeerSession: MCNearbyServiceAdvertiserDelegate {
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
invitationHandler(true, session)
}
}
extension PeerSession: MCNearbyServiceBrowserDelegate {
func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
browser.invitePeer(peerID, to: session, withContext: nil, timeout: 10)
}
func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {}
}