Skip to content

Instantly share code, notes, and snippets.

@szktty
Last active June 11, 2023 14:46
Show Gist options
  • Save szktty/999a34c64cc4ea60de43c4c1adc93203 to your computer and use it in GitHub Desktop.
Save szktty/999a34c64cc4ea60de43c4c1adc93203 to your computer and use it in GitHub Desktop.
WebRTC ネイティブ iOS ライブラリ API ガイド

WebRTC ネイティブ iOS ライブラリ API ガイド (M62)

Safari が WebRTC に対応したり、 React-Native があったりと、 WebRTC を使う iOS アプリの開発ではネイティブライブラリ (WebRTC.framework) + Swift を使わない選択肢も十分あります。でも最終的に手の込んだことをやろうとすると、もしくは最新のリリースブランチを使うとなると、ネイティブライブラリに頼らざるを得ません。いかんせんネイティブライブラリの情報が少なくて辛いですね。参考になれば辛い、いえ幸いです。

ここで扱うのはメディアチャネルのみです。データチャネルについては扱いません。知りませんので。ビルドについても扱いません。なんとかしてください。

私は主に商用 WebRTC SFU SoraiOS SDK を開発しており、そのため以下の内容が環境に依存してしまっている可能性があります。その場合はご指摘頂けると助かります。

Swift での利用

ネイティブライブラリは ObjC で実装されています。幸い低レベルなデータ型を扱う API はないので、 Swift でも問題なく使えます。

Bitcode について

ネイティブライブラリは Bitcode に対応していますが、 CocoaPods の公式ビルド も含めた有志の方々が公開されているバイナリは Bitcode に対応していません。 Bitcode に対応するとサイズが 700MB を超えるからのようです。圧縮後でもサイズが 200MB を超えます。アプリケーションも優に 300MB を超えるので、テストするにも時間と容量を食います。

というわけで、 Bitcode に対応するには残念ながら自前でビルドする必要があります。がんばってね!( Sora では Bitcode 対応のバイナリを用意しています。 あくまで Sora 専用のビルドですので他の環境での動作は保証できませんが、試してみたければどうぞお使いください)

主な API

だいたいこの辺のクラスを押さえておけば大丈夫だと思います。

  • 接続
    • RTCPeerConnection
    • RTCPeerConnectionFactory
  • 設定
    • RTCConfiguration
    • RTCMediaConstraints
  • ストリーム
    • RTCMediaStream
    • RTCMediaStreamTrack
  • 映像の描画
    • RTCEAGLVideoView
    • RTCVideoCapturer
    • RTCCameraVideoCapturer
    • RTCVideoFrame
  • プロトコル関連
    • RTCSessionDescription
    • RTCIceCandidate
    • RTCIceServer

Info.plist

WebRTC.framework はカメラとマイクを使うので、 Info.plist ファイルにカメラとマイクの用途を記述する必要があります。この記述を追加しないとアプリケーションが強制終了するので、プロジェクトを作ったら最初に次の項目を編集しておきましょう。

  • "Privacy - Camera Usage Description"
  • "Privacy - Microphone Usage Description"

接続の流れ

基本的に、WebRTCの簡易シグナリング で解説されている流れで接続できます。ただし、 WebRTC ではシグナリングの実現手段は未定義であり実装依存です。環境によって事情が異なるので注意してください。 Sora では WebSocket + JSON でシグナリングを行います。

  • RTCPeerConnectionFactory を生成する (init())
  • RTCPeerConnectionFactory を使って RTCPeerChannel を生成する (peerConnection(configuration:constraints:delegate:))
  • 発信側
    • RTCPeerChannel を使って offer の SDP をシグナリングサーバーに生成する (offer(for:completionHandler:))
  • 受信側
    • 受信した offer SDP を RTCPeerChannel の remote description にセットする (setRemoteDescription:completionHandler:))
    • RTCPeerChannel を使って answer の SDP を生成する (answer(for:completionHandler:))
    • 生成した SDP を local description にセットする (setLocalDescription(_:completionHandler:))
    • answer SDP をシグナリングサーバーに送信する
  • 発信側
    • 受信した answer SDP を remote description にセットする (setRemoteDescription(_:completionHandler:))

RTCPeerConnection と RTCPeerConnectionFactory

接続を始めるにあたってまず用意するべきは RTCPeerConnection です。このオブジェクトは RTCPeerConnectionFactory で生成できます。意外と重要なのがこの RTCPeerConnectionFactory で、配信用のストリームやトラックは RTCPeerConnectionFactory が生成します。 RTCPeerConnection の生成後も RTCPeerConnectionFactory を使います。

RTCPeerConnectionFactory は 1 つあれば十分です (複数生成するとバグの原因になる可能性が拭い切れません) 。コンストラクタはシンプルに init で十分なので、生成した RTCPeerConnectionFactory を静的変数やらプロパティやらで保持しておくのがおすすめです。

