Как стать автором
Обновить
2121.48

Шпаргалка по работе с медиа в браузере

Время на прочтение19 мин
Количество просмотров12K



Привет, друзья!


В данной шпаргалке представлены все основные интерфейсы и методы по работе с медиа в браузере, описываемые в следующих спецификациях:



Шпаргалка представлена в форме вопрос-ответ.



Туториалы по теме:



Если вам это интересно, прошу под кат.


Содержание:



1. Как получить список медиаустройств пользователя?


Для получения списка медиаустройств пользователя предназначен метод enumerateDevices интерфейса MediaDevices объекта Navigator:


const devices = await navigator.mediaDevices.enumerateDevices()


Список моих устройств:





Свойство kind может использоваться для формирования требований (constraints) к медиапотоку (MediaStream) (далее — поток) (см. ниже), поэтому имеет смысл временно сохранять в браузере информацию о доступных устройствах пользователя:


const STORAGE_KEY = 'user_media_devices'

export async function enumerateDevices() {
  try {
    const devices = sessionStorage.getItem(STORAGE_KEY)
      ? JSON.parse(sessionStorage.getItem(STORAGE_KEY))
      : await navigator.mediaDevices.enumerateDevices()

    if (!sessionStorage.getItem(STORAGE_KEY)) {
      sessionStorage.setItem(STORAGE_KEY, JSON.stringify(devices))
    }

    return { devices }
  } catch (error) {
    return { error }
  }
}

Обработчик:


const stringify = (data) => JSON.stringify(data, null, 2)

const handleError = (e) => {
  console.error(e)
}

// <button id="enumerateDevicesBtn">Enumerate devices</button>
enumerateDevicesBtn.onclick = async () => {
  const { devices, error } = await enumerateDevices()
  if (error) return handleError(error)

  // <pre id="logBox"></pre>
  logBox.textContent = stringify(devices)
}

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


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


const constraints = await navigator.mediaDevices.getSupportedConstraints()


Список требований, поддерживаемых моим браузером (последняя версия Chrome):





Обратите внимание: в данном списке представлены не все требования, которые можно применять к потоку. Некоторые требования из списка относятся к категории "продвинутых" (advanced) и применяются несколько иначе, чем обычные. Многие требования являются экспериментальными и на сегодняшний день поддерживаются не в полной мере.


export async function getSupportedConstraints() {
  try {
    const constraints = await navigator.mediaDevices.getSupportedConstraints()
    return { constraints }
  } catch (error) {
    return { error }
  }
}

Обработчик:


// <button id="getSupportedConstraintsBtn">Get supported constraints</button>
getSupportedConstraintsBtn.onclick = async () => {
  const { constraints, error } = await getSupportedConstraints()
  if (error) return handleError(error)

  logBox.textContent = stringify(constraints)
}

3. Как захватить поток с устройств пользователя?


Для захвата потока с устройств пользователя используется метод getUserMedia:


const stream = await navigator.mediaDevices.getUserMedia(constraints?)


Данный метод принимает опциональные требования к потоку:



Дефолтные требования:


{ audio: true, video: true }

Пример кастомных требований:


export const DEFAULT_AUDIO_CONSTRAINTS = {
  echoCancellation: true,
  autoGainControl: true,
  noiseSuppression: true
}

export const DEFAULT_VIDEO_CONSTRAINTS = {
  width: 1920,
  height: 1080,
  frameRate: 60
}

getUserMedia возвращает поток с устройств пользователя:



Поток представляет собой коллекцию медиатреков (MediaStreamTrack) (далее — трек):



Поток предоставляет несколько методов для работы с треками:


  • getTracks — возвращает список медиатреков;
  • getAudioTracks — возвращает список аудиотреков;
  • getVideoTracks — возвращает список видеотреков;
  • addTrack — добавляет трек в поток;
  • removeTrack — удаляет трек из потока и др.

