Разработка веб-приложения музыкального аудио-стриминга

Проектирование архитектуры и базы данных приложения музыкального аудио-стриминга. Способы воспроизведения звука в браузер. Алгоритм организации стримингового процесса. Реализация процессов остановки, воссоздания воспроизведения, прокрутки композиции.

Рубрика Программирование, компьютеры и кибернетика
Вид дипломная работа
Язык русский
Дата добавления 16.08.2020
Размер файла 2,7 M

Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже

Студенты, аспиранты, молодые ученые, использующие базу знаний в своей учебе и работе, будут вам очень благодарны.

const playWhileLoading = setInterval(() => { if (chunks.length !== 0) { let chunk = chunks.shift(); audioCtx.decodeAudioData(chunk) .then((audioBufferChunk) => { audioBuffer = audioBuffer ? appendBuffer(audioBuffer, audioBufferChunk) : audioBufferChunk; let source = audioCtx.createBufferSource(); source.buffer = audioBuffer; source.connect(gainNode); gainNode.connect(audioCtx.destination); sources.push(source); source.start(startTime + nextTime, audioBuffer.duration - audioBufferChunk.duration, audioBufferChunk.duration); nextTime += audioBufferChunk.duration - 0.05; } }); } }, 500);

socket.emit('track', (e) => {}); nextTime = 0; startTime = audioCtx.currentTime; socket.on('audio', (chunk_) => { chunks.push(chunk_); });

Листинг 5

Мы вернули использование массива объектов AudioBufferSourceNode из первого подхода, только теперь в нем буфер каждого следующего объекта содержит не только данные одного фрагмента, но и всех предыдущих. За конкатенацию буферов отвечает функция appendBuffer(buffer1, buffer2). Когда проигрывание заканчивается, мы удаляем его из массива. Перед началом воспроизведения первого фрагмента мы сохраняем текущее время в переменную startTime и впоследствии при расчете задержки воспроизведения очередного фрагмента мы будем уже к ее значению прибавлять продолжительность проигрывания предыдущих, как будто все части загрузились на клиент одновременно. В методе start вторым параметром мы указали смещение времени внутри аудиобуфера, с которого должно начаться воспроизведение, а третьим - продолжительность проигрывания текущего фрагмента.

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

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

Рис.22 Аудиоволна при организации воспроизведения с использованием 3го подхода

Глава 5. Регулирование воспроизведения

5.1 Остановка/возобновление воспроизведения

Рис.23 Схема процессов остановки/возобновления воспроизведения

Когда срабатывает событие о приостановке воспроизведения, инициируемое пользователем при нажатии на кнопку паузы, на клиенте начинает свое выполнение функция stopPlaying. Она вызывает метод stop() у всех объектов AudioBufferSourceNode из массива sources, поскольку Web Audio API не предоставляет возможности отследить состояние активности у AudioBufferSourceNode и мы не можем проверить играет ли он в текущий момент или нет, а значит и узнать у какого из объектов массива sources вызывать метод stop(). Перед этим в функции сохраняется продолжительность проигрывания с момента старта и до паузы в переменную playedDuration - это будет смещение в буфере, которое нам пригодится при воссоздании воспроизведения, его значение равно сумме всех временных промежутков между моментом остановки (текущим временем) и startTime. Также мы ввели булевую переменную isPlaying, которая отвечает за временную остановку планирования воспроизведения в функции playWhileLoading, чтобы приходящие фрагменты не начали играть после постановки на паузу. Фрагмент кода с реализацией процессов остановки и возобновления воспроизведения, описанных выше:

let isPlaying = true; let playedDuration = 0; const playWhileLoading = setInterval(() => { … if(isPlaying) { if(startTime === 0) { startTime = audioContext.currentTime; } source.start(startTime + nextTime, audioBuffer.duration - audioBufferChunk.duration, audioBufferChunk.duration); nextTime += audioBufferChunk.duration; } … }, 500); const stopPlaying = () => { if(sources.length) { isPlaying = false; playedDuration += audioContext.currentTime - startTime; sources.forEach((source) => source && source.stop(0)); } };

Листинг 6

