├── .clang-format ├── .github ├── dependabot.yaml └── workflows │ ├── deps.yaml │ ├── swift.yaml │ └── test.yaml ├── .gitignore ├── .swift-format ├── .swift-version ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── RTP │ ├── Connection.swift │ ├── Data+Extensions.swift │ ├── FixedWidthInteger+Extensions.swift │ ├── Packet.swift │ ├── Packetizer.swift │ └── Types.swift └── Tests └── RTPTests ├── ConnectionTests.swift ├── PacketTests.swift ├── PacketizerTests.swift └── TypeTests.swift /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: Google 3 | ColumnLimit: 2048 4 | IndentWidth: 8 5 | UseTab: ForIndentation 6 | SpacesBeforeTrailingComments: 1 7 | --- 8 | Language: Proto 9 | Cpp11BracedListStyle: true 10 | --- 11 | 12 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | 8 | - package-ecosystem: gitsubmodule 9 | directory: "/" 10 | schedule: 11 | interval: weekly 12 | -------------------------------------------------------------------------------- /.github/workflows/deps.yaml: -------------------------------------------------------------------------------- 1 | name: Dependencies 2 | 3 | on: 4 | schedule: 5 | - cron: "11 13 * * 1" # 13:11 UTC every Monday 6 | 7 | env: 8 | GITHUB_TOKEN: ${{ secrets.ALTACI_RW_GITHUB_ACCESS_TOKEN }} 9 | 10 | jobs: 11 | update-swift-packages: 12 | name: Update Swift packages 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repo 16 | uses: actions/checkout@v4 17 | with: 18 | ref: main 19 | submodules: recursive 20 | 21 | - name: Update Swift packages 22 | run: swift package update 23 | 24 | - name: Create a pull request 25 | run: | 26 | git diff --exit-code HEAD || 27 | ( 28 | git config --global user.email ci@alta.software && 29 | git config --global user.name altaci && 30 | git checkout -b "deps/$(date -u '+%Y-%m-%dT%H.%M.%SZ')/swift-packages" && 31 | git add . && git commit -m 'deps: update Swift packages' && 32 | hub pull-request --push --base main --labels swift,dependencies -m 'deps: update Swift packages' 33 | ) 34 | -------------------------------------------------------------------------------- /.github/workflows/swift.yaml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "**.swift" 9 | - "Package.resolved" 10 | pull_request: 11 | paths: 12 | - "**.swift" 13 | - "Package.resolved" 14 | 15 | jobs: 16 | format: 17 | name: Format 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout repo 21 | uses: actions/checkout@v4 22 | with: 23 | submodules: recursive 24 | 25 | - name: Install swift-format 26 | uses: Cyberbeni/install-swift-tool@v3 27 | with: 28 | url: https://github.com/apple/swift-format 29 | 30 | - name: Format Swift code 31 | run: swift-format -r -i ./ 32 | 33 | - name: Verify formatted code is unchanged 34 | run: git diff --exit-code HEAD -w -G'(^[^# /])|(^#\w)|(^\s+[^#/])' # Ignore whitespace and comments 35 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - ".github/workflows/test.yaml" 9 | - ".gitmodules" 10 | - "**.swift" 11 | - "Package.resolved" 12 | pull_request: 13 | paths: 14 | - ".github/workflows/test.yaml" 15 | - ".gitmodules" 16 | - "**.swift" 17 | - "Package.resolved" 18 | 19 | jobs: 20 | test: 21 | name: Test 22 | runs-on: ${{ matrix.os }} 23 | timeout-minutes: 10 24 | strategy: 25 | matrix: 26 | os: 27 | - macos-latest 28 | - ubuntu-latest 29 | steps: 30 | - name: Checkout repo 31 | uses: actions/checkout@v4 32 | with: 33 | submodules: recursive 34 | 35 | - name: Test 36 | run: swift test 37 | 38 | - name: Generate release build 39 | run: swift build -c release 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | !**/.metadata_never_index 4 | 5 | # Xcode/Swift 6 | .build/ 7 | *.pbxuser 8 | !default.pbxuser 9 | *.mode1v3 10 | !default.mode1v3 11 | *.mode2v3 12 | !default.mode2v3 13 | *.perspectivev3 14 | !default.perspectivev3 15 | xcuserdata 16 | *.xccheckout 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | *.app 22 | *.dSYM.zip 23 | *.xcuserstate 24 | .swiftpm 25 | -------------------------------------------------------------------------------- /.swift-format: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "lineLength": 2048, 4 | "indentation": { 5 | "tabs": 1 6 | }, 7 | "tabWidth": 4, 8 | "maximumBlankLines": 1, 9 | "respectsExistingLineBreaks": true, 10 | "lineBreakBeforeControlFlowKeywords": true, 11 | "lineBreakBeforeEachArgument": false, 12 | "prioritizeKeepingFunctionOutputTogether": true 13 | } 14 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.3 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // List of extensions which should be recommended for users of this workspace. 3 | // See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 4 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 5 | "recommendations": [ 6 | "davidanson.vscode-markdownlint", 7 | "eamodio.gitlens", 8 | "eriklynd.json-tools", 9 | "foxundermoon.shell-format", 10 | "redhat.vscode-yaml", 11 | "stkb.rewrap", 12 | "vknabel.vscode-apple-swift-format", 13 | "xaver.clang-format", 14 | ], 15 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 16 | "unwantedRecommendations": [] 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Editor 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": "explicit" 5 | }, 6 | "editor.detectIndentation": false, 7 | "editor.formatOnSave": true, 8 | "editor.insertSpaces": false, 9 | "editor.tabSize": 4, 10 | "files.eol": "\n", 11 | // 12 | // Files 13 | "files.exclude": { 14 | "**/.build": true, 15 | "**/.git": true, 16 | // "Sources/Copus": true, 17 | }, 18 | "files.associations": {}, 19 | "files.insertFinalNewline": true, 20 | "files.watcherExclude": { 21 | "**/.git/objects/**": true, 22 | "**/.git/subtree-cache/**": true, 23 | }, 24 | "files.trimTrailingWhitespace": true, 25 | // 26 | // HTML 27 | "html.format.contentUnformatted": "pre,code,textarea,script", 28 | "html.format.unformatted": "pre,code,textarea,script,wbr", 29 | // 30 | // JSON 31 | "[json]": { 32 | "editor.defaultFormatter": "vscode.json-language-features", 33 | }, 34 | "[jsonc]": { 35 | "editor.defaultFormatter": "vscode.json-language-features", 36 | }, 37 | // 38 | // Make 39 | "[makefile]": { 40 | "editor.tabSize": 4 41 | }, 42 | // 43 | // Markdown 44 | "[markdown]": { 45 | "editor.rulers": [ 46 | 80 47 | ], 48 | "editor.codeActionsOnSave": { 49 | "source.fixAll.markdownlint": "explicit" 50 | } 51 | }, 52 | "markdown.preview.breaks": true, 53 | "markdownlint.config": { 54 | "default": true, 55 | "MD022": false, 56 | "MD024": false, 57 | "MD032": false 58 | }, 59 | // 60 | // Property Lists 61 | "[plist]": { 62 | "editor.formatOnSave": true, 63 | "editor.tabSize": 4, 64 | }, 65 | // 66 | // Shell 67 | "[shellscript]": { 68 | "editor.defaultFormatter": "foxundermoon.shell-format", 69 | }, 70 | // 71 | // Swift 72 | "[swift]": { 73 | "editor.detectIndentation": true, 74 | "editor.insertSpaces": false, 75 | "editor.tabSize": 4, 76 | }, 77 | // 78 | // YAML 79 | "[yaml]": { 80 | "editor.autoIndent": "keep", 81 | "editor.detectIndentation": true, 82 | "editor.insertSpaces": true, 83 | "editor.tabSize": 2, 84 | "editor.quickSuggestions": { 85 | "other": true, 86 | "comments": false, 87 | "strings": true 88 | } 89 | }, 90 | "[github-actions-workflow]": { 91 | "editor.autoIndent": "keep", 92 | "editor.detectIndentation": true, 93 | "editor.insertSpaces": true, 94 | "editor.tabSize": 2, 95 | "editor.quickSuggestions": { 96 | "other": true, 97 | "comments": false, 98 | "strings": true 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © Alta Software 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "RTP", 6 | platforms: [ 7 | .macOS(.v10_14), 8 | .iOS(.v12), 9 | ], 10 | products: [ 11 | .library( 12 | name: "RTP", 13 | targets: ["RTP"] 14 | ) 15 | ], 16 | dependencies: [], 17 | targets: [ 18 | .target( 19 | name: "RTP", 20 | dependencies: [] 21 | ), 22 | .testTarget( 23 | name: "RTPTests", 24 | dependencies: ["RTP"] 25 | ), 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift RTP 2 | 3 | Basic [RTP](https://en.wikipedia.org/wiki/Real-time_Transport_Protocol) (Real-time Transport protocol) in Swift. This package enables basic RTP packet encoding and decoding and transmission on Apple platforms (iOS, macOS, watchOS, tvOS). This was built for a now-defunct audio app for iOS and macOS, and runs reliably with multiple 48khz Opus audio channels over a typical 4G connection on modern iPhone devices. 4 | 5 | ## Installation 6 | 7 | Use Swift Package Manager to add this to your Xcode project or Swift package. 8 | 9 | ## Security 10 | 11 | **Note:** this package does *not* handle encryption or secure transmission of RTP data. It is up to consumers of this package to wrap the connection in some form of secure protocol using DTLS or QUIC. 12 | ## Author 13 | 14 | © Alta Software, LLC 15 | -------------------------------------------------------------------------------- /Sources/RTP/Connection.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Network) 2 | 3 | import Foundation 4 | import Network 5 | 6 | public class Connection { 7 | public typealias ReceiverBlock = (Packet) -> Void 8 | 9 | public static let defaultQueue = DispatchQueue(label: "rtp") 10 | 11 | var queue: DispatchQueue 12 | var receiverBlock: ReceiverBlock 13 | var conn: NWConnection 14 | 15 | public init(host: NWEndpoint.Host, port: NWEndpoint.Port, queue: DispatchQueue = Connection.defaultQueue, receiverBlock: @escaping ReceiverBlock) { 16 | print("Opening RTP connection to \(host):\(port)…") 17 | self.queue = queue 18 | self.receiverBlock = receiverBlock 19 | conn = NWConnection(host: host, port: port, using: .udp) 20 | } 21 | 22 | deinit { 23 | stop() 24 | } 25 | 26 | public func start() { 27 | print("Starting RTP connection to \(conn.endpoint)…") 28 | conn.start(queue: queue) 29 | receive() 30 | } 31 | 32 | public func stop() { 33 | conn.cancel() 34 | } 35 | 36 | public func send(_ packet: Packet) { 37 | let data = packet.encode() 38 | // print("Sending RTP packet: \(packet.ssrc) \(packet.sequenceNumber) to \(conn.endpoint)") 39 | conn.send(content: data, completion: .contentProcessed(contentProcessed)) 40 | } 41 | 42 | func contentProcessed(error: NWError?) { 43 | if let error = error { 44 | print("Error sending UDP packet: \(error)") 45 | } 46 | } 47 | 48 | func receive() { 49 | conn.receiveMessage { [weak self] data, _, _, error in 50 | if let error = error { 51 | print("Error receiving UDP packet: \(error)") 52 | return 53 | } 54 | else if let data = data { 55 | // print("⬇️ Received UDP packet of size: \(data.count)") 56 | 57 | do { 58 | let packet = try Packet(from: data) 59 | // print("🅿️ Parsed RTP packet: \(packet)") 60 | self?.receiverBlock(packet) 61 | } 62 | catch { 63 | print("Error handling RTP: \(error)") 64 | return 65 | } 66 | } 67 | self?.receive() 68 | } 69 | } 70 | } 71 | 72 | #endif 73 | -------------------------------------------------------------------------------- /Sources/RTP/Data+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Data { 4 | // big returns a big-endian integer of type T extracted from the bytes at the specified offset. 5 | func big(at offset: Int) -> T { 6 | var value: T = 0 7 | withUnsafeMutablePointer(to: &value) { 8 | self.copyBytes(to: UnsafeMutableBufferPointer(start: $0, count: 1), from: offset...size) 9 | } 10 | return T(bigEndian: value) 11 | } 12 | 13 | // big returns a little-endian integer of type T extracted from the bytes at the specified offset. 14 | func little(at offset: Int) -> T { 15 | var value: T = 0 16 | withUnsafeMutablePointer(to: &value) { 17 | self.copyBytes(to: UnsafeMutableBufferPointer(start: $0, count: 1), from: offset...size) 18 | } 19 | return T(littleEndian: value) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/RTP/FixedWidthInteger+Extensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension FixedWidthInteger { 4 | // random is a convenience function to generate a random value of the concrete type in [min,max] 5 | public static func random() -> Self { 6 | Self.random(in: .min ... .max) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/RTP/Packet.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // Packet represents an individual RTP packet. 4 | public struct Packet: Sendable { 5 | static let version: UInt8 = 2 6 | static let versionMask: UInt8 = 0b1100_0000 7 | static let paddingMask: UInt8 = 0b0010_0000 8 | static let extensionMask: UInt8 = 0b0001_0000 9 | static let csrcCountOffset = 0 10 | static let csrcCountMask: UInt8 = 0b0000_1111 11 | static let maxCSRCs = 15 12 | static let markerOffset = 1 13 | static let markerMask: UInt8 = 0b1000_0000 14 | static let payloadTypeOffset = 1 15 | static let payloadTypeMask: UInt8 = 0b0111_1111 16 | static let sequenceOffset = 2 17 | static let timestampOffset = 4 18 | static let ssrcOffset = 8 19 | static let csrcOffset = 12 20 | static let headerSize = csrcOffset 21 | 22 | public let payloadType: PayloadType 23 | public let marker: Bool 24 | public let sequenceNumber: SequenceNumber 25 | public let timestamp: Timestamp 26 | public let ssrc: SourceID 27 | public let csrcs: [SourceID]? 28 | public let `extension`: Extension? 29 | public let payload: Data 30 | public let padding: UInt8 31 | 32 | var payloadWithoutPadding: Data { 33 | payload[0...size 38 | let extensionSize = `extension`?.encodedSize ?? 0 39 | return Self.headerSize + csrcsSize + extensionSize + payload.count + Int(padding) 40 | } 41 | 42 | // TODO: add extension data 43 | init(_ sequencer: Sequencer, payload: Data) throws { 44 | try self.init( 45 | payloadType: sequencer.payloadType, 46 | payload: payload, 47 | ssrc: sequencer.ssrc, 48 | sequenceNumber: sequencer.sequenceNumber, 49 | timestamp: sequencer.timestamp 50 | ) 51 | } 52 | 53 | // TODO: add extension data 54 | public init(payloadType: PayloadType, payload: Data, ssrc: SourceID, sequenceNumber: SequenceNumber, timestamp: Timestamp, padding: UInt8 = 0, marker: Bool = false, csrcs: [SourceID]? = nil) throws { 55 | if let csrcs = csrcs { 56 | if csrcs.count > Self.maxCSRCs { 57 | throw EncodingError.tooManyCSRCs(csrcs.count) 58 | } 59 | } 60 | 61 | self.payloadType = payloadType 62 | self.marker = marker 63 | self.sequenceNumber = sequenceNumber 64 | self.timestamp = timestamp 65 | self.ssrc = ssrc 66 | self.csrcs = csrcs 67 | `extension` = nil 68 | self.payload = payload 69 | self.padding = padding 70 | } 71 | 72 | public init(from data: Data) throws { 73 | if data.count < Self.headerSize { 74 | throw EncodingError.dataTooSmall(Self.headerSize) 75 | } 76 | 77 | // Parse first octect (version, padding, extension) 78 | let version = (data[0] & Self.versionMask) >> 6 79 | if version != Self.version { 80 | throw EncodingError.unknownVersion(version) 81 | } 82 | let hasPadding = (data[0] & Self.paddingMask) != 0 83 | let hasExtension = (data[0] & Self.extensionMask) != 0 84 | let sizeWithPaddingAndExtension = Self.headerSize + (hasPadding ? 1 : 0) + (hasExtension ? Extension.headerSize : 0) 85 | 86 | // Parse second octet 87 | marker = (data[Self.markerOffset] & Self.markerMask) != 0 88 | let csrcCount = Int(data[Self.csrcCountOffset] & Self.csrcCountMask) 89 | let csrcSize = csrcCount * MemoryLayout.size 90 | let sizeWithCSRCs = sizeWithPaddingAndExtension + csrcSize 91 | if data.count < sizeWithCSRCs { 92 | throw EncodingError.dataTooSmall(sizeWithCSRCs) 93 | } 94 | payloadType = PayloadType(data[Self.payloadTypeOffset] & Self.payloadTypeMask) 95 | 96 | // Parse sequence number from octets 3-4 97 | sequenceNumber = data.big(at: Self.sequenceOffset) 98 | 99 | // Parse timestamp from octets 5-8 100 | timestamp = data.big(at: Self.timestampOffset) 101 | 102 | // Parse SSRC from octets 9-12 103 | ssrc = data.big(at: Self.ssrcOffset) 104 | 105 | // Parse optional CSRCs in octets 13+ 106 | if csrcCount > 0 { 107 | csrcs = (0.. Data { 129 | var data = Data(capacity: encodedSize) 130 | 131 | // Encode first octect (version, padding, extension) 132 | data.append(contentsOf: [(Self.version << 6 & Self.versionMask) | (padding > 0 ? Self.paddingMask : 0) | (`extension` != nil ? Self.extensionMask : 0) | (UInt8(csrcs?.count ?? 0) & Self.csrcCountMask)]) 133 | 134 | // Encode second octet 135 | data.append(contentsOf: [(marker ? Self.markerMask : 0) | (payloadType.rawValue & Self.payloadTypeMask)]) 136 | 137 | // Encode sequence number 138 | data.append(contentsOf: [UInt8(sequenceNumber >> 8 & 0xFF), UInt8(sequenceNumber & 0xFF)]) 139 | 140 | // Encode timestamp 141 | data.append(contentsOf: [UInt8(timestamp >> 24 & 0xFF), UInt8(timestamp >> 16 & 0xFF), UInt8(timestamp >> 8 & 0xFF), UInt8(timestamp & 0xFF)]) 142 | 143 | // Encode SSRC 144 | data.append(contentsOf: [UInt8(ssrc >> 24 & 0xFF), UInt8(ssrc >> 16 & 0xFF), UInt8(ssrc >> 8 & 0xFF), UInt8(ssrc & 0xFF)]) 145 | 146 | // Encode CSRCs 147 | if let csrcs = csrcs { 148 | for i in 0..> 24 & 0xFF), UInt8(csrcs[i] >> 16 & 0xFF), UInt8(csrcs[i] >> 8 & 0xFF), UInt8(csrcs[i] & 0xFF)]) 150 | } 151 | } 152 | 153 | // Encode extension 154 | if let ext = `extension` { 155 | data.append(contentsOf: [UInt8(ext.profileID >> 8 & 0xFF), UInt8(ext.profileID & 0xFF)]) 156 | let wordCount = ext.payload.count / MemoryLayout.size 157 | data.append(contentsOf: [UInt8(wordCount >> 8 & 0xFF), UInt8(wordCount & 0xFF)]) 158 | data.append(ext.payload) 159 | } 160 | 161 | // Encode payload 162 | data.append(payloadWithoutPadding) 163 | 164 | // Encode padding 165 | if padding > 0 { 166 | data.append(Data(count: Int(padding) - 1)) 167 | data.append(contentsOf: [padding]) 168 | } 169 | 170 | return data 171 | } 172 | } 173 | 174 | // Extension represents an RTP extension. 175 | public struct Extension: Sendable { 176 | public typealias ProfileID = UInt16 177 | 178 | static let headerSize = 4 179 | static let profileIDOffset = 0 180 | static let sizeOffset = 2 181 | 182 | public let profileID: ProfileID 183 | public let payload: Data 184 | 185 | public var encodedSize: Int { 186 | Self.headerSize + payload.count 187 | } 188 | 189 | public init(from data: Data) throws { 190 | if data.count < Self.headerSize { 191 | throw EncodingError.extensionDataTooSmall(Self.headerSize) 192 | } 193 | 194 | profileID = data.big(at: Self.profileIDOffset) 195 | let payloadSize = data.big(at: Self.sizeOffset) * MemoryLayout.size 196 | let size = Self.headerSize + payloadSize 197 | if data.count < size { 198 | throw EncodingError.extensionDataTooSmall(size) 199 | } 200 | 201 | payload = data[Self.headerSize.. Packet { 37 | let packet = try Packet(self, payload: payload) 38 | increment(samples) 39 | return packet 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/RTP/Types.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum EncodingError: Error { 4 | case malformedHeader 5 | case unsupportedPayloadType(_ payloadType: PayloadType) 6 | case unknownVersion(_ version: UInt8) 7 | case dataTooSmall(_ expected: Int) 8 | case extensionDataTooSmall(_ expected: Int) 9 | case paddingTooLarge(_ padding: UInt8) 10 | case tooManyCSRCs(_ count: Int) 11 | } 12 | 13 | public struct PayloadType: ExpressibleByIntegerLiteral, RawRepresentable, Equatable, Sendable { 14 | public typealias IntegerLiteralType = UInt8 15 | 16 | public static let marker: Self = 0b1000_0000 17 | public static let opus: Self = 111 18 | 19 | public var rawValue: IntegerLiteralType 20 | 21 | public init(integerLiteral value: IntegerLiteralType) { 22 | rawValue = value 23 | } 24 | 25 | public init?(rawValue: IntegerLiteralType) { 26 | self.rawValue = rawValue 27 | } 28 | 29 | public init(_ value: IntegerLiteralType) { 30 | self.init(integerLiteral: value) 31 | } 32 | } 33 | 34 | public typealias SourceID = UInt32 35 | public typealias SequenceNumber = UInt16 36 | public typealias Timestamp = UInt32 37 | -------------------------------------------------------------------------------- /Tests/RTPTests/ConnectionTests.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Network) 2 | 3 | import Network 4 | import XCTest 5 | 6 | @testable import RTP 7 | 8 | class RTPConnectionTests: XCTestCase { 9 | func testConnectionStop() { 10 | let conn = RTP.Connection(host: "localhost", port: 12345) { _ in } 11 | XCTAssertEqual(conn.conn.state, .setup) 12 | conn.start() 13 | sleep(1) 14 | XCTAssertEqual(conn.conn.state, .ready) 15 | conn.stop() 16 | sleep(1) 17 | XCTAssertEqual(conn.conn.state, .cancelled) 18 | conn.conn.restart() 19 | sleep(1) 20 | XCTAssertEqual(conn.conn.state, .cancelled) // NWConnection instances cannot be restarted once cancelled 21 | } 22 | } 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /Tests/RTPTests/PacketTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import RTP 4 | 5 | class PacketTests: XCTestCase { 6 | func testInitWithData() throws { 7 | let bytes: [UInt8] = [ 8 | 2 << 6, // Version 2, no marker or extension 9 | 111, // Opus payload type 10 | 0, 123, // Sequence number 123 11 | 0, 0, 0, 1, // Timestamp 1 12 | 0, 255, 255, 255, // SSRC 0x00FFFFFF 13 | ] 14 | 15 | let packet = try RTP.Packet(from: Data(bytes)) 16 | 17 | XCTAssertEqual(packet.payloadType, .opus) 18 | XCTAssertEqual(packet.sequenceNumber, 123) 19 | XCTAssertEqual(packet.timestamp, 1) 20 | XCTAssertEqual(packet.ssrc, 0x00FF_FFFF) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/RTPTests/PacketizerTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import RTP 4 | 5 | class PacketizerTests: XCTestCase { 6 | func testIncrementOverflow() { 7 | var packetizer = RTP.Packetizer(for: .opus, sequenceNumber: .max, timestamp: .max) 8 | 9 | packetizer.increment(48000) 10 | XCTAssertEqual(packetizer.sequenceNumber, 0) 11 | XCTAssertEqual(packetizer.timestamp, 48000 - 1) 12 | 13 | packetizer.increment(48000) 14 | XCTAssertEqual(packetizer.sequenceNumber, 1) 15 | XCTAssertEqual(packetizer.timestamp, 48000 * 2 - 1) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Tests/RTPTests/TypeTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @testable import RTP 4 | 5 | class TypeTests: XCTestCase { 6 | func testMemorySizes() { 7 | XCTAssertEqual(MemoryLayout.size, 1) 8 | XCTAssertEqual(MemoryLayout.size, 4) 9 | XCTAssertEqual(MemoryLayout.size, 2) 10 | XCTAssertEqual(MemoryLayout.size, 4) 11 | } 12 | 13 | func testPacketizerIncrementOverflow() { 14 | var packetizer = RTP.Packetizer(for: .opus, sequenceNumber: .max, timestamp: .max) 15 | 16 | packetizer.increment(48000) 17 | XCTAssertEqual(packetizer.sequenceNumber, 0) 18 | XCTAssertEqual(packetizer.timestamp, 48000 - 1) 19 | 20 | packetizer.increment(48000) 21 | XCTAssertEqual(packetizer.sequenceNumber, 1) 22 | XCTAssertEqual(packetizer.timestamp, 48000 * 2 - 1) 23 | } 24 | 25 | func testPacketInitWithData() throws { 26 | let bytes: [UInt8] = [ 27 | 2 << 6, // Version 2, no marker or extension 28 | 111, // Opus payload type 29 | 0, 123, // Sequence number 123 30 | 0, 0, 0, 1, // Timestamp 1 31 | 0, 255, 255, 255, // SSRC 0x00FFFFFF 32 | ] 33 | 34 | let packet = try RTP.Packet(from: Data(bytes)) 35 | 36 | XCTAssertEqual(packet.payloadType, .opus) 37 | XCTAssertEqual(packet.sequenceNumber, 123) 38 | XCTAssertEqual(packet.timestamp, 1) 39 | XCTAssertEqual(packet.ssrc, 0x00FF_FFFF) 40 | } 41 | } 42 | --------------------------------------------------------------------------------