Обратите внимание: захваченный поток должен быть "одиночкой" (singleton). Это позволяет избежать повторного захвата и правильно останавливать захват.


let mediaStream

export async function getUserMedia(
  constraints = {
    audio: DEFAULT_AUDIO_CONSTRAINTS,
    video: DEFAULT_VIDEO_CONSTRAINTS
  }
) {
  try {
    const stream = mediaStream
      ? mediaStream
      : (mediaStream = await navigator.mediaDevices.getUserMedia(constraints))

    const tracks = stream.getTracks()
    const audioTracks = stream.getAudioTracks()
    const videoTracks = stream.getVideoTracks()

    return { stream, tracks, audioTracks, videoTracks }
  } catch (error) {
    return { error }
  }
}

Для прямой передачи потока в приемник (например, DOM-элемент video) используется свойство srcObject. Приемник должен иметь атрибуты autoplay и muted:


// <video id="streamReceiver" controls autoplay muted></video>
streamReceiver.srcObject = stream

Трек предоставляет такие методы, как:


  • getCapabilities — возвращает список возможностей (настроек), поддерживаемых треком;
  • getConstraints — возвращает список требований, примененных к треку;
  • getSettings — возвращает список требований и настроек, примененных к треку;
  • applyConstraints — применяет требования к треку;
  • stop — останавливает получение данных из источника трека и др.

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


// <button id="getUserMediaBtn">Get user media</button>
getUserMediaBtn.onclick = async () => {
  const { devices, error: devicesError } = await enumerateDevices()
  if (devicesError) return handleError(devicesError)

  let constraints
  if (devices.some((device) => device.kind === 'audioinput')) {
    constraints = { audio: DEFAULT_AUDIO_CONSTRAINTS }
  }
  if (devices.some((device) => device.kind === 'videoinput')) {
    constraints = { ...constraints, video: DEFAULT_VIDEO_CONSTRAINTS }
  }
  if (!constraints) {
    return handleError('User has no devices to capture.')
  }

  const { stream, tracks, error: mediaError } = await getUserMedia(constraints)
  if (mediaError) return handleError(mediaError)
  console.log('@stream', stream)

  streamReceiver.srcObject = stream

  const [firstTrack] = tracks
  console.log('@first track', firstTrack)

  const trackCapabilities = firstTrack.getCapabilities()
  const trackConstraints = firstTrack.getConstraints()
  const trackSettings = firstTrack.getSettings()

  logBox.textContent = stringify({
    trackCapabilities,
    trackConstraints,
    trackSettings
  })
}

Пример захваченного потока и первого трека:





Пример информации о треке:





4. Как захватить поток с экрана пользователя?


Для захвата потока с экрана пользователя предназначен метод getDisplayMedia:


const stream = await navigator.mediaDevices.getDisplayMedia(constraints?)


В целом, данный метод аналогичен методу getUserMedia, но поддерживает несколько дополнительных требований к потоку:



Пример дополнительных требований:


export const ADDITIONAL_VIDEO_CONSTRAINTS = {
  displaySurface: 'window',
  cursor: 'motion'
}

Обратите внимание: на сегодняшний день аудио при захвате экрана не поддерживается.


Функция для захвата экрана:


// поток должен быть одиночкой
let displayStream

export async function getDisplayMedia(
  constraints = {
    video: { ...DEFAULT_VIDEO_CONSTRAINTS, ...ADDITIONAL_VIDEO_CONSTRAINTS }
  }
) {
  try {
    const stream = displayStream
      ? displayStream
      : (displayStream = await navigator.mediaDevices.getDisplayMedia(constraints))

    const [tracks, audioTracks, videoTracks] = [
      stream.getTracks(),
      stream.getAudioTracks(),
      stream.getVideoTracks()
    ]

    return { stream, tracks, audioTracks, videoTracks }
  } catch (error) {
    return { error }
  }
}

Соответствующий обработчик:


