Skip to main content

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

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

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

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

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', 'speechSynthesis'].forEach(
(f) => {
if (!(f in window)) {
unsupportedFeatures.push(f)
}
}
)

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

return unsupportedFeatures
}

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

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

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

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

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