6
4

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-05-02

image.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

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

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

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

ターミナル
% pod init

同じディレクトリに作成されたPodfileが作成されます。SkyWayの公式チュートリアルを参考にPodfileにSkyWay SDKを追加します。
このままだとM1 Mac環境ではシミュレーターやSwiftUIのプレビューでビルドが通らないので、SkyWayサポート記事を参考に、Podfileの末尾にシミュレーター環境としてarm64を指定します。

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をインストールします。

ターミナル
% pod setup
% pod install

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

image.png

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

image.png

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

image.png

SkyWay SDKをラップする

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

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の初期化後は着信待ちになるため、相手からの着信にも対応しています。

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

これで完成です。やはりSwiftUIで開発できると効率がいいですね。
次回は複数人とビデオチャットが可能なROOM機能を試してみます。

参考

blog記事を参考にさせていただきました。貴重な情報ありがとうございました。

公式ドキュメント

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4