// <button id="getDisplayMediaBtn">Get display media</button>
getDisplayMediaBtn.onclick = async () => {
  const { stream, tracks, error } = await getDisplayMedia()
  if (error) return handleError(error)
  console.log('@display stream', stream)

  streamReceiver.srcObject = stream

  const [firstTrack] = tracks
  console.log('@display first track', firstTrack)

  const [trackCapabilities, trackConstraints, trackSettings] = [
    firstTrack.getCapabilities(),
    firstTrack.getConstraints(),
    firstTrack.getSettings()
  ]

  logBox.textContent = stringify({
    trackCapabilities,
    trackConstraints,
    trackSettings
  })
}

Пример захваченного потока и первого трека:





Пример информации о треке:





5. Как захватить поток из DOM-элемента?


Для захвата потока из таких DOM-элементов, как audio, video и canvas используется метод captureStream интерфейса HTMLMediaElement или, соответственно, HTMLCanvasElement:


const stream = await mediaElement.captureStream()


При захвате потока из медиаэлемента имеет смысл проверять, во-первых, что переданный аргумент является медиаэлементом, во-вторых, готовность медиа к воспроизведению до конца без прерываний с помощью свойства readyState:


if (!(mediaElement instanceof HTMLMediaElement)) {
  throw new Error('Argument must be an instance of HTMLMediaElement.')
}
if (mediaElement.readyState !== 4) {
  throw new Error(
    'Media element has not enough data to be played through the end without interruption.'
  )
}

В случае с DOM-элементами может одновременно захватываться несколько потоков.


Функция для захвата потока из медиаэлемента:


let mediaElementStreams = []

export async function captureStreamFromMediaElement(mediaElement) {
  if (!(mediaElement instanceof HTMLMediaElement)) {
    throw new Error('Argument must be an instance of HTMLMediaElement.')
  }
  if (mediaElement.readyState !== 4) {
    throw new Error(
      'Media element has not enough data to be played through the end without interruption.'
    )
  }
  try {
    const stream = await mediaElement.captureStream()
    mediaElementStreams.push(stream)

    const [tracks, audioTracks, videoTracks] = [
      stream.getTracks(),
      stream.getAudioTracks(),
      stream.getVideoTracks()
    ]

    return { stream, tracks, audioTracks, videoTracks }
  } catch (error) {
    return { error }
  }
}

Соответствующий обработчик:


// <button id="getStreamFromMediaElementBtn">Get stream from media element</button>
getStreamFromMediaElementBtn.onclick = async () => {
  // <video id="videoEl" src="./assets/forest.mp4" controls></video>
  const { stream, tracks, error } = await captureStreamFromMediaElement(videoEl)
  if (error) return handleError(error)
  console.log('@media element stream', stream)

  streamReceiver.srcObject = stream

  const [firstTrack] = tracks
  console.log('@media element first track', firstTrack)

  const [trackCapabilities, trackConstraints, trackSettings] = [
    firstTrack.getCapabilities(),
    firstTrack.getConstraints(),
    firstTrack.getSettings()
  ]

  logBox.textContent = stringify({
    trackCapabilities,
    trackConstraints,
    trackSettings
  })
}

Пример захваченного потока и первого трека:





Пример информации о треке:





6. Как остановить захват потока?


Для остановки захвата потока необходимо прекратить получение данных из каждого его источника, т.е. трека. Для этого следует вызвать метод stop каждого трека:


export function stopTracks() {
  mediaStream?.getTracks().forEach((track) => {
    track.stop()
  })
  displayStream?.getTracks().forEach((track) => {
    track.stop()
  })
  for (const stream of mediaElementStreams) {
    stream?.getTracks().forEach((track) => {
      track.stop()
    })
  }
  mediaStream = null
  displayStream = null
  mediaElementStreams = []
}

7. Как захватить изображение из видеотрека?


Для захвата изображения из видеотрека (или кадра из холста) предназначен метод takePhoto интерфейса ImageCapture:


const imageCapture = new ImageCapture(videoTrack)
const blob = await imageCapture.takePhoto(photoSettings?)


