WebAssembly позволяет расширить браузер новыми функциями. В этой статье показано, как портировать видеодекодер AV1 и воспроизводить видео AV1 в любом современном браузере.
Одна из лучших особенностей WebAssembly — это возможность экспериментировать с новыми возможностями и реализовывать новые идеи до того, как браузер отправит эти функции в исходное состояние (если вообще будет). Вы можете думать об использовании WebAssembly как о высокопроизводительном механизме полифилла, в котором вы пишете свою функцию на C/C++ или Rust, а не на JavaScript.
Благодаря множеству существующего кода, доступного для портирования, в браузере можно делать вещи, которые были невозможны до появления WebAssembly.
В этой статье будет рассмотрен пример того, как взять существующий исходный код видеокодека AV1, создать для него оболочку и опробовать ее в браузере, а также даны советы, которые помогут создать тестовую программу для отладки оболочки. Полный исходный код приведенного здесь примера доступен по адресу github.com/GoogleChromeLabs/wasm-av1 для справки.
Загрузите один из этих двух тестовых видеофайлов со скоростью 24 кадра в секунду и опробуйте их на нашей встроенной демо-версии .
Выбор интересной кодовой базы
Вот уже несколько лет мы видим, что большой процент веб-трафика состоит из видеоданных. По оценкам Cisco, на самом деле этот показатель составляет целых 80 %! Конечно, производители браузеров и видеосайты прекрасно осознают желание сократить объем данных, потребляемых всем этим видеоконтентом. Ключом к этому, конечно же, является лучшее сжатие, и, как и следовало ожидать, проводится множество исследований в области сжатия видео следующего поколения, направленных на снижение нагрузки на данные при доставке видео через Интернет.
Так получилось, что Альянс открытых медиа работает над схемой сжатия видео следующего поколения под названием AV1 , которая обещает значительно сократить размер видеоданных. В будущем мы ожидаем, что браузеры предоставят встроенную поддержку AV1, но, к счастью, исходный код компрессора и декомпрессора является открытым , что делает его идеальным кандидатом для того, чтобы попытаться скомпилировать его в WebAssembly, чтобы мы могли поэкспериментировать с ним в браузере.

