GO + Reactでライブストリーミングサービスのサンプルを作った

WebRTCの勉強で作った画面共有のライブストリーミングサービスを紹介します。

先に完成したリポジトリを貼っておきます ↓

https://github.com/yama-is-bocchi/simple-webrtc-server

コーディングエージェントを使わずに自力で作ってみたのですがデバッグが思った以上に面倒だったので、自分のはまったポイントなどを紹介します。

私はDevContainer信者なので何か開発を始める場合は必ず開発コンテナーを作るのですが、WebRTCではいつも使ってる設定では動きませんでした。

というのもいつもはnetwork_mode: bridgeで開発してるのですが、このネットワーク設定ではSTUNサーバーから受け取った情報をそのまま使うとcandidateの交換が失敗してしまいます。

一番手っ取り早い解決策はコンテナーのnetwork_modeをホストにすることです。

これでSTUNサーバーから取得できる情報とコンテナーのネットワーク情報が一致するのでcandidate交換が成功します。

2. 使用するポートを開放しホストマシンのプライベートアドレスを指定

Section titled “2. 使用するポートを開放しホストマシンのプライベートアドレスを指定”

network_modeをブリッジのまま通信したい場合は使用するポートを開放し、ピアコネクションのポート範囲を指定します。

さらに、アドレスの上書きルールにホストマシンのプライベートIPアドレスを追加する必要があります。

この解決策はソースコードがコンテナーの都合に左右されてしまうので私は避けました。

P2P接続はすぐに実装できました。

シグナリングサーバーさえあればすぐに作成できます。

SFUの実装はかなり厄介でした。

まずは構成を紹介します。

こちらのシステムアーキテクチャのように1対多でのリアルタイム通信を実現しようとしました。

これを実現するには、ブラウザtoGOサーバーでWebRTCのピアコネクションを確立する必要があります。

そして配信者のRTPパケットを取り出して視聴者とのコネクションのトラックに追加すれば完了です。

ここまで聞くとめっちゃ簡単そうなんですが、そんな単純な問題ではありませんでした。

SFU実装ポイント1. ローカルトラックには複数の種類がある

Section titled “SFU実装ポイント1. ローカルトラックには複数の種類がある”

今回は画面共有をリアルタイム配信しようとしたので、videoトラックとaudioトラックの2種類があります。

なので配信者のRTPパケットを取り出す場合は以下のように、ローカルトラックのポインター保持する必要があります。

client.peerConnection.OnTrack(func(
remoteTrack *webrtc.TrackRemote,
receiver *webrtc.RTPReceiver,
) {
localTrack, err := webrtc.NewTrackLocalStaticRTP(
remoteTrack.Codec().RTPCodecCapability,
remoteTrack.ID(),
remoteTrack.StreamID(),
)
if err != nil {
log.Printf("failed to create local track: %v", err)
return
}
client.localTracks[remoteTrack.Kind()] = localTrack
//(以下略)
}
)

SFU実装ポイント2. キーフレームの受信

Section titled “SFU実装ポイント2. キーフレームの受信”

RTPパケットを追加しただけだと、視聴者ブラウザでは差分フレームしか受け取れないので映像や音声をデコードすることができません。

なので視聴者ブラウザとGOサーバーでWebRTCのピアコネクションが確立したら、配信者にPLIを送信しキーフレームを送信させる必要があります。

func (orchestrator *sfuOrchestrator) dispatchKeyframes() error {
orchestrator.mutex.Lock()
defer orchestrator.mutex.Unlock()
for _, receiver := range orchestrator.publisher.peerConnection.GetReceivers() {
if receiver.Track() == nil {
continue
}
_ = orchestrator.publisher.peerConnection.WriteRTCP([]rtcp.Packet{
&rtcp.PictureLossIndication{
MediaSSRC: uint32(receiver.Track().SSRC()),
},
})
}
return nil
}

上記のようなディスパッチ処理を以下のコードのように実行する必要があります。

if role == Viewer {
connection.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
if state == webrtc.ICEConnectionStateConnected {
dispatchKeyframesProcess()
}
})
}

これで画面共有の映像、音声がデコードできます!

これは左上のタブを3ブラウザにリアルタイム配信してます。

シグナリングやデコードが失敗したとき、的確なエラーメッセージが出ることがまず無かったです。

chrome://webrtc-internals ここから対象の通信のログを見たり、pion-webrtcの開発ログを表示して頑張って特定するしかないです。

したがって、SFUに関してはWebRTCの知識がないと自力で実装するのは難しいと感じました。

今回の学習を通してリアルタイム通信の知識が深まったので、今後WebRTCを応用したアプリを作ってみようと思います。