С заботой о комфортном просмотре: как команда VK справляется с высокой нагрузкой на видеоплеер на Android-устройствах

от автора

По итогам четвёртого квартала 2024 года только в VK Видео количество суточных просмотров выросло до 2,7 миллиарда, а месячная аудитория — до 72,2 миллиона человек. Часть этих просмотров приходится на Android-устройства.

Меня зовут Егор Баженов. Я Android-разработчик в команде VK Видео. В этой статье расскажу о специфике работы видеоплееров в Android-сервисах с большой нагрузкой, возможных ошибках и способах их исправления.

Немного контекста

Библиотека One Video — ключевой компонент большинства Android-сервисов экосистемы VK. Например, она интегрирована в видеоплееры мобильных версий ВКонтакте, VK Видео, VK Клипы, Дзен, VK Видео Live и других продуктов компании.

One Video написана на Kotlin, имеет много оптимизаций и использует в качестве базового решения ExoPlayer — самую популярную библиотеку для работы с видео под Android.

Но реализация библиотеки и ее интеграция вместе с плеером в приложение — не финал пути, ведь после внедрения нужно гарантировать, что плеер будет воспроизводить видео стабильно. И здесь Android-разработчики могут столкнуться с довольно большим набором распространенных ошибок.

Поиск источников ошибок

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

Логирование

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

Как выглядит стэктрейс ошибки
androidx.media3.exoplayer.ExoPlaybackException: Source error         at androidx.media3.exoplayer.ExoPlayerImplInternal.handleIoException(ExoPlayerImplInternal.java:736)         at androidx.media3.exoplayer.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:708)         at android.os.Handler.dispatchMessage(Handler.java:102)         at android.os.Looper.loopOnce(Looper.java:224)         at android.os.Looper.loop(Looper.java:318)         at android.os.HandlerThread.run(HandlerThread.java:67) Caused by: androidx.media3.datasource.HttpDataSource$HttpDataSourceException: java.io.IOException: java.util.concurrent.ExecutionException: com.vk.knet.core.exceptions.NoNetworkException: Exception in CronetUrlRequest: net::ERR_INTERNET_DISCONNECTED, ErrorCode=2, InternalErrorCode=-106, Retryable=false         at androidx.media3.datasource.okhttp.OkHttpDataSource.open(OkHttpDataSource.java:272)         at androidx.media3.datasource.ResolvingDataSource.open(ResolvingDataSource.java:110)         at androidx.media3.datasource.DefaultDataSource.open(DefaultDataSource.java:275)         at one.video.exo.datasource.CustomHttpDataSource.open(CustomHttpDataSource.kt:84)         at androidx.media3.datasource.StatsDataSource.open(StatsDataSource.java:86)         at androidx.media3.datasource.DataSourceInputStream.checkOpened(DataSourceInputStream.java:101)         at androidx.media3.datasource.DataSourceInputStream.open(DataSourceInputStream.java:64)         at androidx.media3.exoplayer.upstream.ParsingLoadable.load(ParsingLoadable.java:182)         at androidx.media3.exoplayer.upstream.Loader$LoadTask.run(Loader.java:421)         at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)         at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)         at java.lang.Thread.run(Thread.java:1012) 

Сбор статистики

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

  • модель устройства;

  • версию приложения;

  • тип ошибки;

  • код ошибки;

  • вид ошибки;

  • ID видео;

  • позицию;

  • общую длину видео и другие метрики.

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

График числа ошибок каждого кода

График числа ошибок каждого кода

Обратная связь

Собирать данные об ошибках можно с помощью обратной связи от пользователей. Чтобы реализовать такой обмен, мы не так давно внедрили в плеер отдельную кнопку, которая позволяет пользователю сообщить о возникших проблемах при просмотре. На основании текстового описания и данных из статистики мы можем оперативно помогать пользователям, даже если они не обращаются в поддержку.

Пример экрана плеера для сотрудников с иконкой обратной связи

Пример экрана плеера для сотрудников с иконкой обратной связи
Пример экрана плеера для пользователей с иконкой обратной связи

Пример экрана плеера для пользователей с иконкой обратной связи

Виды ошибок

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

Ошибки, генерируемые плеером, являются ExoPlaybackException. У этого класса есть поле типа, которое указывает на то, какой тип ошибок произошел. Всего их четыре:

  • TYPE_SOURCE — ошибка источника данных;

  • TYPE_RENDERER — сбой в рендерере (аудио/видео), например, несовместимый кодек или ошибка декодирования;

  • TYPE_UNEXPECTED — непредвиденная ошибка (например, исключение в коде ExoPlayer или сторонних библиотек);

  • TYPE_REMOTE — ошибка удаленного воспроизведения (например, при использовании Chromecast или Android TV). 

