天天看點

QQ視訊通話、抖音的視訊回顯 是如何實作的

QQ視訊通話、抖音的視訊回顯 是如何實作的

先說為什麼會有這一篇文章:

2014年聯想曾經做過一款 短視訊軟體,叫“魔力秀”。可以說和現在的抖音基本是一樣的,但因為“魔力秀App”出生于聯想,注定無法在一個硬體公司成長為一棵參天大樹,最終隻發了一個版本就結束了。

當時“魔力秀App”的視訊回顯子產品是我設計實作的,是以就有了這篇文章。

事過多年,将這篇文章拿出來整理,因為這項技術依然不過時,反而被廣泛應用...

這篇文章之前叫做 Opengl ES中YUV420轉RGB 是一個技術标題。整理時,發現用這個标題,大家實際是不知道這個技術有什麼用,是以換了這個比較醒目的名字。

Opengl ES中YUV420轉RGB 這項技術主要是實作視訊高效、節省帶寬的回顯視訊圖像。

  • 為什麼說高效?

    因為直接用 OpenGL ES 實作,本身繞開了Androi的層層封裝;

    而且Opengl 本身就是圖形學接口,實作效率天然高效。

  • 為什麼說節省帶寬?

    因為網絡傳輸中,采用的YUV420資料格式,本身是一種有損的資料格式。但由于格式的特性,色彩還原後基本對圖像顯示效果沒有影響,是以在視訊通話場景中廣泛使用。

這裡通過以下幾個方面具體說明Opengl ES中YUV420轉RGB 這項技術的實作方式:

  • 先了解一個概念“灰階圖”
  • YUV資料格式
  • YUV444和YUV420
  • YUV420轉RGB
  • OpenGL ES中YUV420P轉RGB

一、先了解一個概念“灰階圖”

這裡先了解一下灰階 Y 的概念。不知道大家是否看過老式的黑白電視機?

老式黑白電視機的圖像就隻有Y一個通道,老式黑白電視機上的圖像就是灰階圖成像(隻用接收一個Y通道資料就能播放出電視畫面,前輩們果然厲害... ;而後來的彩色電視用的是YUV資料信号,這樣既相容了老的黑白電視,又可以在新式彩色電視上顯示彩色圖像,前輩們太厲害了...)

  • 灰階圖的定義:
  • 灰階值與RGB的計算公式
  • 将“彩色圖轉”轉化為“灰階圖”shader實作

1.1、灰階圖的定義:

把白色與黑色之間按對數關系分為若幹等級,稱為灰階。灰階分為256階。

1.2、灰階值Y與RGB的計算公式:

Y = 0.299R + 0.587G + 0.114*B
           

電視台發出信号時,将RGB資料這樣轉化為Y 資料。老式黑白電視機接收到Y信号,就能展示圖象了。

1.4、将“彩色圖轉”轉化為“灰階圖”shader實作

這裡說一個技術實作,在OpenGL ES中,如何用shader片元着色器,把一個彩色紋理圖轉化為一個灰階圖?

效果如下:

轉化當然要用到我們上邊說道的RGB 轉 Y的公式,下邊我們看具體的片源着色器 shader 代碼實作:

// shader 片元着色器
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D sTexture;

void main() {
		// 從紋理圖sTexture 讀取目前片元的RGB顔色
         vec4 color=texture2D(sTexture, vTextureCoord);
         // 公式計算灰階值
         float col=color.r*0.299+color.g*0.587+color.b*0.114;
         // 将生成的Y 灰階值設定給RGB通道
         color.r=col;
         color.g=col;
         color.b=col;
         // 傳給片源着色器
         gl_FragColor =color;
}
           

在shader實作中,我特意加了注釋。

了解glsl文法的同學,可以仔細讀一下上邊的代碼實作;

當然不了解文法的同學,更要簡單讀一遍(glsl是一種類C語言,隻要學過C語言應該就能讀懂)

二、YUV資料格式

上邊我們了解了灰階圖的實作,這裡我們介紹一個YUV資料格式。

主要分為以下幾個部分:

  • YUV定義
  • 使用YUV的好處
  • YUV與RGB轉換公式

2.1、YUV

YUV的具體定義如下:

Y:就是灰階值;
UV:用來指定像素的顔色。
           

對于UV現在有些懵沒關系,我們繼續往下看

2.2、YUV與RGB轉換公式

// RGB轉YUV
Y= 0.299*R + 0.587*G + 0.114*B
U= -0.147*R - 0.289*G + 0.436*B = 0.492*(B- Y)
V= 0.615*R - 0.515*G - 0.100*B = 0.877*(R- Y)
//############################################
// YUV轉RGB
R = Y + 1.140*V
G = Y - 0.394*U - 0.581*V
B = Y + 2.032*U
           

2.3、使用YUV的好處:

  • 傳輸信号向後相容老式黑白電視機(用于優化彩色視訊信号的傳輸,使其向後相容老式黑白電視)
  • YUV420占用的帶寬少(這個我們在前邊提過,至于具體為什麼,後邊會有詳細介紹)

YUV420與RGB視訊信号傳輸相比,它最大的優點在于隻需占用極少的頻寬(後面來介紹)

2.4、YUV444和YUV420

前邊我們一直說的YUV資料,其實是YUV420資料格式。YUV420資料格式在傳輸上UV色彩是有損傳輸,而YUV444 其實是一種無損的資料格式。

那為什麼這裡我們還是要說一下YUV444格式呢?

其實是為了後邊實作 将YUV420資料還原成RGB做準備

首先介紹YUV444 資料格式:

  • YUV444:

    一個像素點對應一個Y一個U一個V(YUV一一對應)

    YUV444資料格式 如下圖所示:

YUV444 中YUV通道一一對應,了解簡單。下邊這個是YUV420資料格式,UV資料有損。

  • YUV420:

    一個像素點對應一個Y;

    四個像素點對應一個U一個V;

具體資料格式如下:

從上圖可以看到,UV色彩通道是有損失的,這也是為什麼YUV420在展示時,占用的帶寬更少一下。

a、Y、U、V沒有一一對應,圖像有顔色損失

b、這也就是為什麼占用的帶寬少了;

c、同樣網絡傳輸中,占用的流量也同樣減少了;

d、但對圖像的色彩展示幾乎沒有影響

因為占用的流量較少,對色彩展示幾乎沒有影響,是以廣泛應用于各中視訊通話場景,視訊回顯場景等。

三、YUV420轉RGB

哇去,基礎知識終于說完了,這裡說到我們的核心技術點:YUV420轉RGB

  • 第一個步驟YUV420轉YUV444;
  • 第二個步驟YUV444轉RGB。

為什麼要把YUV420轉為YUV444?

因為在傳輸時,YUV420中的UV通道資料損失了。但我們渲染時,需要把這損失掉的UV色彩資料通道還原回來,再進行YUV444 轉 RGB

先說 YUV420 轉 YUV444

3.1、YUV420轉YUV444

要把YUV420轉為YUV444就得把“上圖 YUV420” U與V中 “?” 的部分填滿。

通過YUV420資料中,已有的U 與 Y資料,通過內插補點計算的方式,填補上空缺的部分。以下是內插補點運算的具體實作公式,內插補點計算如下(建議參照YUV420資料格式圖來看,要不容易懵):

U01 = (U00 + U02)/2; // 利用已有的 U00、U02來計算U01
U10 = (U00 + U20)/2; // 利用已有的 U00、U20來計算U10
U11 = (U00 + U02 + U20 + U22)/4;// 利用已有的 U00、U02、U20、U22來計算U11

//######################
V01 = (V00 + V02)/2; // 利用已有的 V00、V02來計算V01
V10 = (V00 + V20)/2; // 利用已有的 V00、V20來計算V10
V11 = (V00 + V02 + V20 + V22)/4; // 利用已有的 V00、V02、V20、V22來計算V11
           

經過以上公式,YUV420轉YUV444 完成(資料補全成功),下邊來說YUV444如何轉RGB。

3.2、YUV444轉RGB

YUV444轉RGB是有現成公式的,我們直接拿來用就行了,YUV轉RGB的公式:

