Przechwytywanie dźwięku i obrazu w HTML5

Wprowadzenie

Przechwytywanie dźwięku i obrazu od dawna było „świętym Graalem” programistów stron internetowych. Przez wiele lat musieliśmy polegać na wtyczkach do przeglądarek (Flash lub Silverlight), aby to osiągnąć. No dalej!

HTML5 na ratunek. Może nie jest to oczywiste, ale rozwój HTML5 przyniósł gwałtowny wzrost dostępu do sprzętu urządzenia. Geolokalizacja (GPS), Orientation API (akcelerometr), WebGL (GPU) i Web Audio API (sprzęt audio) to doskonałe przykłady. Te funkcje są niezwykle zaawansowane, ponieważ udostępniają interfejsy JavaScript API wysokiego poziomu, które wykorzystują możliwości sprzętowe systemu.

W tym samouczku przedstawiamy nowy interfejs API GetUserMedia, który umożliwia aplikacjom internetowym dostęp do kamery i mikrofonu użytkownika.

Droga do getUserMedia()

Jeśli nie znasz historii interfejsu API getUserMedia(), to, jak do niego doszliśmy, jest ciekawą opowieścią.

W ciągu ostatnich kilku lat powstało kilka wariantów interfejsów API do przechwytywania multimediów. Wiele osób dostrzegło potrzebę dostępu do urządzeń natywnych w internecie, ale doprowadziło to do tego, że każdy zaczął tworzyć własną specyfikację. Sytuacja stała się tak skomplikowana, że W3C zdecydowało się utworzyć grupę roboczą. Ich jedyny cel? Zrozumieć szaleństwo! Zadaniem grupy roboczej Device APIs Policy (DAP) jest konsolidacja i standaryzacja wielu propozycji.

Spróbuję podsumować, co się wydarzyło w 2011 roku…

Runda 1. Przechwytywanie multimediów HTML

HTML Media Capture to pierwsza próba standaryzacji przechwytywania multimediów w internecie przez DAP. Działa to przez przeciążenie <input type="file"> i dodanie nowych wartości parametru accept.

Jeśli chcesz, aby użytkownicy mogli zrobić sobie zdjęcie kamerą internetową, możesz to zrobić za pomocą capture=camera:

<input type="file" accept="image/*;capture=camera">

Nagrywanie filmu lub dźwięku przebiega podobnie:

<input type="file" accept="video/*;capture=camcorder">
<input type="file" accept="audio/*;capture=microphone">

Fajnie, prawda? Szczególnie podoba mi się to, że ponownie wykorzystuje dane wejściowe pliku. Semantycznie ma to sens. Ten interfejs API nie radzi sobie jednak z efektami w czasie rzeczywistym (np. renderowaniem danych z kamery internetowej na żywo do elementu <canvas> i stosowaniem filtrów WebGL). HTML Media Capture umożliwia tylko nagrywanie pliku multimedialnego lub robienie zdjęcia.

Pomoc:

  • Przeglądarka Android 3.0 – jedna z pierwszych implementacji. Obejrzyj ten film, aby zobaczyć, jak to działa.
  • Chrome na Androida (0,16)
  • Firefox Mobile 10.0
  • iOS6 Safari i Chrome (częściowa obsługa)

Runda 2. Element urządzenia

Wielu uważało, że HTML Media Capture jest zbyt ograniczony, więc powstała nowa specyfikacja, która obsługuje dowolny typ (przyszłego) urządzenia. Nic dziwnego, że projekt wymagał nowego elementu, <device>, który stał się poprzednikiem getUserMedia().

Opera była jedną z pierwszych przeglądarek, które utworzyły wstępne implementacje przechwytywania wideo na podstawie elementu <device>. Wkrótce potem (tego samego dnia, żeby być dokładnym) WhatWG zdecydowało się usunąć tag <device> na rzecz innego, tym razem interfejsu API JavaScript o nazwie navigator.getUserMedia(). Tydzień później Opera wydała nowe kompilacje, które obsługiwały zaktualizowaną specyfikację getUserMedia(). Jeszcze w tym samym roku Microsoft dołączył do grona, wydając Lab dla IE9, który obsługiwał nową specyfikację.

Oto jak wyglądałby film <device>:

<device type="media" onchange="update(this.data)"></device>
<video autoplay></video>
<script>
  function update(stream) {
    document.querySelector('video').src = stream.url;
  }
</script>

Pomoc:

Niestety żadna opublikowana przeglądarka nie zawierała funkcji <device>. O jeden interfejs API mniej do zmartwień :) <device> miał jednak 2 zalety: 1) był semantyczny i 2) można go było łatwo rozszerzyć, aby obsługiwał nie tylko urządzenia audio/wideo.

Weź oddech. Wszystko dzieje się bardzo szybko.

Runda 3. WebRTC

Element <device> ostatecznie odszedł w zapomnienie.

Prace nad znalezieniem odpowiedniego interfejsu API do przechwytywania przyspieszyły dzięki większemu zaangażowaniu w WebRTC (Web Real Time Communications). Specyfikację tę nadzoruje grupa robocza W3C WebRTC. Google, Opera, Mozilla i kilka innych firm mają swoje implementacje.

getUserMedia() jest powiązany z WebRTC, ponieważ jest bramą do tego zestawu interfejsów API. Umożliwia dostęp do lokalnego strumienia z kamery lub mikrofonu użytkownika.

Pomoc:

getUserMedia() jest obsługiwany od Chrome 21, Opery 18 i Firefoksa 17.