Но этих данных самих по себе недостаточно для того, чтобы понять природу возникновения ошибки. Вместе с тем, ExoPlaybackException является наследником класса PlaybackException, внутри которого есть поле, указывающее код ошибки. И суммарно этой информации уже хватает, чтобы сделать вывод о том, где и почему произошла ошибка.

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

Miscellaneous errors

Miscellaneous errors в Android — общая группа ошибок, которые не относятся к стандартным категориям (например, сетевым, аппаратным или ошибкам конкретных приложений). Они могут возникать из-за разных причин, включая программные конфликты, неправильные настройки, поврежденные данные или временные сбои системы.

Подробное описание ошибок
  /** Caused by an error whose cause could not be identified. */   public static final int ERROR_CODE_UNSPECIFIED = 1000;    /**    * Caused by an unidentified error in a remote Player, which is a Player that runs on a different    * host or process.    */   public static final int ERROR_CODE_REMOTE_ERROR = 1001;    /** Caused by the loading position falling behind the sliding window of available live content. */   public static final int ERROR_CODE_BEHIND_LIVE_WINDOW = 1002;    /** Caused by a generic timeout. */   public static final int ERROR_CODE_TIMEOUT = 1003;    /**    * Caused by a failed runtime check.    *    * <p>This can happen when the application fails to comply with the player's API requirements (for    * example, by passing invalid arguments), or when the player reaches an invalid state.    */   public static final int ERROR_CODE_FAILED_RUNTIME_CHECK = 1004; 

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

Лучшей практикой для их поиска и устранения является анализ логов и отслеживание закономерностей их возникновения. Если закономерности нет, но ошибки продолжают появляться, — стоит добавить workaround в виде перезагрузки плеера. В большинстве случаев это поможет, так как пересоздание обнуляет текущий стейт плеера, за счет чего вероятность возникновения подобных ошибок уменьшается.

Input/Output errors

Ошибки Input/Output (I/O) errors в Android связаны с проблемами чтения или записи данных на внутреннюю память устройства или внешние накопители (например, SD-карту). Они часто возникают из-за сбоев в работе файловой системы, повреждения носителя, аппаратных неисправностей или конфликтов программного обеспечения.

Подробное описание ошибок
/** Caused by an Input/Output error which could not be identified. */ public static final int ERROR_CODE_IO_UNSPECIFIED = 2000;   /** * Caused by a network connection failure. * * <p>The following is a non-exhaustive list of possible reasons: * * <ul> *   <li>There is no network connectivity (you can check this by querying {@link *       ConnectivityManager#getActiveNetwork}). *   <li>The URL's domain is misspelled or does not exist. *   <li>The target host is unreachable. *   <li>The server unexpectedly closes the connection. * </ul> */ public static final int ERROR_CODE_IO_NETWORK_CONNECTION_FAILED = 2001;   /** Caused by a network timeout, meaning the server is taking too long to fulfill a request. */ public static final int ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT = 2002;   /** * Caused by a server returning a resource with an invalid "Content-Type" HTTP header value. * * <p>For example, this can happen when the player is expecting a piece of media, but the server * returns a paywall HTML page, with content type "text/html". */ public static final int ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE = 2003;   /** Caused by an HTTP server returning an unexpected HTTP response status code. */ public static final int ERROR_CODE_IO_BAD_HTTP_STATUS = 2004;   /** Caused by a non-existent file. */ public static final int ERROR_CODE_IO_FILE_NOT_FOUND = 2005;   /** * Caused by lack of permission to perform an IO operation. For example, lack of permission to * access internet or external storage. */ public static final int ERROR_CODE_IO_NO_PERMISSION = 2006;   /** * Caused by the player trying to access cleartext HTTP traffic (meaning http:// rather than * https://) when the app's Network Security Configuration does not permit it. * * <p>See <a * href="https://developer.android.com/guide/topics/media/issues/cleartext-not-permitted">this * corresponding troubleshooting topic</a>. */ public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 2007;   /** Caused by reading data out of the data bound. */ public static final int ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE = 2008;

Ошибки, связанные с выводом и вводом, являются самыми частыми при работе плеера, так как они зависят не только от самого плеера, но и от данных, которые приходят. 

Здесь стоит подробнее остановиться на нескольких кодах ошибок.

Ошибки сети ERROR_CODE_IO_NETWORK_CONNECTION_FAILED и ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT  

