         }
     }
 
-    func sendConnectMessage(error: Error?) {
+    func add(stream: MediaStream) {
+        streams.append(stream)
+        Logger.debug(type: .peerChannel, message: "call onAddStream")
+        internalHandlers.onAddStream?(stream)
+    }
+
+    func remove(streamId: String) {
+        let stream = streams.first { stream in stream.streamId == streamId }
+        if let stream = stream {
+            remove(stream: stream)
+        }
+    }
+
+    func remove(stream: MediaStream) {
+        streams = streams.filter { each in each.streamId != stream.streamId }
+        Logger.debug(type: .peerChannel, message: "call onRemoveStream")
+        internalHandlers.onRemoveStream?(stream)
+    }
+
+    func add(iceCandidate: ICECandidate) {
+        iceCandidates.append(iceCandidate)
+    }
+
+    func remove(iceCandidate: ICECandidate) {
+        iceCandidates = iceCandidates.filter { each in each == iceCandidate }
+    }
+
+    // MARK: - Private methods
+
+    private func sendConnectMessage(error: Error?) {
         if let error = error {
             Logger.error(type: .peerChannel,
                          message: "failed connecting to signaling channel (\(error.localizedDescription))")

    // type: redirect のために SDP を保存しておく
    // 値が設定されている場合2回目の type: connect メッセージ送信とみなし、 redirect 中であると判断する
    private var sdp: String?
    
    func connect(handler: @escaping (Error?) -> Void) {
        if state == .connecting || state == .connected {
            handler(SoraError.connectionBusy(reason:

        }
    }

    private func createAndSendAnswer(offer: SignalingOffer) {
        Logger.debug(type: .peerChannel, message: "try sending answer")
        offerEncodings = offer.encodings

