こんにちは!
最近ではコロナの影響もあるのか、リアルタイム通信を行うオンライン動画サービスの開発依頼が増えてきました。
リアルタイムな WEB サービス構築にノウハウが溜まってきたので、私たちが普段使用している技術などを一挙紹介したいと思います!リアルタイムな WEB サービスの構築を検討している方はぜひご一読ください。
銀の弾丸はありませんので、本記事で紹介していない製品・サードパーティサービスでの構築も十分に検討の上、ご自身のプロジェクトにフィットするアーキテクチャーを検討するようにして下さい。本記事はあくまでも、私たちの過去のリアルタイム系サービスの開発経験に基づき執筆しているものです。
無料でアーキテクチャーの相談も受け付けておりますので、お気軽にご連絡ください。
本記事では、WebRTC に関する詳しい解説はしません。WebRTC に精通していなくとも、極力理解しやすい内容に努めますが、理解していた方がよりスムーズにキャッチアップできるかと思います。
以下に、理解しておいた方が良い用語をリストで記載しますので、お時間あればぜひ一度調べてみて下さい。一般的な WEB エンジニア・プロジェクトマネージャー職の方が、独学で深く理解するのは中々難易度が高いですので概要レベルの理解で結構です。(私たちも実装しながら深く理解していった次第です)
リアルタイム通信に関連しない、その他の細かな技術仕様については記載しません。
ユースケース | 採用技術 |
---|---|
WebRTC ( ICE/SFU サーバー ) | Vonage |
接続中の資料共有機能 | Vonage AWS AppSync |
接続中の画面共有機能 | Vonage AWS AppSync |
通話のレコーディング ( 録画 / 録音 ) | Vonage AWS AppSync |
接続中のリアルタイムチャット | AWS AppSync |
音声文字起こし | AWS Transcribe |
文字起こし結果の多言語翻訳 | AWS Translate |
文字起こし結果の機械学習 自然言語処理 (NLP) | AWS Comprehend |
決済 ( 例 : オンライン診療後の診療代の決済 ) | PayJP AWS AppSync AWS SQS AWS Lambda ( TypeScript ) |
利用者の認証認可(例 : Facebook / Auth0 / Email サインイン) | AWS Cognito AWS IAM STS |
WEB フロントエンド | NuxtJS(Vue3 API) TypeScript Lodash SCSS HTML Video API HTML RTCPeerConnection API |
この技術選定の中で最も重要なのは、WebRTC ( ICE/SFUサーバー ) の技術選定です。後述にて補足します。
まず、WebRTC を提供するサービスは海外含めれば無数に存在します。その中で私たちは、以下指標で技術選定を行いました。
一般的なサードパーティ製品の選定基準と類似しますが、中でも特徴的なのは WebRTC を提供するサーバーが利用者の所在地に物理的に近いこと、SFU サーバーが存在することを入念に確認しています。
これは、WebRTC 通信は UDP 通信を行う性質上、データの欠損が発生する頻度・可能性が高く、可能な限り欠損のない通信が実現できる低レイテンシーのサーバーを検討する必要があるためです。(結果画質が良くなります)
上記の指標で検討した結果、私たちは以下のサービスの技術検証を実施しました。
SkyWay は大手 NTT コミュニケーションズの提供する WebRTC サービスです。ベンダーの規模・知名度ともに申し分なく、かつ日本語のドキュメントが充実しており、WebRTC 初心者にはオススメなサービスです。
ただ、SkyWay には録画機能が存在しないのと、録音機能でエクスポート先に指定可能なのが GCP のクラウドストレージのみなので、私たちは採用を見送りました。
※ 録画機能自体は自前でストリーミング処理用のサーバーを用意し、MediaStream をバッファリングすれば実現できなくもないですが、本記事のテーマでもある「1ヶ月以内で構築」を実現できない可能性が上がるため却下とします。
意外と知られていないですが、実は Kinesis ファミリーには WebRTC を提供する Kinesis Video Stream ( WebRTC ) サーバーが存在します。私たちは通常 AWS サーバーレス製品を使用して WEB サービス/スマフォアプリを構築するため、実は一番最初に目をつけたサービスです。
KinesisWebRTCは、なんといってもスペックの高い ICE サーバー+ SFU サーバーを提供してくれるので、画質の良い ( つまり低レイテンシー ) 通信を実現させてくれます。
ただKinesisWebRTCは、「1対1」 or 「1対N」 の接続を前提とするため、標準では MCU 非対応となっており採用を見送りしました。もう一点残念なのは、KinesisVidesStream を使用した録画機能の実現について、IoT やウェアラブルデバイス、スマフォアプリを前提としているため、WEB 通信向けの JavaScript の SDK が提供されていない様子でした。
また、KinesisWebRTC を使用する場合、フロントエンド実装の際にある程度 WebRTC の仕様の理解を求められますので、やや実装の敷居が高い印象です。SDK でよしなに Video タグをストリームへ流すなどしてくれないので、自身でストリームイベントを検知し Video タグへ流すなどの実装が別途必要です。逆にいうと柔軟な実装に対応できるので、玄人向けの実装と言えます。
※ 実は Master を各デバイスのストリームの HUB にすれば無理矢理ですが MCU を実現可能です ( 本来の用途ではないため非推奨 )
最後に、私たちが採用している Vonage です。まず Vonage の強みとしてストリーミング処理の豊富な機能が挙げられます。以下のようなリアルタイム通信でよくある機能を Vonage が提供している SDK を使用するとサクッと実装することが可能です。
後述で実装方法を紹介します。
Vonage は、以下のワークフローで WebRTC 通信を行います。
※ セッション ( SessionID ) = 接続先のミーティングルームの ID
※ トークン ( Token ) = ミーティングルームへの接続用の鍵
※ ServerSide の処理にはシークレットトークンを用いるがシークレットトークンは漏洩するとまずいので AWS のシークレットマネージャーでエンクリプトして利用することを推奨
Vonage の開発ドキュメントのソースコードを、私たち独自に TypeScript で実装したサンプルです。ご自身の環境に合わせてカスタムして利用してください。尚、以降のサンプルは MCU のビデオ通話には対応していますが、アーカイブ機能(録画/録音)には対応していません。(実装はお気軽にお問い合わせください)
$ yarn add @opentok/client
# or
$ npm install @opentok/client --save
import OT, { Session, Event, Connection, Stream } from "@opentok/client";
type SessionDisconnectedEvent = Event<"sessionDisconnected", Session> & {
reason: string;
};
type ConnectionCreatedEvent = Event<"connectionCreated", Session> & {
connection: Connection;
};
type ConnectionDestroyedEvent = Event<"connectionDestroyed", Session> & {
connection: Connection;
reason: string;
};
type StreamCreatedEvent = Event<"streamCreated", Publisher> & {
stream: Stream;
};
type Handler = {
handleError?: Function;
onConnectionCreated?: Function;
onConnectionDestroyed?: Function;
onSessionDisconnected?: Function;
onConnected?: Function;
onPublisherCompleted?: Function;
onPublisherConnected?: Function;
};
type Publisher = {
element: HTMLElement;
opt: Record<string, unknown>;
};
type Subscriber = {
element: HTMLElement;
opt: Record<string, unknown>;
};
export default class VonageService {
private apiKey: string;
private sessionId: string;
private token: string;
private handleErrorFunction: Function;
private onConnectionCreated: Function;
private onConnectionDestroyed: Function;
private onSessionDisconnected: Function;
private onConnected: Function;
private onPublisherCompleted: Function;
private onPublisherConnected: Function;
private publisher: Publisher;
private subscriber: Subscriber;
private session: Session;
constructor(
apiKey: string,
sessionId: string,
token: string,
publisher: Publisher,
subscriber: Subscriber,
handler?: Handler
) {
if (OT.checkSystemRequirements() !== 1)
throw new Error("ご使用のブラウザはWebRTC非対応です。");
this.handleErrorFunction = handler?.handleError || this.defaultHandleError;
this.onConnectionCreated =
handler?.onConnectionCreated || this.defaultHandler;
this.onConnectionDestroyed =
handler?.onConnectionDestroyed || this.defaultHandler;
this.onSessionDisconnected =
handler?.onSessionDisconnected || this.defaultHandler;
this.onConnected = handler?.onConnected || this.defaultHandler;
this.onPublisherCompleted =
handler?.onPublisherCompleted || this.defaultHandler;
this.onPublisherConnected =
handler?.onPublisherConnected || this.defaultHandler;
this.sessionId = sessionId;
this.publisher = publisher;
this.subscriber = subscriber;
this.apiKey = apiKey;
this.token = token;
this.session = OT.initSession(this.apiKey, this.sessionId);
}
public get getSession(): Session | undefined {
return this.session;
}
public connect() {
const handleError = this.handleErrorFunction;
const onConnectionCreated = this.onConnectionCreated;
const onConnectionDestroyed = this.onConnectionDestroyed;
const onSessionDisconnected = this.onSessionDisconnected;
const onConnected = this.onConnected;
const onPublisherCompleted = this.onPublisherCompleted;
const publish = () => {
const publisher = OT.initPublisher(
this.publisher.element,
this.publisher.opt,
(err) => {
if (err) {
handleError();
} else {
onPublisherCompleted();
}
}
);
this.session.publish(publisher, (err) => {
if (err) {
handleError();
} else {
this.onPublisherConnected();
}
});
};
const subscribe = (event: StreamCreatedEvent) => {
function handleError(error: any) {
if (error) {
alert(error.message);
}
}
this.session.subscribe(
event.stream,
this.subscriber.element,
this.subscriber.opt,
handleError
);
};
this.session.on({
streamCreated: function (event: StreamCreatedEvent) {
subscribe(event);
},
connectionCreated: function (event: ConnectionCreatedEvent) {
onConnectionCreated(event);
},
connectionDestroyed: function (event: ConnectionDestroyedEvent) {
onConnectionDestroyed(event);
},
sessionDisconnected: function sessionDisconnectHandler(
event: SessionDisconnectedEvent
) {
if (event.reason == "networkDisconnected")
throw new Error("Your network connection terminated.");
onSessionDisconnected(event);
},
});
this.session.connect(this.token, function (error) {
if (error) {
handleError(error);
} else {
publish();
onConnected();
}
});
}
public disconnect() {
this.session.disconnect();
}
private defaultHandleError(error?: any): void {
if (error) throw new Error(error);
}
private defaultHandler(event: any): void {
console.info(event);
}
}
上記を、フロントエンドの View 側で利用してください。以下は NuxtJS ( Vue3 / Composition-API ) の実装サンプルです。
<template>
<div>
<div id="publisher-view" />
<div id="subscriber-view" />
</div>
</template>
import {
defineComponent,
onMounted,
useContext,
ref,
} from "@nuxtjs/composition-api";
import _ from "lodash";
import VonageService from "~/services/vonageService";
export default defineComponent({
middleware: "authenticated",
onMounted(() => {
const connectionCount = ref(0); // 接続人数
const context = useContext();
const start = async () => {
// TODO: セッションIDはWEBサーバーで作成し取得すること
const sessionId = "XXXX";
// TODO: トークンはWEBサーバーで作成し取得すること
const token = "XXXX";
const onConnectionCreated = () => {
console.log("onConnectionCreated on vue file.");
connectionCount.value++;
};
const onConnectionDestroyed = () => {
console.log("onConnectionDestroyed on vue file.");
connectionCount.value--;
};
const onSessionDisconnected = () => {
console.log("onSessionDisconnected on vue file.");
connectionCount.value--;
};
const onConnected = () => {
console.log("onConnected on vue file.");
connectionCount.value = 0;
};
const onPublisherCompleted = () => {
console.log("onPublisherCompleted on vue file.");
};
const onPublisherConnected = () => {
console.log("onPublisherConnected on vue file.");
};
const publisher = {
element: document.getElementById("publisher-view") as HTMLElement,
opt: {
insertMode: "append",
width: "calc(50vw - 15px)",
height: "calc(100% - 40px)",
},
};
const subscriber = {
element: document.getElementById("subscriber-view") as HTMLElement,
opt: {
insertMode: "append",
width: "calc(50vw - 15px)",
height: "calc(100% - 40px)",
},
};
const apiKey = context.$config.VONAGE_API_KEY; // nuxt.config.jsへ要設定(publicRuntimeConfigプロパティー)
const vonageService = new VonageService(
apiKey,
sessionId,
token,
publisher,
subscriber,
{
onPublisherCompleted,
onPublisherConnected,
onConnectionCreated,
onConnectionDestroyed,
onSessionDisconnected,
onConnected,
}
);
vonageService.connect();
});
return {};
}
});
最新の WebRTC の仕組み・実装を覚えれば、WEB 上でのリアルタイムな通信を行うアプリ構築が可能となり、WEB で表現可能な範囲が一気に広がると思います。
日本の開発市場では WebRTC を自由に触れるエンジニアがまだまだ希少なので、本記事をきっかけに WebRTC を使用した高度な実装にチャレンジする方が増えていけば幸いです。
リアルタイムなアプリケーション構築はお気軽にお問い合わせください。
スモールスタート開発支援、サーバーレス・NoSQLのことなら
ラーゲイトまでご相談ください
低コスト、サーバーレスの
モダナイズ開発をご検討なら
下請け対応可能
Sler企業様からの依頼も歓迎