diff --git a/CHANGES.md b/CHANGES.md index d3269ef7..3ecfc2a9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,68 @@ - FIX - バグ修正 +## 2.4.0 + +### CHANGE + +- システム条件を更新した + + - Xcode 10.3 + +- WebRTC M76 に対応した + +- サイマルキャスト機能に対応した + +- スポットライト機能に対応した + +- VAD 機能を削除した + +- 音声ビットレートの指定に対応した + +- シグナリングのメタデータに対応した + +- API: `Configuration`: `audioBitRate` プロパティを追加した + +- API: `Configuration`: `maxNumberOfSpeakers` プロパティを削除した + +- API: `Configuration`: `simulcastEnabled` プロパティを追加した + +- API: `Configuration`: `simulcastQuality` プロパティを追加した + +- API: `Configuration`: `spotlight` プロパティを追加した + +- API: `SimulcastQuality`: 追加した + +- API: シグナリングに関する API の名前を変更した + + - `SignalingMessage` -> `Signaling` + - `SignalingNotificationEventType` -> `SignalingNotifyEventType` + - `SignalingConnectMessage` -> `SignalingConnect` + - `SignalingOfferMessage` -> `SignalingOffer` + - `SignalingOfferMessage.Configuration` -> `SignalingOffer.Configuration` + - `SignalingPongMessage` -> `SignalingPong` + - `SignalingPushMessage` -> `SignalingPush` + +- API: `SignalingAnswer`: 追加した + +- API: `SignalingCandidate`: 追加した + +- API: `SignalingClientMetadata`: 追加した + +- API: `SignalingMetadata`: 追加した + +- API: `SignalingNotifyConnection`: 追加した + +- API: `SignalingNotifyNetworkStatus`: 追加した + +- API: `SignalingNotifySpotlightChanged`: 追加した + +- API: `SignalingOffer.Encoding`: 追加した + +- API: `SignalingUpdate`: 追加した + +- API: `Signaling`: 追加した + ## 2.3.2 ### CHANGE diff --git a/Cartfile b/Cartfile index c5b31d98..bc87f9e6 100644 --- a/Cartfile +++ b/Cartfile @@ -1,2 +1,2 @@ -github "shiguredo/sora-webrtc-ios" "66.8.0" +github "shiguredo/sora-webrtc-ios" "76.3.0" github "shiguredo/SocketRocket" "0.5.1-carthage.1" diff --git a/Cartfile.resolved b/Cartfile.resolved index 987a4594..10ed5838 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,2 +1,2 @@ github "shiguredo/SocketRocket" "0.5.1-carthage.1" -github "shiguredo/sora-webrtc-ios" "66.8.0" +github "shiguredo/sora-webrtc-ios" "76.3.0" diff --git a/README.md b/README.md index 51695bc2..4e93a47d 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Sora iOS SDK に対する有償のサポートについては現在提供して - iOS 10.0 以降 - アーキテクチャ arm64, armv7 (シミュレーターは非対応) - macOS 10.14.4 以降 -- Xcode 10.2 +- Xcode 10.3 - Swift 5.0 - Carthage 0.33.0 以降、または CocoaPods 1.6.1 以降 - WebRTC SFU Sora 19.04.0 以降 diff --git a/Sora.podspec b/Sora.podspec index 4fa7f8f4..e2bf26a7 100644 --- a/Sora.podspec +++ b/Sora.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Sora" - s.version = "2.3.2" + s.version = "2.4.0" s.summary = "Sora iOS SDK" s.description = <<-DESC A library to develop Sora client applications. @@ -13,6 +13,6 @@ Pod::Spec.new do |s| s.source_files = "Carthage/Build/iOS/Sora.framework/Headers/*.h" s.frameworks = "SocketRocket" s.vendored_frameworks = "Carthage/Build/iOS/Sora.framework" - s.dependency "WebRTC", "66.8.0" + s.dependency "WebRTC", "76.3.0" s.dependency "SocketRocket", "0.5.1" end diff --git a/Sora.xcodeproj/project.pbxproj b/Sora.xcodeproj/project.pbxproj index d645cd7f..83e1a776 100644 --- a/Sora.xcodeproj/project.pbxproj +++ b/Sora.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 910EABE61F4FF88B00D81213 /* NativePeerChannelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 910EABE51F4FF88B00D81213 /* NativePeerChannelFactory.swift */; }; 9128A4A91F2A6CB100AC6B1E /* MediaStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9128A4A81F2A6CB100AC6B1E /* MediaStream.swift */; }; 9138B4D01E655728006A76FB /* WebRTCInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9138B4CF1E655728006A76FB /* WebRTCInfo.swift */; }; + 913C895A226EFC61001B2569 /* Signaling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 913C8959226EFC61001B2569 /* Signaling.swift */; }; 9141AB641F4204C0007C4D1C /* ConnectionTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9141AB631F4204C0007C4D1C /* ConnectionTimer.swift */; }; 9145A5901F0CB093002D6EC6 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9145A58F1F0CB093002D6EC6 /* Utilities.swift */; }; 915344121FE96F750083762B /* SoraError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 915344111FE96F750083762B /* SoraError.swift */; }; @@ -20,7 +21,6 @@ 915CEC901F821A90006E45E2 /* CameraVideoCapturer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 915CEC8F1F821A90006E45E2 /* CameraVideoCapturer.swift */; }; 916134DD1F7B5EFF00ABDDAF /* AspectRatio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9174A88E1F7395CA00D586C4 /* AspectRatio.swift */; }; 91629BF21F8E5099001193D0 /* Array+Base.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91629BF11F8E5098001193D0 /* Array+Base.swift */; }; - 9163401B1F74F1F300303516 /* RTCPeerConnection+SessionDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9163401A1F74F1F300303516 /* RTCPeerConnection+SessionDescription.swift */; }; 91672C791F78F7FC002300E7 /* WebRTCConfigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91672C781F78F7FC002300E7 /* WebRTCConfigration.swift */; }; 916BBF721F19EF5800846166 /* ICECandidate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 916BBF711F19EF5800846166 /* ICECandidate.swift */; }; 91705B801DED66D300D79306 /* WebRTC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 918201901D58668E00178E2B /* WebRTC.framework */; }; @@ -48,7 +48,6 @@ 91C7B0991D54636A006F5FA2 /* SoraTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91C7B0981D54636A006F5FA2 /* SoraTests.swift */; }; 91CD2A4A1F288A6A00D039D1 /* Sora.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91CD2A491F288A6A00D039D1 /* Sora.swift */; }; 91FA6F211D93CA9800D38DB4 /* VideoFrame.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91FA6F201D93CA9800D38DB4 /* VideoFrame.swift */; }; - 91FAB3EC1F29130700EDF53C /* SignalingMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91FAB3EB1F29130700EDF53C /* SignalingMessage.swift */; }; 91FD95751DCA06F700047BA9 /* RTC+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91FD95741DCA06F700047BA9 /* RTC+Description.swift */; }; C5D3C7421F7CEB18004660F5 /* VideoCapturerDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5D3C7411F7CEB18004660F5 /* VideoCapturerDevice.swift */; }; /* End PBXBuildFile section */ @@ -82,13 +81,13 @@ 910EABE51F4FF88B00D81213 /* NativePeerChannelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePeerChannelFactory.swift; sourceTree = ""; }; 9128A4A81F2A6CB100AC6B1E /* MediaStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaStream.swift; sourceTree = ""; }; 9138B4CF1E655728006A76FB /* WebRTCInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebRTCInfo.swift; sourceTree = ""; }; + 913C8959226EFC61001B2569 /* Signaling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signaling.swift; sourceTree = ""; }; 9141AB631F4204C0007C4D1C /* ConnectionTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionTimer.swift; sourceTree = ""; }; 9145A58F1F0CB093002D6EC6 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; 915344111FE96F750083762B /* SoraError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoraError.swift; sourceTree = ""; }; 91554F421F179CFD00403C39 /* WebSocketChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebSocketChannel.swift; sourceTree = ""; }; 915CEC8F1F821A90006E45E2 /* CameraVideoCapturer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraVideoCapturer.swift; sourceTree = ""; }; 91629BF11F8E5098001193D0 /* Array+Base.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Base.swift"; sourceTree = ""; }; - 9163401A1F74F1F300303516 /* RTCPeerConnection+SessionDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RTCPeerConnection+SessionDescription.swift"; sourceTree = ""; }; 91672C781F78F7FC002300E7 /* WebRTCConfigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTCConfigration.swift; sourceTree = ""; }; 916BBF711F19EF5800846166 /* ICECandidate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ICECandidate.swift; sourceTree = ""; }; 9173595D1F2CCEBF00806F8B /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; @@ -120,7 +119,6 @@ 91C7B0A81D5463EA006F5FA2 /* Cartfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Cartfile; sourceTree = ""; }; 91CD2A491F288A6A00D039D1 /* Sora.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sora.swift; sourceTree = ""; }; 91FA6F201D93CA9800D38DB4 /* VideoFrame.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoFrame.swift; sourceTree = ""; }; - 91FAB3EB1F29130700EDF53C /* SignalingMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalingMessage.swift; sourceTree = ""; }; 91FD95741DCA06F700047BA9 /* RTC+Description.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RTC+Description.swift"; sourceTree = ""; }; C5D3C7411F7CEB18004660F5 /* VideoCapturerDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCapturerDevice.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -162,7 +160,6 @@ children = ( 91629BF11F8E5098001193D0 /* Array+Base.swift */, 91FD95741DCA06F700047BA9 /* RTC+Description.swift */, - 9163401A1F74F1F300303516 /* RTCPeerConnection+SessionDescription.swift */, ); path = Extensions; sourceTree = ""; @@ -209,8 +206,8 @@ 91A19B8B1F19366B00A76852 /* PeerChannel.swift */, 9174A8901F73F89400D586C4 /* Role.swift */, 91BD9488204536D9006ED524 /* SDKInfo.swift */, + 913C8959226EFC61001B2569 /* Signaling.swift */, 91A19B891F19366000A76852 /* SignalingChannel.swift */, - 91FAB3EB1F29130700EDF53C /* SignalingMessage.swift */, 91CD2A491F288A6A00D039D1 /* Sora.swift */, 915344111FE96F750083762B /* SoraError.swift */, 9174A8981F73F9A200D586C4 /* TLSSecurityPolicy.swift */, @@ -374,7 +371,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "sh $SRCROOT/Sora/info.sh > $SRCROOT/Sora/info.json"; + shellScript = "sh $SRCROOT/Sora/info.sh > $SRCROOT/Sora/info.json\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -386,8 +383,6 @@ 91554F431F179CFD00403C39 /* WebSocketChannel.swift in Sources */, 91756E221F90ADE900B70C53 /* PeerChannel.swift in Sources */, 91A19B8E1F19367700A76852 /* MediaChannel.swift in Sources */, - 91FAB3EC1F29130700EDF53C /* SignalingMessage.swift in Sources */, - 9163401B1F74F1F300303516 /* RTCPeerConnection+SessionDescription.swift in Sources */, 915CEC901F821A90006E45E2 /* CameraVideoCapturer.swift in Sources */, 91672C791F78F7FC002300E7 /* WebRTCConfigration.swift in Sources */, 9174A89B1F73F9EE00D586C4 /* CameraPosition.swift in Sources */, @@ -401,6 +396,7 @@ 9174A8971F73F93B00D586C4 /* ICETransportPolicy.swift in Sources */, 91B1D6461D75E11F00112A4E /* VideoRenderer.swift in Sources */, 9177FF9C1F2E2D1600B4FA1A /* VideoCapturer.swift in Sources */, + 913C895A226EFC61001B2569 /* Signaling.swift in Sources */, 91BD9489204536D9006ED524 /* SDKInfo.swift in Sources */, 91FD95751DCA06F700047BA9 /* RTC+Description.swift in Sources */, 91CD2A4A1F288A6A00D039D1 /* Sora.swift in Sources */, diff --git a/Sora/AspectRatio.swift b/Sora/AspectRatio.swift index 827a797c..5502c2e5 100644 --- a/Sora/AspectRatio.swift +++ b/Sora/AspectRatio.swift @@ -26,7 +26,8 @@ public enum AspectRatio { } private var aspectRatioTable: PairTable = - PairTable(pairs: [("standard", .standard), + PairTable(name: "AspectRatio", + pairs: [("standard", .standard), ("wide", .wide)]) /// :nodoc: diff --git a/Sora/AudioCodec.swift b/Sora/AudioCodec.swift index 035f6853..d34c9664 100644 --- a/Sora/AudioCodec.swift +++ b/Sora/AudioCodec.swift @@ -1,7 +1,8 @@ import Foundation private let descriptionTable: PairTable = - PairTable(pairs: [("default", .default), + PairTable(name: "AudioCodec", + pairs: [("default", .default), ("OPUS", .opus), ("PCMU", .pcmu)]) diff --git a/Sora/CameraPosition.swift b/Sora/CameraPosition.swift index b5b0bf8b..0ff7b376 100644 --- a/Sora/CameraPosition.swift +++ b/Sora/CameraPosition.swift @@ -1,7 +1,8 @@ import Foundation private var descriptionTable: PairTable = - PairTable(pairs: [("front", .front), + PairTable(name: "CameraPosition", + pairs: [("front", .front), ("back", .back)]) /** diff --git a/Sora/CameraVideoCapturer.swift b/Sora/CameraVideoCapturer.swift index 77ef04c1..638a7302 100644 --- a/Sora/CameraVideoCapturer.swift +++ b/Sora/CameraVideoCapturer.swift @@ -340,7 +340,8 @@ extension CameraVideoCapturer.Settings: Codable { } private var resolutionTable: PairTable = - PairTable(pairs: [("qvga240p", .qvga240p), + PairTable(name: "CameraVideoCapturer.Settings.Resolution", + pairs: [("qvga240p", .qvga240p), ("vga480p", .vga480p), ("hd720p", .hd720p), ("hd1080p", .hd1080p)]) diff --git a/Sora/Configuration.swift b/Sora/Configuration.swift index e80c410d..13b9ad33 100644 --- a/Sora/Configuration.swift +++ b/Sora/Configuration.swift @@ -1,6 +1,8 @@ import Foundation import WebRTC +// MARK: デフォルト値 + private let defaultPublisherStreamId: String = "mainStream" private let defaultPublisherVideoTrackId: String = "mainVideo" private let defaultPublisherAudioTrackId: String = "mainAudio" @@ -10,14 +12,6 @@ private let defaultPublisherAudioTrackId: String = "mainAudio" */ public struct Configuration { - // MARK: デフォルト値 - - /// 映像の最大ビットレート - public static let maxVideoVideoBitRate = 5000 - - /// デフォルトの接続タイムアウト時間 (秒) - public static let defaultConnectionTimeout = 10 - // MARK: - 接続に関する設定 /// サーバーの URL @@ -29,7 +23,9 @@ public struct Configuration { /// ロール public var role: Role - /// メタデータ。 `connect` シグナリングメッセージにセットされます。 + /// このプロパティは `signalingConnectMetadata` に置き換えられました。 + @available(*, deprecated, renamed: "signalingConnectMetadata", + message: "このプロパティは signalingConnectMetadata に置き換えられました。") public var metadata: String? /** @@ -50,7 +46,10 @@ public struct Configuration { /// 音声コーデック。デフォルトは `.default` です。 public var audioCodec: AudioCodec = .default - + + /// 音声ビットレート。デフォルトは無指定です。 + public var audioBitRate: Int? + /// 映像の可否。 `true` であれば映像を送受信します。 /// デフォルトは `true` です。 public var videoEnabled: Bool = true @@ -59,18 +58,42 @@ public struct Configuration { /// デフォルトは `true` です。 public var audioEnabled: Bool = true - /** - 最大話者数。マルチストリーム時のみ有効です。 - - このプロパティをセットすると、直近に発言した話者の映像のみを参加者に配信できます。 - 映像の配信者数を制限できるため、参加者の端末やサーバーの負荷を減らすことが可能です。 - 詳しくは Sora の音声検出 (VAD) 機能を参照してください。 - */ + /// サイマルキャストの可否。 `true` であればサイマルキャストを有効にします。 + public var simulcastEnabled: Bool = false + + /// サイマルキャストの品質。 + /// ロールが `.subscriber` または `.groupSub` のときのみ有効です。 + /// デフォルトは `.high` です。 + public var simulcastQuality: SimulcastQuality = .high + + /// このプロパティは廃止されました。 + @available(*, deprecated) public var maxNumberOfSpeakers: Int? + /// アクティブな配信数。 + /// 詳しくは Sora のスポットライト機能を参照してください。 + public var spotlight: Int? + /// WebRTC に関する設定 public var webRTCConfiguration: WebRTCConfiguration = WebRTCConfiguration() + + /// `connect` シグナリングに含めるメタデータ + public var signalingConnectMetadata: Encodable? + + /// `connect` シグナリングに含める通知用のメタデータ + public var signalingConnectNotifyMetadata: Encodable? + + // MARK: - イベントハンドラ + /// シグナリングチャネルに関するイベントハンドラ + public var signalingChannelHandlers: SignalingChannelHandlers = SignalingChannelHandlers() + + /// ピアチャネルに関するイベントハンドラ + public var peerChannelHandlers: PeerChannelHandlers = PeerChannelHandlers() + + /// メディアチャネルに関するイベントハンドラ + public var mediaChannelHandlers: MediaChannelHandlers = MediaChannelHandlers() + // MARK: - 接続チャネルに関する設定 /** @@ -153,10 +176,15 @@ extension Configuration: Codable { case videoBitRate case videoCapturerDevice case audioCodec + case audioBitRate case videoEnabled case audioEnabled - case maxNumberOfSpeakers + case simulcastEnabled + case simulcastQuality + case spotlight case webRTCConfiguration + case signalingConnectMetadata + case signalingConnectNotifyMetadata case signalingChannelType case webSocketChannelType case peerChannelType @@ -166,14 +194,12 @@ extension Configuration: Codable { } public init(from decoder: Decoder) throws { + // NOTE: メタデータとイベントハンドラはサポートしない let container = try decoder.container(keyedBy: CodingKeys.self) let url = try container.decode(URL.self, forKey: .url) let channelId = try container.decode(String.self, forKey: .channelId) let role = try container.decode(Role.self, forKey: .role) self.init(url: url, channelId: channelId, role: role) - if container.contains(.metadata) { - metadata = try container.decode(String.self, forKey: .metadata) - } connectionTimeout = try container.decode(Int.self, forKey: .connectionTimeout) videoEnabled = try container.decode(Bool.self, forKey: .videoEnabled) @@ -185,10 +211,14 @@ extension Configuration: Codable { } audioCodec = try container.decode(AudioCodec.self, forKey: .audioCodec) audioEnabled = try container.decode(Bool.self, forKey: .audioEnabled) - if container.contains(.maxNumberOfSpeakers) { - maxNumberOfSpeakers = try container.decode(Int.self, - forKey: .maxNumberOfSpeakers) + audioBitRate = try container.decodeIfPresent(Int.self, forKey: .audioBitRate) + if container.contains(.spotlight) { + spotlight = try container.decode(Int.self, + forKey: .spotlight) } + simulcastEnabled = try container.decode(Bool.self, forKey: .simulcastEnabled) + simulcastQuality = try container.decode(SimulcastQuality.self, + forKey: .simulcastQuality) webRTCConfiguration = try container.decode(WebRTCConfiguration.self, forKey: .webRTCConfiguration) publisherStreamId = try container.decode(String.self, @@ -201,13 +231,13 @@ extension Configuration: Codable { } public func encode(to encoder: Encoder) throws { + // NOTE: メタデータとイベントハンドラはサポートしない var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(url, forKey: .url) try container.encode(channelId, forKey: .channelId) try container.encode(role, forKey: .role) - if let metadata = self.metadata { - try container.encode(metadata, forKey: .metadata) - } + try container.encode(simulcastEnabled, forKey: .simulcastEnabled) + try container.encode(simulcastQuality, forKey: .simulcastQuality) try container.encode(connectionTimeout, forKey: .connectionTimeout) try container.encode(videoEnabled, forKey: .videoEnabled) try container.encode(videoCodec, forKey: .videoCodec) @@ -217,8 +247,9 @@ extension Configuration: Codable { try container.encode(videoCapturerDevice, forKey: .videoCapturerDevice) try container.encode(audioCodec, forKey: .audioCodec) try container.encode(audioEnabled, forKey: .audioEnabled) - if let num = self.maxNumberOfSpeakers { - try container.encode(num, forKey: .maxNumberOfSpeakers) + try container.encodeIfPresent(audioBitRate, forKey: .audioBitRate) + if let num = self.spotlight { + try container.encode(num, forKey: .spotlight) } try container.encode(webRTCConfiguration, forKey: .webRTCConfiguration) try container.encode(publisherStreamId, forKey: .publisherStreamId) diff --git a/Sora/Extensions/RTCPeerConnection+SessionDescription.swift b/Sora/Extensions/RTCPeerConnection+SessionDescription.swift deleted file mode 100644 index ef3c87fd..00000000 --- a/Sora/Extensions/RTCPeerConnection+SessionDescription.swift +++ /dev/null @@ -1,55 +0,0 @@ -import Foundation -import WebRTC - -/// :nodoc: -extension RTCPeerConnection { - - func createAnswer(forOffer offer: String, - constraints: RTCMediaConstraints, - handler: @escaping (String?, Error?) -> Void) { - Logger.debug(type: .nativePeerChannel, message: "try create answer") - Logger.debug(type: .nativePeerChannel, message: offer) - - Logger.debug(type: .nativePeerChannel, message: "try setting remote description") - let offer = RTCSessionDescription(type: .offer, sdp: offer) - setRemoteDescription(offer) { error in - guard error == nil else { - Logger.debug(type: .nativePeerChannel, - message: "failed setting remote description: (\(error!.localizedDescription)") - handler(nil, error) - return - } - Logger.debug(type: .nativePeerChannel, message: "did set remote description") - Logger.debug(type: .nativePeerChannel, message: "\(offer.sdpDescription)") - - Logger.debug(type: .nativePeerChannel, message: "try creating native answer") - self.answer(for: constraints) { answer, error in - guard error == nil else { - Logger.debug(type: .nativePeerChannel, - message: "failed creating native answer (\(error!.localizedDescription)") - handler(nil, error) - return - } - Logger.debug(type: .nativePeerChannel, message: "did create answer") - - Logger.debug(type: .nativePeerChannel, message: "try setting local description") - self.setLocalDescription(answer!) { error in - guard error == nil else { - Logger.debug(type: .nativePeerChannel, - message: "failed setting local description") - handler(nil, error) - return - } - Logger.debug(type: .nativePeerChannel, - message: "did set local description") - Logger.debug(type: .nativePeerChannel, - message: "\(answer!.sdpDescription)") - Logger.debug(type: .nativePeerChannel, - message: "did create answer") - handler(answer!.sdp, nil) - } - } - } - } - -} diff --git a/Sora/ICECandidate.swift b/Sora/ICECandidate.swift index 8e29e823..2f7c0955 100644 --- a/Sora/ICECandidate.swift +++ b/Sora/ICECandidate.swift @@ -40,3 +40,19 @@ public final class ICECandidate: Equatable { } } + +/// :nodoc: +extension ICECandidate: Codable { + + public convenience init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let sdp = try container.decode(String.self) + self.init(url: nil, sdp: sdp) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(sdp) + } + +} diff --git a/Sora/ICETransportPolicy.swift b/Sora/ICETransportPolicy.swift index e6c6eca5..6d3f3290 100644 --- a/Sora/ICETransportPolicy.swift +++ b/Sora/ICETransportPolicy.swift @@ -2,7 +2,8 @@ import Foundation import WebRTC private var iceTransportPolicyTable: PairTable = - PairTable(pairs: [(.relay, .relay), (.all, .all)]) + PairTable(name: "ICETransportPolicy", + pairs: [(.relay, .relay), (.all, .all)]) /** ICE 通信ポリシーを表します。 @@ -27,9 +28,12 @@ public enum ICETransportPolicy { extension ICETransportPolicy: CustomStringConvertible { public var description: String { - let encoder = JSONEncoder() - let data = try! encoder.encode(self) - return String(data: data, encoding: .utf8)! + switch self { + case .relay: + return "relay" + case .all: + return "all" + } } } diff --git a/Sora/Info.plist b/Sora/Info.plist index 23e35671..b6cbd289 100644 --- a/Sora/Info.plist +++ b/Sora/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 2.3.2 + 2.4.0 CFBundleSignature ???? CFBundleVersion diff --git a/Sora/MediaChannel.swift b/Sora/MediaChannel.swift index fe11703c..551c0f64 100644 --- a/Sora/MediaChannel.swift +++ b/Sora/MediaChannel.swift @@ -9,18 +9,15 @@ public final class MediaChannelHandlers { /// 接続解除時に呼ばれるブロック public var onDisconnectHandler: ((Error?) -> Void)? - /// シグナリングメッセージの受信時に呼ばれるブロック - public var onMessageHandler: ((SignalingMessage) -> Void)? - /// ストリームが追加されたときに呼ばれるブロック public var onAddStreamHandler: ((MediaStream) -> Void)? /// ストリームが除去されたときに呼ばれるブロック public var onRemoveStreamHandler: ((MediaStream) -> Void)? - /// サーバーからのイベント通知の受信時に呼ばれるハンドラ - public var onNotifyHandler: ((SignalingNotifyMessage) -> Void)? - + /// シグナリング受信時に呼ばれるブロック + public var onReceiveSignalingHandler: ((Signaling) -> Void)? + } // MARK: - @@ -48,10 +45,10 @@ public final class MediaChannel { // MARK: - イベントハンドラ /// イベントハンドラ - public let handlers: MediaChannelHandlers = MediaChannelHandlers() + public var handlers: MediaChannelHandlers = MediaChannelHandlers() /// 内部処理で使われるイベントハンドラ - let internalHandlers: MediaChannelHandlers = MediaChannelHandlers() + var internalHandlers: MediaChannelHandlers = MediaChannelHandlers() // MARK: - 接続情報 @@ -158,9 +155,14 @@ public final class MediaChannel { self.configuration = configuration signalingChannel = configuration._signalingChannelType .init(configuration: configuration) + signalingChannel.handlers = + configuration.signalingChannelHandlers peerChannel = configuration._peerChannelType .init(configuration: configuration, signalingChannel: signalingChannel) + peerChannel.handlers = + configuration.peerChannelHandlers + handlers = configuration.mediaChannelHandlers connectionTimer = ConnectionTimer(monitors: [ .webSocketChannel(signalingChannel.webSocketChannel), @@ -180,7 +182,7 @@ public final class MediaChannel { - parameter error: (接続失敗時) エラー */ func connect(webRTCConfiguration: WebRTCConfiguration, - timeout: Int = Configuration.defaultConnectionTimeout, + timeout: Int = 30, handler: @escaping (_ error: Error?) -> Void) -> ConnectionTask { let task = ConnectionTask() if state.isConnecting { @@ -229,14 +231,19 @@ public final class MediaChannel { self.handlers.onRemoveStreamHandler?(stream) } - peerChannel.internalHandlers.onNotifyHandler = { message in - Logger.debug(type: .mediaChannel, message: "receive event notification") - self.publisherCount = message.publisherCount - self.subscriberCount = message.subscriberCount + peerChannel.internalHandlers.onReceiveSignalingHandler = { message in + Logger.debug(type: .mediaChannel, message: "receive signaling") + switch message { + case .notifyConnection(let message): + self.publisherCount = message.publisherCount + self.subscriberCount = message.subscriberCount + default: + break + } - Logger.debug(type: .mediaChannel, message: "call onNotifyHandler") - self.internalHandlers.onNotifyHandler?(message) - self.handlers.onNotifyHandler?(message) + Logger.debug(type: .mediaChannel, message: "call onReceiveSignalingHandler") + self.internalHandlers.onReceiveSignalingHandler?(message) + self.handlers.onReceiveSignalingHandler?(message) } peerChannel.connect() { error in diff --git a/Sora/NativePeerChannelFactory.swift b/Sora/NativePeerChannelFactory.swift index e5917eb0..3056c193 100644 --- a/Sora/NativePeerChannelFactory.swift +++ b/Sora/NativePeerChannelFactory.swift @@ -97,8 +97,8 @@ class NativePeerChannelFactory { videoTrackId: "video", audioTrackId: "audio", constraints: constraints) - peer.add(stream.videoTracks[0], streamLabels: [stream.streamId]) - peer.add(stream.audioTracks[0], streamLabels: [stream.streamId]) + peer.add(stream.videoTracks[0], streamIds: [stream.streamId]) + peer.add(stream.audioTracks[0], streamIds: [stream.streamId]) peer.offer(for: constraints.nativeValue) { sdp, error in if let error = error { handler(nil, error) diff --git a/Sora/PeerChannel.swift b/Sora/PeerChannel.swift index cb6981ec..c8201302 100644 --- a/Sora/PeerChannel.swift +++ b/Sora/PeerChannel.swift @@ -2,7 +2,8 @@ import Foundation import WebRTC private let peerChannelSignalingStateTable: PairTable = - PairTable(pairs: [(.stable, .stable), + PairTable(name: "PeerChannelSignalingState", + pairs: [(.stable, .stable), (.haveLocalOffer, .haveLocalOffer), (.haveLocalPrAnswer, .haveLocalPrAnswer), (.haveRemoteOffer, .haveRemoteOffer), @@ -10,7 +11,8 @@ private let peerChannelSignalingStateTable: PairTable = - PairTable(pairs: [(.new, .new), + PairTable(name: "ICEConnectionState", + pairs: [(.new, .new), (.checking, .checking), (.connected, .connected), (.completed, .completed), @@ -20,7 +22,8 @@ private let iceConnectionStateTable: PairTable = - PairTable(pairs: [(.new, .new), + PairTable(name: "ICEGatheringState", + pairs: [(.new, .new), (.gathering, .gathering), (.complete, .complete)]) @@ -121,10 +124,7 @@ class PeerChannelInternalState { onCompleteHandler?() onCompleteHandler = nil } else if isConnected { - if signalingState == .closed - || iceConnectionState == .failed - || iceConnectionState == .disconnected - || iceConnectionState == .closed { + if signalingState == .closed { Logger.debug(type: .peerChannel, message: "peer channel state: disconnected") Logger.debug(type: .peerChannel, @@ -167,12 +167,9 @@ public final class PeerChannelHandlers { /// 更新により、ストリームの追加または除去が行われます。 public var onUpdateHandler: ((String) -> Void)? - /// イベント通知の受信時に呼ばれるブロック - public var onNotifyHandler: ((SignalingNotifyMessage) -> Void)? - - /// ping の受信時に呼ばれるブロック - public var onPingHandler: (() -> Void)? - + /// シグナリング受信時に呼ばれるブロック + public var onReceiveSignalingHandler: ((Signaling) -> Void)? + } // MARK: - @@ -192,13 +189,13 @@ public protocol PeerChannel: class { // MARK: - イベントハンドラ /// イベントハンドラ - var handlers: PeerChannelHandlers { get } + var handlers: PeerChannelHandlers { get set } /** 内部処理で使われるイベントハンドラ。 このハンドラをカスタマイズに使うべきではありません。 */ - var internalHandlers: PeerChannelHandlers { get } + var internalHandlers: PeerChannelHandlers { get set } // MARK: - 接続情報 @@ -250,8 +247,8 @@ public protocol PeerChannel: class { class BasicPeerChannel: PeerChannel { - let handlers: PeerChannelHandlers = PeerChannelHandlers() - let internalHandlers: PeerChannelHandlers = PeerChannelHandlers() + var handlers: PeerChannelHandlers = PeerChannelHandlers() + var internalHandlers: PeerChannelHandlers = PeerChannelHandlers() let configuration: Configuration let signalingChannel: SignalingChannel @@ -278,7 +275,7 @@ class BasicPeerChannel: PeerChannel { } private var context: BasicPeerChannelContext! - + required init(configuration: Configuration, signalingChannel: SignalingChannel) { self.configuration = configuration self.signalingChannel = signalingChannel @@ -347,6 +344,49 @@ class BasicPeerChannelContext: NSObject, RTCPeerConnectionDelegate { case disconnected } + final class Lock { + + weak var context: BasicPeerChannelContext? + var count: Int = 0 + var shouldDisconnect: (Bool, Error?) = (false, nil) + + func waitDisconnect(error: Error?) { + if count == 0 { + context?.basicDisconnect(error: error) + } else { + shouldDisconnect = (true, error) + } + } + + func lock() { + count += 1 + } + + func unlock() { + if count <= 0 { + fatalError("count is already 0") + } + count -= 1 + if count == 0 { + disconnect() + } + } + + func disconnect() { + switch shouldDisconnect { + case (true, let error): + shouldDisconnect = (false, nil) + if let context = context { + if context.state != .disconnecting && context.state != .disconnected { + context.basicDisconnect(error: error) + } + } + default: + break + } + } + } + weak var channel: BasicPeerChannel! var state: State = .disconnected var nativeChannel: RTCPeerConnection! @@ -365,15 +405,19 @@ class BasicPeerChannelContext: NSObject, RTCPeerConnectionDelegate { var onConnectHandler: ((Error?) -> Void)? + private var lock: Lock + init(channel: BasicPeerChannel) { self.channel = channel + lock = Lock() super.init() - + lock.context = self + signalingChannel.internalHandlers.onDisconnectHandler = { error in self.disconnect(error: error) } - signalingChannel.internalHandlers.onMessageHandler = handleMessage + signalingChannel.internalHandlers.onReceiveSignalingHandler = handle } func connect(handler: @escaping (Error?) -> Void) { @@ -386,7 +430,10 @@ class BasicPeerChannelContext: NSObject, RTCPeerConnectionDelegate { Logger.debug(type: .peerChannel, message: "try connecting") Logger.debug(type: .peerChannel, message: "try connecting to signaling channel") + // このロックは finishConnecting() で解除される + lock.lock() onConnectHandler = handler + self.webRTCConfiguration = channel.configuration.webRTCConfiguration nativeChannel = NativePeerChannelFactory.default .createNativePeerChannel(configuration: webRTCConfiguration, @@ -410,10 +457,12 @@ class BasicPeerChannelContext: NSObject, RTCPeerConnectionDelegate { } func sendConnectMessage(error: Error?) { - Logger.debug(type: .peerChannel, message: "try creating offer SDP") - NativePeerChannelFactory.default - .createClientOfferSDP(configuration: webRTCConfiguration, - constraints: webRTCConfiguration.constraints) + switch configuration.role { + case .publisher, .group: + Logger.debug(type: .peerChannel, message: "try creating offer SDP") + NativePeerChannelFactory.default + .createClientOfferSDP(configuration: webRTCConfiguration, + constraints: webRTCConfiguration.constraints) { sdp, sdpError in if let error = sdpError { Logger.debug(type: .peerChannel, @@ -423,6 +472,9 @@ class BasicPeerChannelContext: NSObject, RTCPeerConnectionDelegate { message: "did create offer SDP") } self.sendConnectMessage(with: sdp, error: error) + } + default: + self.sendConnectMessage(with: nil, error: error) } } @@ -444,40 +496,43 @@ class BasicPeerChannelContext: NSObject, RTCPeerConnectionDelegate { switch configuration.role { case .publisher: role = .upstream - initializePublisherStream() case .subscriber: role = .downstream case .group: role = .upstream multistream = true - initializePublisherStream() case .groupSub: role = .downstream multistream = true } - let connect = - SignalingConnectMessage(role: role, - channelId: configuration.channelId, - metadata: configuration.metadata, - sdp: sdp, - multistreamEnabled: multistream, - planBEnabled: webRTCConfiguration.sdpSemantics == .planB, - videoEnabled: configuration.videoEnabled, - videoCodec: configuration.videoCodec, - videoBitRate: configuration.videoBitRate, - // WARN: video only では answer 生成に失敗するため、 - // 音声トラックを使用しない方法で回避する - // audioEnabled: config.audioEnabled, - audioEnabled: true, - audioCodec: configuration.audioCodec, - maxNumberOfSpeakers: configuration.maxNumberOfSpeakers) - let message = SignalingMessage.connect(message: connect) + let connect = SignalingConnect( + role: role, + channelId: configuration.channelId, + metadata: configuration.signalingConnectMetadata, + notifyMetadata: configuration.signalingConnectNotifyMetadata, + sdp: sdp, + multistreamEnabled: multistream, + planBEnabled: webRTCConfiguration.sdpSemantics == .planB, + videoEnabled: configuration.videoEnabled, + videoCodec: configuration.videoCodec, + videoBitRate: configuration.videoBitRate, + // WARN: video only では answer 生成に失敗するため、 + // 音声トラックを使用しない方法で回避する + // audioEnabled: config.audioEnabled, + audioEnabled: true, + audioCodec: configuration.audioCodec, + audioBitRate: configuration.audioBitRate, + spotlight: configuration.spotlight, + simulcastEnabled: configuration.simulcastEnabled, + simulcastQuality: configuration.simulcastQuality) Logger.debug(type: .peerChannel, message: "send connect") - signalingChannel.send(message: message) + signalingChannel.send(message: Signaling.connect(connect)) } func initializePublisherStream() { + Logger.debug(type: .peerChannel, message: "init publisher stream") + let nativeStream = NativePeerChannelFactory.default .createNativePublisherStream(streamId: configuration.publisherStreamId, videoTrackId: @@ -506,11 +561,11 @@ class BasicPeerChannelContext: NSObject, RTCPeerConnectionDelegate { if let track = stream.nativeVideoTrack { nativeChannel.add(track, - streamLabels: [stream.nativeStream.streamId]) + streamIds: [stream.nativeStream.streamId]) } if let track = stream.nativeAudioTrack { nativeChannel.add(track, - streamLabels: [stream.nativeStream.streamId]) + streamIds: [stream.nativeStream.streamId]) } channel.add(stream: stream) Logger.debug(type: .peerChannel, @@ -534,51 +589,120 @@ class BasicPeerChannelContext: NSObject, RTCPeerConnectionDelegate { } } } - - func createAndSendAnswerMessage(offer: SignalingOfferMessage) { + + func createAnswer(initsPublisher: Bool, + offer: String, + constraints: RTCMediaConstraints, + handler: @escaping (String?, Error?) -> Void) { + Logger.debug(type: .peerChannel, message: "try create answer") + Logger.debug(type: .peerChannel, message: offer) + + Logger.debug(type: .peerChannel, message: "try setting remote description") + let offer = RTCSessionDescription(type: .offer, sdp: offer) + nativeChannel.setRemoteDescription(offer) { error in + guard error == nil else { + Logger.debug(type: .peerChannel, + message: "failed setting remote description: (\(error!.localizedDescription)") + handler(nil, error) + return + } + Logger.debug(type: .peerChannel, message: "did set remote description") + Logger.debug(type: .peerChannel, message: "\(offer.sdpDescription)") + + if initsPublisher { + self.initializePublisherStream() + } + + Logger.debug(type: .peerChannel, message: "try creating native answer") + self.nativeChannel.answer(for: constraints) { answer, error in + guard error == nil else { + Logger.debug(type: .peerChannel, + message: "failed creating native answer (\(error!.localizedDescription)") + handler(nil, error) + return + } + Logger.debug(type: .peerChannel, message: "did create answer") + + Logger.debug(type: .peerChannel, message: "try setting local description") + self.nativeChannel.setLocalDescription(answer!) { error in + guard error == nil else { + Logger.debug(type: .peerChannel, + message: "failed setting local description") + handler(nil, error) + return + } + Logger.debug(type: .peerChannel, + message: "did set local description") + Logger.debug(type: .peerChannel, + message: "\(answer!.sdpDescription)") + Logger.debug(type: .peerChannel, + message: "did create answer") + handler(answer!.sdp, nil) + } + } + } + } + + func createAndSendAnswer(offer: SignalingOffer) { Logger.debug(type: .peerChannel, message: "try sending answer") state = .waitingComplete if let config = offer.configuration { Logger.debug(type: .peerChannel, message: "update configuration") - Logger.debug(type: .peerChannel, message: config.description) + Logger.debug(type: .peerChannel, message: "ICE server infos => \(config.iceServerInfos)") + Logger.debug(type: .peerChannel, message: "ICE transport policy => \(config.iceTransportPolicy)") webRTCConfiguration.iceServerInfos = config.iceServerInfos webRTCConfiguration.iceTransportPolicy = config.iceTransportPolicy nativeChannel.setConfiguration(webRTCConfiguration.nativeValue) } - nativeChannel.createAnswer(forOffer: offer.sdp, - constraints: webRTCConfiguration.nativeConstraints) + lock.lock() + var initsPublisher = false + switch configuration.role { + case .publisher, .group: + initsPublisher = true + default: + break + } + createAnswer(initsPublisher: initsPublisher, + offer: offer.sdp, + constraints: webRTCConfiguration.nativeConstraints) { sdp, error in guard error == nil else { Logger.error(type: .peerChannel, message: "failed to create answer (\(error!.localizedDescription))") + self.lock.unlock() self.disconnect(error: SoraError .peerChannelError(reason: "failed to create answer")) return } - let answer = SignalingMessage.answer(sdp: sdp!) - self.signalingChannel.send(message: answer) + let answer = SignalingAnswer(sdp: sdp!) + self.signalingChannel.send(message: Signaling.answer(answer)) + self.lock.unlock() Logger.debug(type: .peerChannel, message: "did send answer") } } - func createAndSendUpdateAnswerMessage(forOffer offer: String) { + func createAndSendUpdateAnswer(forOffer offer: String) { + Logger.debug(type: .peerChannel, message: "create and send update-answer") + lock.lock() state = .waitingUpdateComplete - nativeChannel.createAnswer(forOffer: offer, - constraints: webRTCConfiguration.nativeConstraints) + createAnswer(initsPublisher: false, + offer: offer, + constraints: webRTCConfiguration.nativeConstraints) { answer, error in guard error == nil else { Logger.error(type: .peerChannel, message: "failed to create update-answer (\(error!.localizedDescription)") + self.lock.unlock() self.disconnect(error: SoraError .peerChannelError(reason: "failed to create update-answer")) return } - let message = SignalingMessage.update(sdp: answer!) + let message = Signaling.update(SignalingUpdate(sdp: answer!)) self.signalingChannel.send(message: message) // Answer 送信後に RTCPeerConnection の状態に変化はないため、 @@ -588,69 +712,48 @@ class BasicPeerChannelContext: NSObject, RTCPeerConnectionDelegate { Logger.debug(type: .peerChannel, message: "call onUpdateHandler") self.channel.internalHandlers.onUpdateHandler?(answer!) self.channel.handlers.onUpdateHandler?(answer!) + + self.lock.unlock() } } - func handleMessage(_ message: SignalingMessage?, _ text: String) { - guard let message = message else { - Logger.debug(type: .peerChannel, message: "receive invalid signaling") - return - } - - Logger.debug(type: .peerChannel, message: "receive signaling '\(message.shortDescription)'") - - switch message { - case .notify(message: let message): - Logger.debug(type: .peerChannel, message: "call onNotifyHandler") - channel.internalHandlers.onNotifyHandler?(message) - channel.handlers.onNotifyHandler?(message) - default: - break - } - + func handle(signaling: Signaling) { + Logger.debug(type: .mediaStream, message: "handle signaling => \(signaling.typeName())") switch state { case .waitingOffer: - switch message { + switch signaling { case .offer(let offer): clientId = offer.clientId - createAndSendAnswerMessage(offer: offer) - - case .notify(message: _): - break + createAndSendAnswer(offer: offer) default: - Logger.debug(type: .peerChannel, message: "discard invalid signaling") + // discard + break } case .connected: - switch message { - case .update(sdp: let sdp): + switch signaling { + case .update(let update): guard configuration.role == .group || configuration.role == .groupSub else { return } - createAndSendUpdateAnswerMessage(forOffer: sdp) - + createAndSendUpdateAnswer(forOffer: update.sdp) + case .ping: - signalingChannel.send(message: .pong) - Logger.debug(type: .peerChannel, message: "call onPingHandler") - channel.internalHandlers.onPingHandler?() - channel.handlers.onPingHandler?() - - case .notify(message: _): - break - + signalingChannel.send(message: .pong(SignalingPong())) + default: - Logger.debug(type: .peerChannel, message: "discard invalid signaling") + // discard + break } default: - switch message { - case .notify(message: _): - break - - default: - Logger.debug(type: .peerChannel, message: "discard invalid signaling") - } + // discard + break } + + Logger.debug(type: .peerChannel, message: "call onReceiveSignalingHandler") + channel.internalHandlers.onReceiveSignalingHandler?(signaling) + channel.handlers.onReceiveSignalingHandler?(signaling) } func finishConnecting() { @@ -658,7 +761,9 @@ class BasicPeerChannelContext: NSObject, RTCPeerConnectionDelegate { Logger.debug(type: .peerChannel, message: "media streams = \(channel.streams.count)") Logger.debug(type: .peerChannel, - message: "native media streams = \(nativeChannel.localStreams.count)") + message: "native senders = \(nativeChannel.senders.count)") + Logger.debug(type: .peerChannel, + message: "native receivers = \(nativeChannel.receivers.count)") state = .connected if onConnectHandler != nil { @@ -666,47 +771,52 @@ class BasicPeerChannelContext: NSObject, RTCPeerConnectionDelegate { onConnectHandler!(nil) onConnectHandler = nil } + lock.unlock() } func disconnect(error: Error?) { switch state { case .disconnecting, .disconnected: break - default: - Logger.debug(type: .peerChannel, message: "try disconnecting") - if let error = error { - Logger.error(type: .peerChannel, - message: "error: \(error.localizedDescription)") - } - - state = .disconnecting - - switch configuration.role { - case .publisher: terminatePublisherStream() - case .subscriber, .groupSub: break - case .group: terminatePublisherStream() - } - channel.terminateAllStreams() - nativeChannel.close() - - signalingChannel.send(message: SignalingMessage.disconnect) - signalingChannel.disconnect(error: error) - - state = .disconnected - - Logger.debug(type: .peerChannel, message: "call onDisconnectHandler") - channel.internalHandlers.onDisconnectHandler?(error) - channel.handlers.onDisconnectHandler?(error) - - if onConnectHandler != nil { - Logger.debug(type: .peerChannel, message: "call connect(handler:) handler") - onConnectHandler!(error) - onConnectHandler = nil - } + Logger.debug(type: .peerChannel, message: "wait to disconnect") + lock.waitDisconnect(error: error) + } + } - Logger.debug(type: .peerChannel, message: "did disconnect") + func basicDisconnect(error: Error?) { + Logger.debug(type: .peerChannel, message: "try disconnecting") + if let error = error { + Logger.error(type: .peerChannel, + message: "error: \(error.localizedDescription)") } + + state = .disconnecting + + switch configuration.role { + case .publisher: terminatePublisherStream() + case .subscriber, .groupSub: break + case .group: terminatePublisherStream() + } + channel.terminateAllStreams() + nativeChannel.close() + + signalingChannel.send(message: Signaling.disconnect) + signalingChannel.disconnect(error: error) + + state = .disconnected + + Logger.debug(type: .peerChannel, message: "call onDisconnectHandler") + channel.internalHandlers.onDisconnectHandler?(error) + channel.handlers.onDisconnectHandler?(error) + + if onConnectHandler != nil { + Logger.debug(type: .peerChannel, message: "call connect(handler:) handler") + onConnectHandler!(error) + onConnectHandler = nil + } + + Logger.debug(type: .peerChannel, message: "did disconnect") } // MARK: - RTCPeerConnectionDelegate @@ -790,7 +900,7 @@ class BasicPeerChannelContext: NSObject, RTCPeerConnectionDelegate { message: "generated ICE candidate \(candidate)") let candidate = ICECandidate(nativeICECandidate: candidate) channel.add(iceCandidate: candidate) - let message = SignalingMessage.candidate(candidate) + let message = Signaling.candidate(SignalingCandidate(candidate: candidate)) signalingChannel.send(message: message) } diff --git a/Sora/Role.swift b/Sora/Role.swift index ae7bdca6..981e858e 100644 --- a/Sora/Role.swift +++ b/Sora/Role.swift @@ -19,7 +19,8 @@ public enum Role { } private var roleTable: PairTable = - PairTable(pairs: [("publisher", .publisher), + PairTable(name: "Role", + pairs: [("publisher", .publisher), ("subscriber", .subscriber), ("group", .group), ("groupSub", .groupSub)]) diff --git a/Sora/Signaling.swift b/Sora/Signaling.swift new file mode 100644 index 00000000..a902fda3 --- /dev/null +++ b/Sora/Signaling.swift @@ -0,0 +1,950 @@ +import Foundation +import WebRTC + +/** + シグナリングの種別です。 + */ +public enum Signaling { + + /// "connect" シグナリング + case connect(SignalingConnect) + + /// "offer" シグナリング + case offer(SignalingOffer) + + /// "answer" シグナリング + case answer(SignalingAnswer) + + /// "update" シグナリング + case update(SignalingUpdate) + + /// "candidate" シグナリング + case candidate(SignalingCandidate) + + /// "notify" シグナリング ("connection.created", "connection.updated", "connection.destroyed") + case notifyConnection(SignalingNotifyConnection) + + /// "notify" シグナリング ("spotlight.changed") + case notifySpotlightChanged(SignalingNotifySpotlightChanged) + + /// "notify" シグナリング ("network.status") + case notifyNetworkStatus(SignalingNotifyNetworkStatus) + + /// "ping" シグナリング + case ping + + /// "pong" シグナリング + case pong(SignalingPong) + + /// "disconnect" シグナリング + case disconnect + + /// "pong" シグナリング + case push(SignalingPush) + + /// :nodoc: + public func typeName() -> String { + switch self { + case .connect(_): + return "connect" + case .offer(_): + return "offer" + case .answer(_): + return "answer" + case .update(_): + return "update" + case .candidate(_): + return "candidate" + case .notifyConnection(let notify): + return "notify(\(notify.eventType))" + case .notifySpotlightChanged(_): + return "notify(spotlight.changed)" + case .notifyNetworkStatus(_): + return "notify(network.status)" + case .ping: + return "ping" + case .pong(_): + return "pong" + case .disconnect: + return "disconnect" + case .push(_): + return "push" + } + } + +} + +/** + サイマルキャストの品質を表します。 + */ +public enum SimulcastQuality { + + /// 低画質 + case low + + /// 中画質 + case middle + + /// 高画質 + case high + +} + +/** + シグナリングに含まれるメタデータ (任意のデータ) を表します。 + サーバーから受信するシグナリングにメタデータが含まれる場合は、 + `decoder` プロパティに JSON デコーダーがセットされます。 + 受信したメタデータを任意のデータ型に変換するには、このデコーダーを使ってください。 + */ +public struct SignalingMetadata { + + /// シグナリングに含まれるメタデータの JSON デコーダー + public var decoder: Decoder + +} + +/** + シグナリングに含まれる、同チャネルに接続中のクライアントに関するメタデータ (任意のデータ) を表します。 + */ +public struct SignalingClientMetadata { + + /// クライアント ID + public var clientId: String? + + /// 接続 ID + public var connectionId: String? + + /// メタデータ + public var metadata: SignalingMetadata + +} + +/** + "connect" シグナリングメッセージを表します。 + このメッセージはシグナリング接続の確立後、最初に送信されます。 + */ +public struct SignalingConnect { + + /// ロール + public var role: SignalingRole + + /// チャネル ID + public var channelId: String + + /// メタデータ + public var metadata: Encodable? + + /// notify メタデータ + public var notifyMetadata: Encodable? + + /// SDP 。クライアントの判別に使われます。 + public var sdp: String? + + /// マルチストリームの可否 + public var multistreamEnabled: Bool? + + /// Plan B の可否 + public var planBEnabled: Bool? + + /// 映像の可否 + public var videoEnabled: Bool + + /// 映像コーデック + public var videoCodec: VideoCodec + + /// 映像ビットレート + public var videoBitRate: Int? + + /// 音声の可否 + public var audioEnabled: Bool + + /// 音声コーデック + public var audioCodec: AudioCodec + + /// 音声ビットレート + public var audioBitRate: Int? + + /// スポットライト + public var spotlight: Int? + + /// サイマルキャストの可否 + public var simulcastEnabled: Bool + + /// サイマルキャストの品質 + public var simulcastQuality: SimulcastQuality + +} + +/** + "offer" シグナリングメッセージを表します。 + このメッセージは SDK が "connect" を送信した後に、サーバーから送信されます。 + */ +public struct SignalingOffer { + + /** + クライアントが更新すべき設定を表します。 + */ + public struct Configuration { + + /// ICE サーバーの情報のリスト + public let iceServerInfos: [ICEServerInfo] + + /// ICE 通信ポリシー + public let iceTransportPolicy: ICETransportPolicy + } + + /** + RTP ペイロードに含まれる映像・音声エンコーディングの情報です。 + + 次のリンクも参考にしてください。 + https://w3c.github.io/webrtc-pc/#rtcrtpencodingparameters + */ + public struct Encoding { + + /// RTP ストリーム ID + public let rid: String? + + /// 最大ビットレート + public let maxBitrate: Int? + + /// 最大フレームレート + public let maxFramerate: Double? + + /// 映像解像度を送信前に下げる度合 + public let scaleResolutionDownBy: Double? + + /// RTP エンコーディングに関するパラメーター + public var rtpEncodingParameters: RTCRtpEncodingParameters { + get { + let params = RTCRtpEncodingParameters() + params.rid = rid + if let value = maxBitrate { + params.maxBitrateBps = NSNumber(value: value) + } + if let value = maxFramerate { + params.maxFramerate = NSNumber(value: value) + } + if let value = scaleResolutionDownBy { + params.scaleResolutionDownBy = NSNumber(value: value) + } + return params + } + } + + } + + /// クライアント ID + public let clientId: String + + /// 接続 ID + public let connectionId: String + + /// SDP メッセージ + public let sdp: String + + /// クライアントが更新すべき設定 + public let configuration: Configuration? + + /// メタデータ + public let metadata: SignalingMetadata? + + /// エンコーディング + public let encodings: [Encoding]? + +} + +/** + "answer" シグナリングメッセージを表します。 + */ +public struct SignalingAnswer { + + /// SDP メッセージ + public let sdp: String + +} + +/** + "candidate" シグナリングメッセージを表します。 + */ +public struct SignalingCandidate { + + /// ICE candidate + public let candidate: ICECandidate + +} + +/** + "update" シグナリングメッセージを表します。 + */ +public struct SignalingUpdate { + + /// SDP メッセージ + public let sdp: String + +} + +/** + "push" シグナリングメッセージを表します。 + このメッセージは Sora のプッシュ API を使用して送信されたデータです。 + */ +public struct SignalingPush { + + /// プッシュ通知で送信される JSON データ + public let data: SignalingMetadata + +} + +/** + "notify" シグナリングメッセージで通知されるイベントの種別です。 + 詳細は Sora のドキュメントを参照してください。 + */ +public enum SignalingNotifyEventType { + + /// "connection.created" + case connectionCreated + + /// "connection.updated" + case connectionUpdated + + /// "connection.destroyed" + case connectionDestroyed + + /// "spotlight.changed" + case spotlightChanged + + /// "network.status" + case networkStatus + +} + +/** + "notify" シグナリングメッセージのうち、次のイベントを表します。 + + - `connection.created` + - `connection.updated` + - `connection.destroyed` + + このメッセージは接続の確立後、チャネルへの接続数に変更があるとサーバーから送信されます。 + */ +public struct SignalingNotifyConnection { + + // MARK: イベント情報 + + /// イベントの種別 + public var eventType: SignalingNotifyEventType + + // MARK: 接続情報 + + /// ロール + public var role: SignalingRole + + /// クライアント ID + public var clientId: String? + + /// 接続 ID + public var connectionId: String? + + /// 音声の可否 + public var audioEnabled: Bool? + + /// 映像の可否 + public var videoEnabled: Bool? + + /// メタデータ + public var metadata: SignalingMetadata? + + /// メタデータのリスト + public var metadataList: [SignalingClientMetadata]? + + // MARK: 接続状態 + + /// 接続時間 + public var connectionTime: Int + + /// 接続中のクライアントの数 + public var connectionCount: Int + + /// 接続中のパブリッシャーの数 + public var publisherCount: Int + + /// 接続中のサブスクライバーの数 + public var subscriberCount: Int + +} + +/** + "notify" シグナリングメッセージのうち、 `spotlight.changed` イベントを表します。 + */ +public struct SignalingNotifySpotlightChanged { + + /// クライアント ID + public var clientId: String? + + /// 接続 ID + public var connectionId: String? + + /// スポットライト ID + public var spotlightId: String + + /// 固定の有無 + public var isFixed: Bool? + + /// 音声の可否 + public var audioEnabled: Bool? + + /// 映像の可否 + public var videoEnabled: Bool? + +} + +/** + "notify" シグナリングメッセージのうち、 "network.status" イベントを表します。 + */ +public struct SignalingNotifyNetworkStatus { + + /// ネットワークの不安定度 + public var unstableLevel: Int + +} + +/** + "pong" シグナリングメッセージを表します。 + このメッセージはサーバーから "ping" シグナリングメッセージを受信すると + サーバーに送信されます。 + "ping" 受信後、一定時間内にこのメッセージを返さなければ、 + サーバーとの接続が解除されます。 + */ +public struct SignalingPong {} + +// MARK: - +// MARK: Codable + +/// :nodoc: +extension Signaling: Codable { + + enum MessageType: String { + case connect + case offer + case answer + case update + case candidate + case notify + case ping + case pong + case disconnect + case push + } + + enum CodingKeys: String, CodingKey { + case type + case event_type + case sdp + case candidate + case data + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + switch type { + case "offer": + self = .offer(try SignalingOffer(from: decoder)) + case "update": + self = .update(try SignalingUpdate(from: decoder)) + case "notify": + let eventType = try container.decode(SignalingNotifyEventType.self, + forKey: .event_type) + switch eventType { + case .connectionCreated, + .connectionUpdated, + .connectionDestroyed: + self = .notifyConnection(try SignalingNotifyConnection(from: decoder)) + case .spotlightChanged: + self = .notifySpotlightChanged( + try SignalingNotifySpotlightChanged(from: decoder)) + case .networkStatus: + self = .notifyNetworkStatus( + try SignalingNotifyNetworkStatus(from: decoder)) + } + case "ping": + self = .ping + case "push": + self = .push(try SignalingPush(from: decoder)) + default: + throw SoraError.unknownSignalingMessageType(type: type) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .connect(let message): + try container.encode(MessageType.connect.rawValue, forKey: .type) + try message.encode(to: encoder) + case .offer(let message): + try container.encode(MessageType.offer.rawValue, forKey: .type) + try message.encode(to: encoder) + case .answer(let message): + try container.encode(MessageType.answer.rawValue, forKey: .type) + try message.encode(to: encoder) + case .candidate(let message): + try container.encode(MessageType.candidate.rawValue, forKey: .type) + try message.encode(to: encoder) + case .update(let message): + try container.encode(MessageType.update.rawValue, forKey: .type) + try message.encode(to: encoder) + case .pong: + try container.encode(MessageType.pong.rawValue, forKey: .type) + case .disconnect: + try container.encode(MessageType.disconnect.rawValue, forKey: .type) + default: + throw SoraError.invalidSignalingMessage + } + } + +} + +private var simulcastQualityTable: PairTable = + PairTable(name: "SimulcastQuality", + pairs: [("low", .low), + ("middle", .middle), + ("high", .high)]) + +/// :nodoc: +extension SimulcastQuality: Codable { + + public init(from decoder: Decoder) throws { + throw SoraError.invalidSignalingMessage + } + + public func encode(to encoder: Encoder) throws { + try simulcastQualityTable.encode(self, to: encoder) + } + +} + +/// :nodoc: +extension SignalingMetadata: Decodable { + + public init(from decoder: Decoder) throws { + self.decoder = decoder + } + +} + +/// :nodoc: +extension SignalingClientMetadata: Decodable { + + enum CodingKeys: String, CodingKey { + case client_id + case connection_id + case metadata + } + + public init(from decoder: Decoder) throws { + do { + let container = try decoder.container(keyedBy: CodingKeys.self) + clientId = try container.decodeIfPresent(String.self, forKey: .client_id) + connectionId = try container.decodeIfPresent(String.self, forKey: .connection_id) + metadata = try container.decode(SignalingMetadata.self, forKey: .metadata) + } catch { + metadata = try SignalingMetadata(from: decoder) + } + + } + +} + +private var roleTable: PairTable = + PairTable(name: "SignalingRole", + pairs: [("upstream", .upstream), + ("downstream", .downstream)]) + +/// :nodoc: +extension SignalingRole: Codable { + + public init(from decoder: Decoder) throws { + self = try roleTable.decode(from: decoder) + } + + public func encode(to encoder: Encoder) throws { + try roleTable.encode(self, to: encoder) + } + +} + +/// :nodoc: +extension SignalingConnect: Codable { + + enum CodingKeys: String, CodingKey { + case role + case channel_id + case metadata + case signaling_notify_metadata + case sdp + case multistream + case plan_b + case spotlight + case simulcast + case video + case audio + case vad + } + + enum VideoCodingKeys: String, CodingKey { + case codec_type + case bit_rate + } + + enum AudioCodingKeys: String, CodingKey { + case codec_type + case bit_rate + } + + enum SimulcastQualityCodingKeys: String, CodingKey { + case quality + } + + public init(from decoder: Decoder) throws { + throw SoraError.invalidSignalingMessage + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(role, forKey: .role) + try container.encode(channelId, forKey: .channel_id) + try container.encodeIfPresent(sdp, forKey: .sdp) + let metadataEnc = container.superEncoder(forKey: .metadata) + try metadata?.encode(to: metadataEnc) + let notifyEnc = container.superEncoder(forKey: .signaling_notify_metadata) + try notifyMetadata?.encode(to: notifyEnc) + try container.encodeIfPresent(multistreamEnabled, + forKey: .multistream) + try container.encodeIfPresent(planBEnabled, forKey: .plan_b) + try container.encodeIfPresent(spotlight, forKey: .spotlight) + + if videoEnabled { + if videoCodec != .default || videoBitRate != nil { + var videoContainer = container + .nestedContainer(keyedBy: VideoCodingKeys.self, + forKey: .video) + if videoCodec != .default { + try videoContainer.encode(videoCodec, forKey: .codec_type) + } + try videoContainer.encodeIfPresent(videoBitRate, + forKey: .bit_rate) + } + } else { + try container.encode(false, forKey: .video) + } + + if audioEnabled { + if audioCodec != .default || audioBitRate != nil { + var audioContainer = container + .nestedContainer(keyedBy: AudioCodingKeys.self, + forKey: .audio) + if audioCodec != .default { + try audioContainer.encode(audioCodec, forKey: .codec_type) + } + try audioContainer.encodeIfPresent(audioBitRate, + forKey: .bit_rate) + } + } else { + try container.encode(false, forKey: .audio) + } + + if simulcastEnabled { + switch role { + case .downstream: + var simulcastContainer = container + .nestedContainer(keyedBy: SimulcastQualityCodingKeys.self, forKey: .simulcast) + try simulcastContainer.encode(simulcastQuality, forKey: .quality) + default: + try container.encode(true, forKey: .simulcast) + } + } + } + +} + +/// :nodoc: +extension SignalingOffer.Configuration: Codable { + + enum CodingKeys: String, CodingKey { + case iceServers + case iceTransportPolicy + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + iceServerInfos = try container.decode([ICEServerInfo].self, + forKey: .iceServers) + iceTransportPolicy = try container.decode(ICETransportPolicy.self, + forKey: .iceTransportPolicy) + } + + public func encode(to encoder: Encoder) throws { + throw SoraError.invalidSignalingMessage + } + +} + +/// :nodoc: +extension SignalingOffer.Encoding: Codable { + + enum CodingKeys: String, CodingKey { + case rid + case maxBitrate + case maxFramerate + case scaleResolutionDownBy + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + rid = try container.decodeIfPresent(String.self, forKey: .rid) + maxBitrate = try container.decodeIfPresent(Int.self, forKey: .maxBitrate) + maxFramerate = try container.decodeIfPresent(Double.self, forKey: .maxFramerate) + scaleResolutionDownBy = try container.decodeIfPresent(Double.self, + forKey: .scaleResolutionDownBy) + } + + public func encode(to encoder: Encoder) throws { + throw SoraError.invalidSignalingMessage + } + +} + +/// :nodoc: +extension SignalingOffer: Codable { + + enum CodingKeys: String, CodingKey { + case client_id + case connection_id + case sdp + case config + case metadata + case encodings + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + clientId = try container.decode(String.self, forKey: .client_id) + connectionId = try container.decode(String.self, forKey: .connection_id) + sdp = try container.decode(String.self, forKey: .sdp) + configuration = + try container.decodeIfPresent(Configuration.self, + forKey: .config) + metadata = + try container.decodeIfPresent(SignalingMetadata.self, + forKey: .metadata) + encodings = + try container.decodeIfPresent([Encoding].self, + forKey: .encodings) + } + + public func encode(to encoder: Encoder) throws { + throw SoraError.invalidSignalingMessage + } + +} + +/// :nodoc: +extension SignalingAnswer: Codable { + + enum CodingKeys: String, CodingKey { + case sdp + } + + public init(from decoder: Decoder) throws { + throw SoraError.invalidSignalingMessage + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(sdp, forKey: .sdp) + } + +} + +/// :nodoc: +extension SignalingCandidate: Codable { + + enum CodingKeys: String, CodingKey { + case candidate + } + + public init(from decoder: Decoder) throws { + throw SoraError.invalidSignalingMessage + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(candidate, forKey: .candidate) + } + +} + +/// :nodoc: +extension SignalingUpdate: Codable { + + enum CodingKeys: String, CodingKey { + case sdp + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + sdp = try container.decode(String.self, forKey: .sdp) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(sdp, forKey: .sdp) + } + +} + +/// :nodoc: +extension SignalingPush: Codable { + + enum CodingKeys: String, CodingKey { + case data + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + data = try container.decode(SignalingMetadata.self, forKey: .data) + } + + public func encode(to encoder: Encoder) throws { + throw SoraError.invalidSignalingMessage + } + +} + +/// :nodoc: +extension SignalingNotifyConnection: Codable { + + enum CodingKeys: String, CodingKey { + case event_type + case role + case client_id + case connection_id + case audio + case video + case metadata + case metadata_list + case minutes + case channel_connections + case channel_upstream_connections + case channel_downstream_connections + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + eventType = try container.decode(SignalingNotifyEventType.self, + forKey: .event_type) + role = try container.decode(SignalingRole.self, forKey: .role) + clientId = try container.decodeIfPresent(String.self, forKey: .client_id) + connectionId = try container.decodeIfPresent(String.self, + forKey: .connection_id) + audioEnabled = try container.decodeIfPresent(Bool.self, forKey: .audio) + videoEnabled = try container.decodeIfPresent(Bool.self, forKey: .video) + metadata = try container.decodeIfPresent(SignalingMetadata.self, + forKey: .metadata) + metadataList = + try container.decodeIfPresent([SignalingClientMetadata].self, + forKey: .metadata_list) + connectionTime = try container.decode(Int.self, forKey: .minutes) + connectionCount = + try container.decode(Int.self, forKey: .channel_connections) + publisherCount = + try container.decode(Int.self, forKey: .channel_upstream_connections) + subscriberCount = + try container.decode(Int.self, forKey: .channel_downstream_connections) + } + + public func encode(to encoder: Encoder) throws { + throw SoraError.invalidSignalingMessage + } + +} + +private var signalingNotifyEventType: PairTable = + PairTable(name: "SignalingNotifyEventType", + pairs: [("connection.created", .connectionCreated), + ("connection.updated", .connectionUpdated), + ("connection.destroyed", .connectionDestroyed), + ("spotlight.changed", .spotlightChanged), + ("network.status", .networkStatus)]) + +/// :nodoc: +extension SignalingNotifyEventType: Codable { + + public init(from decoder: Decoder) throws { + self = try signalingNotifyEventType.decode(from: decoder) + } + + public func encode(to encoder: Encoder) throws { + try signalingNotifyEventType.encode(self, to: encoder) + } + +} + +/// :nodoc: +extension SignalingNotifySpotlightChanged: Codable { + + enum CodingKeys: String, CodingKey { + case client_id + case connection_id + case spotlight_id + case fixed + case audio + case video + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + clientId = try container.decode(String?.self, forKey: .client_id) + connectionId = try container.decode(String?.self, forKey: .connection_id) + spotlightId = try container.decode(String.self, forKey: .spotlight_id) + isFixed = try container.decodeIfPresent(Bool.self, forKey: .fixed) + audioEnabled = try container.decodeIfPresent(Bool.self, forKey: .audio) + videoEnabled = try container.decodeIfPresent(Bool.self, forKey: .video) + } + + public func encode(to encoder: Encoder) throws { + throw SoraError.invalidSignalingMessage + } + +} + +/// :nodoc: +extension SignalingNotifyNetworkStatus: Codable { + + enum CodingKeys: String, CodingKey { + case unstable_level + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + unstableLevel = try container.decode(Int.self, forKey: .unstable_level) + } + + public func encode(to encoder: Encoder) throws { + throw SoraError.invalidSignalingMessage + } + +} + +/// :nodoc: +extension SignalingPong: Codable { + + public init(from decoder: Decoder) throws { + throw SoraError.invalidSignalingMessage + } + + public func encode(to encoder: Encoder) throws { + // エンコードするプロパティはない + } + +} diff --git a/Sora/SignalingChannel.swift b/Sora/SignalingChannel.swift index c00189f8..96ada858 100644 --- a/Sora/SignalingChannel.swift +++ b/Sora/SignalingChannel.swift @@ -22,13 +22,9 @@ public final class SignalingChannelHandlers { /// 接続解除時に呼ばれるブロック public var onDisconnectHandler: ((Error?) -> Void)? - /** - メッセージ受信時に呼ばれるブロック。 - 受信したメッセージをシグナリングメッセージとして解析できれば、 - `message` にシグナリングメッセージが指定されます。 - */ - public var onMessageHandler: ((_ message: SignalingMessage?, _ text: String) -> Void)? - + /// シグナリング受信時に呼ばれるブロック + public var onReceiveSignalingHandler: ((Signaling) -> Void)? + } /** @@ -60,13 +56,13 @@ public protocol SignalingChannel: class { // MARK: - イベントハンドラ /// イベントハンドラ - var handlers: SignalingChannelHandlers { get } + var handlers: SignalingChannelHandlers { get set } /** 内部処理で使われるイベントハンドラ。 このハンドラをカスタマイズに使うべきではありません。 */ - var internalHandlers: SignalingChannelHandlers { get } + var internalHandlers: SignalingChannelHandlers { get set } // MARK: - インスタンスの生成 @@ -101,7 +97,7 @@ public protocol SignalingChannel: class { - parameter message: シグナリングメッセージ */ - func send(message: SignalingMessage) + func send(message: Signaling) /** サーバーに任意のメッセージを送信します。 @@ -142,7 +138,7 @@ class BasicSignalingChannel: SignalingChannel { self.disconnect(error: error) } - webSocketChannel.internalHandlers.onMessageHandler = handleMessage + webSocketChannel.internalHandlers.onMessageHandler = handle } func connect(handler: @escaping (Error?) -> Void) { @@ -204,7 +200,7 @@ class BasicSignalingChannel: SignalingChannel { } } - func send(message: SignalingMessage) { + func send(message: Signaling) { Logger.debug(type: .signalingChannel, message: "send message") let encoder = JSONEncoder() do { @@ -222,7 +218,7 @@ class BasicSignalingChannel: SignalingChannel { webSocketChannel.send(message: .text(text)) } - func handleMessage(_ message: WebSocketMessage) { + func handle(message: WebSocketMessage) { Logger.debug(type: .signalingChannel, message: "receive message") switch message { case .binary(_): @@ -232,24 +228,22 @@ class BasicSignalingChannel: SignalingChannel { case .text(let text): guard let data = text.data(using: .utf8) else { Logger.error(type: .signalingChannel, message: "invalid encoding") - - Logger.debug(type: .signalingChannel, message: "call onMessageHandler") - internalHandlers.onMessageHandler?(nil, text) - handlers.onMessageHandler?(nil, text) return } - var sigMessage: SignalingMessage! + var signaling: Signaling! do { - sigMessage = try SignalingMessage.decode(from: data) + let decoder = JSONDecoder() + signaling = try decoder.decode(Signaling.self, from: data) } catch let error { Logger.error(type: .signalingChannel, message: "decode failed (\(error.localizedDescription)) => \(text)") + return } - Logger.debug(type: .signalingChannel, message: "call onMessageHandler") - internalHandlers.onMessageHandler?(sigMessage, text) - handlers.onMessageHandler?(sigMessage, text) + Logger.debug(type: .signalingChannel, message: "call onReceiveSignalingHandler") + internalHandlers.onReceiveSignalingHandler?(signaling) + handlers.onReceiveSignalingHandler?(signaling) } } diff --git a/Sora/SignalingMessage.swift b/Sora/SignalingMessage.swift deleted file mode 100644 index 7b6e742e..00000000 --- a/Sora/SignalingMessage.swift +++ /dev/null @@ -1,648 +0,0 @@ -import Foundation - -/** - "connect" シグナリングメッセージを表します。 - このメッセージはシグナリング接続の確立後、最初に送信されます。 - */ -public struct SignalingConnectMessage { - - /// ロール - public var role: SignalingRole - - /// チャネル ID - public var channelId: String - - /// メタデータ - public var metadata: String? - - /// SDP 。クライアントの判別に使われます。 - public var sdp: String? - - /// マルチストリームの可否 - public var multistreamEnabled: Bool - - /// Plan B の可否 - public var planBEnabled: Bool - - /// 映像の可否 - public var videoEnabled: Bool - - /// 映像コーデック - public var videoCodec: VideoCodec - - /// 映像ビットレート - public var videoBitRate: Int? - - /// 音声の可否 - public var audioEnabled: Bool - - /// 音声コーデック - public var audioCodec: AudioCodec - - /// 最大話者数 - public var maxNumberOfSpeakers: Int? - -} - -/** - "offer" シグナリングメッセージを表します。 - このメッセージは SDK が "connect" を送信した後に、サーバーから送信されます。 - */ -public struct SignalingOfferMessage { - - /** - クライアントが更新すべき設定を表します。 - */ - public struct Configuration { - - /// ICE サーバーの情報のリスト - public let iceServerInfos: [ICEServerInfo] - - /// ICE 通信ポリシー - public let iceTransportPolicy: ICETransportPolicy - } - - /// クライアント ID - public let clientId: String - - /// SDP メッセージ - public let sdp: String - - /// クライアントが更新すべき設定 - public let configuration: Configuration? - -} - -/** - "notify" シグナリングメッセージで通知されるイベントの種別です。 - 詳細は Sora のドキュメントを参照してください。 - */ -public enum SignalingNotificationEventType: String { - - /// "connection.created" - case connectionCreated = "connection.created" - - /// "connection.updated" - case connectionUpdated = "connection.updated" - - /// "connection.destroyed" - case connectionDestroyed = "connection.destroyed" - - /// "spotlight.changed" - case spotlightChanged = "spotlight.changed" - - /// "network.status" - case networkStatus = "network.status" - -} - -/** - "update" シグナリングメッセージを表します。 - このメッセージは送受信の両方で使用されます。 - - マルチストリーム時にストリームの数が増減するとサーバーから送信されます。 - 受信したメッセージの SDP から Answer としての "update" メッセージを生成してサーバーに送信します。 - */ -public struct SignalingUpdateOfferMessage { - - /// SDP メッセージ - public let sdp: String - -} - -/** - "notify" シグナリングメッセージを表します。 - このメッセージはピア接続の確立後、定期的にサーバーから送信されます。 - */ -public struct SignalingNotifyMessage { - - // MARK: イベント情報 - - /// イベントの種別 - public let eventType: SignalingNotificationEventType - - // MARK: 接続情報 - - /// ロール - public let role: SignalingRole? - - /// チャネル ID - public let channelId: String? - - /// クライアント ID - public let clientId: String? - - /// 接続 ID - public let connectionId: String? - - /// 音声の可否 - public let audioEnabled: Bool? - - /// 映像の可否 - public let videoEnabled: Bool? - - // MARK: 接続状態 - - /// 接続時間 - public let connectionTime: Int? - - /// 接続中のクライアントの数 - public let connectionCount: Int? - - /// 接続中のパブリッシャーの数 - public let publisherCount: Int? - - /// 接続中のサブスクライバーの数 - public let subscriberCount: Int? - - /// ネットワークの不安定度 - public let unstableLevel: Int? - - // MARK: スポットライト機能 - - /// スポットライト ID - public let spotlightId: String? - - /// 固定の有無 - public let isFixed: Bool? - - // MARK: メタデータ - - /// メタデータ - public private(set) var metadata: [Any]? - -} - -/** - "pong" シグナリングメッセージを表します。 - このメッセージはサーバーから "ping" シグナリングメッセージを受信すると - サーバーに送信されます。 - "ping" 受信後、一定時間内にこのメッセージを返さなければ、 - サーバーとの接続が解除されます。 - */ -public struct SignalingPongMessage {} - -/** - "push" シグナリングメッセージを表します。 - このメッセージは Sora のプッシュ API を使用して送信されたデータです。 - */ -public struct SignalingPushMessage { - - /// プッシュ通知で送信される JSON データ。 - /// 無効な JSON データであれば nil になります。 - public private(set) var data: Any? - -} - -// MARK: - - -/** - シグナリングメッセージの種別です。 - */ -public enum SignalingMessage { - - /// "connect" シグナリングメッセージ - case connect(message: SignalingConnectMessage) - - /// "offer" シグナリングメッセージ - case offer(message: SignalingOfferMessage) - - /// "answer" シグナリングメッセージ - case answer(sdp: String) - - /// "candidate" シグナリングメッセージ - case candidate(ICECandidate) - - /// "update" シグナリングメッセージ - case update(sdp: String) - - /// "notify" シグナリングメッセージ - case notify(message: SignalingNotifyMessage) - - /// "ping" シグナリングメッセージ - case ping - - /// "pong" シグナリングメッセージ - case pong - - /** - "disconnect" シグナリングメッセージ。 - このメッセージは接続を解除する際にサーバーに送信されます。 - このメッセージの送信後は、サーバーからの応答はありません。 - */ - case disconnect - - /// "push" シグナリングメッセージ - case push(message: SignalingPushMessage) - - static func decode(from data: Data) throws -> SignalingMessage { - let decoder = JSONDecoder() - var msg = try decoder.decode(SignalingMessage.self, from: data) - switch msg { - case .notify(message: var notify): - notify.parseMetadata(from: data) - msg = .notify(message: notify) - case .push(message: var push): - push.parseData(from: data) - msg = .push(message: push) - default: - break - } - return msg - } - - var shortDescription: String { - get { - switch self { - case .connect(message: let message): - return "connect(\(message.channelId), \(message.role))" - case .offer(message: let message): - return "offer(\(message.clientId))" - case .answer(sdp: _): - return "answer" - case .candidate(let candidate): - if let url = candidate.url { - return "candidate(\(url))" - } else { - return "candidate" - } - case .update(sdp: _): - return "update" - case .notify(message: let message): - return "notify(\(message.eventType))" - case .ping: - return "ping" - case .pong: - return "pong" - case .disconnect: - return "disconnect" - case .push(message: _): - return "push" - } - } - } - -} - -// MARK: - -// MARK: Codable - -/// :nodoc: -extension SignalingRole: Codable { - - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let roleStr = try container.decode(String.self) - guard let role = SignalingRole(rawValue: roleStr) else { - throw DecodingError.dataCorruptedError(in: container, - debugDescription: "invalid 'role' value") - } - self = role - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(rawValue) - } - -} - -/// :nodoc: -extension SignalingConnectMessage: Codable { - - enum CodingKeys: String, CodingKey { - case role - case channelId = "channel_id" - case metadata - case sdp - case multistream - case plan_b - case video - case audio - case vad - } - - enum VideoCodingKeys: String, CodingKey { - case codecType = "codec_type" - case bitRate = "bit_rate" - } - - enum AudioCodingKeys: String, CodingKey { - case codecType = "codec_type" - } - - public init(from decoder: Decoder) throws { - throw SoraError.invalidSignalingMessage - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(role, forKey: .role) - try container.encode(channelId, forKey: .channelId) - - if let sdp = sdp { - try container.encode(sdp, forKey: .sdp) - } - - if let metadata = metadata { - try container.encode(metadata, forKey: .metadata) - } - - if multistreamEnabled { - try container.encode(true, forKey: .multistream) - } - - if planBEnabled { - try container.encode(planBEnabled, forKey: .plan_b) - } - - if videoEnabled { - if videoCodec != .default || videoBitRate != nil { - var videoContainer = container - .nestedContainer(keyedBy: VideoCodingKeys.self, - forKey: .video) - if videoCodec != .default { - try videoContainer.encode(videoCodec, forKey: .codecType) - } - if let bitRate = videoBitRate { - try videoContainer.encode(bitRate, forKey: .bitRate) - } - } - } else { - try container.encode(false, forKey: .video) - } - - if audioEnabled { - switch audioCodec { - case .default: - break - default: - var audioContainer = container - .nestedContainer(keyedBy: AudioCodingKeys.self, forKey: .audio) - try audioContainer.encode(audioCodec, forKey: .codecType) - } - } else { - try container.encode(false, forKey: .audio) - } - - if let num = maxNumberOfSpeakers { - try container.encode(num, forKey: .vad) - } - } - -} - -/// :nodoc: -extension SignalingOfferMessage.Configuration: Codable { - - enum CodingKeys: String, CodingKey { - case iceServerInfos = "iceServers" - case iceTransportPolicy = "iceTransportPolicy" - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - iceServerInfos = try container.decode([ICEServerInfo].self, - forKey: .iceServerInfos) - iceTransportPolicy = try container.decode(ICETransportPolicy.self, - forKey: .iceTransportPolicy) - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(iceServerInfos, forKey: .iceServerInfos) - try container.encode(iceTransportPolicy, forKey: .iceTransportPolicy) - } - -} - -/// :nodoc: -extension SignalingOfferMessage: Codable { - - enum CodingKeys: String, CodingKey { - case clientId = "client_id" - case sdp - case configuration = "config" - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - clientId = try container.decode(String.self, forKey: .clientId) - sdp = try container.decode(String.self, forKey: .sdp) - if container.contains(.configuration) { - configuration = try container.decode(Configuration.self, - forKey: .configuration) - } else { - configuration = nil - } - } - - public func encode(to encoder: Encoder) throws { - throw SoraError.invalidSignalingMessage - } - -} - -/// :nodoc: -extension SignalingUpdateOfferMessage: Codable { - - enum CodingKeys: String, CodingKey { - case sdp - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - sdp = try container.decode(String.self, forKey: .sdp) - } - - public func encode(to encoder: Encoder) throws { - throw SoraError.invalidSignalingMessage - } - -} - -/// :nodoc: -extension SignalingNotifyMessage: Codable { - - enum CodingKeys: String, CodingKey { - case eventType = "event_type" - case role = "role" - case connectionTime = "minutes" - case connectionCount = "channel_connections" - case publisherCount = "channel_upstream_connections" - case subscriberCount = "channel_downstream_connections" - case channelId = "channel_id" - case clientId = "client_id" - case connectionId = "connection_id" - case spotlightId = "spotlight_id" - case audio = "audio" - case video = "video" - case fixed = "fixed" - case unstableLevel = "unstable_level" - case metadata = "metadata" - case metadataList = "metadata_list" - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - eventType = SignalingNotificationEventType(rawValue: - try container.decode(String.self, forKey: .eventType))! - - if let raw = try container.decodeIfPresent(String.self, forKey: .role) { - role = SignalingRole(rawValue: raw) - } else { - role = nil - } - - connectionTime = try container.decodeIfPresent(Int.self, forKey: .connectionTime) - connectionCount = try container.decodeIfPresent(Int.self, forKey: .connectionCount) - publisherCount = try container.decodeIfPresent(Int.self, forKey: .publisherCount) - subscriberCount = try container.decodeIfPresent(Int.self, forKey: .subscriberCount) - channelId = try container.decodeIfPresent(String.self, forKey: .channelId) - clientId = try container.decodeIfPresent(String.self, forKey: .clientId) - connectionId = try container.decodeIfPresent(String.self, forKey: .connectionId) - audioEnabled = try container.decodeIfPresent(Bool.self, forKey: .audio) - videoEnabled = try container.decodeIfPresent(Bool.self, forKey: .video) - spotlightId = try container.decodeIfPresent(String.self, forKey: .spotlightId) - isFixed = try container.decodeIfPresent(Bool.self, forKey: .fixed) - unstableLevel = try container.decodeIfPresent(Int.self, forKey: .unstableLevel) - - // metadata には任意のデータが入るため、 Decoder ではデコードできない - } - - mutating func parseMetadata(from data: Data) { - do { - let json = try JSONSerialization.jsonObject(with: data, options: []) - if let msg = json as? [String: Any] { - if let metadata = - msg[SignalingNotifyMessage.CodingKeys.metadata.rawValue] { - self.metadata = [metadata] - } else if let metadataList = - msg[SignalingNotifyMessage.CodingKeys.metadataList.rawValue] { - self.metadata = metadataList as? [Any] - } - } - } catch { - // 何もしない - } - } - - public func encode(to encoder: Encoder) throws { - throw SoraError.invalidSignalingMessage - } - -} - - -/// :nodoc: -extension SignalingPushMessage: Codable { - - enum CodingKeys: String, CodingKey { - case data = "data" - } - - public init(from decoder: Decoder) throws { - // 解析すべきプロパティがないので何もしない - } - - mutating func parseData(from data: Data) { - self.data = nil - do { - let json = try JSONSerialization.jsonObject(with: data, options: []) - if let msg = json as? [String: Any] { - self.data = msg[SignalingPushMessage.CodingKeys.data.rawValue] - } - } catch { - // 何もしない - } - } - - public func encode(to encoder: Encoder) throws { - throw SoraError.invalidSignalingMessage - } - -} - -/// :nodoc: -extension SignalingMessage: Codable { - - enum MessageType: String { - case connect - case offer - case answer - case update - case candidate - case notify - case ping - case pong - case disconnect - case push - } - - enum CodingKeys: String, CodingKey { - case type - case sdp - case candidate - case data - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let type = try container.decode(String.self, forKey: .type) - switch type { - case "offer": - self = .offer(message: try SignalingOfferMessage(from: decoder)) - case "update": - let update = try SignalingUpdateOfferMessage(from: decoder) - self = .update(sdp: update.sdp) - case "notify": - self = .notify(message: try SignalingNotifyMessage(from: decoder)) - case "ping": - self = .ping - case "push": - self = .push(message: try SignalingPushMessage(from: decoder)) - default: - throw SoraError.unknownSignalingMessageType(type: type) - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - switch self { - case .connect(message: let message): - try container.encode(MessageType.connect.rawValue, forKey: .type) - try message.encode(to: encoder) - case .offer(message: let message): - try container.encode(MessageType.offer.rawValue, forKey: .type) - try message.encode(to: encoder) - case .answer(sdp: let sdp): - try container.encode(MessageType.answer.rawValue, forKey: .type) - try container.encode(sdp, forKey: .sdp) - case .candidate(let candidate): - try container.encode(MessageType.candidate.rawValue, forKey: .type) - try container.encode(candidate.sdp, forKey: .candidate) - case .update(sdp: let sdp): - try container.encode(MessageType.update.rawValue, forKey: .type) - try container.encode(sdp, forKey: .sdp) - case .pong: - try container.encode(MessageType.pong.rawValue, forKey: .type) - case .disconnect: - try container.encode(MessageType.disconnect.rawValue, forKey: .type) - default: - throw SoraError.invalidSignalingMessage - } - } - -} - -// MARK: - CustomStringConvertible - -extension SignalingOfferMessage.Configuration: CustomStringConvertible { - - public var description: String { - let encoder = JSONEncoder() - let data = try! encoder.encode(self) - return String(data: data, encoding: .utf8)! - } - -} diff --git a/Sora/Utilities.swift b/Sora/Utilities.swift index f14f44fe..c18158bb 100644 --- a/Sora/Utilities.swift +++ b/Sora/Utilities.swift @@ -53,9 +53,12 @@ public struct Utilities { final class PairTable { + var name: String + private var pairs: [(T, U)] - init(pairs: [(T, U)]) { + init(name: String, pairs: [(T, U)]) { + self.name = name self.pairs = pairs } @@ -78,7 +81,7 @@ extension PairTable where T == String { let key = try container.decode(String.self) return try right(other: key).unwrap { throw DecodingError.dataCorruptedError(in: container, - debugDescription: "invalid value") + debugDescription: "\(self.name) cannot decode '\(key)'") } } @@ -86,6 +89,9 @@ extension PairTable where T == String { var container = encoder.singleValueContainer() if let key = left(other: value) { try container.encode(key) + } else { + throw EncodingError.invalidValue(value, + EncodingError.Context(codingPath: [], debugDescription: "\(self.name) cannot encode \(value)")) } } diff --git a/Sora/VideoCodec.swift b/Sora/VideoCodec.swift index 44ed40e3..fc2b07f8 100644 --- a/Sora/VideoCodec.swift +++ b/Sora/VideoCodec.swift @@ -1,7 +1,8 @@ import Foundation private let descriptionTable: PairTable = - PairTable(pairs: [("default", .default), + PairTable(name: "VideoCodec", + pairs: [("default", .default), ("VP8", .vp8), ("VP9", .vp9), ("H264", .h264)]) diff --git a/Sora/VideoFrame.swift b/Sora/VideoFrame.swift index 757a6ddc..df603d11 100644 --- a/Sora/VideoFrame.swift +++ b/Sora/VideoFrame.swift @@ -7,7 +7,7 @@ import WebRTC 現在の実装では次の映像フレームに対応しています。 - ネイティブの映像フレーム (`RTCVideoFrame`) - - `CMSampleBuffer` (`RTCVideoFrame` に変換されます) + - `CMSampleBuffer` (映像のみ、音声は非対応。 `RTCVideoFrame` に変換されます) */ public enum VideoFrame { @@ -58,6 +58,8 @@ public enum VideoFrame { 指定されたサンプルバッファーからピクセル画像データを取得できなければ `nil` を返します。 + 音声データを含むサンプルバッファーには対応していません。 + - parameter sampleBuffer: ピクセルバッファーを含むサンプルバッファー */ public init?(from sampleBuffer: CMSampleBuffer) { diff --git a/Sora/WebRTCConfigration.swift b/Sora/WebRTCConfigration.swift index 2b1c7366..3553d62c 100644 --- a/Sora/WebRTCConfigration.swift +++ b/Sora/WebRTCConfigration.swift @@ -27,7 +27,7 @@ public struct MediaConstraints { SDP でのマルチストリームの記述方式です。 */ public enum SDPSemantics { - + /// Plan B case planB @@ -106,7 +106,8 @@ public struct WebRTCConfiguration { } private var sdpSemanticsTable: PairTable = - PairTable(pairs: [("planB", .planB), + PairTable(name: "SDPSemantics", + pairs: [("planB", .planB), ("unifiedPlan", .unifiedPlan)]) /// :nodoc: