🚀 クイックスタート
チュートリアルを始めるにあたって
本チュートリアルは、JavaScript の基本的な知識、及び、npm の利用経験を前提としています。
JavaScript の言語仕様や開発環境の構築手順は記載していないため、必要に応じて他の Web サイト等を参考にしてください。
チュートリアル
SkyWay のメディア通信を体験できるシンプルなサンプルアプリケーションを作成します。
本チュートリアルでは Room ライブラリを使用します。
完成品(NPM パッケージ利用)は https://github.com/skyway/js-sdk/tree/main/examples/tutorial にあります。
SDK を CDN 経由で利用する場合と、NPM 経由で利用する場合の 2 パターンの環境構築方法を紹介します。 なお、CDN にホストされているライブラリはアップデートに伴い自動的に最新版に更新されるため、商用環境での利用はおすすめしておりません。また、CDN を介して利用できるのは Room ライブラリのみとなっています。
環境構築 [CDN を利用する場合]
HTML 作成
index.html というファイルを作成し、以下の内容にします。
index.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width" /> <title>SkyWay Tutorial</title> </head> <body> <p>ID: <span id="my-id"></span></p> <div> room name: <input id="room-name" type="text" /> <button id="join">join</button> <button id="leave">leave</button> </div> <video id="local-video" width="400px" muted playsinline></video> <div id="button-area"></div> <div id="remote-media-area"></div> <script src="https://cdn.jsdelivr.net/npm/@skyway-sdk/room/dist/skyway_room-latest.js"></script> <script src="main.js"></script> </body> </html>
JavaScript ファイル作成
index.html と同じディレクトリで main.js というファイルを作成してください。 チュートリアルのプログラムはこのファイルの中に記述します。
各種モジュールの取得
main.js ファイルの先頭に以下の内容を入力し、これから用いる各種モジュールをライブラリより取得します。
main.js
const { nowInSec, SkyWayAuthToken, SkyWayContext, SkyWayRoom, SkyWayStreamFactory, uuidV4 } = skyway_room;
アプリケーションの起動方法
以下に、ローカルサーバーを用意するいくつかの方法を記載します。
- Visual Studio Code の拡張機能 Live Server
- 各種プログラミング言語での起動コマンド
# python 2.X $ python -m SimpleHTTPServer 8080 # python 3.X $ python -m http.server 8080 # ruby $ ruby -run -e httpd . -p 8080 # php $ php -S localhost:8080 # Node.js $ npx http-server -p 8080
ブラウザからローカルサーバーのアドレス経由で index.html ファイルを開くとアプリケーションが起動します。
環境構築 [NPM を利用する場合]
事前準備
-
Node.js のバージョン 16 以降をインストールする
-
任意の作業ディレクトリに tutorial フォルダを作成する
-
tutorial フォルダ直下で次の作業をする
3-1.
npm init
を実行し、対話画面が終了するまで Enter キーをクリックする3-2.
npm i @skyway-sdk/room
を実行する3-3.
npm i -D parcel
を実行する3-4. src フォルダを作成する
3-5. package.json ファイルを開き、scripts の階層に
"dev": "parcel ./src/index.html",
を追記する
HTML 作成
src フォルダの下に index.html というファイルを作成し、以下の内容を入力します。
index.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width" /> <title>SkyWay Tutorial</title> </head> <body> <p>ID: <span id="my-id"></span></p> <div> room name: <input id="room-name" type="text" /> <button id="join">join</button> <button id="leave">leave</button> </div> <video id="local-video" width="400px" muted playsinline></video> <div id="button-area"></div> <div id="remote-media-area"></div> <script type="module" src="main.js"></script> </body> </html>
JavaScript ファイル作成
src フォルダの下に main.js というファイルを作成してください。 チュートリアルのプログラムはこのファイルの中に記述します。
各種モジュールの取得
main.js ファイルの先頭に以下の内容を入力し、これから用いる各種モジュールをライブラリより取得します。
main.js
import { nowInSec, SkyWayAuthToken, SkyWayContext, SkyWayRoom, SkyWayStreamFactory, uuidV4 } from "@skyway-sdk/room";
アプリケーションの起動方法
tutorial ディレクトリで以下のコマンドを実行するとローカルサーバーが起動します。
npm run dev
ローカルサーバーのアドレスをブラウザで開くとアプリケーションが起動します。
アプリケーション ID とシークレットキーの取得
※SkyWay への登録がまだの方はこちらから
SkyWay コンソールへログインし、以下の 3 つを行います。
-
「アプリケーションを作成」ボタンを押す
-
アプリケーション名を入力して作成ボタンを押す
-
アプリケーション一覧からアプリケーション ID とシークレットキーをコピーする
SkyWay Auth Token を作る
SkyWay Auth Token とは、SkyWay を利用するための JWT 形式のトークンです。
トークンごとに権限を細かく設定することでき、例えば room ごとの入室を特定ユーザーに制限する、といったことができます。
ここでは細かい SkyWay Auth Token の設定方法に関する説明は省きます(SkyWay Auth Token についてはこちら)。
本チュートリアルでは、すぐに通信を試していただくために、トークン生成をクライアントアプリケーションで実装していますが、 本来、 SkyWay Auth Token はサーバーアプリケーションで生成してクライアントアプリケーションに渡すようにするべきです。 クライアントアプリケーションでトークン生成を行った場合、任意の Channel(Room) に入ることができるようなトークンを第三者が作成する可能性があります。
SkyWay では、SkyWay Auth Tokenを生成するためのサーバーアプリケーションのサンプルを提供しています。 サーバーアプリケーションを実装する際は、こちらも参考にしてください。
main.js に以下の内容を入力してください。先ほど取得したアプリケーション ID とシークレットキーを置換する必要があります。
main.js
const token = new SkyWayAuthToken({ jti: uuidV4(), iat: nowInSec(), exp: nowInSec() + 60 * 60 * 24, scope: { app: { id: "ここにアプリケーション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("ここにシークレットキーをペーストしてください");
カメラ映像、マイク音声の取得
main.js 内に、カメラから映像を取得して、video タグにセットするコードを追加します。
- 即時実行の async 関数で全体を囲みます。これにより、非同期処理を await で記述できるようになります。以降の JavaScript の処理はこの即時実行関数内に記述します。
- マイク音声とカメラ映像を取得し、それぞれを変数に分割代入します。
- video 要素に映像(video)をセットします(audio は後ほど利用します)。
- セットした映像を再生します。
main.js
(async () => { // 1 const localVideo = document.getElementById("local-video"); const { audio, video } = await SkyWayStreamFactory.createMicrophoneAudioAndCameraStream(); // 2 video.attach(localVideo); // 3 await localVideo.play(); // 4 })(); // 1
ここで、一度アプリケーションを起動して、カメラ映像が表示されるか確かめてみましょう。
環境構築のアプリケーションの起動方法の項目に従ってアプリケーションを起動してください。
通信処理の追加
相手との通信を行うための処理を追加していきます。
HTML 要素の取得
後ほど利用するため、要素を JavaScript 側で変数に格納しておきます。
main.js
const buttonArea = document.getElementById("button-area"); const remoteMediaArea = document.getElementById("remote-media-area"); const roomNameInput = document.getElementById("room-name"); const myId = document.getElementById("my-id"); const joinButton = document.getElementById("join"); const leaveButton = document.getElementById('leave');
room の作成と入室
join ボタンを押した際に実行されるイベントハンドラを作成し、この中に以降の処理を記載していきます。
先ほど生成した SkyWay Auth Token を用いて、context
を作ります。
context
とは、グローバルな情報を管理するオブジェクトです。認証・認可や、ログの設定などの情報を管理します。
また、このとき、roomNameInput
が空の場合には、以降の処理が実行できないため、空かどうかのチェックを入れています。
main.js
joinButton.onclick = async () => { if (roomNameInput.value === "") return; const context = await SkyWayContext.Create(token); };
次に SkyWayRoom.FindOrCreate
という関数の第一引数に、先ほど作成した context
を渡して、room
を作成します。
この関数は、もしすでに同じ name の room が存在しなければ作成し、存在する場合にはその room を取得するという関数です。
第2引数のオブジェクトで、type
には”p2p”を指定します(なお、”sfu”を指定すると SFU ルームを作成可能です)。name には、ユーザーが input 要素に入力した値を用います。
main.js
const room = await SkyWayRoom.FindOrCreate(context, { type: "p2p", name: roomNameInput.value, });
次に、先ほど作成(or 取得)した room
に入室します。 すると Member
オブジェクトが返ってきます。ここでは me
という変数名とします。
自分の ID を表示するために、me.id を span 要素に格納します。
main.js
const me = await room.join(); myId.textContent = me.id;
自分の映像と音声を publish する
Member
オブジェクトの publish
関数の引数に、先ほど取得した audio と video を渡して、音声・映像を publish します。
main.js
await me.publish(audio); await me.publish(video);
相手の映像と音声を subscribe する
相手の映像と音声を subscribe し、audio, video 要素にセットする処理を追加します。
main.js
const subscribeAndAttach = (publication) => { // 3 if (publication.publisher.id === me.id) return; const subscribeButton = document.createElement("button"); // 3-1 subscribeButton.id = `subscribe-button-${publication.id}`; subscribeButton.textContent = `${publication.publisher.id}: ${publication.contentType}`; buttonArea.appendChild(subscribeButton); subscribeButton.onclick = async () => { // 3-2 const { stream } = await me.subscribe(publication.id); // 3-2-1 let newMedia; // 3-2-2 switch (stream.track.kind) { case "video": newMedia = document.createElement("video"); newMedia.playsInline = true; newMedia.autoplay = true; break; case "audio": newMedia = document.createElement("audio"); newMedia.controls = true; newMedia.autoplay = true; break; default: return; } newMedia.id = `media-${publication.id}`; stream.attach(newMedia); // 3-2-3 remoteMediaArea.appendChild(newMedia); }; }; room.publications.forEach(subscribeAndAttach); // 1 room.onStreamPublished.add((e) => { // 2 subscribeAndAttach(e.publication); });
-
room
のpublications
プロパティに、room に存在するpublication
の配列が入っています。この配列の各要素を、subscribeAndAttach
関数の引数に与えています。この関数については後ほど説明します。 -
room
のonStreamPublished
はEvent
型のプロパティです。Event
には、add
という関数があります。この関数の引数にコールバック関数を渡すと、その room 内で誰かが publish された時点でコールバック関数が実行されます。コールバック関数の引数に入っているオブジェクトのpublication
プロパティに、publish されたpublication
が存在していますので、これをsubscribeAndAttach
関数に渡します。 -
subscribeAndAttach
関数を作成します。引数に publication を取ります。この publication が自分(me)が publish したものでない場合に、以降の処理を実行します。3-1. publisher.id と publication の contentType(video or audio)をラベルにしたボタンを、ボタンエリアに追加します。
3-2. 3-1 で作成したボタンのイベントハンドラを作成します。
3-2-1. publication を subscribe します。すると
stream
が返却されます。3-2-2. audio 要素 or video 要素を作成します。
newMedia
という名前の変数を作成し、stream.track.kind
が video であれば video 要素を、audio であれば audio 要素を作成し、それぞれ適切な属性を設定します。3-2-3.
stream
を、先ほど作成したnewMedia
(audio 要素 or video 要素)にセットし、その後、remoteMediaArea
(div 要素)に追加します。
自分の退室処理を実装する
Member
オブジェクトの leave
関数で room
から退室します。退室後は room
についての処理を行わないため、dispose
関数で room
に関連するリソースを解放・破棄します。
leaveButton.onclick = async () => { await me.leave(); await room.dispose(); myId.textContent = ""; buttonArea.replaceChildren(); remoteMediaArea.replaceChildren(); };
相手の退室処理を実装する
onStreamPublished
と同様に、room
の onStreamUnublished
は Event
型のプロパティです。この関数の引数にコールバック関数を渡すと、その room 内で誰かの publication が unpublish された時点でコールバック関数が実行されます。相手が退室すると自動的に相手の publication が unpublish されるため、退室時もこのコールバック関数が実行されます。コールバック関数の引数に入っているオブジェクトの publication
プロパティには unpublish された publication
が存在しています。
room.onStreamUnpublished.add((e) => { document.getElementById(`subscribe-button-${e.publication.id}`)?.remove(); document.getElementById(`media-${e.publication.id}`)?.remove(); });
これで完成です。
アプリケーションの起動方法に従ってアプリケーションを起動して、複数の window で表示しましょう。
それぞれで同じルーム名を入力し join
ボタンを押すと、room 内に存在する publication のボタンが表示されます。そのボタンを押すと、相手の映像と音声が表示されるはずです。
完成したアプリケーションを、GitHub PagesやNetlifyなど、ホスティングサービスを利用してアップロードし、複数の端末からの接続を試してみましょう。
ライブラリの取得部分を除いたコード全体はこちら
main.js
const token = new SkyWayAuthToken({ jti: uuidV4(), iat: nowInSec(), exp: nowInSec() + 60 * 60 * 24, scope: { app: { id: "ここにアプリケーション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("ここにシークレットキーをペーストしてください"); (async () => { const localVideo = document.getElementById("local-video"); const buttonArea = document.getElementById("button-area"); const remoteMediaArea = document.getElementById("remote-media-area"); const roomNameInput = document.getElementById("room-name"); const myId = document.getElementById("my-id"); const joinButton = document.getElementById("join"); const leaveButton = document.getElementById("leave"); const { audio, video } = await SkyWayStreamFactory.createMicrophoneAudioAndCameraStream(); video.attach(localVideo); await localVideo.play(); joinButton.onclick = async () => { if (roomNameInput.value === "") return; const context = await SkyWayContext.Create(token); const room = await SkyWayRoom.FindOrCreate(context, { type: "p2p", name: roomNameInput.value, }); const me = await room.join(); myId.textContent = me.id; await me.publish(audio); await me.publish(video); const subscribeAndAttach = (publication) => { if (publication.publisher.id === me.id) return; const subscribeButton = document.createElement("button"); subscribeButton.id = `subscribe-button-${publication.id}`; subscribeButton.textContent = `${publication.publisher.id}: ${publication.contentType}`; buttonArea.appendChild(subscribeButton); subscribeButton.onclick = async () => { const { stream } = await me.subscribe(publication.id); let newMedia; switch (stream.track.kind) { case "video": newMedia = document.createElement("video"); newMedia.playsInline = true; newMedia.autoplay = true; break; case "audio": newMedia = document.createElement("audio"); newMedia.controls = true; newMedia.autoplay = true; break; default: return; } newMedia.id = `media-${publication.id}`; stream.attach(newMedia); remoteMediaArea.appendChild(newMedia); }; }; room.publications.forEach(subscribeAndAttach); room.onStreamPublished.add((e) => subscribeAndAttach(e.publication)); leaveButton.onclick = async () => { await me.leave(); await room.dispose(); myId.textContent = ""; buttonArea.replaceChildren(); remoteMediaArea.replaceChildren(); }; room.onStreamUnpublished.add((e) => { document.getElementById(`subscribe-button-${e.publication.id}`)?.remove(); document.getElementById(`media-${e.publication.id}`)?.remove(); }); }; })();