Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save recursivecodes/31a0c77d4ee0786b77c6af1069bdc898 to your computer and use it in GitHub Desktop.
Save recursivecodes/31a0c77d4ee0786b77c6af1069bdc898 to your computer and use it in GitHub Desktop.

リアルタイム ストリーミングを始める

設定 (Setup)

  • AWSコンソール上でCould9を開く
  • 左のメニューからMy environments、そしてCreate environmentsをクリック
  • Detailsの中、名前欄にivsdemo
  • その他のオプションはデフォルトで作成をクリック

ターミナル ウィンドウ内で mkdir ivs-real-time と入力して ivs-real-time ディレクトリを作成し、次にcd ivs-real-time を入力して ivs-real-time ディレクトリに変更します。

ステップ 1 - AWS コンソールからステージを作成する (Step 1)

Amazon IVS コンソールから、「Amazon IVS ステージ」を選択し、「開始する」をクリックします。

image

ステージ名として「demo-stage」と入力し、「ステージを作成」をクリックします。

image

ステージの詳細ページから、ステージ ARN をコピーします。

image

ステップ 2 - ローカルサーバーの作成 (Step 2)

リクエストに応答して静的 html および js ファイルを提供する単純な HTTP サーバーと、/stage-token というエンドポイントを作成します。/stage-token エンドポイントはAmazon IVS ステージに接続するために必要なステージトークンを生成します。

ターミナルから npm init es6 -yでプロジェクトを初期化し、npm install @aws-sdk/client-ivs-realtimeで Amazon IVS Real-Time SDK をインストールします。

server.js という名前のファイルを作成し、次のコードを入力します。

import * as http from 'http';
import * as url from 'url';
import * as fs from 'fs';
import * as path from 'path';
import { CreateParticipantTokenCommand, IVSRealTimeClient } from "@aws-sdk/client-ivs-realtime";

const ivsRealtimeClient = new IVSRealTimeClient();

const createStageToken = async (stageArn, attributes, capabilities, userId, duration) => {
  const createStageTokenRequest = new CreateParticipantTokenCommand({
    attributes,
    capabilities,
    userId,
    stageArn,
    duration,
  });
  const createStageTokenResponse = await ivsRealtimeClient.send(createStageTokenRequest);
  return createStageTokenResponse;
};

async function handler(req, res) {
  console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
  const parsedUrl = url.parse(req.url, true);
  if (parsedUrl.pathname === '/stage-token') {
    let body = '';
    req.on('data', chunk => {
      body += chunk.toString();
    });
    req.on('end', async () => {
      const params = JSON.parse(body);
      res.writeHead(200, { 'Content-type': 'application/json' });
      const tokenResponse = await createStageToken(
        params.stageArn,
        params.attributes,
        params.capabilities,
        params.userId,
        params.duration,
      );
      res.write(JSON.stringify(tokenResponse));
      res.end();
    });
  }
  else {
    let filePath = '.' + req.url;
    if (filePath == './') filePath = './index.html';
    let extname = path.extname(filePath);
    let contentType = 'text/html';
    if(extname === '.js') contentType = 'text/javascript';

    fs.readFile(filePath, function (error, content) {
      if (error) {
        if (error.code == 'ENOENT') {
          res.writeHead(404, { 'Content-Type': contentType });
          res.end(content, 'utf-8');
        }
        else {
          res.writeHead(500);
          res.end('Error: ' + error.code + ' ..\n');
        }
      }
      else {
        res.writeHead(200, { 'Content-Type': contentType });
        res.end(content, 'utf-8');
      }
    });

  }
}

const server = http.createServer(handler);
server.listen(8080);

node server.jsを使用してターミナルでサーバーを実行します。 このサーバーを実行したままにして、新しいターミナル ウィンドウを開いて、次のコマンドを実行してテストします。

curl -X POST \
  -H "Content-Type: application/json" \
  -d "{ \
      \"stageArn\": \"[YOUR STAGE ARN]\",
      \"userId\": \"123456\",
      \"capabilities\": [\"PUBLISH\", \"SUBSCRIBE\"],
      \"attributes\": {\"username\": \"todd\"}
    }" \
  localhost:8080/stage-token | jq

このリクエストにより、ターミナルに次のような JSON 結果が出力されます。

