IVWEB воспроизводит серию WASM - Практика рендеринга изображений WEBGL YUV

WebAssembly

Недавно команда использует WASM + FFmpeg для создания WEB-плеера. Мы декодируем видео с помощью FFmpeg, используя язык C, и взаимодействуем с помощью JavaScript, компилируя язык C в WASM, работающий в браузере. По умолчанию данные, декодированные FFmpeg, имеют формат yuv, а canvas поддерживает только рендеринг rgb. Тогда у нас есть два способа справиться с этим yuv. Первый использует метод, предоставленный FFmpeg, для прямого преобразования yuv в rgb и последующего рендеринга в холст. Два используют webgl для преобразования yuv в rgb и рендеринга на холсте. Первое преимущество заключается в том, что метод записи очень прост. Требуется только метод, предоставленный FFmpeg, для прямого преобразования yuv в rgb. Недостатком является то, что он будет потреблять определенное количество ресурсов ЦП. Второе преимущество заключается в том, что он будет использовать GPU для ускорение Знаком с WEBGL. Учитывая, что для снижения нагрузки на CPU и использования GPU для параллельного ускорения мы выбрали второй способ.

Прежде чем говорить о YUV, давайте посмотрим, как получается YUV:

实现播放器必定要经过的步骤
Так как мы пишем плеер, то шаги по реализации плеера должны проходить через следующие этапы:

  1. Демультиплексирование видеофайлов, таких как mp4, avi, flv и т. д., mp4, avi, flv эквивалентно контейнеру, который содержит некоторую информацию, такую ​​как сжатое видео, сжатое аудио и т. д., и извлекает сжатие из контейнера. сжатое видео обычно имеет формат H265, H264 или другие форматы, а сжатое аудио обычно имеет формат aac или mp3.
  2. Сжатое видео и сжатое аудио декодируются соответственно для получения исходного видео и аудио.Исходные аудиоданные обычно имеют формат pcm, а исходные видеоданные обычно имеют формат yuv или rgb.
  3. Затем выполните синхронизацию аудио и видео. После того, как вы сможете просмотреть декодированные сжатые видеоданные, вы, как правило, получите yuv.

YUV

Что такое ЮВ

Для фронтенд-разработчиков YUV на самом деле немного незнаком.Те, кто занимался аудио- и видеоразработкой, обычно с этим соприкасаются.Говоря простым языком, YUV похож на привычный RGB, оба из которых цветные. - закодированы, но их три буквы Значение представления отличается от RGB. «Y» YUV представляет яркость (яркость или яркость), которая является значением серого, а «U» и «V» представляют цветность (Chrominance или Chroma), который описывает цвет изображения, и насыщенность, которые определяют цвет пикселя.

Чтобы у всех было более интуитивное представление о YUV, давайте посмотрим, как выглядят по отдельности Y, U и V. Здесь команда FFmpeg используется для преобразования изображения Наруто Учиха Итачи в YUV420P:

ffmpeg -i frame.jpg -s 352x288 -pix_fmt yuv420p test.yuv

существуетGLYUVPlayоткрыть в программеtest.yuv, показывая исходное изображение:

原图
Компонент Y отображается отдельно:
Y
Компонент U отображается отдельно:
U
Компонент V отображается отдельно:
V
Из приведенного выше видно, что полное изображение может отображаться, когда Y отображается отдельно, но изображение серое. А U и V представляют цветность, один голубоватый, а другой красноватый.

Преимущества использования ЮВ

  1. Из того, что вы только что видели, только Y отображает черно-белое изображение, поэтому формат YUV очень просто преобразовать из цветного в черно-белый, и он может быть совместим со старомодными черно-белыми телевизорами.Эта функция используется на телевизионные сигналы.
  2. Размер данных YUV обычно меньше, чем у формата RGB, что может сэкономить полосу пропускания при передаче. (Но если используется YUV444, это 24 бита, как RGB24)

выборка YUV

Распространенными образцами YUV являются YUV444, YUV422, YUV420:

