API получения метаданных через WebSocket

Адрес сокет сервера:

Общая схема:

  1. Клиент открывает лишь 1 соединение с сокет-сервером.
  2. Если соединение разрывается, клиент должен пытаться его восстановить, до тех пор, пока это не удастся.
  3. После открытия соединения, клиент должен подписаться на нужные ему метаданные (набор станций), а также единожды запросить набор метаданных (если это необходимо). В дальнейшем, клиент может менять конфигурацию подписки.
  4. После того как клиент подписался хотя бы на одну станцию, сокет-сервер будет периодически присылать обновления метаданных.
  5. Вся коммуникация между клиентом и сервером происходит в формате JSON.

IDs станций

Ключевую роль играют IDs станций. Именно по этим идентификаторам формируется подписка, а также их содержат данные присылаемые клиенту.





Формат данных, посылаемых клиентом серверу

Сервер поддерживает 3 команды:

Если используются все возможные команды сразу, то объект передаваемый серверу будет выглядеть так:

ws.send(JSON.stringify({
    subscribe: {
        // хотим получать только текущую песню для 4 станций
        current: ['rfm', 'rfm_70', 'rfm_80', 'rfm_90'],
        // а для выбранной станции и текущую и следующие
        current_and_next: ['ep'],   
    },
    // не хотим ничего более получать по станции Дорожное радио
    unsubscribe: ['dr'],
    fetch: {
        // хотим сразу получить, то что сейчас в эфире
        current: ['rfm', 'rfm_70', 'rfm_80', 'rfm_90'],
        // А для выбранной станции хотим и предыдующие, и текущую, и следующие получить, чтобы сразу все отобразить
        full: ['ep'],   
    },
}));

Сервер гарантирует, что на одну и ту же станцию нельзя подписаться более чем 1 раз (даже на разные типы). Если клиент был подписан на current_and_next: ['ep'], а потом подписался на current: ['ep'], то он будет будет получать данные согласно типу последней подписки на станцию, то есть лишь текущую композицию. Поэтому, вообще говоря, команда unsubscribe вряд ли нужна на практике, так как для переключения типов подписки достаточно лишь команды subscribe.

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

// При переключении с Европы+ на Ретро ФМ
ws.send(JSON.stringify({
    // Ни от чего не отписываемся, а только меняем тип подписки для 2 станций
    subscribe: {
        // Теперь для Европы+ достаточно отслеживать только текущую песню
        current: ['ep'],
        // а для Ретро ФМ и текущую и следующие
        current_and_next: ['rfm'],   
    },
    fetch: {
        // Чтобы отобразить слудующие песни и предыдущие сразу же,
        // запрашиваем полные метаданные для Ретро ФМ. 
        // Для Европы+ ничего не запрашиваем, потому что у нас и так должна быть текущая композиция.
        full: ['rfm'],   
    },
}));

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

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

API для единоразовых запросов

Если все потоки содержат метаданные и не надо отслеживать эфир станций, которые не играют в данный момент, то целесообразнее не устанавливать WebSocket соединение, а просто запросить данные через обычный http запрос по адресу

https://metadataws.hostingradio.ru/data/:stationId/current.json

Например:

await fetch('https://metadataws.hostingradio.ru/data/ep/current.json').then(res => res.json());

А дальше получать метаданные из потока.

Формат данных, присылаемых сервером

Все данные которые приходят от сервера, представляют собой объекты следующего вида

{
  station_id1: {
      current: {/* Объект метаданных */},
      previous: [/* Массив объектов метаданных */],
      next: [/* Массив объектов метаданных */],
  },
  station_id2: { 
    // то же, что и в предыдущем объекте
  }
  // любое количество объектов для разных станций 
}

Все объекты метаданных могут иметь следующий набор полей.

{
  type: 3, // 1 - реклама, 2 - не музыка, 3 - музыка
  startTime: 1574855886528, // время начала трека в эфире в мс
  dbId: 100500, // число
  title: "Symphony", // название песни
  artist: "Space", // имя исполнителя
  cover: "https://performer.emg.fm/thumb/api_300x300/songs/2017/21/5bc6590307fe10f18_1024x1024bb.jpg", // картика песни
  hook: "https://performer.emg.fm/upload/songs/hook/2017/c9/2beb/590307fe1108e.mp3", // ссылка на часть трека
}

Первые 2 поля присутсвуют всегда, остальные могут отсутствовать.

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

for(const [stationId, data] of Object.entries(JSON.parse(jsonFromServer))) {
    // работа с данными
}

Также в зависимости от типа подписки, любое из полей current, next и previous может отсутсвовать. Но если они есть, то формат именно таков, что current - это объект метаданных, а previous и next массивы объектов метаданных, даже если и пустые.

Пример кода для открытия соединения и подписки (можно выполнить прямо в консоли браузера)

ВАЖНО. При обрыве соединения клиент должен пытаться постоянно восстановить его и, после успешного соединения, вновь подписаться на нужные станции.

'use strict';

function connect() {
    console.log(`Trying to connect ...`);
    window.ws = new WebSocket('wss://metadataws.hostingradio.ru');

    ws.onopen = () => {
        console.log(`%cConnection opened`, `color: green`);
        ws.send(JSON.stringify({
            subscribe: {
                current: ['ep', 'rfm'],
            },
            fetch: {
                full: ['ep', 'rfm'],
            }   
        }));
    };

    ws.onerror = () => {};

    ws.onclose = (code, reason) => {
        console.warn(`Connection closed! `, code, reason);
        // Важно переоткрыть соединение в случае разрыва!
        connect();
    };

    ws.onmessage = (e) => {
        console.log(`A message from the server has been gotten! \n\n`, JSON.parse(e.data));
    };
}

connect();