Перейти к основному содержимому

Глава 19: Высокоскоростное видео

Видео в замедленной съемке фиксирует моменты, которые человеческий глаз не может различить: капли воды, отрывающиеся от крана на 120 fps (замедление в 4×), взмахи крыльев колибри на 240 fps (замедление в 8×) или взрыв воздушного шарика на 960 fps (замедление в 32× на некоторых флагманах Samsung). Реализация захвата с высокой частотой кадров в Android Camera2 — это не просто установка SENSOR_FRAME_DURATION на малое значение — необходимо использовать специальный тип сессии CameraConstrainedHighSpeedCaptureSession, отправлять предварительно проверенные пакеты кадров через createHighSpeedRequestList и ограничивать размеры/разрешения вывода специфичным для устройства списком «одобренных для высокой скорости» конфигураций, возвращаемых SCALER_STREAM_CONFIGURATION_MAP.getHighSpeedVideoSizes.

Эта глава непосредственно основана на разделе High-Speed Sessions проектного исследовательского документа, который оценивает стоимость CPU при отправке 240 отдельных CaptureRequest в секунду (неприемлемо — до 70% загрузки CPU на Snapdragon 8 Gen 2, против < 5% с ограниченным списком пакета) и перечисляет точные ограничения, которые HAL налагает на количество выходов, диапазоны FPS и типы шаблонов. Вы можете узнать, какие диапазоны FPS поддерживает ваше устройство для каждого camera ID, в приложении Android Camera Parameters — также доступном в Google Play Store — которое отображает необработанный вывод getHighSpeedVideoSizes() и getHighSpeedVideoFpsRangesFor() на панели Stream Configurations.

Почему стандартные сессии не работают на 240 FPS

Прежде чем углубляться в выделенный высокоскоростной API, поймите, что делает 240 fps принципиально отличным от захвата на 30 fps:

  • Пропускная способность: Один кадр 1080p в 8-битном YUV_420_888 весит ~3,0 МБ. При 240 fps это 720 МБ/с пиксельных данных, проходящих через память — в 8× больше нагрузки на 30 fps, и достаточно, чтобы насытить линк MIPI D-PHY v1.2 на полной полосе пропускания.
  • Бюджет задержки: Один кадровый интервал при 240 fps составляет 4,167 мс. Если служба камеры Android тратит > 2 мс только на маршалинг CaptureRequest parcel из userspace в HAL, вы уже сожгли 50% бюджета до того, как сенсор начал экспозицию.
  • Допуск на джиттер: Индивидуальные вызовы capture() / setRepeatingRequest() проходят через мост Framework → CameraService → HAL через binder IPC, что вносит ±1 мс джиттера под нагрузкой. При 240 fps даже ±1 мс джиттера вызывает видимые несоответствия длительности кадров и дрейф синхронизации A/V.
  • Нагрузка на CPU: Каждый CaptureRequest требует создания объекта, маршалинга parcel, транзакции binder и валидации на стороне HAL. Выполнение этого 240×/сек в userspace было измерено исследовательской командой как 68–74% устойчивой загрузки CPU на Snapdragon 8 Gen 2 (Cortex-X3 + A715), что убьет плавность предпросмотра, разрядит батарею за 20 минут и приведет к сбою Thermal HAL задолго до того, как вы запишете пригодный клип.

Ограниченная высокоскоростная сессия захвата решает все эти проблемы, сворачивая N отдельных CaptureRequest в один предварительно проверенный список пакета, который аппаратный планировщик HAL потребляет напрямую, полностью обходя накладные расходы binder на каждый кадр.

Диаграмма делает архитектурное различие явным: стандартный путь имеет каскад binder IPC для каждого кадра, тогда как высокоскоростной путь строит и валидирует расписание один раз, а затем позволяет выделенному аппаратному секвенсору HAL поставлять кадры без перерывов.

Поддерживаемые диапазоны FPS и коэффициенты замедления

Android Camera2 API не предоставляет «замедленную съемку» как функцию — он предоставляет пары FpsRange [min, max], где min == max для захвата с фиксированным FPS. Коэффициент замедленного воспроизведения выводится делением FPS захвата на FPS воспроизведения (который почти всегда равен 30 fps для потребительского видео):