Примечание. Черная точка представляет собой компонент Y пикселя, из которого производится выборка, а пустой кружок представляет компонент UV пикселя.

  1. Выборка YUV 4:4:4, каждый Y соответствует набору компонентов UV.
  2. Выборка YUV 4:2:2, каждые два Y разделяют набор компонентов UV.
  3. Выборка YUV 4:2:0, каждые четыре Y разделяют набор компонентов UV.

Метод хранения YUV

Существует два типа форматов хранения YUV: упакованный и планарный:

  • В упакованном формате YUV Y, U, V каждого пикселя сохраняются непрерывно и чередуются.
  • Плоский формат YUV хранит Y всех пикселей последовательно, затем U всех пикселей, а затем V всех пикселей.

Например, для планарного режима YUV может хранить YYYYUUVV следующим образом, а для упакованного режима YUV может хранить YUYVYUYV следующим образом.

Обычно существует много форматов YUV, YUV420SP, YUV420P, YUV422P, YUV422SP и т. д. Давайте рассмотрим более распространенные форматы:

  • YUV420P (каждые четыре Y используют набор UV-компонентов):

  • YUV420SP (упакованный, каждые четыре Y будут иметь общий набор компонентов UV, а YUV420P отличается тем, что при сохранении YUV420SP U и V чередуются):

  • YUV422P (плоский, каждые два Y разделяют набор компонентов UV, поэтому U и V будут иметь на одну строку больше, чем YUV420P U и V):

  • YUV422SP (в упаковке, каждые два Y делят набор УФ-деталей):

Среди них YUV420P и YUV420SP можно разделить на 2 формата в соответствии с порядком U и V:

  • YUV420P: U до V послеYUV420P,Также известен какI420, V перед U, звонокYV12.

  • YUV420SP: U передняя V задняя ставкаNV12, V до U после ставкиNV21.

Данные расположены следующим образом:

I420: YYYYYYYY UU VV =>YUV420P

YV12: YYYYYYYY VV UU =>YUV420P

NV12: YYYYYYYY UV UV =>YUV420SP

NV21: YYYYYYYY VU VU =>YUV420SP

Насчет почему столько форматов, то после долгих поисков выяснилось, что причина в адаптации к разным системам ТВ вещания и системам оборудования, типа только этот режим под iosNV12, режим Android естьNV21,НапримерYUV411,YUV420Формат более распространен в данных цифровых камер, первый используется дляNTSCсистема, последняя используется дляPALсистема. Что касается внедрения системы телевещания, мы можем прочитать эту статью[Стандарт] Введение в NTSC, PAL, SECAM

Метод расчета ЮВ

Взяв в качестве примера YUV420P для хранения изображения 1080 x 1280, размер его хранилища составляет((1080 x 1280 x 3) >> 1)байт, как это считается? Давайте посмотрим на картинку ниже:

В хранилище Y420P размер Y равенW x H = 1080x1280, У есть(W/2) * (H/2)= (W*H)/4 = (1080x1280)/4, аналогично V есть(W*H)/4 = (1080x1280)/4, так что картинкаY+U+V = (1080x1280)*3/2. Поскольку все три части являются хранилищем в начале строки, а Y, U, V сохраняются последовательно между тремя частями, то место хранения YUV будет следующим (PS: будет использоваться позже):

Y:0 到 1080*1280
U:1080*1280 到 (1080*1280)*5/4
V:(1080*1280)*5/4 到 (1080*1280)*3/2

WEBGL

Что такое ВЕБГЛ

Проще говоря, WebGL — это технология, используемая для рисования и рендеринга сложной 3D-графики на веб-страницах и позволяющая пользователям взаимодействовать с ними.

WEBGL-композиция

В мире webgl основными графическими элементами, которые можно рисовать, являются только точки, линии и треугольники.Каждое изображение состоит из больших и малых треугольников, как показано на рисунке ниже.Какой бы сложной ни была графика, основные компоненты состоят из треугольников...

图来源于网络

шейдер

Шейдеры — это программы, работающие на графическом процессоре и написанные на языке шейдеров OpenGL ES, который чем-то похож на язык C:

Конкретный синтаксис см.Введение в язык шейдеров GLSL (opengl-shader-language), я не буду вдаваться в подробности здесь.

Для рисования графики в WEBGL необходимо иметь два шейдера:

  • вершинный шейдер
  • фрагментный шейдер

