クイックスタート

本チュートリアルでは、SkyWay LiveStreaming API を利用して映像・音声を配信する方法を紹介します。

なお、SkyWay LiveStreaming API は現在オープンベータ版として提供しています。本機能のご利用をご希望の方は、SkyWayお問い合わせよりご連絡ください。SkyWay LiveStreaming API の Base URL をご案内いたします。

配信サービスの準備

動作確認済みの配信サービス

SkyWay LiveStreaming API では、以下の配信サービスで動作確認を行っています。 配信の開始には、ストリームキーが必要です。以下を参考に取得してください。

YouTube

Twitch


その他の配信サービスで配信を開始するには、配信 URL とストリームキーが必要です。配信サービスのドキュメントをご参照ください。

開発時に用いることができる配信サービス

開発中に YouTube や Twitch などのサービスを使わずに自前でテストを行いたい場合は、外部からアクセス可能なサーバー上に以下のような RTMP/HLS サーバーをデプロイして利用できます。

利用方法は、それぞれのリポジトリのドキュメントをご参照ください。これらのサービスはサポートの対象外となります。

最終的な動作確認は実際に配信を行う際に利用するサービスで実施してください。


チュートリアルアプリの作成

ここからは JavaScript SDK と Node.js を用いて、映像・音声の配信機能を体験できるシンプルなアプリケーションを作成します。

最終的に、以下のような形で映像と音声をYouTubeなどに配信できます。

配信画面の例

構成

本チュートリアルで作成するアプリケーションは、管理者が利用するサーバーサイドと、ブラウザで動作するクライアントサイドで構成されます。

  • サーバーサイド SkyWay のサーバーサイド API を制御し、トークンの管理などを担います。
  • クライアントサイド ブラウザで音声・ビデオ通話を行うアプリケーションを動かし、実際に会議や映像配信を行います。

また、管理者はサーバーの API に対して curl コマンドでリクエストを送信して、以下の操作を行います。

  1. 配信の準備
  2. 配信の開始
  3. 配信設定の更新
  4. 配信の終了

環境構築

  1. Node.js のインストール バージョン20以降をインストールしてください。

  2. ディレクトリ構成 任意の作業ディレクトリに tutorial ディレクトリを作成し、 tutorial ディレクトリの中に以下の内容の package.json を配置します。

{ "name": "tutorial", "version": "1.0.0", "main": "index.js", "type": "module", "scripts": { "server": "node server/main.js", "client": "parcel client/index.html --open" }, "dependencies": { "@skyway-sdk/room": "^1.11.0", "@skyway-sdk/token": "^1.7.0", "cors": "^2.8.5", "express": "^4.21.2", "jsrsasign": "^11.1.0" }, "devDependencies": { "@types/express": "^4.17.21", "parcel": "^2.13.3" } }
  1. パッケージのインストール package.json を配置したディレクトリ内で、以下のコマンドを実行してください。
npm i

サーバーサイド

配信機能(SkyWay Live Streaming API)を HTTP 経由で操作するため、Node.js のサーバーアプリケーションを作成します。

ソースファイルの作成

  1. tutorial ディレクトリ内に server ディレクトリを作成し、server ディレクトリの中に以下の内容の main.js を配置します。

tutorial/server/main.js

import { nowInSec, SkyWayAuthToken, uuidV4 } from "@skyway-sdk/token"; import cors from "cors"; import express from "express"; import jsrsasign from "jsrsasign"; // SkyWayのアプリケーションIDとシークレットキー const appId = "ここにアプリケーションIDをペーストしてください"; const secret = "ここにシークレットキーをペーストしてください"; // LiveStreaming APIのBase URLは、オープンベータ版のご利用申請後にご案内いたします // お問い合わせ: https://support.skyway.ntt.com/hc/ja/requests/new?ticket_form_id=14615614124185 const livestreamingApiBaseUrl = "ここにLiveStreaming APIのBase URLを入力してください"; const channelApiUrl = "https://channel.skyway.ntt.com/v1/json-rpc"; // 配信サービスの設定 const output = { type: "RTMP", target: { service: "ここに「YOUTUBE」か「TWITCH」と入力するか、その他のサービスを利用する場合はRTMPのエンドポイントを入力してください", streamKey: "ここに配信サービスのストリームキーをペーストしてください", }, };

