Przechwytywanie dźwięku i obrazu w HTML5

Od dawna przechwytywanie dźwięku i obrazu było celem, który stanowił „świętą grotę” programowania stron internetowych. Przez wiele lat musieliśmy polegać na wtyczkach do przeglądarki (Flash lub Silverlight). No dalej!

HTML5 na ratunek. Może się to wydawać niejasne, ale rozwój HTML5 spowodował gwałtowny wzrost dostępu do sprzętu na urządzeniach. Idealne są np. geolokalizacja (GPS), Orientation API (akcelerometr), WebGL (GPU) czy Web Audio API (sprzęt audio). Te funkcje są niezwykle zaawansowane i przedstawiają ogólne interfejsy API JavaScript, które leżą nad podstawowymi możliwościami sprzętowymi systemu.

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

Droga do getUserMedia()

Jeśli nie znasz historii, warto dowiedzieć się, jak powstał interfejs API getUserMedia().

W ciągu ostatnich kilku lat powstało kilka wersji interfejsów API do przechwytywania multimediów. Wiele osób wiedziało, że trzeba mieć dostęp do urządzeń natywnych w internecie, ale to skłoniło wszystkich do wspólnego opracowania specyfikacji. Sprawy stały się tak skomplikowane, że W3C w końcu postanowiło utworzyć grupę roboczą. Czy jest to ich jedyny cel? Opanuj szaleństwo! Grupa robocza ds. interfejsów API urządzeń (DAP) otrzymała zadanie skonsolidowania i ujednolicenia wielu propozycji.

Spróbuję podsumować, co działo się w 2011 r.

Runda 1. Przechwytywanie multimediów w HTML

HTML Media Capture to pierwsza próba ujednolicenia rejestrowania multimediów w internecie. Polega ono na przeciążeniu parametru <input type="file"> i dodaniu nowych wartości dla parametru accept.

Jeśli chcesz, aby użytkownicy mogli robić sobie zdjęcia za pomocą kamery internetowej, 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">

Nieźle, prawda? Podoba mi się zwłaszcza to, że można ponownie użyć pliku wejściowego. Z punktu widzenia semantyki jest to bardzo sensowne. Ta konkretna „interfejs API” nie umożliwia tworzenia efektów w czasie rzeczywistym (np. renderowania danych z kamery internetowej na żywo do <canvas> i stosowania filtrów WebGL). Za pomocą HTML Media Capture możesz tylko nagrywać plik multimedialny lub robić zrzuty ekranu w określonym momencie.

Pomoc:

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

Runda 2. Element urządzenia

Wielu użytkowników uważało, że HTML Media Capture jest za zbyt restrykcyjne, i opracowano nową specyfikację obsługującą dowolne (przyszłe) urządzenia. Nic dziwnego, że projekt wymagał dodania nowego elementu – elementu <device>, który stał się poprzednikiem getUserMedia().

Opera była jedną z pierwszych przeglądarek, które wprowadziły <device>pierwsze implementacje<device> przechwytywania wideo. Niedługo potem (dokładnie tego samego dnia) zespół WhatWG zdecydował się usunąć tag <device> z kolejnego i zastępować go kolejnym, tym razem interfejs API JavaScript o nazwie navigator.getUserMedia(). Tydzień później Opera opublikowała nowe wersje, które zawierały obsługę zaktualizowanej specyfikacji getUserMedia(). W tym samym roku Microsoft dołączył do tej grupy, wydając Lab dla IE9, który obsługuje nową specyfikację.

Oto jak wyglądałaby <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 <device>. Mamy o jeden interfejs API mniej, o którym trzeba się martwić :) <device> miał jednak 2 zalety: 1. był semantyczny i 2. można go było łatwo rozszerzyć, aby obsługiwał więcej niż tylko urządzenia audio/wideo.

Weź oddech. Te informacje szybko się poruszają.

Runda 3. WebRTC

Element <device> w końcu stał się celem Dodo.

Tempo znajdowania odpowiedniego interfejsu API do przechwytywania przyspieszyło dzięki większemu zaangażowaniu w rozwój WebRTC (Web Real-Time Communications). Specyfikacja jest nadzorowana przez grupę roboczą W3C WebRTC. Google, Opera, Mozilla i kilka innych mają implementacje.

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

Pomoc:

getUserMedia() jest obsługiwana od wersji 21 przeglądarki Chrome, wersji 18 przeglądarki Opera i wersji 17 przeglądarki Firefox.

Pierwsze kroki

Dzięki navigator.mediaDevices.getUserMedia() możemy w końcu korzystać z kamery internetowej i mikrofonu bez wtyczki. Aby korzystać z kamery, wystarczy telefon, a nie instalacja. Jest ona wbudowana bezpośrednio w przeglądarkę. Już się cieszysz?

Wykrywanie cech

Wykrywanie cech pozwala w prosty sposób sprawdzić, czy navigator.mediaDevices.getUserMedia istnieje:

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 pozwolenie. Pierwszy parametr funkcji navigator.mediaDevices.getUserMedia() to 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 powinna być {video: true}. Aby używać zarówno mikrofonu, jak i kamery, przejdź do {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. O co tu chodzi? Przechwytywanie multimediów to doskonały przykład współpracy nowych interfejsów API HTML5. Działa ona w połączeniu z innymi elementami HTML5, takimi jak <audio><video>. Zwróć uwagę, że nie ustawiamy atrybutu src ani nie uwzględniamy elementów <source> w elemencie <video>. Zamiast podawać adres URL pliku multimedialnego filmu, ustawiamy parametr srcObject w obiekcie LocalMediaStream reprezentującym kamerę internetową.

Ustawiam też <video> na autoplay, bo inaczej obraz byłby zamrożony na pierwszej klatce. Dodawanie elementu controls również przebiega zgodnie z oczekiwaniami.

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

Pierwszy parametr getUserMedia() może też służyć do określania dodatkowych wymagań (lub ograniczeń) dotyczących zwracanego strumienia multimediów. Zamiast wskazywać, że chcesz uzyskać 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 informacji o konfiguracjach znajdziesz w interfejsie API ograniczeń.

Wybieranie źródła multimediów

Metoda enumerateDevices() interfejsu MediaDevices wysyła żądanie o listę dostępnych urządzeń wejściowych i wyjściowych multimediów, takich jak mikrofony, kamery, zestawy słuchawkowe itp. Zwrócona obietnica jest rozwiązywana za pomocą tablicy obiektów MediaDeviceInfo opisujących urządzenia.

W tym przykładzie jako źródło strumienia multimediów wybrano ostatni znaleziony mikrofon i kamerę:

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);
}

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

Bezpieczeństwo

Po wywołaniu funkcji navigator.mediaDevices.getUserMedia() przeglądarki wyświetlają okno uprawnień, które pozwala użytkownikom przyznać lub odmówić dostępu do kamery/mikrofonu. Oto przykład okna uprawnień w Chrome:

Okno uprawnień w Chrome
Okno uprawnień w Chrome

Podanie wartości zastępczej

W przypadku użytkowników, którzy nie obsługują navigator.mediaDevices.getUserMedia(), a gdy interfejs API nie jest obsługiwany lub wywołanie się nie uda, może użyć istniejącego pliku wideo:

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