Создаем Playlist приложение с Webix

В современном мире сложно найти человека, который бы ни разу не слушал музыку онлайн. Осмелюсь также предположить, что большинство разработчиков пробовали написать свой собственный плейлист. И я не исключение 🙂 В этой статье я хочу вам показать, как создать небольшое Playlist приложение с помощью библиотеки Webix UI.

Playlist

В приложении мы реализуем следующее:

  • просмотр списка треков
  • возможность управлять треками:
    • включать, выключать и приостанавливать
    • отслеживать и управлять прогрессом
  • возможность загружать новые треки.

В браузере наше приложение будет выглядеть следующим образом:

Посмотреть код

У библиотеки Webix есть почти все необходимые инструменты, для того чтобы реализовать нужный нам функционал. Список треков, например, мы можем отобразить с помощью компонентов DataTable или List. Для загрузки новых треков можно воспользоваться виджетом Uploader. Также есть различного рода кнопки и иконки, с помощью которых мы можем создать необходимые элементы управления.

Единственное, что нам еще нужно — это плеер для проигрывания аудиозаписей. Но среди компонентов Webix нет подобного виджета. И где же нам его взять, спросите вы? А давайте воспользуемся сторонним плеером Plyr. Возможно у вас появился следующий логичный вопрос: А можно ли совмещать виджеты библиотеки со сторонними компонентами? На самом деле, Webix решает эту задачу достаточно легко. Дело в том, что библиотека включает целый набор расширений, которые можно использовать для интеграции с собственными компонентами. Среди них есть и нужный нам плеер Plyr.

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

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

Создаем плейлист

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

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

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

Лучшим решением будет нарисовать контролы при помощи html темплейтов в рядах таблицы, а сам плеер поместить в layout рядом с таблицей и спрятать его. С помощью созданных html контролов мы будем обращаться к плееру через его API и управлять воспроизведением треков. Давайте так и сделаем. Для начала мы создадим таблицу и настроим ее надлежащим образом. Выглядит это так:

  {
    view:"datatable",
    id:"table",
    data:play_list_data,
    columns:[...]
  }

Сам виджет мы объявляем с помощью выражения view:»datatable». После этого, нам нужно указать уникальный id, с помощью которого мы сможем получить доступ к таблице.

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

  const play_list_data = [
    {
      songTitle:"Bensound - The Jazz Piano",
      source:{
        src:"https://www.bensound.com/bensound-music/bensound-thejazzpiano.mp3",
        type:"audio/mp3"
      }
    },
    //...
  ];

Чтобы загрузить треки, которые находятся на клиенте, мы используем свойство таблицы data.

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

Здесь стоит немного рассказать о контролах, которые мы будем использовать для управления плеером в таблице. Их шаблоны можно задать с помощью метода webix.type(). В качестве аргументов, метод принимает название виджета, в котором шаблоны будут использоваться, а также объект с описанием нужных контролов. В объекте настроек мы указываем название типа через свойство name и темплейт-функции для каждого контрола. Выглядит это так:

  webix.type(webix.ui.datatable, {
    name:"audioTable",
    toggleplay(obj){...},
    stop(){...},
    progress(obj, common){...}
  });

С помощью свойства type таблицы мы подключаем ранее созданные шаблоны, которые будем использовать в настройках столбцов. Стоит учитывать, что теперь эти шаблоны доступны нам внутри template столбцов через объект common:

  • common.toggleplay() — тогл-кнопка для начала и остановки трека
  • common.stop() — кнопка для сброса трека
  • common.progress() — индикатор прогресса

Теперь код с настройками столбцов выглядит так:

  view:"datatable",
  type:"audioTable",
  columns:[
    { id:"songTitle", header:"Title", width:200 },
    { template:`{common.toggleplay()}{common.stop()}`, width:120 },
    { template:`{common.progress()}`, fillspace:true }
  ]

В браузере наш плейлист будет выглядеть следующим образом:

DataTable

Создаем Plyr плеер

Теперь нам нужно создать компонент для проигрывания треков. Для этого мы воспользуемся сторонним плеером Plyr. Чтобы добавить плеер в наше приложение, необходимо сначала подключить соответствующий файл с его конфигурацией в главном html файле. Код интеграции Plyr с библиотекой Webix доступен по следующей ссылке:

<script type="text/javascript" src="https://webix-hub.github.io/components/plyr/plyr.js"></script>

