Создание децентрализованного музыкального плеера на IPFS +27

- такой же как Forbes, только лучше.


В этой статье описаны результаты двухмесячных экспериментов с IPFS. Главным итогом этих экспериментов стало создание proof-of-concept стримингового аудио плеера, способного формировать фонотеку исключительно на основе информации, публикуемой в распределённой сети IPFS, начиная с метаданных (название альбома, треклист, обложка), заканчивая непосредственно аудио-файлами.


Таким образом, будучи десктопным electron-приложением, плеер не зависит ни от одного централизованного ресурса.


IPFS


Как это работает


На первый взгляд технология напоминает собой что-то среднее между BitTorrent, DC, git и blockchain. На второй выясняется, что IPFS может хранить и распространять объекты любого вышеупомянутого протокола.


Начнём с того, что IPFS — это одноранговая сеть компьютеров, которые обмениваются данными. Для её формирования используется комбинация различных сетевых технологий, развивающаяся в рамках отдельного проекта libp2p. На каждом компьютере имеется специальный кэш, где до поры до времени хранятся данные, которые пользователь публиковал либо качал. Информация в кэше IPFS хранится в виде блоков и по умолчанию не закреплена. Как только места станет не хватать, местный garbage collector подчистит блоки, к которым давно не обращались. Методы pin / unpin отвечают за то, чтобы закрепить / открепить информацию в кэше.


Для каждого блока рассчитывается уникальный криптографический отпечаток его содержимого, который потом выступает в качестве ссылки на это содержимое в IPFS-пространстве. Блоки могут объединяться в merkle-tree объекты, узлы которых обладают собственным отпечатком, рассчитанным на основе отпечатков нижних узлов / блоков. Тут и начинается самое любопытное.


IPLD-DAG


IPFS состоит из большого количества более мелких проектов Protocol Labs, один из которых носит название IPLD (InterPlanetary Linked Data). Не уверен, что смогу грамотно объяснить суть, так что любопытным оставлю ссылку на спецификацию.


Если коротко, то описанная в предыдущем параграфе архитектура настолько гибкая, что на её основе можно воссоздавать более сложные структуры, такие как блокчейн, unix-подобная файловая система или репозиторий git. Для этого используется соответствующие интерфейсы / плагины.


Я пока работал исключительно с dag-интерфейсом, позволяющим публиковать в IPFS обычные JSON-объекты:


const obj = {
  simple: 'object'
}

const dagParams = { format: 'dag-cbor', hashAlg: 'sha3-512' }

const cid = await ipfs.dag.put(obj, dagParams)

В случае успеха возвращается объект контент-идентификатора (CID), внутри которого хранится уникальный отпечаток опубликованной информации, а также методы его конвертации в различные форматы. Вот таким образом можно получить base-строку.


const cidString = cid.toBaseEncodedString()
console.log(cidString) // zdpuAzE1oAAMpsfdoexcJv6PmL9UhE8nddUYGU32R98tzV5fv

Эта строка может использоваться для получения данных обратно при помощи метода get. При этом никаких дополнительных параметров уже не требуется, так как в строке содержится информация о формате данных и использованном алгоритме шифрования:


const obj = await ipfs.dag.get(cidString)
console.log(obj.value)
// {
//   simple: 'object'
// }

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


const obj2 = {
  complex: 'object',
  link: {
    '/' : cidString // cid-строка первого объекта
  }
}
const cid2 = await ipfs.dag.put(obj2, dagParams)
const cid2String = cid2.toBaseEncodedString()

При попытке получить obj2 обратно будут разрешены все внутренние ссылки, если специальным параметром не будет указано обратное.


const obj2 = await ipfs.dag.get(cid2String)
console.log(obj2.value)
// {
//   complex: "object",
//   link: {
//     "simple" : "object"
//   }
// }

Можно затребовать значение отдельного поля, для чего нужно к CID-строке добавить постфикс вида /название_поля.


const result = await ipfs.dag.get(cid2String+"/complex")
console.log(result.value)
// object

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