{
  "$metadata": {
    "httpStatusCode": 200,
    "requestId": "...",
    "cfId": "...",
    "attempts": 1,
    "totalRetryDelay": 0
  },
  "participantToken": {
    "attributes": {
      "username": "todd"
    },
    "capabilities": [
      "PUBLISH",
      "SUBSCRIBE"
    ],
    "expirationTime": "2024-02-06T01:48:35.000Z",
    "participantId": "AXRHIx4L6AnO",
    "token": "eyJhbGciOiJLTV...",
    "userId": "123456"
  }
}

ステップ 3 - リアルタイム Web クライアントの作成 (Step 3)

ターミナルで、ivs-real-time ディレクトリから次のコマンドを実行して、このワークショップで使用するクライアント側ファイルを作成します。

touch index.html
touch index.js
touch api.js
touch ivs-realtime-utils.js

api.jsを開き、以下の/stage-token エンドポイントを呼び出してステージ参加者トークンを返すために使用できる関数を入力します。 [YOUR STAGE ARN] を、上で作成したステージの ARN に置き換えてください。

export const getStageToken = async (username) => {
  const stageTokenRequest = await fetch(`/stage-token`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      'stageArn': '[YOUR STAGE ARN]',
      'userId': Date.now().toString(),
      'capabilities': ['PUBLISH', 'SUBSCRIBE'],
      'attributes': {
        username,
      }
    }),
  });
  const stageTokenResponse = await stageTokenRequest.json();
  return stageTokenResponse.participantToken.token;
};

ivs-realtime-utils.js を開き、次のコードを入力します。 このファイルには、デバイス (カメラとマイク) のアクセス許可の取得、デバイスの一覧表示、デバイスからのメディア ストリームの取得に使用するいくつかのユーティリティ関数が含まれています。 また、DOM 要素 (<video> タグ) の生成や、<video> 要素へのメディア ストリームの追加やメディア ストリームの削除などを支援する関数もいくつかあります。

if (typeof IVSBroadcastClient === 'undefined') throw new Error('IVSBroadcastClient not found. You must include the Amazon IVS Web Broadcast SDK before this file.');
const { SubscribeType, StreamType } = IVSBroadcastClient;

class StageStrategy {
  audioStream;
  videoStream;

  constructor(audioStream, videoStream) {
    this.audioStream = audioStream;
    this.videoStream = videoStream;
  }

  // wrap `updateTracks()` with a friendlier name
  updateStreams(newAudioStream, newVideoStream) {
    this.updateTracks(newAudioStream, newVideoStream);
  }

  updateTracks(newAudioStream, newVideoStream) {
    this.audioStream = newAudioStream;
    this.videoStream = newVideoStream;
  }

  stageStreamsToPublish() {
    return [this.audioStream, this.videoStream];
  }

  shouldPublishParticipant(participant) {
    return true;
  }

  shouldSubscribeToParticipant(participant) {
    return SubscribeType.AUDIO_VIDEO;
  }
};

class StageUtils {
  static REAL_TIME_VIDEO_LANDSCAPE = {
    width: { max: 1280 },
    height: { max: 720 },
  };

  static handlePermissions = async (needAudio, needVideo) => {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ video: needVideo, audio: needAudio });
      for (const track of stream.getTracks()) {
        track.stop();
      }
      return { video: needVideo, audio: needAudio };
    }
    catch (err) {
      console.error(err.message);
      return { video: false, audio: false };
    }
  };

  static listVideoDevices = async () => {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return devices.filter((d) => d.kind === 'videoinput');
  };

  static listAudioDevices = async () => {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return devices.filter((d) => d.kind === 'audioinput');
  };

  static getVideoStream = async (deviceId) => {
    return await navigator.mediaDevices.getUserMedia({
      video: {
        deviceId: {
          exact: deviceId,
        },
        width: this.REAL_TIME_VIDEO_LANDSCAPE.width,
        height: this.REAL_TIME_VIDEO_LANDSCAPE.height,
      },
    });
  };

  static getAudioStream = async (deviceId) => {
    return await navigator.mediaDevices.getUserMedia({
      audio: {
        deviceId: {
          exact: deviceId,
        },
      },
    });
  };

  static addParticipantStreams = (element, participant, streams) => {
    let streamsToDisplay = streams;
    if (participant.isLocal) {
      streamsToDisplay = streams.filter((stream) => stream.streamType === StreamType.VIDEO);
    }
    const mediaStream = element.srcObject || new MediaStream();
    streamsToDisplay.forEach((stream) => {
      mediaStream.addTrack(stream.mediaStreamTrack);
    });
    return mediaStream;
  };

  static removeParticipantStreams = (element, streams) => {
    const mediaStream = element.srcObject;
    const newStream = new MediaStream();
    mediaStream.getTracks().forEach((track) => {
      if (!streams.find(t => t.id === track.id)) {
        newStream.addTrack(track);
      }
    });
    element.srcObject = newStream;
    return newStream;
  };

  static generateParticipantVideoElement = (participant, streams) => {
    const participantVideoEl = document.createElement('video');
    participantVideoEl.setAttribute('autoplay', 'autoplay');
    participantVideoEl.setAttribute('playsinline', 'playsinline');
    participantVideoEl.srcObject = new MediaStream();
    return participantVideoEl;
  };

  static generateCameraSelect = async () => {
    const el = document.createElement('select');
    const videoDevices = await this.listVideoDevices();
    videoDevices.forEach((device) => {
      const option = document.createElement('option');
      option.value = device.deviceId;
      option.innerHTML = device.label;
      el.appendChild(option);
    });
    return el;
  };

  static generateMicrophoneSelect = async () => {
    const el = document.createElement('select');
    const audioDevices = await this.listAudioDevices();
    audioDevices.forEach((device) => {
      const option = document.createElement('option');
      option.value = device.deviceId;
      option.innerHTML = device.label;
      el.appendChild(option);
    });
    return el;
  };
}
var IvsStageUtils = {
  StageStrategy,
  StageUtils
};

