
По итогам четвёртого квартала 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 будет достаточен, чтобы пользователи наслаждались контентом с определенной долей стабильности. Для более крупных приложений, или приложений, специализирующихся на видео, стоит уже проработать более тонкий подход к работе с ошибками на основании рекомендаций, данных в этой статье. При этом всегда важно понимать, что количество попыток устранения проблем должно быть ограничено. Иначе можно столкнуться с зацикливанием.
И помните: то, насколько пользователям будет комфортно смотреть видео, — зависит от вас.