10.09.2026 523 материалов

Надёжная загрузка видео в React Native: как пережить сворачивание, убийство приложения и сотовую сеть

Обычный fetch() для загрузки видео умирает, как только пользователь свернёт приложение. Разбираем архитектуру, которая переживёт убийство процесса, потерю сети и перезапуск — и не превратит интерфейс в «вечный прогресс-бар на 37%».

Надёжная загрузка видео в React Native: как пережить сворачивание, убийство приложения и сотовую сеть

Обещание «загрузка завершена на 100%» — это ложь, если видео на сервере ещё не готово к воспроизведению. Настоящая надёжность начинается с признания: ваш JavaScript-контекст — эфемерная сущность, а единственный долговечный объект — запись в базе данных и состояние на сервере.

Проблема, о которой предпочитают не думать

Почти каждый туториал по загрузке файлов в React Native предлагает примерно одну и ту же конструкцию: вызвать fetch() с телом файла, отловить прогресс, обновить UI. Выглядит чисто. Работает — пока никто не свернёт приложение.

Стоит пользователю нажать «домой» или заблокировать экран, как iOS приостанавливает процесс, а Android может его убить вовсе. JavaScript-контекст уничтожается вместе со всеми промисами, замыканиями и обработчиками. Когда человек возвращается в приложение — никакой ошибки, никакого отката. Просто зависший прогресс-бар, который больше никуда не денется.

Это не баг конкретной библиотеки. Это фундаментальное ограничение мобильных платформ: JavaScript-рантайм не имеет права удерживать ресурсы ОС, когда приложение не на переднем плане. И пока разработчики пытаются обойти это «костылями» на уровне JS, проблема будет повторяться.

Что умеют (и чего не умеют) платформы

Чтобы построить надёжное решение, нужно сначала понять рамки. iOS и Android по-разному решают задачу фоновой передачи данных, и у каждого подхода есть жёсткие ограничения.

iOS предоставляет URLSession с фоновой конфигурацией — сессия, которая живёт отдельно от процесса приложения и управляется операционной системой. Но есть нюансы:

  • загружать можно только из файла на диске — потоки (streams) и объекты Data не поддерживаются;
  • нельзя использовать удобные completion-handler API — только делегаты;
  • путь к файлу должен быть стабильным URL, который ОС сможет прочитать даже после убийства вашего процесса.

Android использует foreground-сервис с типом dataSync, который показывает уведомление и удерживает процесс. Но и тут не всё гладко:

  • начиная с Android 14, тип сервиса обязателен;
  • Android 15 вводит лимит в 6 часов на каждые сутки для dataSync и mediaProcessing — после чего система вызывает Service.onTimeout();
  • если бюджет исчерпан, попытка запуска бросит ForegroundServiceStartNotAllowedException.

Важный нюанс: Service.onTimeout(int, int) не существует на Android 14 и ниже. Там система молча «замораживает» сервис без колбэка — не стоит строить логику восстановления, зависящую от этого механизма.

Архитектура: запись вместо промиса

Ключевая идея решения, которое предлагает разработчик Mason в своём гайде, сводится к простому принципу: промис эфемерен, а запись в базе — персистентна.

Прежде чем отправить первый байт, приложение создаёт запись в локальной SQLite-базе. Эта запись содержит идентификатор загрузки, путь к локальному файлу, URL для загрузки на сервер и текущее состояние. Весь жизненный цикл загрузки — это конечный автомат, описанный состояниями:

  • pending — ожидает начала
  • uploading — идёт передача
  • uploaded — байты на сервере, но видео ещё не обработано
  • processing — сервер транскодирует
  • ready — можно воспроизводить
  • failed — ошибка

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

Нативный транспорт — не опция, а необходимость

В экосистеме Expo есть несколько путей для загрузки файлов. Современный expo/fetch — удобный, но не поддерживает фоновые сессии. Старый FileSystem.uploadAsync имел параметр для фонового режима, но он объявлен deprecated начиная с SDK 54 и, по имеющимся данным, давал сбои на больших файлах.

