🧩 Как я делал плагин MediaInfo для Lampa: дорожки и субтитры до запуска плеера

9 минут
Гик создаёт плагин Mediainfo для Lampa

💡 MediaInfo: c чего всё началось

Откуда взялся MediaInfo, плагин для Lampa?

Ко мне обратился администратор сайта tv-ch.ru с простой на первый взгляд просьбой: проверить плагин для Lampa — cub.red/plugin/tracks. Плагин MediaInfo показывает аудиодорожки и субтитры торрент-раздачи ещё до запуска плеера: удобно, когда хочешь заранее понять, есть ли нужная озвучка или сабы, и не качать гигабайты впустую.

В основном я занимаюсь IPTV. Но я посмотрел, как он работает, и подумал: «Сделаю быстрый аналог за вечер». В итоге вечер растянулся на полтора десятка версий, а по дороге пришлось залезть в исходники Lampa, в API TorrServer и даже в особенности нативного моста Android. Рассказываю весь путь честно — со всеми тупиками, потому что именно тупики тут самое интересное.

🎯 Что должен уметь плагин

Идея простая: рядом с каждой раздачей в списке торрентов показать компактные «бейджи» —

  • видео: разрешение, кодек, HDR/Dolby Vision, битрейт;
  • аудио: язык, кодек, число каналов, битрейт, признак Atmos;
  • субтитры: язык и формат (SRT/PGS/ASS).

Источник правды — ffprobe, утилита из FFmpeg, которая читает заголовок медиафайла и отдаёт полную структуру потоков. Вопрос лишь в том, откуда взять этот ffprobe в каждой среде, где крутится Lampa.

🌐 Браузерная версия — действительно за вечер

В браузере всё сложилось почти сразу. Lampa при открытии раздачи показывает список файлов внутри торрента и шлёт событие torrent_file (тип render). В этом событии у элемента уже есть:

  • el.torrent_hash — настоящий BitTorrent-хэш (btih);
  • часто el.ffprobe — готовый список потоков.

Логика тривиальна:

js

// Lampa уже дала ffprobe — рисуем мгновенно
if (el.ffprobe && el.ffprobe.length) {
    render(el.ffprobe);
    return;
}
// иначе спрашиваем сервер-анализатор по хэшу
queryServer(el.torrent_hash, render);

В качестве «спросить сервер» использовался публичный анализатор 185.204.0.61:8080 (он же Tracks Inspector): принимает btih и индекс файла, отдаёт JSON со streams. По HTTP GET /api?hash=…&index=… (быстро, если уже в кэше) и по WebSocket ws://host/?HASH&index=… (запускает анализ). Версия v1.14 работала в браузере отлично.

Первоначальный вид плагина MediaInfo
Первоначальный вид плагина MediaInfo

А потом я включил приставку.

📺 Android TV ломает всё

На Android TV Lampa почти всегда запускает внешний плеер. И тут выясняется фундаментальное:

  • событие torrent_file не приходит вообще — экрана со списком файлов внутри торрента просто нет, раздача сразу уходит в нативный плеер;
  • значит, нет ни el.torrent_hash, ни el.ffprobe — двух китов, на которых стояла браузерная версия.

Та самая «работающая» версия на ТВ показывала пустоту. Надо было искать хэш как-то иначе.

❌ Тупик №1: где взять btih

На Android TV стреляет другое событие — torrent (тип onenter), и в нём есть el.Link. Но это не magnet, а ссылка-редирект вида https://lampa.app/nnmclub/parsemagnet?id=…. По задумке она 302-редиректит на magnet:?xt=urn:btih:…, откуда и берётся хэш.

Логичный ход — сходить по ссылке и прочитать Location:

js

fetch(link, { redirect: 'manual' })  // хотим увидеть заголовок Location

И тут первая стена: в WebView Android TV такой запрос возвращает opaqueredirect — Location: null, тело пустое, хэш не вытащить. Браузер из соображений безопасности прячет редирект. Тупик.

❌ Тупик №2: «Script error.»

Раз ссылку прочитать нельзя, родилась другая идея: Lampa ведь сама как-то обращается к TorrServer (видно статус предзагрузки — пиры, сиды, буфер), значит, где-то летит запрос с хэшем. Давайте перехватим все сетевые вызовы — пропатчим window.fetch, XMLHttpRequest.prototype.open и window.WebSocket — и поймаем URL с хэшем.

Результат — лаконичное и бесполезное:

Message: Script error.

Без файла, без строки, без стека. Я потратил кучу времени, пока не понял две вещи:

  1. Script error. — это маскировка. Когда скрипт загружен с чужого домена (а плагин MediaInfo грузится с CDN), браузер прячет любую ошибку из него и отдаёт в window.onerror голую строку без деталей. Лечится только локальным try/catch внутри самого плагина — тогда видно настоящий текст.
  2. Под маской пряталась реальная ошибка — Illegal invocation: нативные fetch/WebSocket нельзя вызывать с чужим this в strict mode. Но даже если бы я это починил — перехват ничего бы не дал: на этой схеме Lampa отдаёт раздачу нативному коду одной строкой (AndroidJS.openTorrentLink(...)), и весь разговор с TorrServer идёт уже из Java, а не из JS. Ловить в JS было нечего.

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

📖 Разворот: читаем исходники Lampa

Я пошёл в исходники Lampa и нашёл три ключевые вещи.

Первое — в core/android.js видно, что именно происходит на ТВ:

js

function openTorrent(SERVER){
    AndroidJS.openTorrentLink(SERVER.object.MagnetUri || SERVER.object.Link, ...)
}

То есть нативу отдаётся MagnetUri либо Link. Подтвердилось: JS-слой к TorrServer не ходит.

Второе и самое важное — там же есть нативный HTTP-мост AndroidJS.httpReq, доступный из JS через Lampa.Reguest().native(...). Этот клиент ходит по сети нативно, в обход браузера: ему нет дела ни до opaqueredirect, ни до mixed-content, ни до CORS. Вот он, недостающий канал.

Третье — в window.Lampa экспортируются Torserver, Storage, Reguest и прочее. Можно пользоваться всем этим напрямую, без патчей.

🚀 Прорыв: локальный TorrServer

Дальше — диагностика прямо на приставке. Сделал отладочный плагин MediaInfo с зелёным логом поверх экрана (на ТВ ведь нет консоли) и вывел окружение. Картина:

  • platform.android = true, внешний плеер;
  • Lampa.Torserver.url() — пусто (адрес сервера держит натив, JS его не видит);
  • но /echo на http://127.0.0.1:8090 отвечает: MatriX.141.

Вот оно! На приставке крутится встроенный локальный TorrServer, и нативным запросом до него можно достучаться. А раз Lampa добавляет туда раздачу при предзагрузке, у TorrServer уже есть и хэш, и метаданные.

Запрашиваю список торрентов:

js

POST http://127.0.0.1:8090/torrents   {"action":"list"}

И получаю:

- [LAMPA] Мортал Комбат 2 | 158a641fc18774a19474ae349751189d56e6d90d

btih добыт. Без патчей, без opaqueredirect, чистым нативным запросом. (По дороге выяснилось, что таймеры в WebView замораживаются, пока активен внешний плеер, — поэтому опрос списка надо делать, пока пользователь ещё на экране раздач.)

Чтобы не зависеть от того, успел ли натив добавить раздачу, в финале я добавляю magnet в TorrServer сам:

js

POST /torrents   {"action":"add","link": Link, "save_to_db": false}
// → ответ содержит готовый btih, TorrServer сам резолвит parsemagnet

TorrServer следует по редиректу на своей стороне и возвращает хэш одним ответом. После получения хэша раздача убирается из сервера (drop), чтобы не засорять список.

🔬 Дорожки: локальный /ffp и резерв 185

Отладка MediaInfo на Android TV
Отладка MediaInfo на Android TV

Хэш есть — нужен ffprobe. И тут приятная находка: в API TorrServer есть эндпоинт

GET /ffp/{hash}/{index}

Он запускает ffprobe прямо на файле раздачи и возвращает полный JSON. То есть можно получать дорожки локально, со своего сервера, не завися от чужого 185. Идеально.

Почти. На моей сборке MatriX.141 /ffp отвечает 400 — в Android-сборке нет бинаря ffprobe. Это частая история. Поэтому в плагине MediaInfo сделан гибрид:

  1. сначала пробуем локальный /ffp;
  2. если он недоступен (один раз получили 400 — дальше уже не дёргаем, чтобы не терять время) — уходим на публичный Tracks Inspector 185.204.0.61 (тот самый, с которого всё начиналось) по HTTP+WebSocket.

Так плагин MediaInfo работает у всех: у кого сборка с ffprobe — всё локально и быстро; у кого нет — через резерв.

🏷️ Делаем как фильтр Lampa: разбор названия

Отдельный вопрос мучил: почему у Lampa фильтр по HDR/Dolby Vision/качеству срабатывает мгновенно для всех раздач, а мой плагин дёргает сеть на каждую строку?

Ответ нашёлся в components/torrents.js: Lampa не анализирует файлы — она парсит строку названия регулярками.

4K:           (4k|uhd)…|2160p|ultrahd
1080p:        fullhd|1080p
HDR:          [\[| ]hdr[10| |\]|,|$]
Dolby Vision: dolby vision

Названия всех раздач уже в памяти — отсюда и мгновенность. Это меня натолкнуло на двухслойную архитектуру, которая стала финальной:

  • Слой 1 — мгновенно из названия. Регулярками достаём разрешение, HDR/HDR10+/Dolby Vision и кодек, рисуем синий видео-чип сразу для всех строк. Ноль сети.
  • Слой 2 — обогащение по ffprobe. В очереди (по 2–3 за раз) добываем точные аудиодорожки, субтитры с форматами и битрейты и дорисовываем поверх. Если 185 не отдал цветовые поля — флаг HDR/DV всё равно остаётся из названия (слияние слоёв).

