163 lines
6.0 KiB
Swift
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) {}
|
|
}
|