index.html を開き、次のコードを入力します。 このファイルには、(単純な CSS スタイルを使うための)Milligram、Amazon IVS Web Broadcast SDK、ivs-realtime-utils.js スクリプト、および index.js ファイルのインクルードが含まれています。 また、参加者がステージに参加するときに参加者の <video> 要素をレンダリングするために使用する <div> も含まれています。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Amazon IVS Real-Time Demo</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/milligram/1.4.1/milligram.css">
  <script src="https://web-broadcast.live-video.net/1.9.0/amazon-ivs-web-broadcast.js"></script>
  <script src="ivs-realtime-utils.js"></script>
  <script src="index.js" type="module"></script>
  <style>
    video {
      width: 640px;
      height: 360px;
      border-radius: 5px;
      background-color: black;
    }

    #controls {
      width: 500px;
    }

    body {
      padding-top: 10px !important;
    }
  </style>
</head>

<body class="container">
  <div id="participants"></div>
</body>

</html>

index.jsを開き、スクリプトの先頭に次のコードを追加して、api.jsファイルから getStageToken() 関数をインポートし、上記で作成したivs-realtime-utils.js と Amazon Web Broadcast SDKを使うために必要な変数をいくつか宣言します。。

import { getStageToken } from './api.js';
const { StageStrategy, StageUtils } = IvsStageUtils;
const { Stage, LocalStageStream, StageEvents } = IVSBroadcastClient;

次に、API を呼び出してステージ トークンを取得します。

const stageToken = await getStageToken('[your name]');

次に、StageUtils.handlePermissions() 関数を呼び出してデバイスの権限を取得し、ブラウザ内のマイクとカメラのデバイスの一覧を取得します。

await StageUtils.handlePermissions(true, true);

const videoDevices = await StageUtils.listVideoDevices();
const audioDevices = await StageUtils.listAudioDevices();

デフォルトのカメラとマイクの両方からメディア ストリームを取得します。

const camStream = await StageUtils.getVideoStream(videoDevices[0].deviceId);
const micStream = await StageUtils.getAudioStream(audioDevices[0].deviceId);

camStreammicStream に含まれるビデオおよびオーディオ トラックから LocalStageStream のインスタンスを作成します。

let localVideoStream = new LocalStageStream(camStream.getVideoTracks()[0]);
let localAudioStream = new LocalStageStream(micStream.getAudioTracks()[0]);

localVideoStreamlocalAudioStream を使用して StageStrategy を生成します。

const strategy = new StageStrategy(localAudioStream, localVideoStream);

StageStrategy には、Amazon IVS Web Broadcast SDK がステージ参加者を公開するかサブスクライブするかを決定するために使用するいくつかの関数が含まれています。 デフォルトの設定を確認するには、 ivs-realtime-utils.js のクラス定義を参照してください。 このワークショップの後半で説明するように、これは高度な使用例に合わせてカスタマイズできます。 デフォルトの実装では、ローカル参加者を公開し、他のすべての参加者のオーディオとビデオをサブスクライブします。