Концепция IPLD предполагает возможность ссылаться не только на другие DAG-объекты, но и любые другие IPLD-структуры. Таким образом, одно поле вашего объекта может ссылаться на git-коммит, а другое на биткоин-транзакцию.

Pub/Sub


Основополагающую роль в реализации задуманного функционала сыграл интерфейс pubsub. С его помощью можно подписываться на p2p-комнаты, объединённые специальной строкой-ключом, рассылать свои сообщения и принимать чужие.



const topic = 'межпространственное кабельное'

const receiveMsg = (msg) => {
// новое сообщение в виде буфера
}

await ipfs.pubsub.subscribe(topic, receiveMsg)

const msg = new Buffer('новость')

await ipfs.pubsub.publish(topic, msg)

Одним из первых приложений, основанных на pubsub, был irc-подобный p2p-чат orbit (до сих пор работает, если что). Подписываешься на канал, получаешь сообщения, история хранится между участниками и постепенно "забывается". Такой Vypress для всего Интернета.


И в go и в js версии IPFS pubsub-интерфейс включается специальным параметром, так как пока считается экспериментальной технологией.

Задумка


В какой-то момент своей аудиофильской жизни я заметил, что уже давно слушаю музыку исключительно при помощи Google Music. Причём не только на телефоне, но уже и на десктопе. Причин несколько, но главная — ощутимо меньшее количество кликов между "хочу послушать" и "слушаю" в большинстве повседневных ситуаций в сравнении с теми же торрентами. Вторая по важности причина — размер доступной фонотеки. Очень редко я не нахожу то, что ищу (гораздо чаще я встречаю региональные ограничения). Но это уже преимущество не перед торрентами, а другими стриминговыми сервисами.



Стало любопытно, можно ли создать сравнимое по юзабилити приложение, основанное исключительно на распределённых источниках данных. Со временем были сформулированы два главных условия, при которых эта задача становится выполнимой:


  • возможность напрямую отслеживать появление данных в распределённой сети,
  • публикуемые участниками сети данные должны быть стандартизированы.

В BitTorrent-экосистеме когда кому-то захочется что-то скачать ему в большинстве случаев придётся сначала обратиться к торрент-каталогу — обычному сайту, расположенному на обычном сервере за обычным ip под обычным доменом. Первое условие необходимо для того, чтобы приложение могло формировать локальную базу данных автоматически и без участия централизованных ресурсов.


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


Как оказалось, IPFS даёт возможность обеспечить оба эти условия, причём довольно эффективно.


Реализация


На сегодня "Патефон" представляет собой примитивную плеер-фонотеку. Вот практически вся его функциональность:


  • публикация альбомов в IPFS
  • список обнаруженных альбомов
  • возможность проиграть альбом

Начнём с публикации. В предыдущей главе я говорил о том, что информация в распределённой сети должна быть каким-то образом стандартизирована. В "Патефоне" эта задача достигается за счёт json-схемы альбома.


const albumSchema = {
  type: "object",
  properties: {
    title: {
      type: "string"
    },
    artist: {
      type: "string"
    },
    cover: {
      type: "string"
    },
    tracks: {
      type: "array"
      items: {
        type: "object",
        properties: {
          title: {
            type: "string"
          },
          artist: {
            type: "string"
          },
          hash: {
            type: "string"
          }
        }
      }
    }
  }
}

В соответствии с этой схемой конкретный экземпляр проверяется, а затем и публикуется. За это отвечает обычная форма ввода.



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



Важная деталь — прикреплённые файлы публикуются отдельно при помощи ipfs.files.add(). Вернувшиеся CID’ы вставляются в соответствующий инпут отдельной строкой. Инпут оставлен на случай, если файла под рукой нет, но известен его CID.

Когда форма заполнена и пользователь жмёт "ADD ALBUM" итоговые JSON-данные сперва проверяются на соответствие схеме, затем публикуются при помощи DAG-интерфейса, после чего полученный CID в виде буфера рассылается в специальной pubsub-комнате.



const isValid = validateAlbumObj(albumObj) // проверка данных формы на соответствие схеме

