Files
Commilitia-Drop/ios/CDrop/Sources/Engine/LibWebRtcEngine.swift
T

906 lines
37 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import WebRTC
// LibWebRtcEngine iOS libwebrtcSafari RTCPeerConnection
// P2P gomobile/pion pion Go raw socket iOS Network framework
// WebKit/libwebrtc desktop/NATIVE-TRANSFER.md §7
//
// 线 desktop/enginepion web/src/features/transfer/p2p.ts pion
// / iOS JS DataChannel "commilitia-drop-file"ordered
// 64KB payload {type, sdp?, candidate?} session.go +
// engine.go Swift ack
//
// WebRTC + I/O HTTP POST / POST JS
// web/src/features/transfer/p2pIos.ts P2PEngineDelegate 宿 EngineController
// cdropEngine P2PEventsBridge JS
//
// 宿EngineController 4 p2p RPC + net/ios.ts nativeP2P* Swift
// gomobile EngineEngine web IOS_NATIVE=true
// MARK: - 宿 desktop/engine Events
protocol P2PEngineDelegate: AnyObject
{
// ack ack 退
func engineDidProgress(sessionId: String, bytes: Int64)
// connected / completed / failed / closed
func engineDidChangeState(sessionId: String, state: String)
// offer/answer/icepayloadJSON JSON 宿 POST /api/hub/signal
func engineDidEmitSignal(sessionId: String, toPeer: String, payloadJSON: String)
// Documents
func engineDidSave(sessionId: String, path: String)
// "host/udp"
func engineDidSelectIcePair(sessionId: String, local: String, remote: String)
// / ICE / / "session <id> ..."
func engineDidLog(_ line: String)
}
// MARK: - 线 desktop/engine wire.goweb p2p.ts
private enum Wire
{
static let channelName = "commilitia-drop-file"
static let chunkSize = 64 * 1024
static let highWatermark: UInt64 = 16 * 1024 * 1024
static let lowWatermark: UInt64 = 4 * 1024 * 1024
static let ackIntervalMs = 200
static let progressThrottleNs: Int64 = 100 * 1_000_000 // ~10Hz宿线 evaluateJavaScript
static let ackCompleteTimeout: TimeInterval = 30
// ICE web p2p.ts ICE_CANDIDATE_POOL
static let iceCandidatePool: Int32 = 4
}
struct EngineError: LocalizedError
{
let msg: String
init(_ m: String) { msg = m }
var errorDescription: String? { msg }
}
// MARK: - engine.go Engine
final class LibWebRtcEngine
{
// SSL libwebrtc let 线
private static let sslReady: Bool = RTCInitializeSSL()
private let factory: RTCPeerConnectionFactory
// init EngineController P2PEventsBridge宿
weak var delegate: P2PEngineDelegate?
private let lock = NSLock()
private var byPeer: [String: P2PTransferSession] = [:]
private var downloadDirValue: String
init(downloadDir: String, delegate: P2PEngineDelegate)
{
_ = LibWebRtcEngine.sslReady
factory = RTCPeerConnectionFactory()
downloadDirValue = downloadDir
self.delegate = delegate
}
var events: P2PEngineDelegate? { delegate }
func downloadDir() -> String
{
lock.lock(); defer { lock.unlock() }
return downloadDirValue
}
func setDownloadDir(_ dir: String)
{
lock.lock(); downloadDirValue = dir; lock.unlock()
}
// StartOutgoing PeerConnection + DataChannel offer filePath
// filePath commilitia-drop-file://<id> completion offer
// setLocalDescription JS seed p2pIos.ts chain
func startOutgoing(sessionId: String, peerName: String, filePath: String,
iceServersJSON: String, completion: @escaping (Error?) -> Void)
{
let fm = FileManager.default
var isDir: ObjCBool = false
guard fm.fileExists(atPath: filePath, isDirectory: &isDir), !isDir.boolValue
else { completion(EngineError("not a file: \(filePath)")); return }
let size = ((try? fm.attributesOfItem(atPath: filePath))?[.size] as? NSNumber)?.int64Value ?? 0
let s = P2PTransferSession(engine: self, factory: factory,
sessionId: sessionId, peerName: peerName, role: .sender)
s.filePath = filePath
s.fileSize = size
do { try s.createPeerConnection(iceServersJSON) }
catch { completion(error); return }
put(peerName, s)
do { try s.createSenderDataChannel() }
catch { s.fail("create datachannel: \(error.localizedDescription)"); completion(error); return }
s.createOfferAndEmit(completion: completion)
}
// StartIncoming PeerConnection offer DataChannel 线
// offer completion offer/ice
func startIncoming(sessionId: String, peerName: String,
iceServersJSON: String, completion: @escaping (Error?) -> Void)
{
let s = P2PTransferSession(engine: self, factory: factory,
sessionId: sessionId, peerName: peerName, role: .receiver)
do { try s.createPeerConnection(iceServersJSON) }
catch { completion(error); return }
put(peerName, s)
completion(nil)
}
// HandleSignal宿 SSE completion
// offer answer answer setRemoteDescription
// ice addIceCandidate JS offer/answer trickle
func handleSignal(fromPeer: String, payloadJSON: String, completion: @escaping (Error?) -> Void)
{
lock.lock(); let s = byPeer[fromPeer]; lock.unlock()
guard let s
else { completion(EngineError("no active session for peer \(fromPeer)")); return }
s.handleSignal(payloadJSON, completion: completion)
}
// Cancel / 退宿 OnState
func cancel(sessionId: String)
{
lock.lock()
var target: P2PTransferSession?
for (_, s) in byPeer where s.sessionId == sessionId { target = s; break }
lock.unlock()
target?.cleanup()
}
fileprivate func put(_ peer: String, _ s: P2PTransferSession)
{
lock.lock()
let old = byPeer[peer]
byPeer[peer] = s
lock.unlock()
// 沿 web / RPC / 线
// close engine.go `go old.cleanup()`
if let old, old !== s { DispatchQueue.global(qos: .utility).async { old.cleanup() } }
}
fileprivate func remove(_ peer: String, _ s: P2PTransferSession)
{
lock.lock()
if byPeer[peer] === s { byPeer.removeValue(forKey: peer) }
lock.unlock()
}
}
// MARK: - pc + dc + session.go session
final class P2PTransferSession: NSObject
{
enum Role { case sender, receiver }
let sessionId: String
let peerName: String
let role: Role
private weak var engine: LibWebRtcEngine?
private let factory: RTCPeerConnectionFactory
private var pc: RTCPeerConnection?
private var dc: RTCDataChannel?
//
var filePath = ""
var fileSize: Int64 = 0
// 线 / pion / dc cLock
private let cLock = NSLock()
private var bytesSent: Int64 = 0 // SCTP
private var receivedBytes: Int64 = 0 //
private var ackedBytes: Int64 = 0 // ack
private var lastEmitNs: Int64 = 0 //
// outLock outHandle //writeChunk libwebrtc 线cleanup
// 线JS p2pCancel / 退 / SSE ARC retain/release close/write
// Go *os.File fdMutex Swift FileHandle
private var outHandle: FileHandle?
private var outName = ""
private var tmpPath = ""
private let outLock = NSLock()
// /
private let streamQueue: DispatchQueue
private let bufSem = DispatchSemaphore(value: 0) // didChangeBufferedAmount / ack
private var ackTimer: DispatchSourceTimer?
private var senderStarted = false
//
private let stateLock = NSLock()
private var state = ""
private var doneFlag = false
private var cleanedUp = false
init(engine: LibWebRtcEngine, factory: RTCPeerConnectionFactory,
sessionId: String, peerName: String, role: Role)
{
self.engine = engine
self.factory = factory
self.sessionId = sessionId
self.peerName = peerName
self.role = role
streamQueue = DispatchQueue(label: "net.commilitia.cdrop.p2p.\(sessionId)")
super.init()
}
// MARK:
func createPeerConnection(_ iceServersJSON: String) throws
{
let config = RTCConfiguration()
config.iceServers = Self.parseICEServers(iceServersJSON)
config.sdpSemantics = .unifiedPlan
config.continualGatheringPolicy = .gatherOnce
config.iceCandidatePoolSize = Wire.iceCandidatePool
// IPv4libwebrtc iOS Network framework
// hosthost iOS IPv4-only
// iOS-WebKit IPv6 host 退
let constraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
guard let pc = factory.peerConnection(with: config, constraints: constraints, delegate: self)
else { throw EngineError("failed to create peer connection") }
self.pc = pc
}
func createSenderDataChannel() throws
{
guard let pc else { throw EngineError("no pc") }
let dcConfig = RTCDataChannelConfiguration()
dcConfig.isOrdered = true
guard let dc = pc.dataChannel(forLabel: Wire.channelName, configuration: dcConfig)
else { throw EngineError("failed to create data channel") }
dc.delegate = self
self.dc = dc
}
func createOfferAndEmit(completion: @escaping (Error?) -> Void)
{
guard let pc else { completion(EngineError("no pc")); return }
let constraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
pc.offer(for: constraints)
{ [weak self] sdp, err in
guard let self else { completion(EngineError("session gone")); return }
if let err { self.fail("create offer: \(err.localizedDescription)"); completion(err); return }
guard let sdp else { self.fail("create offer: nil"); completion(EngineError("nil offer")); return }
pc.setLocalDescription(sdp)
{ [weak self] err2 in
guard let self else { completion(EngineError("session gone")); return }
if let err2
{
self.fail("set local (offer): \(err2.localizedDescription)"); completion(err2); return
}
self.emitSignal(sdp: sdp, candidate: nil)
completion(nil)
}
}
}
// MARK:
func handleSignal(_ payloadJSON: String, completion: @escaping (Error?) -> Void)
{
guard let pc else { completion(EngineError("no pc")); return }
guard let data = payloadJSON.data(using: .utf8),
let obj = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
let type = obj["type"] as? String
else { completion(EngineError("decode signal")); return }
log("signal in: \(type)")
switch type
{
case "offer":
guard let sdpObj = obj["sdp"] as? [String: Any], let sdpStr = sdpObj["sdp"] as? String
else { completion(EngineError("offer missing sdp")); return }
let desc = RTCSessionDescription(type: .offer, sdp: sdpStr)
pc.setRemoteDescription(desc)
{ [weak self] err in
guard let self else { completion(EngineError("gone")); return }
if let err { completion(err); return }
let constraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil)
pc.answer(for: constraints)
{ [weak self] ans, aerr in
guard let self else { completion(EngineError("gone")); return }
if let aerr { completion(aerr); return }
guard let ans else { completion(EngineError("nil answer")); return }
pc.setLocalDescription(ans)
{ [weak self] lerr in
guard let self else { completion(EngineError("gone")); return }
if let lerr { completion(lerr); return }
self.emitSignal(sdp: ans, candidate: nil)
completion(nil)
}
}
}
case "answer":
guard let sdpObj = obj["sdp"] as? [String: Any], let sdpStr = sdpObj["sdp"] as? String
else { completion(EngineError("answer missing sdp")); return }
let desc = RTCSessionDescription(type: .answer, sdp: sdpStr)
pc.setRemoteDescription(desc) { err in completion(err) }
case "ice":
// / end-of-candidatespion nil candidate
guard let candObj = obj["candidate"] as? [String: Any],
let candStr = candObj["candidate"] as? String, !candStr.isEmpty
else { completion(nil); return }
let mid = candObj["sdpMid"] as? String
let mline = (candObj["sdpMLineIndex"] as? NSNumber)?.int32Value ?? 0
let cand = RTCIceCandidate(sdp: candStr, sdpMLineIndex: mline, sdpMid: mid)
pc.add(cand) { err in completion(err) }
default:
completion(nil)
}
}
// MARK:
// session.go emitSignal candidate ice sdp offer/answer
// JSON web p2p.ts SignalPayload / pion signalPayload parse
private func emitSignal(sdp: RTCSessionDescription?, candidate: RTCIceCandidate?)
{
var payload: [String: Any] = [:]
if let candidate
{
payload["type"] = "ice"
// RTCIceCandidateObjC usernameFragment candidate JSON candidate/sdpMid/
// sdpMLineIndexufrag m-line ice-ufrag offer/answer pion
// addIceCandidate usernameFragment
var c: [String: Any] = [
"candidate": candidate.sdp,
"sdpMLineIndex": Int(candidate.sdpMLineIndex),
]
c["sdpMid"] = candidate.sdpMid ?? NSNull()
payload["candidate"] = c
}
else if let sdp
{
let t = Self.sdpTypeString(sdp.type)
payload["type"] = t
payload["sdp"] = [ "type": t, "sdp": sdp.sdp ]
}
else { return }
guard let data = try? JSONSerialization.data(withJSONObject: payload),
let json = String(data: data, encoding: .utf8)
else { return }
engine?.events?.engineDidEmitSignal(sessionId: sessionId, toPeer: peerName, payloadJSON: json)
}
// MARK:
private func startSenderStream()
{
cLock.lock()
if senderStarted { cLock.unlock(); return }
senderStarted = true
cLock.unlock()
streamQueue.async { [weak self] in self?.streamFile() }
}
// streamFile 64KB pion / web
// HIGH LOW done ack completed
private func streamFile()
{
guard let dc else { fail("no data channel"); return }
let fh: FileHandle
do { fh = try FileHandle(forReadingFrom: URL(fileURLWithPath: filePath)) }
catch { fail("open \(filePath): \(error.localizedDescription)"); return }
defer { try? fh.close() }
if !sendText([ "type": "meta", "name": (filePath as NSString).lastPathComponent, "size": fileSize ])
{
fail("send meta"); return
}
while true
{
if isDone() { return }
let chunk: Data?
do { chunk = try fh.read(upToCount: Wire.chunkSize) }
catch { fail("read \(filePath): \(error.localizedDescription)"); return }
guard let chunk, !chunk.isEmpty else { break } // EOF
if dc.bufferedAmount + UInt64(chunk.count) > Wire.highWatermark
{
waitBufferLow(Wire.lowWatermark)
}
if isDone() { return }
if !dc.sendData(RTCDataBuffer(data: chunk, isBinary: true))
{
fail("send chunk failed (channel closed?)"); return
}
addSent(Int64(chunk.count))
emitProgress()
}
_ = sendText([ "type": "done" ])
waitDrained()
waitAck(fileSize)
emitProgressNow()
setState("completed")
}
@discardableResult
private func sendText(_ dict: [String: Any]) -> Bool
{
guard let dc, let data = try? JSONSerialization.data(withJSONObject: dict) else { return false }
return dc.sendData(RTCDataBuffer(data: data, isBinary: false))
}
// threshold didChangeBufferedAmount bufSem 50ms
private func waitBufferLow(_ threshold: UInt64)
{
let start = Date()
var warned = false
while true
{
guard let dc else { return }
if dc.bufferedAmount <= threshold { return }
if isDone() { return }
_ = bufSem.wait(timeout: .now() + 0.05)
if !warned, Date().timeIntervalSince(start) > 5
{
warned = true
log("send stalled >5s: buffered=\(dc.bufferedAmount) sent=\(getSent()) acked=\(getAcked())")
}
}
}
// done SCTP
private func waitDrained()
{
while true
{
guard let dc else { return }
if dc.bufferedAmount == 0 { return }
if isDone() { return }
_ = bufSem.wait(timeout: .now() + 0.02)
}
}
// ack total ack session.go waitAck
private func waitAck(_ total: Int64)
{
if getAcked() == 0 { return }
let deadline = Date().addingTimeInterval(Wire.ackCompleteTimeout)
while getAcked() < total
{
if Date() > deadline { return }
if isDone() { return }
_ = bufSem.wait(timeout: .now() + 0.05)
}
}
// MARK:
private func bindReceiverDataChannel(_ channel: RTCDataChannel)
{
dc = channel
channel.delegate = self
startAckLoop()
}
// 200ms
private func startAckLoop()
{
let timer = DispatchSource.makeTimerSource(queue: streamQueue)
timer.schedule(deadline: .now() + .milliseconds(Wire.ackIntervalMs),
repeating: .milliseconds(Wire.ackIntervalMs))
var lastSent: Int64 = -1
timer.setEventHandler
{ [weak self] in
guard let self else { return }
let r = self.getReceived()
if r != lastSent { self.sendAck(r); lastSent = r }
}
// cleanup didOpen cancel
stateLock.lock()
if cleanedUp { stateLock.unlock(); timer.cancel(); return }
ackTimer = timer
stateLock.unlock()
timer.resume()
}
private func sendAck(_ bytes: Int64)
{
guard let dc, dc.readyState == .open,
let data = try? JSONSerialization.data(withJSONObject: [ "type": "ack", "bytes": bytes ])
else { return }
_ = dc.sendData(RTCDataBuffer(data: data, isBinary: false))
}
// ack meta / done
private func handleControl(_ data: Data)
{
guard let obj = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
let type = obj["type"] as? String
else { return }
switch type
{
case "meta":
if role == .receiver { openOutput(name: obj["name"] as? String ?? "download") }
case "done":
if role == .receiver { finalizeReceive() }
case "ack":
if role == .sender, let b = (obj["bytes"] as? NSNumber)?.int64Value
{
setAcked(b)
bufSem.signal() // waitAck / waitDrained
emitProgress()
}
default:
break
}
}
private func openOutput(name: String)
{
outName = name
let dir = engine?.downloadDir() ?? NSTemporaryDirectory()
let tmp = (dir as NSString).appendingPathComponent(".\(sessionId).part")
tmpPath = tmp
FileManager.default.createFile(atPath: tmp, contents: nil)
let handle = FileHandle(forWritingAtPath: tmp)
outLock.lock(); outHandle = handle; outLock.unlock()
if handle == nil { fail("create \(tmp)") }
}
// base64 OPFS WebView dc
private func writeChunk(_ data: Data)
{
// outLock cleanup / outHandle
outLock.lock()
if let h = outHandle
{
do { try h.write(contentsOf: data) }
catch { outLock.unlock(); fail("write chunk: \(error.localizedDescription)"); return }
}
outLock.unlock()
_ = addReceived(Int64(data.count))
emitProgress()
}
// doneorderedreceivedBytes ack
// completed NSObject finalize() override
private func finalizeReceive()
{
sendAck(getReceived())
outLock.lock(); let h = outHandle; outHandle = nil; outLock.unlock()
if let h
{
try? h.close()
let dir = engine?.downloadDir() ?? NSTemporaryDirectory()
let final = Self.nonCollidingPath(dir: dir, name: outName)
do { try FileManager.default.moveItem(atPath: tmpPath, toPath: final) }
catch { fail("rename to \(final): \(error.localizedDescription)"); return }
engine?.events?.engineDidSave(sessionId: sessionId, path: final)
}
emitProgressNow()
setState("completed")
}
// MARK: / /
// ack ack 退
// session.go progressBytes
private func progressBytes() -> Int64
{
if role == .sender
{
let a = getAcked()
if a > 0 { return min(fileSize, a) }
let buffered = Int64(dc?.bufferedAmount ?? 0)
let b = getSent() - buffered
return b < 0 ? 0 : b
}
return getReceived()
}
// ~10Hz宿线 evaluateJavaScript
private func emitProgress()
{
let now = Int64(DispatchTime.now().uptimeNanoseconds)
cLock.lock()
if now - lastEmitNs < Wire.progressThrottleNs { cLock.unlock(); return }
lastEmitNs = now
cLock.unlock()
engine?.events?.engineDidProgress(sessionId: sessionId, bytes: progressBytes())
}
// 宿 99%
private func emitProgressNow()
{
cLock.lock(); lastEmitNs = Int64(DispatchTime.now().uptimeNanoseconds); cLock.unlock()
engine?.events?.engineDidProgress(sessionId: sessionId, bytes: progressBytes())
}
private func setState(_ st: String)
{
stateLock.lock()
// completed/failed 便 closed/failed 退
if state == st || state == "completed" || state == "failed"
{
stateLock.unlock(); return
}
state = st
stateLock.unlock()
engine?.events?.engineDidChangeState(sessionId: sessionId, state: st)
switch st
{
case "failed", "closed":
cleanup()
case "completed":
// ack ack
// ackLoop 60s 宿 SSE cancel
streamQueue.asyncAfter(deadline: .now() + 60) { [weak self] in self?.cleanup() }
default:
break
}
}
func fail(_ msg: String)
{
log("fail: \(msg)")
setState("failed")
}
func cleanup()
{
stateLock.lock()
if cleanedUp { stateLock.unlock(); return }
cleanedUp = true
doneFlag = true
let timer = ackTimer // startAckLoop stateLock ackTimer ARC
ackTimer = nil
stateLock.unlock()
bufSem.signal() // / ack
timer?.cancel()
// outHandle outLock writeChunk writeChunk cleanup
// close/write ARC
outLock.lock(); let h = outHandle; outHandle = nil; outLock.unlock()
if let h { try? h.close() }
dc?.close()
pc?.close()
engine?.remove(peerName, self)
}
// MARK: cLock
private func addSent(_ n: Int64) { cLock.lock(); bytesSent += n; cLock.unlock() }
private func getSent() -> Int64 { cLock.lock(); defer { cLock.unlock() }; return bytesSent }
private func addReceived(_ n: Int64) -> Int64
{
cLock.lock(); receivedBytes += n; let r = receivedBytes; cLock.unlock(); return r
}
private func getReceived() -> Int64 { cLock.lock(); defer { cLock.unlock() }; return receivedBytes }
private func setAcked(_ v: Int64) { cLock.lock(); if v > ackedBytes { ackedBytes = v }; cLock.unlock() }
private func getAcked() -> Int64 { cLock.lock(); defer { cLock.unlock() }; return ackedBytes }
private func isDone() -> Bool { stateLock.lock(); defer { stateLock.unlock() }; return doneFlag }
// MARK: best-effort
private func logSelectedPair()
{
// streamQueue streamFile
//
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 0.3)
{ [weak self] in self?.querySelectedPair(attempt: 0) }
}
private func querySelectedPair(attempt: Int)
{
guard attempt < 12, !isDone(), let pc else { return }
pc.statistics
{ [weak self] report in
guard let self else { return }
if let pair = Self.extractSelectedPair(report)
{
self.log("ice pair: local=\(pair.0) remote=\(pair.1)")
self.engine?.events?.engineDidSelectIcePair(sessionId: self.sessionId,
local: pair.0, remote: pair.1)
}
else
{
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 0.3)
{ [weak self] in self?.querySelectedPair(attempt: attempt + 1) }
}
}
}
// MARK:
private static func parseICEServers(_ raw: String) -> [RTCIceServer]
{
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let data = trimmed.data(using: .utf8),
let arr = (try? JSONSerialization.jsonObject(with: data)) as? [[String: Any]]
else { return [] }
var out: [RTCIceServer] = []
for s in arr
{
var urls: [String] = []
if let one = s["urls"] as? String { urls = [one] }
else if let many = s["urls"] as? [String] { urls = many }
if urls.isEmpty { continue }
let username = s["username"] as? String
let credential = s["credential"] as? String
if let username, let credential, !username.isEmpty
{
out.append(RTCIceServer(urlStrings: urls, username: username, credential: credential))
}
else
{
out.append(RTCIceServer(urlStrings: urls))
}
}
return out
}
// dir namename (1)name (2) wire.go nonCollidingPath
private static func nonCollidingPath(dir: String, name: String) -> String
{
let fm = FileManager.default
let base = (dir as NSString).appendingPathComponent(name)
if !fm.fileExists(atPath: base) { return base }
let ext = (name as NSString).pathExtension
let stem = (name as NSString).deletingPathExtension
var i = 1
while true
{
let candidate = ext.isEmpty ? "\(stem) (\(i))" : "\(stem) (\(i)).\(ext)"
let p = (dir as NSString).appendingPathComponent(candidate)
if !fm.fileExists(atPath: p) { return p }
i += 1
}
}
private static func extractSelectedPair(_ report: RTCStatisticsReport) -> (String, String)?
{
let stats = report.statistics
var localId: String?
var remoteId: String?
for (_, s) in stats where s.type == "candidate-pair"
{
let v = s.values
let succeeded = (v["state"] as? String) == "succeeded"
let nominated = (v["nominated"] as? NSNumber)?.boolValue ?? false
if succeeded, nominated
{
localId = v["localCandidateId"] as? String
remoteId = v["remoteCandidateId"] as? String
break
}
}
guard let localId, let remoteId else { return nil }
func label(_ id: String) -> String
{
guard let c = stats[id] else { return "?" }
let t = (c.values["candidateType"] as? String) ?? "?"
let proto = (c.values["protocol"] as? String) ?? "?"
return "\(t)/\(proto)"
}
return (label(localId), label(remoteId))
}
private static func sdpTypeString(_ t: RTCSdpType) -> String
{
switch t
{
case .offer: return "offer"
case .answer: return "answer"
case .prAnswer: return "pranswer"
case .rollback: return "rollback"
@unknown default: return "offer"
}
}
private static func iceConnString(_ s: RTCIceConnectionState) -> String
{
switch s
{
case .new: return "new"
case .checking: return "checking"
case .connected: return "connected"
case .completed: return "completed"
case .failed: return "failed"
case .disconnected: return "disconnected"
case .closed: return "closed"
case .count: return "count"
@unknown default: return "?"
}
}
// SDP "type/proto"best-effort
// candidate:<foundation> <component> <proto> <priority> <ip> <port> typ <type> ...
private static func candTypeString(_ sdp: String) -> String
{
let parts = sdp.split(separator: " ").map(String.init)
var typ = "?"
var proto = "?"
if parts.count > 2 { proto = parts[2].lowercased() }
if let i = parts.firstIndex(of: "typ"), i + 1 < parts.count { typ = parts[i + 1] }
return "\(typ)/\(proto)"
}
private func log(_ msg: String)
{
engine?.events?.engineDidLog("session \(sessionId) \(msg)")
}
}
// MARK: - RTCPeerConnectionDelegate libwebrtc 线线
extension P2PTransferSession: RTCPeerConnectionDelegate
{
func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState) {}
func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) {}
func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) {}
func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) {}
func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState)
{
log("ice: \(Self.iceConnString(newState))")
}
func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState)
{
if newState == .complete { log("local gathering done") }
}
func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate)
{
log("cand local: \(Self.candTypeString(candidate.sdp))")
emitSignal(sdp: nil, candidate: candidate)
}
func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) {}
func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel)
{
if role == .receiver { bindReceiverDataChannel(dataChannel) }
}
// ice+dtls session.go OnConnectionStateChange connected/failed/closed
func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCPeerConnectionState)
{
switch newState
{
case .connected:
setState("connected")
logSelectedPair()
case .failed:
fail("peerconnection failed")
case .closed:
setState("closed")
default:
break
}
}
}
// MARK: - RTCDataChannelDelegate libwebrtc 线
extension P2PTransferSession: RTCDataChannelDelegate
{
func dataChannelDidChangeState(_ dataChannel: RTCDataChannel)
{
// streamFile session.go dc.OnOpen go streamFile
if role == .sender, dataChannel.readyState == .open { startSenderStream() }
}
func dataChannel(_ dataChannel: RTCDataChannel, didReceiveMessageWith buffer: RTCDataBuffer)
{
if buffer.isBinary { writeChunk(buffer.data) }
else { handleControl(buffer.data) }
}
func dataChannel(_ dataChannel: RTCDataChannel, didChangeBufferedAmount amount: UInt64)
{
if amount <= Wire.lowWatermark { bufSem.signal() }
}
}