🚀 クイックスタート

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

  1. Room 名を指定して入室する
  2. 自分のマイク音声とカメラ映像を送信する
  3. 相手のマイク音声とカメラ映像を受信する

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

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

開発環境

  • Xcode 26.6
  • iOS 26.5

アプリケーション 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 (プレビュー)と受信した Remote の View を描画します。

SwiftUI では UIKit ベースの VideoViewUIViewRepresentable でラップして使用します。

LocalVideoView.swift を新規作成します。LocalVideoStream を受け取り、updateUIView 内でアタッチすることで自身の映像を描画します。

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

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

import SkyWayRoom import SwiftUI 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) } }

ViewModel の作成

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

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

@MainActor を指定、 ObservableObjectRoomDelegate に準拠します。

また、後ほど必要になるプロパティを宣言します。

import Foundation import SkyWayRoom @MainActor final class RoomViewModel: ObservableObject, RoomDelegate { @Published var localVideoStream: LocalVideoStream? @Published var remoteVideoStream: RemoteVideoStream? private var localAudioStream: LocalAudioStream? private var room: Room? private var localRoomMember: LocalRoomMember?

SkyWay のセットアップ

RoomViewModel にて、 start(roomName:) という関数を定義し、アプリケーション ID とシークレットキーを宣言します。

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

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

func start(roomName: String) async { let appId = "アプリケーションIDを入力してください" let secretKey = "シークレットキーを入力してください" // SkyWayのセットアップ let contextOptions: ContextOptions = .init() contextOptions.logLevel = .trace try? await Context.setupForDev(withAppId: appId, secretKey: secretKey, options: contextOptions)

Room の作成

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

Room.findOrCreate(with:) で Room を作成し、プロパティに保持します。

// Roomの作成 let roomInit: Room.InitOptions = .init() roomInit.name = roomName guard let room = try? await Room.findOrCreate(with: roomInit) else { print("[Tutorial] Creating room failed.") return } self.room = room

Room への参加

Room.join(with:) で Room に参加し、 Member を作成します。

作成した Member はプロパティに保持します。

// Roomへの参加 let memberInit: Room.MemberInitOptions = .init() memberInit.name = "member_\(UUID().uuidString)" guard let member = try? await room.join(with: memberInit) else { print("[Tutorial] Join failed.") return } localRoomMember = member

Publish の準備

カメラ映像とマイク音声を取得し、 Room に Publish する準備を行います。

カメラ映像ソースの Stream の作成と UI への表示

自身のカメラ映像を取得し UI へ反映します。

CameraVideoSource.supportedCameras() で SkyWay がサポートしているカメラの一覧を取得できます。 今回は前面カメラデバイスを取得します。

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

CameraVideoSource.shared().createStream()LocalVideoStream を作成します。

localVideoStream プロパティを更新することで、ContentViewLocalVideoView で描画されます。

// カメラからの映像取得とUIへの表示 // カメラリソースの取得 if let camera = CameraVideoSource.supportedCameras().first(where: { $0.position == .front }) { // カメラ映像のキャプチャを開始します try? await CameraVideoSource.shared().startCapturing(with: camera, options: nil) } else { print("[Tutorial] Supported camera is not found.") } // 描画やPublishが可能なStreamを作成します // @Publishedプロパティを更新し、ContentViewのLocalVideoViewで描画します localVideoStream = CameraVideoSource.shared().createStream()

マイク音声の Stream の作成

自身のマイク音声を取得します。

MicrophoneAudioSource を作成し、createStream()LocalAudioStream を作成します。

// マイクからの音声取得 // Publishが可能なStreamを作成します localAudioStream = MicrophoneAudioSource().createStream()

Stream の Publish

Room に入室している他の Member へ Stream を Publish します。

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

Publish することで、 Room に Publication が作成されます。

他の Member は対象の Publication を Subscribe することで、 Stream を受信することが可能になります。

// StreamのPublish _ = try? await member.publish(localVideoStream!, options: nil) _ = try? await member.publish(localAudioStream!, options: nil)

Publication の Subscribe

Publication を Subscribe し Stream を受信します。

まず、 RoomDelegateroom(_:didPublishStreamOf:) を実装することで、 Room 内で新たに Publish された Publication を Subscribe することが可能です。

次に、入室時に存在する Publication を Subscribe します。

LocalRoomMember.subscribe(publicationId:options:) で Publication を Subscribe します。

受信した映像を表示するため、 Subscribe 後に Stream を取得し、RemoteVideoStream へのキャストを試みます。

remoteVideoStream プロパティを更新することで、ContentViewRemoteVideoView で描画されます。

なお、 AudioStream の場合は Subscribe が完了したタイミングでスピーカーから音声が流れます。

自身で Publish した Publication は Subscribe できないことに注意してください。

// Room内でStreamがPublishされるとroom(_:didPublishStreamOf:)が呼ばれるようにdelegateを登録します room.delegate = self // PublicationのSubscribe // 入室時に他のMemberのPublicationをSubscribeします for publication in room.publications { await subscribe(publication) } } // Room内のMemberがPublishしているPublicationをSubscribeします private func subscribe(_ publication: RoomPublication) async { // 自身のPublicationは除く if publication.publisher == localRoomMember { return } // PublicationをSubscribeします guard let subscription = try? await localRoomMember?.subscribe(publicationId: publication.id, options: nil) else { return } // Videoの場合はremoteVideoStreamを更新して描画します if let videoStream = subscription.stream as? RemoteVideoStream { self.remoteVideoStream = videoStream } } // MARK: - RoomDelegate nonisolated func room(_ room: Room, didPublishStreamOf publication: RoomPublication) { Task { await subscribe(publication) } }

View と ViewModel を接続

ContentView.swiftRoomViewModel@StateObject として保持します。

Room 名を入力して Join ボタンを押すと、 RoomViewModel.start(roomName:) が呼ばれ、 Room への入室と Publish が行われます。

import SwiftUI struct ContentView: View { @StateObject private var viewModel = RoomViewModel() @State private var roomName: String = "" var body: some View { VStack { HStack { TextField("Room name", text: $roomName) .textFieldStyle(.roundedBorder) Button("Join") { Task { await viewModel.start(roomName: roomName) } } .buttonStyle(.borderedProminent) .disabled(roomName.isEmpty) } LocalVideoView(stream: viewModel.localVideoStream) .frame(maxWidth: .infinity, maxHeight: .infinity) RemoteVideoView(stream: viewModel.remoteVideoStream) .frame(maxWidth: .infinity, maxHeight: .infinity) } .padding() } }

Tutorial完成コード

LocalVideoView.swift

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

RemoteVideoView.swift

import SkyWayRoom import SwiftUI 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) } }