Для загрузок, которые должны пережить сворачивание, остаётся один путь — нативные модули. Автор использует react-native-background-upload, который под капотом использует iOS URLSession и Android foreground-сервис. На Android модуль создаёт канал уведомлений, на iOS — работает через делегаты.

Здесь есть важное предупреждение для тех, кто выбирает библиотеку: начиная с React Native 0.82 возможность отказаться от New Architecture была удалена, а Expo SDK 55 (React Native 0.83) работает только на New Architecture. Если нативный модуль не адаптирован — это не «временное неудобство», а тупик. Прежде чем интегрировать библиотеку для фоновых загрузок, стоит проверить её статус совместимости.

Согласование после перезапуска

Самая хитрая часть — не сама загрузка, а поведение при возврате. После убийства процесса все подписки на события (progress, completed, error) исчезают. Прогресс-бар молчит. Что произошло с загрузкой — знает только сервер.

Алгоритм reconciliation выглядит так:

  1. При запуске приложения читаем из SQLite все записи с состояниями pending, uploading, uploaded или processing.
  2. Если у записи нет asset_id (загрузка не успела создать ресурс на сервере) — переводим в pending и ставим в очередь заново.
  3. Если asset_id есть — делаем запрос к API и спрашиваем сервер, в каком состоянии ресурс.
  4. Серверные состояния маппим в локальные и обновляем запись.

Автор формулирует принцип просто: доверяйте серверу, а не локальному состоянию. Это классический паттерн reconciliation — тот же подход, что используется в React при перерисовке DOM и в блокчинах при достижении консенсуса.

«100%» — не «готово»

Пожалуй, самый полезный из описанных паттернов — это разделение понятий «байты доставлены» и «видео готово». Когда прогресс-бар доходит до 100% на этапе uploaded, пользователь считает, что всё. Но на сервере ещё идёт транскодирование — HLS-сегментация, генерация превью, извлечение метаданных.

Если показать «Готово!» на этапе uploaded, а потом видео окажется недоступно — это категория тикетов в техподдержку, которую можно было не создавать. Правильный подход: состояние uploaded отображается как «Загрузка завершена, подготавливаем видео», и только переход в ready (подтверждённый вебхуком или поллингом от сервера) означает реальную готовность.

Автор приводит простую функцию маппинга состояний в текстовые метки — ничего революционного, но именно такие «мелочи» определяют качество UX.

Тест, который действительно проверяет архитектуру

Стандартные unit-тесты здесь бесполезны. Автор предлагает конкретный сценарий ручного тестирования:

  1. Начать загрузку файла ~200 МБ.
  2. На ~30% свернуть приложение.
  3. Заблокировать телефон на 2 минуты.
  4. Включить и выключить режим полёта (имитация потери и восстановления сети).
  5. Принудительно убить приложение.
  6. Открыть заново.

Корректное поведение: загрузка либо возобновилась в фоне, либо запись вернулась в состояние pending и была поставлена в очередь. Некорректное: прогресс-бар застыл на 30%, или на сервере создался дубликат ресурса.

Размер решения и дальнейшие шаги

Весь описанный подход укладывается примерно в 150 строк кода — не считая нативных модулей. Это не «минимально жизнеспособный продукт», а полная архитектура: хранилище, транспорт, reconciliation и UI.

Для продакшена автор рекомендует два дополнения:

  • Протокол с возобновлением (tus или S3 multipart upload). Разница между «загрузка пошла с нуля» и «загрузка продолжилась с места обрыва» на мобильной сети — это разница между функцией, которой пользуются, и функцией, от которой отказались.
  • Метрики на стороне сервера. Клиентские отчёты об успешности загрузок учитывают только те, которые дошли до конца — неудачи происходят в процессе, которого уже не существует. Считать нужно соотношение созданных ресурсов к тем, что дошли до состояния ready.

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