こんにちは。
今回は、WebRTCの技術を利用して、異なるネットワーク間での通信を検証した。
また、RaspberryPiを用いての通信も検証する。
参考になれば、幸いです。
目次
- WebRTCとは?
- 今回のやること
- システム構成図
- コード
- 結果
- 感想
- 今後目指していきたいこと
WebRTCとは?
WebRTCとは、Web Real Time Communicationの略。
WebRTCを一言で言うと、"データ"をネットワークを通じて、端末間で送受信するためのプロトコルの集まりのことである。
2011年にGoogleで提唱され、APIレベルでの標準化が進んでいる
メディアチャネル:DTLS-SRTP
データチャネル:DTLS
で暗号化されている。
また、WebRTCはUDPプロトコルでの通信(TURNなど一部TCPでの通信のケースもあり)を行っており、データを相手に送り付ける、及び独自の再送や輻輳制御を行っているため、遅延を抑える仕組みがある。
今回のやること
・リモート(例:自宅)に配置したRaspberryPiのカメラの映像を社内の環境(ローカル)から、WebRTCを利用してリアルタイムに映像を確認すること。
・NAT配下からの通信が成功した際に使用したシグナリングのプロトコルの調査
システム構成図
環境
-
ローカルPC
- Windows10
- Microsoft edge(ブラウザ)
-
RaspberryPi 4B
- bullsea(OS)
- Chronium(ブラウザ)
-
Webアプリ
- Vue3(Webフレームワーク)
- SkyWay SDK
- Bulma (CSSフレームワーク)
-
Skyway認証用API
- Node.js(express)
- dotenv
-
デプロイ環境
- GAE(Google App Engine)
手順/コード
認証APIの作成
SkyWayの開発者ドキュメントでも記載があるが、SDKを使用するための認証APIはバックエンドで行うことが望ましい。
SkyWay Auth Tokenを作成するためのアプリケーションIDやシークレットキーが必要となる。
これらの取得については、公式ガイドを参照。
従って、認証のAPIを
-
SkyWayライブラリ(token)のインストール
$ npm @skyway-sdk/token
-
認証API作成
今回、SkyWay Auth Token作成に必要なアプリケーションIDやシークレットキーは.envに記述する。APP_ID="アプリケーションID" SECRET_KEY="シークレットキー"
下記の内容をindex.jsを用意して、記述する
index.jsconst express = require('express'); const app = express(); const sdkToken = require('@skyway-sdk/token') const fs = require('fs'); require('dotenv').config(); const { APP_ID } = process.env const { SECRET_KEY } = process.env const iat = Math.floor(Date.now() / 1000); const exp = Math.floor(Date.now() / 1000) + 36000; app.set('trust proxy', true); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use((_, res, next) => { // 異なるオリジン間でデータをやり取りする res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); res.header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTION"); next(); }); // Test用 GETメソッド app.get('/', (req, res) => { res.send('Hello World!'); }); // /authenticateにGETリクエストすると、SkyWay Auth Tokenを返す app.get('/authenticate', (req, res) => { res.send(makeToken); }); const listener = app.listen(process.env.PORT || 8080, () => { console.log(`Server listening on port ${listener.address().port}`) }); const makeToken = new sdkToken.SkyWayAuthToken({ jti: sdkToken.uuidV4(), iat: iat, exp: exp, scope: { app: { id: APP_ID, turn: true, actions: ['read'], channels: [ { id: '*', name: '*', actions: ['write'], members: [ { id: '*', name: '*', actions: ['write'], publication: { actions: ['write'], }, subscription: { actions: ['write'], }, }, ], sfuBots: [ { actions: ['write'], forwardings: [ { actions: ['write'], }, ], }, ], }, ], }, }, }).encode(SECRET_KEY);
-
デプロイ
GCPのアカウント作成やプロジェクト作成、アプリ作成等はここでは記述しない。
そちらについては、公式ドキュメントや他者Qiitaの記事等を参照のこと。app.yamlをプロジェクトのルートディレクトリに作成して、下記内容を記述する。
app.yamlruntime: nodejs18 build_env_variables: GOOGLE_NODE_RUN_SCRIPTS: ''
GAEに、gcloudコマンドを利用して、デプロイする。
$ gcloud app deploy
Webアプリ作成
-
SkyWayライブラリ(room)のインストール
$ npm @skyway-sdk/room
-
Webアプリ作成
app.vueを用意して、下記を記述する。app.vue<script setup lang="ts"> import {computed, ref, onMounted} from 'vue'; import axios from 'axios'; const authToken = async() => { return axios.get('ここにSkyWay Auth Tokenを取得するAPIのパスを入れてください',{ }).then((Response) => Response.data) } // 通信処理 import { nowInSec, SkyWayAuthToken, SkyWayContext, SkyWayRoom, SkyWayStreamFactory, uuidV4 } from '@skyway-sdk/room'; const myVideo = ref(null) const myAudio = ref(null) const localVideo = ref(null) const remoteVideoArea = ref("") // const remoteAudioArea = ref(null) const myId = ref('') const roomName = ref('') // 即時実行のasyncで関数全体を囲むと、非同期処理awaitが可能 const join = async () => { if (roomName.value == '') return // promise型で返ってくるため、非同期処理awaitで行う const token = await authToken() // Roomを作成 const context = await SkyWayContext.Create(token) const room = await SkyWayRoom.FindOrCreate(context, { type: 'p2p', name: roomName.value, }) // Roomに入る const me = await room.join() myId.value = me.id room.onMemberJoined.add((e) => { console.log(e) }) // Cameraのストリームを作成 const videoStream = await SkyWayStreamFactory.createCameraVideoStream(); // 自分の映像と音声を公開する await me.publish(videoStream) // 他のユーザーがいた・入室してきた時の処理 const subscribeAndAttach = async (publication: any) => { if (publication.publisher.id === me.id) return; const { stream } = await me.subscribe(publication.id); // DOMに直接videoとaudioのelementを追加 let newMedia; switch (stream.track.kind) { case 'video': newMedia = document.createElement('video') newMedia.width = 300 newMedia.playsInline = true newMedia.autoplay = true stream.attach(newMedia) remoteVideoArea.value.appendChild(newMedia) break /* case 'audio': newMedia = document.createElement('audio') newMedia.controls = true newMedia.autoplay = true stream.attach(newMedia) remoteAudioArea.value.appendChild(newMedia) break */ default: return } } room.publications.forEach(subscribeAndAttach) room.onStreamPublished.add((e: any) => subscribeAndAttach(e.publication)) } onMounted(async () => { // 自分の音声と映像を取得 const { audio, video } = await SkyWayStreamFactory.createMicrophoneAudioAndCameraStream() myVideo.value = video // myAudio.value = audio // 自分の映像を表示 myVideo.value.attach(localVideo.value) await localVideo.value.play() }) </script> <template> <section class="hero is-primary"> <div class="hero-body"> <p class="title"> Video2 </p> <p class="subtitle"> 接続しているカメラデバイスから映像を取得できます </p> </div> </section> <div class="has-text-centered"> <div>ID: <span>{{ myId }}</span></div> <video ref="localVideo" muted playsinline class="local-video" /> <div class="room-name"> Room Name: <div v-if="!myId"> <input v-model="roomName" type="text" /> <button @click="join">Join</button> </div> <div v-else>{{roomName}}</div> </div> <div ref="remoteVideoArea" class="remote-videos" /> <div ref="remoteAudioArea" class="remote-audios" /> </div> </template> <style scoped> .hero{ margin-top: 10px; } .has-text-centered{ margin: 10px; } </style>
-
ビルド
下記コマンドを実行して、distフォルダにビルドしたファイルが含まれていることを確認。$ npm run build
-
デプロイ
こちらも認証API同様で、app.yamlをプロジェクトのルートディレクトリに作成して、下記内容を記述する。app.yamlruntime: php55 handlers: - url: / static_files: dist/index.html upload: dist/index.html - url: /(.*) static_files: dist/\1 upload: dist/(.*)
GAEに、gcloudコマンドを利用して、デプロイする。
$ gcloud app deploy
※app.yaml内で、serviceを指定すると同プロジェクト内の別サービスとしてデプロイすることが可能。
結果
・リモート(例:自宅)に配置したRaspberryPiのカメラの映像を社内の環境(ローカル)から、WebRTCを利用してリアルタイムに映像を確認すること。
無事、自宅にあるRaspberryPiとNAT配下の社内ネットワークに接続しているPC間で通信が成功した。
今回のように、社内のようなNAT配下やファイアウォールの環境下でもTURNサーバを利用することで通信が可能。
・NAT配下からの通信が成功した際に使用したシグナリングのプロトコルの調査
Chromeを利用して、端末間の通信がどのようなプロトコルで行われているかを確認することができる。
TURNサーバを利用しているかを確認する方法
携帯(キャリア)と社内ネットワーク内のPC間の接続を確認した。
〇ローカル(社内)
NAT配下のネットワークに接続しているPCのシグナリングに用いられたプロトコルの結果である。
TURNサーバで、tcpプロトコルを用いていることが分かる。
感想
SkyWayのSDKを使用することで自前で実装するよりも簡単にオンラインコミュニケーションアプリを動作させることができた。
SDKを利用することで、TURNサーバなどを用意/実装することなく通信することが可能である。
全体の通信の約10~20%がTURNサーバを介した通信であること、また、WebRTCのアップデートの対応の観点からも、WebRTCプラットフォームを提供しているサービスを利用することが望ましいと感じた。
携帯のキャリアのネットワークとも接続ができるので、ネットワークのある環境ならどこでも通信することが可能。
今後目指していきたいこと
- Zoomのような会議アプリ
今回は1対1での通信であったが、SFUサーバを利用した多対多の通信も行いたい。 - ARを利用したリアルタイムコミュニケーション
今回は、RaspberryPiであったが、カメラ付きのAR機器(ゴーグル)と組み合わせることで、リアルタイムに作業指示を始めとする情報をARで表示することが可能になる。
ユースケースとしては、工事現場を始めとする作業現場や歩行者の立体的な道案内などである。 - ドローンや産業機器などの遠隔操作
ドローンや産業機器から送られてくる映像を遠隔でリアルタイムに確認しながら、操作を行う