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を指定します。
SkyWay SDKをプロジェクトに追加(CocoaPodsを利用)
CocoaPodsをまだインストールしていない場合は公式サイトに従ってインストールしておきます。
https://cocoapods.org
ターミナルでXcodeのプロジェクトのディレクトリに移動してpodsを初期化します。
% pod init
同じディレクトリに作成されたPodfileが作成されます。SkyWayの公式チュートリアルを参考にPodfileにSkyWay SDKを追加します。
このままだとM1 Mac環境ではシミュレーターやSwiftUIのプレビューでビルドが通らないので、SkyWayサポート記事を参考に、Podfileの末尾にシミュレーター環境としてarm64を指定します。
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が組み込まれたプロジェクトファイル(白背景で青字のアイコン)を開きます。
Xcodeが起動したら、Build Settingsを開いて、Excluded ArchitecturesのiOSシミュレータにarm64を指定します。これで、シミュレーターと実機のどちらでもビルドが通るようになります。
そして、info.plistにカメラ(NSCameraUsageDescription)とマイク(NSMicrophoneUsageDescription)のアクセス許可を追加しておきます。
SkyWay SDKをラップする
現在のところSkyWay SDKはSwiftUIに対応していないため、UIViewRepresentableを使ってラップします。
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の初期化後は着信待ちになるため、相手からの着信にも対応しています。
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()
}
}
実行結果
これで完成です。やはりSwiftUIで開発できると効率がいいですね。
次回は複数人とビデオチャットが可能なROOM機能を試してみます。
参考
blog記事を参考にさせていただきました。貴重な情報ありがとうございました。