При снятии паузы и возобновлении воспроизведения срабатывает функция resumePlaying. Сначала она удаляет из массива sources все объекты кроме последнего, который записывается в переменную currSource. Далее создается новый объект source класса AudioBufferSourceNode, в буфер которого записываются данные из буфера currSource, и он добавляется в массив sources. Перед началом воспроизведения также пересчитывается значение nextTime, которое используется в функции playWhileLoading в качестве задержки воспроизведения нового полученного фрагмента с сервера. Его значение равно разнице между продолжительностью проигрывания всего буфера в source и playedDuration - временем, затраченным на проигрывание до момента постановки на паузу. A также обновляется значение startTime текущим временем. После этого у объекта source вызывается метод start() с параметрами, указывающими на немедленное воспроизведение оставшейся не проигранной части буфера, чье значение равно nextTime, со смещением в буфере, равным playedDuration.

const resumePlaying = (offsetSec) => { sources.splice(0, sources.length - 1); const currSource = sources.pop(); let source = audioCtx.createBufferSource(); source.buffer = currSource.buffer; source.connect(audioCtx.destination); sources.push(source); nextTime = source.buffer.duration - offsetSec; startTime = audioCtx.currentTime + audioCtx.baseLatency; source.start(0, offsetSec, nextTime); isPlaying = true; }; const onPlayBtnClick = () => { if(sources && sources.length) { dispatch(onPlay()); resumePlaying(playedDuration); } };

Листинг 7

5.2 Смена точки воспроизведения

5.2.1 Клиент

Когда происходит прокрутка воспроизведения на определенную позицию на временной шкале, обработчик события сначала проверяет состояние плеера на текущий момент, если идет воспроизведение, то его необходимо остановить, прежде чем запрашивать с сервера данные. Для остановки проигрывания у нас уже есть готовая функция stopPlaying(), которую мы описали выше. Затем обработчику необходимо сбросить глобальное состояние приложения к значениям, установленным по умолчанию, а также сообщить серверу, что нужно остановить чтение и передачу фрагментов аудиоданных, если она еще не завершена, отправив для этого серверу событие с именем «stopLoad». После того как сервер обработает событие и вызовет callback функцию, клиент установит значение состояния воспроизведения isPlaying в true.

Далее независимо от того, было ли установлено значение isPlaying в true к началу инициирования события смены точки воспроизведения, клиент отправит серверу событие «play» с номером байта startSize, с которого серверу необходимо начать присылать аудиоданные клиенту для их воспроизведения. Значение startSize рассчитывается через составление пропорции:

startSize / fileSize = timepoint / 100%, откуда

startSize = (fileSize * timepoint) / 100%

Фрагмент кода с реализацией описанного процесса:

const playByTimepoint = (timepoint) => { if (isStarted) { if (isPlaying && sources && sources.length) { stopPlaying(); chunks = []; sources = []; startTime = 0; playedDuration = 0; currTime = (fileDuration * timepoint) / 100; loadRate = 0; nextTime = 0; audioBuffer = null; socket.emit('stopLoad', () => { isPlaying = true; }); } const startSize = (fileSize * timepoint) / 100; socket.emit('play', {startSize}); } };

Листинг 8

5.2.2 Сервер

Слушатель события «stopLoad» на сервере вызывает метод destroy() у существующего объекта класса Readable, который уничтожает поток чтения, освобождает все ассоциированные с ним ресурсы. Как только это будет сделано, обработчик вызовет callback функцию, чтобы сообщить клиенту, что чтение и отправка фрагментов прекращены.

client.on('stopLoad', (callback) => { readStream && readstream.destroy(); callback(); });

Листинг 9

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

- как было выяснено раннее фрагменты MP3-файла должны состоять из полноценных фреймов с заголовками, поэтому если нам необходимо отправлять фрагменты клиенту с определенного байта, то нужно найти и добавить к отправляемым аудиоданным ближайший к заданному байту заголовок;

- отправляемые фрагменты не должны быть слишком маленького размера, чтобы на клиенте их можно было декодировать.