if (isValid) {
  const albumCid = await ipfs.dag.put(albumObj, dagParams) // публикация объекта альбома при помощи dag
  await ipfs.pubsub.publish(albumSchemaCidString, albumCid.buffer) // рассылка CID альбома в pubsub в виде буфера
}

Очевидным решением было бы использовать в качестве ключа комнаты что-то вроде "albums", "music-albums" или, в крайнем случае, "pathephone", если хочется изолировать сеть в рамках своего приложения. Но потом в голову пришла более умная мысль — использовать в качестве ключа CID-строку json-схемы альбома. В конце концов именно cхема данных по-настоящему объединяет все копии приложения. Более того, именно от этой комнаты можно ожидать, что все принимаемые сообщения будут содержать проверенные экземпляры этой схемы.



const handleReceivedMessage = async (message) => {
  try {
    const { data, from } = message // data - буфер полученных данных, from - id отправителя
    const cid = new CID(data).toBaseEncodedString() // предполагая, что полученный буфер является CID-отпечатком, создаём на его основе CID-объект и сразу же конвертируем в base-строку
    const { value } = await ipfs.dag.get(cid)
    const isValid = validateAlbumObj(value) // проверяем полученный объект на соответствие схеме альбома
    if (isValid) {
      // value - проверенный экземпляр альбома, делаем с ним всё, что душе угодно
    } else {
      throw new Error('invalid schema instance received')
    }
  } catch (error) {
    // обработка исключений
  }
}

const albumSchemaCid = await ipfs.dag.put(albumSchema, dagParams)
const albumSchemaCidString = albumSchemaCid.toBaseEncodedString()
await ipfs.pubsub.subscribe(
  albumSchemaCidString, handleReceivedMessage
)

Как видите, каждый "прилетающий" объект всё равно проверяется на соответствие схеме, так как никто не мешает любому участнику сети, зная ключ, целенаправленно начать флудить в комнате. В то же время уникальность ключа сводит к минимуму вероятность случайного вмешательства. С другой стороны, если какое-нибудь другое приложение начнёт пользоваться той же схемой, что и "Патефон", то они вместе начнут формировать и использовать одно и то же пространство данных, что может пойти в плюс.


За скобками остались некоторые специфичные для приложения вещи, вроде сохранения объекта в базе данных.


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

Metabin Gate


Всё описанное выше я оформил в виде npm-модуля под названием @metabin/gate. Он прячет детали, оставляя разработчику лишь передать ipfs-ноду и json-схему:



const gate = await openGate(
  ipfsNode, // запущенный экземпляр js-ipfs или js-ipfs-api
  schema // валидная json-schema,
  (instance, cid) => {
    // instance - проверенный экземпляр указанной схемы
    // cid - base-encoded отпечаток экземпляра
  }
)

await gate.send(instance) // публикация экземпляра

await gate.close()

Выводы


IPFS — одна из самых продвинутых технологий распределённого обмена информацией. Несмотря на то, что обе (и go и javascript) версии находятся в стадии альфы, ими уже пользуются некоторые относительно популярные приложения, вроде OpenBazaar или d.tube. Другое дело, что используют его в основном только в качестве файлового хранилища.


Это можно понять, учитывая что IPFS в основном и рассматривается как своеобразная альтернатива BitTorrent. Концепция IPLD и возможности соответствующих интерфейсов редко оказываются в центре внимания. Хотя, на мой взгляд, это самая перспективная разработка Protocol Labs, дающая возможность распространять через IPFS обычные объекты данных.


А совместив нехитрым образом files, dag и pubsub-интерфейсы разработчик получает бесплатный, самоорганизующийся источник данных для своего приложения. Добавьте Electron и получите довольно заманчивый стек технологий:


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

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


Тем не менее возможности и перспективы открываются любопытные.


Ссылки


Вы можете помочь и перевести немного средств на развитие сайта

Теги:



