You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
141 lines
6.2 KiB
141 lines
6.2 KiB
import Foundation
|
|
import Testing
|
|
@testable import Intercom
|
|
|
|
@Suite struct SDPTests {
|
|
// Shortened recvonly offer as Chrome/Safari produce it for two transceivers.
|
|
let offer = """
|
|
v=0\r
|
|
o=- 4611731400430051336 2 IN IP4 127.0.0.1\r
|
|
s=-\r
|
|
t=0 0\r
|
|
a=group:BUNDLE 0 1\r
|
|
a=extmap-allow-mixed\r
|
|
a=msid-semantic: WMS\r
|
|
m=audio 9 UDP/TLS/RTP/SAVPF 111 63 9 0 8\r
|
|
c=IN IP4 0.0.0.0\r
|
|
a=rtcp:9 IN IP4 0.0.0.0\r
|
|
a=ice-ufrag:Ab3x\r
|
|
a=ice-pwd:k9Qmw0Rr1qY2yT7cXv8sJp4u\r
|
|
a=ice-options:trickle\r
|
|
a=fingerprint:sha-256 4c:1b:00:8f:6d:3f:9b:39:72:20:4e:69:ef:4a:16:1c:c2:39:d9:e4:3f:1e:f1:5b:3b:91:85:73:a0:cd:7a:4e\r
|
|
a=setup:actpass\r
|
|
a=mid:0\r
|
|
a=recvonly\r
|
|
a=rtcp-mux\r
|
|
a=rtpmap:111 opus/48000/2\r
|
|
a=fmtp:111 minptime=10;useinbandfec=1\r
|
|
a=rtpmap:63 red/48000/2\r
|
|
a=rtpmap:9 G722/8000\r
|
|
m=video 9 UDP/TLS/RTP/SAVPF 96 97 102 103 127 125\r
|
|
c=IN IP4 0.0.0.0\r
|
|
a=ice-ufrag:Ab3x\r
|
|
a=ice-pwd:k9Qmw0Rr1qY2yT7cXv8sJp4u\r
|
|
a=fingerprint:sha-256 4c:1b:00:8f:6d:3f:9b:39:72:20:4e:69:ef:4a:16:1c:c2:39:d9:e4:3f:1e:f1:5b:3b:91:85:73:a0:cd:7a:4e\r
|
|
a=setup:actpass\r
|
|
a=mid:1\r
|
|
a=recvonly\r
|
|
a=rtcp-mux\r
|
|
a=rtpmap:96 VP8/90000\r
|
|
a=rtpmap:97 rtx/90000\r
|
|
a=fmtp:97 apt=96\r
|
|
a=rtpmap:102 H264/90000\r
|
|
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f\r
|
|
a=rtpmap:103 H264/90000\r
|
|
a=fmtp:103 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\r
|
|
a=rtpmap:127 H264/90000\r
|
|
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r
|
|
a=rtpmap:125 H264/90000\r
|
|
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f\r
|
|
|
|
"""
|
|
|
|
@Test func parsesOffer() throws {
|
|
let parsed = try SDPOffer(offer)
|
|
#expect(parsed.iceUfrag == "Ab3x")
|
|
#expect(parsed.icePwd == "k9Qmw0Rr1qY2yT7cXv8sJp4u")
|
|
#expect(parsed.fingerprint.hasPrefix("4C:1B:00:8F"))
|
|
#expect(parsed.media.map(\.kind) == ["audio", "video"])
|
|
#expect(parsed.media.map(\.mid) == ["0", "1"])
|
|
#expect(parsed.opusPayloadType(in: parsed.media[0]) == 111)
|
|
// Constrained baseline with packetization-mode=1 wins.
|
|
#expect(parsed.h264PayloadType(in: parsed.media[1]) == 127)
|
|
}
|
|
|
|
@Test func missingCredentialsFail() {
|
|
#expect(throws: SDPOffer.ParseError.missing("ice-ufrag")) { try SDPOffer("v=0\r\nm=audio 9 UDP/TLS/RTP/SAVPF 111\r\n") }
|
|
}
|
|
|
|
@Test func answerIsIceLitePassiveSendOnly() throws {
|
|
let answer = SDPAnswer(iceUfrag: "musu", icePwd: "0123456789abcdefghijklmn", fingerprint: "AA:BB", address: "192.168.1.20",
|
|
port: 50000, tracks: [.init(mid: "0", kind: "audio", payloadType: 111, ssrc: 11),
|
|
.init(mid: "1", kind: "video", payloadType: 127, ssrc: 22)]).text
|
|
#expect(answer.contains("a=ice-lite\r\n"))
|
|
#expect(answer.contains("a=group:BUNDLE 0 1\r\n"))
|
|
#expect(answer.contains("m=audio 50000 UDP/TLS/RTP/SAVPF 111\r\n"))
|
|
#expect(answer.contains("m=video 50000 UDP/TLS/RTP/SAVPF 127\r\n"))
|
|
#expect(answer.contains("a=setup:passive\r\n"))
|
|
#expect(answer.contains("a=candidate:1 1 udp 2130706431 192.168.1.20 50000 typ host\r\n"))
|
|
#expect(!answer.contains("a=recvonly"))
|
|
}
|
|
|
|
@Test func rejectedTrackGetsPortZero() {
|
|
let answer = SDPAnswer(iceUfrag: "u", icePwd: "p", fingerprint: "F", address: "10.0.0.1", port: 1,
|
|
tracks: [.init(mid: "0", kind: "audio", payloadType: 111, ssrc: 1),
|
|
.init(mid: "1", kind: "video", payloadType: nil, ssrc: 2)]).text
|
|
#expect(answer.contains("a=group:BUNDLE 0\r\n"))
|
|
#expect(answer.contains("m=video 0 UDP/TLS/RTP/SAVPF 0\r\n"))
|
|
}
|
|
}
|
|
|
|
@Suite struct RTPTests {
|
|
@Test func headerFields() {
|
|
var stream = RTPStream(ssrc: 0x0102_0304, payloadType: 111)
|
|
let first = stream.packet(timestamp: 960, marker: true, payload: [9, 9])
|
|
let second = stream.packet(timestamp: 1920, marker: false, payload: [])
|
|
#expect(first[0] == 0x80)
|
|
#expect(first[1] == 0x80 | 111)
|
|
#expect(Array(first[4..<12]) == [0, 0, 3, 0xC0, 1, 2, 3, 4])
|
|
#expect(Array(first[12...]) == [9, 9])
|
|
let seq1 = UInt16(first[2]) << 8 | UInt16(first[3])
|
|
let seq2 = UInt16(second[2]) << 8 | UInt16(second[3])
|
|
#expect(seq2 == seq1 &+ 1)
|
|
}
|
|
|
|
@Test func aggregatesParameterSetsAndKeepsSmallNALs() {
|
|
let sps: [UInt8] = [0x67, 1, 2, 3]
|
|
let pps: [UInt8] = [0x68, 4]
|
|
let idr: [UInt8] = [0x65] + Array(repeating: 7, count: 100)
|
|
let payloads = H264Packetizer.payloads(for: [sps, pps, idr])
|
|
#expect(payloads.count == 2)
|
|
#expect(payloads[0] == [0x78, 0, 4] + sps + [0, 2] + pps) // STAP-A with NRI 3
|
|
#expect(payloads[1] == idr)
|
|
}
|
|
|
|
@Test func fragmentsLargeNALAndReassembles() {
|
|
let nal: [UInt8] = [0x65] + (0..<5000).map { UInt8($0 & 0xFF) }
|
|
let payloads = H264Packetizer.payloads(for: [nal])
|
|
#expect(payloads.count > 1)
|
|
#expect(payloads.allSatisfy { $0.count <= H264Packetizer.maxPayload })
|
|
#expect(payloads.first?[1] == 0x85) // start bit + type 5
|
|
#expect(payloads.last?[1] == 0x45) // end bit + type 5
|
|
#expect(payloads.allSatisfy { $0[0] == 0x7C }) // FU-A, NRI 3
|
|
let rebuilt = [UInt8(0x65)] + payloads.flatMap { $0.dropFirst(2) }
|
|
#expect(rebuilt == nal)
|
|
}
|
|
|
|
@Test func splitsAVCC() {
|
|
let avcc: [UInt8] = [0, 0, 0, 2, 0x67, 1, 0, 0, 0, 3, 0x65, 2, 3]
|
|
#expect(H264Packetizer.nalUnits(avcc: avcc) == [[0x67, 1], [0x65, 2, 3]])
|
|
}
|
|
|
|
@Test func detectsKeyFrameRequests() {
|
|
let receiverReport: [UInt8] = [0x80, 201, 0, 1, 0, 0, 0, 1]
|
|
let pli: [UInt8] = [0x81, 206, 0, 2, 0, 0, 0, 1, 0, 0, 0, 2]
|
|
#expect(RTCPFeedback.requestsKeyFrame(receiverReport + pli))
|
|
#expect(!RTCPFeedback.requestsKeyFrame(receiverReport))
|
|
#expect(RTCPFeedback.isRTCP(pli))
|
|
#expect(!RTCPFeedback.isRTCP([0x80, 111, 0, 1, 0, 0, 0, 0]))
|
|
}
|
|
}
|