Подобные ошибки возникают из-за проблем доступности контента по сети. Основные причины: 

  • недоступность хоста; 

  • разрыв соединения со стороны сервера;

  • отсутствие стабильного интернет-соединения. 

И если с отсутствием сети мы, как разработчики, справиться не можем, то для решения проблем с недоступностью хоста есть эффективное решение — запасной (failover) хост, который бэк предоставляет для переключения на него. 

То есть, при возникновении такого типа ошибок следует попробовать изменить хост в ссылке доступа к видео. Если это не помогло, надо сообщить пользователю о том, что у него нестабильное интернет-подключение, и ему следует подключиться к другой сети.

Ошибки ERROR_CODE_IO_BAD_HTTP_STATUS

Ошибка ERROR_CODE_IO_BAD_HTTP_STATUS в Android обычно возникает при работе с сетевыми запросами, особенно в контексте мультимедийных приложений (например, при использовании ExoPlayer или других библиотек для потоковой передачи данных). Эта ошибка указывает на то, что сервер вернул HTTP-статус, который клиентское приложение не может корректно обработать.

К таким относятся хорошо знакомые всем коды 400, 403, 404, 500. 

Среди причин возникновения ошибки может быть: некорректный URL, ошибки на сервере, отсутствие доступа, ограничения сети или даже устаревшие данные. Соответственно, чтобы исправить ошибки типа ERROR_CODE_IO_BAD_HTTP_STATUS можно:

  • проверить и повторно запросить URL у бэкенда;

  • проанализировать HTTP-статус: перехватить код статуса из ошибки и проверить его значение;

  • проверить доступность ресурса.

Также может быть полезным повторный запрос ссылки и попытка проигрывания на failover хосте — это позволит исключить посторонние факторы, из-за которых возникают проблемы.

Content parsing errors

Ошибки Content parsing errors в Android возникают, когда приложение не может корректно обработать (распарсить) данные, полученные из внешних источников (например, JSON/XML с сервера, данные из файла или базы данных). Эти ошибки связаны с несоответствием структуры или формата данных ожидаемой модели.

Подробное описание ошибок
/** Caused by a parsing error associated with a media container format bitstream. */ public static final int ERROR_CODE_PARSING_CONTAINER_MALFORMED = 3001;   /** * Caused by a parsing error associated with a media manifest. Examples of a media manifest are a * DASH or a SmoothStreaming manifest, or an HLS playlist. */ public static final int ERROR_CODE_PARSING_MANIFEST_MALFORMED = 3002;   /** * Caused by attempting to extract a file with an unsupported media container format, or an * unsupported media container feature. */ public static final int ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED = 3003;   /** * Caused by an unsupported feature in a media manifest. Examples of a media manifest are a DASH * or a SmoothStreaming manifest, or an HLS playlist. */ public static final int ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED = 3004;

В контексте видеоплееров данный тип ошибок связан с проблемами манифеста, в частности, DASH и HLS. Они имеют особую структуру, на основании которой плеер подбирает нужную видеодорожку для проигрывания. 

Зачастую проблемы возникают из-за неверного составления манифеста на стороне бэкенда. Поэтому для исправления ошибки также стоит сделать повторный запрос.

Помимо этого, полезно будет добавить логирование манифеста. Для этого надо расширить стандартные dash- и hls-парсеры (DashManifestParser, HlsPlaylistParserFactory) таким образом, чтобы при вычитке данных полный текст манифеста попадал в статистику. Это поможет найти проблемные места и быстрее пофиксить проблемы.

Decoding errors

Ошибки Decoding errors в Android возникают, когда приложение не может преобразовать данные из одного формата в другой. Это часто связано с обработкой медиафайлов (изображения, видео, аудио), бинарных данных (например, Protobuf), шифрованных данных (Base64) или сетевых ответов. Ошибки декодирования могут привести к падению приложения, некорректному отображению контента или потере информации. 

Подробное описание ошибок
/** Caused by a decoder initialization failure. */ public static final int ERROR_CODE_DECODER_INIT_FAILED = 4001;   /** Caused by a decoder query failure. */ public static final int ERROR_CODE_DECODER_QUERY_FAILED = 4002;   /** Caused by a failure while trying to decode media samples. */ public static final int ERROR_CODE_DECODING_FAILED = 4003;   /** Caused by trying to decode content whose format exceeds the capabilities of the device. */ public static final int ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES = 4004;   /** Caused by trying to decode content whose format is not supported. */ public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 4005; /** Caused by higher priority task reclaiming resources needed for decoding. */ @UnstableApi public static final int ERROR_CODE_DECODING_RESOURCES_RECLAIMED = 4006;