При этом основная функция вершинного шейдера используется для обработки вершин и фрагментов шейдера используется для обработки каждого фрагмента, генерируемого этапом растирации (PS: фрагмент, можно понять как пиксель), и, наконец, рассчитывается для каждого цвета пикселя.

Процесс рисования WEBGL

1. Укажите координаты вершинТак как программа тупая и не знает вершины графа, нам нужно предоставить ее самим, координаты вершин можно прописать вручную или экспортировать программно:

В этом графе мы записываем вершины в буфер, объект буфера — это область памяти в системе WebGL, мы можем за один раз заполнить большое количество данных вершин в объект буфера, а затем сохранить данные в нем, для использование вершинными шейдерами. Затем мы создаем и компилируем вершинные и фрагментные шейдеры и используем программу для соединения двух шейдеров и их использования. Например, чтобы понять, почему это делается, мы можем понимать это как создание элемента Fragment:let f = document.createDocumentFragment(), После того, как все шейдеры будут созданы и скомпилированы, они будут в свободном состоянии, нам нужно их подключить и использовать (что можно понимать какdocument.body.appendChild(f), добавлены в тело, элементы dom видно, то есть контактировать и пользоваться). Затем нам также нужно буферизировать и подключить вершинные шейдеры, чтобы они вступили в силу.

2. Элемент сборкиПосле того, как мы предоставим вершины, графический процессор выполнит программу вершинного шейдера одну за другой в соответствии с количеством предоставленных нами вершин, сгенерирует окончательные координаты вершин и соберет граф. Можно понять, что для изготовления воздушного змея нужно сначала построить каркас воздушного змея, а сборка примитивов находится на этом этапе.

3. РастеризацияЭтот этап похож на изготовление воздушного змея.После создания скелета воздушного змея он не может летать в это время, потому что внутри пусто, и к скелету нужно добавить ткань. На данном этапе происходит растеризация, преобразование собранной геометрии примитивов во фрагменты (PS: под фрагментами можно понимать пиксели).

4. Затенение и рендеринг

Этот этап раскрашивания является как бы завершением построения полотна змея, но узора в это время нет, а рисунок нужно нарисовать, чтобы змей стал красивее, то есть растеризованная графика на данном этапе не имеет цвета. время, и должен быть обработан фрагментным шейдером.Фрагменты раскрашиваются и записываются в цветовой буфер, и, наконец, геометрия с изображением может отображаться в браузере.

СуммироватьПроцесс рисования WEBGL можно обобщить следующим образом:

  1. Укажите координаты вершины (требуется, чтобы мы предоставили)
  2. Сборка элементов (собрана в графику по типу элемента)
  3. Растеризация (собранная графика с примитивами для генерации пикселей)
  4. Укажите значения цвета (можно рассчитать динамически, окраска пикселей)
  5. Рисование в браузере через холст.

Идеи для рисования WEBGL YUV

Так как изображение каждого видеокадра разное, мы не должны знать столько вершин, так как же нам нарисовать изображение видеокадра с помощью webgl? Здесь используется хитрость — наложение текстуры. Проще говоря, это наклеивание изображения на поверхность геометрической фигуры, чтобы геометрическая фигура выглядела как геометрическая фигура с изображением, то есть взаимно-однозначное соответствие между координатами текстуры и координатами системы webgl:

Как показано на рисунке выше, это координата текстуры, которая делится на координаты s и t (или координаты uv), диапазон значений находится между [0, 1], и значение не имеет ничего общего с изображением. размер и разрешение. На картинке ниже представлена ​​система координат webgl, которая представляет собой трехмерную систему координат. Здесь объявлены четыре вершины, и два треугольника используются для формирования прямоугольника, а затем вершины текстурных координат соответствуют системе координат webgl один -к одному и, наконец, переданный шейдеру фрагментов, шейдер фрагментов извлекает цвет каждого тексела изображения, выводит его в цветовой буфер и, наконец, отрисовывает в браузере (PS: тексели можно понимать как пиксели, которые составить текстурное изображение). Однако, если взаимно однозначное соответствие выполняется по рисунку, отображение будет обратным, т.к. координаты изображения холста по умолчанию (0, 0) находятся в верхнем левом углу:

Координаты текстуры находятся в левом нижнем углу, поэтому при отрисовке изображение будет перевернутым.Решений два:

  • Чтобы перевернуть ось Y изображения текстуры, webgl предоставляет API:
// 1代表对纹理图像进行y轴反转
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
  • Координаты текстуры и отображение координат webgl меняются местами, например, как показано на рисунке выше, исходные координаты текстуры(0.0,1.0)Соответствует координатам webgl(-1.0,1.0,0.0),(0.0,0.0)соответствует(-1.0,-1.0,0.0), то мы обращаем его,(0.0,1.0)соответствует(-1.0,-1.0,0.0),а также(0.0,0.0)соответствует(-1.0,1.0,0.0), так что изображение в браузере не будет инвертировано.

подробные шаги

  • раздел шейдеров
// 顶点着色器vertexShader
attribute lowp vec4 a_vertexPosition; // 通过 js 传递顶点坐标
attribute vec2 a_texturePosition; // 通过 js 传递纹理坐标
varying vec2 v_texCoord; // 传递纹理坐标给片元着色器
void main(){
	gl_Position=a_vertexPosition;// 设置顶点坐标
	v_texCoord=a_texturePosition;// 设置纹理坐标
}


// 片元着色器fragmentShader
precision lowp float;// lowp代表计算精度,考虑节约性能使用了最低精度
uniform sampler2D samplerY;// sampler2D是取样器类型,图片纹理最终存储在该类型对象中
uniform sampler2D samplerU;// sampler2D是取样器类型,图片纹理最终存储在该类型对象中
uniform sampler2D samplerV;// sampler2D是取样器类型,图片纹理最终存储在该类型对象中
varying vec2 v_texCoord; // 接受顶点着色器传来的纹理坐标
void main(){
  float r,g,b,y,u,v,fYmul;
  y = texture2D(samplerY, v_texCoord).r;
  u = texture2D(samplerU, v_texCoord).r;
  v = texture2D(samplerV, v_texCoord).r;
	
	// YUV420P 转 RGB	
  fYmul = y * 1.1643828125;
  r = fYmul + 1.59602734375 * v - 0.870787598;
  g = fYmul - 0.39176171875 * u - 0.81296875 * v + 0.52959375;
  b = fYmul + 2.01723046875 * u - 1.081389160375;
  gl_FragColor = vec4(r, g, b, 1.0);
}
  • Создайте и скомпилируйте шейдер, подключите вершинный шейдер и фрагментный шейдер к программе и используйте:
let vertexShader=this._compileShader(vertexShaderSource,gl.VERTEX_SHADER);// 创建并编译顶点着色器
let fragmentShader=this._compileShader(fragmentShaderSource,gl.FRAGMENT_SHADER);// 创建并编译片元着色器

let program=this._createProgram(vertexShader,fragmentShader);// 创建program并连接着色器

  • Создадим буфер, сохраним вершины и текстурные координаты (PS: Буферный объект - это область памяти в системе WebGL, мы можем за один раз залить в буферный объект большое количество вершинных данных, а затем сохранить в нем эти данные для вершин использование шейдера).
let vertexBuffer = gl.createBuffer();
let vertexRectangle = new Float32Array([
	1.0,
	1.0,
	0.0,
	-1.0,
	1.0,
	0.0,
	1.0,
	-1.0,
	0.0,
	-1.0,
	-1.0,
	0.0
]);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 向缓冲区写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertexRectangle, gl.STATIC_DRAW);
// 找到顶点的位置
let vertexPositionAttribute = gl.getAttribLocation(program, 'a_vertexPosition');
// 告诉显卡从当前绑定的缓冲区中读取顶点数据
gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
// 连接vertexPosition 变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(vertexPositionAttribute);

