ESPN DeportesCanelo y el método Usyk: ¿qué cambió en su preparación?ESPNInside the stunning 72-hour Lakers sale -- and the uncertainty that still loomsInquirerCalamba City sizzles as heat index hits 43°CThe Jerusalem PostEisenkot is hoping to beat Netanyahu, but what does the Yashar leader stand for? - explainerZDF heuteEntdecken Sie das ZDF-NachrichtenstudioVarietyNetflix Grapples With Digital-Ad Squeeze Amid Widening Glut of Streaming CommercialsCapital FMIEBC suspends Migwani ward by-election following court order7sur7Philippe et De Wever: les secrets d’un mariage contre natureVanguardChinese state TV suspends La Liga broadcasts after Mbappé remarkVilaWebLa participació en les eleccions d’Israel supera el 31% al migdia, la més alta d’ençà del 1973Wirtualna PolskaHołownia rozmawiał z Tuskiem. "Uzyskałem zapewnienie"CNN بالعربيةلقطات توثق اعتداءً مزعومًا على رجل يهودي في نيويورك.. واتهام امرأة بجرائم كراهية
The Daily Newsstand · Free, Always
Tuesday, October 27, 2026

Пропуск вшитых интеграций в TikTok на Android: анализ TTVideoEngine, ReVanced-патч и k-анонимность

Translate

В коротких видео формат рекламы давно сместился от официальных промо-постов к прямым интеграциям: авторы монтируют вставки (рулетки скинов CS:GO, платформы кейсов, телеграм-каналы) прямо в середину ролика.

Для YouTube эту проблему решает SponsorBlock. Мне стало интересно, насколько реально перенести подобную механику в Android-клиент TikTok. В процессе пришлось столкнуться с проприетарным плеером ByteDance, обфускацией байт-кода, оптимизацией вызовов к плееру и вопросами приватности запросов к базе таймкодов.

В этой статье описываю техническую сторону Proof of Concept: как устроен плеер TikTok, как стабильно перехватывать его методы через ReVanced и как организовать базу меток без слежки за пользователями.

1. Почему стандартные фильтры здесь не работают

Обычная блокировка рекламы в клиентах строится двумя путями:

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

  2. Фильтрация ленты (Feed Filter): перехват ответа API /aweme/v1/feed/. Официальные рекламные ролики приходят с флагом is_ads: true, и патч просто вырезает их из списка до показа.

В случае со вшитой интеграцией ролик для приложения ничем не отличается от обычного контента:

{
  "aweme_id": "7421892381928391200",
  "desc": "Полезный гайд #gaming",
  "is_ads": false,
  "video": {
    "play_addr": { "url_list": ["https://v16m-default.akamaized.net/..."] },
    "duration": 48200
  }
}

Единственный рабочий вариант пропустить такую вставку — перехватить воспроизведение в самом приложении и в нужный момент программно вызвать перемотку (seekTo).

2. Анализ плеера TTVideoEngine и обфускация

TikTok не использует системный MediaPlayer или стандартный ExoPlayer. ByteDance использует собственный закрытый движок — TTVideoEngine, работающий в связке с нативными библиотеками libttvideoengine.so и libmdl.so.

APK-файл версий 36.x разбит более чем на 20 DEX-файлов:

  • Логика ленты и модели данных (Aweme) находятся в classes2.dex–classes5.dex.

  • Сам плеер TTVideoEngine расположен глубже (обычно classes16.dex или соседние).

Код плотно обфусцирован R8:

  • Имена классов сжаты: Lo6f/cO;, LX/0vR;.

  • Имена методов и полей случайные: LJJlJllJl(), LIZ(), LIZIZ().

  • При минорных обновлениях приложения имена меняются.

При этом TTVideoEngine является базовым SDK для разных сервисов ByteDance (например, он же используется в демонстрационном VEVodDemo от Volcengine). Поэтому сам класс com.ss.ttvideoengine.TTVideoEngine не переименовывается, а ключевые методы сохраняют сигнатуры типов:

  • play() -> void

  • getCurrentPlaybackTime() -> int (позиция в миллисекундах)

  • getDuration() -> int

  • seekTo(int, SeekCompletionListener) -> void