FPS захватаФиксированный FpsRangeВоспроизведение @ 30 fps → Коэффициент замедленияТипичное минимальное разрешениеТипичный уровень устройства
120[120, 120]120 ÷ 30 = в 4× медленнее1280×720 (720p)Средний и выше
240[240, 240]240 ÷ 30 = в 8× медленнее1280×720 или 1920×1080Флагман (Snapdragon 8-series, Exynos 2xxx)
480[480, 480]480 ÷ 30 = в 16× медленнее720p (обычно кроп)Игровые телефоны (Black Shark, ROG Phone, RedMagic)
960[960, 960]960 ÷ 30 = в 32× медленнее720p (с буфером DRAM, короткие серии <0,5 сек)Samsung Galaxy S/Ultra, Sony Xperia 1-series

Критически важно, что режимы 960 fps и 480 fps обычно являются режимами «супер-замедленной съемки», требующими буферизации DRAM на сенсоре, и захватывают лишь ~0,33–0,5 секунды материала до заполнения буфера — эти режимы НЕ предоставляются через CameraConstrainedHighSpeedCaptureSession (стандартная сессия не успевает), а вместо этого обрабатываются расширениями, специфичными для вендора, или через CameraX ExtensionsManager на устройствах из белого списка OEM. Эта глава сосредоточена на 120 fps и 240 fps — двух диапазонах, которые стандартный ограниченный высокоскоростной API Camera2 поддерживает повсеместно.

Запрос высокоскоростных размеров и диапазонов FPS

Правильный способ перечисления поддерживаемых высокоскоростных конфигураций — НЕ getOutputSizes() — регулярные выходные размеры часто включают 1080p, но HAL может отклонить 1080p при 240 fps из-за ограничений пропускной способности MIPI. Необходимо вызвать два выделенных метода на StreamConfigurationMap:

import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.params.StreamConfigurationMap
import android.util.Range
import android.util.Size

data class HighSpeedProfile(
val size: Size,
val fpsRanges: List<Range<Int>>
)

fun queryHighSpeedProfiles(
characteristics: CameraCharacteristics
): List<HighSpeedProfile> {
val configMap: StreamConfigurationMap? =
characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
?: return emptyList()

val highSpeedSizes: Array<Size> =
configMap.highSpeedVideoSizes ?: return emptyList()

return highSpeedSizes.map { size ->
val fpsRangesForSize =
configMap.getHighSpeedVideoFpsRangesFor(size)?.toList()
?: emptyList()
HighSpeedProfile(size, fpsRangesForSize)
}
}

Свойство highSpeedVideoSizes является авторитетным списком. Если размер 1080p здесь не появляется, то попытка создать ограниченную высокоскоростную сессию в 1080p вызовет IllegalArgumentException, даже если getOutputSizes(PRAGMA) его перечисляет. Исследовательский документ отмечает, что флагманы 2021–2024 повсеместно поддерживают Size(1920, 1080) как с [120,120], так и с [240,240], тогда как устройства среднего уровня поддерживают только Size(1280, 720) с [120,120].

Приложение Android Camera Parameters отображает точный вывод highSpeedVideoSizes и getHighSpeedVideoFpsRangesFor() на вкладке Stream Config → High-Speed, так что вы можете сверить вывод вашего кода с проверенным перечислителем.

Ограничения, налагаемые HAL

Раздел High-Speed Sessions проектного исследовательского документа перечисляет точные ограничения ввода/вывода, которые createCaptureSession проверит перед созданием CameraConstrainedHighSpeedCaptureSession. Нарушите любое ограничение — и вы получите обратный вызов onConfigureFailed() без объяснений:

