序章

HTML5では、 MediaDevicesAPIなどのデバイスハードウェアにアクセスできるAPIが導入されました。 このAPIは、オーディオやビデオなどのメディア入力デバイスへのアクセスを提供します。

このAPIの助けを借りて、開発者はオーディオおよびビデオデバイスにアクセスして、ブラウザーでライブビデオフィードをストリーミングおよび表示できます。 このチュートリアルでは、ユーザーのデバイスからビデオフィードにアクセスし、getUserMediaメソッドを使用してブラウザーに表示します。

getUserMedia APIは、メディア入力デバイスを使用してMediaStreamを生成します。 このMediaStreamには、オーディオかビデオかを問わず、要求されたメディアタイプが含まれています。 APIから返されたストリームを使用して、ビデオフィードをブラウザに表示できます。これは、ブラウザでのリアルタイム通信に役立ちます。

MediaStream Recording API と併用すると、ブラウザでキャプチャしたメディアデータを記録および保存できます。 このAPIは、新しく導入された他のAPIと同様に、安全なオリジンでのみ機能しますが、localhostおよびファイルURLでも機能します。

前提条件

このチュートリアルでは、最初に概念を説明し、Codepenを使用して例を示します。 最後のステップでは、ブラウザ用に機能するビデオフィードを作成します。

ステップ1—デバイスサポートの確認

まず、ユーザーのブラウザがmediaDevicesAPIをサポートしているかどうかを確認する方法を説明します。 このAPIはナビゲーターインターフェース内に存在し、ユーザーエージェントの現在の状態とIDを含みます。 チェックは、Codepenに貼り付けることができる次のコードを使用して実行されます。

if ('mediaDevices' in navigator && 'getUserMedia' in navigator.mediaDevices) {
  console.log("Let's get this party started")
}

まず、mediaDevicesAPIがnavigator内に存在するかどうかを確認し、次にgetUserMediaAPIがmediaDevices内で使用可能かどうかを確認します。 これがtrueを返す場合は、開始できます。

ステップ2—ユーザー権限をリクエストする

ブラウザによるgetUserMediaのサポートを確認した後、ユーザーエージェントでメディア入力デバイスを使用するための許可を要求する必要があります。 通常、ユーザーがアクセス許可を付与すると、Promiseが返され、メディアストリームに解決されます。 このPromiseは、これらのデバイスへのアクセスをブロックするユーザーによってアクセス許可が拒否された場合は返されません。

次の行をCodepenに貼り付けて、許可をリクエストします。

navigator.mediaDevices.getUserMedia({video: true})

getUserMediaメソッドの引数として提供されるオブジェクトは、constraintsと呼ばれます。 これにより、アクセス許可を要求しているメディア入力デバイスが決まります。 たとえば、オブジェクトにaudio: trueが含まれている場合、ユーザーはオーディオ入力デバイスへのアクセスを許可するように求められます。

ステップ3—メディアの制約を理解する

このセクションでは、制約の一般的な概念について説明します。 constraintsオブジェクトは、 MediaStreamConstraints オブジェクトであり、要求するメディアのタイプと各メディアタイプの要件を指定します。 使用するストリームの解像度(frontback)など、constraintsオブジェクトを使用して、要求されたストリームの要件を指定できます。

リクエストの際は、audioまたはvideoのいずれかを指定する必要があります。 要求されたメディアタイプがユーザーのブラウザで見つからない場合は、NotFoundErrorが返されます。

1280 x 720解像度のビデオストリームをリクエストする場合は、constraintsオブジェクトを次のように更新できます。

{
  video: {
    width: 1280,
    height: 720,
  }
}

このアップデートでは、ブラウザはストリームに指定された品質設定を一致させようとします。 ビデオデバイスがこの解像度を提供できない場合、ブラウザは他の利用可能な解像度を返します。

ブラウザが提供された解像度以上の解像度を返すようにするには、minプロパティを使用する必要があります。 constraintsオブジェクトを更新してminプロパティを含める方法は次のとおりです。