Слушатель завершения перемотки (SeekCompletionListener) в коде обфусцирован и выглядит как интерфейс с генерированным именем (например, Lo6f/c0;).

3. Поиск точек входа (Fingerprints) и хукинг

Чтобы патч не ломался при каждом обновлении TikTok, вместо поиска по временным именам методов используется поиск по типам аргументов и возвращаемых значений через ReVanced Patcher:

// Fingerprints.kt

// Старт воспроизведения: метод без параметров с возвратом void
internal val BytecodePatchContext.enginePlayMethod by gettingFirstMethodDeclaratively {
    name("play")
    definingClass("Lcom/ss/ttvideoengine/TTVideoEngine;")
    returnType("V")
    parameterTypes()
}

// Перемотка: int (позиция) + обфусцированный интерфейс слушателя
internal val BytecodePatchContext.engineSeekMethod by gettingFirstMethodDeclaratively {
    definingClass("Lcom/ss/ttvideoengine/TTVideoEngine;")
    returnType("V")
    parameterTypes("I", "Lo6f/c0;")
}

Для проверки точек входа до сборки APK используется скрипт на базе androguard (find_hooks.py), который парсит методы DEX-файлов:

CHECKS = [
    ("плеер: TTVideoEngine.play()V", "Lcom/ss/ttvideoengine/TTVideoEngine;", "play", "()V"),
    ("плеер: getCurrentPlaybackTime()", "Lcom/ss/ttvideoengine/TTVideoEngine;", "getCurrentPlaybackTime", "()"),
    ("плеер: getDuration()", "Lcom/ss/ttvideoengine/TTVideoEngine;", "getDuration", "()"),
    ("плеер: seekTo(I, …)", "Lcom/ss/ttvideoengine/TTVideoEngine;", "seekTo", "(I"),
    ("лента: getCurrentAweme()", "/BaseListFragmentPanel;", "getCurrentAweme", "()Lcom/ss/android/ugc/aweme/feed/model/Aweme;"),
]

Внедрение инструкций в Smali

В коде патча перехватываются ключевые события и передаются в Java-класс SponsorManager:

// Хук на старт воспроизведения
enginePlayMethod.addInstruction(
    0,
    "invoke-static/range { p0 .. p0 }, Lapp/revanced/extension/tiktok/mimo/SponsorManager;->onEnginePlay(Ljava/lang/Object;)V"
)

// Хук на перемотку: p1 содержит целевую позицию в миллисекундах
engineSeekMethod.addInstruction(
    0,
    "invoke-static/range { p0 .. p1 }, Lapp/revanced/extension/tiktok/mimo/SponsorManager;->onEngineSeek(Ljava/lang/Object;I)V"
)

Нюанс: важно использовать инструкцию invoke-static/range. Оптимизатор R8 часто выносит регистр this (p0) за пределы первых 16 регистров (> v15). Обычный invoke-static в таком случае приведёт к ошибке VerifyError при загрузке класса в рантайме.

4. Алгоритм автопропуска: оптимизация проверок таймлайна

Прямолинейный подход — опрашивать getCurrentPlaybackTime() по таймеру каждые 16 мс (60 кадров в секунду). Но постоянный JNI-поллинг в фоновом цикле нагружает процессор и может вызывать подёргивания интерфейса.

В SponsorManager.java интервал следующей проверки рассчитывается динамически — через деление расстояния до целевой точки (бисекция):

/**
 * Задержка до следующей проверки таймлайна.
 * Интервал равен половине расстояния до ближайшего сегмента.
 */
private static long nextDelay(long pos, Segment next, long dur) {
    long gap;
    if (next != null) {
        gap = next.startMs - pos;
    } else if (dur > 0 && !segments.isEmpty()) {
        // До конца ролика сегментов нет — целимся в первый сегмент следующего круга
        gap = dur - pos + segments.get(0).startMs;
    } else {
        return FAR_TICK_MS; // 1000 мс
    }
    return Math.max(MIN_TICK_MS, Math.min(FAR_TICK_MS, gap / 2));
}
  • Далеко от рекламы: проверка вызывается 1 раз в секунду (1000 мс).

  • При приближении к рекламному блоку: интервал делится пополам (gap / 2) и сокращается вплоть до одного кадра (16 мс) непосредственно перед прыжком.