Комментарии (19):

  1. dmitryrf
    /#10561488

    1. Можно ли автоматически заполнять Title из имени файла?
    2. По-моему, не нужно над каждой строкой трека писать названия полей, достаточно одного заголовка сверху, а то при 10 треках половину экрана занимают названия полей
    3. После нажатия «Add album» долго крутился бублик, но ничего заметного после его исчезновения не произошло — то же самое окно «Share an album».

    • negamaxi
      /#10561658

      1. Лучше на основе метаданных, вроде id3 тегов. Названия файлов, насколько я помню, не все символы поддерживают.
      2. Форма сейчас на переработки, учтём.
      3. У себя на компьютере тоже такое замечал, пока не разобрался.

  2. unsafePtr
    /#10561560

    Мне вот интересно что произойдет если два одинаковых файла получат один хеш в сети. Как сеть избегает таких конфликтов?

    • kozyabka
      /#10561580

      использовать сразу несколько подписей разными алгоритмами?

  3. negamaxi
    /#10561666

    Если два одинаковых файла получают один и тот же хэш, значит IPFS работает как задумано.

  4. ValdikSS
    /#10561840

    negamaxi, вы пользуетесь какими-либо закрытыми музыкальными торрент-трекерами (желательно на gazelle)? Возьмите все лучшее с них: автозаполнение информации об альбоме из musicbrains, конкретные разрешенные форматы, фиксированные правила по называнию композиций и файлов, теги к каждому автору и альбому, система визуального отображения похожих исполнителей, система заявок.

    Я думаю над подобной системой уже 2 месяца. Основная идея: вся музыка бесплатна, хранится в распределенном виде между всеми участниками системы, с избыточностью. Особенности системы:
    * Децентрализованные метаданные (информация о всей музыке в системе) с централизованным управлением
    Метаданные должны синхронизироваться между всеми участниками обмена, но изменять их могут только администраторы. Это нужно для того, чтобы у нас не было дубликатов, низкокачественных транскодов, спама.

    * Жестко заданные форматы и централизованный транскодинг
    Современные аудиокодеки, такие как Opus, отлично звучат при 128 Кбит/с, что заметно сократит требования к пропускной способности канала, особенно если замахиваться на мобильные устройства, где либо оплата по трафику, либо ограничения трафика.
    Конечно, не нужно ограничиваться только Opus 128, должен быть выбор из «обычного» качества (Opus), «высокого» качества (Vorbis Q8), и FLAC. Централизованный транскодинг из FLAC в Opus и Vorbis.

    Opus 128 kbit/s:
    Пример 1: FLAC (22.9 МБ), Opus 128k (3 МБ)
    Пример 2: FLAC (25.2 МБ), Opus 128k (3.2 МБ)

    * Прямые платежи авторам песен и «общие» платежи а-ля Flattr
    Я часто покупаю музыку напрямую у музыкантов. Нужно, чтобы в системе у любого музыканта была возможность добавить свой кошелек какой-то криптовалюты. Нравится музыка — отправляешь средства напрямую музыканту, без посредников и комиссий (но это дожлен быть абсолютно точно не Bitcoin, из-за его чрезвычайно высоких комиссий и на текущий момент, и даже год назад).
    Также, общие платежи. Пользователь вносит на общий счет условные $10, система запоминает, музыку каких музыкантов вы прослушивали в течение месяца, и разделяет $10 между всеми авторами прослушанных композиций.

    * Продвижение исполнителей и альбомов
    Это ­— неотъемлемая и важная часть системы. Каждый день будет уникальный «альбом дня», который будет видеть любой пользователь, и, таким образом, знакомиться с новыми исполнителями. Исполнители должны будут подтвердить свой аккаунт, и только после этого смогут добавить свой адрес криптокошелька, чтобы пользователи могли им перечислять деньги. Подтверждение аккаунта требует от исполнителя оставить заметку (в соц. сетях или где-то еще), что его музыка доступна в системе, и что ее там можно слушать бесплатно. Исполнители будут получать деньги из счета для «общих» платежей, а также прямые платежи, если кому-то музыка очень понравилась.

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

    1. Невозможности отслеживания конкретных слушателей правообладателями, чтобы избежать штрафов за нарушение DMCA, как это сейчас в США, Великобритании, Германии и других странах за раздачу контента через Bittorrent.
    2. Улучшения связности из-за отсутствия необходимости иметь «открытый» порт и маршрутизируемый IP-адрес.
    Все это можно сделать на основе Tor, он, вопреки распространенному мнению, очень быстрый в качестве оверлейной сети (а медленные у него только Exit-ноды). Причем, те пользователи, которые находятся в странах, не подверженных DMCA, могут отключить анонимизицию (перевести Tor в режим 1 Hop).

    Если все реализовать грамотно, подобная система серьезно изменит музыкальную индустрию, я в этом совершенно уверен.

    • negamaxi
      /#10562838

      Как сказал мой друг, идей на пять лет разработки)

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

  5. granade18
    /#10562456

    Я не понимаю одного, плеер в названии это маркетинговый ход? Чем эта система отличается от других систем распределённого хранения файлов?

    • Scondo
      /#10562712 / +1

      Система хранения файлов — IPFS.
      «Плеер» в данном случае получается системой каталогизации и воспроизведения файлов, хранимых в этой системе. Как Google Music, с которым и сравнивается.

  6. ivan386
    /#10562606

    Для большей дедупликации mp3 файлов их можно делить на блоки особым образом: id3 в отдельных блоках, музыка в отдельных. Тогда у одинаковых mp3 с изменёнными тегами в теле будут одинаковые блоки. Ну и желательно использовать raw блоки для файлов. Тогда можно будет публиковать без копирования в репозитарий и вытаскавать из репозитария с републикацией тогда данные не будут занимать двойной объём.

    • negamaxi
      /#10562764

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

      По поводу отделения тегов от аудио-данных — метаданные и так хранятся в dag-обёртке, так что я бы уже говорил о выбрасывании встроенных тегов за ненадобностью. Но это уже надо с разными контейнерами / форматами разбираться. Либо, можно как ValdikSS предлагает, в один целевой формат всё транскодировать.

      Раз зашли на огонёк, при помощи IPFS можно как-нибудь стримить аудио с подстройкой качества? Самое простое, что приходит в голову — держать в dag-объекте для каждого трека массив альтернативных форматов (mp3-256, mp3-320, FLAC ...) и давать возможность пользователю указывать при проигрывании предпочитаемый, но это как-то аляповато.

      • ivan386
        /#10563002

        Выбрасывать теги из файла не советую так как кто-то может эти файлы закидывать себе на плеер потом. Для mp3 достаточно научиться читать фреймы. Самая большая непрерывная цепочка фреймов в файле и будет музыкой. Остальное теги различного формата. Код для распознования mp3 фреймов можно из Shareaza перевести.


        Примерно так думаю и работают интернет плееры. Им даётся список потоков в разном качестве и они переключаются между ними вовремя воспроизведения. Есть различные форматы. Вот информация в вики по этому вопросу Adaptive bitrate streaming.

  7. FreeMind2000
    /#10563848

    Народ, кто-нибудь видел хорошую статью на русском, как в IPFS компьютеры находят друг друга?

    Ведь в общем случае, если допустим в сети только 2 разбросанных по бескрайнему интернету компа без некой «подсказки» они друг друга не найдут… только перебор всех айпишников. У торрентов такая подсказка — это трекеры (слабое звено), хранящие БД адресов пиров, а что использует IPFS?

    • negamaxi
      /#10564010

      Я не так давно узнал, что DHT так же работает. Там есть такое понятие как bootstrap nodes — обычные сервера, у которых приложения получают первый список других участников распределённой сети. Потом уже эти сервера никак не участвуют, но всё же.

      Альтернатива — действительно перебор ip. Я его как-то пробовал ради эксперимента и первого DHT-пира нашло минут через пять, так что тоже рабочий вариант. Но не для варианта с двумя компами, само собой.

      Я так понял, чтобы стать участником любой распределённой сети нужно знать рабочую точку входа.

      • FreeMind2000
        /#10564102 / +1

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