// 声明纹理坐标
let textureRectangle = new Float32Array([1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0]);
let textureBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer);
gl.bufferData(gl.ARRAY_BUFFER, textureRectangle, gl.STATIC_DRAW);
let textureCoord = gl.getAttribLocation(program, 'a_texturePosition');
gl.vertexAttribPointer(textureCoord, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(textureCoord); 
  • Инициализировать и активировать текстурный блок (YUV)
//激活指定的纹理单元
gl.activeTexture(gl.TEXTURE0);
gl.y=this._createTexture(); // 创建纹理
gl.uniform1i(gl.getUniformLocation(program,'samplerY'),0);//获取samplerY变量的存储位置,指定纹理单元编号0将纹理对象传递给samplerY

gl.activeTexture(gl.TEXTURE1);
gl.u=this._createTexture();
gl.uniform1i(gl.getUniformLocation(program,'samplerU'),1);//获取samplerU变量的存储位置,指定纹理单元编号1将纹理对象传递给samplerU

gl.activeTexture(gl.TEXTURE2);
gl.v=this._createTexture();
gl.uniform1i(gl.getUniformLocation(program,'samplerV'),2);//获取samplerV变量的存储位置,指定纹理单元编号2将纹理对象传递给samplerV
  • Рендеринг и рисование (PS: поскольку мы получили данные YUV420P, метод расчета может относиться к только что упомянутому методу расчета).
 // 设置清空颜色缓冲时的颜色值
 gl.clearColor(0, 0, 0, 0);
 // 清空缓冲
 gl.clear(gl.COLOR_BUFFER_BIT);

let uOffset = width * height;
let vOffset = (width >> 1) * (height >> 1);

gl.bindTexture(gl.TEXTURE_2D, gl.y);
// 填充Y纹理,Y 的宽度和高度就是 width,和 height,存储的位置就是data.subarray(0, width * height)
gl.texImage2D(
	gl.TEXTURE_2D,
	0,
	gl.LUMINANCE,
	width,
	height,
	0,
	gl.LUMINANCE,
	gl.UNSIGNED_BYTE,
	data.subarray(0, uOffset)
);

gl.bindTexture(gl.TEXTURE_2D, gl.u);
// 填充U纹理,Y 的宽度和高度就是 width/2 和 height/2,存储的位置就是data.subarray(width * height, width/2 * height/2 + width * height)
gl.texImage2D(
	gl.TEXTURE_2D,
	0,
	gl.LUMINANCE,
	width >> 1,
	height >> 1,
	0,
	gl.LUMINANCE,
	gl.UNSIGNED_BYTE,
	data.subarray(uOffset, uOffset + vOffset)
);

gl.bindTexture(gl.TEXTURE_2D, gl.v);
// 填充U纹理,Y 的宽度和高度就是 width/2 和 height/2,存储的位置就是data.subarray(width/2 * height/2 + width * height, data.length)
gl.texImage2D(
	gl.TEXTURE_2D,
	0,
	gl.LUMINANCE,
	width >> 1,
	height >> 1,
	0,
	gl.LUMINANCE,
	gl.UNSIGNED_BYTE,
	data.subarray(uOffset + vOffset, data.length)
);

gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); // 绘制四个点,也就是长方形

Вышеупомянутые шаги, наконец, могут быть нарисованы в этой картине:

Полный код:

export default class WebglScreen {
    constructor(canvas) {
        this.canvas = canvas;
        this.gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
        this._init();
    }

    _init() {
        let gl = this.gl;
        if (!gl) {
            console.log('gl not support!');
            return;
        }
        // 图像预处理
        gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
        // GLSL 格式的顶点着色器代码
        let vertexShaderSource = `
            attribute lowp vec4 a_vertexPosition;
            attribute vec2 a_texturePosition;
            varying vec2 v_texCoord;
            void main() {
                gl_Position = a_vertexPosition;
                v_texCoord = a_texturePosition;
            }
        `;

        let fragmentShaderSource = `
            precision lowp float;
            uniform sampler2D samplerY;
            uniform sampler2D samplerU;
            uniform sampler2D samplerV;
            varying vec2 v_texCoord;
            void main() {
                float r,g,b,y,u,v,fYmul;
                y = texture2D(samplerY, v_texCoord).r;
                u = texture2D(samplerU, v_texCoord).r;
                v = texture2D(samplerV, v_texCoord).r;

                fYmul = y * 1.1643828125;
                r = fYmul + 1.59602734375 * v - 0.870787598;
                g = fYmul - 0.39176171875 * u - 0.81296875 * v + 0.52959375;
                b = fYmul + 2.01723046875 * u - 1.081389160375;
                gl_FragColor = vec4(r, g, b, 1.0);
            }
        `;

        let vertexShader = this._compileShader(vertexShaderSource, gl.VERTEX_SHADER);
        let fragmentShader = this._compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER);