次に、 stageTokenstrategy を使用して Stage を作成します。

const stage = new Stage(stageToken, strategy);

次に、参加者 (ローカル参加者を含む) がステージに参加またはステージから退出したときにアクションを実行するイベント ハンドラーをいくつか追加します。 まず、参加者が参加したときと参加者ストリームが追加されたときに処理する 2 つのリスナーを追加します。 ストリームが追加されると、最初に <video> 要素が DOM に存在するかどうかを確認します。 存在しない場合は、 StageUtils.generateParticipantVideoElement() を使用して作成し、DOM に追加します。 次に、StageUtils.addParticipantStreams() を使用して、受信ストリームを要素に追加します。

stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant, streams) => {
  console.log('STAGE_PARTICIPANT_JOINED');
});

stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED, (participant, streams) => {
  console.log('STAGE_PARTICIPANT_STREAMS_ADDED');
  const participantId = `participant-${participant.id}`;
  let participantVideoEl = document.getElementById(participantId);
  if (!participantVideoEl) {
    participantVideoEl = StageUtils.generateParticipantVideoElement(participant, streams);
    participantVideoEl.setAttribute('id', participantId);
    document.getElementById('participants').appendChild(participantVideoEl);
  }
  StageUtils.addParticipantStreams(participantVideoEl, participant, streams);
});

次に、参加者が退席したとき、または参加者のストリームが削除されたときに処理する 2 つのリスナーを追加します。 リモート参加者がビデオまたはオーディオを非公開にすることを決定した場合、ストリームは削除される可能性があるため、必要に応じて DOM <video> 要素を更新する必要があります。

stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_REMOVED, (participant, streams) => {
  console.log('STAGE_PARTICIPANT_STREAMS_REMOVED');
  const participantVideoEl = document.getElementById(`participant-${participant.id}`);
  StageUtils.removeParticipantStreams(participantVideoEl, streams);
});


stage.on(StageEvents.STAGE_PARTICIPANT_LEFT, (participant, streams) => {
  console.log('STAGE_PARTICIPANT_LEFT');
  document.getElementById(`participant-${participant.id}`).remove();
});

次に、ステージに参加します。

await stage.join();

ステップ2で作成したサーバーがターミナル上でまだ実行されていることを確認してください。実行されていない場合は、node server.jsで起動してください。Cloud9上で「Preview」をクリックし、「Preview Running Application」を選択してください。

image

ブラウザのプレビュータブで、「Pop Out Into New Window」をクリックしてください。

image

新しいブラウザタブで、デフォルトのカメラとマイクを使用してステージに参加したことを確認してください。

💡 注意: オーディオ エコーを防ぐために、2 番目のブラウザ タブでテストする前にラップトップをミュートする必要があります。

複数の参加者をリアルタイムステージでテストするために、プレビューURLをコピーして新しいブラウザタブにペーストしてください。

ステップ 4 - リアルタイム Web クライアントを拡張する (Step 4)

index.html を開き、<body> を次のように変更します。

<body class="container">
  <div id="controls">
    <div>
      <input type="checkbox" name="isPublishing" id="isPublishing" checked />
      <label class="label-inline" for="isPublishing">Publish?</label>
    </div>
    <div id="audioDevices"></div>
    <div id="videoDevices"></div>
    <div>
      <button id="muteMic">Mute Mic</button>
      <button id="muteCam">Mute Cam</button>
    </div>
  </div>
  <div id="participants"></div>
</body>

次に、index.js で次の行を見つけます。

const strategy = new StageStrategy(localAudioStream, localVideoStream);
const stage = new Stage(stageToken, strategy);

そしてそれらを次のように置き換えます。

let isPublishing = true;
class CustomStageStrategy extends StageStrategy {
  shouldPublishParticipant() {
    return isPublishing;
  }
};
const strategy = new CustomStageStrategy(localAudioStream, localVideoStream);
const stage = new Stage(stageToken, strategy);

ここでは、StageStrategy クラスを拡張し、ブール値の isPublishing 値を返すように shouldPublishParticipant() 関数をオーバーライドすることで、カスタム ステージ ストラテジー オブジェクトを実装しています。

次に、 isPublishing 入力要素の変更イベントをリッスンするイベント ハンドラーを追加します。 このイベントが発生すると、 isPublishing 変数の値を切り替え、ステージ ストラテジーを更新します。

document.getElementById('isPublishing').addEventListener('change', () => {
  isPublishing = !isPublishing;
  stage.refreshStrategy();
});