Теперь мы можем создать плеер и использовать его API для управления треками. Давайте так и сделаем. Чтобы добавить компонент Plyr к нашему плейлисту, необходимо включить объект с его настройками в лейаут приложения. Выглядит это так:

  {
    view:"plyr-player",
    id:"audio",
    hidden:true,
    height:50,
    config:{
        controls:["play", "progress", "current-time"]
    },
    source:{ type:"audio" }
  },
  //...

Сам плеер мы объявляем с помощью выражения view:»plyr-player». После этого, нам необходимо задать виджету уникальный id, с помощью которого мы сможем обращаться к нему.

В объекте свойства config мы можем задать необходимые элементы управления плеером. Здесь нам понадобится кнопка для начала/остановки трека («play»), полоса прогресса («progress») и текущее время проигрывания («current-time»). Через свойство source нам нужно указать тип файлов, которые будут воспроизводиться. Вот и все настройки. В браузере плеер будет выглядеть следующим образом:

Plyr player

Посмотреть код

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

Связываем плеер с таблицей

Когда интерфейс плейлиста и плеер для проигрывания треков готовы, нам осталось настроить взаимодействие между ними. А начнем мы с контролов для управления треками в таблице. Хочу напомнить, что в каждом ряду у нас есть тогл кнопка, с помощью которой пользователи будут запускать трек и ставить его на паузу. Рядом с ней находится кнопка для сброса трека. В ячейках последнего столбца расположены индикаторы прогресса.

Давайте установим соответствующие обработчики на событие клика по этим контролам. У каждого из них есть свой css класс. С помощью свойства onClick мы можем отследить клик по элементам таблицы с определенным css классом и установить на них соответствующие обработчики.. Этим мы и воспользуемся:

  view:"datatable",
  columns:[...],
  onClick:{
    toggleplay:togglePlayback,
    stop:stopPlayback,
    progress:setProgress
  }

Для каждого обработчика виджет передаст объект события, а также id ряда, внутри которого произошел клик.

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

  const audio = $$("audio");
  const table = $$("table");

В дальнейшем мы будем обращаться к виджетам именно через эти переменные.

Запуск трека и пауза

Теперь давайте займемся непосредственно обработчиками. А начнем мы с функции togglePlayback(), которая будет запускать трек, если он остановлен, и останавливать, если он проигрывается. В качестве аргумента она принимает id ряда, в котором находится кнопка. С помощью метода таблицы getItem() мы получаем объект данных ряда по его id и сохраняем объект в переменную item.

Дальше мы переходим к работе непосредственно с плеером, чтобы запустить трек . Для того чтобы дождаться полной загрузки плеера, мы вызываем его метод getPlyr(), который возвращает промис. Когда промис выполнится, внутри resolve функции метода then() мы получим доступ к объекту плеера с его API. Код функции выглядит так:

  function togglePlayback(e, id){
    const item = this.getItem(id);
    audio.getPlyr(true).then(plyr => {...});
  };

Внутри колбэк-функции метода then(plyr => {…}) нам нужно синхронизировать состояние кнопки toggleplay в выбранном ряде с работой плеера. Это значит, что если пользователь кликнет по кнопке в состоянии play (false), функция должна запустить плеер и заменить состояние кнопки на pause (true). При повторном клике должно произойти обратное. Давайте рассмотрим детальнее как это сделать.

Сначала нам нужно сохранить id выбранного ряда в свойство плеера $linkedRow. Это нужно сделать для того, чтобы сверять id текущего трека с выбранным в таблице при повторном клике или клике по кнопке из другого ряда:

  plyr => {
    audio.$linkedRow = id.row;
  }

Когда мы первый раз запускаем трек, нужно передать плееру данные о нем. Сделать это можно при помощи метода плеера define(), который принимает объект с соответствующими настройками и данными о треке:

  plyr => {
    audio.$linkedRow = id.row;
    audio.define({
      source:{
        type:"audio",
        sources:[ webix.copy(item.source) ]
      }
    });
  }

В объекте свойства source мы задаем тип файла через свойство type и его источник через свойство sources. Источник мы берем из объекта item выбранного ряда и копируем его при помощи метода webix.copy(). Сделать это нужно для того, чтобы плеер не смог каким либо образом изменить исходные данные о треке в таблице.

Теперь давайте запустим сам трек. Для этого мы воспользуемся методом плеера play(). Может возникнуть ситуация, когда трек еще не загрузился, а плеер начнет его проигрывать. Чтобы избежать ошибки, мы начнем проигрывать трек только тогда, когда он полностью загрузился. Чтобы это сделать, нам необходимо вызвать метод плеера once() с ключем “canplay”, а затем уже запустить трек методом play(). Выглядит это так:

  //…
  audio.$linkedRow = id.row;
  audio.define({...});
  plyr.once("canplay", () => plyr.play());