ID ограниченияТребование
HS-1Количество выходных surface должно быть ≤ 2. Типичная комбинация: входной surface MediaCodec + предпросмотр SurfaceView. Добавление 3-го surface (например, ImageReader для фото) НЕ допускается.
HS-2Все выходные surface ДОЛЖНЫ иметь размеры, перечисленные в highSpeedVideoSizes (одинаковый размер для обоих surface, или один размер из списка на каждый surface).
HS-3Диапазон FPS в каждом CaptureRequest в пакете ДОЛЖЕН быть из getHighSpeedVideoFpsRangesFor(size) для выбранного размера. [30,120] адаптивный FPS НЕ допускается — min должен равняться max для фиксированного FPS.
HS-4Допускаются только шаблоны TEMPLATE_RECORD и TEMPLATE_PREVIEW. TEMPLATE_STILL_CAPTURE, TEMPLATE_MANUAL и TEMPLATE_VIDEO_SNAPSHOT отклоняются createHighSpeedRequestList.
HS-5Длина пакета из createHighSpeedRequestList() должна быть ≥ 2 кадров. Планировщику HAL нужен как минимум один полный кадровый интервал для предварительной загрузки таймингов.
HS-6Выходной формат ограничен PRIVATE (SurfaceView / surface MediaCodec) или YUV_420_888 (ImageReader для обработки на устройстве). JPEG, RAW_SENSOR и HEIC запрещены.
HS-7CONTROL_AE_TARGET_FPS_RANGE блокируется на значении FPS пакета, как только сессия активна. Попытка изменить его в последующем пакете приведет к тихому отбрасыванию этого пакета.

Ограничение HS-1 нарушается на практике чаще всего — разработчики пытаются подключить ImageReader для покадрового YUV-анализа наряду с кодированием MediaCodec, и HAL молча отказывает в конфигурации. Если вам нужен одновременный предпросмотр + кодирование + покадровая обработка на 240 fps, используйте выходной surface MediaCodec и считывайте YUV-кадры обратно из выходного ByteBuffer кодировщика через MediaCodec.dequeueOutputBuffer() с фильтрацией BUFFER_FLAG_KEY_FRAME — никогда не подключайте два независимых YUV-выхода.

Настройка ограниченной высокоскоростной сессии и записи

Шаг 1: Построение кодировщика MediaRecorder / MediaCodec

Для простоты код ниже использует MediaRecorder (который обрабатывает мультиплексирование аудио внутри). Для кодирования HEVC или потоковой передачи с низкой задержкой вы бы использовали MediaCodec.createEncoderByType("video/hevc") напрямую, но Surface, питающий любой из них, идентичен.

import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession
import android.hardware.camera2.CaptureRequest
import android.media.MediaRecorder
import android.util.Size
import android.view.SurfaceView
import java.io.File

lateinit var cameraDevice: CameraDevice
lateinit var mediaRecorder: MediaRecorder
lateinit var recordingSurface: Surface
lateinit var previewSurface: Surface
var chosenHighSpeedSize: Size = Size(1920, 1080)
var chosenFpsRange: Range<Int> = Range(240, 240)

fun setupMediaRecorder(outputFile: File,
size: Size,
fps: Int) {
mediaRecorder = MediaRecorder().apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setVideoSource(MediaRecorder.VideoSource.SURFACE)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setOutputFile(outputFile.absolutePath)

setVideoEncodingBitRate(40_000_000) // 40 Мбит/с для 240fps 1080p
setVideoFrameRate(fps)
setVideoSize(size.width, size.height)
setVideoEncoder(MediaRecorder.VideoEncoder.HEVC) // H.265 для лучшего размера

setAudioEncodingBitRate(192_000)
setAudioSamplingRate(48_000)
setAudioChannels(2)
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)

setCaptureRate(fps.toDouble()) // ЭТО И ЕСТЬ ТО, ЧТО ЗАПУСКАЕТ ЗАМЕДЛЕННУЮ СЪЕМКУ
// ^ Захват на переменной fps, но воспроизведение в метаданных MP4 = 30 fps
// в результате замедление в (fps / 30)×

prepare()
}
recordingSurface = mediaRecorder.surface
}

Ключевая строка, которая фактически создает замедленную съемку (а не просто воспроизведение с высокой частотой кадров), — это setCaptureRate(fps.toDouble()). Она записывает MP4-бокс tkhd с масштабом времени воспроизведения 30 fps и длительностью каждого кадра, равной 1/fps секунд во время захвата. Большинство видеоплееров (YouTube, Instagram, Google Photos, ExoPlayer) учитывают метаданные частоты захвата и воспроизводят клип на 30 fps, обеспечивая замедление 4× (120÷30) или 8× (240÷30), которого ожидают пользователи.

