
Привет, друзья!
В данной шпаргалке представлены все основные интерфейсы и методы по работе с медиа в браузере, описываемые в следующих спецификациях:
- Media Capture and Streams
- Screen Capture
- Media Capture from DOM Elements
- MediaStream Image Capture
- MediaStream Recording
- Web Speech API
Шпаргалка представлена в форме вопрос-ответ.
Туториалы по теме:
- JavaScript: разрабатываем приложение для записи звука
- JavaScript: разрабатываем приложение для записи экрана
- JavaScript: захват медиапотока из DOM элементов
- JavaScript: делаем селфи с помощью браузера
- JavaScript: разрабатываем чат с помощью Socket.io, Express и React с акцентом на работе с медиа
Если вам это интересно, прошу под кат.
Содержание:
- 1. Как получить список медиаустройств пользователя?
- 2. Как получить список требований к потоку, поддерживаемых браузером?
- 3. Как захватить поток с устройств пользователя?
- 4. Как захватить поток с экрана пользователя?
- 5. Как захватить поток из DOM-элемента?
- 6. Как остановить захват потока?
- 7. Как захватить изображение из видеотрека?
- 8. Как записать поток?
- 9. Как преобразовать текст в речь?
- 10. Как преобразовать речь в текст?
- 11. Как определить поддержку возможностей по работе с медиа браузером?
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!

