О HEVC

2020-11-13

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

H.264, он же Advanced Video Coding, он же AVC, он же MPEG-4 Part 10, он же ISO/IEC 14496-10. Разработан в 2003 совместно ITU-T и MPEG. Используется в телевидении высокой чёткости и на Blu-Ray дисках. Как правило, пакуется в контейнеры MP4 или (реже) в Matroska. В ffmpeg H.264 видео кодируется с помощью libx264.

VP8 — свободный кодек. Разработан On2 Technologies в 2008 году. С 2010 года продвигается Google как часть формата WebM. VP8 является наследником Ogg Theora. WebM — это, по сути, тот же контейнер Matroska с кодеками VP8 или VP9 для видео, и Vorbis или Opus для аудио. В ffmpeg VP8 кодируется с помощью libvpx.

Технически H.264 и VP8 примерно равны. Но аппаратная поддержка декодирования H.264 уже давно присутствует в мобильных чипсетах, что вовсе не так с VP8. Но VP8 в виде WebM продвигается как новый стандарт видео для веба. И современные браузер от Google его умеют.

И вот, на сцену выходит новое поколение видеокодеков. Появляются видео файлы, закодированные этими кодеками. От ITU-T/MPEG это кодек HEVC. От Гугла это кодек AV1.

HEVC, он же High Efficiency Video Coding, он же H.265, он же MPEG-H Part 2. Вроде как он обеспечивает сравнимое с H.264 качество изображения при битрейте в полтора-два раза меньше. Первые аппаратные декодеры HEVC появились в 2012. В ffmpeg HEVC кодируется с помощью libx265.

Основное отличие HEVC от H.264 в размере блоков. H.264 разбивает изображение на блоки 16х16 пикселей. К блоку применяется дискретное косинусное преобразование, как в старом добром JPEG. HEVC может оперировать блоками разного размера, до 64х64 пикселей, что более эффективно для изображений высокого разрешения. Также более совершенно предсказание движения.

AV1, он же AOMedia Video 1. Разрабатывается Alliance for Open Media, куда входят Amazon, Apple, ARM, Cisco, Facebook, Google, IBM, Intel, Microsoft, Mozilla, Netflix, Nvidia и другие. Действительно имеет шанс стать всенародным открытым стандартом. Первая версия стандарта вышла в 2018. В ffmpeg AV1 кодируется с помощью libaom-av1.

AV1 тоже использует блоки большего размера, до 128х128 пикселей.

Но это ещё не всё. Ещё у нас наступает эпоха HDR видео.

HDR, или HDRI, или High Dynamic Range Imaging в фотографии — это способ обойти ограничения динамического диапазона камер, чтобы получить снимки, полные деталей и в тени, и на освещённых участках. Для этого делается несколько снимков одной сцены с разной экспозицией, а затем из них с помощью специальных алгоритмов собирается одно изображение.

В HDR Video всё несколько сложнее.

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

И у нас тут используются разные цветовые модели.

Самая понятная модель — RGB. Это когда цвет представляется в виде суммы красного, зелёного и синего компонент. Эта модель естественна для современных камер и мониторов в силу их физического устройства. Камеры воспринимают цвет через красные, зелёные и синие субпиксели. Мониторы отображают цвет через красные, зелёные и синие субпиксели.

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

RGB

Этот разноцветный кривой треугольник — это так называемая диаграмма цветности CIE. CIE — это "Commission Internationale de l'éclairage", или International Commission on Illumination. В 1931 году они придумали цветовую модель, которая описывает все возможные цвета, доступные для восприятия человеком. Эта диаграмма — двумерное представление этой модели. И все более поздние цветовые модели и пространства для наглядности натягивают на этот универсальный глобус.

В телевидении, а также в цифровом кодировании изображений, чаще используется YUV модель. В разных применениях её также называют YCbCr или YPbPr. Это цветоразностная модель. Отдельно указывается яркость пикселя (Y), а отдельно «синесть» и «красность» пикселя (U и V).

YUV

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

Стандартов HDR видео у нас несколько: HDR10, HDR10+, Dolby Vision (проприетарный), HLG. В основном они сводятся к тому, что цвет нужно представлять 10 битами или больше на канал и тем, как передавать метаданные. Метаданные нужны, чтобы правильно воспроизвести цвета на устройстве отображения. В основном эти стандарты работают поверх HEVC или AVC кодеков, но какая-то поддержка HDR есть и в VP9 и AV1. Разные производители и распространители контента продвигают свои стандарты.

Воспроизведение HDR видео требует аппаратной поддержки. Ну чтобы точно знать, какие цвета ваш конкретный монитор может выводить, и вывести те самые цвета, какие увидела камера. В Linux такой поддержки нет, поэтому, когда я попытался открыть Dolby Vision видео в VLC, я лишь увидел совершенно неправильные цвета на картинке. А вот в Android поддержка HDR видео может быть.

HDR в исполнении VLC

С теорией всё сложно. А вот практическая задача.