Исходя из этих условий код обработчика события «play» был модифицирован, чтобы он реализовывал логику не только отправки фрагментов с самого первого байта аудиофайла, но и с заданным смещением. Для этого была разработана следующая схема формирования корректных фрагментов с определенного байта для отправки клиенту:

Рис.24 Схема процесса смены точки начала воспроизведения

У метода fs.createReadStream() есть опции start и end, которые задают диапазон байтов для чтения из файла. Мы могли бы использовать параметр start, чтобы не пришлось считывать ненужные фрагменты, которые клиент не запрашивал. Но использование такого подхода не дает возможности найти заголовок, соответствующий фрагменту, в диапазон которого входит запрашиваемое смещение. Для решения данной проблемы было рассмотрено три варианта:

- рассчитать номер фрагмента, в который входит запрашиваемое смещение, зная размер одной порции данных и размер всего файла, и начать читать файл в поток со смещения, по которому находится данный фрагмент;

- сдвинуть смещение чтения из файла влево на несколько байт, чтобы обнаружить ближайший заголовок, но данный способ не совсем надежный, поскольку заголовка там может не оказаться;

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

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

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

if ((totalBuffer.length - chunkBuffer.length) <= startSize && startSize <= totalBuffer.length)

Если нужный фрагмент был найден, то в интервале между началом totalBuffer и смещением startSize ищется самый последний заголовок, и он присоединяется к той порции данных, которая находится по смещению startSize относительно totalBuffer. Поскольку смещение startSize может находится близко к концу текущего значения длины totalBuffer, то размер отправляемого клиенту фрагмента может оказаться слишком мал для его последующего декодирования и воспроизведения. В связи с этим сформированный фрагмент не передается сразу, а объединяется со следующим прочитанным фрагментом. Но перед тем, как переходить к чтению следующего фрагмента необходимо добавить к текущему оставшуюся часть от прочитанной порции данных, которую мы отрезали для формирования корректного фрагмента с цельными фреймами. Пример реализации:

let offset_ = 0; let chunkBuffer = null; let totalBuffer = null; readStream.on('data', (chunk) => { const previousOffset = Object.assign({}, offset_); offset_ = chunk.lastIndexOf(new Uint8Array([255, 251])); if (offset_ !== -1) { chunkBuffer = chunkBuffer ? Buffer.concat([chunkBuffer, chunk.slice(0, offset_)]) : chunk.slice(0, offset_); totalBuffer = totalBuffer ? Buffer.concat([totalBuffer, chunkBuffer]) : chunkBuffer; if(startSize > 0) { if ((totalBuffer.length - chunkBuffer.length) <= startSize && startSize <= totalBuffer.length) { const offset = totalBuffer.slice(0, startSize).lastIndexOf(new Uint8Array([255, 251])); if (offset !== -1) { const header = chunkBuffer.slice(offset, offset + 4); const startChunk = totalBuffer.slice(startSize); totalBuffer = totalBuffer.slice(0, startSize); chunkBuffer = Buffer.concat([header, startChunk]); } startSize = 0; chunkBuffer = Buffer.concat([chunkBuffer, chunk.slice(offset_)]); } else chunkBuffer = chunk.slice(offset_); } else { client.emit('audio', chunkBuffer); chunkBuffer = chunk.slice(offset_) } else { chunkBuffer = chunkBuffer ? Buffer.concat([chunkBuffer, chunk]) : chunk; } });

Листинг 10

Заключение

Таким образом, в рамках выпускной квалификационной работы были выполнены задачи определения требований, анализа и проектирования, была предложена реализация потоковой передачи аудио данных с сервера, а также разработано возможное решение по организации плавного воспроизведения на клиенте потока отдельных аудио фрагментов как цельной аудиодорожки с помощью Web Audio API. Было реализовано веб-приложение музыкального аудио-стриминга со стандартным функционалом аудио-проигрывателя для контроля над воспроизведением с учетом технологии потокового передачи аудио и поставленная цель была достигнута.

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

Размещено на Allbest.ru


Подобные документы

Работы в архивах красиво оформлены согласно требованиям ВУЗов и содержат рисунки, диаграммы, формулы и т.д.
PPT, PPTX и PDF-файлы представлены только в архивах.
Рекомендуем скачать работу.