💡 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 работала в браузере отлично.

А потом я включил приставку.
📺 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.
Без файла, без строки, без стека. Я потратил кучу времени, пока не понял две вещи:
Script error.— это маскировка. Когда скрипт загружен с чужого домена (а плагин MediaInfo грузится с CDN), браузер прячет любую ошибку из него и отдаёт вwindow.onerrorголую строку без деталей. Лечится только локальнымtry/catchвнутри самого плагина — тогда видно настоящий текст.- Под маской пряталась реальная ошибка —
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

Хэш есть — нужен ffprobe. И тут приятная находка: в API TorrServer есть эндпоинт
GET /ffp/{hash}/{index}
Он запускает ffprobe прямо на файле раздачи и возвращает полный JSON. То есть можно получать дорожки локально, со своего сервера, не завися от чужого 185. Идеально.
Почти. На моей сборке MatriX.141 /ffp отвечает 400 — в Android-сборке нет бинаря ffprobe. Это частая история. Поэтому в плагине MediaInfo сделан гибрид:
- сначала пробуем локальный
/ffp; - если он недоступен (один раз получили
400— дальше уже не дёргаем, чтобы не терять время) — уходим на публичный Tracks Inspector185.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 выглядит так:

Слева показан путь браузерной версии Lampa, справа — Android TV с внешним плеером. Синие бейджи появляются мгновенно из названия раздачи, а детальная информация о дорожках и субтитрах догружается через ffprobe локально или через резервный анализатор.
Никаких патчей глобальных объектов — поэтому «Script error.» в принципе невозможен.
🛠️ Как пользоваться
- В 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

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