From ca51a5e6067f1088e7cfb8410fa9fd0573785d12 Mon Sep 17 00:00:00 2001 From: Ilias Pavlidakis Date: Sun, 24 Nov 2024 12:25:52 +0200 Subject: [PATCH] SDP Parser implementation --- .../WebRTC/v2/SDP Parsing/README.md | 109 ++++++++++++++++++ .../WebRTC/v2/SDP Parsing/SDPParser.swift | 42 +++++++ .../v2/SDP Parsing/SupportedPrefix.swift | 22 ++++ .../SDP Parsing/Visitors/RTPMapVisitor.swift | 39 +++++++ .../SDP Parsing/Visitors/SDPLineVisitor.swift | 12 ++ .../WebRTC/v2/WebRTCJoinRequestFactory.swift | 15 ++- StreamVideo.xcodeproj/project.pbxproj | 32 +++++ 7 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 Sources/StreamVideo/WebRTC/v2/SDP Parsing/README.md create mode 100644 Sources/StreamVideo/WebRTC/v2/SDP Parsing/SDPParser.swift create mode 100644 Sources/StreamVideo/WebRTC/v2/SDP Parsing/SupportedPrefix.swift create mode 100644 Sources/StreamVideo/WebRTC/v2/SDP Parsing/Visitors/RTPMapVisitor.swift create mode 100644 Sources/StreamVideo/WebRTC/v2/SDP Parsing/Visitors/SDPLineVisitor.swift diff --git a/Sources/StreamVideo/WebRTC/v2/SDP Parsing/README.md b/Sources/StreamVideo/WebRTC/v2/SDP Parsing/README.md new file mode 100644 index 000000000..2038ab2ee --- /dev/null +++ b/Sources/StreamVideo/WebRTC/v2/SDP Parsing/README.md @@ -0,0 +1,109 @@ +# SDP Parser + +This repository contains an implementation of an SDP (Session Description Protocol) parser. The parser is designed using the Visitor pattern to process different types of SDP lines efficiently. + +## Architecture + +### Overview + +The SDP parser is built around the Visitor pattern, which allows for flexible and extensible processing of different SDP line types. The main components of the architecture are: + +- **SDPParser**: The main parser class that processes the SDP string and delegates line processing to registered visitors. +- **SDPLineVisitor**: A protocol that defines the interface for visitors that process specific SDP line prefixes. +- **SupportedPrefix**: An enumeration that defines the supported SDP line prefixes. + +### Visitor Pattern + +The Visitor pattern is used to separate the algorithm for processing SDP lines from the objects on which it operates. This allows for adding new processing logic without modifying the existing parser code. + +#### Components + +- **SDPParser**: The main parser that holds a list of visitors and delegates line processing to them. +- **SDPLineVisitor**: A protocol that visitors must conform to. Each visitor handles specific SDP line prefixes. +- **SupportedPrefix**: An enumeration that defines the supported prefixes and provides a method to check if a line has a supported prefix. + +#### Class Diagram + +```mermaid +classDiagram + class SDPParser { + - visitors: [SDPLineVisitor] + + registerVisitor(visitor: SDPLineVisitor) + + parse(sdp: String) async + } + + class SDPLineVisitor { + <> + + supportedPrefixes: [SupportedPrefix] + + visit(line: String) + } + + class SupportedPrefix { + <> + + isPrefixSupported(for line: String) -> SupportedPrefix + } + + SDPParser --> SDPLineVisitor : uses + SDPLineVisitor --> SupportedPrefix : uses +``` + +#### Data Flow + +- **SDP String Input**: The SDP string is provided to the SDPParser's parse method. +- **Line Splitting**: The SDP string is split into individual lines. +- **Prefix Checking**: Each line is checked to see if it has a supported prefix using the SupportedPrefix enumeration. +- **Visitor Delegation**: If a line has a supported prefix, the parser delegates the line processing to the registered visitors that support the prefix. +- **Line Processing**: Each visitor processes the line according to its specific logic. + +##### Sequence Diagram + +``` +sequenceDiagram + participant Client + participant SDPParser + participant SupportedPrefix + participant SDPLineVisitor + + Client->>SDPParser: parse(sdp: String) + SDPParser->>SDPParser: split(sdp, "\r\n") + loop for each line + SDPParser->>SupportedPrefix: isPrefixSupported(for: line) + alt supported prefix + SDPParser->>SDPLineVisitor: visit(line: String) + end + end +``` + +##### Performance Considerations + +The SDP parser is designed to be efficient and scalable. Key performance considerations include: + +- **Asynchronous Parsing**: The parse method is asynchronous, allowing for non-blocking parsing of large SDP strings. +- **Visitor Pattern**: The use of the Visitor pattern allows for efficient delegation of line processing, reducing the complexity of the parser. +- **Prefix Checking**: The SupportedPrefix enumeration provides a fast way to check if a line has a supported prefix, minimizing the overhead of line processing. + +#### Example Usage + +```swift +let parser = SDPParser() +let visitor = MySDPLineVisitor() +parser.registerVisitor(visitor) + +let sdpString = """ +v=0 +o=- 46117317 2 IN IP4 127.0.0.1 +s=- +c=IN IP4 127.0.0.1 +t=0 0 +a=rtpmap:96 opus/48000/2 +""" + +Task { + await parser.parse(sdp: sdpString) +} +``` + +In this example, MySDPLineVisitor is a custom visitor that conforms to the SDPLineVisitor protocol and processes lines with specific prefixes. + +#### Conclusion +The SDP parser is a flexible and efficient solution for processing SDP strings. The use of the Visitor pattern allows for easy extension and maintenance, while the asynchronous parsing ensures that the parser can handle large SDP strings without blocking the main thread. \ No newline at end of file diff --git a/Sources/StreamVideo/WebRTC/v2/SDP Parsing/SDPParser.swift b/Sources/StreamVideo/WebRTC/v2/SDP Parsing/SDPParser.swift new file mode 100644 index 000000000..5db395d17 --- /dev/null +++ b/Sources/StreamVideo/WebRTC/v2/SDP Parsing/SDPParser.swift @@ -0,0 +1,42 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import Foundation + +/// The main SDP parser that uses visitors to process lines. +final class SDPParser { + private var visitors: [SDPLineVisitor] = [] + + /// Registers a visitor for a specific SDP line prefix. + /// - Parameters: + /// - prefix: The line prefix to handle (e.g., "a=rtpmap"). + /// - visitor: The visitor that processes lines with the specified prefix. + func registerVisitor(_ visitor: SDPLineVisitor) { + visitors.append(visitor) + } + + /// Parses the provided SDP string asynchronously. + /// - Parameter sdp: The SDP string to parse. + func parse(sdp: String) async { + let lines = sdp.split(separator: "\r\n") + for line in lines { + let line = String(line) + let supportedPrefix = SupportedPrefix.isPrefixSupported(for: line) + guard + supportedPrefix != .unsupported + else { + continue + } + + visitors.forEach { + guard + $0.supportedPrefixes.contains(supportedPrefix) + else { + return + } + $0.visit(line: line) + } + } + } +} diff --git a/Sources/StreamVideo/WebRTC/v2/SDP Parsing/SupportedPrefix.swift b/Sources/StreamVideo/WebRTC/v2/SDP Parsing/SupportedPrefix.swift new file mode 100644 index 000000000..72ab64e45 --- /dev/null +++ b/Sources/StreamVideo/WebRTC/v2/SDP Parsing/SupportedPrefix.swift @@ -0,0 +1,22 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import Foundation + +enum SupportedPrefix: String, Hashable, CaseIterable { + case unsupported + case rtmap = "a=rtpmap:" + + static func isPrefixSupported(for line: String) -> SupportedPrefix { + guard + let supportedPrefix = SupportedPrefix + .allCases + .first(where: { $0 != .unsupported && line.hasPrefix($0.rawValue) }) + else { + return .unsupported + } + + return supportedPrefix + } +} diff --git a/Sources/StreamVideo/WebRTC/v2/SDP Parsing/Visitors/RTPMapVisitor.swift b/Sources/StreamVideo/WebRTC/v2/SDP Parsing/Visitors/RTPMapVisitor.swift new file mode 100644 index 000000000..823def952 --- /dev/null +++ b/Sources/StreamVideo/WebRTC/v2/SDP Parsing/Visitors/RTPMapVisitor.swift @@ -0,0 +1,39 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import Foundation + +/// A visitor for processing `a=rtpmap` lines. +final class RTPMapVisitor: SDPLineVisitor { + private var codecMap: [String: Int] = [:] + + var supportedPrefixes: Set = [.rtmap] + + func visit(line: String) { + // Parse the `a=rtpmap` line and extract codec information + let components = line + .replacingOccurrences(of: SupportedPrefix.rtmap.rawValue, with: "") + .split(separator: " ") + + guard + components.count == 2, + let payloadType = Int(components[0]) + else { + return + } + + let codecName = components[1] + .split(separator: "/") + .first? + .lowercased() ?? "" + codecMap[codecName] = payloadType + } + + /// Retrieves the payload type for a given codec name. + /// - Parameter codec: The codec name to search for. + /// - Returns: The payload type, or `nil` if not found. + func payloadType(for codec: VideoCodec) -> Int? { + codecMap[codec.rawValue.lowercased()] + } +} diff --git a/Sources/StreamVideo/WebRTC/v2/SDP Parsing/Visitors/SDPLineVisitor.swift b/Sources/StreamVideo/WebRTC/v2/SDP Parsing/Visitors/SDPLineVisitor.swift new file mode 100644 index 000000000..da69954c6 --- /dev/null +++ b/Sources/StreamVideo/WebRTC/v2/SDP Parsing/Visitors/SDPLineVisitor.swift @@ -0,0 +1,12 @@ +// +// Copyright © 2024 Stream.io Inc. All rights reserved. +// + +import Foundation + +protocol SDPLineVisitor { + + var supportedPrefixes: Set { get } + + func visit(line: String) +} diff --git a/Sources/StreamVideo/WebRTC/v2/WebRTCJoinRequestFactory.swift b/Sources/StreamVideo/WebRTC/v2/WebRTCJoinRequestFactory.swift index 99fe8cce1..59ee53fc7 100644 --- a/Sources/StreamVideo/WebRTC/v2/WebRTCJoinRequestFactory.swift +++ b/Sources/StreamVideo/WebRTC/v2/WebRTCJoinRequestFactory.swift @@ -55,7 +55,8 @@ struct WebRTCJoinRequestFactory { result.fastReconnect = connectionType.isFastReconnect result.token = await coordinator.stateAdapter.token result.preferredPublishOptions = await buildPreferredPublishOptions( - coordinator: coordinator + coordinator: coordinator, + publisherSdp: publisherSdp ) if let reconnectDetails = await buildReconnectDetails( for: connectionType, @@ -243,7 +244,8 @@ struct WebRTCJoinRequestFactory { } func buildPreferredPublishOptions( - coordinator: WebRTCCoordinator + coordinator: WebRTCCoordinator, + publisherSdp: String ) async -> [Stream_Video_Sfu_Models_PublishOption] { var result = [Stream_Video_Sfu_Models_PublishOption]() let videoOptions = await coordinator @@ -257,7 +259,7 @@ struct WebRTCJoinRequestFactory { } guard - let codec = coordinator + var codec = coordinator .stateAdapter .peerConnectionFactory .codecCapabilities(for: videoOptions.preferredVideoCodec) @@ -266,6 +268,13 @@ struct WebRTCJoinRequestFactory { return result } + let sdpParser = SDPParser() + let rtmapVisitor = RTPMapVisitor() + sdpParser.registerVisitor(rtmapVisitor) + await sdpParser.parse(sdp: publisherSdp) + let payloadType = rtmapVisitor.payloadType(for: videoOptions.preferredVideoCodec) ?? 0 + codec.payloadType = UInt32(payloadType) + result.append( .init( trackType: .video, diff --git a/StreamVideo.xcodeproj/project.pbxproj b/StreamVideo.xcodeproj/project.pbxproj index 4ae7461e6..ca07a7e82 100644 --- a/StreamVideo.xcodeproj/project.pbxproj +++ b/StreamVideo.xcodeproj/project.pbxproj @@ -189,6 +189,10 @@ 404C27CC2BF2552900DF2937 /* XCTestCase+PredicateFulfillment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 409CA7982BEE21720045F7AA /* XCTestCase+PredicateFulfillment.swift */; }; 404CAEE72B8F48F6007087BC /* DemoBackgroundEffectSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A0E95F2B88ABC80089E8D3 /* DemoBackgroundEffectSelector.swift */; }; 4059C3422AAF0CE40006928E /* DemoChatViewModel+Injection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4059C3412AAF0CE40006928E /* DemoChatViewModel+Injection.swift */; }; + 406128812CF32FEF007F5CDC /* SDPLineVisitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 406128802CF32FEF007F5CDC /* SDPLineVisitor.swift */; }; + 406128832CF33000007F5CDC /* SDPParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 406128822CF33000007F5CDC /* SDPParser.swift */; }; + 406128882CF33029007F5CDC /* RTPMapVisitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 406128872CF33029007F5CDC /* RTPMapVisitor.swift */; }; + 4061288B2CF33088007F5CDC /* SupportedPrefix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4061288A2CF33088007F5CDC /* SupportedPrefix.swift */; }; 4063033F2AD847EC0091AE77 /* CallState_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4063033E2AD847EC0091AE77 /* CallState_Tests.swift */; }; 406303422AD848000091AE77 /* CallParticipant_Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 406303412AD848000091AE77 /* CallParticipant_Mock.swift */; }; 406303462AD9432D0091AE77 /* GoogleSignInSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 406303442AD942ED0091AE77 /* GoogleSignInSwift */; }; @@ -1569,6 +1573,10 @@ 4049CE832BBBF8EF003D07D2 /* StreamAsyncImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamAsyncImage.swift; sourceTree = ""; }; 404A5CFA2AD5648100EF1C62 /* DemoChatModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoChatModifier.swift; sourceTree = ""; }; 4059C3412AAF0CE40006928E /* DemoChatViewModel+Injection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DemoChatViewModel+Injection.swift"; sourceTree = ""; }; + 406128802CF32FEF007F5CDC /* SDPLineVisitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDPLineVisitor.swift; sourceTree = ""; }; + 406128822CF33000007F5CDC /* SDPParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDPParser.swift; sourceTree = ""; }; + 406128872CF33029007F5CDC /* RTPMapVisitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTPMapVisitor.swift; sourceTree = ""; }; + 4061288A2CF33088007F5CDC /* SupportedPrefix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedPrefix.swift; sourceTree = ""; }; 4063033E2AD847EC0091AE77 /* CallState_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallState_Tests.swift; sourceTree = ""; }; 406303412AD848000091AE77 /* CallParticipant_Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallParticipant_Mock.swift; sourceTree = ""; }; 406583852B87694B00B4F979 /* BlurBackgroundVideoFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurBackgroundVideoFilter.swift; sourceTree = ""; }; @@ -3104,6 +3112,25 @@ path = AsyncImage; sourceTree = ""; }; + 4061287F2CF32FE4007F5CDC /* SDP Parsing */ = { + isa = PBXGroup; + children = ( + 406128842CF33018007F5CDC /* Visitors */, + 4061288A2CF33088007F5CDC /* SupportedPrefix.swift */, + 406128822CF33000007F5CDC /* SDPParser.swift */, + ); + path = "SDP Parsing"; + sourceTree = ""; + }; + 406128842CF33018007F5CDC /* Visitors */ = { + isa = PBXGroup; + children = ( + 406128802CF32FEF007F5CDC /* SDPLineVisitor.swift */, + 406128872CF33029007F5CDC /* RTPMapVisitor.swift */, + ); + path = Visitors; + sourceTree = ""; + }; 4063033D2AD847E60091AE77 /* CallState */ = { isa = PBXGroup; children = ( @@ -3784,6 +3811,7 @@ 40BBC4C12C637377002AEF92 /* v2 */ = { isa = PBXGroup; children = ( + 4061287F2CF32FE4007F5CDC /* SDP Parsing */, 40483CB72C9B1DEE00B4FCA8 /* WebRTCCoordinatorProviding.swift */, 40BBC4C52C638915002AEF92 /* WebRTCCoordinator.swift */, 40BBC4C22C6373C4002AEF92 /* WebRTCStateAdapter.swift */, @@ -6468,6 +6496,7 @@ 40BBC4D02C639054002AEF92 /* WebRTCCoordinator+Idle.swift in Sources */, 8456E6D3287EC343004E180E /* BaseLogDestination.swift in Sources */, 84DCA2142A38A428000C3411 /* CoordinatorModels.swift in Sources */, + 4061288B2CF33088007F5CDC /* SupportedPrefix.swift in Sources */, 40BBC4C02C629408002AEF92 /* RTCTemporaryPeerConnection.swift in Sources */, 84B0091B2A4C521100CF1FA7 /* Retries.swift in Sources */, 84DC38CD29ADFCFD00946713 /* SendEventRequest.swift in Sources */, @@ -6498,9 +6527,11 @@ 40BBC4C62C638915002AEF92 /* WebRTCCoordinator.swift in Sources */, 841BAA392BD15CDE000C73E4 /* UserSessionStats.swift in Sources */, 406B3BD72C8F332200FC93A1 /* RTCVideoTrack+Sendable.swift in Sources */, + 406128812CF32FEF007F5CDC /* SDPLineVisitor.swift in Sources */, 4067F3132CDA33C6002E28BD /* RTCAudioSessionConfiguration+Default.swift in Sources */, 8409465829AF4EEC007AF5BF /* SendReactionRequest.swift in Sources */, 40BBC4BA2C627F83002AEF92 /* TrackEvent.swift in Sources */, + 406128832CF33000007F5CDC /* SDPParser.swift in Sources */, 84B9A56D29112F39004DE31A /* EndpointConfig.swift in Sources */, 8469593829BB6B4E00134EA0 /* GetEdgesResponse.swift in Sources */, 40AB34AE2C5D02D400B5B6B3 /* SFUAdapter.swift in Sources */, @@ -6663,6 +6694,7 @@ 406B3C432C91E41400FC93A1 /* WebRTCAuthenticator.swift in Sources */, 84BAD77A2A6BFEF900733156 /* BroadcastBufferUploader.swift in Sources */, 40C4DF4B2C1C2C330035DBC2 /* ParticipantAutoLeavePolicy.swift in Sources */, + 406128882CF33029007F5CDC /* RTPMapVisitor.swift in Sources */, 84DC38A429ADFCFD00946713 /* BackstageSettings.swift in Sources */, 84BBF62D28AFC72700387A02 /* DefaultRTCMediaConstraints.swift in Sources */, 40BBC4A72C623D03002AEF92 /* StreamRTCPeerConnection+DelegatePublisher.swift in Sources */,