RoomViewModel.swift

import Foundation import SkyWayRoom @MainActor final class RoomViewModel: ObservableObject, RoomDelegate { @Published var localVideoStream: LocalVideoStream? @Published var remoteVideoStream: RemoteVideoStream? private var localAudioStream: LocalAudioStream? private var room: Room? private var localRoomMember: LocalRoomMember? func start(roomName: String) async { let appId = "アプリケーションIDを入力してください" let secretKey = "シークレットキーを入力してください" // SkyWayのセットアップ let contextOptions: ContextOptions = .init() contextOptions.logLevel = .trace try? await Context.setupForDev(withAppId: appId, secretKey: secretKey, options: contextOptions) // Roomの作成 let roomInit: Room.InitOptions = .init() roomInit.name = roomName guard let room = try? await Room.findOrCreate(with: roomInit) else { print("[Tutorial] Creating room failed.") return } self.room = room // Roomへの参加 let memberInit: Room.MemberInitOptions = .init() memberInit.name = "member_\(UUID().uuidString)" guard let member = try? await room.join(with: memberInit) else { print("[Tutorial] Join failed.") return } localRoomMember = member // カメラからの映像取得とUIへの表示 // カメラリソースの取得 if let camera = CameraVideoSource.supportedCameras().first(where: { $0.position == .front }) { // カメラ映像のキャプチャを開始します try? await CameraVideoSource.shared().startCapturing(with: camera, options: nil) } else { print("[Tutorial] Supported camera is not found.") } // 描画やPublishが可能なStreamを作成します // @Publishedプロパティを更新し、ContentViewのLocalVideoViewで描画します localVideoStream = CameraVideoSource.shared().createStream() // マイクからの音声取得 // Publishが可能なStreamを作成します localAudioStream = MicrophoneAudioSource().createStream() // StreamのPublish _ = try? await member.publish(localVideoStream!, options: nil) _ = try? await member.publish(localAudioStream!, options: nil) // Room内でStreamがPublishされるとroom(_:didPublishStreamOf:)が呼ばれるようにdelegateを登録します room.delegate = self // PublicationのSubscribe // 入室時に他のMemberのPublicationをSubscribeします for publication in room.publications { await subscribe(publication) } } // Room内のMemberがPublishしているPublicationをSubscribeします private func subscribe(_ publication: RoomPublication) async { // 自身のPublicationは除く if publication.publisher == localRoomMember { return } // PublicationをSubscribeします guard let subscription = try? await localRoomMember?.subscribe(publicationId: publication.id, options: nil) else { return } // Videoの場合はremoteVideoStreamを更新して描画します if let videoStream = subscription.stream as? RemoteVideoStream { self.remoteVideoStream = videoStream } } // MARK: - RoomDelegate nonisolated func room(_ room: Room, didPublishStreamOf publication: RoomPublication) { Task { await subscribe(publication) } } }

ContentView.swift

import SwiftUI struct ContentView: View { @StateObject private var viewModel = RoomViewModel() @State private var roomName: String = "" var body: some View { VStack { HStack { TextField("Room name", text: $roomName) .textFieldStyle(.roundedBorder) Button("Join") { Task { await viewModel.start(roomName: roomName) } } .buttonStyle(.borderedProminent) .disabled(roomName.isEmpty) } LocalVideoView(stream: viewModel.localVideoStream) .frame(maxWidth: .infinity, maxHeight: .infinity) RemoteVideoView(stream: viewModel.remoteVideoStream) .frame(maxWidth: .infinity, maxHeight: .infinity) } .padding() } }

動作確認

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

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

次のステップ

今回はメディアを Publish, Subscribe するシンプルな例でした。

その他のサンプルアプリケーションはこちらをご参照ください。

サンプルコード

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

iOS SDKの開発ドキュメント