Добавить Автора
предисловие
рассмотрение
Что такое Н.265?
В этой статье не будет представлен H.265. Заинтересованные друзья могут прочитать следующую статью для деталей. (Первая статья — это статья, которую мы опубликовали в марте 2019 года, прошло уже 2 года, так быстро летит время)«Разработка и расшифровка плеера H.265 в сети»
Эволюция WebAssembly
После прочтения вышеуказанной статьи 2-х летней давности должно быть понятно, что браузер поддерживает H.265. Хорошей новостью является то, что после двух лет разработки Webassembly выпустила версию 1.1, добавив множество новых функций и улучшив производительность. Плохая новость заключается в том, что браузеры до сих пор не поддерживают H.265 и, вероятно, не будут поддерживать его в будущем. Итак, два года спустя, если мы хотим воспроизводить H.265 в браузере, нам все еще нужно заимствовать возможности Webassembly+FFmpeg. Эта статья не будет вводить больше, смотрите ссылку ниже для деталей.Webassembly FFmpeg
статус-кво
Какова цель этой статьи?
Прошло почти два года с тех пор, как плеер H.265 (Videox.js) был запущен на Taobao Live. Предыдущий дизайн архитектуры в основном нацелен на сцену прямой трансляции, воспроизводя прямую трансляцию m3u8 и flv.Поскольку сценой приземления прямой трансляции является якорная консоль B-стороны, сцену можно использовать только для предварительного просмотра экрана, поэтому частота кадров не высокая. Тем не менее, сервисы коротких видео в этом году в основном предназначены для конечных пользователей C. Если им необходимо воспроизводить видео 1080P/720P H.265 в веб-сценариях, они должны соответствовать требованиям основного разрешения коротких видео + битрейт для плавного воспроизведения. В то же время бизнес также должен поддерживать потребности нескольких видеоформатов, таких как (mp4/fmp4), поэтому исходная архитектура была обновлена после всесторонней оценки. Поскольку есть апгрейд, естественно накапливать опыт. По обычному распорядку захожу на статью. Конечно, за последние два года в отрасли появилось большое количество проигрывателей H.265.Я написал эту статью, чтобы поделиться собственным опытом использования этой возможности рефакторинга, надеясь помочь вам избежать новых ловушек.
Видео демонстрация
Далее будет продемонстрирована новая версия проигрывателя для воспроизведения 1-минутного видео 1080p/25fps/H.265 MP4 Конкретные параметры видео следующие:
- Предварительно загрузите 1 000 000 кадров (то есть все видео) и полностью декодируйте использование памяти, использование ЦП и интервал декодирования, которые не воспроизводятся.
Поскольку весь процесс декодирования не воспроизводится, интервал декодирования = длительному декодированию одного кадра.
Как видно из приведенного выше видео, файл размером в десятки M может быть полностью декодирован при использовании памяти в 4,6 ГБ, а загрузка ЦП достигает более 300 (4 ядра). Конечно, это совершенно неограниченно, с полным декодированием огневой мощи. Но также можно сделать вывод, что для декодирования кадра 1080p без помех требуется всего 13 мс (на основе версии mbp2015).
Старой версии живого плеера требуется 26 мс для декодирования 720p (на основе версии mbp2015), в то время как текущие 13 мс новой версии плеера для воспроизведения 1080p не являются пределом, мы продолжим исследовать возможности оптимизации в будущем. .
- Предварительно загрузите 10 кадров и декодируйте, а затем декодируйте соответствующие данные во время трансляции
Демо 1 слишком экстремально и не соответствует сцене повседневного использования, а поскольку среднее декодирование занимает всего 13 мс в крайнем случае, а частота кадров видео 25 (то есть интервал 40 мс), то можно скормить несколько кадров в декодер с интервалами, что уравновешивает воспроизведение. После скорости декодирования загрузка ЦП упала примерно до 120, а использование памяти упало до 300 МБ. В то же время он может играть плавно. Тем не менее, существует множество игровых стратегий, и вы можете связаться со мной, если у вас есть план получше.
Архитектурный дизайн
Общий архитектурный дизайн
На картинке выше показан базовый скелет нового проигрывателя, включая основные модули. Модули независимы друг от друга, и каждый получает параметры общего протокола. Например, данные, передаваемые загрузчиком в демультиплексор, представляют собой ArrayBuffer, который декапсулируется демультиплексором в данные буфера формата пакета (приложение-B) и подается в визуализатор. На приведенном выше рисунке в качестве примера используется MP4 (HVCC — это один из форматов кодового потока H.265), и замена на форматы flv и ts также следует этому процессу. Renderer отвечает за планирование декодера, синхронизацию аудио и видео, воспроизведение аудио и видео и т. д. Можно сказать, что это основной модуль проигрывателя. Представление пользовательского интерфейса в основном используется для отображения пользовательского интерфейса управления игроком, например индикатора выполнения. В этой статье не ставится цель подробно представить каждую функцию, а только подробно разобрать декодер, а другие связанные модули лишь кратко объясняются и реализуются.
ДЕМО Архитектура
Поскольку демультиплексора нет, поток Annex-B считывается непосредственно загрузчиком.
- Чтение данных Uint8Array потока Приложения-B через загрузчик
- Декодирование пакета WASM, которое отправляет данные в рабочий поток через postMessage
- WASM возвращает данные YUV в Worker через функцию обратного вызова, а затем в основной поток Canvas через postMessage.
Практические шаги
Как скомпилировать FFmpeg в пакет WASM
Теперь давайте перейдем к делу.Первым шагом будет компиляция FFmpeg для его упрощения.Зачем? Потому что FFmpeg — это не только библиотека C, но и очень большая библиотека C. Если мы хотим использовать его в Интернете, нам нужно удалить некоторые бесполезные модули.К счастью, FFmpeg предоставляет возможность соответствующей настройки.Используйте файл конфигурации корневого каталога и выполните следующие действия.
1. Подготовьте
- Перед компиляцией нам нужно перейти кофициальный сайт эмскриптенаЗагрузите последнюю версию emsdk
emsdk — это инструмент, используемый для компиляции FFmpeg в пакет wasm.
- Официальный сайтFFmpegЗагрузите исходную версию FFmpeg (эта статья основана на 4.1)
2. Скомпилируйте статическую библиотеку FFmpeg
Создайте make_decoder.sh
echo "Beginning Build:"
rm -r ./ffmpeg-lite
mkdir -p ./ffmpeg-lite # dist目录
cd ../ffmpeg # src目录,ffmpeg源码
make clean
emconfigure ./configure --cc="emcc" --cxx="em++" --ar="emar" --ranlib="emranlib" --prefix=$(pwd)/../ffmpeg-wasm/ffmpeg-lite --enable-cross-compile --target-os=none --arch=x86_32 --cpu=generic \
--enable-gpl --enable-version3 \
--disable-swresample --disable-postproc --disable-logging --disable-everything \
--disable-programs --disable-asm --disable-doc --disable-network --disable-debug \
--disable-iconv --disable-sdl2 \ # 三方库
--disable-avdevice \ # 设备
--disable-avformat \ # 格式
--disable-avfilter \ # 滤镜
--disable-decoders \ # 解码器
--disable-encoders \ # 编码器
--disable-hwaccels \ # 硬件加速
--disable-demuxers \ # 解封装
--disable-muxers \ # 封装
--disable-parsers \ # 解析器
--disable-protocols \ # 协议
--disable-bsfs \ # bit stream filter,码流转换
--disable-indevs \ # 输入设备
--disable-outdevs \ #输出设备
--disable-filters \ # 滤镜
--enable-decoder=hevc \
--enable-parser=hevc
make
make install
Поскольку возможности поддержки wasm все еще относительно ограничены, некоторые модули, которые FFmpeg использует для оптимизации производительности, должны быть отключены (например, аппаратное ускорение, сборка и т. д.). В этой статье также рассматривается только декодирование. Поэтому функции, задействованные в воспроизведении, используют только hevc-декодер (hevc=h265), а все остальные функции запрещены.
Выполните make_decoder.sh, чтобы сгенерировать упрощенную статическую библиотеку FFmpeg и соответствующий файл объявления .h в папке ffmpeg-lite.
3. Напишите входной файл
После компиляции зависимой библиотеки это не означает, что ее можно использовать напрямую.Вам также необходимо самостоятельно написать код файла порта для вызова интерфейса FFmpeg.На этом этапе вам нужно немного знать язык C . Назовем его decoder.c
Инициализировать декодер
Сначала мы вызываем init_decoder для инициализации декодера, а затем, в свою очередь, инициализируем codec, dec_ctx, синтаксический анализатор, фрейм и pkt. frame и pkt используются как глобальные переменные для последующего обмена данными. init_decoder получает функцию обратного вызова JS в качестве входного параметра. Позже данные возвращаются в рабочий поток JS через эту функцию обратного вызова. Объявление функции обратного вызова определяет три входных параметра, за которыми следуют начальный адрес данных, длина и точки. Эта статья пока не касается pts, и можно не передавать ее дальше.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
typedef void(*OnBuffer)(unsigned char* data_y, int size, int pts);
AVCodec *codec = NULL;
AVCodecContext *dec_ctx = NULL;
AVCodecParserContext *parser_ctx = NULL;
AVPacket *pkt = NULL;
AVFrame *frame = NULL;
OnBuffer decoder_callback = NULL;
void init_decoder(OnBuffer callback) {
// 找到hevc解码器
codec = avcodec_find_decoder(AV_CODEC_ID_HEVC);
// 初始化对应的解析器
parser_ctx = av_parser_init(codec->id);
// 初始化上下文
dec_ctx = avcodec_alloc_context3(codec);
// 打开decoder
avcodec_open2(dec_ctx, codec, NULL);
// 分配一个frame内存,并指明yuv 420p格式
frame = av_frame_alloc();
frame->format = AV_PIX_FMT_YUV420P;
// 分配一个pkt内存
pkt = av_packet_alloc();
// 暂存回调
decoder_callback = callback;
}
uint8 в AVPacket
Этот шаг заключается в получении видеоданных JS в метод av_parser_parse2, av_parser_parse2 получает данные буфера любой длины и анализирует структуру avpacket из буфера до тех пор, пока не будет данных. avpacket хранит сжатые мультимедийные данные.Если это тип видео, он обычно представляет один кадр, а аудиоданные представляют N кадров. Ниже приведен отрывок из комментария к исходному коду FFmpeg.
This structure stores compressed data. It is typically exported by demuxers
and then passed as input to decoders, or received as output from encoders and then passed to muxers. For video, it should typically contain one compressed frame. For audio it may contain several compressed frames. Encoders are allowed to output empty packets, with no compressed data, containing only side data (e.g. to update some stream parameters at the end of encoding).
void decode_buffer(uint8_t* buffer, size_t data_size) { // 入参是js传入的uint8array数据以及数据长度
while (data_size > 0) {
// 从buffer中解析出packet
int size = av_parser_parse2(parser_ctx, dec_ctx, &pkt->data, &pkt->size,
buffer, data_size, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
if (size < 0) {
break;
}
buffer += size;
data_size -= size;
if (pkt->size) {
// 解码packet
decode_packet(dec_ctx, frame, pkt);
}
}
}
Декодировать AVPacket, получить AVFrame
После получения avpacket вам нужно вызвать avcodec_send_packet, чтобы передать данные декодеру для декодирования.Как упоминалось выше, пакет аудиоданных может содержать несколько кадров (т.е. avframe), поэтому вызовите avcodec_receive_frame через цикл while, чтобы получить данные avframe из декодера. пока не вернет AVERROR(EAGAIN), AVERROR_EOF или ошибку. Avframe содержит декодированные данные.
AVERROR (EAGAIN) указывает на то, что пакетные данные израсходованы и необходимы новые данные. И AVERROR_EOF срабатывает, когда ваш входной pkt->data равен NULL. Обычно декодер кэширует несколько фреймов данных, и если вы хотите получить эти данные, вам нужно передать декодеру NULL pkt.
avcodec_send_packet — это новый порт версии 4.x, 3.x — это avcodec_decode_video2 и avcodec_decode_audio4. Первый, как упоминалось выше, вводится один раз и выводится много раз. Последнее — когда данных pkt недостаточно для генерации кадра, необходимо слить данные и снова вызвать метод для декодирования при поступлении последующих данных.
int decode_packet(AVCodecContext* ctx, AVFrame* frame, AVPacket* pkt)
{
int ret = 0;
// 发送packet到解码器
ret = avcodec_send_packet(dec, pkt);
if (ret < 0) {
return ret;
}
// 从解码器接收frame
while (ret >= 0) {
ret = avcodec_receive_frame(dec, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
// handle error
break;
}
// 输出yuv buffer数据
output_yuv_buffer(frame);
}
return ret;
}
AVFrame в YUV uint8
После получения декодированных данных avframe нам нужно передать их в JS, но поскольку данные avframe представляют собой двухслойный массив. И нам нужно преобразовать его в uint8 и передать в поток JS.
Существует два формата хранения изображений YUV:
- Упакованные форматы: Y, U, V трехканальные значения пикселей расположены последовательно, то есть Y0 U0 V0 Y1 U1 V1...
- Плоские форматы: сначала расположите все значения пикселей Y, затем U и, наконец, V
Формат плоскости используется в YUV420p с выборкой по горизонтали 2:1 и выборкой по вертикали 2:1, то есть каждые 4 компонента Y соответствуют компоненту U, V.
Как показано на рисунке выше, мы пишем код для копирования данных avframe в yuv_buffer по очереди и используем decoder_callback для передачи их в поток JS.
На самом деле, на этом шаге вы можете сохранить все, что захотите, но при рендеринге вам придется извлекать данные в соответствии с порядком хранения и рендерить их в формате 420p.
void output_yuv_buffer(AVFrame *frame) {
int width, height, frame_size;
uint8_t *yuv_buffer = NULL;
width = frame->width;
height = frame->height;
// 根据格式,获取buffer大小
frame_size = av_image_get_buffer_size(frame->format, width, height, 1);
// 分配内存
yuv_buffer = (uint8_t *)av_mallocz(frame_size * sizeof(uint8_t));
// 将frame数据按照yuv的格式依次填充到bufferr中。下面的步骤可以用工具函数av_image_copy_to_buffer代替。
int i, j, k;
// Y
for(i = 0; i < height; i++) {
memcpy(yuv_buffer + width*i,
frame->data[0]+frame->linesize[0]*i,
width);
}
for(j = 0; j < height / 2; j++) {
memcpy(yuv_buffer + width * i + width / 2 * j,
frame->data[1] + frame->linesize[1] * j,
width / 2);
}
for(k =0; k < height / 2; k++) {
memcpy(yuv_buffer + width * i + width / 2 * j + width / 2 * k,
frame->data[2] + frame->linesize[2] * k,
width / 2);
}
// 通过之前传入的回调函数发给js
decoder_callback(yuv_buffer, frame_size, frame->pts);
av_free(yuv_buffer);
}
Выше приведен весь код входного файла, я стараюсь использовать самый простой код для представления. Всего включены init_decoder, decode_buffer, decode_packet, output_yuv_buffer. Другие некритические части опущены, такие как (close_decoder, обработка исключений и т. д.)
Примечание. Поскольку демультиплексор и bsfs не включаются во время компиляции. Таким образом, данные буфера, полученные decoder_buffer, должны быть потоком приложенияb.
4. Скомпилируйте пакет WASM
Наконец, в конце этого раздела скомпилируйте входной файл + зависимую библиотеку в пакет wasm. Этот шаг относительно прост, создайте файл build_decoder.sh, напишите следующий код, а затем выполните его.
export TOTAL_MEMORY=67108864
export EXPORTED_FUNCTIONS="[ \
'_init_decoder', \
'_decode_buffer'
]"
echo "Running Emscripten..."
# 入口文件+3个依赖库文件
emcc decoder.c ffmpeg-lite/lib/libavcodec.a ffmpeg-lite/lib/libavutil.a ffmpeg-lite/lib/libswscale.a \
-O2 \
-I "ffmpeg-lite/include" \
-s WASM=1 \
-s ASSERTIONS=1 \
-s LLD_REPORT_UNDEFINED \
-s NO_EXIT_RUNTIME=1 \
-s DISABLE_EXCEPTION_CATCHING=1 \
-s TOTAL_MEMORY=${TOTAL_MEMORY} \
-s EXPORTED_FUNCTIONS="${EXPORTED_FUNCTIONS}" \
-s EXTRA_EXPORTED_RUNTIME_METHODS="['addFunction', 'removeFunction']" \
-s RESERVED_FUNCTION_POINTERS=14 \
-s FORCE_FILESYSTEM=1 \
-o ./wasm/libffmpeg.js
echo "Finished Build"
EXPORTED_FUNCTIONS — это метод, который необходимо указать в файле записи. Не забудьте добавить_
Продукт сборки выглядит следующим образом:
libffmpeg.js — это входной файл JS пакета wasm.
Как JS загружает и вызывает методы пакета WASM
Рабочий раздел
Эта часть является нашим основным полем, написанием кода JS (используя синтаксис TypeScript, это не должно влиять на чтение). Потому что код WASM должен выполняться в рабочем потоке. Таким образом, переменные среды следующего кода могут быть доступны только в рабочем
decoder.ts
export class Decoder extends EventEmitter<IEventMap> {
M: any
init(M: any) {
// M = self.Module 即wasm环境变量
this.M = M
// 创建wasm的回调函数,viii表示有3个int参数
const callback = this.M.addFunction(this._handleYUVData, 'viii')
// 通过我们上面decoder.c文件的方法传入回调
this.M._init_decoder(callback)
}
decode(packet: IPacket) {
const { data } = packet
const typedArray = data
const bufferLength = typedArray.length
// 申请内存区,并放入数据
const bufferPtr = this.M._malloc(bufferLength)
this.M.HEAPU8.set(typedArray, bufferPtr)
// 解码buffer
this.M._decode_buffer(bufferPtr, bufferLength)
// 释放内存区
this.M._free(bufferPtr)
}
private _handleYUVData = (start: number, size: number, pts: number) => {
// 回调传回来的第一个参数是yuv_buffer的内存起始索引
const u8s = this.M.HEAPU8.subarray(start, start + size)
const output = new Uint8Array(u8s)
this.emit('decoded-frame', {
data: output,
pts,
})
}
}
decoder-manager.ts
Поскольку рабочий поток загружает файл wasm асинхронно, а метод wasm нужно вызывать после onRuntimeInitialized, поэтому написан простой менеджер для управления декодером.
import { Decoder } from './decoder'
const global = self as any
export class DecoderManager {
loaded = false
decoder = new Decoder()
cachePackets: IPacket[] = []
load() {
// 表明wasm文件的位置
global.Module = {
locateFile: (wasm: string) => './wasm/' + wasm,
}
global.importScripts('./wasm/libffmpeg.js')
// 初始化之后,执行一次push,把缓存的packet送到decoder里
global.Module.onRuntimeInitialized = () => {
this.loaded = true
this.decoder.init(global.Module)
this.push([])
}
this.decoder.on('decoded-frame', this.handleYUVBuffer)
}
push(packets: IPacket[]) {
// 没加载就缓存起来,加载了就先取缓存
if (!this.loaded) {
this.cachePackets = this.cachePackets.concat(packets)
} else {
if (this.cachePackets.length) {
this.cachePackets.forEach((frame) => this.decoder.decode(frame))
this.cachePackets = []
}
packets.forEach((frame) => this.decoder.decode(frame))
}
}
handleYUVBuffer = (frame) => {
global.postMessage({
type: 'decoded-frame',
data: frame,
})
}
}
const manager = new DecoderManager()
manager.load()
self.onmessage = function(event) {
const data = event.data
const type = data.type
switch (type) {
case 'decode':
manager.push(data.data)
break
}
}
Основная часть потока JS
Этот шаг заключается в загрузке рабочего кода и связи. Процесс загрузки воркера очень прост, просто используйте webpack+worker-loader, а затем используйте fetch для рекурсивного чтения данных и отправки их в рабочий поток, а кодировщик декодирует данные, когда он их получит.
import Worker from 'worker-loader!../worker/decoder-manager'
const worker = new Worker()
const url = 'http://xx.com' // 码流地址
fetch(url)
.then((res) => {
if (res.body) {
const reader = res.body.getReader()
const read = () => {
// 递归读取buffer数据
reader.read().then((json) => {
if (!json.done) {
worker.postMessage({
type: 'decode',
data: [{
data: json
}],
})
read()
}
})
}
read()
}
})
Эпилог
В соответствии с приведенным выше кодом может быть реализован простой декодер H.265.Ниже приведены данные, напечатанные JS, имитирующие структуры AVPacket и AVFrame, перечисленные выше:
Перед декодированием: данные передаются в WASM из основного потока JS.
После декодирования: данные передаются из WASM в основной поток JS.
Сравнение приведенного выше изображения показывает, насколько ужасающим является количество декодированных данных, поэтому, как показано в видео в начале, управление памятью после декодирования очень важно.
Выше приведено полное содержание главы о декодировании видео H.265. Декодирование аудио также может повторно использовать приведенную выше ссылку для декодирования или использовать decodeAudioData, поставляемый с браузером. Воспроизведение аудио использует AudioContext. Текущие основные форматы кодирования аудио поддерживаются браузерами. Наконец, я надеюсь, что описанный выше обмен опытом поможет вам меньше наступать на ямы. Помимо воспроизведения H.265, FFmpeg также может выполнять большую обработку видео. Вы можете подумать о возможных сценариях применения с дивергентным мышлением, и в будущем будет больше статей о сериях игроков.