R = Y + 1.140*V
G = Y - 0.394*U - 0.581*V
B = Y + 2.032*U
           

公式有了,那具體的代碼實作是怎麼實作的呢?

注:

一、二、三、四,這四點介紹的是YUV轉RGB的基本原理,下邊是具體實作。

四、OpenGL ES中YUV420P轉RGB

這一節介紹具體技術實作,但開始時,還是要介紹兩種資料格式(哎、我知道你們都煩了,我其實也煩,但還是得說)

  • YUV420p的資料格式
  • YUV420sp的資料格式(YUV420sp轉RGB這裡不做介紹)
  • YUV420sp 轉RGB

4.1、YUV420p的資料格式

YUV420p的資料格式如下圖所示(為一個byte[]):

其中資料的4/6為Y;1/6為U;1/6為V。

4.2、YUV420sp的資料格式(YUV420sp轉RGB這裡不做介紹)

YUV420sp的資料格式如下圖所示(為一個byte[]):

4.3、YUV420sp 轉RGB

其實大概原理就是:

  • 将YUV420資料中的Y U V 資料分别取出來,分别生成三張紋理圖
  • 利用片元着色器每個片元執行一次的特性,将YUV420資料轉為YUV444資料
  • 從YUV444資料中,取出一一對應的YUV資料
  • 最後,利用公式YUV444 轉 RGB
  • 完事大吉

以下為YUV三張紋理圖效果圖:

YUV420轉YUV444

這裡如何補全YUV420資料中UV部分的顔色資料?

這裡有一個讨巧的方式:

在OpenGL ES生成紋理時,采用線性紋理采樣方式。線性采樣出U、V紋理中“?”部分的顔色值。這樣就就可以拿到一一對應的YUV444資料。

對應的Java代碼如下:

/**
	 * 
	 * @param w
	 * @param h
	 * @param date
	 *            資料
	 * @param textureY
	 * @param textureU
	 * @param textureV
	 * @param isUpdate
	 *            是否為更新
	 */
	public static boolean bindYUV420pTexture(int frameWidth, int frameHeight,
			byte frameData[], int textureY, int textureU, int textureV,
			boolean isUpdate) {

		if (frameData == null || frameData.length == 0) {
			return false;
		}
		Log.d(TAG, "----bindYUV420pTexture-----");

		if (isUpdate == false) {

			/**
			 * 資料緩沖區
			 */
			// Y
			ByteBuffer buffer = LeBuffer.byteToBuffer(frameData);
			// GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
			GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureY);

			GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
					GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);// GL_LINEAR_MIPMAP_NEAREST
			GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
					GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);

			GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
					GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
			GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
					GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);

			/**
			 * target 指定目标紋理,這個值必須是GL_TEXTURE_2D; level
			 * 執行細節級别,0是最基本的圖像級别,n表示第N級貼圖細化級别; internalformat
			 * 指定紋理中的顔色元件,可選的值有GL_ALPHA,GL_RGB,GL_RGBA,GL_LUMINANCE,
			 * GL_LUMINANCE_ALPHA 等幾種; width 指定紋理圖像的寬度; height 指定紋理圖像的高度; border
			 * 指定邊框的寬度; format 像素資料的顔色格式,可選的值參考internalformat; type
			 * 指定像素資料的資料類型,可以使用的值有GL_UNSIGNED_BYTE
			 * ,GL_UNSIGNED_SHORT_5_6_5,GL_UNSIGNED_SHORT_4_4_4_4
			 * ,GL_UNSIGNED_SHORT_5_5_5_1; pixels 指定記憶體中指向圖像資料的指針;
			 * 
			 */
			GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,
					frameWidth, frameHeight, 0, GLES20.GL_LUMINANCE,
					GLES20.GL_UNSIGNED_BYTE, buffer);

			/**
			 * 
			 */
			// U
			buffer.clear();
			buffer = LeBuffer.byteToBuffer(frameData);
			buffer.position(frameWidth * frameHeight);
			//
			// GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
			GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureU);

			GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
					GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);// GL_LINEAR_MIPMAP_NEAREST
			GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
					GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);

			GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
					GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
			GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
					GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);

			GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,
					frameWidth / 2, frameHeight / 2, 0, GLES20.GL_LUMINANCE,
					GLES20.GL_UNSIGNED_BYTE, buffer);

			/**
			 * 
			 */
			// V
			buffer.clear();
			buffer = LeBuffer.byteToBuffer(frameData);
			buffer.position(frameWidth * frameHeight * 5 / 4);
			//
			// GLES20.glActiveTexture(GLES20.GL_TEXTURE2);
			GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureV);

			GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
					GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);// GL_LINEAR_MIPMAP_NEAREST
			GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
					GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);

			GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
					GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
			GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
					GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);

			GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_LUMINANCE,
					frameWidth / 2, frameHeight / 2, 0, GLES20.GL_LUMINANCE,
					GLES20.GL_UNSIGNED_BYTE, buffer);

		} else {
			/**
			 * Y
			 */
			ByteBuffer buffer = LeBuffer.byteToBuffer(frameData);
			GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureY);
			GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, frameWidth,
					frameHeight, GLES20.GL_LUMINANCE, GLES20.GL_UNSIGNED_BYTE,
					buffer);

			/**
			 * U
			 */
			//
			buffer.clear();
			buffer = LeBuffer.byteToBuffer(frameData);
			buffer.position(frameWidth * frameHeight);
			//
			GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureU);
			GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0,
					frameWidth / 2, frameHeight / 2, GLES20.GL_LUMINANCE,
					GLES20.GL_UNSIGNED_BYTE, buffer);
			/**
			 * V
			 */
			//
			buffer.clear();
			buffer = LeBuffer.byteToBuffer(frameData);
			buffer.position(frameWidth * frameHeight * 5 / 4);
			//
			GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureV);
			GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0,
					frameWidth / 2, frameHeight / 2, GLES20.GL_LUMINANCE,
					GLES20.GL_UNSIGNED_BYTE, buffer);
		}
		return true;
	}
           

