【7-2】RaspberryPiから低遅延で映像を飛ばす
WebRTCサーバーmomoの環境準備だけで、長くなってしまいましたので
ブラウザ側にWebRTCクライアントを追加する部分を分けます。
目次
- 【1】Raspberry pi の、GPIOをTypescriptから操作
- 【2】DCモーターをPWMで速度制御
- 【3】サーボモーターを制御
- 【4】DualSenseをブラウザに接続
- 【5】DualSenseの情報をRaspberryPiに飛ばす
- 【6】DualSenseのチャタリング?問題対応
- 【7-1】RaspberryPiから低遅延で映像を飛ばす
- 【7-2】RaspberryPiから低遅延で映像を飛ばす
- 【8】ThreeJSでVRもどきを作成
- 【9】iPhoneの加速度から頭の向きをVRに反映
- 【9-2】スマホVRゴーグル向けデザインに変える
- 【10】ラジコン本体の製作
- 【11】パワーアップとバッテリー問題の解決
配信映像をラジコン操作画面に持ってくる
momoのテスト画面で映像の配信がみれましたので
同じ内容を、ラジコン操作側に持ってきます。
本来は、経路情報、デバイスやらチャンネル情報を含めたICE、などを
手動かWebSocket経由で交換する必要がありますが
OpenAyame/ayame-web-sdkを使用すると
- 交換に必要なシグナリングサーバー(OpenAyameプロジェクトが提供)
- 送受信、受信のみ、送信のみ、のバリエーションでサンプルがすでに用意されている
と言う恩恵が受けられます。
今回は受信のみのサンプルを元に、WebRTCクライアントサービスを作って活用します。
ayame-sdk自体は
WebRTC接続にかかる、面倒なプロセスを極力簡素化しつつ
ストリームとデータ配信が簡単に実装できるようにまとめられていますので
WebRTCの取っ掛かりには間違いなく素晴らしい参考アイテムです。
独自に実装し、拡張していく場合は
ayame-sdkを使用すると、型情報の不足からビルドエラーになる部分があるのと
シグナリングサーバーにProxyを挟む、などネットワーク周りで独自問題があると思います。
momo側のサンプルを参考に、自環境に実装するにあたり、不足箇所を補う
という進め方のほうが多分良いと思います。
AyameLaboのシグナリングキー取得
シグナリングサーバーにayamelaboを拝借しますので
こちらからhttps://ayame-labo.shiguredo.app/ 登録を済ませ
シグナリングキーを取得します。
WebRTCクライアント実装
定番になってますが、まず設定情報系を保持するオブジェクトを作ります。
今回は環境を固定してますが
- ラジコン側の映像を複数の視聴者に送る
- 中継配信サーバーに映像を一旦飛ばす
などの発展形を考える場合は
映像コーデックを揃える。と言う難関が待っていますので注意が要ります。
export type ConnectionOptionsType = {
signalingKey: string
signalingUrl: string
roomIdPrefix: string
roomName: string
clientId: string
video: boolean
audio: boolean
simulcast: boolean
videoCodecType: 'VP8' | 'VP9' | 'H264' | 'AV1' | 'H265'
audioCodecType: 'OPUS' | 'ISAC' | 'G722' | 'PCMU' | 'PCMA' | 'G729' | 'ILBC' | 'AAC' | 'HEAAC'
iceServers: RTCIceServer[]
}
const ConnectionOptions: ConnectionOptionsType = {
signalingKey: 'OpenAyameプロジェクトで取得したキー',
signalingUrl: 'wss://ayame-labo.shiguredo.app/signaling',
roomIdPrefix: '自分のOpenAyameプロジェクトのID',
roomName: 'ayame-labo-sample',
clientId: crypto.randomUUID(),
video: true,
audio: true,
simulcast: true,
videoCodecType: "VP8",
audioCodecType: "OPUS",
iceServers: [
{
urls: "stun:stun.l.google.com:19302",
},
],
}
イベント処理
WebRTC特有の問題な気がしますが
httpやwebsocketと異なり、基本的に通信内容が見れません
なので、開発時は発生するすべてのイベントにリスナーを設定し
コンソールに吐くような処理を入れていないと、何が起こっているか分からない。
と言う素敵な出来事を経験出来ます
言い方を変えると
何かしらイベントと必ず紐づくので
該当のイベントに関数を設定するだけで良い
という見方も出来、実際勝手がわかると、接続さえ出来てしまえば
かなりいろんなことが出来ます。
- バイナリベースのファイル共有
- マウス座標を共有化した、ホワイトボードアプリ
- 自分が取得したストリームを、他に流して、ネットワーク集中を防ぐ
使い方は無限大
今回の場合
- open:接続
- disconect:切断
- addstream:ストリーム追加
の、3件のみ
OpenAI APIのRealtimeで必要な30数個に比べれば非常に簡単です。
分ける必要はありませんが
「接続・切断」と「ストリーム追加」で関数を分けています。
内容は単純に
接続イベント側は
- 接続時に、画面の指定要素にステータスを表示
- 切断時に、リモートビデオを消す
const addListenner = () => {
if (!conn) return
// WebRTC が確立したら connection-state に pc.connectionState 反映する
conn.on("open", (event: Event) => {
if (!conn) return;
const pc = conn.peerConnection;
if (pc) {
pc.onconnectionstatechange = (event: Event) => {
const connectionStateElement =
document.getElementById("connection-state") as HTMLSpanElement;
if (connectionStateElement) {
connectionStateElement.dataset.connectionState = pc.connectionState;
}
};
}
});
conn.on("disconnect", (event: Event) => {
if (!conn) return;
conn = null;
const remoteVideoElement = document.getElementById("remote-video",) as HTMLVideoElement;
if (remoteVideoElement) {
remoteVideoElement.srcObject = null;
}
});
conn.connect(null);
}
ストリームイベント側は
- 追加時にリモートビデオにストリームを繋ぐ
const addStream = () => {
if (!conn) return
conn.on("addstream", (event: any) => {
const remoteVideoElement = document.getElementById("video",) as HTMLVideoElement;
if (remoteVideoElement) {
remoteVideoElement.srcObject = event.stream;
}
});
}
接続開始
接続設定、リスナーの処理をまとめて呼び出し
接続を開始する、スタート関数を作ります。
loadViewer()がありますが
最終的に、ステータスを表示していないので無くても良いです。
export const connect = () => {
if (conn !== null) {
console.log('Ayame connection is already established')
return
}
options.clientId = ConnectionOptions.clientId
options.signalingKey = ConnectionOptions.signalingKey
const roomId = `${ConnectionOptions.roomIdPrefix}${ConnectionOptions.roomName}`
conn = connection(ConnectionOptions.signalingUrl, roomId, options, debug)
loadViewer()
addListenner()
addStream()
}
以上を内容を含めて、
webrtc.service.ts
import {
connection,
} from "@open-ayame/ayame-web-sdk"
import { defaultOptions } from "@open-ayame/ayame-web-sdk"
import Connection from "@open-ayame/ayame-web-sdk/dist/connection"
export type ConnectionOptionsType = {
signalingKey: string
signalingUrl: string
roomIdPrefix: string
roomName: string
clientId: string
video: boolean
audio: boolean
simulcast: boolean
videoCodecType: 'VP8' | 'VP9' | 'H264' | 'AV1' | 'H265'
audioCodecType: 'OPUS' | 'ISAC' | 'G722' | 'PCMU' | 'PCMA' | 'G729' | 'ILBC' | 'AAC' | 'HEAAC'
iceServers: RTCIceServer[]
}
const ConnectionOptions: ConnectionOptionsType = {
signalingKey: 'OpenAyameプロジェクトで取得したキー',
signalingUrl: 'wss://ayame-labo.shiguredo.app/signaling',
roomIdPrefix: '自分のOpenAyameプロジェクトのID',
roomName: 'ayame-labo-sample',
clientId: crypto.randomUUID(),
video: true,
audio: true,
simulcast: true,
videoCodecType: "VP8",
audioCodecType: "OPUS",
iceServers: [
{
urls: "stun:stun.l.google.com:19302",
},
],
}
export type StatusViewElementType = {
roomId: HTMLSpanElement | string
roomName: HTMLSpanElement | string
clientId: HTMLSpanElement | string
connectionState: HTMLSpanElement | string
remoteVideo: HTMLVideoElement | string
}
const StatusViewElement = {
roomId: '',
roomName: '',
clientId: '',
connectionState: '',
remoteVideo: 'video',
}
const debug = true;
const options = defaultOptions;
let conn: Connection | null = null;
export const setOptions = (options: ConnectionOptionsType) => {
Object.assign(ConnectionOptions, options);
}
export const setElements = (elements: StatusViewElementType) => {
Object.assign(StatusViewElement, elements);
}
export const setStatusDOM = (
roomId: string,
roomName: string,
clientId: string,
) => {
const roomIdElement = document.querySelector(
"#room-id",
) as HTMLSpanElement;
if (roomIdElement) {
roomIdElement.textContent = roomId;
}
const roomNameElement = document.querySelector(
"#room-name",
) as HTMLSpanElement;
if (roomNameElement) {
roomNameElement.textContent = roomName;
}
const clientIdElement = document.querySelector(
"#client-id",
) as HTMLSpanElement;
if (clientIdElement) {
clientIdElement.textContent = clientId;
}
}
export const connect = () => {
if (conn !== null) {
console.log('Ayame connection is already established')
return
}
options.clientId = ConnectionOptions.clientId
options.signalingKey = ConnectionOptions.signalingKey
const roomId = `${ConnectionOptions.roomIdPrefix}${ConnectionOptions.roomName}`
conn = connection(ConnectionOptions.signalingUrl, roomId, options, debug)
loadViewer()
addListenner()
addStream()
}
export const disconnect = () => {
if (!conn) {
console.log('Ayame connection is not established')
return
}
conn.disconnect()
}
const addStream = () => {
if (!conn) return
conn.on("addstream", (event: any) => {
const remoteVideoElement = document.getElementById(
"video",
) as HTMLVideoElement;
if (remoteVideoElement) {
remoteVideoElement.srcObject = event.stream;
}
});
}
const addListenner = () => {
if (!conn) return
// WebRTC が確立したら connection-state に pc.connectionState 反映する
conn.on("open", (event: Event) => {
if (!conn) return
const pc = conn.peerConnection;
if (pc) {
pc.onconnectionstatechange = (event: Event) => {
const connectionStateElement = document.getElementById(
"connection-state",
) as HTMLSpanElement;
if (connectionStateElement) {
connectionStateElement.dataset.connectionState = pc.connectionState;
}
};
}
});
conn.on("disconnect", (event: Event) => {
if (!conn) return;
conn = null;
const remoteVideoElement = document.getElementById("remote-video") as HTMLVideoElement;
if (remoteVideoElement) {
remoteVideoElement.srcObject = null;
}
});
conn.connect(null);
}
const loadViewer = () => {
for (const key in StatusViewElement) {
if (typeof StatusViewElement[key] === 'string' && StatusViewElement[key] !== '') {
StatusViewElement[key] = document.getElementById(StatusViewElement[key]) as HTMLSpanElement
}
}
}
配信の再生
テストで用意した画面では、「MOVIE」ボタンに作成した関数を割り当てています。
コレを押すと、このように配信映像がVideoタグで再生されます。
ビデオ要素は次の工程で非表示になる運命が待っていますので
配信が見れればここではOKです。
UZAYA
Uzayaでは、多分仕事を求めています。
何かの役に立ちそうでしたら、是非お知らせを。