RTCPeerConnection とストリーム

RTCPeerConnection は複数のストリーム (RTCMediaStream) を localStreams プロパティで持ちます。ストリームは送受信するメディアデータ (映像や音声) を扱います。 localStreams は名前の通りローカルなストリーム、つまりクライアントアプリで使うストリームです。ストリームの方向が送信であれ受信であれ、どちらも localStreams として扱われます。

ストリームは RTCPeerConnection と独立しており、 add(stream:)remove(stream:) を使って任意のタイミングでストリームを取捨選択できます。映像を切り替えたければ配信中のストリームを除き、他のストリームを追加することで映像を切り替えられます。

マルチストリームと RTCPeerConnection

マルチストリームの接続では RTCPeerConnection に複数のストリームが追加されます。接続中のチャネル ID に接続があればストリームが追加され、接続が解除されればストリームが除去されます。

マルチストリームについてはブラウザや SFU/MCU サーバーによって対応に差があるので、まずは使用環境のマルチストリーム事情を押さえておくのがいいと思います。

ストリーム、トラック、ソース

ストリームは複数のトラック (RTCMediaStreamTrack) で構成されます。映像 (RTCVideoTrack) も音声 (RTCAudioTrack) もトラックです。実際には 1 つのストリームが 3 つ以上のトラックを持つことはそうないと思われます。トラックについては 1 つの点を除いて気にしなくてもいいでしょう。

注意すべき 1 つの点は、配信時のストリーム ID とトラック ID 、それぞれの重複です。 1 つの RTCPeerConnection が持つ複数のローカルストリームでストリーム ID は重複してはならず、 1 つのストリーム内でトラック ID は重複してはいけません。必ず映像と音声で別々の ID にする必要があります。どちらもトラックの一種 (RTCMediaStreamTrack のサブクラス) ですので当然かもしれません。

トラックに流れるデータは、ソース (RTCMediaSource) から取得されます。映像トラック (RTCVideoTrack) のソースは RTCVideoSource で、音声トラック (RTCAudioTrack) のソースは RTCAudioSource です。音声ソースについては少し前まで RTCAVFoundationVideoSource が使われてたんですが、 M61 で deprecated になりました。ソースの詳細も特に詳しくなる必要はありません。

それから、独自のストリームとトラックは実装できません。任意の映像を送信したい場合は他の方法を使います。

ストリームの増減

RTCPeerConnection が管理するストリームの増減はデリゲート (RTCPeerConnectionDelegate) で捕捉可能です。

Swift だと、ストリームの追加に peerConnection(_:didAdd:) が、除去時に peerConnection(_:didRemove:) が呼ばれます。これはシングルストリームでもマルチストリームでも同様です。

注意点として、これらのメソッドは明示的にストリームを追加・除去した場合は呼ばれません。配信用のストリーム(後述)はこちらで生成して追加するので、デリゲートメソッドは呼ばれません。ただし面倒なことにマルチストリームだと配信ストリームも peerConnection(_:didAdd:) が呼ばれる可能性があります。これはバグではないので、マルチストリーム時はストリーム ID などで配信ストリームと受信ストリームを区別する必要があります。

接続成立の判断

RTCPeerConnection の接続状態は 3 種類あります。

  • RTCIceSignalingState
  • RTCIceConnectionState
  • RTCIceGatheringState

接続を開始するとこれらの状態が変化します。それぞれの状態が次の case になれば接続成立とみなせます。いつまでも以下の状態にならなければタイムアウトとして扱っていいでしょう。

  • RTCIceSignalingState -> .stable
  • RTCIceConnectionState -> .connected
  • RTCIceGatheringState -> .complete

これらの状態はデリゲートで捕捉可能です。 Swift だといずれも同名になってしまうので、型も含めて表記します。

  • func peerConnection(_ peerConnection: RTCPeerConnection, didChange peerConnection: RTCSignalingState)
  • func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState)
  • func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState)

配信ストリームの生成

配信を行う場合はストリームを手動で生成する必要があります。次の手順で生成します。いずれも RTCPeerConnectionFactory で生成します。

  1. カメラから映像を取得する映像ソースを用意する
  2. 1 の映像ソースを使う映像トラックを用意する
  3. マイクから音声を取得する音声ソースを用意する
  4. 2 の音声ソースを使う音声トラックを用意する
  5. 2, 4 のトラックを持つストリームを生成する

例:

let factory = RTCPeerConnectionFactory()
let peerConn = factory.peerConnection(with: RTCConfiguration(),
                                      constraints: nil,
                                      delegate: nil)

// 映像トラック
let videoSource = factory.videoSource()
let videoTrack = factory.videoTrack(with: videoSource,
                                    trackId: videoTrackId)

// 音声トラック
let audioSource = factory.audioSource(with: nil)
let audioTrack = factory.audioTrack(with: audioSource,
                                    trackId: audioTrackId)