Шаг 2: Создание CameraConstrainedHighSpeedCaptureSession

Имя конструктора сессии — явный сигнал: вместо createCaptureSession вы вызываете createConstrainedHighSpeedCaptureSession и предоставляете список вывода, ограниченный правилами ограничений (≤ 2 surface, оба из highSpeedVideoSizes).

import android.hardware.camera2.CameraCaptureSession
import android.os.Handler
import android.view.Surface

fun createHighSpeedSession(
previewSurfaceView: SurfaceView,
backgroundHandler: Handler
) {
previewSurface = previewSurfaceView.holder.surface

val outputSurfaces = listOf(previewSurface, recordingSurface)

cameraDevice.createConstrainedHighSpeedCaptureSession(
outputSurfaces,
object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
val highSpeedSession =
session as CameraConstrainedHighSpeedCaptureSession

val recordBuilder = cameraDevice.createCaptureRequest(
CameraDevice.TEMPLATE_RECORD
).apply {
addTarget(previewSurface)
addTarget(recordingSurface)
set(
CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE,
chosenFpsRange
)
set(
CaptureRequest.CONTROL_MODE,
CaptureRequest.CONTROL_MODE_AUTO
)
}

val highSpeedRequestList =
highSpeedSession.createHighSpeedRequestList(
recordBuilder.build()
)

highSpeedSession.setRepeatingBurst(
highSpeedRequestList,
null, // Пропустить покадровые обратные вызовы на 240fps!
backgroundHandler
)

// Теперь запустите MediaRecorder, когда пользователь нажмет кнопку ЗАПИСЬ
mediaRecorder.start()
}

override fun onConfigureFailed(session: CameraCaptureSession) {
Log.e(TAG, "High-speed session config FAILED. " +
"Check HS-1..HS-7 constraints are satisfied.")
}
},
backgroundHandler
)
}

Каждая строка здесь намеренная и напрямую отображается на ограничение из исследовательского документа:

  • TEMPLATE_RECORD → удовлетворяет ограничению HS-4.
  • CONTROL_AE_TARGET_FPS_RANGE = [240,240] → удовлетворяет ограничению HS-3.
  • Ровно 2 выходных surface (предпросмотр + запись) → удовлетворяет ограничению HS-1.
  • createHighSpeedRequestList(recordBuilder.build()) → создает предварительно проверенный пакет минимальной длины (2 кадра), который планировщик HAL потребляет напрямую.
  • Покадровый CaptureCallback равен null → еще одна оптимизация производительности. Включение покадровых обратных вызовов на 240 fps вызывает потоки binder IPC ~2 МБ/с parcel CaptureResult, что измеримо в троттлинге CPU Thermal HAL. Включайте обратные вызовы только для коротких окон отладки, никогда в рабочей записи.

Архитектура: Обычный конвейер vs Высокоскоростной конвейер (подробный Mermaid)

Высокоскоростной ISP (блок HP2) намеренно легковесный: большинство флагманов снижают разрешение demosaic путем 2× binning, пропускают многокадровое временное подавление шума (только пространственное для одного кадра) и применяют линейную тональную кривую вместо стандартного нелинейного гамма-распределения — всё ради укладки в бюджет 4,167 мс/кадр. Именно поэтому видео 240 fps выглядит более мягким и шумным, чем видео 30 fps при том же разрешении — это не ваше воображение, это намеренный компромисс ISP, продиктованный физикой.

Бенчмарки нагрузки на CPU из исследовательского документа

Раздел High-Speed Sessions проектного исследовательского документа содержит следующие эмпирические измерения на Snapdragon 8 Gen 2 (Xiaomi 13) при разрешении 1920×1080:

