
【新機能】SkyWayの録音・録画機能がP2P Roomの通話を対象に利用可能になりました
まえがき
みなさまこんにちは。SkyWayチームのryuryuと申します。
SkyWayはこの度、録音・録画機能において機能の追加を行いました。本記事ではその利用方法の説明を行います。
概要
SkyWayでは、録音・録画機能を提供しています。
こちらの機能はRoom内に存在するメディアデータをSFUサーバーを介して録音・録画するものとなっています。

これまでSFUを利用したRoomでのみ利用できる機能でしたが、今回P2P Roomでの通話も録音・録画できる機能が追加されました。今回P2Pを利用した録音録画の利用を解説します。
なお、SkyWayおよび録音・録画機能の基本的な利用方法は以下をご参照ください。
https://skyway.ntt.com/ja/docs/
https://skyway.ntt.com/ja/docs/user-guide/recording/recording-overview/
使い方
ユーザー間の通信を行う P2P Room とは別に、録音・録画機能を利用するための SFU Room を同時に利用することで、ユーザー間の通信は P2P Room を利用しながら録音・録画機能を利用できます。なお、この際に専用の設定を行うことで、SFU通信料とSFUリソース確保料は生じません。

今回作るサンプルアプリ

今回は録音・録画機能の記事の クイックスタート の構成を追従しつつ、P2P RoomとSFU Roomを併用する形で実装を行います。クライアントアプリとサーバーの2つから構成します。
まず、通話を行うクライアントはサーバーにRoomの作成をリクエストします。リクエストを受けたサーバーは通話を行うP2P用のRoomと、録音・録画処理を行うSFU用のRoomを作成します。それらのRoomを利用するためのトークンをクライアントに返します。

トークンを受け取ったクライアントは、P2P RoomとSFU Roomのそれぞれに参加します。通話自体はP2P Room内で行いますが、SFU Roomの中でも録音・録画用に同じメディアデータをpublishしておきます。
クライアントがサーバーに録音・録画処理をリクエストすると、SFU Room内のPublicationを録音・録画するようにSkyWayにリクエストを送ります。ここからSFU Room内の情報を元に録音・録画処理を開始されます。