Pierwsze kroki

Dzięki navigator.mediaDevices.getUserMedia() możemy wreszcie korzystać z kamery internetowej i mikrofonu bez wtyczki. Dostęp do aparatu jest teraz możliwy przez wywołanie, a nie instalację. Jest ona wbudowana bezpośrednio w przeglądarkę. Brzmi dobrze?

Wykrywanie funkcji

Wykrywanie funkcji to proste sprawdzenie, czy istnieje navigator.mediaDevices.getUserMedia:

if (navigator.mediaDevices?.getUserMedia) {
  // Good to go!
} else {
  alert("navigator.mediaDevices.getUserMedia() is not supported");
}

Uzyskiwanie dostępu do urządzenia wejściowego

Aby korzystać z kamery internetowej lub mikrofonu, musimy poprosić o uprawnienia. Pierwszym parametrem funkcji navigator.mediaDevices.getUserMedia() jest obiekt określający szczegóły i wymagania dotyczące każdego typu multimediów, do których chcesz uzyskać dostęp. Jeśli na przykład chcesz uzyskać dostęp do kamery internetowej, pierwszym parametrem powinien być {video: true}. Aby używać zarówno mikrofonu, jak i kamery, przekaż {video: true, audio: true}:

<video autoplay></video>

<script>
  navigator.mediaDevices
    .getUserMedia({ video: true, audio: true })
    .then((localMediaStream) => {
      const video = document.querySelector("video");
      video.srcObject = localMediaStream;
    })
    .catch((error) => {
      console.log("Rejected!", error);
    });
</script>

OK. Co się zatem dzieje? Przechwytywanie multimediów to doskonały przykład współdziałania nowych interfejsów API HTML5. Działa on w połączeniu z innymi elementami HTML5, takimi jak <audio> i <video>. Zwróć uwagę, że nie ustawiamy atrybutu src ani nie dodajemy elementów <source> do elementu <video>. Zamiast podawać adres URL pliku multimedialnego, ustawiamy srcObject na obiekt LocalMediaStream reprezentujący kamerę internetową.

Mówię też <video>, żeby autoplay, bo inaczej obraz zatrzymałby się na pierwszej klatce. Dodanie controls również działa zgodnie z oczekiwaniami.

ustawianie ograniczeń multimediów (rozdzielczość, wysokość, szerokość);

Pierwszy parametr funkcji getUserMedia() może też służyć do określenia dodatkowych wymagań (lub ograniczeń) dotyczących zwracanego strumienia multimediów. Na przykład zamiast wskazywać, że chcesz mieć podstawowy dostęp do filmu (np. {video: true}), możesz dodatkowo wymagać, aby strumień był w jakości HD:

const hdConstraints = {
  video: { width: { exact:  1280} , height: { exact: 720 } },
};

const stream = await navigator.mediaDevices.getUserMedia(hdConstraints);
const vgaConstraints = {
  video: { width: { exact:  640} , height: { exact: 360 } },
};

const stream = await navigator.mediaDevices.getUserMedia(hdConstraints);

Więcej konfiguracji znajdziesz w interfejsie API ograniczeń.

Wybieranie źródła multimediów

Metoda enumerateDevices() interfejsu MediaDevices żąda listy dostępnych urządzeń wejściowych i wyjściowych multimediów, takich jak mikrofony, kamery, słuchawki itp. Zwrócony obiekt Promise jest rozwiązywany za pomocą tablicy obiektów MediaDeviceInfo opisujących urządzenia.

W tym przykładzie ostatni znaleziony mikrofon i kamera są wybierane jako źródło strumienia multimediów:

if (!navigator.mediaDevices?.enumerateDevices) {
  console.log("enumerateDevices() not supported.");
} else {
  // List cameras and microphones.
  navigator.mediaDevices
    .enumerateDevices()
    .then((devices) => {
      let audioSource = null;
      let videoSource = null;

      devices.forEach((device) => {
        if (device.kind === "audioinput") {
          audioSource = device.deviceId;
        } else if (device.kind === "videoinput") {
          videoSource = device.deviceId;
        }
      });
      sourceSelected(audioSource, videoSource);
    })
    .catch((err) => {
      console.error(`${err.name}: ${err.message}`);
    });
}

async function sourceSelected(audioSource, videoSource) {
  const constraints = {
    audio: { deviceId: audioSource },
    video: { deviceId: videoSource },
  };
  const stream = await navigator.mediaDevices.getUserMedia(constraints);
}

Zobacz świetną prezentację Sama Duttona, w której pokazuje, jak umożliwić użytkownikom wybór źródła multimediów.

Bezpieczeństwo

Po wywołaniu navigator.mediaDevices.getUserMedia() przeglądarki wyświetlają okno z prośbą o uprawnienia, w którym użytkownicy mogą przyznać lub odmówić dostępu do kamery lub mikrofonu. Oto na przykład okno uprawnień w Chrome:

Okno uprawnień w Chrome
Okno uprawnień w Chrome

Zapewnianie kreacji zastępczej

W przypadku użytkowników, którzy nie mają obsługi navigator.mediaDevices.getUserMedia(), jedną z opcji jest użycie istniejącego pliku wideo, jeśli interfejs API nie jest obsługiwany lub wywołanie nie powiedzie się z jakiegoś powodu:

if (!navigator.mediaDevices?.getUserMedia) {
  video.src = "fallbackvideo.webm";
} else {
  const stream = await navigator.mediaDevices.getUserMedia({ video: true });
  video.srcObject = stream;
}