В симуляции на 10 000 прогонов это снизило медианное число вызовов getCurrentPlaybackTime() с 60 до 1.6 вызова в секунду, не теряя точности момента перемотки.

Краевые случаи

  1. Склейка соседних меток: если несколько рекламных отрезков идут подряд (или с зазором менее 300 мс), они объединяются в один вызов перемотки без лишних микро-скачков.

  2. Ручная перемотка: если пользователь сам перемотал ползунок в середину рекламной вставки, хук onEngineSeek отмечает этот отрезок как просмотренный. Плеер не выбрасывает пользователя обратно.

  3. Защита от зацикливания (Loop Guard): если разметка ошибочна (занимает более 80% ролика или вызывает 3 прыжка подряд без реального воспроизведения контента), автопропуск для этого видео отключается.

5. Бэкенд: k-анонимность и Cloudflare Workers

Для работы нужна база таймкодов. Если клиент будет отправлять на сервер прямой идентификатор ролика (GET /api/segments?video_id=...), владелец сервера получит историю просмотров каждого пользователя.

Схема запросов с префиксом хэша

Для защиты приватности используется схема k-анонимности (аналогично SponsorBlock):

[Клиент]                                                 [Cloudflare Worker]
   │                                                              │
   ├─ ID ролика: "7421892381928391200"                            │
   ├─ SHA-256(ID) = "a3f1b80c2e..."                               │
   ├─ Префикс: "a3f1" (первые 2 байта / 16 бит)                   │
   │                                                              │
   ├────────────── GET /api/skip/v1/a3f1 ─────────────────────────>
   │                                                              │
   │                                                Поиск в базе D1:
   │                                                WHERE hash LIKE 'a3f1%'
   │                                                (плюс Cache API)
   │                                                              │
   <────────────── Ответ со всеми роликами корзины a3f1 ──────────┤
   │                                                              │
   ├─ Локальный поиск нужного хэша в полученном списке           │
   └─ Передача отрезков в плеер                                   │
  1. Клиент считает SHA-256 от ID ролика.

  2. На сервер отправляются только первые 4 символа hex (2 байта).

  3. Все ролики распределены по 2^{16} = 65\,536 корзинам.

  4. Сервер возвращает все известные ему отрезки для роликов с таким префиксом.

  5. Нужный ролик клиент находит уже локально в памяти. Сервер не знает, какое именно видео из корзины открыто на устройстве.

Серверная часть

Бэкенд реализован на Cloudflare Workers + базе D1:

  • Edge Cache: популярные корзины кешируются через Cache API в памяти дата-центра Cloudflare, не нагружая базу данных D1 при повторных запросах.

  • Анонимизация пользователей: идентификаторы пользователей при отправке меток не сохраняются в открытом виде — пишется только HMAC-SHA256(userID, PEPPER).

  • Консенсус: новые отметки валидируются по порогу голосов (система апвоутов/даунвоутов в consensus.ts). При накоплении отрицательных голосов сегмент скрывается из выдачи.

6. Итоги

В рамках этого Proof of Concept удалось решить технические задачи:

  1. Подтверждена возможность надежного перехвата TTVideoEngine через сигнатуры параметров без привязки к обфусцированным именам методов.

  2. Адаптивный шаг таймера снизил частоту опроса плеера до 1.6 вызова/сек.

  3. Протокол с 16-битным префиксом хэша решает задачу приватности при запросах к базе.

Поддерживать собственный модифицированный APK TikTok на постоянной основе я не планирую: сопровождение крупного стороннего клиента со всеми его UI-багами, региональными нюансами и регулярными перестроениями кода требует слишком много времени.

Цель статьи — зафиксировать рабочую логику и механику перехвата плеера. Надеюсь, описанный подход и сигнатуры пригодятся мейнтейнерам ReVanced или другим разработчикам, работающим с анализом Android-приложений.

Материалы PoC

  • Сигнатуры методов плеера: app/revanced/patches/tiktok/mimo/Fingerprints.kt

  • Логика бисекции и пропуска: app/revanced/extension/tiktok/mimo/SponsorManager.java

  • Спецификация API префиксов: эндпоинты /api/skip/v1/{prefix}

  • Логика консенсуса меток: worker/src/consensus.ts

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.