Ошибки декодирования — самые сложные для отлова и устранения. Это связано с тем, что возникают они не только из-за состояния системы и других запущенных приложений на устройстве, но также из-за реализации вендорами самих устройств.

Для наглядности можно посмотреть диаграммы, где видно, на каких устройствах и кодеках чаще всего встречаются такие ошибки.

Диаграмма

Распределение ошибок по устройствам
Диаграмма

Распределение ошибок оп кодекам

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

В данном случае самым очевидным способом решения проблемы является перезапуск плеера. Это позволит освободить ресурсы, ввиду чего проблема может быть исправлена. 

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

Также с ошибками типа Decoding errors может помочь приоритизация кодеков. Подход основан на том, что зачастую у телефона есть несколько реализаций одного и того же кодека (например, hardware и software), а переключение между ними при ошибках позволит выбирать наиболее надежный для проигрывания. Для реализации этого метода следует расширить MediaCodecSelector и добавить туда логику выбора кодеков. Пример расширения MediaCodecSelector представлен ниже.

internal class OneVideoMediaCodecSelector(private val lessPriorityCodecsProvider: () -> List<String>) : MediaCodecSelector {    override fun getDecoderInfos(        mimeType: String,        requiresSecureDecoder: Boolean,        requiresTunnelingDecoder: Boolean    ): MutableList<MediaCodecInfo> {        val lessPriorityCodecs = lessPriorityCodecsProvider.invoke()        return MediaCodecUtil.getDecoderInfos(mimeType, requiresSecureDecoder, requiresTunnelingDecoder)            .sortedBy {                lessPriorityCodecs.indexOf(it.name)            }            .toMutableList()    } }

AudioTrack errors

Ошибки AudioTrack errors в Android связаны с работой класса AudioTrack, который используется для воспроизведения аудиопотоков. Эти ошибки возникают при создании аудиодорожки, записи данных или управлении воспроизведением.

Подробное описание ошибок
/** Caused by an AudioTrack initialization failure. */ public static final int ERROR_CODE_AUDIO_TRACK_INIT_FAILED = 5001;   /** Caused by an AudioTrack write operation failure. */ public static final int ERROR_CODE_AUDIO_TRACK_WRITE_FAILED = 5002;   /** Caused by an AudioTrack write operation failure in offload mode. */ public static final int ERROR_CODE_AUDIO_TRACK_OFFLOAD_WRITE_FAILED = 5003;   /** Caused by an AudioTrack init operation failure in offload mode. */ public static final int ERROR_CODE_AUDIO_TRACK_OFFLOAD_INIT_FAILED = 5004;

Данный тип ошибок отдаленно похож на предыдущий, но так как аудиокодеки требуют сильно меньше ресурсов, такие ошибки возникают сильно реже. 

В данном случае пересоздание плеера решает почти все проблемы и покрывает 99% всех пользовательских сценариев.

DRM errors

Ошибки DRM (Digital Rights Management) errors в Android возникают при работе с защищенным цифровым контентом (видео, аудио, книги), который требует авторизации и лицензирования для воспроизведения или использования. Эти ошибки связаны с системами защиты, такими как Widevine, PlayReady или FairPlay, и часто появляются в стриминговых приложениях или сервисах с платным контентом.

Подробное описание ошибок
/** Caused by an unspecified error related to DRM protection. */ public static final int ERROR_CODE_DRM_UNSPECIFIED = 6000;   /** * Caused by a chosen DRM protection scheme not being supported by the device. Examples of DRM * protection schemes are ClearKey and Widevine. */ public static final int ERROR_CODE_DRM_SCHEME_UNSUPPORTED = 6001;   /** Caused by a failure while provisioning the device. */ public static final int ERROR_CODE_DRM_PROVISIONING_FAILED = 6002;   /** * Caused by attempting to play incompatible DRM-protected content. * * <p>For example, this can happen when attempting to play a DRM protected stream using a scheme * (like Widevine) for which there is no corresponding license acquisition data (like a pssh box). */ public static final int ERROR_CODE_DRM_CONTENT_ERROR = 6003;   /** Caused by a failure while trying to obtain a license. */ public static final int ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED = 6004;   /** Caused by an operation being disallowed by a license policy. */ public static final int ERROR_CODE_DRM_DISALLOWED_OPERATION = 6005;   /** Caused by an error in the DRM system. */ public static final int ERROR_CODE_DRM_SYSTEM_ERROR = 6006;   /** Caused by the device having revoked DRM privileges. */ public static final int ERROR_CODE_DRM_DEVICE_REVOKED = 6007;   /** Caused by an expired DRM license being loaded into an open DRM session. */ public static final int ERROR_CODE_DRM_LICENSE_EXPIRED = 6008;