アプリケーションを実行し、ローカル参加者の非公開と再公開をテストします。

次に、ユーザーが Web カメラを変更できるようにする <select> 要素を生成します。 この要素の change イベント ハンドラーで、strategyvideoStream を更新し、新しく選択したカメラを使用するようにストラテジーを更新します。

const camSelectEl = await StageUtils.generateCameraSelect();
camSelectEl.setAttribute('id', 'cameras');
document.getElementById('videoDevices').appendChild(camSelectEl);

camSelectEl.addEventListener('change', async (evt) => {
  const videoStream = await StageUtils.getVideoStream(evt.target.value);
  strategy.videoStream = new LocalStageStream(videoStream.getVideoTracks()[0]);
  stage.refreshStrategy();
});

ユーザーがマイクを変更できるようにする別の <select> 要素を追加します。 この要素の change イベント ハンドラーで、strategyaudioStream を更新し、新しく選択されたマイクを使用するように ストラテジーを更新します。

const micSelectEl = await StageUtils.generateMicrophoneSelect();
micSelectEl.setAttribute('id', 'microphones');
document.getElementById('audioDevices').appendChild(micSelectEl);

micSelectEl.addEventListener('change', async (evt) => {
  const audioStream = await StageUtils.getAudioStream(evt.target.value);
  strategy.audioStream = new LocalStageStream(audioStream.getAudioTracks()[0]);
  stage.refreshStrategy();
});

アプリケーションを実行し、新しいカメラとマイクを選択します。

「click」イベントのイベント リスナーを「Mute Cam」ボタンと「Mute Mic」ボタンに追加し、参加者からの音声とビデオをミュートできるようにします。

let isAudioMuted = false;
let isVideoMuted = false;

document.getElementById('muteMic').addEventListener('click', (evt) => {
  isAudioMuted = !isAudioMuted;
  evt.currentTarget.innerHTML = isAudioMuted ? 'Unmute Mic' : 'Mute Mic';
  localAudioStream.setMuted(isAudioMuted);
});

document.getElementById('muteCam').addEventListener('click', (evt) => {
  isVideoMuted = !isVideoMuted;
  evt.currentTarget.innerHTML = isVideoMuted ? 'Unmute Cam' : 'Mute Cam';
  localVideoStream.setMuted(isVideoMuted);
});

ステップ 5 - サーバーサイドコンポジションによる低レイテンシーチャネルへのブロードキャスト (Step 5)

Amazon IVS ステージでは、公開およびサブスクライブする参加者は最大 12 人、サブスクライブのみの参加者は最大 10,000 人まで参加することができます。 サーバーサイドコンポジションを使用すればオーディオとビデオを低レイテンシーチャネルにブロードキャストし、10,000 人以上の視聴者に届けることができます。

💡 注: サーバーサイドコンポジションの詳細については、リアルタイム ストリーミング用 Amazon IVS ユーザーガイドを参照してください。

Amazon IVS 低レイテンシーチャネルを作成します。

image

チャネルに「ivs-demo-channel」という名前を付け、デフォルト設定を受け入れて「チャネルを作成」をクリックします。

image

左側のサイドバーで、「サーバーサイドの構成」の下にある「エンコーダー設定」を選択します。

image

「エンコーダー設定を作成」をクリックします。

image

設定に「demo-encoder-configuration」という名前を付け、デフォルトの設定を受け入れ、「エンコーダー設定を作成」をクリックします。

image

左側のサイドバーで、「サーバーサイドの構成」の下にある「コンポジション」を選択します。

image

「コンポジションを開始」をクリックします。

image

「ソース」で、ステップ 1 で作成した Amazon IVS ステージを選択します。

image

「レイアウト」で「グリッド」を選択します。

image

「目的地」で「送信先を追加」をクリックします。

image

送信先に「demo-destination」という名前を付け、「送信先タイプ」で「チャネル」を選択し、このステップで作成した低レイテンシーを選択し、このステップで作成したエンコーダー設定を選択して、「追加」をクリックします。

image

「コンポジションを開始」をクリックします。

image

コンポジションが「アクティブ」であることを確認します。

image

「目的地」まで下にスクロールし、目的地のリストを展開します。 チャンネル名をクリックします。

image

チャンネル ページで下にスクロールし、[再生] の下にある再生をクリックして、低レイテンシーチャンネルへのステージ ブロードキャストを表示します。

image
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment