Media Capture and Streams API (он же MediaStream API) позволяет записывать аудио с микрофона пользователя, а затем получать записанные аудио или мультимедийные элементы в виде дорожек. Затем вы можете воспроизвести эти треки сразу после их записи или загрузить медиафайл на свой сервер.
В этом руководстве мы создадим
Вы можете найти полный код этого руководства в этом репозитории GitHub.
Настройка сервера
Сначала мы начнем с создания сервера Node.js и Express. Поэтому сначала обязательно загрузите и установите Node.js, если его нет на вашем компьютере.
Создать каталог
Создайте новый каталог, в котором будет храниться проект, и перейдите в этот каталог:
mkdir
cd
Инициализировать проект
Затем инициализируйте проект с помощью npm:
npm init -y
Опция -yсоздается package.jsonсо значениями по умолчанию.
Установите зависимости
Затем мы установим Express для создаваемого нами сервера и nodemon для перезапуска сервера при любых изменениях:
npm i express nodemon
Создайте
Теперь мы можем начать с создания простого сервера. Создать index.jsв корне проекта со следующим содержимым:
const path = require ('path’) ;
const express = require ('express’) ;
const app = express () ;
const port = process.env.PORT || 3000;
app.use (express.static ('public/assets’));
app.listen (port, () => {
console.log (`App listening at http: //localhost: ${port}`) ;
}) ;
Это создает сервер, который будет работать на порту 3000, если порт не установлен в среде, и предоставляет каталог public/assets— который мы скоро создадим — который будет содержать файлы и изображения JavaScript и CSS.
Добавить скрипт
Наконец, добавьте startскрипт в scriptsin package.json:
«scripts»: {
«start»: «nodemon index.js»
},
Запустите
Давайте протестируем наш сервер. Выполните следующее, чтобы запустить сервер:
npm start
И сервер должен запускаться с порта 3000. Вы можете попробовать получить к нему доступ localhost:3000через, но вы увидите сообщение «Cannot GET /», поскольку у нас еще нет определенных маршрутов.
Создание страницы записи
Далее мы создадим страницу, которая будет главной страницей сайта. Пользователь будет использовать эту страницу для записи, просмотра и воспроизведения записей.
Создайте publicкаталог и внутри него создайте index.htmlфайл со следующим содержимым:
<! DOCTYPE html>
http-equiv="X-UA-Compatible" content="IE=edge">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
Record Your Voice
Эта страница использует Bootstrap 5 для стилизации. На данный момент на странице просто отображается кнопка, которую пользователь может использовать для записи.
Обратите внимание, что мы используем изображение для микрофона. Скачать иконку можно на Iconscout, а можно использовать модифицированную версию в репозитории GitHub.
Загрузите иконку и поместите ее внутрь public/assets/imagesс названием microphone.png.
Добавление стилей
Мы также связываем таблицу стилей index.css, поэтому создайте public/assets/css/index.cssфайл со следующим содержимым:
.
height: 8em;
width: 8em;
}
.
}
Создание маршрута
Наконец, нам просто нужно добавить новый маршрут в index.js. Добавьте следующее перед app.listen:
app.get ('/', (req, res) => {
res.sendFile (path.join (__dirname, 'public/index.html’));
}) ;
Если сервер еще не запущен, запустите сервер с расширением npm start. Затем зайдите localhost:3000в свой браузер. Вы увидите кнопку записи.
Страница записи
Кнопка пока ничего не делает. Нам нужно привязать событие клика, которое вызовет запись.
Создайте public/assets/js/record.jsфайл со следующим содержимым:
//initialize elements we’ll use
const recordButton = document.getElementById ('recordButton’) ;
const recordButtonImage = recordButton.firstElementChild;
let chunks = []; //will be used later to record audio
let mediaRecorder = null; //will be used later to record audio
let audioBlob = null; //the blob that will hold the recorded audio
Мы инициализируем переменные, которые будем использовать позже. Затем создайте recordфункцию, которая будет прослушивателем событий для события click recordButton:
function record () {
//TODO start recording
}
recordButton.addEventListener ('click’, record) ;
Мы также прикрепляем эту функцию в качестве прослушивателя событий к кнопке записи.
Медиазапись
Чтобы начать запись, нам нужно использовать метод mediaDevices.getUserMedia ().
Этот метод позволяет нам получать поток и записывать аудио и/или видео пользователя только после того, как пользователь предоставит
getUserMediaпринимает в качестве параметра объект MediaStreamConstraints, который содержит набор ограничений, определяющих ожидаемые типы мультимедиа в потоке, который мы получим из getUserMedia. Эти ограничения могут быть либо аудио, либо видео с логическими значениями.
Если значение равно false, это означает, что мы не заинтересованы в доступе к этому устройству или записи этого носителя.
getUserMediaвозвращает обещание. Если пользователь разрешает
Захват мультимедиа и потоки
Чтобы использовать объекты API MediaStream для захвата дорожек мультимедиа, нам нужно использовать интерфейс MediaRecorder. Нам нужно создать новый объект интерфейса, который принимает объект MediaStream в конструкторе и позволяет нам легко управлять записью через его методы.
Внутри recordфункции добавьте следующее:
//check if browser supports getUserMedia
if (! navigator.mediaDevices ||! navigator.mediaDevices.getUserMedia) {
alert ('Your browser does not support recording!') ;
return;
}
// browser supports getUserMedia
// change image in button
recordButtonImage.src = `/images/${mediaRecorder && mediaRecorder.state === 'recording’? 'microphone’: 'stop’}.png`;
if (! mediaRecorder) {
// start recording
navigator.mediaDevices.getUserMedia ({
audio: true,
})
.then ((stream) => {
mediaRecorder = new MediaRecorder (stream) ;
mediaRecorder.start () ;
mediaRecorder.ondataavailable = mediaRecorderDataAvailable;
mediaRecorder.onstop = mediaRecorderStop;
})
.catch ((err) => {
alert (`The following error occurred: ${err}`) ;
// change image in button
recordButtonImage.src = '/images/microphone.png’;
}) ;
} else {
// stop recording
mediaRecorder.stop () ;
}
Поддержка браузера
Сначала мы проверяем, определены ли navigator.mediaDevicesи navigator.mediaDevices.getUserMedia, поскольку существуют такие браузеры, как Internet Explorer, Chrome на Android и другие, которые его не поддерживают.
Кроме того, для использования getUserMediaтребуются безопасные
Начать запись
Если условие ложно (то есть поддерживаются оба mediaDevicesи getUserMedia), мы сначала меняем изображение кнопки записи на stop.png, которое вы можете скачать с Iconscout или репозитория GitHub и поместить в public/assets/images.
Затем мы проверяем, является ли значение, mediaRecorderкоторое мы определили в начале файла, нулевым или нет.
Если он равен нулю, это означает, что запись не ведется. Итак, мы получаем экземпляр MediaStream для начала записи с использованием getUserMedia.
Мы передаем ему объект только с ключом audioи значением true, так как мы просто записываем звук.
Здесь браузер предлагает пользователю разрешить
mediaRecorder = new MediaRecorder (stream) ;
mediaRecorder.start () ;
mediaRecorder.ondataavailable = mediaRecorderDataAvailable;
mediaRecorder.onstop = mediaRecorderStop;
Здесь мы создаем новый MediaRecorder, назначая его тому, mediaRecorderчто мы определили в начале файла.
Мы передаем конструктору поток, полученный от getUserMedia. Затем мы начинаем запись с помощью mediaRecorder.start ().
Наконец, мы привязываем обработчики событий (которые мы скоро создадим) к двум событиям dataavailableи stop.
Мы также добавили catchобработчик на случай, если пользователь не разрешит
Остановить запись
Все это происходит, если mediaRecorderне равно нулю. Если он равен нулю, это означает, что запись продолжается, и пользователь завершает ее. Итак, мы используем метод mediaRecorder.stop () для остановки записи:
} else {
//stop recording
mediaRecorder.stop () ;
}
Обработка событий записи мультимедиа
Пока что наш код запускает и останавливает запись, когда пользователь нажимает кнопку записи. Далее мы добавим обработчики событий для dataavailableи stop.
По доступным данным
Событие dataavailableзапускается либо при выполнении полной записи, либо на основе необязательного параметра timeslice, передаваемого mediaRecorder.start () для указания количества миллисекунд, в течение которого должно запускаться это событие. Передача кванта времени позволяет нарезать запись и получать ее фрагментами.
Создадим mediaRecorderDataAvailableфункцию, которая будет обрабатывать dataavailableсобытие, просто добавив звуковую дорожку Blob в полученном параметре BlobEvent в chunksмассив, который мы определили в начале файла:
function mediaRecorderDataAvailable (e) {
chunks.push (e.data) ;
}
Чанк будет представлять собой массив звуковых дорожек записи пользователя.
На остановке
Прежде чем мы создадим mediaRecorderStop, который будет обрабатывать событие остановки, давайте сначала добавим контейнер
Добавьте следующее public/index.htmlнепосредственно перед закрывающим тегом:
<div class="recorded-audio-container mt-5 d-none flex-column justify-content-center align-items-center"
id="recordedAudioContainer">
Затем в начале public/assets/js/record.jsдобавьте переменную, которая будет экземпляром Node #recordedAudioContainerэлемента:
const recordedAudioContainer = document.getElementById ('recordedAudioContainer’) ;
Теперь мы можем реализовать mediaRecorderStop. Эта функция сначала удалит любой аудиоэлемент, который был ранее записан и не сохранен, создаст новый аудиоэлемент мультимедиа, установит srcзначение Blob записанного потока и покажет контейнер:
function mediaRecorderStop () {
//check if there are any previous recordings and remove them
if (recordedAudioContainer.firstElementChild.tagName === 'AUDIO’) {
recordedAudioContainer.firstElementChild.remove () ;
}
//create a new audio element that will hold the recorded audio
const audioElm = document.createElement ('audio’) ;
audioElm.setAttribute ('controls’, '') ; //add controls
//create the Blob from the chunks
audioBlob = new Blob (chunks, { type: 'audio/mp3' }) ;
const audioURL = window.URL.createObjectURL (audioBlob) ;
audioElm.src = audioURL;
//show audio
recordedAudioContainer.insertBefore (audioElm, recordedAudioContainer.firstElementChild) ;
recordedAudioContainer.classList.add ('
recordedAudioContainer.classList.remove ('
//reset to default
mediaRecorder = null;
chunks = [];
}
В конце концов, мы сбрасываем mediaRecorderи chunksдо их начальных значений, чтобы обрабатывать следующие записи. С помощью этого кода наш
Последнее, что нам нужно сделать, это создать ссылку на record.jsфайл index.html. Добавьте scriptв конце body:
Тестовая запись
Давайте посмотрим это сейчас. Зайдите localhost:3000в свой браузер и нажмите на кнопку записи. Вам будет предложено разрешить
Разрешить запрос
Убедитесь, что вы загружаете
Нажмите Разрешить. Затем изображение с микрофона изменится на
Значок записи
Попробуйте записать несколько секунд. Затем нажмите на кнопку остановки. Изображение кнопки снова изменится на изображение микрофона, а аудиоплеер появится с двумя кнопками — «Сохранить «и «Отменить «.
Аудиоплеер
Далее мы реализуем события нажатия кнопок «Сохранить «и «Отменить «. Кнопка «Сохранить «должна загрузить аудио на сервер, а кнопка «Отменить «должна удалить его.
Отменить обработчик события клика
Сначала мы реализуем обработчик событий для кнопки Discard. Нажатие этой кнопки должно сначала показать пользователю приглашение подтвердить, что он хочет отказаться от записи. Затем, если пользователь подтвердит, он удалит аудиоплеер и скроет кнопки.
Добавьте переменную, которая будет удерживать кнопку Discard, в начало public/assets/js/record.js:
const discardAudioButton = document.getElementById ('discardButton’) ;
Затем добавьте в конец файла следующее:
function discardRecording () {
//show the user the prompt to confirm they want to discard
if (confirm ('Are you sure you want to discard the recording?')) {
//discard audio just recorded
resetRecording () ;
}
}
function resetRecording () {
if (recordedAudioContainer.firstElementChild.tagName === 'AUDIO’) {
//remove the audio
recordedAudioContainer.firstElementChild.remove () ;
//hide recordedAudioContainer
recordedAudioContainer.classList.add ('
recordedAudioContainer.classList.remove ('
}
//reset audioBlob for the next recording
audioBlob = null;
}
//add the event listener to the button
discardAudioButton.addEventListener ('click’, discardRecording) ;
Теперь вы можете попробовать записать
Загрузить на сервер
Сохранить обработчик события клика
Теперь мы реализуем обработчик кликов для кнопки «Сохранить «. Этот обработчик будет загружать на audioBlobсервер с помощью Fetch API, когда пользователь нажимает кнопку «Сохранить «.
Если вы не знакомы с Fetch API, вы можете узнать больше в нашем руководстве «Введение в Fetch API «.
Начнем с создания uploadsкаталога в корне проекта:
mkdir uploads
Затем в начале record.jsдобавьте переменную, которая будет содержать элемент кнопки «Сохранить «:
const saveAudioButton = document.getElementById ('saveButton’) ;
Затем, в конце, добавьте следующее:
function saveRecording () {
//the form data that will hold the Blob to upload
const formData = new FormData () ;
//add the Blob to formData
formData.append ('audio’, audioBlob, 'recording.mp3') ;
//send the request to the endpoint
fetch ('/record’, {
method: 'POST’,
body: formData
})
.then ((response) => response.json ())
.then (() => {
alert («Your recording is saved») ;
//reset for next recording
resetRecording () ;
//TODO fetch recordings
})
.catch ((err) => {
console.error (err) ;
alert («An error occurred, please try again later») ;
//reset for next recording
resetRecording () ;
})
}
//add the event handler to the click event
saveAudioButton.addEventListener ('click’, saveRecording) ;
Обратите внимание, что после загрузки записи мы используем resetRecordingдля сброса звука для следующей записи. Позже мы получим все записи, чтобы показать их пользователю.
Создать конечную точку API
Теперь нам нужно реализовать конечную точку API. Конечная точка загрузит аудио в uploadsкаталог.
Чтобы легко обрабатывать загрузку файлов в Express, мы будем использовать библиотеку Multer. Multer предоставляет промежуточное программное обеспечение для обработки загрузки файлов.
Запустите следующее, чтобы установить его:
npm i multer
Затем в index.js, добавьте в начало файла следующее:
const fs = require ('fs’) ;
const multer = require ('multer’) ;
const storage = multer.diskStorage ({
destination (req, file, cb) {
cb (null, 'uploads/') ;
},
filename (req, file, cb) {
const fileNameArr = file.originalname.split ('.') ;
cb (null, `${Date.now () }. ${fileNameArr[fileNameArr.length — 1]}`) ;
},
}) ;
const upload = multer ({ storage }) ;
Мы объявили storageиспользование multer.diskStorage, которое мы настраиваем для хранения файлов в uploadsкаталоге, и мы сохраняем файлы на основе текущей метки времени с расширением.
Затем мы объявили upload, который будет промежуточным программным обеспечением, которое будет загружать файлы.
Далее мы хотим сделать файлы внутри uploadsкаталога общедоступными. Итак, добавьте следующее перед app.listen:
app.use (express.static ('uploads’));
Наконец, мы создадим конечную точку загрузки. Эта конечная точка будет просто использовать uploadпромежуточное ПО для загрузки аудио и возврата ответа JSON:
app.post ('/record’, upload.single ('audio’), (req, res) => res.json ({ success: true }));
Промежуточное uploadПО будет обрабатывать загрузку файла. Нам просто нужно передать имя поля файла, который мы загружаем в upload.single.
Обратите внимание, что обычно вам необходимо выполнить проверку файлов и убедиться, что загружаются правильные, ожидаемые типы файлов. Для простоты мы опускаем это в этом уроке.
Тестовая загрузка
Давайте проверим это. Снова зайдите localhost:3000в свой браузер, запишите
Запрос будет отправлен на конечную точку, файл будет загружен, а пользователю будет показано предупреждение о том, что запись сохранена.
Вы можете убедиться, что аудио действительно загружено, проверив uploadsкаталог в корне вашего проекта. Там вы должны найти аудиофайл MP3.
Показать записи
Создайте конечную точку API
Последнее, что мы сделаем, это покажем все записи пользователю, чтобы он мог их воспроизвести.
app.get ('/recordings’, (req, res) => {
let files = fs.readdirSync (path.join (__dirname, 'uploads’));
files = files.filter ((file) => {
// check that the files are audio files
const fileNameArr = file.split ('.') ;
return fileNameArr[fileNameArr.length — 1] === 'mp3';
}).map ((file) => `/${file}`) ;
return res.json ({ success: true, files }) ;
}) ;
Мы просто читаем файлы внутри uploadsкаталога, фильтруем их, чтобы получить только mp3файлы, и добавляем /к каждому имени файла. Наконец, мы возвращаем объект JSON с файлами.
Добавьте элемент контейнера записей
Далее мы добавим
Saved Recordings
Получить файлы из API
Также добавьте в начало record.jsпеременной, которая будет содержать #recordingsэлемент:
const recordingsContainer = document.getElementById ('recordings’) ;
Затем мы добавим fetchRecordingsфункцию, которая вызовет конечную точку, которую мы создали ранее, а затем с помощью этой createRecordingElementфункции отобразит элементы, которые будут аудиоплеерами.
Мы также добавим playRecordingпрослушиватель событий для события нажатия на кнопку, которая будет воспроизводить звук.
Добавьте следующее в конце record.js:
function fetchRecordings () {
fetch ('/recordings’)
.then ((response) => response.json ())
.then ((response) => {
if (response.success && response.files) {
//remove all previous recordings shown
recordingsContainer.innerHTML = '';
response.files.forEach ((file) => {
//create the recording element
const recordingElement = createRecordingElement (file) ;
//add it the recordings container
recordingsContainer.appendChild (recordingElement) ;
})
}
})
.catch ((err) => console.error (err));
}
//create the recording element
function createRecordingElement (file) {
//container element
const recordingElement = document.createElement ('div’) ;
recordingElement.classList.add ('
//audio element
const audio = document.createElement ('audio’) ;
audio.src = file;
audio.onended = (e) => {
//when the audio ends, change the image inside the button to play again
e.target.nextElementSibling.firstElementChild.src = 'images/play.png’;
};
recordingElement.appendChild (audio) ;
//button element
const playButton = document.createElement ('button’) ;
playButton.classList.add ('
//image element inside button
const playImage = document.createElement ('img’) ;
playImage.src = '/images/play.png’;
playImage.classList.add ('
playButton.appendChild (playImage) ;
//add event listener to the button to play the recording
playButton.addEventListener ('click’, playRecording) ;
recordingElement.appendChild (playButton) ;
//return the container element
return recordingElement;
}
function playRecording (e) {
let button = e.target;
if (button.tagName === 'IMG’) {
//get parent button
button = button.parentElement;
}
//get audio sibling
const audio = button.previousElementSibling;
if (audio && audio.tagName === 'AUDIO’) {
if (audio.paused) {
//if audio is paused, play it
audio.play () ;
//change the image inside the button to pause
button.firstElementChild.src = 'images/pause.png’;
} else {
//if audio is playing, pause it
audio.pause () ;
//change the image inside the button to play
button.firstElementChild.src = 'images/play.png’;
}
}
}
Обратите внимание, что внутри playRecordingфункции мы проверяем, воспроизводится ли звук, используя функцию, audio.pausedкоторая вернет true, если в данный момент звук не воспроизводится.
Мы также используем значки воспроизведения и паузы, которые будут отображаться внутри каждой записи. Получить эти иконки можно из Iconscout или репозитория GitHub.
Мы будем использовать fetchRecordingsпри загрузке страницы и при загрузке новой записи.
Итак, вызовите функцию в конце record.jsи внутри обработчика выполнения saveRecordingвместо TODOкомментария:
.then (() => {
alert («Your recording is saved») ;
//reset for next recording
resetRecording () ;
//fetch recordings
fetchRecordings () ;
})
Добавление стилей
Последнее, что нам нужно сделать, это добавить стиль к элементам, которые мы создаем. Добавьте следующее public/assets/css/index.css:
.
}
.
height: 8em;
width: 8em;
}
Тестируйте все
Теперь все готово. Откройте
Теперь пользователь может записывать свой голос, сохранять или удалять его. Пользователь также может просмотреть все загруженные записи и воспроизвести их.
Сохраненные записи
Заключение
Использование API MediaStream позволяет нам добавлять мультимедийные функции для пользователя, например запись звука.