こうしてP2P Roomでの通話を利用しながら、同時に録音・録画処理を行うことができます。
今回はこちらのアプリをJavaScript SDK を利用して作成します。
アカウントの準備
SkyWayコンソール( https://console.skyway.ntt.com/ )からアプリケーションを作成し、アプリケーションIDとシークレットキーを控えます。

なお、開発・検証目的であればFreeプラン(利用上限あり)のアカウントで利用することができます。
詳しくは 料金 のページをご参照ください。
ストレージの設定
任意のクラウドストレージに対してストレージの設定を行います。詳細は以下を参照してください。
環境構築
Node.js のバージョン 20 以降をインストールし、任意の作業ディレクトリに tutorial ディレクトリを作成します。tutorial ディレクトリ直下に以下の内容の package.json ファイルを作成します。
{
"name": "tutorial",
"version": "1.0.0",
"type": "module",
"scripts": {
"client": "parcel client/index.html",
"server": "node server/main.js"
},
"browserslist": ["last 3 chrome versions"],
"dependencies": {
"@skyway-sdk/room": "^1.7.0",
"cors": "^2.8.5",
"express": "^4.18.2",
"jsrsasign": "^11.1.0"
},
"devDependencies": {
"parcel": "^2.11.0"
}
}
サーバーサイド
録音・録画機能はサーバーサイドから SkyWay Recording API を通じて操作するので、そのためのサーバーアプリケーションを作成する必要があります。
ソースファイルの作成
tutorial/server/main.js ファイルを作成し、以下のコードを記述します。
// tutorial/server/main.js
import cors from 'cors';
import express from 'express';
import jsrsasign from 'jsrsasign';
import { SkyWayAuthToken, nowInSec, uuidV4 } from '@skyway-sdk/token';
import crypto from 'crypto';
const appId = 'ここにアプリケーションIDをペーストしてください';
const secret = 'ここにシークレットキーをペーストしてください';
const gcsConfig = {
service: 'GOOGLE_CLOUD_STORAGE',
credential: JSON.stringify({
// サービスアカウントの鍵のJSONファイルの内容をコピーペーストする
type: '',
project_id: '',
private_key_id: '',
private_key: '',
client_email: '',
client_id: '',
auth_uri: '',
token_uri: '',
auth_provider_x509_cert_url: '',
client_x509_cert_url: '',
}),
bucket: '',
};
const s3Config = {
service: 'AMAZON_S3',
bucket: '',
accessKeyId: '',
secretAccessKey: '',
region: '',
};
const wasabiConfig = {
service: 'WASABI',
bucket: '',
accessKeyId: '',
secretAccessKey: '',
endpoint: '',
};
const recordingApiBaseUrl = 'https://recording.skyway.ntt.com/v1';
const channelApiUrl = 'https://channel.skyway.ntt.com/v1/json-rpc';
appId,secret には自身のアプリケーションの値を入力してください。
また、利用するクラウドストレージに合わせて gcsConfig、s3Config、wasabiConfig のいずれかに必要な情報を入力してください。 wasabiConfig の endpoint は、 https://s3.<リージョン名>.wasabisys.com のように設定してください。 (例: https://s3.ap-northeast-1.wasabisys.com )
SkyWay Admin Auth Token の作成
SkyWay の Recording API および Channel API を操作するために SkyWay Admin Auth Token を作成する必要があります。
SkyWay Admin Auth Token の詳細な仕様は次の記事を参照してください。
SkyWay Admin Auth Token
// tutorial/server/main.js
// Recording API と Channel API を操作するためのトークン
const createSkyWayAdminAuthToken = () => {
const token = jsrsasign.KJUR.jws.JWS.sign(
'HS256',
JSON.stringify({ alg: 'HS256', typ: 'JWT' }),
JSON.stringify({
exp: nowInSec() + 60,
iat: nowInSec(),
jti: uuidV4(),
appId,
}),
secret
);
return token;
};
SkyWay Auth Token の作成
SkyWay のクライアントサイド SDK で利用する SkyWay Auth Token を作成します。
SkyWay Auth Token の詳細な仕様は次の記事を参照してください。
SkyWay Auth Token
以下では、通話を行うRoomの名称と録音録画を行うRoomの名称を分けて設定しています。
録音録画を行うRoomでは、maxSubscribers の設定を 0 と設定することによって録音・録画に伴うSFU 通信料とSFUリソース確保料が発生しなくなっています。なお、こちらの設定はSkyWay Auth Token のバージョン 3 から対応しております。
// tutorial/server/main.js
// クライアント用のトークン
const createSkywayAuthToken = (roomName, recordingSFURoom) => {
const token = new SkyWayAuthToken({
jti: uuidV4(),
iat: nowInSec(),
exp: nowInSec() + 60 * 60 * 24,
version: 3,
scope: {
appId: appId,
rooms: [
{
id: "*",
name: roomName,
methods: ["create", "close", "updateMetadata"],
sfu: {
enabled: false,
},
member: {
id: "*",
name: "*",
methods: ["publish", "subscribe", "updateMetadata"],
},
},
{
id: "*",
name: recordingSFURoom,
methods: ["create", "close", "updateMetadata"],
sfu: {
enabled: true,
maxSubscribersLimit: 0,
},
member: {
id: "*",
name: "*",
methods: ["publish", "subscribe", "updateMetadata"],
},
},
],
},
}).encode(secret);
return token;
};
サーバーフレームワークの設定
このチュートリアルアプリでは、サーバーフレームワークとして express を利用します。こちらの設定を行います。
// tutorial/server/main.js
const app = express();
app.use(cors());
app.use(express.json());
Roomの入室管理
クライアントから Room 名を受け取って、その Room 名の Room を SkyWay Channel API で作成しています。
この際に、合わせて録音・録画用のRoomをもう1つ作成し、このidをroomNameと紐づけて保存します。
// tutorial/server/main.js
const tokenHashRoomIdMap = {};
const roomNameIdMap = {};
const sha256 = (s) => crypto.createHash("sha256").update(s).digest("hex");
app.post("/rooms/:roomName/join", async (req, res) => {
const { roomName: p2pRoomName } = req.params;
console.log("join", { p2pRoomName });
// 入力のroomNameのroomを作成する
await fetch(channelApiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${createSkyWayAdminAuthToken()}`,
},
body: JSON.stringify({
jsonrpc: "2.0",
id: uuidV4(),
method: "findOrCreateChannel",
params: {
name: p2pRoomName,
},
}),
});
// 入力の録音用のRoomを作成する
const sfuRoomName = p2pRoomName + "-recording";
const sfuRoomResponse = await fetch(channelApiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${createSkyWayAdminAuthToken()}`,
},
body: JSON.stringify({
jsonrpc: "2.0",
id: uuidV4(),
method: "findOrCreateChannel",
params: {
name: sfuRoomName,
},
}),
});
const {
result: {
channel: { id: sfuRoomId },
},
} = await sfuRoomResponse.json();
roomNameIdMap[p2pRoomName] = sfuRoomId;
// 入室できるroomNameを制限したトークンを作成する
const token = createSkywayAuthToken(p2pRoomName, sfuRoomName);
// 今後の録音・録画の開始、終了操作を認証するためにtokenとroomIdの紐付けを行う
tokenHashRoomIdMap[sha256(token)] = sfuRoomId;
res.send({ token });
});
録音・録画 の開始
クライアントから受け取ったroomNameから録音・録画用に作成したRoomのidを取得します。その Room の全 Publication を録音・録画するように CreateRecordingSession API を呼び出します。
// tutorial/server/main.js
const roomNameRecordingMap = {};
app.post("/rooms/:roomName/start", async (req, res) => {
console.log("start", req.params);
const { roomName } = req.params;
const { authorization } = req.headers;
const recordingRoomId = roomNameIdMap[roomName];
console.log("start", { roomName, recordingRoomId });
if (roomNameRecordingMap[roomName]) {
res.status(200).send({ message: "already recording" });
return;
}
// roomIdとtokenの紐付けを確認する
if (recordingRoomId !== tokenHashRoomIdMap[sha256(authorization ?? "")]) {
res.status(403).send({ message: "Forbidden" });
return;
}
// Recordingを開始する
const response = await fetch(
`${recordingApiBaseUrl}/channels/${recordingRoomId}/sessions`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${createSkyWayAdminAuthToken()}`,
},
body: JSON.stringify({
input: {
kind: "SFU",
publications: [{ id: "*" }], // すべてのPublicationを保存する
},
output: gcsConfig, // Amazon S3を使う場合は s3Config を、Wasabiを使う場合は wasabiConfig を指定
}),
}
);
const { error, id } = await response.json();
if (error) {
res.status(500).send({ message: error.message });
return;
}
roomNameRecordingMap[roomName] = id;
res.status(201).send({ id });
});
録音・録画の終了
RecordingSession を削除するとすべての録音・録画処理が終了し、録音・録画ファイルが指定したクラウドストレージに保存されます。
// tutorial/server/main.js
app.delete("/rooms/:roomName/stop", async (req, res) => {
const { roomName } = req.params;
const { authorization } = req.headers;
const recordingRoomId = roomNameIdMap[roomName];
const sessionId = roomNameRecordingMap[roomName];
console.log("stop", { roomName, recordingRoomId, sessionId });
// roomIdとtokenの紐付けを確認する
if (recordingRoomId !== tokenHashRoomIdMap[sha256(authorization ?? "")]) {
res.status(403).send({ message: "Forbidden" });
return;
}
// 録音・録画を終了する
const response = await fetch(
`${recordingApiBaseUrl}/channels/${recordingRoomId}/sessions/${sessionId}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${createSkyWayAdminAuthToken()}`,
},
}
);
// 録音・録画したファイルの一覧を取得する
const { error, files } = await response.json();
if (error) {
res.status(500).send({ message: error.message });
return;
}
const filePaths = files.map((file) => file.path);
// Recordingしたファイルのパスを出力する。クラウドストレージのこのパスにアップロードされている
console.log("filePaths", filePaths);
delete roomNameRecordingMap[recordingRoomId];
res.status(200).send({ filePaths });
});
サーバーの起動
ポート 9090 でサーバーを起動します。
tutorial/server/main.js
// tutorial/server/main.js
app.listen(9090);
console.log('Server is running on http://localhost:9090');
クライアントサイド
以下を参考に tutorial/client/index.html ファイルを作成してください。
// tutorial/client/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="startRecording">start recording</button>
<button id="stopRecording">stop recording</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>
次にスクリプトファイルを用意します。 以下を参照して、tutorial/client/main.js ファイルを作成してください。
// tutorial/client/main.js
import {
SkyWayContext,
SkyWayRoom,
SkyWayStreamFactory,
} from "@skyway-sdk/room";
(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 startRecordingButton = document.getElementById("startRecording");
const stopRecordingButton = document.getElementById("stopRecording");
const { audio, video } =
await SkyWayStreamFactory.createMicrophoneAudioAndCameraStream();
video.attach(localVideo);
await localVideo.play();
入室時の処理を作成します。この際に通話用のRoomと別に録音・録画用のRoomを作成し、そこで通話用と同様にPublishを行います。
// tutorial/client/main.js
joinButton.onclick = async () => {
const roomName = roomNameInput.value;
if (roomName === "") return;
const response = await fetch(
`http://localhost:9090/rooms/${roomName}/join`,
{
method: "POST",
}
);
const { token } = await response.json();
// ユーザー間の通信にはP2P Roomを利用します
const context = await SkyWayContext.Create(token);
const room = await SkyWayRoom.Find(context, { name: roomName }, "p2p");
const me = await room.join();
myId.textContent = me.id;
await me.publish(audio);
await me.publish(video);
// 録音録画機能を利用するために、SFU Roomを利用します
const sfuRoomName = roomName + "-recording";
const recordingSFURoom = await SkyWayRoom.Find(context, {
type: "sfu",
name: sfuRoomName,
});
const recordingAgent = await recordingSFURoom.join();
// 一度PublishしたStreamは再度Publishすることができないので、改めて録音・録画用のStreamを取得します
const { audio: recordingAudio, video: recordingVideo } =
await SkyWayStreamFactory.createMicrophoneAudioAndCameraStream();
await recordingAgent.publish(recordingAudio, {
maxSubscribers: 0,
});
await recordingAgent.publish(recordingVideo, {
maxSubscribers: 0,
});
通話用のRoomでのsubscribe処理を記述します。なお、録音・録画用のRoomに関してはmaxSubscribers オプションを 0 に設定しているためsubscribe処理を行うことができません。このままの状態で録音・録画専用の Publication として扱われています。
// tutorial/client/main.js
const subscribeAndAttach = (publication) => {
if (publication.publisher.id === me.id) return;
const subscribeButton = document.createElement("button");
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;
}
stream.attach(newMedia);
remoteMediaArea.appendChild(newMedia);
};
};
room.publications.forEach(subscribeAndAttach);
room.onStreamPublished.add((e) => subscribeAndAttach(e.publication));
サーバー側に録音・録画の開始/停止をリクエストする処理を作成して完成です。
// tutorial/client/main.js
startRecordingButton.onclick = async () => {
await fetch(`http://localhost:9090/rooms/${room.name}/start`, {
method: "POST",
headers: {
Authorization: context.authTokenString,
},
});
};
stopRecordingButton.onclick = async () => {
await fetch(`http://localhost:9090/rooms/${room.name}/stop`, {
method: "DELETE",
headers: {
Authorization: context.authTokenString,
},
});
};
};
})().catch((e) => console.error("main error", e));
チュートリアルアプリの実行
アプリケーションの起動
ターミナルで npm run server を実行してサーバーを起動します。
別のターミナルで npm run client を実行してクライアントを起動します。なお、この際にターミナルにローカルアドレスが表示されるので、そのアドレスをブラウザで開いてください。
Room に join したあと、startRecording ボタンを押すと録音・録画が始まります。stopRecording ボタンを押すと録音・録画が終了します。
保存されたファイルの確認
stopRecording ボタンを押すと、サーバー側の標準出力にクラウドストレージにアップロードされたファイルのパスが出力されます。 クラウドストレージのコンソールにアクセスし、ファイルがアップロードされていることを確認してください。
保存されるファイルの詳細な仕様については概要の録音・録画ファイルの項目を参照してください。
終わりに
P2P Roomを利用した通話を行いながら、同時に録音・録画処理を行う方法を紹介しました。
ぜひ便利になったSkyWayを利用してみてください。
SkyWayのサービスサイトはこちら https://go.skyway.ntt.com/ja
SkyWay導入に関するご質問はこちらの問い合わせからお願いします。 https://go.skyway.ntt.com/note_request