Адаптация для использования в браузере
Первое, что нам нужно сделать, чтобы разместить этот код в браузере, — это познакомиться с существующим кодом, чтобы понять, что представляет собой API. При первом взгляде на этот код можно выделить две вещи:
- Дерево исходного кода создается с помощью инструмента
cmake
; и - Существует ряд примеров, каждый из которых предполагает наличие некоторого файлового интерфейса.
Все примеры, которые собираются по умолчанию, можно запускать из командной строки, и это, вероятно, верно для многих других баз кода, доступных в сообществе. Итак, интерфейс, который мы собираемся создать для запуска его в браузере, может быть полезен для многих других инструментов командной строки.
Использование cmake
для сборки исходного кода
К счастью, авторы AV1 экспериментировали с Emscripten , SDK, который мы собираемся использовать для создания нашей версии WebAssembly. В корне репозитория AV1 файл CMakeLists.txt
содержит такие правила сборки:
if(EMSCRIPTEN)
add_preproc_definition(_POSIX_SOURCE)
append_link_flag_to_target("inspect" "-s TOTAL_MEMORY=402653184")
append_link_flag_to_target("inspect" "-s MODULARIZE=1")
append_link_flag_to_target("inspect"
"-s EXPORT_NAME=\"\'DecoderModule\'\"")
append_link_flag_to_target("inspect" "--memory-init-file 0")
if("${CMAKE_BUILD_TYPE}" STREQUAL "")
# Default to -O3 when no build type is specified.
append_compiler_flag("-O3")
endif()
em_link_post_js(inspect "${AOM_ROOT}/tools/inspect-post.js")
endif()
Инструментальная цепочка Emscripten может генерировать выходные данные в двух форматах: один называется asm.js
, а другой — WebAssembly. Мы будем ориентироваться на WebAssembly, поскольку он производит меньший результат и может работать быстрее. Эти существующие правила сборки предназначены для компиляции версии библиотеки asm.js
для использования в приложении-инспекторе, которое используется для просмотра содержимого видеофайла. Для нашего использования нам нужны выходные данные WebAssembly, поэтому мы добавляем эти строки непосредственно перед закрывающим оператором endif()
в приведенных выше правилах.
# Force generation of Wasm instead of asm.js
append_link_flag_to_target("inspect" "-s WASM=1")
append_compiler_flag("-s WASM=1")
Сборка с помощью cmake
означает сначала создание нескольких Makefiles
путем запуска самого cmake
, а затем запуск команды make
, которая выполнит этап компиляции. Обратите внимание: поскольку мы используем Emscripten, нам нужно использовать набор инструментов компилятора Emscripten, а не компилятор хоста по умолчанию. Это достигается за счет использования Emscripten.cmake
, который является частью Emscripten SDK , и передачи его пути в качестве параметра самому cmake
. Приведенная ниже командная строка — это то, что мы используем для создания файлов Makefile:
cmake path/to/aom \
-DENABLE_CCACHE=1 -DAOM_TARGET_CPU=generic -DENABLE_DOCS=0 \
-DCONFIG_ACCOUNTING=1 -DCONFIG_INSPECTION=1 -DCONFIG_MULTITHREAD=0 \
-DCONFIG_RUNTIME_CPU_DETECT=0 -DCONFIG_UNIT_TESTS=0
-DCONFIG_WEBM_IO=0 \
-DCMAKE_TOOLCHAIN_FILE=path/to/emsdk-portable/.../Emscripten.cmake
Параметр path/to/aom
должен быть установлен на полный путь к местоположению исходных файлов библиотеки AV1. Для параметра path/to/emsdk-portable/…/Emscripten.cmake
необходимо указать путь к файлу описания цепочки инструментов Emscripten.cmake.
Для удобства мы используем сценарий оболочки, чтобы найти этот файл:
#!/bin/sh
EMCC_LOC=`which emcc`
EMSDK_LOC=`echo $EMCC_LOC | sed 's?/emscripten/[0-9.]*/emcc??'`
EMCMAKE_LOC=`find $EMSDK_LOC -name Emscripten.cmake -print`
echo $EMCMAKE_LOC
Если вы посмотрите на Makefile
верхнего уровня для этого проекта, вы увидите, как этот сценарий используется для настройки сборки.
Теперь, когда все настройки выполнены, мы просто вызываем make
, который построит все дерево исходного кода, включая образцы, но, что наиболее важно, сгенерируем libaom.a
, который содержит скомпилированный видеодекодер, готовый для включения в наш проект.
Разработка API для взаимодействия с библиотекой.
После того, как мы создали нашу библиотеку, нам нужно решить, как взаимодействовать с ней, чтобы отправлять в нее сжатые видеоданные, а затем считывать кадры видео, которые мы можем отобразить в браузере.
Если взглянуть на дерево кода AV1, хорошей отправной точкой будет пример видеодекодера, который можно найти в файле [simple_decoder.c](https://aomedia.googlesource.com/aom/+/master/examples/simple_decoder.c)
. Этот декодер считывает файл IVF и декодирует его в серию изображений, представляющих кадры видео.
Наш интерфейс мы реализуем в исходном файле [decode-av1.c](https://github.com/GoogleChromeLabs/wasm-av1/blob/master/decode-av1.c)
.
Поскольку наш браузер не может читать файлы из файловой системы, нам необходимо разработать некую форму интерфейса, который позволит нам абстрагировать ввод-вывод, чтобы мы могли создать что-то похожее на пример декодера для получения данных в нашу библиотеку AV1.
В командной строке файловый ввод-вывод — это то, что известно как потоковый интерфейс, поэтому мы можем просто определить наш собственный интерфейс, который выглядит как потоковый ввод-вывод, и построить все, что нам нравится, в базовой реализации.
Мы определяем наш интерфейс следующим образом:
DATA_Source *DS_open(const char *what);
size_t DS_read(DATA_Source *ds,
unsigned char *buf, size_t bytes);
int DS_empty(DATA_Source *ds);
void DS_close(DATA_Source *ds);
// Helper function for blob support
void DS_set_blob(DATA_Source *ds, void *buf, size_t len);
Функции open/read/empty/close
очень похожи на обычные операции файлового ввода-вывода, что позволяет нам легко сопоставлять их с файловым вводом-выводом для приложения командной строки или реализовывать их каким-либо другим способом при запуске внутри браузера. Тип DATA_Source
непрозрачен со стороны JavaScript и служит лишь для инкапсуляции интерфейса. Обратите внимание, что создание API, который точно соответствует семантике файлов, упрощает повторное использование во многих других базах кода, которые предназначены для использования из командной строки (например, diff, sed и т. д.).
Нам также необходимо определить вспомогательную функцию под названием DS_set_blob
, которая связывает необработанные двоичные данные с нашими потоковыми функциями ввода-вывода. Это позволяет «читать» большой двоичный объект, как если бы он был потоком (т. е. выглядеть как файл, считываемый последовательно).
Наш пример реализации позволяет читать переданный большой двоичный объект, как если бы это был источник данных, считываемый последовательно. Справочный код можно найти в файле blob-api.c
, а вся реализация такова:
struct DATA_Source {
void *ds_Buf;
size_t ds_Len;
size_t ds_Pos;
};
DATA_Source *
DS_open(const char *what) {
DATA_Source *ds;
ds = malloc(sizeof *ds);
if (ds != NULL) {
memset(ds, 0, sizeof *ds);
}
return ds;
}
size_t
DS_read(DATA_Source *ds, unsigned char *buf, size_t bytes) {
if (DS_empty(ds) || buf == NULL) {
return 0;
}
if (bytes > (ds->ds_Len - ds->ds_Pos)) {
bytes = ds->ds_Len - ds->ds_Pos;
}
memcpy(buf, &ds->ds_Buf[ds->ds_Pos], bytes);
ds->ds_Pos += bytes;
return bytes;
}
int
DS_empty(DATA_Source *ds) {
return ds->ds_Pos >= ds->ds_Len;
}
void
DS_close(DATA_Source *ds) {
free(ds);
}
void
DS_set_blob(DATA_Source *ds, void *buf, size_t len) {
ds->ds_Buf = buf;
ds->ds_Len = len;
ds->ds_Pos = 0;
}
Создание тестовой среды для тестирования вне браузера
Одна из лучших практик в разработке программного обеспечения — создание модульных тестов для кода в сочетании с интеграционными тестами.
При сборке с помощью WebAssembly в браузере имеет смысл создать некоторую форму модульного теста для интерфейса кода, с которым мы работаем, чтобы мы могли выполнять отладку вне браузера, а также иметь возможность протестировать созданный нами интерфейс.
В этом примере мы эмулировали потоковый API в качестве интерфейса к библиотеке AV1. Таким образом, логически имеет смысл создать тестовую программу, которую мы можем использовать для создания версии нашего API, которая запускается в командной строке и выполняет фактический файловый ввод-вывод «под капотом», реализуя сам файловый ввод-вывод под нашим API DATA_Source
.
Код потокового ввода-вывода для нашей тестовой программы прост и выглядит следующим образом:
DATA_Source *
DS_open(const char *what) {
return (DATA_Source *)fopen(what, "rb");
}
size_t
DS_read(DATA_Source *ds, unsigned char *buf, size_t bytes) {
return fread(buf, 1, bytes, (FILE *)ds);
}
int
DS_empty(DATA_Source *ds) {
return feof((FILE *)ds);
}
void
DS_close(DATA_Source *ds) {
fclose((FILE *)ds);
}
Абстрагируя интерфейс потока, мы можем построить наш модуль WebAssembly, который будет использовать двоичные объекты данных в браузере и взаимодействовать с реальными файлами, когда мы создаем код для тестирования из командной строки. Код нашей тестовой программы можно найти в примере исходного файла test.c
Реализация механизма буферизации для нескольких видеокадров
При воспроизведении видео принято буферизировать несколько кадров, чтобы обеспечить более плавное воспроизведение. Для наших целей мы просто реализуем буфер из 10 кадров видео, поэтому мы буферизуем 10 кадров перед началом воспроизведения. Затем каждый раз, когда отображается кадр, мы пытаемся декодировать другой кадр, чтобы буфер оставался полным. Такой подход обеспечивает заранее доступность кадров, что помогает предотвратить заикание видео.
В нашем простом примере все сжатое видео доступно для чтения, поэтому буферизация на самом деле не требуется. Однако, если мы хотим расширить интерфейс исходных данных для поддержки потокового ввода с сервера, нам необходимо иметь механизм буферизации.
Код в decode-av1.c
для чтения кадров видеоданных из библиотеки AV1 и сохранения в буфере такой:
void
AVX_Decoder_run(AVX_Decoder *ad) {
...
// Try to decode an image from the compressed stream, and buffer
while (ad->ad_NumBuffered < NUM_FRAMES_BUFFERED) {
ad->ad_Image = aom_codec_get_frame(&ad->ad_Codec,
&ad->ad_Iterator);
if (ad->ad_Image == NULL) {
break;
}
else {
buffer_frame(ad);
}
}
Мы решили, что буфер будет содержать 10 кадров видео, что является произвольным выбором. Буферизация большего количества кадров означает увеличение времени ожидания начала воспроизведения видео, тогда как буферизация слишком малого количества кадров может привести к остановке во время воспроизведения. В собственной реализации браузера буферизация кадров гораздо сложнее, чем в этой реализации.
Перенос видеокадров на страницу с помощью WebGL
Кадры видео, которые мы буферизировали, должны отображаться на нашей странице. Поскольку это динамический видеоконтент, мы хотим сделать это как можно быстрее. Для этого мы обратимся к WebGL .
WebGL позволяет нам взять изображение, например кадр видео, и использовать его в качестве текстуры, которая наносится на некоторую геометрию. В мире WebGL всё состоит из треугольников. Итак, для нашего случая мы можем использовать удобную встроенную функцию WebGL под названием gl.TRIANGLE_FAN.
Однако есть небольшая проблема. Текстуры WebGL должны представлять собой изображения RGB, по одному байту на цветовой канал. Выходные данные нашего декодера AV1 представляют собой изображения в так называемом формате YUV, где выход по умолчанию имеет 16 бит на канал, а также каждое значение U или V соответствует 4 пикселям в фактическом выходном изображении. Все это означает, что нам нужно преобразовать изображение в цвет, прежде чем мы сможем передать его в WebGL для отображения.
Для этого мы реализуем функцию AVX_YUV_to_RGB()
, которую вы можете найти в исходном файле yuv-to-rgb.c
. Эта функция преобразует выходные данные декодера AV1 во что-то, что мы можем передать в WebGL. Обратите внимание: когда мы вызываем эту функцию из JavaScript, нам необходимо убедиться, что память, в которую мы записываем преобразованное изображение, была выделена внутри памяти модуля WebAssembly — в противном случае он не сможет получить к ней доступ. Функция получения изображения из модуля WebAssembly и рисования его на экране выглядит следующим образом:
function show_frame(af) {
if (rgb_image != 0) {
// Convert The 16-bit YUV to 8-bit RGB
let buf = Module._AVX_Video_Frame_get_buffer(af);
Module._AVX_YUV_to_RGB(rgb_image, buf, WIDTH, HEIGHT);
// Paint the image onto the canvas
drawImageToCanvas(new Uint8Array(Module.HEAPU8.buffer,
rgb_image, 3 * WIDTH * HEIGHT), WIDTH, HEIGHT);
}
}
Функцию drawImageToCanvas()
, реализующую рисование WebGL, можно найти в исходном файле draw-image.js
для справки.
Будущая работа и выводы
Испытание нашей демонстрации на двух тестовых видеофайлах (записанных как видео с частотой 24 кадра в секунду ) учит нас нескольким вещам:
- Вполне возможно создать сложную кодовую базу для эффективной работы в браузере с помощью WebAssembly; и
- С помощью WebAssembly можно реализовать такую ресурсоемкую задачу, как расширенное декодирование видео.
Однако есть некоторые ограничения: вся реализация выполняется в основном потоке, и мы чередуем рисование и декодирование видео в этом единственном потоке. Выгрузка декодирования в веб-воркер может обеспечить более плавное воспроизведение, поскольку время декодирования кадров сильно зависит от содержимого этого кадра и иногда может занять больше времени, чем мы запланировали.
При компиляции в WebAssembly используется конфигурация AV1 для общего типа ЦП. Если мы компилируем в командной строке для обычного ЦП, мы видим такую же загрузку ЦП для декодирования видео, как и в версии WebAssembly, однако библиотека декодера AV1 также включает реализации SIMD , которые работают до 5 раз быстрее. Группа сообщества WebAssembly в настоящее время работает над расширением стандарта, включив в него примитивы SIMD , и когда это произойдет, обещают значительно ускорить декодирование. Когда это произойдет, станет вполне возможным декодировать HD-видео 4K в режиме реального времени с помощью видеодекодера WebAssembly.
В любом случае, пример кода полезен в качестве руководства, помогающего портировать любую существующую утилиту командной строки для запуска в качестве модуля WebAssembly и показывает, что возможно в Интернете уже сегодня.
Кредиты
Благодарим Джеффа Посника, Эрика Бидельмана и Томаса Штайнера за ценные обзоры и отзывы.
,WebAssembly позволяет расширить браузер новыми функциями. В этой статье показано, как портировать видеодекодер AV1 и воспроизводить видео AV1 в любом современном браузере.
Одна из лучших особенностей WebAssembly — это возможность экспериментировать с новыми возможностями и реализовывать новые идеи до того, как браузер отправит эти функции в исходное состояние (если вообще будет). Вы можете думать об использовании WebAssembly как о высокопроизводительном механизме полифилла, в котором вы пишете свою функцию на C/C++ или Rust, а не на JavaScript.
Благодаря множеству существующего кода, доступного для портирования, в браузере можно делать вещи, которые были невозможны до появления WebAssembly.
В этой статье будет рассмотрен пример того, как взять существующий исходный код видеокодека AV1, создать для него оболочку и опробовать его в браузере, а также даны советы, которые помогут создать тестовую программу для отладки оболочки. Полный исходный код приведенного здесь примера доступен по адресу github.com/GoogleChromeLabs/wasm-av1 для справки.
Загрузите один из этих двух тестовых видеофайлов со скоростью 24 кадра в секунду и опробуйте их на нашей встроенной демо-версии .
Выбор интересной кодовой базы
Вот уже несколько лет мы видим, что большой процент веб-трафика состоит из видеоданных, по оценкам Cisco, на самом деле это целых 80%! Конечно, производители браузеров и видеосайты прекрасно осознают желание сократить объем данных, потребляемых всем этим видеоконтентом. Ключом к этому, конечно же, является лучшее сжатие, и, как и следовало ожидать, проводится множество исследований в области сжатия видео следующего поколения, направленных на снижение нагрузки на данные при доставке видео через Интернет.
Так получилось, что Альянс открытых медиа работает над схемой сжатия видео следующего поколения под названием AV1 , которая обещает значительно сократить размер видеоданных. В будущем мы ожидаем, что браузеры предоставят встроенную поддержку AV1, но, к счастью, исходный код компрессора и декомпрессора является открытым , что делает его идеальным кандидатом для того, чтобы попытаться скомпилировать его в WebAssembly, чтобы мы могли поэкспериментировать с ним в браузере.

Адаптация для использования в браузере
Первое, что нам нужно сделать, чтобы разместить этот код в браузере, — это познакомиться с существующим кодом, чтобы понять, что представляет собой API. При первом взгляде на этот код можно выделить две вещи:
- Дерево исходного кода создается с помощью инструмента
cmake
; и - Существует ряд примеров, каждый из которых предполагает наличие некоторого файлового интерфейса.
Все примеры, которые собираются по умолчанию, можно запускать из командной строки, и это, вероятно, верно для многих других баз кода, доступных в сообществе. Итак, интерфейс, который мы собираемся создать для запуска его в браузере, может быть полезен для многих других инструментов командной строки.
Использование cmake
для сборки исходного кода
К счастью, авторы AV1 экспериментировали с Emscripten , SDK, который мы собираемся использовать для создания нашей версии WebAssembly. В корне репозитория AV1 файл CMakeLists.txt
содержит такие правила сборки:
if(EMSCRIPTEN)
add_preproc_definition(_POSIX_SOURCE)
append_link_flag_to_target("inspect" "-s TOTAL_MEMORY=402653184")
append_link_flag_to_target("inspect" "-s MODULARIZE=1")
append_link_flag_to_target("inspect"
"-s EXPORT_NAME=\"\'DecoderModule\'\"")
append_link_flag_to_target("inspect" "--memory-init-file 0")
if("${CMAKE_BUILD_TYPE}" STREQUAL "")
# Default to -O3 when no build type is specified.
append_compiler_flag("-O3")
endif()
em_link_post_js(inspect "${AOM_ROOT}/tools/inspect-post.js")
endif()
Инструментальная цепочка Emscripten может генерировать выходные данные в двух форматах: один называется asm.js
, а другой — WebAssembly. Мы будем ориентироваться на WebAssembly, поскольку он производит меньший результат и может работать быстрее. Эти существующие правила сборки предназначены для компиляции версии библиотеки asm.js
для использования в приложении-инспекторе, которое используется для просмотра содержимого видеофайла. Для нашего использования нам нужны выходные данные WebAssembly, поэтому мы добавляем эти строки непосредственно перед закрывающим оператором endif()
в приведенных выше правилах.
# Force generation of Wasm instead of asm.js
append_link_flag_to_target("inspect" "-s WASM=1")
append_compiler_flag("-s WASM=1")
Сборка с помощью cmake
означает сначала создание нескольких Makefiles
путем запуска самого cmake
, а затем запуск команды make
, которая выполнит этап компиляции. Обратите внимание: поскольку мы используем Emscripten, нам нужно использовать набор инструментов компилятора Emscripten, а не компилятор хоста по умолчанию. Это достигается за счет использования Emscripten.cmake
, который является частью Emscripten SDK , и передачи его пути в качестве параметра самому cmake
. Командная строка ниже — это то, что мы используем для создания файлов Makefile:
cmake path/to/aom \
-DENABLE_CCACHE=1 -DAOM_TARGET_CPU=generic -DENABLE_DOCS=0 \
-DCONFIG_ACCOUNTING=1 -DCONFIG_INSPECTION=1 -DCONFIG_MULTITHREAD=0 \
-DCONFIG_RUNTIME_CPU_DETECT=0 -DCONFIG_UNIT_TESTS=0
-DCONFIG_WEBM_IO=0 \
-DCMAKE_TOOLCHAIN_FILE=path/to/emsdk-portable/.../Emscripten.cmake
Параметр path/to/aom
должен быть установлен на полный путь к местоположению исходных файлов библиотеки AV1. Для параметра path/to/emsdk-portable/…/Emscripten.cmake
необходимо указать путь к файлу описания цепочки инструментов Emscripten.cmake.
Для удобства мы используем сценарий оболочки, чтобы найти этот файл:
#!/bin/sh
EMCC_LOC=`which emcc`
EMSDK_LOC=`echo $EMCC_LOC | sed 's?/emscripten/[0-9.]*/emcc??'`
EMCMAKE_LOC=`find $EMSDK_LOC -name Emscripten.cmake -print`
echo $EMCMAKE_LOC
Если вы посмотрите на Makefile
верхнего уровня для этого проекта, вы увидите, как этот сценарий используется для настройки сборки.
Теперь, когда все настройки выполнены, мы просто вызываем make
, который построит все дерево исходного кода, включая образцы, но, что наиболее важно, сгенерируем libaom.a
, который содержит скомпилированный видеодекодер, готовый для включения в наш проект.
Разработка API для взаимодействия с библиотекой.
После того, как мы создали нашу библиотеку, нам нужно решить, как взаимодействовать с ней, чтобы отправлять в нее сжатые видеоданные, а затем считывать кадры видео, которые мы можем отобразить в браузере.
Если взглянуть на дерево кода AV1, хорошей отправной точкой будет пример видеодекодера, который можно найти в файле [simple_decoder.c](https://aomedia.googlesource.com/aom/+/master/examples/simple_decoder.c)
. Этот декодер считывает файл IVF и декодирует его в серию изображений, представляющих кадры видео.
Наш интерфейс мы реализуем в исходном файле [decode-av1.c](https://github.com/GoogleChromeLabs/wasm-av1/blob/master/decode-av1.c)
.
Поскольку наш браузер не может читать файлы из файловой системы, нам необходимо разработать некую форму интерфейса, который позволит нам абстрагировать ввод-вывод, чтобы мы могли создать что-то похожее на пример декодера для получения данных в нашу библиотеку AV1.
В командной строке файловый ввод-вывод — это то, что известно как потоковый интерфейс, поэтому мы можем просто определить наш собственный интерфейс, который выглядит как потоковый ввод-вывод, и построить все, что нам нравится, в базовой реализации.
Мы определяем наш интерфейс следующим образом:
DATA_Source *DS_open(const char *what);
size_t DS_read(DATA_Source *ds,
unsigned char *buf, size_t bytes);
int DS_empty(DATA_Source *ds);
void DS_close(DATA_Source *ds);
// Helper function for blob support
void DS_set_blob(DATA_Source *ds, void *buf, size_t len);
Функции open/read/empty/close
очень похожи на обычные операции файлового ввода-вывода, что позволяет нам легко сопоставлять их с файловым вводом-выводом для приложения командной строки или реализовывать их каким-либо другим способом при запуске внутри браузера. Тип DATA_Source
непрозрачен со стороны JavaScript и служит лишь для инкапсуляции интерфейса. Обратите внимание, что создание API, который точно соответствует семантике файлов, упрощает повторное использование во многих других базах кода, которые предназначены для использования из командной строки (например, diff, sed и т. д.).
Нам также необходимо определить вспомогательную функцию DS_set_blob
, которая связывает необработанные двоичные данные с нашими потоковыми функциями ввода-вывода. Это позволяет «читать» большой двоичный объект так, как если бы он был потоком (т. е. выглядеть как файл, считываемый последовательно).
Наш пример реализации позволяет читать переданный большой двоичный объект, как если бы это был источник данных, считываемый последовательно. Справочный код можно найти в файле blob-api.c
, а вся реализация такова:
struct DATA_Source {
void *ds_Buf;
size_t ds_Len;
size_t ds_Pos;
};
DATA_Source *
DS_open(const char *what) {
DATA_Source *ds;
ds = malloc(sizeof *ds);
if (ds != NULL) {
memset(ds, 0, sizeof *ds);
}
return ds;
}
size_t
DS_read(DATA_Source *ds, unsigned char *buf, size_t bytes) {
if (DS_empty(ds) || buf == NULL) {
return 0;
}
if (bytes > (ds->ds_Len - ds->ds_Pos)) {
bytes = ds->ds_Len - ds->ds_Pos;
}
memcpy(buf, &ds->ds_Buf[ds->ds_Pos], bytes);
ds->ds_Pos += bytes;
return bytes;
}
int
DS_empty(DATA_Source *ds) {
return ds->ds_Pos >= ds->ds_Len;
}
void
DS_close(DATA_Source *ds) {
free(ds);
}
void
DS_set_blob(DATA_Source *ds, void *buf, size_t len) {
ds->ds_Buf = buf;
ds->ds_Len = len;
ds->ds_Pos = 0;
}
Создание тестовой среды для тестирования вне браузера
Одна из лучших практик в разработке программного обеспечения — создание модульных тестов для кода в сочетании с интеграционными тестами.
При сборке с помощью WebAssembly в браузере имеет смысл создать некоторую форму модульного теста для интерфейса кода, с которым мы работаем, чтобы мы могли выполнять отладку вне браузера, а также иметь возможность протестировать созданный нами интерфейс.
В этом примере мы эмулировали потоковый API в качестве интерфейса к библиотеке AV1. Таким образом, логически имеет смысл создать тестовую программу, которую мы можем использовать для создания версии нашего API, которая запускается в командной строке и выполняет фактический файловый ввод-вывод «под капотом», реализуя сам файловый ввод-вывод под нашим API DATA_Source
.
Код потокового ввода-вывода для нашей тестовой программы прост и выглядит следующим образом:
DATA_Source *
DS_open(const char *what) {
return (DATA_Source *)fopen(what, "rb");
}
size_t
DS_read(DATA_Source *ds, unsigned char *buf, size_t bytes) {
return fread(buf, 1, bytes, (FILE *)ds);
}
int
DS_empty(DATA_Source *ds) {
return feof((FILE *)ds);
}
void
DS_close(DATA_Source *ds) {
fclose((FILE *)ds);
}
Абстрагируя интерфейс потока, мы можем построить наш модуль WebAssembly, который будет использовать двоичные объекты данных в браузере и взаимодействовать с реальными файлами, когда мы создаем код для тестирования из командной строки. Код нашей тестовой программы можно найти в примере исходного файла test.c
Реализация механизма буферизации для нескольких видеокадров
При воспроизведении видео принято буферизировать несколько кадров, чтобы обеспечить более плавное воспроизведение. Для наших целей мы просто реализуем буфер из 10 кадров видео, поэтому мы буферизуем 10 кадров перед началом воспроизведения. Затем каждый раз, когда отображается кадр, мы пытаемся декодировать другой кадр, чтобы буфер оставался полным. Такой подход обеспечивает заранее доступность кадров, что помогает предотвратить заикание видео.
В нашем простом примере все сжатое видео доступно для чтения, поэтому буферизация на самом деле не требуется. Однако, если мы хотим расширить интерфейс исходных данных для поддержки потокового ввода с сервера, нам необходимо иметь механизм буферизации.
Код в decode-av1.c
для чтения кадров видеоданных из библиотеки AV1 и сохранения в буфере такой:
void
AVX_Decoder_run(AVX_Decoder *ad) {
...
// Try to decode an image from the compressed stream, and buffer
while (ad->ad_NumBuffered < NUM_FRAMES_BUFFERED) {
ad->ad_Image = aom_codec_get_frame(&ad->ad_Codec,
&ad->ad_Iterator);
if (ad->ad_Image == NULL) {
break;
}
else {
buffer_frame(ad);
}
}
Мы решили, что буфер будет содержать 10 кадров видео, что является произвольным выбором. Буферизация большего количества кадров означает увеличение времени ожидания начала воспроизведения видео, тогда как буферизация слишком малого количества кадров может привести к остановке во время воспроизведения. В собственной реализации браузера буферизация кадров гораздо сложнее, чем в этой реализации.
Перенос видеокадров на страницу с помощью WebGL
Кадры видео, которые мы буферизировали, должны отображаться на нашей странице. Поскольку это динамический видеоконтент, мы хотим сделать это как можно быстрее. Для этого мы обратимся к WebGL .
WebGL позволяет нам взять изображение, например кадр видео, и использовать его в качестве текстуры, которая наносится на некоторую геометрию. В мире WebGL всё состоит из треугольников. Итак, для нашего случая мы можем использовать удобную встроенную функцию WebGL под названием gl.TRIANGLE_FAN.
Однако есть небольшая проблема. Текстуры WebGL должны представлять собой изображения RGB, по одному байту на цветовой канал. Выходные данные нашего декодера AV1 представляют собой изображения в так называемом формате YUV, где выход по умолчанию имеет 16 бит на канал, а также каждое значение U или V соответствует 4 пикселям в фактическом выходном изображении. Все это означает, что нам нужно преобразовать изображение в цвет, прежде чем мы сможем передать его в WebGL для отображения.
Для этого мы реализуем функцию AVX_YUV_to_RGB()
, которую вы можете найти в исходном файле yuv-to-rgb.c
. Эта функция преобразует выходные данные декодера AV1 во что-то, что мы можем передать в WebGL. Обратите внимание: когда мы вызываем эту функцию из JavaScript, нам необходимо убедиться, что память, в которую мы записываем преобразованное изображение, была выделена внутри памяти модуля WebAssembly — в противном случае он не сможет получить к ней доступ. Функция получения изображения из модуля WebAssembly и рисования его на экране выглядит следующим образом:
function show_frame(af) {
if (rgb_image != 0) {
// Convert The 16-bit YUV to 8-bit RGB
let buf = Module._AVX_Video_Frame_get_buffer(af);
Module._AVX_YUV_to_RGB(rgb_image, buf, WIDTH, HEIGHT);
// Paint the image onto the canvas
drawImageToCanvas(new Uint8Array(Module.HEAPU8.buffer,
rgb_image, 3 * WIDTH * HEIGHT), WIDTH, HEIGHT);
}
}
Функцию drawImageToCanvas()
, реализующую рисование WebGL, можно найти в исходном файле draw-image.js
для справки.
Будущая работа и выводы
Испытание нашей демонстрации на двух тестовых видеофайлах (записанных как видео с частотой 24 кадра в секунду ) учит нас нескольким вещам:
- Вполне возможно создать сложную кодовую базу для эффективной работы в браузере с помощью WebAssembly; и
- С помощью WebAssembly можно реализовать такую ресурсоемкую задачу, как расширенное декодирование видео.
Однако есть некоторые ограничения: вся реализация выполняется в основном потоке, и мы чередуем рисование и декодирование видео в этом единственном потоке. Выгрузка декодирования в веб-воркер может обеспечить более плавное воспроизведение, поскольку время декодирования кадров сильно зависит от содержимого этого кадра и иногда может занять больше времени, чем мы запланировали.
При компиляции в WebAssembly используется конфигурация AV1 для общего типа ЦП. Если мы компилируем в командной строке для обычного ЦП, мы видим аналогичную загрузку ЦП для декодирования видео, как и в версии WebAssembly, однако библиотека декодера AV1 также включает реализации SIMD , которые работают до 5 раз быстрее. Группа сообщества WebAssembly в настоящее время работает над расширением стандарта, включив в него примитивы SIMD , и когда это произойдет, обещают значительно ускорить декодирование. Когда это произойдет, станет вполне возможным декодировать HD-видео 4K в режиме реального времени с помощью видеодекодера WebAssembly.
В любом случае пример кода полезен в качестве руководства, помогающего портировать любую существующую утилиту командной строки для запуска в качестве модуля WebAssembly и показывает, что возможно в Интернете уже сегодня.
Кредиты
Благодарим Джеффа Посника, Эрика Бидельмана и Томаса Штайнера за ценные обзоры и отзывы.
,WebAssembly позволяет расширить браузер новыми функциями. В этой статье показано, как портировать видеодекодер AV1 и воспроизводить видео AV1 в любом современном браузере.
Одна из лучших особенностей WebAssembly — это возможность экспериментировать с новыми возможностями и реализовывать новые идеи до того, как браузер отправит эти функции в исходное состояние (если вообще будет). Вы можете думать об использовании WebAssembly как о высокопроизводительном механизме полифилла, в котором вы пишете свою функцию на C/C++ или Rust, а не на JavaScript.
Благодаря множеству существующего кода, доступного для портирования, в браузере можно делать вещи, которые были невозможны до появления WebAssembly.
В этой статье будет рассмотрен пример того, как взять существующий исходный код видеокодека AV1, создать для него оболочку и опробовать ее в браузере, а также даны советы, которые помогут создать тестовую программу для отладки оболочки. Полный исходный код приведенного здесь примера доступен по адресу github.com/GoogleChromeLabs/wasm-av1 для справки.
Загрузите один из этих двух тестовых видеофайлов со скоростью 24 кадра в секунду и опробуйте их на нашей встроенной демо-версии .
Выбор интересной кодовой базы
Вот уже несколько лет мы видим, что большой процент веб-трафика состоит из видеоданных. По оценкам Cisco, на самом деле этот показатель составляет целых 80 %! Конечно, производители браузеров и видеосайты прекрасно осознают желание сократить объем данных, потребляемых всем этим видеоконтентом. Ключом к этому, конечно же, является лучшее сжатие, и, как и следовало ожидать, проводится множество исследований в области сжатия видео следующего поколения, направленных на снижение нагрузки на данные при доставке видео через Интернет.
Так получилось, что Альянс открытых медиа работает над схемой сжатия видео следующего поколения под названием AV1 , которая обещает значительно сократить размер видеоданных. В будущем мы ожидаем, что браузеры будут поддерживать встроенную поддержку AV1, но, к счастью, исходный код компрессора и декомпрессора открыт , что делает его идеальным кандидатом для того, чтобы попытаться скомпилировать его в WebAssembly, чтобы мы могли поэкспериментировать с ним в браузере.

Адаптация для использования в браузере
Первое, что нам нужно сделать, чтобы разместить этот код в браузере, — это познакомиться с существующим кодом, чтобы понять, что представляет собой API. При первом взгляде на этот код можно выделить две вещи:
- Дерево исходного кода создается с помощью инструмента
cmake
; и - Существует ряд примеров, каждый из которых предполагает наличие некоторого файлового интерфейса.
Все примеры, которые собираются по умолчанию, можно запускать из командной строки, и это, вероятно, верно для многих других баз кода, доступных в сообществе. Итак, интерфейс, который мы собираемся создать для запуска его в браузере, может быть полезен для многих других инструментов командной строки.
Использование cmake
для сборки исходного кода
К счастью, авторы AV1 экспериментировали с Emscripten , SDK, который мы собираемся использовать для создания нашей версии WebAssembly. В корне репозитория AV1 файл CMakeLists.txt
содержит такие правила сборки:
if(EMSCRIPTEN)
add_preproc_definition(_POSIX_SOURCE)
append_link_flag_to_target("inspect" "-s TOTAL_MEMORY=402653184")
append_link_flag_to_target("inspect" "-s MODULARIZE=1")
append_link_flag_to_target("inspect"
"-s EXPORT_NAME=\"\'DecoderModule\'\"")
append_link_flag_to_target("inspect" "--memory-init-file 0")
if("${CMAKE_BUILD_TYPE}" STREQUAL "")
# Default to -O3 when no build type is specified.
append_compiler_flag("-O3")
endif()
em_link_post_js(inspect "${AOM_ROOT}/tools/inspect-post.js")
endif()
Инструментальная цепочка Emscripten может генерировать выходные данные в двух форматах: один называется asm.js
, а другой — WebAssembly. Мы будем ориентироваться на WebAssembly, поскольку он производит меньший результат и может работать быстрее. Эти существующие правила сборки предназначены для компиляции версии библиотеки asm.js
для использования в приложении-инспекторе, которое используется для просмотра содержимого видеофайла. Для нашего использования нам нужны выходные данные WebAssembly, поэтому мы добавляем эти строки непосредственно перед закрывающим оператором endif()
в приведенных выше правилах.
# Force generation of Wasm instead of asm.js
append_link_flag_to_target("inspect" "-s WASM=1")
append_compiler_flag("-s WASM=1")
Сборка с помощью cmake
означает сначала создание нескольких Makefiles
путем запуска самого cmake
, а затем запуск команды make
, которая выполнит этап компиляции. Обратите внимание: поскольку мы используем Emscripten, нам нужно использовать набор инструментов компилятора Emscripten, а не компилятор хоста по умолчанию. Это достигается за счет использования Emscripten.cmake
, который является частью Emscripten SDK , и передачи его пути в качестве параметра самому cmake
. Командная строка ниже — это то, что мы используем для создания файлов Makefile:
cmake path/to/aom \
-DENABLE_CCACHE=1 -DAOM_TARGET_CPU=generic -DENABLE_DOCS=0 \
-DCONFIG_ACCOUNTING=1 -DCONFIG_INSPECTION=1 -DCONFIG_MULTITHREAD=0 \
-DCONFIG_RUNTIME_CPU_DETECT=0 -DCONFIG_UNIT_TESTS=0
-DCONFIG_WEBM_IO=0 \
-DCMAKE_TOOLCHAIN_FILE=path/to/emsdk-portable/.../Emscripten.cmake
Параметр path/to/aom
должен быть установлен на полный путь к местоположению исходных файлов библиотеки AV1. Для параметра path/to/emsdk-portable/…/Emscripten.cmake
необходимо указать путь к файлу описания цепочки инструментов Emscripten.cmake.
Для удобства мы используем сценарий оболочки, чтобы найти этот файл:
#!/bin/sh
EMCC_LOC=`which emcc`
EMSDK_LOC=`echo $EMCC_LOC | sed 's?/emscripten/[0-9.]*/emcc??'`
EMCMAKE_LOC=`find $EMSDK_LOC -name Emscripten.cmake -print`
echo $EMCMAKE_LOC
Если вы посмотрите на Makefile
верхнего уровня для этого проекта, вы увидите, как этот сценарий используется для настройки сборки.
Теперь, когда все настройки выполнены, мы просто вызываем make
, который построит все дерево исходного кода, включая образцы, но, что наиболее важно, сгенерируем libaom.a
, который содержит скомпилированный видеодекодер, готовый для включения в наш проект.
Разработка API для взаимодействия с библиотекой.
После того, как мы создали нашу библиотеку, нам нужно решить, как взаимодействовать с ней, чтобы отправлять в нее сжатые видеоданные, а затем считывать кадры видео, которые мы можем отобразить в браузере.
Если заглянуть внутрь дерева кода AV1, хорошей отправной точкой будет пример видеодекодера, который можно найти в файле [simple_decoder.c](https://aomedia.googlesource.com/aom/+/master/examples/simple_decoder.c)
. Этот декодер считывает файл IVF и декодирует его в серию изображений, представляющих кадры видео.
Наш интерфейс мы реализуем в исходном файле [decode-av1.c](https://github.com/GoogleChromeLabs/wasm-av1/blob/master/decode-av1.c)
.
Поскольку наш браузер не может читать файлы из файловой системы, нам необходимо разработать некую форму интерфейса, который позволит нам абстрагировать ввод-вывод, чтобы мы могли создать что-то похожее на пример декодера для получения данных в нашу библиотеку AV1.
В командной строке файловый ввод-вывод — это так называемый потоковый интерфейс, поэтому мы можем просто определить наш собственный интерфейс, который выглядит как потоковый ввод-вывод, и построить все, что нам нравится, в базовой реализации.
Мы определяем наш интерфейс следующим образом:
DATA_Source *DS_open(const char *what);
size_t DS_read(DATA_Source *ds,
unsigned char *buf, size_t bytes);
int DS_empty(DATA_Source *ds);
void DS_close(DATA_Source *ds);
// Helper function for blob support
void DS_set_blob(DATA_Source *ds, void *buf, size_t len);
Функции open/read/empty/close
очень похожи на обычные операции файлового ввода-вывода, что позволяет нам легко сопоставлять их с файловым вводом-выводом для приложения командной строки или реализовывать их каким-либо другим способом при запуске внутри браузера. Тип DATA_Source
непрозрачен со стороны JavaScript и служит лишь для инкапсуляции интерфейса. Обратите внимание, что создание API, который точно соответствует семантике файлов, позволяет легко повторно использовать его во многих других базах кода, которые предназначены для использования из командной строки (например, diff, sed и т. д.).
Нам также необходимо определить вспомогательную функцию, называемую DS_set_blob
, которая связывает необработанные бинарные данные с нашими функциями ввода -вывода потока. Это позволяет Blob «читать», как будто это поток (то есть выглядит как файл последовательно чтения).
Наш пример реализации позволяет читать пропущенную в Blob, как если бы это было последовательно считывание источника данных. Справочный код можно найти в файле blob-api.c
, и вся реализация-именно эта:
struct DATA_Source {
void *ds_Buf;
size_t ds_Len;
size_t ds_Pos;
};
DATA_Source *
DS_open(const char *what) {
DATA_Source *ds;
ds = malloc(sizeof *ds);
if (ds != NULL) {
memset(ds, 0, sizeof *ds);
}
return ds;
}
size_t
DS_read(DATA_Source *ds, unsigned char *buf, size_t bytes) {
if (DS_empty(ds) || buf == NULL) {
return 0;
}
if (bytes > (ds->ds_Len - ds->ds_Pos)) {
bytes = ds->ds_Len - ds->ds_Pos;
}
memcpy(buf, &ds->ds_Buf[ds->ds_Pos], bytes);
ds->ds_Pos += bytes;
return bytes;
}
int
DS_empty(DATA_Source *ds) {
return ds->ds_Pos >= ds->ds_Len;
}
void
DS_close(DATA_Source *ds) {
free(ds);
}
void
DS_set_blob(DATA_Source *ds, void *buf, size_t len) {
ds->ds_Buf = buf;
ds->ds_Len = len;
ds->ds_Pos = 0;
}
Создание испытательного жгута для тестирования вне браузера
Одной из лучших практик в области разработки программного обеспечения является создание модульных тестов для кода в сочетании с интеграционными тестами.
При создании с Webassembly в браузере имеет смысл создать некоторую форму модульного теста для интерфейса для кода, с которым мы работаем, чтобы мы могли отлаживать за пределами браузера, а также иметь возможность проверить построенный нами интерфейс.
В этом примере мы подражали API на основе потока как интерфейс для библиотеки AV1. Таким образом, логически имеет смысл создать тестовый жгут, который мы можем использовать для создания версии нашего API, которая работает в командной строке и выполняет фактический ввод -вывод файлов под капотом, внедряя сам/вывод файла под нашим API DATA_Source
.
Код ввода -вывода потока для нашего тестового жгута прост и выглядит так:
DATA_Source *
DS_open(const char *what) {
return (DATA_Source *)fopen(what, "rb");
}
size_t
DS_read(DATA_Source *ds, unsigned char *buf, size_t bytes) {
return fread(buf, 1, bytes, (FILE *)ds);
}
int
DS_empty(DATA_Source *ds) {
return feof((FILE *)ds);
}
void
DS_close(DATA_Source *ds) {
fclose((FILE *)ds);
}
Аннотация интерфейса потока мы можем создать наш модуль Webassembly, чтобы использовать двоичные каждые данные в браузере, а интерфейс для реальных файлов, когда мы создаем код для тестирования из командной строки. Наш тестовый код жгута можно найти в примере исходного файла test.c
Реализация буферизации для нескольких видео кадров
При воспроизведении видео, это обычная практика, чтобы буферировать несколько кадров, чтобы помочь с более плавным воспроизведением. Для наших целей мы просто реализуем буфер из 10 кадров видео, поэтому мы буферем 10 кадров, прежде чем начнем воспроизведение. Затем каждый раз, когда отображается кадр, мы стараемся декодировать другую кадр, чтобы сохранить буфер наполненным. Этот подход гарантирует, что кадры доступны заранее, чтобы помочь остановить заикание видео.
С помощью нашего простого примера все сжатое видео доступно для чтения, поэтому буферизация на самом деле не нужна. Однако, если мы расширяем интерфейс исходных данных для поддержки потокового ввода с сервера, то нам необходимо иметь буферный механизм на месте.
Код в decode-av1.c
для чтения кадров видеодантеров из библиотеки AV1 и хранения в буфере как:
void
AVX_Decoder_run(AVX_Decoder *ad) {
...
// Try to decode an image from the compressed stream, and buffer
while (ad->ad_NumBuffered < NUM_FRAMES_BUFFERED) {
ad->ad_Image = aom_codec_get_frame(&ad->ad_Codec,
&ad->ad_Iterator);
if (ad->ad_Image == NULL) {
break;
}
else {
buffer_frame(ad);
}
}
Мы решили заставить буфер содержать 10 кадров видео, что является лишь произвольным выбором. Буферизация большего количества кадров означает больше времени ожидания для видео, чтобы начать воспроизведение, в то время как буферизация слишком мало кадров может вызвать остановку во время воспроизведения. В нативном реализации браузера буферизация кадров гораздо сложнее, чем эта реализация.
Получение видео кадров на странице с Webgl
Рамки видео, которые мы буферировали, должны отображаться на нашей странице. Поскольку это динамический видеоконтент, мы хотим иметь возможность сделать это как можно быстрее. Для этого мы обращаемся к Webgl .
Webgl позволяет нам снять изображение, такое как кадр видео, и использовать его в качестве текстуры, которая нарисована для некоторой геометрии. В мире Webgl все состоит из треугольников. Таким образом, для нашего случая мы можем использовать удобную встроенную функцию WebGL, называемую GL.Triangle_FAN.
Однако есть небольшая проблема. Предполагается, что текстуры Webgl представляют собой изображения RGB, один байт на цветной канал. Выход из нашего декодера AV1 представляет собой изображения в так называемом формате YUV, где выход по умолчанию имеет 16 бит на канал, а также каждое значение U или V соответствует 4 пикселям в фактическом выходном изображении. Все это означает, что нам нужно раскрасить изображение, прежде чем мы сможем передать его в Webgl для отображения.
Для этого мы реализуем функцию AVX_YUV_to_RGB()
, которую вы можете найти в исходном файле yuv-to-rgb.c
. Эта функция преобразует выход из декодера AV1 в то, что мы можем передать в WebGL. Обратите внимание, что когда мы называем эту функцию из JavaScript, нам нужно убедиться, что память, в которую мы записываем преобразованное изображение, была выделена в памяти модуля Webassembly - в противном случае оно не может получить к нему доступ. Функция для выхода изображения из модуля Webassembly и нарисовать его на экран, заключается в следующем:
function show_frame(af) {
if (rgb_image != 0) {
// Convert The 16-bit YUV to 8-bit RGB
let buf = Module._AVX_Video_Frame_get_buffer(af);
Module._AVX_YUV_to_RGB(rgb_image, buf, WIDTH, HEIGHT);
// Paint the image onto the canvas
drawImageToCanvas(new Uint8Array(Module.HEAPU8.buffer,
rgb_image, 3 * WIDTH * HEIGHT), WIDTH, HEIGHT);
}
}
Функция drawImageToCanvas()
, которая реализует картину WebGl, можно найти в исходном файле draw-image.js
для справки.
Будущая работа и вынос
Попробуйте нашу демонстрацию на двух тестовых видеофайлах (записано как 24 -кадр видео ) обучает нас нескольким вещам:
- Вполне возможно создать сложную кодовую базу для выполнения выполнения в браузере с использованием Webassembly; и
- Что -то столь же интенсивное процессор, как расширенное декодирование видео, возможно через Webassembly.
Хотя есть некоторые ограничения: все реализация работает в основной ветке, и мы интеркалируем живопись и декодирование видео в этом единственном потоке. Разгрузка декодирования в веб -работника может предоставить нам более плавное воспроизведение, так как время декодирования кадров сильно зависит от содержания этой кадры и иногда может занимать больше времени, чем у нас в бюджете.
Компиляция в Webassembly использует конфигурацию AV1 для общего типа ЦП. Если мы назнакомимся на командной строке для общего процессора, мы увидим аналогичную загрузку ЦП, чтобы декодировать видео, как с версией Webassembly, однако библиотека декодеров AV1 также включает реализации SIMD , которые работают в 5 раз быстрее. Группа сообщества Webassembly в настоящее время работает над расширением стандарта, чтобы включить примитивы SIMD , и когда это идет вдоль, она обещает значительно ускорить декодирование. Когда это произойдет, будет полностью возможно декодировать видео 4K HD в режиме реального времени из видео-декодера Webassembly.
В любом случае, пример кода полезен в качестве руководства, помогающего портировать любую существующую утилиту командной строки для запуска в качестве модуля Webassembly, и показывает, что возможно в Интернете уже сегодня.
Кредиты
Благодаря Джеффу Поснику, Эрику Баделману и Томасу Штайнеру за предоставление ценного обзора и отзывов.
,Webassembly позволяет нам расширить браузер с помощью новых функций. В этой статье показано, как перенести видео -декодер видео и воспроизвести видео AV1 в любом современном браузере.
Одной из лучших вещей в WebAssembly является эксперимент с новыми возможностями и реализовать новые идеи, прежде чем браузер погасит эти функции изначально (если вообще). Вы можете думать об использовании Webassembly таким образом в качестве высокопроизводительного механизма полифилля, где вы пишете свою функцию в C/C ++ или ржавчине, а не JavaScript.
Благодаря множеству существующего кода, доступного для портирования, можно сделать что -то в браузере, которые не были жизнеспособными, пока не появилась Webassembly.
Эта статья пройдет через пример того, как взять существующий исходный код видеокодека AV1, создать для него обертку и попробовать его в браузере и советы, чтобы помочь создать тестовый жгут для отладки обертки. Полный исходный код для примера здесь доступен по адресу github.com/googlechromelabs/wasm-av1 для справки.
Загрузите один из этих двух тестовых видеофайлов 24FPS и попробуйте их на нашей встроенной демонстрации .
Выбор интересной кодовой базы
В течение ряда лет мы видели, что большой процент трафика в Интернете состоит из видеодантов, Cisco оценивает его на 80% на самом деле! Конечно, поставщики браузеров и видео -сайты очень знают о желании уменьшить данные, потребляемые всем этим видеоконтентом. Ключом к этому, конечно, является лучшее сжатие, и, как и следовало ожидать, есть много исследований сжатия видео следующего поколения, направленных на снижение бремени данных о доставке видео в Интернете.
Как это происходит, альянс для Open Media работал над схемой сжатия видео следующего поколения под названием AV1 , которая обещает значительно сократить размер видеодантеров. В будущем мы ожидаем, что браузеры отправят собственную поддержку AV1, но, к счастью, исходным кодом для компрессора и декомпрессора является открытый исходный код , что делает его идеальным кандидатом для попытки собрать его в веб -ассемберри, чтобы мы могли экспериментировать с ним в браузере.

Адаптация для использования в браузере
Одна из первых вещей, которые нам нужно сделать, чтобы получить этот код в браузер, - это познакомиться с существующим кодом, чтобы понять, на что похож API. Впервые посмотрев на этот код, две вещи выделяются:
- Исходное дерево построено с использованием инструмента под названием
cmake
; и - Существует ряд примеров, которые предполагают какой-то интерфейс на основе файлов.
Все примеры, которые создаются по умолчанию, могут быть запускаются в командной строке, и это, вероятно, будет верно во многих других кодовых базах, доступных в сообществе. Таким образом, интерфейс, который мы собираемся создать, чтобы он запустил в браузере, может быть полезен для многих других инструментов командной строки.
Использование cmake
для создания исходного кода
К счастью, авторы AV1 экспериментировали с Emscripten , SDK, который мы собираемся использовать для создания нашей версии Webassembly. В корне репозитория AV1 файл CMakeLists.txt
содержит эти правила сборки:
if(EMSCRIPTEN)
add_preproc_definition(_POSIX_SOURCE)
append_link_flag_to_target("inspect" "-s TOTAL_MEMORY=402653184")
append_link_flag_to_target("inspect" "-s MODULARIZE=1")
append_link_flag_to_target("inspect"
"-s EXPORT_NAME=\"\'DecoderModule\'\"")
append_link_flag_to_target("inspect" "--memory-init-file 0")
if("${CMAKE_BUILD_TYPE}" STREQUAL "")
# Default to -O3 when no build type is specified.
append_compiler_flag("-O3")
endif()
em_link_post_js(inspect "${AOM_ROOT}/tools/inspect-post.js")
endif()
Emscripten Toolchain может генерировать выход в двух форматах, один называется asm.js
, а другой - WebAssembly. Мы будем ориентироваться на веб -ассемберри, поскольку она производит меньший выход и сможем работать быстрее. Эти существующие правила сборки предназначены для составления версии библиотеки asm.js
для использования в приложении инспектора, которое используется для просмотра содержания видеофайла. Для нашего использования нам нужен вывод Webassembly, поэтому мы добавляем эти строки непосредственно перед закрытием оператора endif()
в приведенных выше правилах.
# Force generation of Wasm instead of asm.js
append_link_flag_to_target("inspect" "-s WASM=1")
append_compiler_flag("-s WASM=1")
Строительство с cmake
означает сначала генерировать некоторые Makefiles
, запустив сам cmake
, а затем выполняется make
команды, которая выполнит шаг компиляции. Обратите внимание, что, поскольку мы используем emscripten, нам нужно использовать инструмент инструментальной компилятора Emscripten, а не компилятор хоста по умолчанию. Это достигается с использованием Emscripten.cmake
, который является частью Emscripten SDK и проходя его путь в качестве параметра самому cmake
. Командная строка ниже - это то, что мы используем для генерации Makefiles:
cmake path/to/aom \
-DENABLE_CCACHE=1 -DAOM_TARGET_CPU=generic -DENABLE_DOCS=0 \
-DCONFIG_ACCOUNTING=1 -DCONFIG_INSPECTION=1 -DCONFIG_MULTITHREAD=0 \
-DCONFIG_RUNTIME_CPU_DETECT=0 -DCONFIG_UNIT_TESTS=0
-DCONFIG_WEBM_IO=0 \
-DCMAKE_TOOLCHAIN_FILE=path/to/emsdk-portable/.../Emscripten.cmake
path/to/aom
должен быть установлен на полный путь местоположения исходных файлов библиотеки AV1. Параметр path/to/emsdk-portable/…/Emscripten.cmake
должен быть установлен на путь для файла описания инструмента emscripten.cmake инструмента.
Для удобства мы используем скрипт оболочки, чтобы найти этот файл:
#!/bin/sh
EMCC_LOC=`which emcc`
EMSDK_LOC=`echo $EMCC_LOC | sed 's?/emscripten/[0-9.]*/emcc??'`
EMCMAKE_LOC=`find $EMSDK_LOC -name Emscripten.cmake -print`
echo $EMCMAKE_LOC
Если вы посмотрите на Makefile
верхнего уровня для этого проекта, вы сможете увидеть, как этот скрипт используется для настройки сборки.
Теперь, когда вся настройка была сделана, мы просто называем make
, которая построит все дерево источника, включая образцы, но, что наиболее важно, генерировать libaom.a
, который содержит видео -декодер, собранной и готов к тому, чтобы включить в наш проект.
Проектирование API для взаимодействия с библиотекой
После того, как мы создали нашу библиотеку, нам нужно решить, как взаимодействовать с ней, чтобы отправить в нее сжатые видеоданные, а затем прочитать обратные рамы видео, которые мы можем отобразить в браузере.
Заглядывая в дерево кода AV1, хорошей отправной точкой является пример видеодеродера видео, который можно найти в файле [simple_decoder.c](https://aomedia.googlesource.com/aom/+/master/examples/simple_decoder.c)
. Этот декодер читает в файле ЭКО и декодирует его в серию изображений, которые представляют кадры в видео.
Мы реализуем наш интерфейс в исходном файле [decode-av1.c](https://github.com/GoogleChromeLabs/wasm-av1/blob/master/decode-av1.c)
.
Поскольку наш браузер не может читать файлы из файловой системы, нам нужно разработать какую -то форму интерфейса, которая позволяет нам отменить наш ввод -вывод, чтобы мы могли создать что -то похожее на пример декодер, чтобы получить данные в нашу библиотеку AV1.
В командной строке файл ввод -вывод - это то, что известен как интерфейс потока, поэтому мы можем просто определить наш собственный интерфейс, который выглядит как ввод -вывод потока и создать все, что нам нравится в базовой реализации.
Мы определяем наш интерфейс как этот:
DATA_Source *DS_open(const char *what);
size_t DS_read(DATA_Source *ds,
unsigned char *buf, size_t bytes);
int DS_empty(DATA_Source *ds);
void DS_close(DATA_Source *ds);
// Helper function for blob support
void DS_set_blob(DATA_Source *ds, void *buf, size_t len);
Функции open/read/empty/close
очень похожи на обычные операции ввода -вывода файла, которые позволяют нам легко отображать их на файле ввод -вывода для приложения командной строки или реализовать их другим способом при запуске внутри браузера. Тип DATA_Source
непрозрачен со стороны JavaScript и просто служит для инкапсуляции интерфейса. Обратите внимание, что построение API, которое внимательно следует за семантикой файла, позволяет легко использовать во многих других кодовых базах, которые предназначены для использования из командной строки (например, Diff, SED и т. Д.).
Нам также необходимо определить вспомогательную функцию, называемую DS_set_blob
, которая связывает необработанные бинарные данные с нашими функциями ввода -вывода потока. Это позволяет Blob «читать», как будто это поток (то есть выглядит как файл последовательно чтения).
Наш пример реализации позволяет читать пропущенную в Blob, как если бы это было последовательно считывание источника данных. Справочный код можно найти в файле blob-api.c
, и вся реализация-именно эта:
struct DATA_Source {
void *ds_Buf;
size_t ds_Len;
size_t ds_Pos;
};
DATA_Source *
DS_open(const char *what) {
DATA_Source *ds;
ds = malloc(sizeof *ds);
if (ds != NULL) {
memset(ds, 0, sizeof *ds);
}
return ds;
}
size_t
DS_read(DATA_Source *ds, unsigned char *buf, size_t bytes) {
if (DS_empty(ds) || buf == NULL) {
return 0;
}
if (bytes > (ds->ds_Len - ds->ds_Pos)) {
bytes = ds->ds_Len - ds->ds_Pos;
}
memcpy(buf, &ds->ds_Buf[ds->ds_Pos], bytes);
ds->ds_Pos += bytes;
return bytes;
}
int
DS_empty(DATA_Source *ds) {
return ds->ds_Pos >= ds->ds_Len;
}
void
DS_close(DATA_Source *ds) {
free(ds);
}
void
DS_set_blob(DATA_Source *ds, void *buf, size_t len) {
ds->ds_Buf = buf;
ds->ds_Len = len;
ds->ds_Pos = 0;
}
Создание испытательного жгута для тестирования вне браузера
Одной из лучших практик в области разработки программного обеспечения является создание модульных тестов для кода в сочетании с интеграционными тестами.
При создании с Webassembly в браузере имеет смысл создать некоторую форму модульного теста для интерфейса для кода, с которым мы работаем, чтобы мы могли отлаживать за пределами браузера, а также иметь возможность проверить построенный нами интерфейс.
В этом примере мы подражали API на основе потока как интерфейс для библиотеки AV1. Таким образом, логически имеет смысл создать тестовый жгут, который мы можем использовать для создания версии нашего API, которая работает в командной строке и выполняет фактический ввод -вывод файлов под капотом, внедряя сам/вывод файла под нашим API DATA_Source
.
Код ввода -вывода потока для нашего тестового жгута прост и выглядит так:
DATA_Source *
DS_open(const char *what) {
return (DATA_Source *)fopen(what, "rb");
}
size_t
DS_read(DATA_Source *ds, unsigned char *buf, size_t bytes) {
return fread(buf, 1, bytes, (FILE *)ds);
}
int
DS_empty(DATA_Source *ds) {
return feof((FILE *)ds);
}
void
DS_close(DATA_Source *ds) {
fclose((FILE *)ds);
}
Аннотация интерфейса потока мы можем создать наш модуль Webassembly, чтобы использовать двоичные каждые данные в браузере, а интерфейс для реальных файлов, когда мы создаем код для тестирования из командной строки. Наш тестовый код жгута можно найти в примере исходного файла test.c
Реализация буферизации для нескольких видео кадров
При воспроизведении видео, это обычная практика, чтобы буферировать несколько кадров, чтобы помочь с более плавным воспроизведением. Для наших целей мы просто реализуем буфер из 10 кадров видео, поэтому мы буферем 10 кадров, прежде чем начнем воспроизведение. Затем каждый раз, когда отображается кадр, мы стараемся декодировать другую кадр, чтобы сохранить буфер наполненным. Этот подход гарантирует, что кадры доступны заранее, чтобы помочь остановить заикание видео.
С помощью нашего простого примера все сжатое видео доступно для чтения, поэтому буферизация на самом деле не нужна. Однако, если мы расширяем интерфейс исходных данных для поддержки потокового ввода с сервера, то нам необходимо иметь буферный механизм на месте.
Код в decode-av1.c
для чтения кадров видеодантеров из библиотеки AV1 и хранения в буфере как:
void
AVX_Decoder_run(AVX_Decoder *ad) {
...
// Try to decode an image from the compressed stream, and buffer
while (ad->ad_NumBuffered < NUM_FRAMES_BUFFERED) {
ad->ad_Image = aom_codec_get_frame(&ad->ad_Codec,
&ad->ad_Iterator);
if (ad->ad_Image == NULL) {
break;
}
else {
buffer_frame(ad);
}
}
Мы решили заставить буфер содержать 10 кадров видео, что является лишь произвольным выбором. Буферизация большего количества кадров означает больше времени ожидания для видео, чтобы начать воспроизведение, в то время как буферизация слишком мало кадров может вызвать остановку во время воспроизведения. В нативном реализации браузера буферизация кадров гораздо сложнее, чем эта реализация.
Получение видео кадров на странице с Webgl
Рамки видео, которые мы буферировали, должны отображаться на нашей странице. Поскольку это динамический видеоконтент, мы хотим иметь возможность сделать это как можно быстрее. Для этого мы обращаемся к Webgl .
Webgl позволяет нам снять изображение, такое как кадр видео, и использовать его в качестве текстуры, которая нарисована для некоторой геометрии. В мире Webgl все состоит из треугольников. Таким образом, для нашего случая мы можем использовать удобную встроенную функцию WebGL, называемую GL.Triangle_FAN.
Однако есть небольшая проблема. Предполагается, что текстуры Webgl представляют собой изображения RGB, один байт на цветной канал. Выход из нашего декодера AV1 представляет собой изображения в так называемом формате YUV, где выход по умолчанию имеет 16 бит на канал, а также каждое значение U или V соответствует 4 пикселям в фактическом выходном изображении. Все это означает, что нам нужно раскрасить изображение, прежде чем мы сможем передать его в Webgl для отображения.
Для этого мы реализуем функцию AVX_YUV_to_RGB()
, которую вы можете найти в исходном файле yuv-to-rgb.c
. Эта функция преобразует выход из декодера AV1 в то, что мы можем передать в WebGL. Обратите внимание, что когда мы называем эту функцию из JavaScript, нам нужно убедиться, что память, в которую мы записываем преобразованное изображение, была выделена в памяти модуля Webassembly - в противном случае оно не может получить к нему доступ. Функция для выхода изображения из модуля Webassembly и нарисовать его на экран, заключается в следующем:
function show_frame(af) {
if (rgb_image != 0) {
// Convert The 16-bit YUV to 8-bit RGB
let buf = Module._AVX_Video_Frame_get_buffer(af);
Module._AVX_YUV_to_RGB(rgb_image, buf, WIDTH, HEIGHT);
// Paint the image onto the canvas
drawImageToCanvas(new Uint8Array(Module.HEAPU8.buffer,
rgb_image, 3 * WIDTH * HEIGHT), WIDTH, HEIGHT);
}
}
Функция drawImageToCanvas()
, которая реализует картину WebGl, можно найти в исходном файле draw-image.js
для справки.
Будущая работа и вынос
Попробуйте нашу демонстрацию на двух тестовых видеофайлах (записано как 24 -кадр видео ) обучает нас нескольким вещам:
- Вполне возможно создать сложную кодовую базу для выполнения выполнения в браузере с использованием Webassembly; и
- Что -то столь же интенсивное процессор, как расширенное декодирование видео, возможно через Webassembly.
Хотя есть некоторые ограничения: все реализация работает в основной ветке, и мы интеркалируем живопись и декодирование видео в этом единственном потоке. Разгрузка декодирования в веб -работника может предоставить нам более плавное воспроизведение, так как время декодирования кадров сильно зависит от содержания этой кадры и иногда может занимать больше времени, чем у нас в бюджете.
Компиляция в Webassembly использует конфигурацию AV1 для общего типа ЦП. Если мы назнакомимся на командной строке для общего процессора, мы увидим аналогичную загрузку ЦП, чтобы декодировать видео, как с версией Webassembly, однако библиотека декодеров AV1 также включает реализации SIMD , которые работают в 5 раз быстрее. Группа сообщества Webassembly в настоящее время работает над расширением стандарта, чтобы включить примитивы SIMD , и когда это идет вдоль, она обещает значительно ускорить декодирование. Когда это произойдет, будет полностью возможно декодировать видео 4K HD в режиме реального времени из видео-декодера Webassembly.
В любом случае, пример кода полезен в качестве руководства, помогающего портировать любую существующую утилиту командной строки для запуска в качестве модуля Webassembly, и показывает, что возможно в Интернете уже сегодня.
Кредиты
Благодаря Джеффу Поснику, Эрику Баделману и Томасу Штайнеру за предоставление ценного обзора и отзывов.