        let program = this._createProgram(vertexShader, fragmentShader);

        this._initVertexBuffers(program);

        // 激活指定的纹理单元
        gl.activeTexture(gl.TEXTURE0);
        gl.y = this._createTexture();
        gl.uniform1i(gl.getUniformLocation(program, 'samplerY'), 0);

        gl.activeTexture(gl.TEXTURE1);
        gl.u = this._createTexture();
        gl.uniform1i(gl.getUniformLocation(program, 'samplerU'), 1);

        gl.activeTexture(gl.TEXTURE2);
        gl.v = this._createTexture();
        gl.uniform1i(gl.getUniformLocation(program, 'samplerV'), 2);
    }
    /**
     * 初始化顶点 buffer
     * @param {glProgram} program 程序
     */

    _initVertexBuffers(program) {
        let gl = this.gl;
        let vertexBuffer = gl.createBuffer();
        let vertexRectangle = new Float32Array([
            1.0,
            1.0,
            0.0,
            -1.0,
            1.0,
            0.0,
            1.0,
            -1.0,
            0.0,
            -1.0,
            -1.0,
            0.0
        ]);
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
        // 向缓冲区写入数据
        gl.bufferData(gl.ARRAY_BUFFER, vertexRectangle, gl.STATIC_DRAW);
        // 找到顶点的位置
        let vertexPositionAttribute = gl.getAttribLocation(program, 'a_vertexPosition');
        // 告诉显卡从当前绑定的缓冲区中读取顶点数据
        gl.vertexAttribPointer(vertexPositionAttribute, 3, gl.FLOAT, false, 0, 0);
        // 连接vertexPosition 变量与分配给它的缓冲区对象
        gl.enableVertexAttribArray(vertexPositionAttribute);

        let textureRectangle = new Float32Array([1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0]);
        let textureBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, textureRectangle, gl.STATIC_DRAW);
        let textureCoord = gl.getAttribLocation(program, 'a_texturePosition');
        gl.vertexAttribPointer(textureCoord, 2, gl.FLOAT, false, 0, 0);
        gl.enableVertexAttribArray(textureCoord);
    }

    /**
     * 创建并编译一个着色器
     * @param {string} shaderSource GLSL 格式的着色器代码
     * @param {number} shaderType 着色器类型, VERTEX_SHADER 或 FRAGMENT_SHADER。
     * @return {glShader} 着色器。
     */
    _compileShader(shaderSource, shaderType) {
        // 创建着色器程序
        let shader = this.gl.createShader(shaderType);
        // 设置着色器的源码
        this.gl.shaderSource(shader, shaderSource);
        // 编译着色器
        this.gl.compileShader(shader);
        const success = this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS);
        if (!success) {
            let err = this.gl.getShaderInfoLog(shader);
            this.gl.deleteShader(shader);
            console.error('could not compile shader', err);
            return;
        }

        return shader;
    }

    /**
     * 从 2 个着色器中创建一个程序
     * @param {glShader} vertexShader 顶点着色器。
     * @param {glShader} fragmentShader 片断着色器。
     * @return {glProgram} 程序
     */
    _createProgram(vertexShader, fragmentShader) {
        const gl = this.gl;
        let program = gl.createProgram();

        // 附上着色器
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);

        gl.linkProgram(program);
        // 将 WebGLProgram 对象添加到当前的渲染状态中
        gl.useProgram(program);
        const success = this.gl.getProgramParameter(program, this.gl.LINK_STATUS);

        if (!success) {
            console.err('program fail to link' + this.gl.getShaderInfoLog(program));
            return;
        }

        return program;
    }

    /**
     * 设置纹理
     */
    _createTexture(filter = this.gl.LINEAR) {
        let gl = this.gl;
        let t = gl.createTexture();
        // 将给定的 glTexture 绑定到目标(绑定点
        gl.bindTexture(gl.TEXTURE_2D, t);
        // 纹理包装 参考https://github.com/fem-d/webGL/blob/master/blog/WebGL基础学习篇(Lesson%207).md -> Texture wrapping
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        // 设置纹理过滤方式
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
        return t;
    }

    /**
     * 渲染图片出来
     * @param {number} width 宽度
     * @param {number} height 高度
     */
    renderImg(width, height, data) {
        let gl = this.gl;
        // 设置视口,即指定从标准设备到窗口坐标的x、y仿射变换
        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
        // 设置清空颜色缓冲时的颜色值
        gl.clearColor(0, 0, 0, 0);
        // 清空缓冲
        gl.clear(gl.COLOR_BUFFER_BIT);

        let uOffset = width * height;
        let vOffset = (width >> 1) * (height >> 1);

        gl.bindTexture(gl.TEXTURE_2D, gl.y);
        // 填充纹理
        gl.texImage2D(
            gl.TEXTURE_2D,
            0,
            gl.LUMINANCE,
            width,
            height,
            0,
            gl.LUMINANCE,
            gl.UNSIGNED_BYTE,
            data.subarray(0, uOffset)
        );

        gl.bindTexture(gl.TEXTURE_2D, gl.u);
        gl.texImage2D(
            gl.TEXTURE_2D,
            0,
            gl.LUMINANCE,
            width >> 1,
            height >> 1,
            0,
            gl.LUMINANCE,
            gl.UNSIGNED_BYTE,
            data.subarray(uOffset, uOffset + vOffset)
        );

        gl.bindTexture(gl.TEXTURE_2D, gl.v);
        gl.texImage2D(
            gl.TEXTURE_2D,
            0,
            gl.LUMINANCE,
            width >> 1,
            height >> 1,
            0,
            gl.LUMINANCE,
            gl.UNSIGNED_BYTE,
            data.subarray(uOffset + vOffset, data.length)
        );

        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    }

    /**
     * 根据重新设置 canvas 大小
     * @param {number} width 宽度
     * @param {number} height 高度
     * @param {number} maxWidth 最大宽度
     */
    setSize(width, height, maxWidth) {
        let canvasWidth = Math.min(maxWidth, width);
        this.canvas.width = canvasWidth;
        this.canvas.height = canvasWidth * height / width;
    }

    destroy() {
        const {
            gl
        } = this;

        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
    }
}

