c43dd3ca4a
A native iOS receiver so an iPhone can act as the second receiver, speaking the existing signaling + RTP protocol (no C++ changes) and mirroring the Android receiver (Phase 8) source-to-source. - RTP core (header/packet, jitter buffer, H.264 depacketizer) ported from the Android receiver - BSD-socket signaling server (dual-stack, most-recent-peer, never-throwing sends) + NSBonjourServices - VideoToolbox H.264 decode (in-band SPS/PPS, real-time, rebuilds on size change) -> AVSampleBufferDisplayLayer - PLI keyframe recovery (500 ms) + pendingOffer for late surface attach - XcodeGen project + bootstrap.sh; XCTest port of the Android suite + new coverage - .gitignore for generated artifacts; CHANGELOG; PHASES + MEMORY updated Status: authored; on-device validation pending a Mac + Xcode 26 + iPhone 16.
90 lines
3.4 KiB
Swift
90 lines
3.4 KiB
Swift
import Foundation
|
|
|
|
/// JSON wire format shared with the C++ implementation: one JSON object per
|
|
/// newline-terminated TCP line (offer / answer / pli). Mirrors the C++
|
|
/// `SignalingMessage` and the Kotlin receiver.
|
|
enum SignalingMessage {
|
|
case offer(sessionId: String,
|
|
codec: String,
|
|
width: Int,
|
|
height: Int,
|
|
frameRateNum: Int,
|
|
frameRateDen: Int,
|
|
rtpAddress: String,
|
|
rtpPort: Int)
|
|
case answer(sessionId: String,
|
|
rtpAddress: String,
|
|
rtpPort: Int,
|
|
displayWidth: Int,
|
|
displayHeight: Int)
|
|
case pli(sessionId: String)
|
|
|
|
static let maxMessageBytes = 64 * 1024
|
|
|
|
static func parse(_ line: String) -> SignalingMessage? {
|
|
guard line.count <= maxMessageBytes else { return nil }
|
|
guard let data = line.data(using: .utf8),
|
|
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
let type = obj["type"] as? String else {
|
|
return nil
|
|
}
|
|
switch type {
|
|
case "offer":
|
|
return .offer(
|
|
sessionId: obj["session_id"] as? String ?? "",
|
|
codec: obj["codec"] as? String ?? "",
|
|
width: obj["width"] as? Int ?? 0,
|
|
height: obj["height"] as? Int ?? 0,
|
|
frameRateNum: obj["frame_rate_num"] as? Int ?? 30,
|
|
frameRateDen: obj["frame_rate_den"] as? Int ?? 1,
|
|
rtpAddress: obj["rtp_address"] as? String ?? "",
|
|
rtpPort: obj["rtp_port"] as? Int ?? 0)
|
|
case "answer":
|
|
return .answer(
|
|
sessionId: obj["session_id"] as? String ?? "",
|
|
rtpAddress: obj["rtp_address"] as? String ?? "",
|
|
rtpPort: obj["rtp_port"] as? Int ?? 0,
|
|
displayWidth: obj["display_width"] as? Int ?? 0,
|
|
displayHeight: obj["display_height"] as? Int ?? 0)
|
|
case "pli":
|
|
return .pli(sessionId: obj["session_id"] as? String ?? "")
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
static func serialize(_ message: SignalingMessage) -> String {
|
|
let json: [String: Any]
|
|
switch message {
|
|
case let .offer(sessionId, codec, width, height, frameRateNum, frameRateDen, rtpAddress, rtpPort):
|
|
json = [
|
|
"type": "offer",
|
|
"session_id": sessionId,
|
|
"codec": codec,
|
|
"width": width,
|
|
"height": height,
|
|
"frame_rate_num": frameRateNum,
|
|
"frame_rate_den": frameRateDen,
|
|
"rtp_address": rtpAddress,
|
|
"rtp_port": rtpPort,
|
|
]
|
|
case let .answer(sessionId, rtpAddress, rtpPort, displayWidth, displayHeight):
|
|
json = [
|
|
"type": "answer",
|
|
"session_id": sessionId,
|
|
"rtp_address": rtpAddress,
|
|
"rtp_port": rtpPort,
|
|
"display_width": displayWidth,
|
|
"display_height": displayHeight,
|
|
]
|
|
case let .pli(sessionId):
|
|
json = ["type": "pli", "session_id": sessionId]
|
|
}
|
|
guard let data = try? JSONSerialization.data(withJSONObject: json),
|
|
let s = String(data: data, encoding: .utf8) else {
|
|
return "{}\n" // unreachable for our message types; serialize is total
|
|
}
|
|
return s + "\n"
|
|
}
|
|
}
|