Есть набор видеофайлов. Для каждого видео есть пять вариантов. Они отличаются разрешением: FullHD (1920х1080) или 4K (точнее UHD-1, 3840x2160), кодеком: H.264 (только FullHD) или HEVC, наличием HDR (Dolby Vision, только поверх HEVC). Нужно выбрать наиболее качественный вариант, который будет гарантированно нормально отображаться на данном Android устройстве.

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

    private val FULLHD_BUILDER = Format.Builder().setWidth(1920).setHeight(1080)
    private val FULLHD_HEVC_HDR_FORMAT = FULLHD_BUILDER.setSampleMimeType("video/dolby-vision").build()
    private val FULLHD_HEVC_FORMAT = FULLHD_BUILDER.setSampleMimeType("video/hevc").build()
    private val FULLHD_H264_FORMAT = FULLHD_BUILDER.setSampleMimeType("video/avc").build()
    private val UHD1_BUILDER = Format.Builder().setWidth(3840).setHeight(2160)
    private val UHD1_HEVC_HDR_FORMAT = UHD1_BUILDER.setSampleMimeType("video/dolby-vision").build()
    private val UHD1_HEVC_FORMAT = UHD1_BUILDER.setSampleMimeType("video/hevc").build()

    private val CHECK_FORMATS = mapOf(
        UHD1_HEVC_HDR_FORMAT to (VideoCodec.HEVC_HDR to VideoResolution.UHD1),
        UHD1_HEVC_FORMAT to (VideoCodec.HEVC to VideoResolution.UHD1),
        FULLHD_HEVC_HDR_FORMAT to (VideoCodec.HEVC_HDR to VideoResolution.FULLHD),
        FULLHD_HEVC_FORMAT to (VideoCodec.HEVC to VideoResolution.FULLHD),
        FULLHD_H264_FORMAT to (VideoCodec.H264 to VideoResolution.FULLHD)
    )

    private fun getSupportedCodecResolution(): Pair<VideoCodec, VideoResolution> {
        for ((format, result) in CHECK_FORMATS.entries) {
            if (isFormatSupported(format)) {
                log.info("Found supported format=$format")
                return result
            }
        }

        log.warn("No good hardware codec found")
        return VideoCodec.H264 to VideoResolution.FULLHD
    }

    private fun isFormatSupported(format: Format): Boolean {
        val codec = MediaCodecUtil.getDecoderInfo(format.sampleMimeType!!, false, false)
        log.info("Found codec format=$format codec=$codec")
        return codec?.hardwareAccelerated == true
            && codec.isFormatSupported(format)
    }

MediaCodecUtil.getDecoderInfo() возвращает сведения о наиболее подходящем кодеке для данного MIME типа. Пришлось порыться в исходниках ExoPlayer, чтобы найти, что ожидаемые MIME типы для AVC: video/avc, для HEVC: video/hevc, для Dolby Vision: video/dolby-vision.

Внутри MediaCodecUtil обращается к андроидному MediaCodecList.

Имена кодеков в Android выглядят как "OMX.google.hevc.decoder", или "OMX.google.h264.decoder". Эти "OMX.google.*" — программные декодеры. Они есть даже в эмуляторе. Но они совершенно не подходят для декодирования 4К изображений. Более настоящие аппаратные декодеры могут называться, например: "OMX.qcom.video.decoder.avc" или "OMX.qcom.video.decoder.hevc".

Но нам не надо париться по поводу имён кодеков. Для выбора аппаратных декодеров у нас уже есть флаг hardwareAccelerated.

Но наличия нужных аппаратных кодеков недостаточно. Эти кодеки могут не поддерживать нужного разрешения. Скажем, софтверный HEVC в эмуляторе не может даже FullHD, а вот AVC с FullHD справляется.

Для проверки поддержки разрешения мы вызываем MediaCodecInfo.isFormatSupported(), куда передаём описание формата с MIME типом и разрешением. Внутри вызывается андроидный MediaCodecInfo.VideoCapabilities.areSizeAndRateSupported().

Кодек может поддерживать 4K разрешение, но нужного количества пикселей может просто не быть на экране устройства. Или же, как часто бывает в телевизорах, разрешение, доступное для отрисовки UI Андроида может быть меньше разрешения матрицы, но через SurfaceView можно выводить видео в полном разрешении. Например, в моём Philips UI отриcовывается в FullHD, но он способен проигрывать UHD видео.

    private fun getSupportedResolution(): VideoResolution {
        val size = Util.getCurrentDisplayModeSize(context)
        log.info("Detected display size=$size")
        return if (size.x >= 2160 && size.y >= 2160) VideoResolution.UHD1   // for any screen direction
            else VideoResolution.FULLHD
    }

В ExoPlayer есть метод Util.getCurrentDisplayModeSize(), который возвращает действительное доступное разрешение экрана. Внутри запрятаны обращения к системным свойствам "sys.display-size" или "vendor.display-size", а также грязные хаки для телевизоров Sony. Стандартного Android API, чтобы узнать действительное физическое разрешение экрана, не существует.

Вот так всё сложно.

Одно и то же видео в моём наборе, но закодированное разными кодеками, имеет разный битрейт. H.264 FullHD — порядка 10 mb/s. HEVC FullHD — порядка 6 mb/s, на 40% меньше. HEVC 4k — порядка 12 mb/s, в два раза больше, чем FullHD. Dolby Vision FullHD — порядка 12 mb/s, очень интересно, чего там в два раза больше накодировано, но посмотреть не на чем. Dolby Vision 4k — порядка 25 mb/s, ещё в два раза больше.

Зато я впервые разглядел, чем 4K картинка на телевизоре отличается от FullHD. На FullHD, если встать вплотную к телевизору, видны артефакты сжатия и муар на мелких деталях. На 4K ничего подобного не видно, картинка просто чёткая. Не вполне ясно, дело в разрешении или же просто в вдвое большем битрейте. Всё равно, в метре от экрана разница уже незаметна.

Разрешение становится всё больше. Картинка всё сочнее. Нужно ли это просто для видео?

P.S. Все эти мучения ради этого приложения.