🚀 クイックスタート

SkyWay のメディア通信を体験できるシンプルなサンプルアプリケーションを作成します。

本チュートリアルでは SFU でのメディア通信を使用します。

ここでは、iPhone のマイク音源とカメラ映像を SFU サーバーに送信し、それぞれを自分が受信して音声を再生と映像の描画するというアプリケーションを作ります。

このクイックスタートの実行には iPhone 実機が必要です。

完成品は https://github.com/skyway/ios-sdk/tree/main/Tutorial にあります。

開発環境

  • Xcode 26.0.1
  • iOS 26.0.1

アプリケーション ID とシークレットキーの取得

※SkyWay への登録がまだの方はこちらから

SkyWay コンソールへログインし、以下の 3 つを行います。

  1. 「アプリケーションを作成」ボタンを押す Peer
  2. アプリケーション名を入力して作成ボタンを押す
  3. アプリケーション一覧からアプリケーション ID とシークレットキーをコピーする

SkyWay Auth Token の作成

SkyWay を利用するためには、初めに JWT(JSON Web Token)を用いて Context を初期化します。

SkyWay Auth Token は本来サーバーサイドで生成するため、 iOS SDK にはトークンの生成機能はございません。

クイックスタートでは、 Dev 環境専用の API である Context.setupForDev(withAppId:secretKey:options:completion:) を用いて初期化するため、 SkyWay Auth Token の作成は省略します。

認証認可について、詳しくはこちらをご覧ください。

XcodeProjectの作成・フレームワークの設定

新規で Xcodeproject を作成してください。

作成時、Interface は SwiftUI を選択してください。

パーミッションの設定

info.plistPrivacy - Microphone Usage DescriptionPrivacy - Camera Usage Description を追加して、value にはユーザーに利用許可を確認するプロンプトのメッセージを登録してください。

ここでは、マイクを利用しますカメラを利用します と登録します。

InfoPlist

SDKのダウンロード

今回は Swift Package Manager にてダウンロードします。

Xcode から Project を選択し、 Package Dependencies を選択します。

左下 + ボタンからパッケージ検索のモーダルを表示させ、右上の URL 検索ボックスに https://github.com/skyway/ios-sdk.git と入力します。

Package Product SkyWayRoom がチェックされていることを確認し、 Add Package を押下します。

映像ビューのセットアップ

今回は、カメラからキャプチャしている Local の View (プレビュー)と SFU サーバーから受信した Remote の View を描画します。

SwiftUI では UIKit の CameraPreviewViewVideoViewUIViewRepresentable でラップして使用します。

CameraPreview.swift を新規作成します。updateUIView 内で CameraVideoSource.shared().attach(_:) を呼ぶことで、カメラ映像のプレビューを描画します。

import SwiftUI import SkyWayRoom struct CameraPreview: UIViewRepresentable { func makeUIView(context: Context) -> CameraPreviewView { return CameraPreviewView() } func updateUIView(_ uiView: CameraPreviewView, context: Context) { CameraVideoSource.shared().attach(uiView) } static func dismantleUIView(_ uiView: CameraPreviewView, coordinator: ()) { CameraVideoSource.shared().detach(uiView) } }

次に RemoteVideoView.swift を新規作成します。RemoteVideoStream を受け取り、updateUIView 内でアタッチすることで SFU から受信した映像を描画します。

import SwiftUI import SkyWayRoom struct RemoteVideoView: UIViewRepresentable { let stream: RemoteVideoStream? func makeUIView(context: Context) -> VideoView { let view = VideoView() view.videoContentMode = .scaleAspectFit return view } func updateUIView(_ uiView: VideoView, context: Context) { stream?.attach(uiView) } static func dismantleUIView(_ uiView: VideoView, coordinator: ()) {} }

Contextのセットアップ

RoomViewModel.swift を新規作成し、SkyWay のロジックをまとめます。

@MainActorObservableObject を指定し、@Published で Remote の映像ストリームを保持します。

import SkyWayRoom でフレームワークを import してください。

アプリケーション ID とシークレットキーをメンバー変数で宣言します。

Context.setupForDev(withAppId:secretKey:options:completion:) で SkyWay のセットアップを行います。

Context.setupForDev(withAppId:secretKey:options:completion:) は Dev 環境での利用が想定されている API です。 本番環境では、SecretKeyを秘匿するため Context.setup(withToken:options:completion:) をご利用ください。

