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. Geolokalizacja (GPS), interfejs Orientation API (przyspieszeniomierz), WebGL (procesor graficzny) i interfejs Web Audio API (sprzęt audio) to doskonałe przykłady. Te funkcje są niesamowicie wydajne i oferują 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 aparatu i mikrofonu użytkownika.

Droga do getUserMedia()

Jeśli nie znasz historii, to jak powstał interfejs API getUserMedia(), to ciekawa historia.

W ciągu ostatnich kilku lat powstało kilka wersji interfejsów API do przechwytywania multimediów. Wielu ludzi dostrzegło potrzebę zapewnienia dostępu do natywnych urządzeń w internecie, ale to doprowadziło do stworzenia nowej specyfikacji. Sprawy skomplikowały się do tego stopnia, że W3C postanowiło w końcu utworzyć grupę roboczą. Czy jest to ich jedyny cel? Zrozumieć szaleństwo Grupa robocza ds. zasad dotyczących interfejsów Device API (DAP) ma za zadanie skonsolidować i ujednolicić liczne propozycje.

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">

Fajnie, prawda? Podoba mi się zwłaszcza to, że można ponownie użyć pliku wejściowego. Semantycznie ma to sens. 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 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 iOS 6 (obsługa częściowa)

Runda 2. Element urządzenia

Wiele osób uważało, że HTML Media Capture jest zbyt ograniczony, więc pojawiła się nowa specyfikacja, która obsługuje dowolny typ (przyszłego) urządzenia. Nie dziwi, że projekt wymagał wprowadzenia nowego elementu, czyli elementu <device>, który stał się poprzednikiem elementu getUserMedia().

Opera była jedną z pierwszych przeglądarek, które wprowadziły pierwsze implementacje przechwytywania wideo.<device> Niedługo potem (dokładnie tego samego dnia) WhatWG zdecydowało się zrezygnować z tagu <device> na rzecz innego, obiecującego interfejsu 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. To wszystko dzieje się bardzo szybko.

Runda 3: WebRTC

Element <device> ostatecznie zniknął.

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.

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ł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. Dostęp do aparatu jest teraz dostępny po jednym kliknięciu, a nie po instalacji. Jest ona wbudowana bezpośrednio w przeglądarkę. Już się cieszysz?

Wykrywanie cech

Wykrywanie funkcji to prosta kontrola istnienia 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 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. Co się tutaj dzieje? 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, 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. Dodanie controls też działa 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 różnych konfiguracjach znajdziesz w interfejsie API ograniczeń.

Wybieranie źródła multimediów

Metoda enumerateDevices() interfejsu MediaDevices wysyła żądanie listy dostępnych urządzeń wejściowych i wyjściowych multimediów, takich jak mikrofony, kamery, zestawy słuchawkowe itp. Zwracana 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 prezentację Sama Duttona, w której pokazuje on, 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 z prośbą o dostęp do aparatu i mikrofonu, w którym użytkownicy mogą zezwolić na dostęp lub go odmówić. Oto przykład:

Okno z uprawnieniami w Chrome
Okno uprawnień w Chrome

Podanie wartości zastępczej

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

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