🚀 クイックスタート

チュートリアルを始めるにあたって

本チュートリアルは、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 を利用する場合]

事前準備

  1. Node.js のバージョン 16 以降をインストールする

  2. 任意の作業ディレクトリに tutorial フォルダを作成する

  3. 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 つを行います。

  1. 「アプリケーションを作成」ボタンを押す Peer

  2. アプリケーション名を入力して作成ボタンを押す

  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 タグにセットするコードを追加します。

  1. 即時実行の async 関数で全体を囲みます。これにより、非同期処理を await で記述できるようになります。以降の JavaScript の処理はこの即時実行関数内に記述します。
  2. マイク音声とカメラ映像を取得し、それぞれを変数に分割代入します。
  3. video 要素に映像(video)をセットします(audio は後ほど利用します)。
  4. セットした映像を再生します。

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); });
  1. roompublications プロパティに、room に存在する publication の配列が入っています。この配列の各要素を、subscribeAndAttach 関数の引数に与えています。この関数については後ほど説明します。

  2. roomonStreamPublishedEvent 型のプロパティです。Event には、add という関数があります。この関数の引数にコールバック関数を渡すと、その room 内で誰かが publish された時点でコールバック関数が実行されます。コールバック関数の引数に入っているオブジェクトの publication プロパティに、publish された publication が存在していますので、これを subscribeAndAttach 関数に渡します。

  3. 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 と同様に、roomonStreamUnublishedEvent 型のプロパティです。この関数の引数にコールバック関数を渡すと、その 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 PagesNetlifyなど、ホスティングサービスを利用してアップロードし、複数の端末からの接続を試してみましょう。

ライブラリの取得部分を除いたコード全体はこちら

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(); }); }; })();