
По итогам четвёртого квартала 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/
Добавить комментарий