{
  video: {
    width: {
      min: 1280,
    },
    height: {
      min: 720,
    }
  }
}

これにより、返されるストリーム解像度が少なくとも1280 x 720になることが保証されます。 この最小要件を満たせない場合、約束はOverconstrainedErrorで拒否されます。

場合によっては、データの保存に懸念があり、設定された解像度を超えないようにストリームが必要になることがあります。 これは、ユーザーが限られたプランを使用している場合に便利です。 この機能を有効にするには、maxフィールドを含むように制約オブジェクトを更新します。

{
  video: {
    width: {
      min: 1280,
      max: 1920,
    },
    height: {
      min: 720,
      max: 1080
    }
  }
}

これらの設定により、ブラウザは、リターンストリームが1280 x 720を下回ったり、1920 x 1080を超えたりしないようにします。

使用できる他の用語には、exactおよびidealが含まれます。 ideal設定は通常、minおよびmaxプロパティとともに使用され、提供された理想値に最も近い可能な限り最良の設定を見つけます。

idealキーワードを使用するように制約を更新できます。

{
  video: {
    width: {
      min: 1280,
      ideal: 1920,
      max: 2560,
    },
    height: {
      min: 720,
      ideal: 1080,
      max: 1440
    }
  }
}

デバイスの前面または背面(モバイルの場合)カメラを使用するようにブラウザに指示するには、videoオブジェクトでfacingModeプロパティを指定できます。

{
  video: {
    width: {
      min: 1280,
      ideal: 1920,
      max: 2560,
    },
    height: {
      min: 720,
      ideal: 1080,
      max: 1440
    },
    facingMode: 'user'
  }
}

この設定では、すべてのデバイスで常に前面カメラが使用されます。 モバイルデバイスでバックカメラを利用するには、facingModeプロパティをenvironmentに変更できます。

{
  video: {
    ...
    facingMode: {
      exact: 'environment'
    }
  }
}

ステップ4—enumerateDevicesメソッドを使用する

enumerateDevicesメソッドが呼び出されると、ユーザーのPCで使用可能なすべての入力メディアデバイスが返されます。

この方法では、オーディオまたはビデオコンテンツのストリーミングに使用する入力メディアデバイスのユーザーオプションを提供できます。 このメソッドは、各デバイスに関する情報を含むMediaDeviceInfo配列に解決されたPromiseを返します。

このメソッドを使用する方法の例を以下のスニペットに示します。

async function getDevices() {
  const devices = await navigator.mediaDevices.enumerateDevices();
}

各デバイスの応答例は次のようになります。

{
  deviceId: "23e77f76e308d9b56cad920fe36883f30239491b8952ae36603c650fd5d8fbgj",
  groupId: "e0be8445bd846722962662d91c9eb04ia624aa42c2ca7c8e876187d1db3a3875",
  kind: "audiooutput",
  label: "",
}

注:利用可能なストリームが利用可能でない限り、またはユーザーがデバイスアクセス許可を付与している場合、ラベルは返されません。

ステップ5—ブラウザにビデオストリームを表示する

メディアデバイスへのアクセスを要求して取得するプロセスを経て、必要な解像度を含めるように制約を構成し、ビデオを録画するために必要なカメラを選択しました。

これらのすべての手順を実行した後、少なくとも、構成された設定に基づいてストリームが配信されているかどうかを確認する必要があります。 これを確実にするために、<video>要素を使用して、ブラウザーにビデオストリームを表示します。

前述のように、getUserMediaメソッドは、ストリームに解決できるPromiseを返します。 返されたストリームは、createObjectURLメソッドを使用してオブジェクトURLに変換できます。 このURLはビデオソースとして設定されます。

利用可能なビデオデバイスのリストからユーザーが選択できる短いデモを作成します。 enumerateDevicesメソッドを使用します。

これはnavigator.mediaDevicesメソッドです。 マイクやカメラなどの利用可能なメディアデバイスが一覧表示されます。 使用可能なメディアデバイスの詳細を示すオブジェクトの配列に解決可能なPromiseを返します。

index.htmlファイルを作成し、以下のコードで内容を更新します。

index.html
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
    <link rel="stylesheet" href="style.css">
    <title>Document</title>
</head>
<body>
<div class="display-cover">
    <video autoplay></video>
    <canvas class="d-none"></canvas>

    <div class="video-options">
        <select name="" id="" class="custom-select">
            <option value="">Select camera</option>
        </select>
    </div>

    <img class="screenshot-image d-none" alt="">

    <div class="controls">
        <button class="btn btn-danger play" title="Play"><i data-feather="play-circle"></i></button>
        <button class="btn btn-info pause d-none" title="Pause"><i data-feather="pause"></i></button>
        <button class="btn btn-outline-success screenshot d-none" title="ScreenShot"><i data-feather="image"></i></button>
    </div>
</div>

<script src="https://unpkg.com/feather-icons"></script>
<script src="script.js"></script>
</body>
</html>

上記のスニペットでは、必要な要素とビデオのいくつかのコントロールを設定しました。 現在のビデオフィードのスクリーンショットを撮るためのボタンも含まれています。

それでは、これらのコンポーネントを少しスタイルアップしましょう。

style.cssファイルを作成し、次のスタイルをコピーします。 Bootstrap が含まれており、コンポーネントを動作させるために作成する必要のあるCSSの量を減らすことができます。

style.css
.screenshot-image {
    width: 150px;
    height: 90px;
    border-radius: 4px;
    border: 2px solid whitesmoke;
    box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.1);
    position: absolute;
    bottom: 5px;
    left: 10px;
    background: white;
}

.display-cover {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 70%;
    margin: 5% auto;
    position: relative;
}

video {
    width: 100%;
    background: rgba(0, 0, 0, 0.2);
}

.video-options {
    position: absolute;
    left: 20px;
    top: 30px;
}

.controls {
    position: absolute;
    right: 20px;
    top: 20px;
    display: flex;
}

.controls > button {
    width: 45px;
    height: 45px;
    text-align: center;
    border-radius: 100%;
    margin: 0 6px;
    background: transparent;
}

.controls > button:hover svg {
    color: white !important;
}

@media (min-width: 300px) and (max-width: 400px) {
    .controls {
        flex-direction: column;
    }

    .controls button {
        margin: 5px 0 !important;
    }
}

.controls > button > svg {
    height: 20px;
    width: 18px;
    text-align: center;
    margin: 0 auto;
    padding: 0;
}

.controls button:nth-child(1) {
    border: 2px solid #D2002E;
}

.controls button:nth-child(1) svg {
    color: #D2002E;
}

.controls button:nth-child(2) {
    border: 2px solid #008496;
}

.controls button:nth-child(2) svg {
    color: #008496;
}

.controls button:nth-child(3) {
    border: 2px solid #00B541;
}

.controls button:nth-child(3) svg {
    color: #00B541;
}

.controls > button {
    width: 45px;
    height: 45px;
    text-align: center;
    border-radius: 100%;
    margin: 0 6px;
    background: transparent;
}

.controls > button:hover svg {
    color: white;
}

次のステップは、デモに機能を追加することです。 enumerateDevicesメソッドを使用して、使用可能なビデオデバイスを取得し、それをselect要素内のオプションとして設定します。 script.jsというファイルを作成し、次のスニペットで更新します。

script.js
feather.replace();

const controls = document.querySelector('.controls');
const cameraOptions = document.querySelector('.video-options>select');
const video = document.querySelector('video');
const canvas = document.querySelector('canvas');
const screenshotImage = document.querySelector('img');
const buttons = [...controls.querySelectorAll('button')];
let streamStarted = false;

const [play, pause, screenshot] = buttons;

const constraints = {
  video: {
    width: {
      min: 1280,
      ideal: 1920,
      max: 2560,
    },
    height: {
      min: 720,
      ideal: 1080,
      max: 1440
    },
  }
};

const getCameraSelection = async () => {
  const devices = await navigator.mediaDevices.enumerateDevices();
  const videoDevices = devices.filter(device => device.kind === 'videoinput');
  const options = videoDevices.map(videoDevice => {
    return `<option value="${videoDevice.deviceId}">${videoDevice.label}</option>`;
  });
  cameraOptions.innerHTML = options.join('');
};

play.onclick = () => {
  if (streamStarted) {
    video.play();
    play.classList.add('d-none');
    pause.classList.remove('d-none');
    return;
  }
  if ('mediaDevices' in navigator && navigator.mediaDevices.getUserMedia) {
    const updatedConstraints = {
      ...constraints,
      deviceId: {
        exact: cameraOptions.value
      }
    };
    startStream(updatedConstraints);
  }
};

const startStream = async (constraints) => {
  const stream = await navigator.mediaDevices.getUserMedia(constraints);
  handleStream(stream);
};

const handleStream = (stream) => {
  video.srcObject = stream;
  play.classList.add('d-none');
  pause.classList.remove('d-none');
  screenshot.classList.remove('d-none');
  streamStarted = true;
};

getCameraSelection();

上記のスニペットでは、いくつかのことが起こっています。 それらを分解しましょう:

  1. feather.replace():このメソッド呼び出しは、Web開発用のアイコンセットであるfeatherをインスタンス化します。
  2. constraints変数は、ストリームの初期構成を保持します。 これは、ユーザーが選択したメディアデバイスを含むように拡張されます。
  3. getCameraSelection:この関数はenumerateDevicesメソッドを呼び出します。 次に、解決されたPromiseからアレイをフィルタリングし、ビデオ入力デバイスを選択します。 フィルタリングされた結果から、<select>要素の<option>を作成します。
  4. getUserMediaメソッドの呼び出しは、playボタンのonclickリスナー内で行われます。 ここでは、ストリームを開始する前に、このメソッドがユーザーのブラウザでサポートされているかどうかを確認します。
  5. 次に、constraints引数を取るstartStream関数を呼び出します。 提供されたconstraintsを使用してgetUserMediaメソッドを呼び出します。 handleStreamは、解決されたPromiseからのストリームを使用して呼び出されます。 このメソッドは、返されるストリームをビデオ要素のsrcObjectに設定します。

次に、pausestopのページのボタンコントロールにクリックリスナーを追加し、screenshotsを取得します。 また、<select>要素にリスナーを追加して、選択したビデオデバイスのストリーム制約を更新します。

script.jsファイルを次のコードで更新します。

script.js
...
cameraOptions.onchange = () => {
  const updatedConstraints = {
    ...constraints,
    deviceId: {
      exact: cameraOptions.value
    }
  };
  startStream(updatedConstraints);
};

const pauseStream = () => {
  video.pause();
  play.classList.remove('d-none');
  pause.classList.add('d-none');
};

const doScreenshot = () => {
  canvas.width = video.videoWidth;
  canvas.height = video.videoHeight;
  canvas.getContext('2d').drawImage(video, 0, 0);
  screenshotImage.src = canvas.toDataURL('image/webp');
  screenshotImage.classList.remove('d-none');
};

pause.onclick = pauseStream;
screenshot.onclick = doScreenshot;

これで、ブラウザでindex.htmlファイルを開いたときに、再生ボタンをクリックするとストリームが開始されます。

これが完全なデモです:

https://codepen.io/chrisbeast/pen/ebYwpX

結論

このチュートリアルでは、getUserMediaAPIを紹介しました。 これは、Web上のメディアをキャプチャするプロセスを容易にするHTML5への興味深い追加です。

APIは、オーディオおよびビデオ入力デバイスへのアクセスを構成するために使用できるパラメーター(constraints)を取ります。 また、アプリケーションに必要なビデオ解像度を指定するためにも使用できます。

デモをさらに拡張して、MediaStream Recording APIを使用して、撮影したスクリーンショットを保存したり、ビデオおよびオーディオデータを記録および保存したりするオプションをユーザーに提供できます。