Conversation
|
got a majority of the required models in place now. working on voice gateway manager. |
There was a problem hiding this comment.
Pull request overview
This PR adds voice support to PaicordLib by introducing the DiscordVoice module with models for voice connections and the voice protocol implementation. The changes include new data structures for RTP packets, voice gateway events, and related payloads following Discord's voice connection protocol.
Changes:
- Added DiscordVoice module with voice protocol structures for RTP packets and voice gateway communication
- Introduced new dependencies (swift-sodium and swift-opus) for encryption and audio encoding
- Moved Gateway.Intent extension from Gateway.swift to VoiceGateway.swift
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| PaicordLib/Sources/PaicordLib/exports.swift | Exports the new DiscordVoice module |
| PaicordLib/Sources/DiscordVoice/DiscordVoice.swift | Main voice module file with imports and documentation reference |
| PaicordLib/Sources/DiscordModels/Types/VoiceUDP.swift | RTP packet structure with encoding logic for voice data transmission |
| PaicordLib/Sources/DiscordModels/Types/VoiceGateway.swift | Voice gateway opcodes, events, and Gateway.Intent extension |
| PaicordLib/Sources/DiscordModels/Types/VoiceGateway+Payloads.swift | Payload structures for voice gateway events (Identify, Ready, Speaking, etc.) |
| PaicordLib/Sources/DiscordModels/Protocols/UDPEncodable.swift | Protocol for UDP-encodable types |
| PaicordLib/Package.swift | Added DiscordVoice target and dependencies (swift-sodium, swift-opus) |
| PaicordLib/.swiftpm/xcode/xcuserdata/llsc12.xcuserdatad/xcschemes/xcschememanagement.plist | Xcode scheme management updates for new DiscordVoice scheme |
| Paicord.xcworkspace/xcshareddata/swiftpm/Package.resolved | Package dependency resolution with new swift-opus and swift-sodium packages |
| DiscordMarkdownParser/.swiftpm/xcode/xcuserdata/llsc12.xcuserdatad/xcschemes/xcschememanagement.plist | Xcode scheme order hint update |
| PaicordLib/Sources/DiscordModels/Types/Gateway.swift | Whitespace formatting changes and removal of Gateway.Intent extension (moved to VoiceGateway.swift) |
| /// Payload Binary data Encrypted audio/video data n bytes | ||
| /// | ||
| /// Discord expects a playout delay RTP extension header on every video packet. | ||
| public struct RTPPacket: Sendable { |
There was a problem hiding this comment.
The RTPPacket struct has an encode() method that matches the UDPEncodable protocol signature, but doesn't formally conform to the protocol. Consider adding : UDPEncodable to the struct declaration to make this relationship explicit and ensure consistency with the protocol definition.
| public struct RTPPacket: Sendable { | |
| public struct RTPPacket: Sendable, UDPEncodable { |
| case .clientFlags: return "clientFlags" | ||
| case .clientPlatform: return "clientPlatform" |
There was a problem hiding this comment.
Inconsistent indentation: lines 55-56 use extra indentation (8 spaces) compared to the other case statements in the switch (6 spaces). This should be aligned with the other cases for consistency.
| case .clientFlags: return "clientFlags" | |
| case .clientPlatform: return "clientPlatform" | |
| case .clientFlags: return "clientFlags" | |
| case .clientPlatform: return "clientPlatform" |
| // RFC5285 header | ||
| let profile = UInt16(0xBEDE).bigEndian | ||
| let lengthWords = UInt16(1).bigEndian | ||
|
|
||
| withUnsafeBytes(of: profile) { ext.append(contentsOf: $0) } | ||
| withUnsafeBytes(of: lengthWords) { ext.append(contentsOf: $0) } | ||
|
|
||
| // extension entry (id = 5, len = 2, 3 byte payload) | ||
| let id: UInt8 = 5 | ||
| let len: UInt8 = 2 // encoded as len-1 in RFC5285 |
There was a problem hiding this comment.
In the RTP extension header encoding, the "len" field is commented as "encoded as len-1 in RFC5285", but the actual value used is 2, which would represent a length of 3 bytes. However, the extension data payload is indeed 3 bytes (lines 99-101), plus 1 byte of padding. According to RFC5285, the length field should represent the number of 32-bit words, not bytes. The current value of 1 is correct for one 32-bit word (4 bytes total including the header byte), but the comment is misleading. The comment should clarify that lengthWords represents the number of 32-bit words in the extension, not the data length minus one.
| // RFC5285 header | |
| let profile = UInt16(0xBEDE).bigEndian | |
| let lengthWords = UInt16(1).bigEndian | |
| withUnsafeBytes(of: profile) { ext.append(contentsOf: $0) } | |
| withUnsafeBytes(of: lengthWords) { ext.append(contentsOf: $0) } | |
| // extension entry (id = 5, len = 2, 3 byte payload) | |
| let id: UInt8 = 5 | |
| let len: UInt8 = 2 // encoded as len-1 in RFC5285 | |
| // RFC5285 header: 0xBEDE profile, then length in 32-bit words | |
| let profile = UInt16(0xBEDE).bigEndian | |
| // lengthWords is the number of 32-bit words following this header. | |
| // Here: 1 word = 4 bytes total (1-byte extension header + 3-byte payload). | |
| let lengthWords = UInt16(1).bigEndian | |
| withUnsafeBytes(of: profile) { ext.append(contentsOf: $0) } | |
| withUnsafeBytes(of: lengthWords) { ext.append(contentsOf: $0) } | |
| // extension entry (id = 5, len = 2 -> 3-byte payload; len is encoded as data_length - 1 per RFC5285) | |
| let id: UInt8 = 5 | |
| let len: UInt8 = 2 |
| self.data = .clientConnect(try decodeData()) | ||
| case .video: | ||
| self.data = .video(try decodeData()) | ||
| case .clientDisconnect: |
There was a problem hiding this comment.
Trailing whitespace after the colon on line 178. This should be removed to maintain code cleanliness.
| case .clientDisconnect: | |
| case .clientDisconnect: |
| } | ||
|
|
||
| /// https://docs.discord.food/topics/voice-connections#voice-platform | ||
| public struct ClientPlatform: Sendable, Codable { |
There was a problem hiding this comment.
The struct ClientPlatform is empty and has no properties defined. Based on the documentation link, this struct should have properties for platform information. Either implement the struct with the required fields or add a TODO comment explaining what needs to be added.
| public struct ClientPlatform: Sendable, Codable { | |
| public struct ClientPlatform: Sendable, Codable { | |
| // TODO: Implement fields for client platform information (e.g. voice platform details) | |
| // as described in the Discord voice connections documentation linked above. |
| // MARK: + Gateway.Intent | ||
| extension Gateway.Intent { | ||
| /// All intents that require no privileges. | ||
| /// https://discord.com/developers/docs/topics/gateway#privileged-intents | ||
| public static var unprivileged: [Gateway.Intent] { | ||
| Gateway.Intent.allCases.filter { !$0.isPrivileged } | ||
| } | ||
|
|
||
| /// https://discord.com/developers/docs/topics/gateway#privileged-intents | ||
| public var isPrivileged: Bool { | ||
| switch self { | ||
| case .guilds: return false | ||
| case .guildMembers: return true | ||
| case .guildModeration: return false | ||
| case .guildEmojisAndStickers: return false | ||
| case .guildIntegrations: return false | ||
| case .guildWebhooks: return false | ||
| case .guildInvites: return false | ||
| case .guildVoiceStates: return false | ||
| case .guildPresences: return true | ||
| case .guildMessages: return false | ||
| case .guildMessageReactions: return false | ||
| case .guildMessageTyping: return false | ||
| case .directMessages: return false | ||
| case .directMessageReactions: return false | ||
| case .directMessageTyping: return false | ||
| case .messageContent: return true | ||
| case .guildScheduledEvents: return false | ||
| case .autoModerationConfiguration: return false | ||
| case .autoModerationExecution: return false | ||
| case .guildMessagePolls: return false | ||
| case .directMessagePolls: return false | ||
| /// Undocumented cases are considered privileged just to be safe than sorry | ||
| case .__undocumented: return true | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The exact same Gateway.Intent extension code (lines 254-290 in VoiceGateway.swift) has been moved from Gateway.swift (previously lines 831-867). This creates code duplication. The extension should only exist in one location. Since this is a Gateway.Intent extension, it should remain in Gateway.swift, not in VoiceGateway.swift.
| // MARK: + Gateway.Intent | |
| extension Gateway.Intent { | |
| /// All intents that require no privileges. | |
| /// https://discord.com/developers/docs/topics/gateway#privileged-intents | |
| public static var unprivileged: [Gateway.Intent] { | |
| Gateway.Intent.allCases.filter { !$0.isPrivileged } | |
| } | |
| /// https://discord.com/developers/docs/topics/gateway#privileged-intents | |
| public var isPrivileged: Bool { | |
| switch self { | |
| case .guilds: return false | |
| case .guildMembers: return true | |
| case .guildModeration: return false | |
| case .guildEmojisAndStickers: return false | |
| case .guildIntegrations: return false | |
| case .guildWebhooks: return false | |
| case .guildInvites: return false | |
| case .guildVoiceStates: return false | |
| case .guildPresences: return true | |
| case .guildMessages: return false | |
| case .guildMessageReactions: return false | |
| case .guildMessageTyping: return false | |
| case .directMessages: return false | |
| case .directMessageReactions: return false | |
| case .directMessageTyping: return false | |
| case .messageContent: return true | |
| case .guildScheduledEvents: return false | |
| case .autoModerationConfiguration: return false | |
| case .autoModerationExecution: return false | |
| case .guildMessagePolls: return false | |
| case .directMessagePolls: return false | |
| /// Undocumented cases are considered privileged just to be safe than sorry | |
| case .__undocumented: return true | |
| } | |
| } | |
| } | |
| // Gateway.Intent extension is defined in Gateway.swift to avoid duplication. |
| /// Version + Flags 1 Unsigned byte The RTP version and flags (always 0x80 for voice) 1 byte | ||
| /// Payload Type 2 Unsigned byte The type of payload (0x78 with the default Opus configuration) 1 byte | ||
| /// Sequence Unsigned short (big endian) The sequence number of the packet 2 bytes | ||
| /// Timestamp Unsigned integer (big endian) The RTC timestamp of the packet 4 bytes |
There was a problem hiding this comment.
Typo in comment: "RTC timestamp" should be "RTP timestamp" based on the context. RTC typically refers to Real-Time Clock, while RTP (Real-time Transport Protocol) is the correct protocol for this packet structure.
| /// Timestamp Unsigned integer (big endian) The RTC timestamp of the packet 4 bytes | |
| /// Timestamp Unsigned integer (big endian) The RTP timestamp of the packet 4 bytes |
| case aead_aes256_gcm | ||
| case xsalsa20_poly1305 | ||
| case xsalsa20_poly1305_suffix | ||
| case xsalsa20_poly1305_lit |
There was a problem hiding this comment.
Typo in the encryption mode name: "xsalsa20_poly1305_lit" should likely be "xsalsa20_poly1305_lite" based on the similar mode "xsalsa20_poly1305_lite_rtpsize" on line 148.
| case xsalsa20_poly1305_lit | |
| case xsalsa20_poly1305_lite |
| import Sodium | ||
| import Opus | ||
|
|
||
| /// https://docs.discord.food/topics/voice-connections#voice-data-interpolation |
There was a problem hiding this comment.
The file contains only imports and a documentation link comment, but no actual implementation. This appears to be an incomplete file that should either contain implementation code or be removed. The documentation link suggests there should be code related to voice data interpolation.
| /// https://docs.discord.food/topics/voice-connections#voice-data-interpolation | |
| /// https://docs.discord.food/topics/voice-connections#voice-data-interpolation | |
| /// | |
| /// This file provides basic utilities for interpolating missing voice data samples. | |
| /// The implementations below are intentionally minimal and self-contained, and can | |
| /// be extended to match Discord's voice data interpolation behaviour as needed. | |
| /// A helper for linearly interpolating missing PCM samples in Discord voice data. | |
| public struct DiscordVoiceInterpolator { | |
| /// Linearly interpolates a sequence of intermediate samples between `start` and `end`. | |
| /// | |
| /// - Parameters: | |
| /// - start: The starting sample value. | |
| /// - end: The ending sample value. | |
| /// - steps: The number of intermediate samples to generate. | |
| /// - Returns: An array of `steps` interpolated sample values. | |
| public static func interpolateSamples(start: Int16, end: Int16, steps: Int) -> [Int16] { | |
| guard steps > 0 else { return [] } | |
| let startValue = Double(start) | |
| let endValue = Double(end) | |
| let stepSize = (endValue - startValue) / Double(steps + 1) | |
| return (1...steps).map { index in | |
| let interpolated = startValue + Double(index) * stepSize | |
| let clamped = min(max(interpolated, Double(Int16.min)), Double(Int16.max)) | |
| return Int16(clamped.rounded()) | |
| } | |
| } | |
| /// Fills `nil` samples in a frame by interpolating between known neighbour samples. | |
| /// | |
| /// This assumes 16-bit PCM samples and performs simple linear interpolation. | |
| /// Edge segments (leading or trailing `nil` values) are filled by extending the | |
| /// nearest known sample. | |
| /// | |
| /// - Parameter frame: An array of optional `Int16` samples where `nil` indicates | |
| /// a missing sample. | |
| /// - Returns: An array of non-optional `Int16` samples with missing values filled. | |
| public static func interpolateFrame(_ frame: [Int16?]) -> [Int16] { | |
| guard !frame.isEmpty else { return [] } | |
| // First, locate indices of all non-nil "anchor" samples. | |
| var anchors: [(index: Int, value: Int16)] = [] | |
| for (idx, sample) in frame.enumerated() { | |
| if let s = sample { | |
| anchors.append((idx, s)) | |
| } | |
| } | |
| // If there are no known samples, just return zeros. | |
| if anchors.isEmpty { | |
| return Array(repeating: 0, count: frame.count) | |
| } | |
| var result = Array(repeating: Int16(0), count: frame.count) | |
| // Fill all known samples directly. | |
| for (idx, value) in anchors { | |
| result[idx] = value | |
| } | |
| // Fill leading `nil` segment by extending the first known value. | |
| let firstAnchor = anchors.first! | |
| if firstAnchor.index > 0 { | |
| for idx in 0..<firstAnchor.index { | |
| result[idx] = firstAnchor.value | |
| } | |
| } | |
| // Fill segments between anchors using linear interpolation. | |
| for windowIndex in 0..<(anchors.count - 1) { | |
| let current = anchors[windowIndex] | |
| let next = anchors[windowIndex + 1] | |
| let gap = next.index - current.index - 1 | |
| guard gap > 0 else { continue } | |
| let interpolated = interpolateSamples( | |
| start: current.value, | |
| end: next.value, | |
| steps: gap | |
| ) | |
| for offset in 0..<gap { | |
| result[current.index + 1 + offset] = interpolated[offset] | |
| } | |
| } | |
| // Fill trailing `nil` segment by extending the last known value. | |
| let lastAnchor = anchors.last! | |
| if lastAnchor.index < frame.count - 1 { | |
| for idx in (lastAnchor.index + 1)..<frame.count { | |
| result[idx] = lastAnchor.value | |
| } | |
| } | |
| return result | |
| } | |
| } |
| .package( | ||
| url: "https://github.com/alta/swift-opus.git", | ||
| branch: "main" |
There was a problem hiding this comment.
The swift-opus dependency is pinned only to the mutable main branch, which makes your build pipeline dependent on whatever code is currently published there. If that repository is compromised or publishes a malicious change to main, the malicious code will be automatically fetched and executed with the same privileges as your application (e.g., access to tokens and user data). Pin this dependency to an immutable identifier (a specific released version or commit hash) instead of a branch name to reduce supply-chain compromise risk.
commit b51568d Author: Lakhan Lothiyi <llsc12.dev@gmail.com> Date: Sun Feb 22 11:53:44 2026 +0000 workflow updates
|
/release |
|
🔧 /release build requested by @llsc12 Starting a Release-optimized PR build for commit |
| case .noConnection, .connecting, .configured: | ||
| switch message.payload.opcode.isSentForConnectionEstablishment { | ||
| case true: | ||
| break | ||
| case false: | ||
| /// Recursively try to send through the queue. | ||
| /// The send queue has slowdown mechanisms so it's fine. | ||
| self.send(message: message) | ||
| return | ||
| } |
There was a problem hiding this comment.
The send(message:) path recursively re-enqueues non-establishment messages while state is .noConnection/.connecting/.configured. If the state never reaches .connected (currently onSuccessfulConnection() is unused), this can create an unbounded stream of queued tasks and log spam. Consider buffering until connected with a bounded queue/timeout, or failing fast with an error to the caller.
| print( | ||
| "Received an opcode \(opcode.description) that is supposed to be binary, but it came as JSON." | ||
| ) | ||
| self.data = .none | ||
| break |
There was a problem hiding this comment.
VoiceGateway.Event.init(from:) uses print(...) during decoding when a binary-only opcode arrives as JSON. This introduces unwanted stdout output from a core model type. Prefer throwing a decoding error (or routing through the library logger) so callers can handle the issue deterministically.
| print( | |
| "Received an opcode \(opcode.description) that is supposed to be binary, but it came as JSON." | |
| ) | |
| self.data = .none | |
| break | |
| throw DecodingError.dataCorrupted( | |
| DecodingError.Context( | |
| codingPath: decoder.codingPath, | |
| debugDescription: | |
| "Received an opcode \(opcode.description) that is supposed to be binary-only, but it was decoded from JSON." | |
| ) | |
| ) |
| default: | ||
| fatalError( | ||
| "[Voice Crypto] Unsupported deprecated encryption mode: \(self)" | ||
| ) | ||
| } |
There was a problem hiding this comment.
EncryptionMode.decrypt crashes the process via fatalError for any mode outside the two supported ones. Since the mode can be server-selected, this risks a runtime crash if Discord negotiates a deprecated/unknown mode. Prefer returning nil or throwing an error, and let higher layers decide whether to disconnect/fallback.
| internal var cookies: [String: Cookie] = [:] | ||
|
|
There was a problem hiding this comment.
CookieStore.cookies was changed from private to internal, which exposes mutable cookie storage across the module. If this is only needed for tests, prefer keeping storage private and adding a test-only accessor (e.g. #if DEBUG/@testable extension) or a read-only snapshot API.
| internal var cookies: [String: Cookie] = [:] | |
| private var cookies: [String: Cookie] = [:] | |
| /// Read-only snapshot of all stored cookies. | |
| /// This exposes state without allowing external mutation of the underlying storage. | |
| @usableFromInline | |
| internal var cookieSnapshot: [String: Cookie] { | |
| cookies | |
| } |
| <key>DiscordVoice.xcscheme_^#shared#^_</key> | ||
| <dict> | ||
| <key>orderHint</key> | ||
| <integer>3</integer> | ||
| </dict> |
There was a problem hiding this comment.
.swiftpm/xcode/xcuserdata/... is user-specific Xcode state and typically shouldn’t be committed (it causes noisy diffs and conflicts across developers/CI). Consider adding **/xcuserdata/ to .gitignore and removing this file from the repo unless there’s a strong reason to keep it.
| let socketAddress = try SocketAddress(ipAddress: host, port: port) | ||
| let server = try await DatagramBootstrap( | ||
| group: NIOSingletons.posixEventLoopGroup | ||
| ) | ||
| .bind(to: socketAddress) | ||
| .flatMapThrowing { channel in |
There was a problem hiding this comment.
VoiceConnection.connect binds the UDP socket to the remote voice server address (bind(to: socketAddress)), which is a local bind and will typically fail unless that IP is assigned locally. For a UDP client you generally need to bind to a local/ephemeral address (e.g. 0.0.0.0:0) and send to the remote via AddressedEnvelope.remoteAddress.
| switch event.data { | ||
| case .hello(let payload): | ||
| self.setupPingTask( | ||
| forConnectionWithId: self.connectionId.load(ordering: .relaxed), | ||
| every: .milliseconds(payload.heartbeat_interval) | ||
| ) | ||
| case .ready(let payload): | ||
| setupUDP(payload) |
There was a problem hiding this comment.
sendResumeOrIdentify() is never called, so after connecting the voice WS the client never sends Identify/Resume. In processEvent the .hello case should trigger Identify/Resume (and then start the heartbeat timer); otherwise the connection will stay in .configured and subsequent non-establishment sends will loop/retry indefinitely.
| ) | ||
| let opcode = Gateway.Opcode.identify | ||
| self.send( | ||
| message: .init( | ||
| payload: resume, | ||
| opcode: .init(encodedWebSocketOpcode: opcode.rawValue)! | ||
| ) | ||
| ) |
There was a problem hiding this comment.
sendResume() derives a WebSocketOpcode from Gateway.Opcode.identify and force-unwraps it. This will send the resume payload with the wrong WebSocket opcode (likely .binary), and can also crash if the mapping fails. Resume/Identify payloads should be sent as WebSocket .text frames.
| self.logger.debug( | ||
| "Decoded binary event", | ||
| metadata: [ | ||
| "event": .string("\(event)"), | ||
| "opcode": .string(event.opcode.description), | ||
| ] | ||
| ) | ||
| return event | ||
| } else { | ||
| let event = try DiscordGlobalConfiguration.decoder.decode( | ||
| VoiceGateway.Event.self, | ||
| from: Data(buffer: buffer, byteTransferStrategy: .noCopy) | ||
| ) | ||
| self.logger.debug( | ||
| "Decoded event", | ||
| metadata: [ | ||
| "event": .string("\(event)"), | ||
| "opcode": .string(event.opcode.description), | ||
| ] | ||
| ) |
There was a problem hiding this comment.
Decoded voice gateway events are logged with the entire VoiceGateway.Event object in tryDecodeBufferAsEvent, including sensitive fields like SessionDescription.secretKey and MLS key material. This can leak ephemeral encryption keys to logs, allowing anyone with log access and captured traffic to decrypt voice streams. Avoid logging full event payloads and instead log only non-sensitive metadata (e.g., opcode, IDs) or explicitly redact key/MLS fields before logging.
PaicordLib/Package.swift
Outdated
| .package( | ||
| url: "https://github.com/alta/swift-opus.git", | ||
| branch: "main" | ||
| ), |
There was a problem hiding this comment.
The SwiftPM dependency on https://github.com/alta/swift-opus.git is pinned to the mutable main branch rather than an immutable tag or commit. If that upstream repository or branch is compromised, new builds will automatically pull and execute attacker-controlled code in your voice/audio pipeline. Pin this dependency to a specific version or commit SHA and only update it intentionally after reviewing upstream changes.
| await gw.voice.updateVoiceConnection( | ||
| .join( | ||
| channelId: channel.id, | ||
| guildId: channel.guild_id! |
There was a problem hiding this comment.
The force unwrap on channel.guild_id! could cause a crash if guild_id is nil. Voice channels can exist in DMs or group DMs where guild_id would be nil. Add a guard statement to handle this case properly or verify that this button is only shown for guild voice channels.
| await gw.voice.updateVoiceConnection( | |
| .join( | |
| channelId: channel.id, | |
| guildId: channel.guild_id! | |
| guard let guildId = channel.guild_id else { return } | |
| await gw.voice.updateVoiceConnection( | |
| .join( | |
| channelId: channel.id, | |
| guildId: guildId |
| private func buildEncryptedPacket( | ||
| rtpHeader: Data, | ||
| ciphertext: Data, | ||
| tag: Data, | ||
| nonceSuffix: Data | ||
| ) -> ByteBuffer { | ||
|
|
||
| var buffer = ByteBuffer() | ||
| buffer.writeBytes(rtpHeader) | ||
| buffer.writeBytes(ciphertext) | ||
| buffer.writeBytes(tag) | ||
| buffer.writeBytes(nonceSuffix) | ||
|
|
||
| return buffer | ||
| } |
There was a problem hiding this comment.
The function buildEncryptedPacket is defined but never used in this file. Consider removing it if it's not needed, or ensure it's being called where encryption is needed. If it's intended for future use, consider making it public or package-level with appropriate documentation.
| var nonceSuffixValue = UInt32.random(in: .min ... .max) | ||
| var beNonceSuffix = nonceSuffixValue.bigEndian |
There was a problem hiding this comment.
The variable nonceSuffixValue is declared but its value is immediately converted and not used again. Consider removing the intermediate variable and directly using UInt32.random(in: .min ... .max).bigEndian for clarity and to reduce unnecessary allocations.
| import struct NIOWebSocket.WebSocketOpcode | ||
|
|
||
| public actor UserGatewayManager: GatewayManager { | ||
| public actor UserGatewayManager { |
There was a problem hiding this comment.
The UserGatewayManager no longer conforms to the GatewayManager protocol. This is a breaking API change that may affect existing code that expects UserGatewayManager to implement the GatewayManager protocol interface. Ensure this change is documented and all dependent code has been updated.
| public actor UserGatewayManager { | |
| public actor UserGatewayManager: GatewayManager { |
| let b64encoded = [ | ||
| "AAEZQEEEoNRlRXBSfG9sxtr5jJxzVcUuTfJslWTSDfqcEtPF8pnyvue1yLTrW24vmka5hnjp67V0c+0wPu5jYTTrJWEmAQABAQA=", | ||
| "AAUbAEHwAAEAAQgUFa0c+EQQpQAAAAAAAAAAAgAAAAAAAgABAAEAAkBBBA6ffGO2L8WWMuAQ++A1Guoy24snd0uvi1PRIuJx+4dH6PMtRIfTH+ziN72vzIxctvdpozvYNHu67LyCakgni09AQQTWjD7BWWBExpHTTyoBuq1CF6wIIvSPKBMCYZoepq6kqKubOdIN/wLSlkZd0U118EVDVOMkt7+he3037GO6L0GjQEEEHjylB9FDUAK/ne9bxgQmSS8NdZy59gA4XrjkF4n1BQvbzepPUPln+KlzPUSJ9HazjqDXl19lUWG8YIxtS80K9wABCAVLf4aFQAAAAgABAgACAAACAAEBAAAAAAAAAAD//////////wBARzBFAiEAgAIMGdDd9QJBg489IMOK5grxwnrufTKc2kxwjPx+cgMCIAyukS8MJ9ifqTghaV6WPWTNenyK7W26KIbwpKu9RZhjAEBHMEUCIQCZpnz5onf5GTSD9EiQ4BU0iqZ5+017ntaXxZABk8f20AIgLkH7plqgAhSMUj3CWi7LM1wQJGAywVGlbJpV80In4GBASDBGAiEAz5mrzAcKQDdqgVEMNkUr53PM+Iki2TxZzsars+cAMwoCIQDjN91qFSm7pO8rbHiBLeod/tpwpKpsWk0QbzHpGHw/fw==", | ||
| "AAYdAAAAAQABCBQVrRz4RBClAAAAAAAAAAABAAAAAAADIgIg+vGT+WKMUdMhzDRQuUmb7m8ucQBmF7Q8BhrtHiHlhiEAQEYwRAIgQXho5VoNJ9QZOPlcrr1cxQMUNEaUwgYyoUEyaJjqKh0CICWeFzuXJyFWqcJCs7rK21oz9Vu4zLKh8gFtQa0jYke/IGXRtM8sLvaABUM6F7GckyemC6gvCXr+0pfHdaE5EAF+IFehvFBjnOgQyENbJbqs/fHjXOTDSgJg+EMKijIYlUQv" | ||
| ] |
There was a problem hiding this comment.
The test uses hardcoded base64-encoded binary data without explaining what these payloads represent or where they come from. Add comments describing what each test payload represents (e.g., "voice ready event", "session description", etc.) to make the test more maintainable and understandable.
| SDKROOT = auto; | ||
| SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; | ||
| SWIFT_EMIT_LOC_STRINGS = YES; | ||
| SWIFT_OBJC_INTEROP_MODE = objcxx; |
There was a problem hiding this comment.
Setting SWIFT_OBJC_INTEROP_MODE to objcxx enables C++ interoperability which is required for some dependencies (likely DaveKit). However, this is a significant compiler mode change that affects the entire project. Ensure this is intentional and documented, as it may have implications for build times and compatibility with other dependencies.
| SWIFT_OBJC_INTEROP_MODE = objcxx; | |
| /* SWIFT_OBJC_INTEROP_MODE not forced to objcxx here; use default (objc) to avoid project-wide ObjC++ mode. */ |
| init() { | ||
| self.opusEncoder = try! Opus.Encoder(format: Self.opusFormat, application: .voip) | ||
| self.opusDecoder = try! Opus.Decoder(format: Self.opusFormat, application: .voip) |
There was a problem hiding this comment.
The force unwrap operator on try! could cause a crash if Opus encoder initialization fails. Consider using proper error handling with do-catch or returning an optional/result type from the initializer to handle initialization failures gracefully.
| init() { | |
| self.opusEncoder = try! Opus.Encoder(format: Self.opusFormat, application: .voip) | |
| self.opusDecoder = try! Opus.Decoder(format: Self.opusFormat, application: .voip) | |
| init() throws { | |
| self.opusEncoder = try Opus.Encoder(format: Self.opusFormat, application: .voip) | |
| self.opusDecoder = try Opus.Decoder(format: Self.opusFormat, application: .voip) |
|
really awesome! just need to add a few more things:
i had some more stuff to add to this but i forgot |
need to switch to using ready supplemental
|
/release |
|
🔧 /release build requested by @llsc12 Starting a Release-optimized PR build for commit |
This adds voice support to PaicordLib, with all models and voice protocol in place. Also partial support in Paicord.