import Foundation import SkyWayRoom @MainActor final class RoomViewModel: ObservableObject { @Published var remoteVideoStream: RemoteVideoStream? func start() async { let appId = "アプリケーションIDを入力してください" let secretKey = "シークレットキーを入力してください" // SkyWayのセットアップ let contextOpt: ContextOptions = .init() contextOpt.logLevel = .trace try? await Context.setupForDev(withAppId: appId, secretKey: secretKey, options: contextOpt)

Roomの作成

SkyWay のセットアップが完了したら Room を作成します。

Room.create(with:completion:) で Room を作成できます。

let roomInit: Room.InitOptions = .init() guard let room: Room = try? await .create(with: roomInit) else { print("[Tutorial] Creating room failed.") return }

Roomへの参加

Room.join(with:completion:) でルームに参加し、メンバーを作成します。

let memberInit: Room.MemberInitOptions = .init() memberInit.name = "Alice" // Memberに名前を付けることができます guard let member = try? await room.join(with: memberInit) else { print("[Tutorial] Join failed.") return }

マイク音源のAudioStreamの作成とRoomへのPublish

マイクを音声入力として Stream を作成します。

Publish をするための LocalStream を作成するためには、Source が必要です。

MicrophoneAudioSource のイニシャライザから Source を作成し、createStream() で LocalStream を作成します。

通信方式は P2P とメディアサーバーを介する SFU がありますが、今回は SFU を利用します。

通信方式は RoomPublicationOptionstype プロパティで指定できます。

LocalRoomMember.publish(_:options:completion:) で Room に Publish します。

// AudioStreamの作成 let audioSource: MicrophoneAudioSource = .init() let audioStream = audioSource.createStream() let audioPublicationOptions: RoomPublicationOptions = .init() audioPublicationOptions.type = .SFU guard let audioPublication = try? await member.publish(audioStream, options: audioPublicationOptions) else { print("[Tutorial] Publishing failed.") return }

AudioStreamのSubscribe

先ほど Publish した Stream を LocalRoomMember.subscribe(publicationId:options:completion:) を利用して、Subscribe して Stream を受け取ります。

PublicationID は RoomPublicationid で取得できます。

※P2P方式では、自分のPublishしたStreamをSubscribeできませんのでご注意ください
// Audioの場合、subscribeした時から音声が流れます guard let _ = try? await member.subscribe(publicationId: audioPublication.id, options: nil) else { print("[Tutorial] Subscribing failed.") return } print("🎉Subscribing audio stream successfully.")

カメラの設定とキャプチャの開始

カメラ映像ソースの場合はカメラの設定とキャプチャ操作が必要です。

CameraVideoSource.supportedCameras() で SkyWay がサポートしているカメラの一覧を取得できます。

今回は前面カメラデバイスを取得します。

// Cameraの設定 guard let camera = CameraVideoSource.supportedCameras().first(where: { $0.position == .front }) else { print("Supported cameras is not found.") return }

CameraVideoSource.shared().startCapturing(with:options:completion:) でキャプチャーを開始します。

キャプチャが開始されると、CameraPreview が自動的にカメラ映像を描画します。

// キャプチャーの開始 try! await CameraVideoSource.shared().startCapturing(with: camera, options: nil)

カメラ映像ソースのVideoStreamの作成とRoomへのPublish

Stream は CameraVideoSource.shared().createStream() で作成できます。

AudioStream 同様、publish(_:options:completion:) で Publish を行います。

// VideoStreamの作成 let localVideoStream = CameraVideoSource.shared().createStream() let videoPublicationOptions: RoomPublicationOptions = .init() videoPublicationOptions.type = .SFU guard let videoPublication = try? await member.publish(localVideoStream, options: videoPublicationOptions) else { print("[Tutorial] Publishing failed.") return }

VideoStreamのSubscribe

AudioStream 同様、subscribe(publicationId:completion:) を利用して、Subscribe して Stream を受け取ります。

guard let videoSubscription = try? await member.subscribe(publicationId: videoPublication.id, options: nil) else { print("[Tutorial] Subscribing failed.") return } print("🎉Subscribing video stream successfully.")

RemoteVideoViewの描画

Subscription の stream を取得し、RemoteVideoStream にキャストします。

remoteVideoStream プロパティに代入すると ContentView が再描画され、RemoteVideoView に SFU から受信した映像が表示されます。

let remoteVideoStream = videoSubscription.stream as! RemoteVideoStream self.remoteVideoStream = remoteVideoStream

View と ViewModel を接続

ContentView.swiftRoomViewModel@StateObject として保持し、.onAppearstart() を呼び出します。

import SwiftUI struct ContentView: View { @StateObject private var viewModel = RoomViewModel() var body: some View { VStack(spacing: 0) { CameraPreview() .frame(maxWidth: .infinity, maxHeight: .infinity) RemoteVideoView(stream: viewModel.remoteVideoStream) .frame(maxWidth: .infinity, maxHeight: .infinity) } .onAppear { Task { await viewModel.start() } } } }

Tutorial完成コード

CameraPreview.swift

import SwiftUI import SkyWayRoom struct CameraPreview: UIViewRepresentable { typealias UIViewType = CameraPreviewView typealias Context = UIViewRepresentableContext<Self> func makeUIView(context: Context) -> CameraPreviewView { return CameraPreviewView() } func updateUIView(_ uiView: CameraPreviewView, context: Context) { CameraVideoSource.shared().attach(uiView) } static func dismantleUIView(_ uiView: CameraPreviewView, coordinator: ()) { CameraVideoSource.shared().detach(uiView) } }

RemoteVideoView.swift

import SwiftUI import SkyWayRoom struct RemoteVideoView: UIViewRepresentable { typealias UIViewType = VideoView typealias Context = UIViewRepresentableContext<Self> let stream: RemoteVideoStream? func makeUIView(context: Context) -> VideoView { let view = VideoView() view.videoContentMode = .scaleAspectFit return view } func updateUIView(_ uiView: VideoView, context: Context) { stream?.attach(uiView) } static func dismantleUIView(_ uiView: VideoView, coordinator: ()) {} }

RoomViewModel.swift

import Foundation import SkyWayRoom @MainActor final class RoomViewModel: ObservableObject { @Published var remoteVideoStream: RemoteVideoStream? func start() async { let appId = "アプリケーションIDを入力してください" let secretKey = "シークレットキーを入力してください" // SkyWayのセットアップ let contextOpt: ContextOptions = .init() contextOpt.logLevel = .trace try? await Context.setupForDev(withAppId: appId, secretKey: secretKey, options: contextOpt) let roomInit: Room.InitOptions = .init() guard let room: Room = try? await .create(with: roomInit) else { print("[Tutorial] Creating room failed.") return } let memberInit: Room.MemberInitOptions = .init() memberInit.name = "Alice" // Memberに名前を付けることができます guard let member = try? await room.join(with: memberInit) else { print("[Tutorial] Join failed.") return } // AudioStreamの作成 let audioSource: MicrophoneAudioSource = .init() let audioStream = audioSource.createStream() let audioPublicationOptions: RoomPublicationOptions = .init() audioPublicationOptions.type = .SFU guard let audioPublication = try? await member.publish(audioStream, options: audioPublicationOptions) else { print("[Tutorial] Publishing failed.") return } // Audioの場合、subscribeした時から音声が流れます guard let _ = try? await member.subscribe(publicationId: audioPublication.id, options: nil) else { print("[Tutorial] Subscribing failed.") return } print("🎉Subscribing audio stream successfully.") // Cameraの設定 guard let camera = CameraVideoSource.supportedCameras().first(where: { $0.position == .front }) else { print("Supported cameras is not found.") return } // キャプチャーの開始 try! await CameraVideoSource.shared().startCapturing(with: camera, options: nil) // VideoStreamの作成 let localVideoStream = CameraVideoSource.shared().createStream() let videoPublicationOptions: RoomPublicationOptions = .init() videoPublicationOptions.type = .SFU guard let videoPublication = try? await member.publish(localVideoStream, options: videoPublicationOptions) else { print("[Tutorial] Publishing failed.") return } guard let videoSubscription = try? await member.subscribe(publicationId: videoPublication.id, options: nil) else { print("[Tutorial] Subscribing failed.") return } print("🎉Subscribing video stream successfully.") let remoteVideoStream = videoSubscription.stream as! RemoteVideoStream self.remoteVideoStream = remoteVideoStream } }

ContentView.swift

import SwiftUI struct ContentView: View { @StateObject private var viewModel = RoomViewModel() var body: some View { VStack(spacing: 0) { CameraPreview() .frame(maxWidth: .infinity, maxHeight: .infinity) RemoteVideoView(stream: viewModel.remoteVideoStream) .frame(maxWidth: .infinity, maxHeight: .infinity) } .onAppear { Task { await viewModel.start() } } } }

実行

iPhone(実機)にて Run します。

iPhone Simulatorではカメラが利用できないので実機にて実行してください。

iPhone 実機に声をかけたり音声を鳴らしてみてください。音声が出力され、上側に Local のカメラ映像、下側に Remote の受信映像が描画されれば成功です。

次のステップ

今回は SFU サーバーを介して自分が Publish したメディアを Subscribe するというシンプルなものでした。

次は、他のクライアントと映像・音声・データをやりとりしてみましょう。他のクライアントと疎通できるサンプルアプリケーションを用意しています。

サンプルコード

また、開発の前に開発ドキュメントもご一読ください。

iOS SDKの開発ドキュメント