Frame processing errors

Ошибки Frame processing errors в Android связаны с проблемами обработки или рендеринга графических кадров (например, в играх, видео, AR/VR-приложениях или при работе с камерой). Они возникают, когда система не может корректно сгенерировать, обработать или отобразить кадр в заданное время, что приводит к задержкам, «фризам» интерфейса или падению приложений. 

Подробное описание ошибок
/** Caused by a failure when initializing a {@link VideoFrameProcessor}. */ @UnstableApi public static final int ERROR_CODE_VIDEO_FRAME_PROCESSOR_INIT_FAILED = 7000;   /** Caused by a failure when processing a video frame. */ @UnstableApi public static final int ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED = 7001;

Ошибки с процессингом фреймов у нас возникают крайне редко. Полностью решить эту проблему помогает перезагрузка плеера.

Реализация VideoErrorHandler 

VideoErrorHandler — механизм, используемый для обработки ошибок, возникающих при воспроизведении, загрузке или декодировании видео. Он позволяет обрабатывать сбои и реагировать на них, управляя состоянием плеера, например: показывать сообщение пользователю, повторять попытку воспроизведения или приоритезировать кодеки.

У нас VideoErrorHandler реализован следующим образом:

class VideoErrorResolver(    networkConfig: NetworkErrorResolver.Config = NetworkErrorResolver.Config.EMPTY,     decoderConfig: DecodeErrorResolver.Config = DecodeErrorResolver.Config.EMPTY ) {      private val resolvers = listOfNotNull(        InitDecoderErrorResolver(),        DecodeErrorResolver(decoderConfig),        SourceErrorResolver(),        ErrorInvalidCodeResolver(),        NetworkErrorResolver(networkConfig),        ManifestParsingResolver(),        UnexpectedErrorResolver()    )      fun resolve(error: Throwable?, videoSource: ExoSource?): List<ErrorCommand> {        return resolvers.firstNotNullOfOrNull { it.resolve(error, videoSource).nullIfEmpty() }            ?: listOf(ShowError)    }      fun reset() {        resolvers.forEach(ErrorResolver::reset)    } }

Стоит отметить, что каждый резолвер имеет свою зону ответственности. При этом каждая ошибка должна быть обработана одним обработчиком или несколькими (но только в случае, если один включает другой). Такой подход обеспечивает последовательность и четкость обработки ошибок. Для соблюдения данного контракта у нас реализованы тесты, которые исключают конфликты при расширении на новые резолверы.

На уровне кода наши резолверы имеют примерно следующую реализацию:

internal class UnexpectedErrorResolver : ErrorResolver {     private var isHandled = false     override fun resolve(error: Throwable?, videoSource: ExoSource?): List<ErrorCommand> {        return if (!isHandled && error is OneVideoPlaybackException && error.type == OneVideoPlaybackException.Type.UNEXPECTED) {            isHandled = true            listOf(ResetPlayer)        } else {            emptyList()        }    }     override fun reset() {        isHandled = false    } }

Для удобства масштабирования каждый из резолверов имеет единый код интерфейса. Также для большей гибкости некоторые резолверы имеют в качестве параметров конструктора специальный конфиг. Он помогает настраивать поведение определенных обработчиков ошибок под особые требования вендоров.

Ниже приведен код интеграции VideoErrorResolver в плеер. Сам резолвер должен реагировать на коллбек плеера onPlayerError, и на основании него принимать дальнейшие решения о том, какие меры предпринять для устранения проблемы.

override fun onPlayerError(error: PlaybackException) {    handleError(error) }   private fun handleError(error: Throwable?) {    val commands = errorResolver.resolve(error, videoSource)    videoTracker?.trackError(        /* exoSource = */ videoSource,        /* quality = */ quality,        /* error = */ error,        /* isUserGetError = */ commands.contains(ShowError)    )      commands.forEach {        when (it) {            ResetPlayer -> resetPlayer()            DecrementPlayerPool -> playerFactory.setPoolSize(PlayerPoolSize.MIN_POOL_SIZE)            is SwitchSource -> handleSourceError(it.source)            is PlayWithDelay -> handleNetworkError(it.delay)            ShowLostNetworkSnackbar -> handleLostNetwork()            ShowError -> makeError(error)            is DecoderFail -> lessPriorityCodecsContainer.addCodec(it.decoder)        }    } }

Вместо выводов

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

И помните: то, насколько пользователям будет комфортно смотреть видео, — зависит от вас. 


ссылка на оригинал статьи https://habr.com/ru/articles/894652/


Комментарии

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

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