Pull to refresh

Снова об Electron или рисуем музыку ВК

Reading time 7 min
Views 27K
image

Добра всем!
Electron — эта такая забавная штука, про которую мало статей на хабре(сходу нашел только habrahabr.ru/post/272075 и habrahabr.ru/post/278951). Давно хотел написать что-нибудь такое-эдакое, вот руки и дошли — заодно и одним велосипедом в мире станет больше.

Итак, если вкратце: electron — это такой гибрид node.js и chromium'а. Зачем? Очень разнообразный диапазон применений — мощное GUI(html/js/css), нехилая расширяемость(в том числе с возможностью использования других языков вроде C++ или C#), всякие приятности вроде jQuery и т.д. В-общем, удобная штука для разработки и дистрибуции standalone кроссплатформенных приложений.
Теперь о приложении. Оно реализует базовый пример расширения функционала стороннего сайта, базовые принципы работы с Raphael.js(графическая библиотека для отрисовки/анимации svg), Dancer.js(библиотека для визуализации звука, в данном случае — получения audio waveform).



Начнем со структуры проекта.

image

Package.json — это описание проекта с указанием точки входа(в нашем случае это index.js). Остальные файлы я обычно сую в папку views — названную так больше по привычке, нежели с желанием указать на ее назначение.

Теперь рассмотрим точку входа. На данный момент я сталкивался с 3 подходами на тему кооперации нодовского контекста и контента страницы: preload скрипт+нормальная навигация по страницам, iframe либо webview.
Webview мне, откровенно говоря, не нравится за счет отсутствия возможности прямой манипуляции контентом из родительского окна(нужно либо использовать IPC, либо глобальные объекты через remote.getGlobal(передается, что характерно, не ссылка на объект, а его, скажем прямо, хреновенький клон. К примеру, манипулировать содержимым window не выйдет). Впрочем, в нем же есть всякие вкусности вроде подмены referer, useragent, preload-скриптов и прочих штучек-дрючек. Отлично подходит, если не нужно работать с контентом страницы и
Iframe уже получше — есть прямой доступ к контенту(даже кроссдоменому, если вырубить web-security), но иногда бывают подставы в стиле проверки window.top. Думаете, здесь есть что-нибудь вроде nwfaketop из node-webkit? Эх, если бы. Я, к сожалению, нормального способа так и не нашел. Отлично подходит для большинства случаев, в которых нет проверки window.top и прочих радостей жизни. К сожалению, у vk они есть.
Третий способ, в общем-то, пожалуй самый простой, хотя и у него есть свои ограничения — к примеру, скрипты в страницах, в которых есть проверка на module/define/exports(типа jQuery или Raphael) радостно дохнут при виде нодовских кусков, а посему приходится вырубать node-integration и использовать нодовский контекст только в preload-скриптах. Впрочем, безопасность этого тоже требует, так что не слишком сильно и огорчаемся.

Index.js у меня, по большей части стандартный, из доков, за парой исключений:

"use strict";
(function () {
  var app = require('app');
  var BrowserWindow = require('browser-window');
  
  app.commandLine.appendSwitch('disable-web-security');
  app.commandLine.appendSwitch('web-security');
  app.commandLine.appendSwitch('allow-displaying-insecure-content');
  app.commandLine.appendSwitch('ignore-certificate-errors');

  var mainWindow = null;
  app.on('window-all-closed', function() {
    if (process.platform != 'darwin')
      app.quit();
  });

  app.on('ready', function() {
    mainWindow = new BrowserWindow({
        width: 800, 
        height: 600,
        'web-preferences': {
            'web-security': false
        },
        'node-integration': false,
        preload: __dirname + '/views/index.js'
    });
    
    
    mainWindow.webContents.session.webRequest.onBeforeRequest({}, (d,c)=>{
       if(d.url.indexOf('http://m.vk.com/js/s_c.js')==0){
            var localFile='file://'+__dirname+"/views/jsadditive/s_c.js";
            console.log(localFile);
            c({redirectURL: localFile});
       }
       else 
            c({cancel: false}) 
    });
    
    mainWindow.loadUrl('http://m.vk.com/audios1?performer=1&q=Tonight%20alive');
    
    mainWindow.toggleDevTools();
    mainWindow.on('closed', function() {
        mainWindow.removeAllListeners();   
        mainWindow = null;
    });
  });
}) ();


Первое отличие — пачка appendSwitch. Это местный переключатель флагов chromium'а. Первые 2 флага нужны потому, что я тупо не в курсе, какой из них реально отключает web-security(скорее всего, все-таки первый). 3 нужен для отключения ошибки подгрузки http контента из https сайта, а 4 — для игнорирования левых сертификатов(вот он, вроде как, не нужен, пытался исправить баг, связанный с тем, что провтыкал другой баг). Собственно, 2 и 4 переключатели можно и удалить.
Второе отличие — node-integration:false и preload-скрипт. Флаг отключает контекст ноды в браузере, preload скрипт содержит основную логику. Там же еще есть web-preferences, в данном случае выключают web-security для конкретного окна. Почему не срабатывают флаги хромиума — это уже второй вопрос.
Третье отличие — onBeforeRequest. Вообще в нем можно блокировать/подменять файлы по маске, тут используется для замены 1 из ВКшных файлов на локальную копию — более адекватного способа для привязки dancer.js к audio-объекту я, к сожалению, не придумал.
Ну и loadUrl подгружает мобильную версию vk потому, что оригинальная использует флеш для воспроизведения музыки, а из него извлечь аудиообъект несколько… сложнее.

Теперь давайте посмотрим на views/index.js. В данном случае этот файл отвечает за подгрузку локальных скриптов.

var fs=require('fs');
var include=(path)=>{
    var exports = undefined;
    (1,eval)(fs.readFileSync(__dirname+path, 'UTF-8'));
};
var link=(path)=>{
    var elem=document.createElement('link');
    elem.setAttribute('rel', 'stylesheet');
    elem.setAttribute('href', 'file://'+__dirname+path);
    document.head.appendChild(elem);
};

window.addEventListener('load', ()=>{
    link('/css/index.css');
    setTimeout(()=>{        
        include('/js/jquery-2.2.3.min.js');
        include('/js/raphael.js');
        include('/js/dancer.min.js');
        include('/js/main.js');
    }, 100);
});


Здесь уже идет гибрид клиентского и нодовского js'а. Хитрая хрень под названием include использует тот факт, что в замыкании мы можем переопределять даже вроде бы неизменяемые объекты типа window, а также то, что eval принимает аргументы из той же области видимости, вместо глобальной. Про фокус с (1,eval) можно почитать здесь, если вкратце — он выполниться из-под глобальной области видимости. Нужно это затем, что бы подключить те самые скрипты типа jQuery с проверкой на наличие exports.
link тупо и без затей подключает локальные css'ки.
Таймаут нужен для того, что бы стили успели примениться перед расчетом скриптов высоты/ширины блока, в котором будет происходить отрисовка.

Ну и, напоследок, основная часть. Осторожно — есть странный код, граничащий и переходящий далеко за черту говнокода.

jQuery('<div>').attr('id', 'output').insertBefore('#au_search_items');
var p=document.querySelector('#output');
var {offsetWidth: w, offsetHeight: h}=p;
var prev;
var time=10;
var mid=h/2;  
var step=10;
var chunks=w/step;
var baseArr=[];
var median=0;
var prevtime=0;
var r = Raphael("output", w, h);



var genPath = (x,isClosing=true)=>{
    var t=[];
    x.forEach((y)=>t.push(...[...y, ' ']));  
    return t.join(' ')+(isClosing?"z":"");
};

var anim, pathq;
var genRand=()=>{  
    var arr=[
        ['M', 0, mid]
    ];
    var baseW = 0;   
    var i=0;
    while(baseW<w)
        arr.push(['L', baseW+=step, baseArr[i++]]);       
    arr.push(['L', w, mid]);
    arr.push(['L', w, h]);
    arr.push(['L', 0, h]);
    
    pathq=genPath(arr, false);
    delete arr;
    
    if(!prev){
        prev=r.path(pathq).attr({
            //stroke: 'grey', 
            fill: 'grey'
        });
    }  
    
    /*if(!anim)
        anim = Raphael.animation({path: path}, time, "<>");
    anim.anim[Object.keys(anim.anim)[0]].path=path;*/
    //prev.animate(anim);
    prev.attr('path', pathq);
};



var dancer = new Dancer();
window.dancer=dancer;
dancer.bind('update', function(){
    var d=Date.now();
    if(d-prevtime>time)
        prevtime=d;
    else
        return;
        
    baseArr=[];     
    var waveForm=Array.from(this.getWaveform());
    var chunkLength=waveForm.length/chunks;
    while(waveForm.length>0)
        baseArr.push((waveForm.splice(0, chunkLength).reduce((a,b)=>a+b)/chunkLength)/((dancer.audio&&(dancer.audio.volume>0))?dancer.audio.volume:1)*h/2+h/2);    
    requestAnimationFrame(genRand);
});


Тут поподробнее.
В 1 же строчке мы добавляем контейнер, в котором будем отрисовывать пафосные графики перед ВК'шным плейлистом. Там еще немножко стилей есть, но их можно взять на гите.
Вот эта странная строчка var {offsetWidth: w, offsetHeight: h}=p; — это я так пытаюсь потихоньку привыкать к ES6. Штука эта имеет мудреное название «реструктуризующее присваивание и подробнее можно почитать здесь. Если вкратце-в данном случае это извлечение полей объекта в отдельные переменные с попутных их переименованием. Эквивалентом является что-нибудь вроде var w=p.offsetWidth. Вообще ES6 — это такая забавная штука, в которой много-много сахара. Если перебрать — можно получить проблемы со здоровьем(от тех, кто этот код будет поддерживать), но вообще очень вкусно и местами полезно.

Дальше идет моя глазовыедательная прелесссть- genPath. В ней сразу используются лямбы(стрелочные функции)(это такая хитрая форма записи обычных функций, в которой this всегда равен контексту того места, где была объявлена лямбда и нет той штуки из функций — arguments), параметры по умолчанию (isClosing=true) и spread operator(просто шикарная штука для склеивания массивов. К примеру, [1,2,3].push(...[4,5]) ->[1,2,3,4,5]. Впрочем, она используется далеко не только для этого, подробнее см. ссылку).
Функция эта нужна для того, что бы генерировать строку svg-пути из массива. Такой способ представления представляется мне несколько более читабельным.

Название функции genRand перекочевало из другого куска кода и нифига оно теперь не genRand, а genSvgFromThoseAudioWaveformWhateverIsIt. Впрочем, nobody cares.
Она, как следует из предполагаемого названия, генерирует svg path из audio waveforms, которое генерируется немного ниже и хранится в массиве baseArr. Ничего особенного здесь нет, единственное что можно раскомментировать stroke и получить обводку(а, закомментировав fill поблизости, получить что-то вроде кривой). Еще там же лежит закомментированный кусок кода, который позволяет реализовать более плавную анимацию ценой просадки cpu(у меня это ~20% против ~10%).
Кстати, отрисовка осуществляется при помощи библиотеки Raphael.js(см. выше).

Ну и, на закуску, dancer.
Это такая хитрая либа, которая умеет разбирать звуки на характеристики при помощи Web audio API. У них раньше была крутая демка здесь, но сейчас она немножко поломалась(у меня, по крайней мере).

Callback update работает при воспроизведении трека, this.getWaveform() возвращает набор амплитуд от 0 до 1, что позволяет нам далее визуализировать их.
Этот кусок кода требует двух уточнений. Первое — requestAnimationFrame. Это такая хитрая функция для планирования обновления анимации на экране. Если вкратце — дает неплохой прирост к скорости работы в некоторых случаях.
Второе — эта вот длинная строка про baseArr.push. Иногда просто хочется немножко странного кода. Конкретно этот бодро использует reduce, а вся строка разбивает массив на куски для отрисовки, находит среднее арифметическое по каждому куску, рассчитывает высоту на графике с учетом текущей максимальной громкости звука и склеивает это все в массив, который далее и используется для построения визуализации.

В-общем, это все.
Скачать проект можно здесь — github.com/demogoran/vkvisual.
Про electron подробнее почитать тут — github.com/electron/electron?utm_content=buffer703cb&utm_medium=social&utm_source=twitter.com&utm_campaign=buffer.
Установка собранного электрона — github.com/electron-userland/electron-prebuilt.

Если вкратце — при установленной ноде npm install -g electron-prebuilt + electron. из папки с проектом.

Всем спасибо за внимание, доброго времени суток и всего наилучшего!
Tags:
Hubs:
+12
Comments 149
Comments Comments 149

Articles