Twitch 拡張機能コードのリファレンス

サンプルの Twitch 拡張機能では、例を簡略化するために JavaScript のみでクライアントコードをプログラミングしています。このセクションでは、Twitch 拡張機能のコードとそれに接続するために使用されるコードについて説明します。

サーバーコード

パブリック URL を内部 Genvid サービスに接続するサーバーは、Twitch のドキュメントで Extension Backend Service (EBS) と呼ばれているものです。

Node.js を使って、 Web サンプル にサーバーコードを記述しました。クロスオリジンリソース共有 (CORS) ライブラリ <CORS library>`_ を利用しています。

// Only to use in a development environment, not in a production environment. This removes the security purpose of CORS
app.use(cors({ credentials: true, origin: true }));

Web サンプルには、サーバーが 3 つのメソッドを転送します。

/api/streams => GET /disco/stream/info
このメソッドは、現在のストリームに関する情報 (ストリーム名、説明、チャンネルなど) を取得します。
/api/channels/join => POST /disco/stream/join
このメソッドは、Genvid EBS への接続に使用する URI とトークンを返します。
/api/commands/game => POST /commands/game

この Web メソッドはゲームにコマンドを送信します。

このサンプルでは、Twitch ダッシュボードのライブ構成パネルからメソッドにアクセスできます。Genvid EBS は、送信されたユーザー/視聴者の JSON Web Token (JWT) を検証することにより、API 呼び出しを保護します。

JWT は、Twitch 拡張機能の clientId を使用してデコードされます。検証が完了すると、視聴者/ユーザーは、拡張機能がインストールされた配信者になります。

web/backend/auth.js のファイルには、検証メソッドが含まれています。

/**
 * Validate Twitch Authentication
 * @param req 
 * @param res 
 * @param next 
 */
module.exports.twitchAuth = function (req, res, next) {
    let token = req.headers['x-access-token'] || req.headers['authorization'];
    if (token.search('Bearer ') === 0) {
        // Remove Bearer from string
        token = token.slice(7, token.length);
        console.log('token', token);
    }

    if (token) {
        jwt.verify(token, Buffer.from(config.webEndpointConfig.secret, 'base64'), (err, decoded) => {
            if (err) {
                return res.status(401).send();
            } else {
                if (decoded.role === 'broadcaster') {
                    return next();
                } else {
                    return res.status(401).send();
                }
            }
        });
    } else {
        return res.status(401).send();
    }
}

Twitch 拡張機能コード

Twitch 拡張機能は、カラーキューブのライブビデオを表示するシンプルな Web アプリケーションです。マルチストリームにも対応しており、ピクチャ・イン・ピクチャ合成を試すこともできます (詳細は Genvid Studio サービス セクションを参照)。キューブの周りには円を表示するビデオオーバーレイが存在します。円の位置は、ビデオ内のキューブの位置と一致します。

拡張機能コードも JavaScript のみで記述します。

ビデオオーバーレイ

ビデオオーバーレイ (video_overlay.html) 拡張機能は、透明なオーバーレイとしてビデオプレーヤーの上にレンダリングされます。チャンネルがライブのときのみ表示可能です。

Twitch Extension Overlay

オーバーレイには、ユーザーがストリームと対話できるようにするコントロールが含まれています。 DirectX サンプル の場合、これらのコントロールには次のものが含まれます。

ツールバーボタン:

  • Help = ヘルプメニューの表示、または非表示。
  • Video info = ビデオ情報パネルの表示、または非表示。
  • Cube name = キューブのコントロールパネルの表示、または非表示。

ビデオ:

  • キューブをクリック = 円を明るくする

キューブパネル:

  • Cheer = プレイヤーの数を変更 (ハートアイコン)。
  • Reset = キューブの位置をリセット。
  • Color = キューブの色を変更。

Genvid オーバーレイは、Twitch プレイヤーとの直接の干渉を防ぎます。また、 Video Info ボタンをクリックしたときに、各キューブの横に、色付きの円と名前、デバッグ情報を表示します。

  • Local = Web ページを表示するマシンのローカル時間。
  • Est. Video = ビデオプレイヤーの予測時間。
  • 受信ストリーム = Compose service からレポートされた直前のデータの時間。
  • 再生ストリーム = 一般化されたストリームの現在の時間。
  • Latency = ゲームと視聴者間の予測遅延時間。
  • DelayOffset = 分単位の同期調整を行うためのオプショナル遅延オフセット

viewer.js

viewer.js のフロントエンドコードは、いくつかの必要な機能を実行します。

  • 承認された Twitch ユーザー (twitch.ext.onAuthorized) であることを検証します。
  • Twitch Configuration Service から Genvid EBS の URL エンドポイントを取得します。
  • /api/public/channels/join 参加要求を行い、ストリーム情報と認証トークンを取得します。
  • ストリーム情報で GenvidClient をインスタンス化します。
/**
 * 1- Validate that viewer is authorized to use a twitch extension
 * 2- Retrieve the endpoint of the cluster from Twitch Configuration Service
 * 3- Initiate Genvid Services...
 */
window.Twitch.ext.onAuthorized((auth) => {
    // save our credentials
    token = auth.token;
    tuid = auth.userId;

    const endpoint = getEndpoint();
    if(endpoint){
        getStreamInfo(endpoint);
    }
});

Web API の IGenvidClient() が Genvid サービスと通信を行い、コールバックを提供します。

/**
 * Callback called regurlary to draw the overlay,
 * in sync with the video frame displayed.
 * @param {*} frameSource
 */
const onDraw = frameSource => {

    // update the overlays to adapt to the composition of the video stream:
    updateOverlays(frameSource.compositionData);

    // Get the background session ID
    let backgroundSessionID = Object.keys(frameSource.sessions)[0];
    if (frameSource.compositionData[0]) {
        backgroundSessionID = frameSource.compositionData[0].sessionId;
    }

    // Assign backgroundSession by default.
    let targetSession = frameSource.sessions[backgroundSessionID];
    if (targetSession && Object.keys(targetSession.streams).length > 0) {
        videoOverlay.style.display = "block";
        // Extract positions, colors, and camera data.
        updateStreamsInfoFromSession(targetSession);
    } else {
        videoOverlay.style.display = "none";
    }
    // Set circles colors to newly received ones.
    updateCubeColors();
    // Update UI with steam info from GenvidClient.
    updateInfo();

    if (isFullScreen !== checkFullScreen()) {
        isFullScreen = checkFullScreen();
    }
}

このサンプルでは、onDisconnect() コールバック (IGenvidClient() インターフェイスにより公開) を使って、クライアントのソケットが閉じるタイミングを通知します。

まずは、指定のコールバックをバインドします。

genvidClient.onDisconnect(() => {onDisconnectDetected();});

クライアントソケットの閉鎖通知が届いたら、スクリプトに再接続試行の指示を行います。

/**
 * Method triggered when a disconnected socket occured  We then initiate
 * a client reconnect
 */
const onDisconnectDetected = () => {
    logger.log('got disconnected, reconnect...');
    const endpoint = getEndpoint();
    if(endpoint){
        getStreamInfo(endpoint).then((streamInfo) => {
            genvidClient.reconnect(
                streamInfo.info,
                streamInfo.uri,
                streamInfo.token,
            )
        });
    }
}

次に、新しい leaf アドレス、新しいトークン、新しい streamInfo の情報リクエストを行います。

リクエストが成功した場合、reconnect() を呼び出して、この情報を使用して新しい websocket 接続を確立します。

リクエストが失敗した場合 (利用可能な leaf がないなど)、しばらく待って再試行します。増分フィボナッチ数の計算結果を使用して、待機時間を決定します。

/**
 * GENVID - Start sleep
 * @param {*} ms 
 */
const sleep = (ms) => {
    return new Promise(resolve => setTimeout(resolve, ms));
}

フィボナッチ数列の結果に 10% のランダム要素を追加します。これにより、2 つのサービスが同時に接続を開始する状況を回避します。接続が成功した際にフィボナッチ数をリセットします。

オーバーレイメソッド

Overlay Methods セクションでは、html 要素のリファレンスや初期化を行います。また、シーンの更新を開始するために initThreeJS を呼び出す場所でもあります。

initThreeJS

initThreeJS は Three.js を使用して、ゲームに必要なシーン、キャンバス、俯瞰カメラ、レンダラーへのリファレンスを作成します。その後、render を呼び出してレンダリングシーケンスを起動します。

render

render 関数は、新たに受信したゲームデータでシーンを更新するために使用します。キューブ名を受け取ると、対応するキューブタグとサークルスプライトを作成します。この部分は一度だけ呼び出します。次に、」Camera」 と 「Positions」 のデータストリームから新たに受信したデータで、カメラオブジェクトとキューブの位置を更新します。 Three.js は、視野、アスペクト比、キューブの位置などの情報を使用して、そのデータを 2D 空間に表示してビデオをオーバーレイします。位置データはプレイヤーパネルの更新にも使用します。最後に render 関数で requestAnimationFrame を引数として呼び出してシーンを更新します。

ユーザーインタラクション

ユーザーインタラクション セクションでは、ユーザーがオーバーレイを操作する方法を定義し、キューブの色の変更、キューブの応援、位置のリセットなどのイベントを生成します。

たとえば、キューブの色を変更するために、キーと値を持つイベントオブジェクトを構築します。キーにはアクションとキューブ ID が含まれ、値には色が含まれます。そして、オブジェクトを genvidClient.sendEvent を渡してイベントに送信します。

ダッシュボード (Live Config) パネル

Live Configuration Panel (live_config.html) は Command Channel へのアクセス方法です。インストールされた拡張機能のダッシュボードページのパネル領域に表示されます。

Twitch アカウントから:

  1. Creator Dashboard を選択します。

  2. Extensions セクションでパネルをアクティブにします。

Twitch Extension Live Config Panel

パネルには、各キューブのセクションが表示されており、以下の機能が含まれています。

  • Direction = 矢印をクリックしてキューブの向きを変更する。
  • Reset = キューブの位置をリセットする。
  • Slower = キューブのスピードを落とす。
  • Faster = キューブのスピードを上げる。

live_config.js

live_config.js ファイルは Command Channel を使ってコマンドをゲームに送信します。これは、配信者がキューブの速度と方向を変更する方法です。

コマンドはゲームを 直接 制御するため、配信者だけがコマンド API にアクセスできます。すべてのリクエストで、ユーザーの Bearer Token (JWT) を EBS に送信して、ユーザーが、有効な Twitch ユーザーかつ配信者であることを検証します。

例として次のコードは、キューブの向きを変更するイベントを示します。

/**
 * Change the direction of the cube
 * @param {*} cubeName 
 * @param {*} x 
 * @param {*} z 
 */
const setDirection = (cubeName, x, z) => {

    let command = {
        id: "direction",
        value: `${cubeName}:${x}:0:${z}`
    };
    twitch.rig.log("command", command);
    const fetchOptions = {
        method: "POST",
        headers: headers, // Contains the Bearer token
        body: JSON.stringify(command)
    };
    fetch(commandApiEndpoint, fetchOptions)
        .then(data => {
            console.log('data', data);
            message = `Set direction to ${cubeName}:${x}:${z}`;
            displayMessage();
        })
        .catch(err => {
            message = `Failed with error ${err} to do Set direction to ${cubeName}:${x}:${z}`;
            console.log('err', err);
            displayErrorMessage();
        });
}

web/backend/auth.js ファイルは、 Node.js サーバー (EBS) の検証を行います。

/**
 * Validate Twitch Authentication
 * @param req 
 * @param res 
 * @param next 
 */
module.exports.twitchAuth = function (req, res, next) {
    let token = req.headers['x-access-token'] || req.headers['authorization'];
    if (token.search('Bearer ') === 0) {
        // Remove Bearer from string
        token = token.slice(7, token.length);
        console.log('token', token);
    }

    if (token) {
        jwt.verify(token, Buffer.from(config.webEndpointConfig.secret, 'base64'), (err, decoded) => {
            if (err) {
                return res.status(401).send();
            } else {
                if (decoded.role === 'broadcaster') {
                    return next();
                } else {
                    return res.status(401).send();
                }
            }
        });
    } else {
        return res.status(401).send();
    }
}

設定ページ

設定ページ では、チャネルごとおよび拡張機能ごとの永続データを格納できます。拡張機能の起動時に、そのデータをフロントエンドに提供します。

今回の例では、 Node.js サーバー (EBS) の API エンドポイントの格納に使用します。

config.js

config.js ファイルは、フォーム情報を取得して Twitch 構成サービスに保存する場所です。

/* Quick and dirty binding form to twitch extension configuration */
const GvBindTwitchConfig = (form, twitchExtConfiguration) => {

    // On Submit, serialize form values into Twitch
    form.addEventListener("submit", function (e) {
        var configuration = GvSerializeForm(form);
        twitchExtConfiguration.set('broadcaster', '1.0', configuration);

        const msg = document.getElementById("message");
        if (msg.classList.contains('gv-displaynone')) {
            msg.classList.remove('gv-displaynone');
        }

        e.preventDefault();    //stop form from submitting
    });

    // On Load, deserialize Twitch configuration into form values
    var content = twitchExtConfiguration.broadcaster ? twitchExtConfiguration.broadcaster.content : {};
    GvDeserializeForm(form, content);
}