目錄
- 基礎(chǔ)知識
- 使用GLSurfaceView播放邊解碼邊播放視頻
- 遇到的問題
- 資料
- 收獲
一怎栽、基礎(chǔ)知識
1.1. YUV和RGB
視頻是由一幅幅圖像或者說一幀幀 YUV 數(shù)據(jù)組成
表示圖片、視頻的色彩空間有幾種:YUV宿饱、RGB熏瞄、HSV等,F(xiàn)Fmpeg解碼后的視頻數(shù)據(jù)是YUV數(shù)據(jù)谬以,而OpenGL ES 渲染時(shí)要使用RGB數(shù)據(jù)强饮,為此我們需要把YUV先轉(zhuǎn)成RGB,對應(yīng)的轉(zhuǎn)換公式如下:
rgb = mat3(
1.0, 1.0, 1.0,
0.0, -0.39465, 2.03211,
1.13983, -0.5806, 0.0
) *yuv;
1.2 OpenGL ES基礎(chǔ)知識
我們在第二個(gè)系列中已經(jīng)對OpenGLES的基本流程和GLSL語法以及繪制各種圖形为黎、矩陣變換等進(jìn)行過學(xué)習(xí)實(shí)踐邮丰。不清楚的或者遺忘的可以回顧下行您。
OpenGL ES涉及的知識點(diǎn)和可以做的東西是非常豐富,后面還會對其有一系列更深入的學(xué)習(xí)實(shí)踐柠座。
音視頻開發(fā)之旅(七) OpenGL ES 基本概念
音視頻開發(fā)之旅(八)GLSL及Shader的渲染流程
音視頻開發(fā)之旅(九) OpenGL ES 繪制平面圖形
音視頻開發(fā)之旅(十) GLSurfaceView源碼解析&EGL環(huán)境
音視頻開發(fā)之旅(11) OpenGL ES矩陣變換與坐標(biāo)系統(tǒng)
二邑雅、使用GLSurfaceView播放解碼的YUV數(shù)據(jù)
在前面幾篇我們實(shí)現(xiàn)了對視頻流的解碼生成了YUV裸流,當(dāng)時(shí)是通過YUVplayer和ffplayer在pc上進(jìn)行的驗(yàn)證妈经。這一小節(jié)淮野,我們通過Android 提供的GLSurfaceview來進(jìn)行視頻的渲染。因?yàn)镚LsurfaceView已經(jīng)有了EGL渲染線程吹泡,本篇我們先通過使用熟悉渲染流程
首先我們寫下頂點(diǎn)著色器和片源著色器骤星。
頂點(diǎn)著色器
//#version 120
attribute vec4 aPosition;
attribute vec2 aTextureCoord;
varying vec2 vTextureCoord;
void main() {
gl_Position = aPosition;
vTextureCoord = aTextureCoord;
}
片源著色器
//#version 120
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D samplerY;
uniform sampler2D samplerU;
uniform sampler2D samplerV;
void main() {
vec3 yuv;
vec3 rgb;
yuv.r=texture2D(samplerY, vTextureCoord).g;
yuv.g=texture2D(samplerU, vTextureCoord).g -0.5;
yuv.b=texture2D(samplerV, vTextureCoord).g-0.5;
rgb = mat3(
1.0, 1.0, 1.0,
0.0, -0.39465, 2.03211,
1.13983, -0.5806, 0.0
) *yuv;
gl_FragColor = vec4(rgb,1.0);
}
Render代碼如下,也是比較常規(guī)的操作,又不清楚的爆哑,可以回看下OpenGL系列內(nèi)容
package android.spport.mylibrary2;
import android.content.res.Resources;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.util.Log;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class MyRender implements GLSurfaceView.Renderer {
private Resources resources;
private int program;
private float verCoords[] = {
// 1.0f, -1.0f,
// -1.0f, -1.0f,
// 1.0f, 1.0f,
// -1.0f, 1.0f
-1f, -1f,
1f, -1f,
-1f, 1f,
1f, 1f
};
private float textureCoords[] = {
// 1.0f, 0.0f,
// 0.0f, 0.0f,
// 1.0f, 1.0f,
// 0.0f, 1.0f
0f,1f,
1f, 1f,
0f, 0f,
1f, 0f
};
private final int BYTES_PER_FLOAT = 4;
private int aPositionLocation;
private int aTextureCoordLocation;
private int samplerYLocation;
private int samplerULocation;
private int samplerVLocation;
private FloatBuffer verCoorFB;
private FloatBuffer textureCoorFB;
private int[] textureIds;
public MyRender(Resources resources) {
this.resources = resources;
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
String vertexShader = ShaderHelper.loadAsset(resources, "vertex_shader.glsl");
String fragShader = ShaderHelper.loadAsset(resources, "frag_shader.glsl");
program = ShaderHelper.loadProgram(vertexShader, fragShader);
aPositionLocation = GLES20.glGetAttribLocation(program, "aPosition");
aTextureCoordLocation = GLES20.glGetAttribLocation(program, "aTextureCoord");
samplerYLocation = GLES20.glGetUniformLocation(program, "samplerY");
samplerULocation = GLES20.glGetUniformLocation(program, "samplerU");
samplerVLocation = GLES20.glGetUniformLocation(program, "samplerV");
verCoorFB = ByteBuffer.allocateDirect(verCoords.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(verCoords);
verCoorFB.position(0);
textureCoorFB = ByteBuffer.allocateDirect(textureCoords.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(textureCoords);
textureCoorFB.position(0);
//對應(yīng)Y U V 三個(gè)紋理
textureIds = new int[3];
GLES20.glGenTextures(3, textureIds, 0);
for (int i = 0; i < 3; i++) {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIds[i]);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
}
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
Log.i("MyRender", "onDrawFrame: width="+width+" height="+height);
if (width > 0 && height > 0 && y != null && u != null && v != null) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glUseProgram(program);
GLES20.glEnableVertexAttribArray(aPositionLocation);
GLES20.glVertexAttribPointer(aPositionLocation, 2, GLES20.GL_FLOAT, false, 2 * BYTES_PER_FLOAT, verCoorFB);
GLES20.glEnableVertexAttribArray(aTextureCoordLocation);
GLES20.glVertexAttribPointer(aTextureCoordLocation, 2, GLES20.GL_FLOAT, false, 2 * BYTES_PER_FLOAT, textureCoorFB);
//激活紋理
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIds[0]);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,
0,
GLES20.GL_LUMINANCE,
width,
height,
0,
GLES20.GL_LUMINANCE,
GLES20.GL_UNSIGNED_BYTE,
y
);
GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIds[1]);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,
0,
GLES20.GL_LUMINANCE,
width / 2,
height / 2,
0,
GLES20.GL_LUMINANCE,
GLES20.GL_UNSIGNED_BYTE,
u
);
GLES20.glActiveTexture(GLES20.GL_TEXTURE2);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIds[2]);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,
0,
GLES20.GL_LUMINANCE,
width/2,
height/2,
0,
GLES20.GL_LUMINANCE,
GLES20.GL_UNSIGNED_BYTE,
v
);
GLES20.glUniform1i(samplerYLocation, 0);
GLES20.glUniform1i(samplerULocation, 1);
GLES20.glUniform1i(samplerVLocation, 2);
y.clear();
y = null;
u.clear();
u = null;
v.clear();
v = null;
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLES20.glDisableVertexAttribArray(aPositionLocation);
GLES20.glDisableVertexAttribArray(aTextureCoordLocation);
}
}
private int width;
private int height;
private ByteBuffer y;
private ByteBuffer u;
private ByteBuffer v;
public void setYUVRenderData(int width, int height, byte[] y, byte[] u, byte[] v) {
this.width = width;
this.height = height;
this.y = ByteBuffer.wrap(y);
this.u = ByteBuffer.wrap(u);
this.v = ByteBuffer.wrap(v);
}
}
視頻解碼后通過JNI洞难,CPP調(diào)用Java的回調(diào)函數(shù)把YUV數(shù)據(jù)給到j(luò)ava層的借助GlSurfaceView進(jìn)行渲染。
extern "C" {
#include "include/libavcodec/avcodec.h"
#include "include/libavformat/avformat.h"
#include "include/log.h"
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <libswresample/swresample.h>
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
#include <libavutil/time.h>
}
jmethodID onCallYuvData;
jobject jcallJavaobj;
JavaVM* javaVM;
extern "C"
JNIEXPORT void JNICALL
Java_android_spport_mylibrary2_Demo_initYUVNativeMethod(JNIEnv *env, jobject thiz) {
// jcallJavaobj = thiz;
jcallJavaobj = env->NewGlobalRef(thiz);
env->GetJavaVM(&javaVM);
onCallYuvData = env->GetMethodID(env->GetObjectClass(thiz), "onCallYUVData",
"(II[B[B[B)V");
}
extern "C"
JNIEXPORT jint JNICALL
Java_android_spport_mylibrary2_Demo_decodeVideo(JNIEnv *env, jobject thiz, jstring inputPath,
jstring outPath) {
...
//把數(shù)據(jù)回調(diào)給java層揭朝,通過OpenGL進(jìn)行渲染(當(dāng)然也可以在native層構(gòu)建OpenGL環(huán)境進(jìn)行實(shí)現(xiàn)队贱,這里借助了GLSurfaceView)
if(onCallYuvData!=NULL)
{
jbyteArray yData = env->NewByteArray(y_size);
jbyteArray uData = env->NewByteArray(y_size/4);
jbyteArray vData = env->NewByteArray(y_size/4);
env->SetByteArrayRegion(yData, 0, y_size,
reinterpret_cast<const jbyte *>(pFrameYUV->data[0]));
env->SetByteArrayRegion(uData, 0, y_size/4, reinterpret_cast<const jbyte *>(pFrameYUV->data[1]));
env->SetByteArrayRegion(vData, 0, y_size/4, reinterpret_cast<const jbyte *>(pFrameYUV->data[2]));
// env->SetByteArrayRegion(vData, 0, y_size/4, reinterpret_cast<const jbyte *>(pFrameYUV->data[1]));
// env->SetByteArrayRegion(uData, 0, y_size/4, reinterpret_cast<const jbyte *>(pFrameYUV->data[2]));
LOGI("native onCallYuvData widith=%d",pCodecParameters->width);
//jcallJavaobj 在賦值時(shí)候要通過env->NewGlobalRef(thiz);設(shè)置偉全局變量,否則會出現(xiàn)野導(dǎo)致指針異常
env->CallVoidMethod(jcallJavaobj,onCallYuvData,pCodecParameters->width,pCodecParameters->height,yData,uData,vData);
env->DeleteLocalRef(yData);
env->DeleteLocalRef(uData);
env->DeleteLocalRef(vData);
//解碼太快潭袱,來不及渲染柱嫌,導(dǎo)致前面待渲染但還沒有渲染的數(shù)據(jù)被后解碼的數(shù)據(jù)給覆蓋了。
//由于渲染和解碼線程現(xiàn)在還沒有做分離同步以及加入解碼buffer屯换,所以此處采用延遲的方案處理解決编丘。
av_usleep(1000 * 50);
}
...
}
實(shí)現(xiàn)視頻的播放。
我們可以通過JNI回調(diào)彤悔,把解碼后的yuv傳給java層進(jìn)行渲染嘉抓,這本是是一個(gè)消耗,是否能夠通過CPP層直接完成渲染吶晕窑?當(dāng)然可以抑片,音頻OpenGL ES提供了Java和native的支持,我們完全可以在native層進(jìn)行渲染杨赤,只不過nativew層沒有類似GLSuerfaceView即封裝好的EGL環(huán)境蓝丙,這樣就需要我們自己創(chuàng)建GL渲染線程進(jìn)行渲染。我們后續(xù)來進(jìn)行學(xué)習(xí)實(shí)踐望拖,在native層通過解碼線程和渲染線程 使用OpenSL ES渲染播放音頻、OpenGL ES渲染視頻挫鸽。
代碼已上傳至github [https://github.com/ayyb1988/ffmpegvideodecodedemo] 歡迎交流说敏,一起學(xué)習(xí)成長。
四丢郊、遇到的問題
- 運(yùn)行時(shí)出現(xiàn) JNI DETECTED ERROR IN APPLICATION異常
5.963 5247-5247/? A/DEBUG: Abort message: 'JNI DETECTED ERROR IN APPLICATION: use of invalid jobject 0x7fcea23564
from int android.spport.mylibrary2.Demo.decodeVideo(java.lang.String, java.lang.String)'
2021-03-10 06:57:35.963 5247-5247/? A/DEBUG: x0 0000000000000000 x1 0000000000000dd6 x2 0000000000000006 x3 0000007fcea22390
2021-03-10 06:57:35.963 5247-5247/? A/DEBUG: x4 fefeff7939517f97 x5 fefeff7939517f97 x6 fefeff7939517f97 x7 7f7f7f7f7f7fffff
2021-03-10 06:57:35.963 5247-5247/? A/DEBUG: x8 00000000000000f0 x9 2cd4cdcb09dc01f0 x10 0000000000000001 x11 0000000000000000
2021-03-10 06:57:35.963 5247-5247/? A/DEBUG: x12 fffffff0fffffbdf x13 ffffffffffffffff x14 0000000000000004 x15 ffffffffffffffff
2021-03-10 06:57:35.963 5247-5247/? A/DEBUG: x16 0000007a3a7618c0 x17 0000007a3a73d900 x18 0000007a3c048000 x19 0000000000000dd6
2021-03-10 06:57:35.963 5247-5247/? A/DEBUG: x20 0000000000000dd6 x21 00000000ffffffff x22 000000799cca7cc0 x23 00000079b5130625
2021-03-10 06:57:35.963 5247-5247/? A/DEBUG: x24 00000079b51520fd x25 0000000000000001 x26 00000079b4fbc258 x27 0000007a3b8067c0
2021-03-10 06:57:35.963 5247-5247/? A/DEBUG: x28 00000079b565b338 x29 0000007fcea22430
2021-03-10 06:57:35.963 5247-5247/? A/DEBUG: sp 0000007fcea22370 lr 0000007a3a6ef0c4 pc 0000007a3a6ef0f0
2021-03-10 06:57:36.026 2647-2647/? E/ndroid.systemu: Invalid ID 0x00000000.
2021-03-10 06:57:36.047 12827-20222/? E/Hack.Hub: net.connect = I'm afraid to call its toString()
2021-03-10 06:57:36.071 5247-5247/? A/DEBUG: backtrace:
2021-03-10 06:57:36.071 5247-5247/? A/DEBUG: #00 pc 00000000000830f0 /apex/com.android.runtime/lib64/bionic/libc.so (abort+160) (BuildId: e55e6e4c631509598633769798683023)
...
2021-03-10 06:57:36.072 5247-5247/? A/DEBUG: #08 pc 000000000036771c /apex/com.android.runtime/lib64/libart.so (art::(anonymous namespace)::ScopedCheck::Check(art::ScopedObjectAccess&, bool, char const*, art::(anonymous namespace)::JniValueType*)+652) (BuildId: d700c52998d7d76cb39e2001d670e654)
2021-03-10 06:57:36.072 5247-5247/? A/DEBUG: #09 pc 000000000036c76c /apex/com.android.runtime/lib64/libart.so (art::(anonymous namespace)::CheckJNI::CheckCallArgs(art::ScopedObjectAccess&, art::(anonymous namespace)::ScopedCheck&, _JNIEnv*, _jobject*, _jclass*, _jmethodID*, art::InvokeType, art::(anonymous namespace)::VarArgs const*)+132) (BuildId: d700c52998d7d76cb39e2001d670e654)
原因
將jobject保存在了一個(gè)全局變量里面盔沫,而沒有使用全局引用医咨,以上面的代碼為例,即本地JNI代碼里進(jìn)行了類似object = caller;的賦值架诞,這顯然是沒有用的拟淮,一旦函數(shù)返回,caller就會被GC回收銷毀谴忧,object指向的就是一個(gè)非法地址很泊,最終導(dǎo)致上面的JNI錯(cuò)誤。
解決方案:
jcallJavaobj = thiz;
-->改為
jcallJavaobj = env->NewGlobalRef(thiz);
2. 設(shè)置RENDERMODE_WHEN_DIRTY模式黑屏
通過查看log 數(shù)據(jù)到來后調(diào)用了requestRender沾谓,但沒有觸發(fā)onDrawFrame委造。
時(shí)序問題,GlSurfaceview被inflater之后其EGL環(huán)境的準(zhǔn)備沒有那么早均驶,通過post延遲解碼渲染
glSurfaceView.postDelayed(new Runnable() {
@Override
public void run() {
demo.initYUVNativeMethod();
demo.decodeVideo(folderurl+"/input.mp4", externalFilesDir+"/output7.yuv");
}
},300);
3. 渲染出來的視頻是顛倒的
private float verCoords[] = {
1.0f, -1.0f,//RB
-1.0f, -1.0f,//LB
1.0f, 1.0f,//RT
-1.0f, 1.0f//LT
};
private float textureCoords[] = {
1.0f, 0.0f,//RB
0.0f, 0.0f,//LB
1.0f, 1.0f,//RT
0.0f, 1.0f//LT
};
--》改為
private float verCoords[] = {
-1f, -1f,//LB
1f, -1f,//RB
-1f, 1f,//LT
1f, 1f//RT
};
private float textureCoords[] = {
0f,1f, //LT
1f, 1f,//RT
0f, 0f,//LB
1f, 0f //RB
};
原因:OpenGL 中紋理坐標(biāo)系和頂點(diǎn)坐標(biāo)系的y軸方向都是向上的昏兆,android手機(jī)坐標(biāo)系的y軸是向下的。所以openGL->手機(jī)顯示妇穴,需要把坐標(biāo)做上下旋轉(zhuǎn)
4. 渲染出來的視頻跳幀了
通過log查看分析爬虱,發(fā)現(xiàn)是解碼太快,來不及渲染腾它,導(dǎo)致前面待渲染但還沒有渲染的數(shù)據(jù)被后解碼的數(shù)據(jù)給覆蓋了跑筝。
由于渲染和解碼線程現(xiàn)在還沒有做分離同步以及加入解碼buffer,所以此處采用延遲的方案處理解決携狭。
在Packet解碼渲染時(shí)加上50ms的延遲
av_usleep(1000 * 50);
5. 出現(xiàn)部分區(qū)域有綠屏并且播放的某些時(shí)刻會出現(xiàn)部分區(qū)域花屏的情況
在pc上通過ffplay播放解碼后的yuv數(shù)據(jù)是正常的继蜡,而在手機(jī)上渲染出來的有問題,那邊肯定是渲染出了問題逛腿,查看render代碼發(fā)現(xiàn)稀并,YUV紋理中的V紋理的寬度和高度設(shè)置不對導(dǎo)致
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,
0,
GLES20.GL_LUMINANCE,
width,
height,
0,
GLES20.GL_LUMINANCE,
GLES20.GL_UNSIGNED_BYTE,
v
);
--> 修改為
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,
0,
GLES20.GL_LUMINANCE,
width/2,
height/2,
0,
GLES20.GL_LUMINANCE,
GLES20.GL_UNSIGNED_BYTE,
v
);
四、資料
- 音視頻學(xué)習(xí) (八) 掌握視頻基礎(chǔ)知識并使用 OpenGL ES 2.0 渲染 YUV 數(shù)據(jù)
- YUV <——> RGB 轉(zhuǎn)換算法
- Android平臺上基于OpenGl渲染yuv視頻
- Android萬能視頻播放器04-OpenGL ES渲染YUV紋理
- JNI DETECTED ERROR IN APPLICATION解決記錄
五单默、收獲
- 回顧YUV和RGB基礎(chǔ)知識
- 通過GLSurfaceView實(shí)現(xiàn)編解碼變渲染視頻數(shù)據(jù)
- 解決遇到的解碼和渲染不同步導(dǎo)致跳幀碘举、渲染時(shí)出現(xiàn)綠屏 花屏、渲染畫面時(shí)顛倒的等問題
感謝你的閱讀
篇外話:
原計(jì)劃時(shí)接下來幾篇是Native層渲染搁廓、音視頻同步引颈、編碼、倍速播放境蜕、rtmp推拉流等蝙场。但最近變得有些浮躁了是因?yàn)椋枰獙W(xué)習(xí)的太多了粱年,不止音視頻還有Android進(jìn)階的各種知識售滤,有個(gè)想分散精力的想法,兼顧兩者,但是精力有限完箩,有時(shí)候必須要專注到像激光一樣才能成事赐俗。
考慮到工作上最近遇到的新領(lǐng)域,業(yè)余時(shí)間和工作上的不能夠相互幫助弊知,導(dǎo)致這種心理阻逮,其實(shí)是在逃避。遇到困難秩彤,面對它叔扼,解決它。
最近工作中使用OpenGL的比較多呐舔,很多內(nèi)容也在學(xué)習(xí)實(shí)踐币励,為了工作和學(xué)習(xí)相結(jié)合達(dá)到事半功倍的效果,決定先暫停FFmpeg系列的更文珊拼,接下來我們聚焦在OpenGL ES渲染上食呻。
調(diào)整下優(yōu)先級和順序。FFmpeg我們后會有期澎现。
下一篇我們來學(xué)習(xí)實(shí)踐FBO仅胞,歡迎關(guān)注公眾號“音視頻開發(fā)之旅”,一起學(xué)習(xí)成長剑辫。
歡迎交流