🚀 クイックスタート
JS-SDK のクイックスタートをベースに、SkyWay JS-SDK と AI Noise Canceller ライブラリを使った簡単なアプリケーション tutorial
を作成します。
クイックスタートの環境
以下の環境で実施してください。また最新版のブラウザ利用を推奨します。
- Node: v20 以降
- 対応ブラウザ: Chrome / Edge
SkyWay を使った通話アプリを作る
環境構築 [NPMを利用する場合] 以降の手順に従って SkyWay を使った通話アプリを作成してください。
完成した通話アプリに以下の変更を加えてください。
createMicrophoneAudioAndCameraStream
にオプションを追加する
// const { audio, video } = // await SkyWayStreamFactory.createMicrophoneAudioAndCameraStream(); // を以下に置き換える const { audio, video } = await SkyWayStreamFactory.createMicrophoneAudioAndCameraStream({ audio: { echoCancellation: false, } });
追加後は下記のような実装になっているはずです。
src/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>
src/main.js
import { nowInSec, SkyWayAuthToken, SkyWayContext, SkyWayRoom, SkyWayStreamFactory, uuidV4 } from "@skyway-sdk/room"; const token = new SkyWayAuthToken({ jti: uuidV4(), iat: nowInSec(), exp: nowInSec() + 60 * 60 * 24, version: 3, scope: { appId: "ここにアプリケーションIDをペーストしてください", rooms: [ { id: "*", methods: ["create", "close", "updateMetadata"], member: { id: "*", methods: ["publish", "subscribe", "updateMetadata"], }, sfu: { enabled: true, }, }, ], turn: { enabled: true, }, analytics: { enabled: true, }, }, }).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({ audio: { echoCancellation: false, } }); 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(); }); }; })();
この時点でのディレクトリ構造は次の通りです。
tutorial ├── package-lock.json ├── package.json ├── node_modules │ └── ... └── src ├── index.html └── main.js
この記述をした段階で npm run dev
してみましょう。 SkyWay の Room パッケージで P2P の通話アプリが動作しているはずです。
本チュートリアルでは、すぐに通信を試していただくためにトークン生成をクライアントアプリケーションで実装しています。 本来は SkyWay Auth Token はサーバーアプリケーションで生成してクライアントアプリケーションに渡すようにする必要があります。 クライアントアプリケーションでトークン生成を行った場合、任意の Channel(Room) に入ることができるようなトークンを第三者が作成する可能性があります。
音声の確認方法
1 人で動作を確認したい場合、以下の手順で確認できます。
- ブラウザのタブを 2 つ開き、それぞれ http://localhost:1234 にアクセスします
- それぞれのタブで同じ room name を入力し join ボタンを押します
- どちらも join した後に「{UUID}: audio」というボタンを押すと対向のタブからの音声を受信(subscribe)できます
※ 本チュートリアルでは、 1 人で動作を確認する際に出力音声へ影響を与えないよう、ブラウザのエコーキャンセリング機能を無効化しています。音声確認時は必ずイヤホンをご利用ください。
AI Noise Canceller を組み込んだアプリへ改修
ライブラリのインストール
ライブラリをインストールする前に、環境変数を設定する必要があります。 appId と secret の値を差し替えて、以下のコマンドを実行してください。
export SKYWAY_APP_ID="your-app-id" export SKYWAY_SECRET_KEY="your-app-secret"
以下のコマンドを実行して、ライブラリをインストールします。
curl -fsSL https://raw.githubusercontent.com/skyway/ai-noise-canceller/refs/heads/main/tools/js/install.sh | bash
上記のコマンドにより、tmp
ディレクトリに最新版バージョンの AI Noise Canceller がダウンロードされ、 node_modules
に追加されます。
ライブラリのインストールが完了したら、tmp
配下にある tgz
ファイルは削除してしまって構いません。
上記で実行するシェルスクリプトは、 端末内で SkyWay Admin Auth Token を生成※しライブラリ取得の認証に利用しています。 この SkyWay Admin Auth Token は、アプリケーションの管理者(サーバー)用APIを利用する際に必要なトークンであり、本トークンが流出した場合は第三者に管理者(サーバー)用APIを悪用されてしまう恐れがあります。 取り扱いには十分に気をつけてください。
※ SkyWay Admin Auth Token の有効期限は1時間です
--download-only
の引数を付与することで、 ライブラリのみの取得も可能です。
# tmp ディレクトリに保存 curl -fsSL https://raw.githubusercontent.com/skyway/ai-noise-canceller/refs/heads/main/tools/js/install.sh | bash -s -- --download-only --dest="tmp"
取得したライブラリが手元にあれば、パッケージマネージャーを利用して追加できます。
# npmを用いた場合 npm install ./tmp/skyway-ai-noise-canceller-x.x.x.tgz
ノイズ抑制の実装
次に src/main.js を編集し、ノイズ抑制の機能を組み込んでいきます。 src/main.js の先頭に以下を記載してライブラリを読み込ませます。
import { SkyWayNoiseCanceller } from "skyway-ai-noise-canceller";
ノイズ抑制の機能を利用する際は認可を追加する必要があります。SkyWayAuthToken
の scope
へ以下のように noiseCancelling
の項目を追加します。
const token = new SkyWayAuthToken({ ... scope: { ... analytics: { enabled: true }, noiseCancelling: { enabled: true } }, }).encode(secret);
続いて、ブラウザが提供する noiseSuppression
は不要なので音声取得の設定を変更して false
にしておきます。
// const { audio, video } = // await SkyWayStreamFactory.createMicrophoneAudioAndCameraStream({ // audio: { // echoCancellation: false, // } // }); // を以下に置き換える const { audio, video } = await SkyWayStreamFactory.createMicrophoneAudioAndCameraStream({ audio: { echoCancellation: false, // 1 人で出力音声を確認する際には出力音声に影響を与えないように false にする noiseSuppression: false, // AI Noise Canceller のノイズ抑制と競合しないように false にする } });
音声にノイズ抑制をかけて送信するためには以下のように変更します。
// await me.publish(audio); を以下に置き換える const noiseCanceller = new SkyWayNoiseCanceller(context); // インスタンスの作成 // onReadyイベント発火時に実行する処理を登録 noiseCanceller.onReady(async () => { const noiseCancelledAudio = await noiseCanceller.connect(audio); // ノイズ抑制後の音声をnoiseCancelledAudioとして取得 await me.publish(noiseCancelledAudio); }); noiseCanceller.init() // インスタンスの初期化処理
SkyWayNoiseCanceller
インスタンスを生成し、 init
で抑制処理の初期化をします。初期化が完了すると onReady
イベントが発火します。
onReady
イベント発火時にマイクから取得した音声ストリームを connect
することで、ノイズ抑制が適用された音声ストリームを noiseCancelledAudio
として受け取ることができます。
また、ノイズ抑制の機能を利用するには SkyWayAuthToken による認証および認可が必要です。SkyWayContext をインスタンス生成時に引き渡してください。
この時点で、 npm run dev
を実行して音声を確認してみましょう。
確認方法は 先程実装した通話アプリでの確認方法 と同様です。
発話しながらマウスクリックやキータイプを行うことで、これらのノイズが低減していることが確認※できます。次の手順で実装する ON/OFF 切り替え機能を利用するとより処理結果を把握しやすくなります。
※ ノイズ抑制によって音声の出力に数十 ms の遅延が発生します。この遅延はビデオ・音声通話による遅延と比べて小さいため、通話の実現について大きな影響はございません。
ノイズ抑制の ON/OFF 切り替え実装
続いて、ノイズ抑制の ON/OFF を切り替えられるように実装を変更してみましょう。 src/index.html を編集し、room name の入力フォームおよびボタンの下にノイズ抑制 ON/OFF ボタンを作ります。
<!DOCTYPE html> <html lang="en"> ... <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を追加 --> <div id="noise-cancel-area"> <!-- ノイズ抑制の on/off 状態を表示 --> noise cancelling: <span id="noise-cancelling">false</span> <!-- ノイズ抑制の on/off を切り替える --> <button id="noise-cancel">noise cancel</button> </div> <!-- ここまで --> <div id="button-area"></div> ... </html>
次に、src/main.js を編集し、ボタン操作の割り当てをするために Element を保持する変数を定義します。
// const leaveButton = document.getElementById("leave"); の下に追加 const noiseCancelButton = document.getElementById("noise-cancel"); const noiseCancelling = document.getElementById("noise-cancelling");
また、先ほど追加したノイズ抑制の実装を以下のように変更します。
// const noiseCanceller = new SkyWayNoiseCanceller(context); // インスタンスの作成 // // onReadyイベント発火時に実行する処理を登録 // noiseCanceller.onReady(async () => { // const noiseCancelledAudio = await noiseCanceller.connect(audio); // ノイズ抑制後の音声をnoiseCancelledAudioとして取得 // await me.publish(noiseCancelledAudio); // }); // noiseCanceller.init() // インスタンスの初期化処理 // 上記を削除して以下に置き換える let noiseCanceller; // ボタンでインスタンスを生成/破棄できるように `let` で宣言 const myAudioPublication = await me.publish(audio); // ボタン操作でストリームを差し替えできるよう、変数で保持 noiseCancelButton.onclick = async () => {};
ノイズ抑制が有効になっている場合とそうでない場合で処理を分けていきます。
- 有効にする際:
- インスタンスの生成
- onReady イベントの登録
- audioStream の処理と Element の差し替え
- ノイズ抑制が有効であることを表示
- インスタンスの初期化処理
- 無効にする際:
- インスタンスの破棄
- 元々の audioStream への Element 差し替え
- ノイズ抑制が無効であることを表示
... noiseCancelButton.onclick = async () => { if (noiseCancelling.textContent === "false") { // 有効時の処理 } else { // 無効時の処理 } };
有効時の処理は今回次のように記述します。
SkyWayNoiseCanceller
のインスタンスを生成します。onReady
イベントハンドラーを登録し、ハンドラーの中でSkyWayNoiseCanceller.connect
を利用し stream にノイズ抑制を適用します- イベントを登録した後に
SkyWayNoiseCanceller.init
で初期化処理を実施します SkyWayNoiseCanceller
の詳しい仕様については API リファレンスを参照ください- replaceStream 時に stream が途切れて再生が止まってしまうことがあるため、
releaseOldStream: false
オプションを付与しています
// インスタンスの作成 noiseCanceller = new SkyWayNoiseCanceller(context); // onReadyイベント発火時に実行する処理を登録 noiseCanceller.onReady(async () => { // ノイズ抑制のinputとoutput const noiseCancelledAudio = await noiseCanceller.connect(audio); // ブラウザに表示(再生)しているaudioElementの差し替え myAudioPublication.replaceStream(noiseCancelledAudio, { releaseOldStream: false, }); // ノイズ抑制が有効であることを表示 noiseCancelling.textContent = 'true'; }); noiseCanceller.init();
無効時の処理は今回次のように記述します。 SkyWayNoiseCanceller.dispose
でインスタンスを破棄します。
myAudioPublication.replaceStream(audio, { releaseOldStream: false, }); // インスタンスの破棄 noiseCanceller.dispose(); // ノイズ抑制が無効であることを表示 noiseCancelling.textContent = "false";
これでボタンによる ON/OFF の切り替えが実装できました。
ノイズ抑制の強度変更
次に、ノイズ抑制の強度を変更できるように実装を変更します。 src/index.html を編集し、強度変更に必要なスライダなどを追加します。
<!DOCTYPE html> <html lang="en"> ... <!-- ノイズ抑制の処理UI --> <div id="noise-cancel-area"> <!-- ノイズ抑制の on/off 状態を表示 --> noise cancelling: <span id="noise-cancelling">false</span> <!-- ノイズ抑制の on/off を切り替える --> <button id="noise-cancel">noise cancel</button> <!-- 以下のdivを追加 --> <div id="noise-cancel-controller" style="display: none"> <label for="noise-cancel-strength-range">strength</label> <!-- ノイズ抑制の strength を変更する --> <input id="noise-cancel-strength-range" type="range" min="1" max="100" value="90" step="1" /> <span id="noise-cancel-strength-value">90</span> </div> <!-- ここまで --> </div> <div id="button-area"></div> ... </html>
強度を変更するために index.html に設定した Element を保持する変数を src/main.js に定義します。
// const noiseCancelling = document.getElementById("noise-cancelling"); の下に追加 const noiseCancelController = document.getElementById( "noise-cancel-controller" ); const noiseCancelStrengthRange = document.getElementById( "noise-cancel-strength-range" ); const noiseCancelStrengthValue = document.getElementById( "noise-cancel-strength-value" );
ノイズ抑制が有効になっている場合のみ強度変更 UI が表示されるように変更します。
// noiseCancelling.textContent = "true"; の下に以下を追加 noiseCancelController.style.display = "block";
// noiseCancelling.textContent = "false";の下に以下を追加 noiseCancelController.style.display = "none";
スライダが操作された際に SkyWayNoiseCanceller.changeStrength
でノイズ抑制の強度を変更できるようにします。
// noiseCancelButton.onclick = async () => {...}; の後に記述 noiseCancelStrengthRange.oninput = (e) => { const strength = Number(e.target.value); // ノイズ抑制の強度変更 noiseCanceller.changeStrength(strength); noiseCancelStrengthValue.textContent = strength.toString(); };
変更が完了したら、もう一度 npm run dev
でアプリを立ち上げて動作を確認してみましょう。
「noise cancel」ボタンを押すことでノイズ抑制の ON/OFF を切り替えることができます。処理が有効になるとスライダが表示され、このスライダを操作することでノイズ抑制の強度を変更できます。処理強度を変更しながら出力音声を確認するとノイズ抑制の効果がわかりやすくなります。
はじめに一度だけノイズ抑制強度を変更すれば良い場合、init の引数から設定する※ことができます。 詳しい使い方は API リファレンスをご参照ください。
※ 本ライブラリでは大きなノイズが発生した際に発話音声の品質を保てるようにデフォルトのノイズ抑制強度を 90% に設定しています。もっと強くノイズを抑制したい場合は 90 より大きな値の利用をお試しください。
退出時にノイズ抑制を停止
room の退出時にノイズ抑制の停止処理を追加します。
leaveButton.onclick = async () => { if (noiseCanceller) { noiseCanceller.dispose(); } // await me.leave(); の上に追加 ... // remoteMediaArea.replaceChildren(); の下に以下を追加 noiseCancelling.textContent = 'false'; noiseCancelStrengthRange.value = '90'; noiseCancelStrengthValue.textContent = '90'; noiseCancelController.style.display = 'none'; }
(推奨) エラーハンドリングの登録
ノイズ抑制の処理に失敗し、内部で回復不可能な状態になると onFatalError
に渡されたコールバック関数が発火します。
このコールバック関数として、ノイズ抑制適用前の stream に戻す処理を登録します。
// noiseCanceller.onReady(async () => { // ... // noiseCancelController.style.display = 'block'; // }); // onReady の後に onFatalError の処理を追加する noiseCanceller.onFatalError((event) => { const error = event.detail; if (error.type === 'ProcessError') { myAudioPublication.replaceStream(audio, { releaseOldStream: false, }); noiseCancelling.textContent = 'false'; noiseCancelController.style.display = 'none'; } });
SkyWayNoiseCanceller.connect
を利用してノイズ抑制を適用している場合、
回復不可能なエラーが発生すると音声ストリームを返さなくなり通話を継続できません。
ノイズ抑制失敗時に通話継続させるため、適用前の stream に戻してあげる必要があります。
完成したコード
src/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> <!-- ノイズ抑制の処理UI --> <div id="noise-cancel-area"> <!-- ノイズ抑制の on/off 状態を表示 --> noise cancelling: <span id="noise-cancelling">false</span> <!-- ノイズ抑制の on/off を切り替える --> <button id="noise-cancel">noise cancel</button> <div id="noise-cancel-controller" style="display: none"> <label for="noise-cancel-strength-range">strength</label> <!-- ノイズ抑制の strength を変更する --> <input id="noise-cancel-strength-range" type="range" min="1" max="100" value="90" step="1" /> <span id="noise-cancel-strength-value">90</span> </div> </div> <div id="button-area"></div> <div id="remote-media-area"></div> <script type="module" src="main.js"></script> </body> </html>
src/main.js
import { nowInSec, SkyWayAuthToken, SkyWayContext, SkyWayRoom, SkyWayStreamFactory, uuidV4 } from "@skyway-sdk/room"; import { SkyWayNoiseCanceller } from "skyway-ai-noise-canceller"; const token = new SkyWayAuthToken({ jti: uuidV4(), iat: nowInSec(), exp: nowInSec() + 60 * 60 * 24, version: 3, scope: { appId: "ここにアプリケーションIDをペーストしてください", rooms: [ { id: "*", methods: ["create", "close", "updateMetadata"], member: { id: "*", methods: ["publish", "subscribe", "updateMetadata"], }, sfu: { enabled: true, }, }, ], turn: { enabled: true, }, analytics: { enabled: true, }, noiseCancelling: { enabled: true }, }, }).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"); // ノイズ抑制用のElement const noiseCancelButton = document.getElementById("noise-cancel"); const noiseCancelling = document.getElementById("noise-cancelling"); const noiseCancelController = document.getElementById( "noise-cancel-controller" ); const noiseCancelStrengthRange = document.getElementById( "noise-cancel-strength-range" ); const noiseCancelStrengthValue = document.getElementById( "noise-cancel-strength-value" ); const { audio, video } = await SkyWayStreamFactory.createMicrophoneAudioAndCameraStream({ audio: { echoCancellation: false, noiseSuppression: false, // SkyWayNoiseCanceller のノイズ抑制と競合しないように false にする } }); 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(video); // インスタンスを保持する変数 let noiseCanceller; const myAudioPublication = await me.publish(audio); noiseCancelButton.onclick = async () => { if (noiseCancelling.textContent === "false") { // インスタンスの作成 noiseCanceller = new SkyWayNoiseCanceller(context); // init() 処理完了時(onReadyイベント発火時)に実行する処理を登録 noiseCanceller.onReady(async () => { // ノイズ抑制のinputとoutput const noiseCancelledAudio = await noiseCanceller.connect(audio); // ブラウザに表示(再生)しているaudioElementの差し替え myAudioPublication.replaceStream(noiseCancelledAudio, { releaseOldStream: false, }); // 各種変数の代入 noiseCancelling.textContent = 'true'; noiseCancelController.style.display = 'block'; }); noiseCanceller.onFatalError((event) => { const error = event.detail; // ノイズ抑制実行中に発生したエラーの場合 if (error.type === 'ProcessError') { // ノイズ抑制適用前の audio に戻す myAudioPublication.replaceStream(audio, { releaseOldStream: false, }); // 各種変数の代入 noiseCancelling.textContent = 'false'; noiseCancelController.style.display = 'none'; } }); // インスタンスの初期化処理 noiseCanceller.init(); } else { myAudioPublication.replaceStream(audio, { releaseOldStream: false, }); // インスタンスの破棄 noiseCanceller.dispose(); noiseCancelling.textContent = "false"; noiseCancelController.style.display = "none"; } }; noiseCancelStrengthRange.oninput = (e) => { const strength = Number(e.target.value); // ノイズ抑制強度の変更 noiseCanceller.changeStrength(strength); noiseCancelStrengthValue.textContent = strength.toString(); }; 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 () => { if (noiseCanceller) { noiseCanceller.dispose(); } await me.leave(); await room.dispose(); myId.textContent = ""; buttonArea.replaceChildren(); remoteMediaArea.replaceChildren(); noiseCancelling.textContent = 'false'; noiseCancelStrengthRange.value = '90'; noiseCancelStrengthValue.textContent = '90'; noiseCancelController.style.display = 'none'; }; room.onStreamUnpublished.add((e) => { document.getElementById(`subscribe-button-${e.publication.id}`)?.remove(); document.getElementById(`media-${e.publication.id}`)?.remove(); }); }; })();