Теперь нам нужно изменить состояние кнопки, по которой пользователь кликнул в таблице. Делаем мы это с помощью метода таблицы updateItem(), которому передаем id выбранного ряда и объект с измененными данными. Стоит отметить, что при создании шаблона кнопки toggleplay, мы задали ему следующее выражение:

${obj.$isPlaying ? "mdi-pause" : "mdi-play"}

Если изменить свойство $isPlaying на true или false, иконка шаблона изменится соответственно на pause и play:

  //...  
  audio.$linkedRow = id.row;
  audio.define({...});
  plyr.once("canplay", () => plyr.play());
  this.updateItem(id.row, { $isPlaying: true });

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

  plyr => {
    if(audio.$linkedRow == id.row){
      const state = item.$isPlaying;
      plyr.togglePlay(!state);
      this.updateItem(id.row, {
        $isPlaying:!state
      });

      return false;
    };
    //...
  }

В этом блоке мы получаем текущее состояние кнопки через ее свойство $isPlaying и сохраняем его в переменную state. Далее нам нужно передать обратное значение этого состояния методу плеера togglePlay(), который запустит или остановит воспроизведение трека в зависимости от переданного значения. Если состояние кнопки было true (трек проигрывается а иконка pause), то обработчик поменяет его на false и поставит трек на паузу. После этого мы обновляем состояние кнопки в таблице через ее метод updateItem() и прекращаем выполнение обработчика методом return.

Мы предусмотрели ситуации, в которых пользователь запускает трек впервые и повторно кликает по кнопке. Давайте предусмотрим и такой сценарий, когда пользователь кликает по кнопке другого трека. Здесь нам нужно:

  • остановить проигрывание текущего трека методом плеера stop()
  • сбросить состояние кнопки toggleplay через свойство $isPlaying
  • сбросить значение индикатора прогресса через свойство $progress
  • сбросить id текущего трека, который хранится в свойстве плеера $linkedRow
  • выполнить логику запуска трека по которому произошел клик.

Код выглядит так:

  plyr => {
    //...
    if(audio.$linkedRow && audio.$linkedRow !== id.row){
      plyr.stop();
      this.updateItem(audio.$linkedRow, {
        $progress:null,
        $isPlaying:false
      });
      audio.$linkedRow = null;
    }
    //...
  }

Теперь пользователь может запускать любой трек и ставить его на паузу.

Остановка трека и сброс контролов

Дальше у нас на очереди функция stopPlayback(), которая будет останавливать текущий трек в плеере и сбрасывать все контролы выбранного ряда в изначальное состояние. Код функции выглядит так:

  function stopPlayback(e, id){
    if(audio.$linkedRow == id.row){
      audio.getPlyr(true).then(plyr => {
        plyr.stop();
        this.updateItem(id.row, { $progress:null, $isPlaying:false });
        audio.$linkedRow = null;
      });
    }
  };

Для начала мы проверяем, совпадает ли id текущего трека с id выбранного ряда. Если они совпадают, мы вызываем метод плеера getPlyr() и получаем доступ к объекту с его API. Внутри колбэк-функции метода then() мы останавливаем текущий трек методом плеера stop(), сбрасываем индикатор прогресса и состояние кнопки toggleplay методом updateItem(), а затем обнуляем id текущего трека, который хранится в свойстве плеера $linkedRow.

Управление прогрессом

И у нас осталась функция setProgress(), которая будет регулировать состояние индикатора прогресса в таблице и время проигрывания трека в плеере. Когда пользователь кликнет по индикатору прогресса в таблице, функция заполнит контрол до нужного уровня и перемотает трек на соответствующее время проигрывания. Код функции выглядит так:

  function setProgress(e, id, node){
    if (audio.$linkedRow == id.row){
      const nodePos = webix.html.offset(node);
      const pointerPos = webix.html.pos(e);
      const progressPart = (pointerPos.x-nodePos.x)/nodePos.width;

      audio.getPlyr(true).then(plyr => {
        const fullTime = plyr.duration;
        const seekTime = Math.round((fullTime*progressPart) * 100) / 100;
        plyr.currentTime = seekTime;
      });
    }

    return false;
  };

Сначала мы проверяем, совпадает ли id текущего трека с id выбранного ряда. Дальше нам необходимо получить параметры расположения и размеры индикатора прогресса (nodePos), а также координаты курсора (pointerPos). Учитывая эти данные, мы вычисляем состояние прогресса в диапазоне от 0 до 1 (progressPart).

