Здравствуйте, уважаемые читатели. Сегодня хотелось бы рассказать Вам о том, как сделать простейший кастомный аудиоплеер с плейлистом. Будем делать его, используя HTML5 и Javascript. В результате, получим вот такой простенький плеер. По внешнему виду похожий на плеер vk.com
Самый простой способ для воспроизведения аудиофайлов в браузере — использование тэга
audio
Атрибут controls отображает стандартные элементы управления, такие как кнопка play/pause, mute.
Различные браузеры отображают плеер со своим дизайном без каких-либо особых возможностей. Если нам необходим настраиваемый аудиоплеер, одним из лучших вариантов будет посмотреть готовые решения, которые были проверены сотнями разработчиков и рядовыми пользователями. Если, вдруг, по какой-то причине мы не хотим использовать готовое решение, например, из-за избыточного функционала или его отсутствия, можно написать собственное решение. Как минимум, это будет полезно в целях самообразования.
Чтобы воспроизвести аудио, используя Javascript, достаточно лишь две строки кода:
Тут создается объект Audio, им-то мы и будем оперировать. Вот краткий список общих методов и свойств:
.play() — запустить проигрывание аудиофайла;
.pause() — поставить на паузу;
.duration — длина дорожки (в секундах). Duration становится доступен только после срабатывания события ‘loadedmetadata’;
currentTime — получить/установить момент проигрывания звуковой дорожки;
Полный список доступен тут
Так как данная реализация всего-лишь фронтенд пример, то добавим аудио файлы в локальную папку files (для удобства и наглядности) и захардкодим их названия:
В продакшене нельзя использовать такой подход по очевидным причинам.
Теперь создадим класс Player с такими методами:
init — инициализация плейлиста;
loadFile — загрузить дорожку, при запросе на запуск, только если она еще не было загружена.
play — запустить/поставить на паузу дорожку;
playNext — запустить следующую дорожку;
playPrev — запустить предыдущую дорожку;
setTitle — установить название файла в шапке плеера;
setProgress — установить процент на сколько заполнен прогресс-бар;
countProgress — посчитать в процентах количество проигранного времени;
runProgress — запустить отрисовку заполнения прогресс-бара;
stopProgress — сбросить процент заполненности прогресс-бара;
pickNewProgress — навигация по файлу с помощью клика по прогресс-бару;
toggleStyles — метод для изменения стилей кнопки play/pause и плейлиста;
Далее цепляемся на событие DOMContentLoaded инициализируем наш плеер и вешаем обработчики на элементы управления:
Функция getByQuery() сокращенный вариант document.querySelector():
А вот она в коде:
Помимо этого было использовано еще несколько кастомных функций, код которых можно найти в репозитории.
Логика работы плеера очень простая. Мы читаем массив файлов и записываем его в this. Кликнув по дорожке, в плейлисте вызывается метод play. Ему мы передаем индекс файла в массиве. Он подгружает дорожку, используя метод loadFileТолько если дорожка еще не была загружена. Затем метод запускает ее проигрывание. Не забываем и за прогресс-бар. В контексте плеера мы также храним статус (play/pause), индекс текущего файла и переменную с таймаутом. Таймаут мы используем, чтоб раз в секунду (при условии проигрывания файла) мы могли перерисовывать прогресс-бар. Ссылка на демо.
В следующей статье разберем более сложные способы реализации аудиоплеера.
Спасибо за внимание.
Над статьей работали varog-norman и greebn9k
Самый простой способ для воспроизведения аудиофайлов в браузере — использование тэга
audio
<audio controls="controls">
<source src="files/track1.mp3" />
</audio>
Атрибут controls отображает стандартные элементы управления, такие как кнопка play/pause, mute.
Различные браузеры отображают плеер со своим дизайном без каких-либо особых возможностей. Если нам необходим настраиваемый аудиоплеер, одним из лучших вариантов будет посмотреть готовые решения, которые были проверены сотнями разработчиков и рядовыми пользователями. Если, вдруг, по какой-то причине мы не хотим использовать готовое решение, например, из-за избыточного функционала или его отсутствия, можно написать собственное решение. Как минимум, это будет полезно в целях самообразования.
Чтобы воспроизвести аудио, используя Javascript, достаточно лишь две строки кода:
let file = new Audio('track1.mp3');
file.play();
Тут создается объект Audio, им-то мы и будем оперировать. Вот краткий список общих методов и свойств:
.play() — запустить проигрывание аудиофайла;
.pause() — поставить на паузу;
.duration — длина дорожки (в секундах). Duration становится доступен только после срабатывания события ‘loadedmetadata’;
currentTime — получить/установить момент проигрывания звуковой дорожки;
Полный список доступен тут
Так как данная реализация всего-лишь фронтенд пример, то добавим аудио файлы в локальную папку files (для удобства и наглядности) и захардкодим их названия:
const FILES = [
'track1',
'track2',
'track3',
'track4'
]
В продакшене нельзя использовать такой подход по очевидным причинам.
Теперь создадим класс Player с такими методами:
init — инициализация плейлиста;
loadFile — загрузить дорожку, при запросе на запуск, только если она еще не было загружена.
play — запустить/поставить на паузу дорожку;
playNext — запустить следующую дорожку;
playPrev — запустить предыдущую дорожку;
setTitle — установить название файла в шапке плеера;
setProgress — установить процент на сколько заполнен прогресс-бар;
countProgress — посчитать в процентах количество проигранного времени;
runProgress — запустить отрисовку заполнения прогресс-бара;
stopProgress — сбросить процент заполненности прогресс-бара;
pickNewProgress — навигация по файлу с помощью клика по прогресс-бару;
toggleStyles — метод для изменения стилей кнопки play/pause и плейлиста;
Код плеера:
class Player {
constructor(files) {
this.current = null;
this.status = 'pause';
this.progress = 0;
this.progressTimeout = null;
this.files = FILES.map(name => {
return {
name: name
}
});
}
init() {
let playlist = getByQuery('.playlist');
this.files.forEach((f, i) => {
let playlistFileContainer = createElem({
type: 'div',
appendTo: playlist,
textContent: f.name,
class: 'fileEntity',
handlers: {
click: this.play.bind(this, null, i)
}
});
createElem({
type: 'div',
appendTo: playlistFileContainer,
textContent: '--:--',
class: 'fileEntity_duration',
})
});
}
loadFile(i) {
let f = this.files[i];
f.file = new Audio(prepareFilePath(f.name));
f.file.addEventListener('loadedmetadata', () => {
getByQuery('.playlist').children[i].children[0].textContent = prettifyTime(f.file.duration);
});
f.file.addEventListener('ended', this.playNext.bind(this, null, i));
}
play(e, i = this.current || 0) {
if (!this.files[i].file) {
this.loadFile(i);
}
let action = 'play';
if (this.current == i) {
action = this.status === 'pause' ? 'play' : 'pause';
this.toggleStyles(action, i);
} else if (typeof this.current !== 'object') {
this.files[this.current].file.pause();
this.files[this.current].file.currentTime = 0;
this.toggleStyles(action, this.current, i);
} else {
this.toggleStyles(action, i);
}
this.current = i;
this.status = action;
this.files[i].file[action]();
if (action == 'play') {
this.setTitle(this.files[i].name);
this.stopProgress();
this.runProgress();
} else {
this.stopProgress();
}
}
playNext(e, currentIndex) {
let nextIndex = (currentIndex ? currentIndex : this.current) + 1;
if (!this.files[nextIndex]) {
nextIndex = 0;
}
this.play(null, nextIndex);
}
playPrev(e, currentIndex) {
let prevIndex = (currentIndex ? currentIndex : this.current) - 1;
if (!this.files[prevIndex]) {
prevIndex = this.files.length - 1;
}
this.play(null, prevIndex);
}
setTitle(title) {
getByQuery('.progress_bar_title').textContent = title;
}
setProgress(percent = 0, cb) {
getByQuery('.progress_bar_container_percentage').style.width = `${percent}%`;
cb && cb();
}
countProgress() {
let file = this.files[this.current].file;
return (file.currentTime * 100 / file.duration) || 0;
}
runProgress(percent = 0) {
let percentage = percent || this.countProgress();
let cb = percent ? () => {
this.files[this.current].file.currentTime = percentage * this.files[this.current].file.duration / 100;
} : null;
this.setProgress(percentage, cb);
this.progressTimeout = setTimeout(this.runProgress.bind(this), 1000)
}
stopProgress() {
clearTimeout(this.progressTimeout);
this.progressTimeout = null;
}
pickNewProgress(e) {
if (this.status != 'play') {
this.play();
}
let coords = e.target.getBoundingClientRect().left;
let progressBar = getByQuery('.progress_bar_stripe');
let newPercent = (e.clientX - coords) / progressBar.offsetWidth * 100;
this.stopProgress();
this.runProgress(newPercent);
}
toggleStyles(action, prev, next) {
let prevNode = getByQuery('.playlist').children[prev];
let nextNode = getByQuery('.playlist').children[next];
let playPause = getByQuery('.play_pause .play_pause_icon');
if (!next && next !== 0) {
if (!prevNode.classList.contains('fileEntity-active')) {
prevNode.classList.add('fileEntity-active');
}
playPause.classList.toggle('play_pause-play');
playPause.classList.toggle('play_pause-pause');
} else {
prevNode.classList.toggle('fileEntity-active');
nextNode.classList.toggle('fileEntity-active');
}
if (playPause.classList.contains('play_pause-play') && action == 'play' && prev != next) {
playPause.classList.toggle('play_pause-play');
playPause.classList.toggle('play_pause-pause');
}
}
}
Далее цепляемся на событие DOMContentLoaded инициализируем наш плеер и вешаем обработчики на элементы управления:
window.addEventListener('DOMContentLoaded', initHandlers);
function initHandlers() {
let player = new Player(FILES);
player.init();
getByQuery('.player .controls .play_pause').addEventListener('click', player.play.bind(player));
getByQuery('.player .controls .navigation_prev').addEventListener('click', player.playPrev.bind(player));
getByQuery('.player .controls .navigation_next').addEventListener('click', player.playNext.bind(player));
getByQuery('.player .controls .progress_bar_stripe').addEventListener('click', player.pickNewProgress.bind(player));
}
Функция getByQuery() сокращенный вариант document.querySelector():
А вот она в коде:
function getByQuery(elem) {
return typeof elem === 'string' ? document.querySelector(elem) : elem;
}
Помимо этого было использовано еще несколько кастомных функций, код которых можно найти в репозитории.
Логика работы плеера очень простая. Мы читаем массив файлов и записываем его в this. Кликнув по дорожке, в плейлисте вызывается метод play. Ему мы передаем индекс файла в массиве. Он подгружает дорожку, используя метод loadFileТолько если дорожка еще не была загружена. Затем метод запускает ее проигрывание. Не забываем и за прогресс-бар. В контексте плеера мы также храним статус (play/pause), индекс текущего файла и переменную с таймаутом. Таймаут мы используем, чтоб раз в секунду (при условии проигрывания файла) мы могли перерисовывать прогресс-бар. Ссылка на демо.
В следующей статье разберем более сложные способы реализации аудиоплеера.
Спасибо за внимание.
Над статьей работали varog-norman и greebn9k
Комментарии (6)
bromzh
13.09.2017 17:20+4this.files = FILES.map(name => { return { name: name } });
А почему не
this.files = FILES.map(name => ({ name }));
JSmitty
13.09.2017 20:34По коду — в конструкторе Player косяк, параметр конструктора не используется, а используется константа FILES напрямую. Определяемый там же this.progress — вообще нигде больше не используется.
getByQuery лучше обозвать покороче, оно у вас helper-функция, им обычно 1-2 символа назначают. Я бы предложил «q». С таким определением:
const q = document.querySelector;
Вот это:
getByQuery('.playlist').children[i].children[0].textContent
даже для jQuery-like кода очень плохо. Завязывает код намертво на структуру DOM (в остальных местах вы ссылаетесь на элементы по классам).
Шторм подсказывает, что сравнение по == не самая лучшая идея, и я с ним согласен.
Функция toggleStyles() объявляет переменные prevNode, nextNode, playPause (которая тоже Node по факту), и далее от них всегда используются classList. Можно сделать так:
let playListNode = getByQuery('.playlist'); let prevNode = (playListNode.children[prev] || {}).classList; let nextNode = (playListNode.children[next] || {}).classList; let playPause = getByQuery('.play_pause .play_pause_icon').classList;
Функции-утилиты очень хорошо было бы вместе с остальным кодом обернуть в (function(){ /* ваш код */ })(), чтобы не загрязнять глобальное пространство имён.
А вообще городить огород, когда эта задача очень декларативно ляжет на тот же Vue.js или React — странно. Как видно из вашего решения — начинается опять изобретение фреймворка (jQuery вы уже изобразили).
PS А почему Gulp? Вроде webpack сейчас мейнстрим.
JSmitty
13.09.2017 21:25По итогу никому из новичков в JS очень не рекомендую этот код воспринимать как инструкцию к действию. Стиль не очень, jquery-like построение интерфейса, архитектура — лапша классическая.
Zenitchik
13.09.2017 23:00Вы правы по сути, но хочу заметить, что вряд ли кто-то (включая автора) будет в таком стиле писать большой проект. Наколеночная поделка — она и есть наколеночная поделка, с характерными стилевыми послаблениями.
MrGobus
Как по мне, не лучшая практика переопределять ключевые слова языка. Думаю лучше использовать current или track.
Zenitchik
Переопределение тут ни при чём. Массив имён файлов записывается в свойство this.