Данный метод принимает опциональные настройки для фото:



Пример настроек для фото:


export const DEFAULT_PHOTO_SETTINGS = {
  imageHeight: 480,
  imageWidth: 640
}

К видеотреку можно применять дополнительные требования, связанные с захватом изображения:



Эти требования являются продвинутыми и применяются с помощью метода applyConstraints:


const advancedConstraints = {
  name: value
}
await videoTrack.applyConstraints({
  advanced: [advancedConstraints]
})

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


// эти требования относятся к видео
export const DEFAULT_PHOTO_CONSTRAINTS = {
  pan: true,
  tilt: true,
  zoom: true
}

Метод takePhoto возвращает объект Blob:



Экземпляр ImageCapture предоставляет следующие методы для получения списка возможностей и настроек для фото:


  • getPhotoCapabilities — возвращает список возможностей для фото;
  • getPhotoSettings — возвращает список настроек для фото.

Функция для получения возможностей и настроек для фото:


export async function getPhotoCapabilitiesAndSettings(videoTrack) {
  const imageCapture = new ImageCapture(videoTrack)
  console.log('@image capture', imageCapture)

  try {
    const [photoCapabilities, photoSettings] = await Promise.all([
      imageCapture.getPhotoCapabilities(),
      imageCapture.getPhotoSettings()
    ])

    return { photoCapabilities, photoSettings }
  } catch (error) {
    return { error }
  }
}

Соответствующий обработчик:


// <button id="getPhotoCapabilitiesAndSettingsBtn">Get photo capabilities and settings</button>
getPhotoCapabilitiesAndSettingsBtn.onclick = async () => {
  const { videoTracks, error: mediaError } = await getUserMedia()
  if (mediaError) return handleError(mediaError)

  const [firstVideoTrack] = videoTracks

  const {
    photoCapabilities,
    photoSettings,
    error: photoError
  } = await getPhotoCapabilitiesAndSettings(firstVideoTrack)
  if (photoError) return handleError(photoError)

  logBox.textContent = stringify({
    photoCapabilities,
    photoSettings
  })
}

Пример возможностей и настроек для фото:





Функция для захвата изображения из видеотрека:


export async function takePhoto({
  videoTrack,
  photoSettings = DEFAULT_PHOTO_SETTINGS
}) {
  const imageCapture = new ImageCapture(videoTrack)

  try {
    const blob = await imageCapture.takePhoto(photoSettings)
    return { blob }
  } catch (error) {
    return { error }
  }
}

Соответствующий обработчик:


// <button id="takePhotoBtn">Take photo</button>
takePhotoBtn.onclick = async () => {
  const { videoTracks, error: mediaError } = await getUserMedia({
    video: { ...DEFAULT_VIDEO_CONSTRAINTS, ...DEFAULT_PHOTO_CONSTRAINTS }
  })
  if (mediaError) return handleError(mediaError)

  const [videoTrack] = videoTracks

  // здесь мы можем применять к треку дополнительные требования
  // await videoTrack.applyConstraints({
  //   advanced: [advancedConstraints]
  // })

  const { blob, error: photoError } = await takePhoto({ videoTrack })
  if (photoError) return handleError(photoError)

  // <img id="imgBox" alt="" />
  const imgSrc = URL.createObjectURL(blob)
  imgBox.src = imgSrc
  // imgBox.addEventListener(
  //   'load',
  //   () => {
  //     URL.revokeObjectURL(imgSrc)
  //   },
  //   { once: true }
  // )
}

Ссылка на источник изображения формируется с помощью метода URL.createObjectURL. Метод URL.revokeObjectURL должен вызываться во избежание утечек памяти, но при его вызове после загрузки изображения, как в приведенном примере, изображение невозможно будет скачать.



8. Как записать поток?


Для записи потока предназначен интерфейс MediaRecorder:



const mediaRecorder = new MediaRecorder(mediaStream, options?)

Конструктор MediaRecorder принимает поток и опциональный объект с настройками, наиболее важной из которых является настройка mimeType — тип создаваемой записи.


Экземпляр MediaRecorder предоставляет следующие методы для управления записью:


  • start(timeslice?) — запускает запись. Данный метод принимает опциональный параметр timeslice — время вызова события dataavailable (см. ниже);
  • pause — приостанавливает запись;
  • resume — продолжает запись;
  • stop — останавливает запись.

В процессе записи возникает ряд событий, наиболее важным из которых является dataavailable. Обработчик этого события принимает объект, содержащий свойство data, в котором находятся части (chunks) записанных данных в виде Blob:


let mediaDataChunks = []

mediaRecorder.ondatavailable = ({ data }) => {
  mediaDataChunks.push(data)
}

Интерфейс MediaRecorder позволяет проверять поддержку типа создаваемой записи с помощью метода isTypeSupported.


Предположим, что мы хотим записать экран пользователя со звуком. Поток экрана будет содержать только видео. Поэтому нам необходимо получить видеотреки экрана и аудиотреки микрофона и объединить их в один поток. Это можно сделать при помощи конструктора MediaStream:


export const createNewStream = (tracks) => new MediaStream(tracks)

Данный конструктор принимает треки в виде массива.


Функция для начала записи:


const DEFAULT_RECORD_MIME_TYPE = 'video/webm'
const DEFAULT_RECORD_TIMESLICE = 250

// лучше, чтобы `mediaRecorder` был одиночкой
let mediaRecorder
let mediaDataChunks = []

export async function startRecording({
  mediaStream,
  mimeType,
  timeslice = DEFAULT_RECORD_TIMESLICE,
  ...restOptions
}) {
  if (mediaRecorder) return

  mediaRecorder = new MediaRecorder(mediaStream, {
    mimeType: MediaRecorder.isTypeSupported(mimeType)
      ? mimeType
      : DEFAULT_RECORD_MIME_TYPE,
    ...restOptions
  })
  console.log('@media recorder', mediaRecorder)

  mediaRecorder.onerror = ({ error }) => {
    return error
  }

  mediaRecorder.ondataavailable = ({ data }) => {
    mediaDataChunks.push(data)
  }

  mediaRecorder.start(timeslice)
}

Соответствующий обработчик:


// <button id="startRecordingBtn">Start recording</button>
startRecordingBtn.onclick = async () => {
  const { devices, error: devicesError } = await enumerateDevices()
  if (devicesError) return handleError(devicesError)

  // мы готовы записывать экран без звука
  let _audioTracks = []
  if (devices.some(({ kind }) => kind === 'audioinput')) {
    const { audioTracks, error: mediaError } = await getUserMedia()
    if (mediaError) return handleError(mediaError)

    _audioTracks = audioTracks
  }

  const { videoTracks, error: displayError } = await getDisplayMedia()
  if (displayError) return handleError(displayError)

  const mediaStream = createNewStream([..._audioTracks, ...videoTracks])
  streamReceiver.srcObject = mediaStream

  // ждем возможную ошибку
  const recordError = await startRecording({ mediaStream })
  if (recordError) return handleError(recordError)
}

Пример "записывателя":





Функция приостановки/продолжения записи:


// в таких случаях удобно использовать `IIFE` и замыкание
export const pauseOrResumeRecording = (function () {
  let paused = false

  return function () {
    if (!mediaRecorder) return

    paused ? mediaRecorder.resume() : mediaRecorder.pause()
    paused = !paused

    return paused
  }
})()

Обработчик:


// <button id="pauseOrResumeRecordingBtn">Pause/Resume recording</button>
pauseOrResumeRecordingBtn.onclick = () => {
  const paused = pauseOrResumeRecording()
  console.log('@recording paused', paused)
}

Функция остановки записи:


export function stopRecording() {
  if (!mediaRecorder) return

  mediaRecorder.stop()

  const _mediaDataChunks = mediaDataChunks
  console.log('@media data chunks', _mediaDataChunks)

  // очитка
  // Явное удаление обработчика события `dataavailable`
  // обеспечивает возможность повторной записи
  mediaRecorder.ondataavailable = null
  mediaRecorder = null
  mediaDataChunks = []

  return _mediaDataChunks
}

Обработчик:


// <button id="stopRecordingBtn">Stop recording</button>
stopRecordingBtn.onclick = () => {
  const chunks = stopRecording()

  const blob = new Blob(chunks, {
    type: DEFAULT_RECORD_MIME_TYPE
  })

  // если необходимо создать файл, например, для передачи на сервер
  // https://w3c.github.io/FileAPI/#file-section
  // const file = new File(
  //   chunks,
  //   `new-record-${Date.now()}.${DEFAULT_RECORD_MIME_TYPE.split('/').at(-1)}`,
  //   {
  //     type: DEFAULT_RECORD_MIME_TYPE
  //   }
  // )

  // <video id="recordBox" controls></video>
  recordBox.src = URL.createObjectURL(blob)
  // в данном случае проблем со скачиванием файла не возникает
  URL.revokeObjectURL(blob)

  stopTracks()
}

Пример частей данных:





9. Как преобразовать текст в речь?



Для преобразования текста в речь предназначен интерфейс SpeechSynthesis:



Данный интерфейс является свойством глобального объекта window (window.speechSynthesis).


Для озвучивания текста применяются голоса (voices), доступные в браузере. Для получения их списка используется метод getVoices:


const voices = speechSynthesis.getVoices()

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


let voices = speechSynthesis.getVoices()

speechSynthesis.onvoiceschanged = () => {
  voices = speechSynthesis.getVoices()
}

speechSynthesis предоставляет следующие методы для озвучивания текста:


  • start(utterance) — запуск озвучивания;
  • pause — приостановка озвучивания;
  • resume — продолжение озвучивания;
  • cancel — отмена (остановка) озвучивания.

Метод start принимает экземпляр SpeechSynthesisUtterance:


const utterance = new SpeechSynthesisUtterance(text?)


Данный конструктор принимает опциональный текст для озвучивания.


utterance имеет несколько сеттеров для настройки озвучивания:


  • text — текст для озвучивания;
  • lang — язык озвучивания;
  • voice — голос для озвучивания и др.

Предположим, что у нас имеется такой текст:


<textarea id="textToSpeech" rows="4">
Мы — источник веселья и скорби рудник.
Мы — вместилище скверны и чистый родник.
Человек, словно в зеркале мир, — многолик.
Он ничтожен — и он же безмерно велик!
</textarea>

Функция для озвучивания этого текста голосом от Google:


// голос для озвучивания
let voiceFromGoogle
// индикатор начала озвучивания
let speakingStarted

export function startSpeechSynthesis() {
  if (voiceFromGoogle) return speak()

  speechSynthesis.getVoices()

  speechSynthesis.onvoiceschanged = () => {
    const voices = speechSynthesis.getVoices()
    console.log('@voices', voices)

    voiceFromGoogle = voices.find((voice) => voice.name === 'Google русский')

    speak()
  }
}

function speak() {
  const trimmedText = textToSpeech.value.trim()
  if (!trimmedText) return

  const utterance = new SpeechSynthesisUtterance(trimmedText)
  utterance.lang = 'ru-RU'
  utterance.voice = voiceFromGoogle
  console.log('@utterance', utterance)

  speechSynthesis.speak(utterance)
  speakingStarted = true

  utterance.onend = () => {
    speakingStarted = false
  }
}

Соответствующий обработчик:


// <button id="startSpeechSynthesisBtn">Start speech synthesis</button>
startSpeechSynthesisBtn.onclick = () => {
  startSpeechSynthesis()
}

Пример списка голосов:





Пример "высказывания":





Функция для приостановки/продолжения озвучивания:


