diff --git a/CHANGES.md b/CHANGES.md index 7d85b7ef..39b6e475 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -46,9 +46,24 @@ - デフォルト値の `unspecified` の場合はシグナリングパラメータに `simulcast_request_rid` を含めない - role が sendrecv または recvonly の場合、かつ simulcast が true の場合にのみ有効 - @zztkm +- [ADD] サイマルキャストの rid を表す汎用型 `Rid` 列挙型を追加する + - @zztkm - [ADD] RPC 機能を追加する + - RPC メソッドを表す列挙型 `RPCMethod` を追加する + - `SignalingOffer` に以下の項目を追加する + - `rpcMethods: [String]?` + - `simulcastRpcRids: [Rid]?` を追加する - `MediaChannel` に `rpc` メソッドを追加する + - `MediaChannel` に以下の項目を追加する + - `rpcMethods: [RPCMethod]` + - `rpcSimulcastRids: [Rid]` - RPC メソッドを定義するための `RPCMethodProtocol` プロトコルを追加する + - `RPCMethodProtocol` に準拠した型を追加する + - `RequestSimulcastRid` + - `RequestSpotlightRid` + - `ResetSpotlightRid` + - `PutSignalingNotifyMetadata` + - `PutSignalingNotifyMetadataItem` - RPC の ID を表す `RPCID` 列挙型を追加する - `int(Int)` と `string(String)` の 2 つのケースをサポート - RPC エラー応答の詳細を表す `RPCErrorDetail` 構造体を追加する @@ -71,6 +86,9 @@ - [UPDATE] jazzy の設定ファイルを更新する - `module_version` を 2025.3.0 に変更 - @zztkm +- [ADD] `Package.swift` に `testTarget` を追加する + - xcodebuild で test を実行するために target を追加 + - @zztkm ## 2025.2.0 diff --git a/Makefile b/Makefile index a0f57d08..c541e050 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: fmt fmt-lint lint +.PHONY: build fmt fmt-lint lint # すべてを実行 all: fmt fmt-lint lint @@ -7,6 +7,19 @@ all: fmt fmt-lint lint fmt: swift format --in-place --recursive Sora SoraTests +# build +build: + xcodebuild \ + -scheme 'Sora' \ + -sdk iphoneos26.1 \ + -configuration Release \ + -derivedDataPath build \ + -destination 'generic/platform=iOS' \ + clean build \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGN_IDENTITY= \ + PROVISIONING_PROFILE= + # swift-format lint fmt-lint: swift format lint --strict --parallel --recursive Sora SoraTests diff --git a/Package.swift b/Package.swift index 3380993f..b30a6aa1 100644 --- a/Package.swift +++ b/Package.swift @@ -30,5 +30,10 @@ let package = Package( exclude: ["Info.plist"], resources: [.process("VideoView.xib")] ), + .testTarget( + name: "SoraTests", + dependencies: ["Sora"], + path: "SoraTests" + ), ] ) diff --git a/Sora/MediaChannel.swift b/Sora/MediaChannel.swift index 30bbf9e1..3f69dcfa 100644 --- a/Sora/MediaChannel.swift +++ b/Sora/MediaChannel.swift @@ -180,7 +180,7 @@ public final class MediaChannel { /// Sora サーバーから通知された、RPC で操作可能なサイマルキャスト rid が取得できます。 /// /// - Returns: 利用可能なサイマルキャスト rid の一覧。RPC が初期化されていない場合は空配列を返します - public var rpcSimulcastRids: [SimulcastRequestRid] { + public var rpcSimulcastRids: [Rid] { peerChannel.rpcChannel?.simulcastRpcRids ?? [] } diff --git a/Sora/PeerChannel.swift b/Sora/PeerChannel.swift index 05cfb99c..f96d313d 100644 --- a/Sora/PeerChannel.swift +++ b/Sora/PeerChannel.swift @@ -126,7 +126,7 @@ class PeerChannel: NSObject, RTCPeerConnectionDelegate { var switchedToDataChannel: Bool = false var signalingOfferMessageDataChannels: [[String: Any]] = [] var rpcAllowedMethods: [String] = [] - var rpcSimulcastRids: [SimulcastRequestRid] = [] + var rpcSimulcastRids: [Rid] = [] var rpcChannel: RPCChannel? weak var mediaChannel: MediaChannel? @@ -956,7 +956,7 @@ class PeerChannel: NSObject, RTCPeerConnectionDelegate { } if let simulcastRpcRids = offer.simulcastRpcRids { - rpcSimulcastRids = simulcastRpcRids.toSimulcastRequestRids() + rpcSimulcastRids = simulcastRpcRids } else { rpcSimulcastRids = [] } diff --git a/Sora/RPC.swift b/Sora/RPC.swift index dddb7f19..7e8bc916 100644 --- a/Sora/RPC.swift +++ b/Sora/RPC.swift @@ -72,10 +72,10 @@ public final class RPCChannel { private let allowedMethodNames: Set /// Sora から払い出されたサイマルキャスト rid の一覧 - let simulcastRpcRids: [SimulcastRequestRid] + let simulcastRpcRids: [Rid] init?( - dataChannel: DataChannel, rpcMethods: [String], simulcastRpcRids: [SimulcastRequestRid] + dataChannel: DataChannel, rpcMethods: [String], simulcastRpcRids: [Rid] ) { guard !rpcMethods.isEmpty else { return nil diff --git a/Sora/RPCTypes.swift b/Sora/RPCTypes.swift index 63b5100f..d3be9346 100644 --- a/Sora/RPCTypes.swift +++ b/Sora/RPCTypes.swift @@ -23,11 +23,11 @@ public protocol RPCMethodProtocol { static var name: String { get } } -public struct RequestSimulcastRidParams: Encodable { - public let rid: String +public struct RequestSimulcastRidParams: Codable { + public let rid: Rid public let senderConnectionId: String? - public init(rid: String, senderConnectionId: String? = nil) { + public init(rid: Rid, senderConnectionId: String? = nil) { self.rid = rid self.senderConnectionId = senderConnectionId } @@ -38,15 +38,15 @@ public struct RequestSimulcastRidParams: Encodable { } } -public struct RequestSpotlightRidParams: Encodable { +public struct RequestSpotlightRidParams: Codable { public let sendConnectionId: String? - public let spotlightFocusRid: String - public let spotlightUnfocusRid: String + public let spotlightFocusRid: Rid + public let spotlightUnfocusRid: Rid public init( sendConnectionId: String? = nil, - spotlightFocusRid: String, - spotlightUnfocusRid: String + spotlightFocusRid: Rid, + spotlightUnfocusRid: Rid ) { self.sendConnectionId = sendConnectionId self.spotlightFocusRid = spotlightFocusRid @@ -97,13 +97,13 @@ public struct PutSignalingNotifyMetadataItemParams: Encodable public struct RequestSimulcastRidResult: Decodable { public let channelId: String public let receiverConnectionId: String - public let rid: String + public let rid: Rid public let senderConnectionId: String? public init( channelId: String, receiverConnectionId: String, - rid: String, + rid: Rid, senderConnectionId: String? ) { self.channelId = channelId @@ -123,14 +123,14 @@ public struct RequestSimulcastRidResult: Decodable { public struct RequestSpotlightRidResult: Decodable { public let channelId: String public let recvConnectionId: String - public let spotlightFocusRid: String - public let spotlightUnfocusRid: String + public let spotlightFocusRid: Rid + public let spotlightUnfocusRid: Rid public init( channelId: String, recvConnectionId: String, - spotlightFocusRid: String, - spotlightUnfocusRid: String + spotlightFocusRid: Rid, + spotlightUnfocusRid: Rid ) { self.channelId = channelId self.recvConnectionId = recvConnectionId diff --git a/Sora/Signaling.swift b/Sora/Signaling.swift index 82dfeecb..39256548 100644 --- a/Sora/Signaling.swift +++ b/Sora/Signaling.swift @@ -479,7 +479,7 @@ public struct SignalingOffer { public var rpcMethods: [String]? /// RPC 経由で切り替えられるサイマルキャストの rid - public var simulcastRpcRids: [String]? + public var simulcastRpcRids: [Rid]? /// audio public let audio: Bool? @@ -1167,7 +1167,7 @@ extension SignalingOffer: Codable { videoCodecType = try container.decodeIfPresent(String.self, forKey: .video_codec_type) videoBitRate = try container.decodeIfPresent(Int.self, forKey: .video_bit_rate) rpcMethods = try container.decodeIfPresent([String].self, forKey: .rpc_methods) - simulcastRpcRids = try container.decodeIfPresent([String].self, forKey: .simulcast_rpc_rids) + simulcastRpcRids = try container.decodeIfPresent([Rid].self, forKey: .simulcast_rpc_rids) } public func encode(to encoder: Encoder) throws { @@ -1446,10 +1446,3 @@ extension SignalingClose: Decodable { reason = try container.decode(String.self, forKey: .reason) } } - -/// :nodoc: -extension Array where Element == String { - func toSimulcastRequestRids() -> [SimulcastRequestRid] { - compactMap { simulcastRequestRidTable.right(other: $0) } - } -} diff --git a/Sora/Types.swift b/Sora/Types.swift new file mode 100644 index 00000000..396e83e4 --- /dev/null +++ b/Sora/Types.swift @@ -0,0 +1,41 @@ +/// 映像の rid を表します。 +/// type: offer の simulcastRpcRids や RPC で利用される汎用 rid 型です。 +public enum Rid: Equatable { + /// 映像を受信しない + case none + + /// r0 + case r0 + + /// r1 + case r1 + + /// r2 + case r2 +} + +private var ridTable: PairTable = + PairTable( + name: "rid", + pairs: [ + ("none", .none), + ("r0", .r0), + ("r1", .r1), + ("r2", .r2), + ]) + +/// :nodoc: +extension Rid: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + guard let rid = ridTable.right(other: string) else { + throw SoraError.invalidSignalingMessage + } + self = rid + } + + public func encode(to encoder: Encoder) throws { + try ridTable.encode(self, to: encoder) + } +} diff --git a/SoraTests/RidTests.swift b/SoraTests/RidTests.swift new file mode 100644 index 00000000..cd912a0b --- /dev/null +++ b/SoraTests/RidTests.swift @@ -0,0 +1,33 @@ +import XCTest + +@testable import Sora + +class RidTests: XCTestCase { + func testRidEncodingAndDecoding() throws { + let cases: [(Rid, String)] = [ + (.none, "\"none\""), + (.r0, "\"r0\""), + (.r1, "\"r1\""), + (.r2, "\"r2\""), + ] + + for (rid, expectedJson) in cases { + // Encoding + let encoder = JSONEncoder() + let encodedData = try encoder.encode(rid) + let encodedJson = String(data: encodedData, encoding: .utf8) + XCTAssertEqual(encodedJson, expectedJson, "Failed encoding \(rid)") + + // Decoding + let decoder = JSONDecoder() + let decodedRid = try decoder.decode(Rid.self, from: expectedJson.data(using: .utf8)!) + XCTAssertEqual(decodedRid, rid, "Failed decoding \(expectedJson)") + } + } + + func testDecodeInvalidRidThrowsError() throws { + let decoder = JSONDecoder() + let data = "\"invalid\"".data(using: .utf8)! + XCTAssertThrowsError(try decoder.decode(Rid.self, from: data)) + } +} diff --git a/SoraTests/SignalingOfferTests.swift b/SoraTests/SignalingOfferTests.swift new file mode 100644 index 00000000..23508b37 --- /dev/null +++ b/SoraTests/SignalingOfferTests.swift @@ -0,0 +1,66 @@ +import XCTest + +@testable import Sora + +class SignalingOfferTests: XCTestCase { + func testDecodeSimulcastRpcRids() throws { + let testCases: [(simulcastRpcRids: [String], expectedRids: [Rid]?, shouldThrow: Bool)] = [ + (["r0", "r1", "r2"], [.r0, .r1, .r2], false), + (["none"], [.none], false), + ([], [], false), + (["invalid"], nil, true), + ] + + for (simulcastRpcRids, expectedRids, shouldThrow) in testCases { + let json: String + if simulcastRpcRids.isEmpty { + json = """ + { + "type": "offer", + "client_id": "client123", + "connection_id": "conn123", + "sdp": "v=0\\r\\no=- 1 1 IN IP4 127.0.0.1\\r\\n", + "simulcast_rpc_rids": [] + } + """ + } else { + let ridsJson = simulcastRpcRids.map { "\"\($0)\"" }.joined(separator: ", ") + json = """ + { + "type": "offer", + "client_id": "client123", + "connection_id": "conn123", + "sdp": "v=0\\r\\no=- 1 1 IN IP4 127.0.0.1\\r\\n", + "simulcast_rpc_rids": [\(ridsJson)] + } + """ + } + + let data = json.data(using: .utf8)! + let decoder = JSONDecoder() + + if shouldThrow { + XCTAssertThrowsError(try decoder.decode(SignalingOffer.self, from: data)) + } else { + let offer = try decoder.decode(SignalingOffer.self, from: data) + XCTAssertEqual(offer.simulcastRpcRids, expectedRids) + } + } + } + + func testDecodeSimulcastRpcRidsNotPresent() throws { + let json = """ + { + "type": "offer", + "client_id": "client123", + "connection_id": "conn123", + "sdp": "v=0\\r\\no=- 1 1 IN IP4 127.0.0.1\\r\\n" + } + """ + let data = json.data(using: .utf8)! + let decoder = JSONDecoder() + let offer = try decoder.decode(SignalingOffer.self, from: data) + + XCTAssertNil(offer.simulcastRpcRids) + } +}