補足:

  • appIdsecret はご自身の SkyWay アプリケーションで発行されたアプリケーション ID・シークレットキーに置き換えてください。
  • service : 動作を保証している YouTube と Twitch に関してはそれぞれのラベルである「YOUTUBE」と「TWITCH」を指定することで利用できます。その他のサービスは RTMP のエンドポイント(rtmp://で始まる)を指定してください
  • streamKey : 配信先(YouTube, Twitch など)のストリームキーを指定してください。

SkyWay Admin Auth Token の作成

SkyWay の LiveStreaming API および Channel API を操作するためには、SkyWay Admin Auth Token を作成する必要があります。詳細は以下のドキュメントを参照してください。

SkyWay Admin Auth Token を作成するための createSkyWayAdminAuthToken 関数を main.js ファイルに追記します。

tutorial/server/main.js

// Channel APIとLiveStreaming 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 を作成するための createSkywayAuthToken 関数を main.js ファイルに追記します。

tutorial/server/main.js

// クライアント用のトークン const createSkywayAuthToken = (channelId) => { const token = new SkyWayAuthToken({ jti: uuidV4(), iat: nowInSec(), exp: nowInSec() + 60 * 60 * 24, version: 3, scope: { appId, rooms: [ { id: channelId, methods: [], member: { id: "*", methods: ["publish", "subscribe", "updateMetadata"], }, }, ], }, }).encode(secret); return token; };

Channelの作成

サーバー起動時に、一度だけ SkyWay の Channel を作成します。 下記のコードでは、fetch 関数を使い、Channel API の createChannel メソッドを呼び出して新しい Channel を作成しています。

SkyWay Channel API の詳細な仕様は以下の記事を参照してください。

Channel API の createChannel メソッドを呼び出すコードを main.js に追記します。

tutorial/server/main.js

// SkyWayのChannelを作成 const createChannelResponse = await fetch(channelApiUrl, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${createSkyWayAdminAuthToken()}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: uuidV4(), method: "createChannel", params: {}, }), }); if (!createChannelResponse.ok) { console.error(await createChannelResponse.json()); throw new Error("Failed to create channel"); } const { result: { channel: { id: channelId }, }, } = await createChannelResponse.json();

サーバーフレームワークの設定

チュートリアルアプリでは、HTTP サーバーを実行するためのフレームワークとして Express を利用します。 Express の設定を行うためのコードを main.js に追記します。

tutorial/server/main.js

const app = express(); app.use(cors()); app.use(express.json());

メディア合成のプリセット設定

メディア合成の設定では、配信される映像と音声がどのような形式になるか定義できます。例えば参加者全員の映像を格子状に並べることや、ある特定の属性を持つ Member や Publication を選択して画面に表示すること等ができます。

このチュートリアルでは、以下 2 種類の合成レイアウトを定義します。

  1. Grid
  • 映像: 複数の参加者の映像を格子状に分割して表示します。
  • 音声: 音量が大きい順に上位4名の音声を出力します。

Gridレイアウト

  1. Picture In Picture (pip)

プレゼンター(presenter)の属性を metadata に持つメンバーの映像をメイン画面に、カメラ映像を小窓に表示します。それ以外のメンバー(audience)の映像は表示しません。

  • 映像: プレゼンターの画面共有映像とカメラ映像を合成して表示します。
  • 音声: プレゼンターの音声を出力します。

PiPレイアウト


メディア合成の詳細な仕様は以下の記事を参照してください

合成のルール

また、メディア合成の結果の事前確認やレイアウト調整に合成結果プレビューツールを使うこともできます。使用方法については以下の記事を参照してください

合成結果プレビューツール

gridpip のレイアウトの定義を main.js に追記します。

tutorial/server/main.js

// 合成のプリセット設定 /** Gridレイアウトでは、numVideoElementsを4に固定していますが、 roomに参加しているメンバーやPublicationの数を反映させて可変にすることも可能です。 **/ const grid = () => { const canvasWidth = 1280; const canvasHeight = 720; // videoElementsの数 (Gridのセルの数)を指定する // ここを適宜変更することで動的にセル数を変化させられます const numVideoElements = 4; // 列数: 要素数の平方根の切り上げ const columns = Math.ceil(Math.sqrt(numVideoElements)); // 行数: 要素数 ÷ 列数の切り上げ const rows = Math.ceil(numVideoElements / columns); // Canvas サイズから計算する1セルあたりの幅・高さ const cellWidth = Math.round(canvasWidth / columns); const cellHeight = Math.round(canvasHeight / rows); const videoElements = [...new Array(numVideoElements).keys()].map((i) => { const row = Math.floor(i / columns); const col = i % columns; return { publisher: { id: "*", query: { sortBy: "AUDIO_LEVEL", index: i, }, }, x: col * cellWidth, y: row * cellHeight, width: cellWidth, height: cellHeight, }; }); // 音声は最大4つまで合成できるため、音量上位4つを設定 const audioElements = [...new Array(4).keys()].map((i) => ({ publisher: { id: "*", query: { sortBy: "AUDIO_LEVEL", index: i, }, }, })); const composite = { mode: "CUSTOM", videoConfig: { canvasWidth, canvasHeight, elements: videoElements, }, audioConfig: { elements: audioElements, }, }; return composite; }; /** Picture in Pictureのメイン画面と小窓には memberのmetadataに"presenter"が指定されているmemberのうち 最も音量が大きいmemberが表示される。 メイン画面にはpublicationのmetadataに"screen"が入ったpublicationが表示され、 小窓には"camera"が入ったpublicationが表示される。 **/ const pip = { mode: "CUSTOM", options: { // クライアントからmetadataによる合成対象の更新を行う際にtrueにする handleUpdateMetadata: true, }, videoConfig: { canvasWidth: 1280, canvasHeight: 720, elements: [ { publisher: { metadata: "presenter", query: { sortBy: "AUDIO_LEVEL", index: 0, }, videoPublication: { metadata: "screen", }, }, x: 0, y: 0, width: 1280, height: 720, }, { publisher: { metadata: "presenter", query: { sortBy: "AUDIO_LEVEL", index: 0, }, videoPublication: { metadata: "camera", }, }, x: 960, y: 540, width: 320, height: 180, zIndex: 1, }, ], }, audioConfig: { elements: [ { publisher: { metadata: "presenter", query: { sortBy: "AUDIO_LEVEL", index: 0, }, }, }, ], }, };

ユーザのChannelへの入室

クライアントからリクエストを受け取り、Channel への入室用トークンと ChannelID を返すエンドポイントの定義を main.js に追記します。

tutorial/server/main.js

// ユーザがChannelに入室するためのエンドポイント app.post("/user/join", async (_, res) => { // 入室できるchannelIdを制限したトークンを作成する const token = createSkywayAuthToken(channelId); res.send({ token, channelId }); });

配信の準備

管理者が配信を行う前に、「配信の準備」をする必要があります。 この処理は最大で 10 分ほどかかる場合があるため、Chunked レスポンスで進捗を返すように実装します。 PrepareLiveStreamingSession API を呼び出し、statusAVAILABLE になるまで定期的に状態を確認する処理を main.js に追記します。

tutorial/server/main.js

// 管理者が配信の準備をするエンドポイント let liveStreamingSessionId = ""; app.post("/admin/prepare", async (_, res) => { // prepare は最大で10分ほどかかる可能性があるため // 逐次ステータスを "Chunked" レスポンスで返す。 // AVAILABLE と表示されたら準備完了。 const response = await fetch( `${livestreamingApiBaseUrl}/channels/${channelId}/sessions/prepare`, { method: "POST", headers: { Authorization: `Bearer ${createSkyWayAdminAuthToken()}`, }, } ); if (!response.ok) { console.error(await response.json()); res.status(500).send(); return; } const { id } = await response.json(); liveStreamingSessionId = id; res.set({ "Content-Type": "text/plain", "Transfer-Encoding": "chunked", }); res.write(`sessionId:${liveStreamingSessionId}\n`); for (;;) { // LiveStreamingセッションのStatusを取得 const response = await fetch( `${livestreamingApiBaseUrl}/channels/${channelId}/sessions/${liveStreamingSessionId}`, { headers: { Authorization: `Bearer ${createSkyWayAdminAuthToken()}`, }, method: "GET", } ); const { status } = await response.json(); res.write(`status:${status}\n`); if (status === "AVAILABLE") { break; } await new Promise((resolve) => setTimeout(resolve, 1000)); } res.end(); });

配信の開始

配信の準備が完了したら、管理者は配信を開始できます。

配信を開始するために、StartLiveStreamingSession API を呼び出す処理を main.js に追記します。 ここでは、デフォルトの合成設定を grid にし、リクエストの preset のパラメーターが "pip" の場合は pip レイアウトに切り替えています。

tutorial/server/main.js

// 管理者が配信を開始するエンドポイント app.post("/admin/start", async (req, res) => { const { preset } = req.body; let composite = grid(); if (preset === "pip") { composite = pip; } const response = await fetch( `${livestreamingApiBaseUrl}/channels/${channelId}/sessions/${liveStreamingSessionId}/start`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${createSkyWayAdminAuthToken()}`, }, body: JSON.stringify({ composite, output, }), } ); if (!response.ok) { console.error(await response.json()); res.status(500).send(); return; } res.send(); });

配信の設定更新

配信中に、gridpip の切り替えなど、メディア合成の設定を更新できます。 メディア合成の設定を更新するために、UpdateLiveStreamingSession API を呼び出す処理を main.js に追記します。

// 管理者が配信のメディア合成設定を変更するエンドポイント app.post("/admin/update", async (req, res) => { const { preset } = req.body; let composite = grid(); if (preset === "pip") { composite = pip; } // canvasWidthとcanvasHeightは更新できないパラメーターであるため、updateのパラメーターから削除する delete composite.videoConfig.canvasWidth; delete composite.videoConfig.canvasHeight; const response = await fetch( `${livestreamingApiBaseUrl}/channels/${channelId}/sessions/${liveStreamingSessionId}`, { method: "PATCH", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${createSkyWayAdminAuthToken()}`, }, body: JSON.stringify({ composite, output, }), } ); if (!response.ok) { console.error(await response.json()); res.status(500).send(); return; } res.send(); });

配信の終了

管理者が配信を終了するためのエンドポイントです。 配信を終了するために、DeleteLiveStreamingSession API を呼び出す処理を main.js に追記します。

tutorial/server/main.js

// 配信を終了するエンドポイント app.post("/admin/delete", async (_, res) => { const response = await fetch( `${livestreamingApiBaseUrl}/channels/${channelId}/sessions/${liveStreamingSessionId}`, { method: "DELETE", headers: { Authorization: `Bearer ${createSkyWayAdminAuthToken()}`, }, } ); if (!response.ok) { console.error(await response.json()); res.status(500).send(); return; } res.send(); });

サーバーの起動

最後に、ポート 9091 で HTTP サーバーを起動するためのコードを main.js に追記します。

app.listen(9091); console.log("Server is running on http://localhost:9091");

クライアントサイド

tutorial ディレクトリ内に client ディレクトリを作成し、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> <button id="join">join</button> <button id="publish-camera">publishCamera</button> <button id="publish-screen">publishScreen</button> </div> <div> <button id="become-presenter">Become Presenter</button> <button id="become-audience">Become Audience</button> <span id="role-label">role: audience</span> </div> <div id="local-media-area"></div> <div id="remote-media-area"></div> <script type="module" src="main.js"></script> </body> </html>

次に、client ディレクトリの中に以下の内容の main.js を配置します。

tutorial/client/main.js

import { SkyWayContext, SkyWayRoom, SkyWayStreamFactory, } from "@skyway-sdk/room"; (async () => { const localMediaArea = document.getElementById("local-media-area"); const remoteMediaArea = document.getElementById("remote-media-area"); const myId = document.getElementById("my-id"); const joinButton = document.getElementById("join"); const publishCamera = document.getElementById("publish-camera"); const publishScreen = document.getElementById("publish-screen"); const becomePresenter = document.getElementById("become-presenter"); const becomeAudience = document.getElementById("become-audience"); const roleLabel = document.getElementById("role-label"); joinButton.onclick = async () => { // サーバーからChannel IDとトークンを取得 const response = await fetch(`http://localhost:9091/user/join`, { method: "POST", }); const { token, channelId } = await response.json(); // SkyWayContextを作成し、既存のChannelに参加 const context = await SkyWayContext.Create(token); const room = await SkyWayRoom.Find(context, { id: channelId }, "sfu"); const me = await room.join(); myId.textContent = me.id; // Presenter / Audience の切り替え becomePresenter.onclick = async () => { roleLabel.textContent = "role: presenter"; // LiveStreaming APIでは次のような構造のメタデータのlabelを参照することができます await me.updateMetadata( JSON.stringify({ SKYWAY_SYSTEM: { liveStreaming: { label: "presenter", }, }, }) ); }; becomeAudience.onclick = async () => { roleLabel.textContent = "role: audience"; await me.updateMetadata( JSON.stringify({ SKYWAY_SYSTEM: { liveStreaming: { label: "audience", }, }, }) ); }; // ストリームのPublishを行う関数 const publish = async (stream, label) => { const publication = await me.publish( stream, label != undefined ? { metadata: JSON.stringify({ SKYWAY_SYSTEM: { liveStreaming: { label, }, }, }), } : undefined ); // DOMに表示 const newMedia = document.createElement("div"); const labelElement = document.createElement("span"); newMedia.appendChild(labelElement); labelElement.textContent = ` id:${publication.id} contentType:${publication.contentType} label:${label} `; const unpublishButton = document.createElement("button"); newMedia.appendChild(unpublishButton); unpublishButton.textContent = "Unpublish"; unpublishButton.onclick = async () => { await me.unpublish(publication); newMedia.remove(); }; localMediaArea.appendChild(newMedia); }; // 音声ストリームをPublish const audio = await SkyWayStreamFactory.createMicrophoneAudioStream(); await publish(audio); // カメラ映像をPublish publishCamera.onclick = async () => { const video = await SkyWayStreamFactory.createCameraVideoStream(); await publish(video, "camera"); }; // 画面共有をPublish publishScreen.onclick = async () => { const { video } = await SkyWayStreamFactory.createDisplayStreams(); await publish(video, "screen"); }; // 他メンバーがPublishしたストリームをSubscribe const subscribeAndAttach = async (publication) => { // 自分がPublishしたストリームは除外 if (publication.publisher.id === me.id) return; 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.onStreamPublished.add((e) => subscribeAndAttach(e.publication)); await Promise.all(room.publications.map(subscribeAndAttach)); }; })().catch((e) => console.error("main error", e));

チュートリアルアプリの実行

1. アプリケーションの起動

  1. サーバーの起動 以下のコマンドでサーバーを起動します。http://localhost:9091 でリクエストを待ち受けます。

    npm run server
  2. クライアントの起動 別のターミナルを開いて以下のコマンドを実行すると、ブラウザが自動的に起動します(またはターミナルに表示される URL を直接開きます)。

    npm run client

2. クライアントサイドの操作

    1. join ボタン サーバーから発行された Token と ChannelID を使って SkyWay の Channel に入室します。
    1. publishCamera / publishScreen ボタン それぞれ、カメラ映像・画面共有映像を配信(Publish)します。
    1. Become Presenter / Become Audience ボタン 現在入室中のメンバーの metadata を切り替え、配信時のメディア合成設定に反映させます(presenteraudience の判定)。

管理者による配信のコントロール

管理者は次のエンドポイントを curl コマンドなどで呼び出すことで、配信を制御します。

1. 配信の準備

curl -X POST http://localhost:9091/admin/prepare
  • コンソール出力でステータスが逐次表示され、AVAILABLE となったら配信可能です。

2. 配信の開始

curl -X POST http://localhost:9091/admin/start
  • 配信開始時のレイアウトはデフォルトで grid が指定されます。
  • レイアウトを指定する場合、以下のように preset を JSON で渡します。
curl -X POST -H "Content-Type: application/json" \ -d '{"preset":"pip"}' \ http://localhost:9091/admin/start

pip レイアウト利用時の注意

  • 「presenter」役のメンバーがいない場合や映像の Publish が行われていない場合、何も表示されません。デモ時には必ず「Become Presenter」ボタンを押し、映像を Publish しているか確認してください。

3. 配信設定の更新

以下のように、presetpip または grid を指定してレイアウトを切り替えます。

curl -X POST -H "Content-Type: application/json" \ -d '{"preset":"pip"}' \ http://localhost:9091/admin/update

4. 配信の終了

curl -X POST http://localhost:9091/admin/delete
  • 配信を終了すると、LiveStreamingSession は削除されます。
  • 再度配信を開始する場合は prepare → start のフローを改めて行ってください。

トラブルシューティング

このクイックスタートでエラーが発生した場合は、エラーレスポンスを元に開発ガイドを参照してください。