// индикатор озвучивания `speechSynthesis.speaking` в настоящее время работает некорректно
export const pauseOrResumeSpeaking = (function () {
  let paused = false

  return function () {
    if (!speakingStarted) return

    paused ? speechSynthesis.resume() : speechSynthesis.pause()
    paused = !paused

    return paused
  }
})()

Обработчик:


// <button id="pauseOrResumeSpeakingBtn">Pause/resume speaking</button>
pauseOrResumeSpeakingBtn.onclick = () => {
  const paused = pauseOrResumeSpeaking()
  console.log('@speaking paused', paused)
}

Функция для остановки озвучивания:


export function stopSpeaking() {
  speechSynthesis.cancel()
}

Обработчик:


// <button id="stopSpeakingBtn">Stop speaking</button>
stopSpeakingBtn.onclick = () => {
  stopSpeaking()
}

10. Как преобразовать речь в текст?


Для преобразования речи в текст предназначен интерфейс SpeechRecognition:


// рекомендованный подход
const speechRecognition =
  window.SpeechRecognition || window.webkitSpeechRecognition
const recognition = new speechRecognition()


recognition имеет несколько сеттеров для настройки распознавания речи:


  • lang — язык для распознавания;
  • continuous — определяет, продолжается ли распознавание после получения первого "финального" результата;
  • interimResults — определяет, обрабатываются ли "промежуточные" результаты распознавания;
  • maxAlternatives — определяет максимальное количество вариантов распознанного слова, возвращаемых браузером. Варианты возвращаются в виде массива, первым элементом которого является наиболее подходящее с точки зрения браузера слово.

Методы для управления распознаванием, предоставляемые recognition:


  • start — запуск распознавания;
  • stop — остановка распознавания;
  • abort — прекращение распознавания.

При распознавании речи браузером происходит следующее:


  • при вызове метода start браузер начинает нас "слушать";
  • каждое сказанное слово регистрируется как отдельная сущность — массив, содержащий несколько (в зависимости от настройки maxAlternatives) вариантов этого слова;
  • регистрация слова приводит к возникновению события result;
  • регистрируемые сущности являются промежуточными (interim) результатами распознавания;
  • по истечении некоторого времени (определяемого браузером) после того, как мы замолчали, промежуточный результат переводится в статус финального (final);
  • снова возникает событие result: значением свойства isFinal результата является true;
  • после регистрации финального результата возникает событие end;
  • если настройка continuous имеет значение false, распознавание завершится после регистрации первого слова;
  • если настройка interimResults имеет значение false, результаты будут сразу регистрироваться как финальные;
  • событие result имеет свойство resultIndex, значением которого является индекс последнего обработанного результата.

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


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


<div class="speech-to-text-wrapper">
  <input type="text" id="interimTranscriptBox" />
  <textarea id="finalTranscriptBox" rows="4"></textarea>
</div>

Для решения проблемы, связанной с пунктуацией, нам потребуется такой словарь:


const DICTIONARY = {
  точка: '.',
  запятая: ',',
  вопрос: '?',
  восклицание: '!',
  двоеточие: ':',
  тире: '-',
  абзац: '\n',
  отступ: '\t'
}

А для решения проблемы, связанной с редактированием, такие функции:


// заменяем слова на знаки препинания
const editInterim = (s) => s
  .split(' ')
  .map((word) => {
    word = word.trim()
    return DICTIONARY[word.toLowerCase()]
      ? DICTIONARY[word.toLowerCase()]
      : word
  })
  .join(' ')

// удаляем лишние пробелы
const editFinal = (s) => s.replace(/\s{1,}([\.+,?!:-])/g, '$1')

Функция для распознавания речи:


// экземпляр "распознавателя"
let recognition
// индикатор начала распознавания
let recognitionStarted
// финальный результат
let finalTranscript

// функция очистки
function resetRecognition() {
  recognition = null
  recognitionStarted = false
  finalTranscript = ''
  interimTranscriptBox.value = ''
  finalTranscriptBox.value = ''
}