Наконец, давайте взглянем на визуализацию:

возникшие проблемы

В процессе разработки мы протестировали несколько прямых трансляций. Иногда при рендеринге изображение отображается нормально, но цвет будет зеленым. После исследования выяснилось, что ширина видео для разных прямых трансляций будет разной. Например, когда якорь находится в поле Когда используется pk, ширина равна 368, ширина популярных якорей будет 720, ширина маленьких якорей будет 540, а ширина 540 будет зеленоватой. Конкретная причина в том, что webgl будет предварительно обработано, а следующие значения будут установлены на 4 по умолчанию:

// 图像预处理
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);

Таким образом, настройка по умолчанию будет обрабатывать 4 байта и 4 байта на строку, а ширина каждой строки компонента Y равна 540, что кратно 4. Байты выровнены, поэтому изображение может отображаться нормально, а ширина компонентов U и V равна540 / 2 = 270, 270 не кратно 4, байты не выровнены, поэтому пигмент будет казаться зеленоватым. В настоящее время существует два способа решения этой проблемы:

  • Во-первых, разрешить webgl напрямую обрабатывать 1 байт на строку (что влияет на производительность):
// 图像预处理
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
  • Второй - сделать ширину полученного изображения кратной 8, чтобы можно было добиться выравнивания по байтам YUV, и не отображался зеленый экран, но делать так не рекомендуется.

Справочная статья

Кодирование изображения и видео и FFmpeg(2) - Введение и применение формата YUV - eustoma - Blog Park

YUV pixel formats

wiki.videolan.org/YUV/

Визуализация видео с использованием 8-битного формата YUV | Microsoft Docs

ЮВ видео формата IOS - Ищу программиста

Графический принцип работы WebGL&Three.js - cnwander - Blog Park