Привет, друзья!
В данной шпаргалке представлены все основные интерфейсы и методы по работе с медиа в браузере, описываемые в следующих спецификациях:
- 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!
Aleksandr-JS-Developer
Прекрасная конструкция)
Спасибо за статью. Несмотря на вкусовые моменты, респект за хорошо выравненный код.