export function startSpeechRecognition() {
  resetRecognition()

  recognition = new speechRecognition()
  // настройки распознавания
  recognition.continuous = true
  recognition.interimResults = true
  recognition.maxAlternatives = 3
  recognition.lang = 'ru-RU'
  console.log('@recognition', recognition)

  recognition.start()
  recognitionStarted = true

  recognition.onend = () => {
    // Повторно запускаем распознавание, если
    // соответствующий индикатор имеет значение `true`
    if (!recognitionStarted) return
    recognition.start()
  }

  recognition.onresult = (e) => {
    // Промежуточные результаты обновляются на каждом цикле распознавания
    let interimTranscript = ''
    // Перебираем результаты с того места, на котором остановились в прошлый раз
    for (let i = e.resultIndex; i < e.results.length; i++) {
      // Атрибут `isFinal` является индикатором того, что речь закончилась (мы перестали говорить)
      if (e.results[i].isFinal) {
        // Редактируем промежуточный результат
        const interimResult = editInterim(e.results[i][0].transcript)
        // и добавляем его к финальному
        finalTranscript += interimResult
      } else {
        // В противном случае, записываем распознанное слово в промежуточный результат
        interimTranscript += e.results[i][0].transcript
      }
    }
    // Записываем промежуточный результат в `input`
    interimTranscriptBox.value = interimTranscript
    // Редактируем финальный результат
    finalTranscript = editFinal(finalTranscript)
    // и записываем его в `textarea`
    finalTranscriptBox.value = finalTranscript
  }
}

Соответствующий обработчик:


// <button id="startSpeechRecognitionBtn">Start speech synthesis</button>
startSpeechRecognitionBtn.onclick = () => {
  startSpeechRecognition()
}

Пример "распознавателя":





Функция остановки распознавания:


export function stopRecognition() {
  if (!recognition) return
  recognition.stop()
  recognitionStarted = false
}

Обработчик:


// <button id="stopRecognitionBtn">Stop recognition</button>
stopRecognitionBtn.onclick = () => {
  stopRecognition()
}

Функция прекращения распознавания:


export function abortRecognition() {
  if (!recognition) return
  recognition.abort()
  resetRecognition()
}

Обработчик:


// <button id="abortRecognitionBtn">Abort recognition</button>
abortRecognitionBtn.onclick = () => {
  abortRecognition()
}

11. Как определить поддержку возможностей по работе с медиа браузером?


Функция для определения возможностей браузера по работе с медиа, рассмотренных в данной шпаргалке:


export function verifySupport() {
  const unsupportedFeatures = []

  if (!('mediaDevices' in navigator)) {
    unsupportedFeatures.push('mediaDevices')
  }

  if (
    !('captureStream' in HTMLAudioElement.prototype) &&
    !('mozCaptureStream' in HTMLAudioElement.prototype)
  ) {
    unsupportedFeatures.push('captureStream')
  }

  ;['MediaStream', 'MediaRecorder', 'Blob', 'File', 'ImageCapture'].forEach(
    (f) => {
      if (!(f in window)) {
        unsupportedFeatures.push(f)
      }
    }
  )

  if (!('speechSynthesis' in window)) {
    unsupportedFeatures.push('speechSynthesis')
  }

  if (
    !('SpeechRecognition' in window) &&
    !('webkitSpeechRecognition' in window)
  ) {
    unsupportedFeatures.push('SpeechRecognition')
  }

  return unsupportedFeatures
}

Пример использования этой функции:


const unsupportedFeatures = verifySupport()
if (unsupportedFeatures.length) {
  console.error(unsupportedFeatures)
}

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


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



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



Благодарю за внимание и happy coding!




Теги:
Хабы:
Всего голосов 13: ↑13 и ↓0+13
Комментарии1

Публикации

Информация

Сайт
timeweb.cloud
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия
Представитель
Timeweb Cloud