Получилось «лучшее из двух миров»: быстро, как у Lampa, и подробнее, чем у неё.

⚡ Скорость и кэш

Несколько важных оптимизаций под конец:

  • после первого 400 от /ffp локальный ffprobe помечается недоступным глобально — больше не теряем по 7,5 секунды на ретраи каждой строки;
  • результаты кэшируются в Lampa.Storage — кэш переживает перезапуск приложения;
  • верхние ~8 видимых строк тянутся автоматически, остальные — по фокусу, причём наведённая строка прыгает в начало очереди;
  • разрешение классифицируется по ширине, а не по высоте, — иначе широкоформатный 4K 3840x1606 ошибочно считался «1080p».

🏗️ Итоговая архитектура

После экспериментов итоговая схема работы плагина MediaInfo выглядит так:

Схема потока данных плагина MediaInfo для Lampa
Схема потока данных плагина MediaInfo для Lampa

Слева показан путь браузерной версии Lampa, справа — Android TV с внешним плеером. Синие бейджи появляются мгновенно из названия раздачи, а детальная информация о дорожках и субтитрах догружается через ffprobe локально или через резервный анализатор.

Никаких патчей глобальных объектов — поэтому «Script error.» в принципе невозможен.

🛠️ Как пользоваться

  1. В Lampa: Настройки → Расширения (Плагины) → Добавить и вставить ссылку через jsDelivr (именно jsDelivr — прямые raw-ссылки GitHub в Lampa часто не подхватываются):
https://cdn.jsdelivr.net/gh/IPTVgeek/lampa-plugins@main/mediainfo/plugin.js

jsDelivr кэширует @main примерно на сутки. Чтобы зафиксировать точную версию или сразу получить свежий код, укажите конкретный коммит вместо @main, например …@14290ed/mediainfo/plugin.js. Именно так плагин тестировался на приставке.

2. Зайти в любую раздачу — под строками появятся бейджи: 🔵 видео, 🟢 аудио, 🟡 субтитры. Точки ··· означают, что идёт обогащение ffprobe.

3. В браузере на экране списка файлов дорожки появляются сразу из готового el.ffprobe.

Особенности и ограничения, которые честно стоит знать:

  • ffprobe в TorrServer. Если в вашей сборке TorrServer есть бинарь ffprobe — данные берутся локально и быстро. Если нет (как в MatriX.141 под Android) — работает публичный резерв 185.204.0.61, который вам не принадлежит и может быть недоступен.
  • Замороженные таймеры. Пока открыт внешний плеер, JS в WebView «спит» — поэтому на ТВ инфо догружается, пока вы ещё в списке, а не в момент самого плеера.
  • Приватные трекеры. Для раздач, которых нет в индексе парсера, данные добываются через TorrServer/анализатор — это требует сети и работает не мгновенно.

📂 Исходники

Всё лежит здесь:

https://github.com/IPTVgeek/lampa-plugins → каталог mediainfo/

  • plugin.js — боевая версия;
  • debug.js — отладочная версия с экранным логом (удобно дорабатывать прямо на приставке).

Полезно знать тем, кто захочет доделывать:

  • для тестов на ТВ удобно грузить плагин через jsDelivr с конкретным хэшем коммита (а не @main — он кэшируется на сутки): https://cdn.jsdelivr.net/gh/IPTVgeek/lampa-plugins@КОММИТ/mediainfo/plugin.js
  • настройки вынесены в начало файла: AUTO_LIMIT (сколько верхних строк тянуть автоматически), CONCURRENCY (параллельность очереди), HOST185 (адрес резервного анализатора), DEBUG (лог в консоль);
  • ключевые точки расширения: parseTitle() — слой из названия, build() — форматирование ffprobe, fetchByHash()/resolveJob() — добыча данных.

🔮 Что можно улучшить дальше

  • Поставить на приставку сборку TorrServer с бинарём ffprobe — тогда всё работает локально, со всеми HDR-полями, и публичный 185 становится не нужен.
  • Разбивка по файлам для сезонных паков (ffprobe каждого эпизода).
  • Свои настройки в меню Lampa (вкл/выкл слои, выбор резервного сервера).

✍️ Послесловие

Главный урок этой истории: не патчить то, что можно спросить. Половина боли ушла, как только я перестал перехватывать глобальные объекты и начал пользоваться тем, что Lampa и так отдаёт — событиями, Lampa.Reguest().native() и локальным TorrServer. А «быстрый аналог за вечер» в итоге оказался полезнее оригинала: показывает больше (HDR/DV/Atmos, битрейты, форматы субтитров) и работает там, где раньше не работало вовсе — на Android TV с внешним плеером.

Спасибо администратору tv-ch.ru за задачу — без неё я бы не полез так глубоко.

— Гик IPTV


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

⬆️ Вверх