// ストリーム
let stream = factory.mediaStream(withStreamId: streamId)
stream.addVideoTrack(videoTrack)
stream.addAudioTrack(audioTrack)

// ストリームの追加
peerConn.add(stream)

汎用的に見えるメソッドが並びますが、これでカメラとマイクと接続できます。

受信ストリームの取得

受信ストリームはチャネル ID に接続があると自動的に追加されます。前述の peerConnection(_:didAdd:) デリゲートメソッドが呼ばれます。

受信ストリームにおけるマイクの扱いについて

受信ストリームの使用時、配信ストリームを用意していないのにも関わらず、ユーザーにマイクへのアクセス許可を要求する場合があります。この挙動は仕様です。 iOS の仕様上の理由で、 WebRTC ライブラリレベルでアクセス許可の遅延ができないようです。まあ一度許可すれば以降は要求されませんから、あらかじめユーザーに説明しておくとか、アプリケーションの起動時にアクセス許可を要求するなどしてごまかすのがいいかもしれません。

映像の描画

映像を描画するには、 RTCVideoRenderer プロトコルに準拠したオブジェクトを 映像トラック に追加します(ストリームではありません)。デフォルトでは RTCEAGLVideoView が用意されているので、描画するだけならこれで問題ないです。あくまでも描画するだけなら、です。実際の開発では継承なりラップするなりして何かしらのサポートを追加する必要があるかもしれません。

RTCEAGLVideoView は Interface Builder でも配置できます。 UIView を配置して、モジュール名に "WebRTC" 、クラス名に "RTCEAGLVideoView" を指定します。

映像フレームの流れと加工

映像フレームは RTCVideoFrame として表されます。映像フレームの生成から描画までの流れを次に示します。

RTCVideoCapturer -> RTCVideoSource (RTCVideoCapturerDelegate) -> RTCVideoTrack -> RTCVideoRenderer (プロトコル)

最初に映像フレームを生成するのは RTCVideoCapturer です。 RTCVideoCapturer はデリゲート RTCVideoCapturerDelegate を保持します。カメラの映像を取得して映像フレームを生成するサブクラス RTCCameraVideoCapturer が用意されています。

生成された映像フレームはデリゲートメソッド capturer(_:didCapture:) に渡されます。 RTCVideoSource がデリゲートプロトコルを実装しているので、 RTCVideoSource をRTCVideoCapturer のデリゲートとしてセットすれば、あとは映像フレームが描画オブジェクトまで流れます。

つまり、任意の映像フレームを描画したい場合は、映像フレームを RTCVideoSource の capturer(_:didCapture:) に渡して呼びます。映像フレームを加工したい場合は、 RTCVideoCapturerDelegate を実装したクラスを用意して RTCVideoCapturer にセットし、 capturer(_:didCapture:) で受け取った映像フレームを加工してから RTCVideoSource の capturer(_:didCapture:) に加工後の映像フレームを渡します。 RTCVideoCapture と RTCVideoSource の間にフィルターを挟むということです。残念ながら手軽に使える 映像フィルターは用意されていません。

任意の映像フレームを生成する

M62 より、 RTCVideoFrame は CVPixelBufferRef から生成可能になりました。 CVPixelBufferRef を用意できれば任意の映像フレームをトラックに流すことが可能です。

音声のカスタマイズについて

音声には映像のようにカスタマイズする仕組みが用意されていません。 WebRTC ライブラリでは音声の取得に AVAudioSession が使われているので、 AVAudioSession を直接操作します。

M61 より RTCAudioSession という音声を扱うクラスが追加されました(プライベートな実装でしたがフレームワークで利用できるようになりました)。ほぼ AVAudioSession のプロパティの単純なラッパーです。このオブジェクトのプロパティを操作すると、 WebRTC で使われるスレッドをロックしてから AVAudioSession のプロパティにアクセスしてくれます。プロパティやセッター以外は AVAudioSession を直接使っても問題ありません。

音声に関しては iOS やハードウェアの制約があり、 WebRTC ライブラリの音声周りの実装もいろいろと制限されます。 API の中でも特に情報がない分野なので、がんばって自力でどうにかしましょう。

音量を変更する

音量の変更は RTCAudioSource の volume プロパティで可能です。 RTCAudioSource は RTCMediaStream.audioTracks -> RTCAudioTrack.source で取得できます。ただし、音量の変更は受信ストリームでのみ可能で、配信ストリームではマイクの音量を変更できません。

現在の実装だと音声は AVAudioSession で管理されていますが、 setInputGain(_:) プロパティは音量に影響しないようです。

ミュートする

現在の実装だと、音声のミュートは配信と受信で異なります。

  • 配信: RTCMediaStream から音声トラックを外します。ただし、稼働中のマイクは停止されません。マイクを停止したければ別途処理を行う必要があります。

  • 受信: 音量を 0 にします。 AVAudioSession が関係しているのか、音声トラックを外しても引き続き音声が流れます。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment