Spec-Zone.ru › Web APIs

Получение статических фотографий с помощью getUserMedia()

В этой статье показано, как использовать navigator.mediaDevices.getUserMedia(), чтобы получить доступ к камере на компьютере или мобильном телефоне с поддержкой getUserMedia() и сделать фотографию.

getUserMedia-based image capture app — on the left we have a video stream taken from a webcam and a take photo button, on the right we have the still image output from taking the photo

Вы также можете перейти сразу к Демонстрации.

HTML-разметка

Наш HTML-интерфейс состоит из двух основных секций: панели потока и захвата и панели отображения. Каждая из этих секций представлена в отдельном <div> для удобства стилизации и управления.

Первая панель слева содержит два компонента: элемент <video>, который будет получать поток с navigator.mediaDevices.getUserMedia(), и кнопку <button>, которую пользователь нажимает для захвата кадра видео.

<div class="camera">
  <video id="video">Video stream not available.</video>
  <button id="start-button">Take photo</button>
</div>

Это просто, и мы увидим, как это работает вместе, когда перейдём к коду JavaScript.

Далее у нас есть элемент <canvas>, в котором хранятся захваченные кадры, которые, возможно, будут как-то обработаны, а затем преобразуются в выходной файл изображения. Этот холст скрыт с помощью стилизации холста display: none, чтобы не захламлять экран — пользователю не нужно видеть этот промежуточный этап.

У нас также есть элемент <img>, в который мы будем отображать изображение — это конечный вывод для пользователя.

<canvas id="canvas"> </canvas>
<div class="output">
  <img id="photo" alt="The screen capture will appear in this box." />
</div>

Это вся необходимая HTML-разметка. Остальное — это просто элементы макета страницы и немного текста с ссылкой на эту страницу.

Код JavaScript

Теперь давайте посмотрим на код JavaScript. Мы разделим его на несколько небольших частей для лучшего понимания.

Инициализация

Мы начинаем с обертывания всего скрипта анонимной функцией, чтобы избежать глобальных переменных, а затем настраиваем различные переменные, которые будем использовать.

(() => {
  const width = 320;    // We will scale the photo width to this
  const height = 0;     // This will be computed based on the input stream

  const streaming = false;

  let video = null;
  let canvas = null;
  let photo = null;
  let startButton = null;

Эти переменные:

width

Независимо от размера входящего видео, мы будем масштабировать полученное изображение до 320 пикселей в ширину.

height

Выходная высота изображения будет вычислена на основе width и отношения сторон потока.

streaming

Указывает, активен ли в данный момент поток видео.

video

Это будет ссылка на элемент <video> после загрузки страницы.

canvas

Это будет ссылка на элемент <canvas> после загрузки страницы.

photo

Это будет ссылка на элемент <img> после загрузки страницы.

startButton

Это будет ссылка на элемент <button>, который используется для запуска захвата. Мы получим его после загрузки страницы.

Функция startup()

Функция startup() выполняется после загрузки страницы благодаря EventTarget.addEventListener. Эта функция запрашивает доступ к веб-камере пользователя, инициализирует выходной элемент <img> в исходном состоянии и устанавливает обработчики событий, необходимые для получения каждого кадра видео с камеры и реагирования при нажатии кнопки для захвата изображения.

Получение ссылок на элементы

Сначала мы получаем ссылки на необходимые элементы.

  function startup() {
    video = document.getElementById('video');
    canvas = document.getElementById('canvas');
    photo = document.getElementById('photo');
    startButton = document.getElementById('start-button');

Получение потока данных

Следующая задача — получить поток данных:

navigator.mediaDevices
  .getUserMedia({ video: true, audio: false })
  .then((stream) => {
    video.srcObject = stream;
    video.play();
  })
  .catch((err) => {
    console.error(`An error occurred: ${err}`);
  });

Здесь мы вызываем MediaDevices.getUserMedia() и запрашиваем поток видео (без аудио). Он возвращает обещание, к которому мы прикрепляем обработчики успешного и неудачного выполнения.

Обработчик успешного выполнения получает объект stream в качестве входных данных. Это источник для элемента <video> нашего нового потока.

После того, как поток связан с элементом <video>, мы запускаем его воспроизведение, вызвав HTMLMediaElement.play().

Обработчик ошибки вызывается, если открытие потока не удалось. Это произойдёт, например, если нет совместимой камеры или пользователь запретил доступ.

Прослушивание начала воспроизведения видео

После вызова HTMLMediaElement.play() на элементе <video>, проходит (желательно короткое) время, прежде чем начнётся поток видео. Чтобы избежать блокировки до этого момента, мы добавляем обработчик события на video для события canplay, которое выполняется при фактическом запуске воспроизведения видео. В этот момент все свойства в объекте video были настроены на основе формата потока.

video.addEventListener(
  "canplay",
  (ev) => {
    if (!streaming) {
      height = (video.videoHeight / video.videoWidth) * width;

      video.setAttribute("width", width);
      video.setAttribute("height", height);
      canvas.setAttribute("width", width);
      canvas.setAttribute("height", height);
      streaming = true;
    }
  },
  false,
);

Этот обработчик ничего не делает, если это не первый вызов; это проверяется с помощью значения нашей переменной streaming, которая равна false в первый раз.

Если это действительно первый запуск, мы устанавливаем высоту видео на основе разницы между фактическим размером видео, video.videoWidth, и шириной, с которой мы его будем рендерить, width.

Наконец, width и height и видео, и холста устанавливаются соответственно, вызывая Element.setAttribute() для каждого из двух свойств каждого элемента и устанавливая ширину и высоту по мере необходимости. Наконец, мы устанавливаем переменную streaming в true чтобы предотвратить непреднамеренное повторное выполнение этого кода настройки.

Обработка нажатий на кнопку

Для захвата статической фотографии при каждом нажатии пользователем кнопки startButton, нам необходимо добавить обработчик события на кнопку, который будет вызываться при возникновении события click:

startButton.addEventListener(
  "click",
  (ev) => {
    takePicture();
    ev.preventDefault();
  },
  false,
);

Этот метод достаточно прост: он просто вызывает нашу функцию takePicture() , определённую ниже в разделе Захват кадра из потока, а затем вызывает Event.preventDefault() на полученном событии, чтобы предотвратить обработку нажатия более одного раза.

Заключение метода startup()

В методе startup() осталось только две строки кода:

    clearPhoto();
  }

Здесь мы вызываем метод clearPhoto(), который мы опишем ниже в разделе Очистка поля фотографии.

Очистка поля фотографии

Очистка поля фотографии включает создание изображения, затем его преобразование в формат, используемый элементом <img> для отображения только что захваченного кадра. Этот код выглядит так:

function clearPhoto() {
  const context = canvas.getContext("2d");
  context.fillStyle = "#AAA";
  context.fillRect(0, 0, canvas.width, canvas.height);

  const data = canvas.toDataURL("image/png");
  photo.setAttribute("src", data);
}

Мы начинаем с получения ссылки на скрытый элемент холста <canvas>, который мы используем для рендеринга офскрин. Далее мы устанавливаем fillStyle в #AAA (довольно светлый серый) и заполняем весь холст этим цветом, вызвав fillRect().

В завершение этой функции мы преобразуем холст в изображение PNG и вызываем photo.setAttribute(), чтобы наше поле захваченной фотографии отображало изображение.

Захват кадра из потока

Осталось определить одну функцию, и это суть всего упражнения: функцию takePicture(), которая предназначена для захвата текущей отображаемой кадра видео, преобразования её в файл PNG и отображения в поле захваченного кадра. Код выглядит так:

function takePicture() {
  const context = canvas.getContext("2d");
  if (width && height) {
    canvas.width = width;
    canvas.height = height;
    context.drawImage(video, 0, 0, width, height);

    const data = canvas.toDataURL("image/png");
    photo.setAttribute("src", data);
  } else {
    clearPhoto();
  }
}

Как и всегда, когда нам нужно работать с содержимым холста, мы начинаем с получения 2D контекста рисования для скрытого холста.

Затем, если ширина и высота обе не равны нулю (что означает, что есть, по крайней мере, потенциально допустимые данные изображения), мы устанавливаем ширину и высоту холста, чтобы соответствовать ширине и высоте захваченного кадра, а затем вызываем drawImage(), чтобы нарисовать текущий кадр видео в контексте, заполнив весь холст изображением кадра.

Примечание: Это использует тот факт, что интерфейс HTMLVideoElement выглядит как HTMLImageElement для любого API, который принимает HTMLImageElement в качестве параметра, при этом текущий кадр видео представлен как содержимое изображения.

После того, как холст содержит захваченное изображение, мы преобразуем его в формат PNG, вызвав HTMLCanvasElement.toDataURL() на нём; в заключение, мы вызываем photo.setAttribute(), чтобы отобразить изображение в нашем поле захваченного кадра.

Если доступно недействительное изображение (то есть, width и height оба равны 0), мы очищаем содержимое поля захваченного кадра, вызвав clearPhoto().

Демо

HTML

<div class="content-area">
  <h1>MDN - navigator.mediaDevices.getUserMedia(): Still photo capture demo</h1>
  <p>
    This example demonstrates how to set up a media stream using your built-in
    webcam, fetch an image from that stream, and create a PNG using that image.
  </p>
  <div class="camera">
    <video id="video">Video stream not available.</video>
    <button id="start-button">Take photo</button>
  </div>
  <canvas id="canvas"> </canvas>
  <div class="output">
    <img id="photo" alt="The screen capture will appear in this box." />
  </div>
  <p>
    Visit our article
    <a
      href="https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API/Taking_still_photos">
      Taking still photos with WebRTC</a
    >
    to learn more about the technologies used here.
  </p>
</div>

CSS

#video {
  border: 1px solid black;
  box-shadow: 2px 2px 3px black;
  width: 320px;
  height: 240px;
}

#photo {
  border: 1px solid black;
  box-shadow: 2px 2px 3px black;
  width: 320px;
  height: 240px;
}

#canvas {
  display: none;
}

.camera {
  width: 340px;
  display: inline-block;
}

.output {
  width: 340px;
  display: inline-block;
  vertical-align: top;
}

#start-button {
  display: block;
  position: relative;
  margin-left: auto;
  margin-right: auto;
  bottom: 32px;
  background-color: rgb(0 150 0 / 50%);
  border: 1px solid rgb(255 255 255 / 70%);
  box-shadow: 0px 0px 1px 2px rgb(0 0 0 / 20%);
  font-size: 14px;
  font-family: "Lucida Grande", "Arial", sans-serif;
  color: rgb(255 255 255 / 100%);
}

.content-area {
  font-size: 16px;
  font-family: "Lucida Grande", "Arial", sans-serif;
  width: 760px;
}

JavaScript

(() => {
  // The width and height of the captured photo. We will set the
  // width to the value defined here, but the height will be
  // calculated based on the aspect ratio of the input stream.

  const width = 320; // We will scale the photo width to this
  let height = 0; // This will be computed based on the input stream

  // |streaming| indicates whether or not we're currently streaming
  // video from the camera. Obviously, we start at false.

  let streaming = false;

  // The various HTML elements we need to configure or control. These
  // will be set by the startup() function.

  let video = null;
  let canvas = null;
  let photo = null;
  let startButton = null;

  function showViewLiveResultButton() {
    if (window.self !== window.top) {
      // Ensure that if our document is in a frame, we get the user
      // to first open it in its own tab or window. Otherwise, it
      // won't be able to request permission for camera access.
      document.querySelector(".content-area").remove();
      const button = document.createElement("button");
      button.textContent = "View live result of the example code above";
      document.body.append(button);
      button.addEventListener("click", () => window.open(location.href));
      return true;
    }
    return false;
  }

  function startup() {
    if (showViewLiveResultButton()) {
      return;
    }
    video = document.getElementById("video");
    canvas = document.getElementById("canvas");
    photo = document.getElementById("photo");
    startButton = document.getElementById("start-button");

    navigator.mediaDevices
      .getUserMedia({ video: true, audio: false })
      .then((stream) => {
        video.srcObject = stream;
        video.play();
      })
      .catch((err) => {
        console.error(`An error occurred: ${err}`);
      });

    video.addEventListener(
      "canplay",
      (ev) => {
        if (!streaming) {
          height = video.videoHeight / (video.videoWidth / width);

          // Firefox currently has a bug where the height can't be read from
          // the video, so we will make assumptions if this happens.

          if (isNaN(height)) {
            height = width / (4 / 3);
          }

          video.setAttribute("width", width);
          video.setAttribute("height", height);
          canvas.setAttribute("width", width);
          canvas.setAttribute("height", height);
          streaming = true;
        }
      },
      false,
    );

    startButton.addEventListener(
      "click",
      (ev) => {
        takePicture();
        ev.preventDefault();
      },
      false,
    );

    clearPhoto();
  }

  // Fill the photo with an indication that none has been
  // captured.

  function clearPhoto() {
    const context = canvas.getContext("2d");
    context.fillStyle = "#AAA";
    context.fillRect(0, 0, canvas.width, canvas.height);

    const data = canvas.toDataURL("image/png");
    photo.setAttribute("src", data);
  }

  // Capture a photo by fetching the current contents of the video
  // and drawing it into a canvas, then converting that to a PNG
  // format data URL. By drawing it on an offscreen canvas and then
  // drawing that to the screen, we can change its size and/or apply
  // other changes before drawing it.

  function takePicture() {
    const context = canvas.getContext("2d");
    if (width && height) {
      canvas.width = width;
      canvas.height = height;
      context.drawImage(video, 0, 0, width, height);

      const data = canvas.toDataURL("image/png");
      photo.setAttribute("src", data);
    } else {
      clearPhoto();
    }
  }

  // Set up our event listener to run the startup process
  // once loading is complete.
  window.addEventListener("load", startup, false);
})();

Результат

Развлечение с фильтрами

Поскольку мы захватываем изображения с веб-камеры пользователя, считывая кадры из элемента <video>, мы можем очень легко применять фильтры и эффекты к видео. Как оказалось, любые CSS фильтры, которые вы применяете к элементу с помощью свойства filter, влияют на захваченное фото. Эти фильтры могут варьироваться от простых (преобразование изображения в черно-белое) до сложных (гауссовские размытия и вращения цветового тона).

Вы можете экспериментировать с этим эффектом, например, используя редактор стилей инструментов разработчика Firefox; см. Изменение CSS-фильтров для получения подробной информации о том, как это сделать.

Использование определённых устройств

При необходимости вы можете ограничить набор разрешённых источников видео до конкретного устройства или набора устройств. Для этого вызовите MediaDevices.enumerateDevices. Когда обещание выполнено с массивом объектов MediaDeviceInfo, описывающих доступные устройства, найдите устройства, которые вы хотите разрешить, и укажите соответствующие deviceId или deviceId в объекте MediaTrackConstraints, передаваемом в getUserMedia().

См. также

  • Пример кода на GitHub
  • MediaDevices.getUserMedia
  • Использование кадров из видео в руководстве по Canvas
  • CanvasRenderingContext2D.drawImage()

© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API/Taking_still_photos

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API