SkyWayをSwiftUIで実装する(M1 Macを使用)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77444/9d13bd58-954d-c69e-a70d-97000765a2b0.png)

SkyWayをSwiftUIで実装した際に、色々とハマったので、そのときのメモです。

現在、SkyWay Betaが公開されていますが、iOS用のSDKはまだ公開されていないので、SkyWay Community Editionを使って実装しました。

## ハマりどころと対処したこと
- SkyWay SDKがM1 Mac環境に対応していない → ビルド指定にarm64を追加
- SkyWay SDKがSwiftUIに対応していない → UIViewRepresentableでSKWVideoをラッピング

## 使用環境
- MacBook Air (M1, 2020)
- macOS Monterey 12.3.1
- Xcode 13.3.1

備忘録を兼ねて、最初から順番にやっていきます。(2022/5/1現在)

# Xcodeプロジェクトを新規作成

InterfaceにSwiftUI、LnaguageにSiwftを指定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77444/8f466c2f-ffc3-c99f-7123-8f91c9335415.png)


# SkyWay SDKをプロジェクトに追加(CocoaPodsを利用)

CocoaPodsをまだインストールしていない場合は公式サイトに従ってインストールしておきます。
https://cocoapods.org

ターミナルでXcodeのプロジェクトのディレクトリに移動してpodsを初期化します。
```zsh:ターミナル
% pod init
```

同じディレクトリに作成されたPodfileが作成されます。[SkyWayの公式チュートリアル](https://webrtc.ecl.ntt.com/documents/ios-sdk.html)を参考にPodfileにSkyWay SDKを追加します。
このままだとM1 Mac環境ではシミュレーターやSwiftUIのプレビューでビルドが通らないので、[SkyWayサポート記事](https://support.skyway.io/hc/ja/community/posts/1500000204522-M1-macでのビルドエラー)を参考に、Podfileの末尾にシミュレーター環境としてarm64を指定します。

```Ruby:Podfile
target 'SkyWayP2pDemo' do
  # Pods for SkyWayP2pDemo
  pod 'SkyWay'

end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
    end
  end
end
```

Podfileを保存して、SkyWay SDKをインストールします。
```zsh:ターミナル
% pod setup
% pod install
```

いったんXcodeを閉じて、podsが組み込まれたプロジェクトファイル(白背景で青字のアイコン)を開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77444/69463457-2352-840a-690e-a18c86a3dfa1.png)

Xcodeが起動したら、Build Settingsを開いて、Excluded ArchitecturesのiOSシミュレータにarm64を指定します。これで、シミュレーターと実機のどちらでもビルドが通るようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77444/0d752f3b-2a25-98e5-9d58-c187d7ae9c90.png)

そして、info.plistにカメラ(NSCameraUsageDescription)とマイク(NSMicrophoneUsageDescription)のアクセス許可を追加しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77444/ccc3e7f2-f16b-0151-103d-785945c11ff5.png)


# SkyWay SDKをラップする

現在のところSkyWay SDKはSwiftUIに対応していないため、UIViewRepresentableを使ってラップします。

```swift:VideoView.swift
import SwiftUI
import SkyWay

struct VideoView: UIViewRepresentable {
    typealias UIViewType = SKWVideo
    let stream: SKWMediaStream
    let videoView = UIViewType()

    func makeUIView(context: Context) -> UIViewType {
        return videoView
    }

    func updateUIView(_ uiView: UIViewType, context: Context) {
        stream.addVideoRenderer(uiView, track: 0)
    }

    final class Coordinator: NSObject {
        let parent: VideoView
        init(_ parent: VideoView) {
            self.parent = parent
        }
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    static func dismantleUIView(_ uiView: UIViewType, coordinator: Coordinator) {
        // perform additional clean-up work related custom view
        coordinator.parent.stream.removeVideoRenderer(uiView, track: 0)
    }
}

struct VideoView_Previews: PreviewProvider {
    static var previews: some View {
        Text("test")
    }
}
```

# サンプルアプリ

SwiftUIのZStackを使って、自分の映像用と相手の映像用の二つのVideoViewを重畳表示するサンプルです。相手のPeer IDを指定してコールすることができます。また、Peerの初期化後は着信待ちになるため、相手からの着信にも対応しています。