КонфигурацияЗагрузка CPU (большие ядра)Загрузка CPU (малые ядра)Время теплового троттлингаПотерянных кадров/10 мин
Стандартная сессия, 60 fps, repeatingRequest8%12%> 30 мин0
Стандартная сессия, 120 fps, repeatingRequest34%41%~11 мин218 кадров
Стандартная сессия, 240 fps, repeatingRequest68–74%59–62%~3,5 мин4 890 кадров
Ограниченная HS сессия, 120 fps, repeatingBurst< 3%< 5%> 30 мин0
Ограниченная HS сессия, 240 fps, repeatingBurst< 5%< 7%> 30 мин2 кадра

Цифры говорят сами за себя. Ограниченный список пакета на 240 fps использует примерно в 8× меньше CPU, чем подход стандартной сессии, никогда не троттлит и теряет лишь 2 кадра за 10 минут (из-за единичного теплового прерывания). Именно поэтому CameraConstrainedHighSpeedCaptureSessionединственный поддерживаемый путь для высокоскоростной записи — любой другой подход технически работоспособен, но практически непригоден из-за тепловых, аккумуляторных проблем и потери кадров.

Остановка записи и освобождение ресурсов

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

import android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession

fun stopHighSpeedRecording(
highSpeedSession: CameraConstrainedHighSpeedCaptureSession
) {
try {
// 1. СНАЧАЛА ОСТАНОВИТЬ MEDIARECORDER
mediaRecorder.stop()
mediaRecorder.reset()
} catch (e: RuntimeException) {
// Не записано валидных кадров — atom MP4 не записан; игнорировать
}

// 2. Прервать повторяющийся пакет
highSpeedSession.stopRepeating()

// 3. Прервать любые ожидающие захваты
highSpeedSession.abortCaptures()

// 4. Закрыть сессию
highSpeedSession.close()

// 5. Освободить MediaRecorder ПОСЛЕДНИМ
mediaRecorder.release()
}

Резюме

Эта глава охватила полную реализацию записи замедленной съемки 120 fps и 240 fps через ограниченный высокоскоростной путь Android Camera2:

  • CameraConstrainedHighSpeedCaptureSession — единственный поддерживаемый API для высоких частот кадров, поскольку индивидуальные покадровые CaptureRequest через binder вызывают неприемлемую нагрузку на CPU (68%+ при 240 fps, тепловой троттлинг за 3,5 минуты по бенчмаркам исследования).
  • getHighSpeedVideoSizes() + getHighSpeedVideoFpsRangesFor(size) — авторитетные перечислители — результаты обычного getOutputSizes() могут отклоняться HAL.
  • Коэффициент замедления = FPS захвата ÷ 30 fps воспроизведения: 120 fps → замедление 4×, 240 fps → замедление 8×. Используйте MediaRecorder.setCaptureRate(fps) для встраивания правильных метаданных замедленного воспроизведения в контейнер MP4.
  • createHighSpeedRequestList(builder.build()) обязательно. Это предварительно проверяет каждый кадр в списке пакета и загружает его напрямую в аппаратный планировщик HAL, устраняя binder IPC на каждый кадр.
  • 7 ограничений HAL (HS-1 — HS-7) строго соблюдаются. Наиболее частый сбой: > 2 выходных surface.
  • Архитектурная Mermaid-диаграмма показывает легковесный/быстрый ISP, используемый при 240 fps (binning, легкое NR), по сравнению с полным ISP в конвейере 30 fps.

Что дальше

В Главе 20: Multi-Camera мы войдем в мир логических камер Android 9+ — виртуальных устройств, группирующих несколько физических камер одного направления (ультраширик, широкоугольная, телеобъектив) и позволяющих HAL прозрачно переключать объективы на порогах зума. Вы научитесь получать getPhysicalCameraIds(), различать синхронизацию сенсора APPROXIMATE и CALIBRATED и использовать OutputConfiguration.setPhysicalCameraId() для захвата кадров одновременно с обоих широкоугольного и телеобъективного сенсоров в одном CaptureRequest для сопоставления диспаратности в вычислительной фотографии.

Проверьте, сообщает ли ваше устройство REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA, в приложении Android Camera Parameters, и просмотрите полный список физических camera ID на логическое устройство в открытом репозитории GitHub — вклады новых отчетов о устройствах всегда приветствуются.