代碼說明:

已上代碼便是将傳入的幀資料byte frameData[],轉為三張紋理圖的代碼。

代碼的16行、5051行、7576行分别為從byte frameData[]中分别取出Y、U、V資料的代碼。

代碼5659行、代碼8184行分别為設定U、V紋理的采樣方式為線性采樣的代碼。

以上代碼運作結束,記憶體中會生成三張紋理圖像。

将三張紋理圖像傳入“片元着色器”執行下一步驟。

YUV444轉RGB

YUV一一對應的紋理有了,這裡該介紹如何實作YUV444轉RGB了:

按照YUV轉RGB的公式,将Y、U、V一一對應的取出,進行YUV轉RGB操作,生成像素點。

對應片元着色器 shader 代碼實作:

recision mediump float;
// 片元着色器中 輸入了Y U V三張紋理
uniform sampler2D sTexture_y;
uniform sampler2D sTexture_u;
uniform sampler2D sTexture_v;

varying vec2 vTextureCoord;

//YUV 轉 RGB的 shader 實作
void getRgbByYuv(in float y, in float u, in float v, inout float r, inout float g, inout float b){	
	//
    y = 1.164*(y - 0.0625);
    u = u - 0.5;
    v = v - 0.5;
    //
    r = y + 1.596023559570*v;
    g = y - 0.3917694091796875*u - 0.8129730224609375*v;
    b = y + 2.017227172851563*u;
}

void main() {
	//
 	float r,g,b;
 	
 	// 從YUV三張紋理中,采樣出一一對應的YUV資料
 	float y = texture2D(sTexture_y, vTextureCoord).r;
    float u = texture2D(sTexture_u, vTextureCoord).r;
    float v = texture2D(sTexture_v, vTextureCoord).r;
	// YUV 轉 RGB
	getRgbByYuv(y, u, v, r, g, b);
	
	// 最終顔色指派
	gl_FragColor = vec4(r,g,b, 1.0); 
}
           

五、完事大吉

源碼真的是懶得整理,是以,大家還是了解了實作原理,自己動手去敲吧,不要找我要代碼了!!!

========== THE END ==========