```swift:ContentView.swift
import SwiftUI
import SkyWay

struct ContentView: View {
    @State var myPeerId = ""
    @State var remotePeerId = ""
    @State var peer: SKWPeer?
    @State var localStream: SKWMediaStream?
    @State var remoteStream: SKWMediaStream?
    @State var mediaConnection: SKWMediaConnection?
    let skywayAPIKey = "YOU API KEY"
    let skywayDomain = "localhost"
    
    var body: some View {
        VStack {
            HStack {
                Text("自分:")
                    .padding()
                
                TextField("My peer ID", text: $myPeerId)
                    .padding()
                
                Button("init") {
                    let option = SKWPeerOption.init()
                    option.key = skywayAPIKey
                    option.domain = skywayDomain
                    if let peer = SKWPeer(options: option) {
                        self.peer = peer
                        
                        registerPeerCallbacks(peer)
                        
                        SKWNavigator.initialize(peer)
                        let constraints = SKWMediaConstraints()
                        constraints.minFrameRate = 30
                        localStream = SKWNavigator.getUserMedia(constraints)
                    }
                }
                .padding()
            }

            if peer != nil {
                HStack {
                    Text("相手: ")
                        .padding()
                    
                    TextField("Remote peer ID", text: $remotePeerId)
                        .padding()

                    Button("call") {
                        // ソフトウェアキーボードを閉じる
                        UIApplication.shared.endEditing()
                        
                        let callOption = SKWCallOption()
                        if let mediaConnection = peer?.call(withId: remotePeerId, stream: localStream, options: callOption){
                            self.mediaConnection = mediaConnection
                            registerMediaConnectionCallbacks(mediaConnection)
                        } else {
                            print("failed to call:", remotePeerId)
                        }
                    }
                    .padding()
                }
                
                ZStack {
                    GeometryReader { geometry in
                        let frameWidth = geometry.frame(in: .global).width
                        let frameHeight = geometry.frame(in: .global).height
                        
                        if let remoteStream = remoteStream {
                            VideoView(stream: remoteStream)
                                .shadow(radius: 7)
                        }
                        
                        if let localStream = localStream {
                            let width = frameWidth / 3
                            let height = frameHeight / 3
                            let x = frameWidth - width / 2 - 10
                            let y = frameHeight - height / 2 - 10

                            VideoView(stream: localStream)
                                .frame(width: width, height: height)
                                .padding()
                                .position(x: x, y: y)
                                .shadow(radius: 7)
                        }
                    }
                }
                .padding()
            }
        }
    }

    func registerPeerCallbacks(_ peer: SKWPeer) {
        peer.on(.PEER_EVENT_OPEN, callback: { (obj) -> Void in
            if let peerId = obj as? String {
                myPeerId = peerId
                print("PEER_EVENT_OPEN: peerId:", peerId)
            }
        })
        
        peer.on(.PEER_EVENT_CONNECTION, callback: { (obj) -> Void in
            if let dataConnection = obj as? SKWDataConnection {
                print("PEER_EVENT_CONNECTION: dataConnection:", dataConnection)
            }
        })
        
        peer.on(.PEER_EVENT_CALL, callback: { (obj) -> Void in
            if let connection = obj as? SKWMediaConnection {
                mediaConnection = connection
                registerMediaConnectionCallbacks(connection)
                connection.answer(localStream)
                print("PEER_EVENT_CALL")
            }
        })
        
        peer.on(.PEER_EVENT_CLOSE, callback: { (obj) -> Void in
            self.peer = nil
            print("PEER_EVENT_CLOSE")
        })
        
        peer.on(.PEER_EVENT_ERROR, callback: { (obj) -> Void in
            if let error = obj as? SKWPeerError {
                print("ROOM_EVENT_ERROR: error:", error)
            }
        })
    }
    
    func registerMediaConnectionCallbacks(_ connection: SKWMediaConnection){
        connection.on(.MEDIACONNECTION_EVENT_STREAM, callback: { (obj) -> Void in
            if let mediaStream = obj as? SKWMediaStream {
                remoteStream = mediaStream
                if let peerID = remoteStream?.peerId {
                    remotePeerId = peerID
                }
                print("MEDIACONNECTION_EVENT_STREAM")
            }
        })
        
        connection.on(.MEDIACONNECTION_EVENT_REMOVE_STREAM, callback: { (obj) -> Void in
            if let _ = obj as? SKWMediaStream {
                remoteStream = nil
                print("MEDIACONNECTION_EVENT_REMOVE_STREAM")
            }
        })
        
        connection.on(.MEDIACONNECTION_EVENT_CLOSE, callback: { (obj) -> Void in
            if let _ = obj as? SKWMediaConnection {
                remoteStream = nil
                mediaConnection = nil
                print("MEDIACONNECTION_EVENT_CLOSE")
            }
        })
        
        connection.on(.MEDIACONNECTION_EVENT_ERROR, callback: { (obj) -> Void in
            if let error = obj as? SKWPeerError {
                print("MEDIACONNECTION_EVENT_ERROR: error:", error)
            }
        })
    }
}

extension UIApplication {
    func endEditing() {
        sendAction(
            #selector(UIResponder.resignFirstResponder),
            to: nil,
            from: nil,
            for: nil
        )
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```

# 実行結果

自分と相手のPeer IDとカメラ画像が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77444/e7748a74-ed52-5c82-6eeb-cbd595eaeadf.png)

これで完成です。やはりSwiftUIで開発できると効率がいいですね。
[次回](https://qiita.com/yuppejp/items/3ab8e69d8b731b960c99)は複数人とビデオチャットが可能なROOM機能を試してみます。

# 参考
blog記事を参考にさせていただきました。貴重な情報ありがとうございました。
- [SwiftUIでSkyWayの映像を表示する（SwiftUIでSKWVideoを使う方法)](https://zenn.dev/yorifuji/articles/swiftui-skyway-skwvideo)
- [SkyWay ~WebRTCアプリ開発~](https://blog.dreamonline.co.jp/2020/05/01/skywaywebrtcアプリ開発)

## 公式ドキュメント
- [SkyWay iOS SDK ドキュメント](https://webrtc.ecl.ntt.com/documents/ios-sdk.html)
- [SkyWay iOS SDK リファレンス](https://webrtc.ecl.ntt.com/ios-reference/)