Дальше мы вызываем метод плеера getPlyr() и в resolve функции получаем доступ к объекту с его API. Мы можем получить полную продолжительность текущего трека через свойство плеера duration. После этого нам нужно вычислить новое время воспроизведения с учетом продолжительности трека и состояния прогресса в таблице (seekTime).

Полученное значение мы передаем плееру через его свойство currentTime и трек начинает проигрываться с указанного времени.

Чтобы обработчик события клика по индикатору прогресса не распространялся на другие события (например select), мы прекращаем его выполнение методом return, который возвращает значение false.

Теперь пользователи могут проигрывать треки, ставить их на паузу, перематывать и сбрасывать в первоначальное состояние при клике по соответствующим контролам в плейлисте. Но и это еще не все. При запуске трека, он начинает проигрываться, а индикатор прогресса в таблице двигаться не будет. Чтобы это исправить, надо подписаться на событие timeupdate в плеере и параллельно обновлять индикатор прогресса в таблице. Для этого мы снова воспользуемся методом плеера getPlyr() и обработаем нужные события в нём. Код выглядит так:

audio.getPlayer(true).then(plyr => {
  plyr.on("timeupdate", e => {
    const id = audio.$linkedRow;
    if (id){
      const item = table.getItem(id);
      const progress = Math.round((plyr.currentTime/plyr.duration) * 100);

      if (item.$progress != progress){
        table.updateItem(id, { $progress:progress });
      }
    }
  });
  //...
});

Помимо обновления индикатора прогресса, в этой же функции нам надо сбросить контролы в первоначальное состояние после завершения трека. Для этого мы подписываемся на событие плеера ended и выполняем сброс когда оно срабатывает. Выглядит это так:

  audio.getPlayer(true).then(plyr => {
    plyr.on("timeupdate", e => {});
    //...
    plyr.on("ended", e => {
      const id = audio.$linkedRow;
      if (id){
        table.updateItem(id, { $isPlaying:false });
      }
    });
  });

Добавляем загрузку треков

Чтобы наш плеер мог полноценно работать, нужно предоставить пользователю возможность загружать в плейлист новые треки. Давайте реализуем это с помощью виджета Uploader библиотеки Webix. Для этого нужно добавить объект с настройками компонента в конструктор приложения. Код выглядит так:

  {
    view:"uploader",
    value:"Upload your music",
    accept:"audio/*",
    inputWidth:200,
    multiple:true,
    autosend:false
  },
  //...

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

Давайте установим обработчик на событие onBeforeFileAdd, которое сработает до того, как виджет попытается загрузить файлы на сервер. С помощью этого обработчика, мы перехватим логику загрузки файлов виджетом Uploader и отправим данные о выбранном треке непосредственно в таблицу нашего плейлиста. Делаем мы это в объекте свойства on. Код виджета будет выглядеть так:

  {
    view:"uploader",
    //…
    on:{
        onBeforeFileAdd:addAudio      
    }
  }

Теперь нужно создать этот обработчик. Код функции будет выглядеть так:

  function addAudio(obj){        
    const file = obj.file;
    const reader = new FileReader();  

    reader.onload = function(e) {
      const track = e.target.result;
      table.add({
        songTitle:obj.name,
        source:{
          src:track,
          type:file.type
        }  
      });
    };          
    reader.readAsDataURL(file);
    return false;
  };

Функция принимает объект с выбранным файлом в качестве параметра. Для того чтобы загрузить этот файл в таблицу, нам нужно создать экземпляр класса FileReader. Через его свойство onload, мы задаем логику действий в обработчике события загрузки файла. В этом обработчике мы формируем объект данных и добавляем их в таблицу через ее метод add().

Когда обработчик события load готов, нам необходимо запустить считывание файла, после чего начнет выполнятся заданная в обработчике логика. Для этого мы используем метод readAsDataURL() класса FileReader и передаем ему выбранный файл в качестве аргумента.

Мы получили нужный файл и добавили его в таблицу. Так как в дальнейшем логика Uploader нам больше не понадобится, мы останавливаем ее при помощи return false в коде обработчика.

Теперь пользователь может загружать любимые треки и управлять ими через интерфейс плейлиста.

Заключение

В это статье мы подробно разобрали интеграцию стороннего плеера Plyr с компонентом DataTable библиотеки Webix. Мы создали небольшое приложение в виде плейлиста для проигрывания треков и рассмотрели несколько способов его реализации. Узнать больше о возможностях интеграции сторонних расширений с виджетами библиотеки Webix вы можете в соответствующей статье документации.