在《安卓使用SurfaceView繪制ffmpeg解碼的視頻數據》雖然我們成功地實現了視頻的渲染,
但是在YUV轉換成RGB的時候,我們調用了ffmpeg內部的轉換函數,這里面包含大量的計算轉換,
所以是很耗費CPU性能的。
今天我們來學習一下如何將YUV轉換RGB的功能轉換到GPU中去執行,減少CPU的計算工作量,達到性能優化的目的。
解決方案是使用OpenGL渲染,將YUV轉換RGB的功能交由著色器去處理。
Android中引入OpenGL
下面以OpenGL 2.0為例。
1、 引入庫文件
在CMakeLists.txt
引入GLESv2
和EGL
庫(在ndk中內置)。
直接target_link_libraries
加入即可:
target_link_libraries(
........
#引入opengl的相關庫
GLESv2
EGL
)
2、 引入相關的頭文件
需要引入的頭文件
#include <EGL/egl.h>
#include <GLES2/gl2.h>
至此,我們的安卓OpenGL環境就算引入成功了。
OpenGL渲染環境搭建
1、 EGLDisplay
OpenGL(移動端稱作的是EGL)要知道把目標內容繪制在哪里。這就是EGLDisplay所需的功能。
EGLDisplay是一個封裝系統物理屏幕的數據類型(可以理解為繪制目標的一個抽象),通常會調用eglGetDisplay方法返回EGLDisplay來作為OpenGL ES渲染的目標。然后通過eglInitialize初始化顯示設備。
代碼如下:
//1 EGL display創建和初始化
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
LOGE("eglGetDisplay failed!");
return;
}
if (EGL_TRUE != eglInitialize(display, 0, 0)) {
LOGE("eglInitialize failed!");
return;
}
2、 EGLConfig
EGL有了Display之后,它就可以將OpenGL ES的輸出和設備的屏幕橋接起來,但是需要指定一些配置項,這時候EGLConfig就閃亮登場啦。
//輸出配置
EGLConfig config;
EGLint configNum;
EGLint configSpec[] = {
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_NONE
};
if (EGL_TRUE != eglChooseConfig(display, configSpec, &config, 1, &configNum)) {
LOGE("eglChooseConfig failed!");
return;
}
3、 創建EGLSurface
有了顯示設備,那么如何將設備的屏幕與EGL鏈接起來呢?EGLSurface粉墨登場。
//獲取原始窗口
//surface是外部的SurfaceView傳遞進來的
ANativeWindow *nwin = ANativeWindow_fromSurface(env, surface);
//創建surface
EGLSurface winsurface = eglCreateWindowSurface(display, config, nwin, 0);
if (winsurface == EGL_NO_SURFACE) {
LOGE("eglCreateWindowSurface failed!");
return;
}
4、 EGLContext
OpenGL所創建的資源, 其實對程序員可見的僅僅是ID而已, 要操作其中內容就需要依賴于EGLContext上下文。
//context 創建關聯的上下文
const EGLint ctxAttr[] = {
EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE
};
EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctxAttr);
if (context == EGL_NO_CONTEXT) {
LOGE("eglCreateContext failed!");
return;
}
if (EGL_TRUE != eglMakeCurrent(display, winsurface, winsurface, context)) {
LOGE("eglMakeCurrent failed!");
return;
}
4、 著色器
所有的渲染都需要位置信息與色彩信息這兩個信息才能成功渲染處理。頂點著色器和片元著色器就完成了這兩個功能。
頂點著色器:
attribute vec4 aPosition; //頂點坐標,在外部獲取傳遞進來
attribute vec2 aTexCoord; //材質(紋理)頂點坐標
varying vec2 vTexCoord; //輸出的材質(紋理)坐標,給片元著色器使用
void main() {
//紋理坐標轉換,以左上角為原點的紋理坐標轉換成以左下角為原點的紋理坐標,
// 比如以左上角為原點的(0,0)對應以左下角為原點的紋理坐標的(0,1)
vTexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
gl_Position = aPosition;
}
片元著色器:
precision mediump float; //精度
varying vec2 vTexCoord; //頂點著色器傳遞的坐標,相同名字opengl會自動關聯
uniform sampler2D yTexture; //輸入的材質(不透明灰度,單像素)
uniform sampler2D uTexture;
uniform sampler2D vTexture;
void main() {
vec3 yuv;
vec3 rgb;
yuv.r = texture2D(yTexture, vTexCoord).r; // y分量
// 因為UV的默認值是127,所以我們這里要減去0.5(OpenGLES的Shader中會把內存中0~255的整數數值換算為0.0~1.0的浮點數值)
yuv.g = texture2D(uTexture, vTexCoord).r - 0.5; // u分量
yuv.b = texture2D(vTexture, vTexCoord).r - 0.5; // v分量
// yuv轉換成rgb,兩種方法,一種是RGB按照特定換算公式單獨轉換
// 另外一種是使用矩陣轉換
rgb = mat3(1.0, 1.0, 1.0,
0.0, -0.39465, 2.03211,
1.13983, -0.58060, 0.0) * yuv;
//輸出像素顏色
gl_FragColor = vec4(rgb, 1.0);
}
著色器編寫好之后如何使用呢?我們看一張圖:
根據圖片流程,我們很方便就創建編譯并且鏈接好著色器。
GLint InitShader(const char *code, GLint type) {
//創建shader
GLint sh = glCreateShader(type);
if (sh == 0) {
LOGE("glCreateShader %d failed!", type);
return 0;
}
//加載shader
glShaderSource(sh,
1, //shader數量
&code, //shader代碼
0); //代碼長度
//編譯shader
glCompileShader(sh);
//獲取編譯情況
GLint status;
glGetShaderiv(sh, GL_COMPILE_STATUS, &status);
if (status == 0) {
LOGE("glCompileShader failed!");
return 0;
}
LOGE("glCompileShader success!");
return sh;
}
........此處省略若干代碼
//頂點和片元shader初始化
//頂點shader初始化
GLint vsh = InitShader(vertexShader, GL_VERTEX_SHADER);
//片元yuv420 shader初始化
GLint fsh = InitShader(fragYUV420P, GL_FRAGMENT_SHADER);
/////////////////////////////////////////////////////////////
//創建渲染程序
GLint program = glCreateProgram();
if (program == 0) {
LOGE("glCreateProgram failed!");
return;
}
//渲染程序中加入著色器代碼
glAttachShader(program, vsh);
glAttachShader(program, fsh);
//鏈接程序
glLinkProgram(program);
GLint status = 0;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status != GL_TRUE) {
LOGE("glLinkProgram failed!");
return;
}
glUseProgram(program);
LOGE("glLinkProgram success!");
/////////////////////////////////////////////////////////////
//加入三維頂點數據 兩個三角形組成正方形
static float vers[] = {
1.0f, -1.0f, 0.0f,
-1.0f, -1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f,
};
GLuint apos = (GLuint) glGetAttribLocation(program, "aPosition");
glEnableVertexAttribArray(apos);
//傳遞頂點
glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 12, vers);
//加入材質坐標數據
static float txts[] = {
1.0f, 0.0f, //右下
0.0f, 0.0f,
1.0f, 1.0f,
0.0, 1.0
};
GLuint atex = (GLuint) glGetAttribLocation(program, "aTexCoord");
glEnableVertexAttribArray(atex);
glVertexAttribPointer(atex, 2, GL_FLOAT, GL_FALSE, 8, txts);
//材質紋理初始化
//設置紋理層
glUniform1i(glGetUniformLocation(program, "yTexture"), 0); //對于紋理第1層
glUniform1i(glGetUniformLocation(program, "uTexture"), 1); //對于紋理第2層
glUniform1i(glGetUniformLocation(program, "vTexture"), 2); //對于紋理第3層
//創建opengl紋理
GLuint texts[3] = {0};
//創建三個紋理
glGenTextures(3, texts);
//設置紋理屬性
glBindTexture(GL_TEXTURE_2D, texts[0]);
//縮小的過濾器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//設置紋理的格式和大小
glTexImage2D(GL_TEXTURE_2D,
0, //細節基本 0默認
GL_LUMINANCE,//gpu內部格式 亮度,灰度圖
width, height, //拉升到全屏
0, //邊框
GL_LUMINANCE,//數據的像素格式 亮度,灰度圖 要與上面一致
GL_UNSIGNED_BYTE, //像素的數據類型
NULL //紋理的數據
);
//設置紋理屬性
glBindTexture(GL_TEXTURE_2D, texts[1]);
//縮小的過濾器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//設置紋理的格式和大小
glTexImage2D(GL_TEXTURE_2D,
0, //細節基本 0默認
GL_LUMINANCE,//gpu內部格式 亮度,灰度圖
width / 2, height / 2, //拉升到全屏
0, //邊框
GL_LUMINANCE,//數據的像素格式 亮度,灰度圖 要與上面一致
GL_UNSIGNED_BYTE, //像素的數據類型
NULL //紋理的數據
);
//設置紋理屬性
glBindTexture(GL_TEXTURE_2D, texts[2]);
//縮小的過濾器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//設置紋理的格式和大小
glTexImage2D(GL_TEXTURE_2D,
0, //細節基本 0默認
GL_LUMINANCE,//gpu內部格式 亮度,灰度圖
width / 2, height / 2, //拉升到全屏
0, //邊框
GL_LUMINANCE,//數據的像素格式 亮度,灰度圖 要與上面一致
GL_UNSIGNED_BYTE, //像素的數據類型
NULL //紋理的數據
);
//////////////////////////////////////////////////////
////紋理的修改和顯示
unsigned char *buf[3] = {0};
buf[0] = new unsigned char[width * height];
buf[1] = new unsigned char[width * height / 4];
buf[2] = new unsigned char[width * height / 4];
5、 傳遞數據
到了這一步,我們的渲染環境算是搭建完畢了,下面就是通過CPU傳遞數據到GPU進行渲染。簡單地說就是獲取在著色器中定義的變量并且對其賦值,然后調用繪制的API即可。
主要代碼:
// 解碼得到YUV數據
// 數據Y
buf[0] = frame->data[0];
memcpy(buf[0],frame->data[0],width*height);
// 數據U
memcpy(buf[1],frame->data[1],width*height/4);
// 數據V
memcpy(buf[2],frame->data[2],width*height/4);
//激活第1層紋理,綁定到創建的opengl紋理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,texts[0]);
//替換紋理內容
glTexSubImage2D(GL_TEXTURE_2D,0,0,0,width,height,GL_LUMINANCE,GL_UNSIGNED_BYTE,buf[0]);
//激活第2層紋理,綁定到創建的opengl紋理
glActiveTexture(GL_TEXTURE0+1);
glBindTexture(GL_TEXTURE_2D,texts[1]);
//替換紋理內容
glTexSubImage2D(GL_TEXTURE_2D,0,0,0,width/2,height/2,GL_LUMINANCE,GL_UNSIGNED_BYTE,buf[1]);
//激活第2層紋理,綁定到創建的opengl紋理
glActiveTexture(GL_TEXTURE0+2);
glBindTexture(GL_TEXTURE_2D,texts[2]);
//替換紋理內容
glTexSubImage2D(GL_TEXTURE_2D,0,0,0,width/2,height/2,GL_LUMINANCE,GL_UNSIGNED_BYTE,buf[2]);
//三維繪制
glDrawArrays(GL_TRIANGLE_STRIP,0,4);
//窗口顯示
eglSwapBuffers(display,winsurface);
至此,我們的渲染過程就完成了,與前面的ffmpeg解碼為YUV數據聯系起來,實現一邊解碼一邊渲染數據。通過對比筆者很明顯覺察到使用OpenGL渲染的視頻畫面流暢很多。而且對比兩種渲染方式的CPU使用率也發現OpenGL的渲染方式確實使得CPU的使用率大大降低。
最后貼一下結合ffmpeg從解碼到渲染全過程的完整代碼:
#include <jni.h>
#include <string>
#include "FlyLog.h"
#include <android/log.h>
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <android/native_window_jni.h>
#include <EGL/egl.h>
#include <GLES2/gl2.h>
// 因為ffmpeg是純C代碼,要在cpp中使用則需要使用 extern "C"
extern "C" {
#include "libavutil/avutil.h"
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
}
//頂點著色器glsl的宏
// 第二個#號的意思是自動鏈接字符串,而不用增加引號,參考ijkplayer的寫法
#define GET_STR(x) #x
static const char *vertexShader = GET_STR(
attribute vec4 aPosition; //頂點坐標,在外部獲取傳遞進來
attribute vec2 aTexCoord; //材質(紋理)頂點坐標
varying vec2 vTexCoord; //輸出的材質(紋理)坐標,給片元著色器使用
void main() {
//紋理坐標轉換,以左上角為原點的紋理坐標轉換成以左下角為原點的紋理坐標,
// 比如以左上角為原點的(0,0)對應以左下角為原點的紋理坐標的(0,1)
vTexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
gl_Position = aPosition;
}
);
//片元著色器,軟解碼和部分x86硬解碼解碼得出來的格式是YUV420p
static const char *fragYUV420P = GET_STR(
precision mediump float; //精度
varying vec2 vTexCoord; //頂點著色器傳遞的坐標,相同名字opengl會自動關聯
uniform sampler2D yTexture; //輸入的材質(不透明灰度,單像素)
uniform sampler2D uTexture;
uniform sampler2D vTexture;
void main() {
vec3 yuv;
vec3 rgb;
yuv.r = texture2D(yTexture, vTexCoord).r; // y分量
// 因為UV的默認值是127,所以我們這里要減去0.5(OpenGLES的Shader中會把內存中0~255的整數數值換算為0.0~1.0的浮點數值)
yuv.g = texture2D(uTexture, vTexCoord).r - 0.5; // u分量
yuv.b = texture2D(vTexture, vTexCoord).r - 0.5; // v分量
// yuv轉換成rgb,兩種方法,一種是RGB按照特定換算公式單獨轉換
// 另外一種是使用矩陣轉換
rgb = mat3(1.0, 1.0, 1.0,
0.0, -0.39465, 2.03211,
1.13983, -0.58060, 0.0) * yuv;
//輸出像素顏色
gl_FragColor = vec4(rgb, 1.0);
}
);
GLint InitShader(const char *code, GLint type) {
//創建shader
GLint sh = glCreateShader(type);
if (sh == 0) {
LOGE("glCreateShader %d failed!", type);
return 0;
}
//加載shader
glShaderSource(sh,
1, //shader數量
&code, //shader代碼
0); //代碼長度
//編譯shader
glCompileShader(sh);
//獲取編譯情況
GLint status;
glGetShaderiv(sh, GL_COMPILE_STATUS, &status);
if (status == 0) {
LOGE("glCompileShader failed!");
return 0;
}
LOGE("glCompileShader success!");
return sh;
}
/**
* 將數據轉換成double類型的一個方法
* @param r
* @return
*/
static double r2d(AVRational r) {
return r.num == 0 || r.den == 0 ? 0 : (double) r.num / (double) r.den;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_flyer_ffmpeg_FlyPlayer_playVideoByOpenGL(JNIEnv *env, jobject thiz, jstring video_path,
jobject surface) {
const char *path = env->GetStringUTFChars(video_path, 0);
AVFormatContext *fmt_ctx;
// 初始化格式化上下文
fmt_ctx = avformat_alloc_context();
// 使用ffmpeg打開文件
int re = avformat_open_input(&fmt_ctx, path, nullptr, nullptr);
if (re != 0) {
LOGE("打開文件失敗:%s", av_err2str(re));
return;
}
//探測流索引
re = avformat_find_stream_info(fmt_ctx, nullptr);
if (re < 0) {
LOGE("索引探測失敗:%s", av_err2str(re));
return;
}
//尋找視頻流索引
int v_idx = av_find_best_stream(
fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
if (v_idx == -1) {
LOGE("獲取視頻流索引失敗");
return;
}
//解碼器參數
AVCodecParameters *c_par;
//解碼器上下文
AVCodecContext *cc_ctx;
//聲明一個解碼器
const AVCodec *codec;
c_par = fmt_ctx->streams[v_idx]->codecpar;
//通過id查找解碼器
codec = avcodec_find_decoder(c_par->codec_id);
if (!codec) {
LOGE("查找解碼器失敗");
return;
}
//用參數c_par實例化編解碼器上下文,,并打開編解碼器
cc_ctx = avcodec_alloc_context3(codec);
// 關聯解碼器上下文
re = avcodec_parameters_to_context(cc_ctx, c_par);
if (re < 0) {
LOGE("解碼器上下文關聯失敗:%s", av_err2str(re));
return;
}
//打開解碼器
re = avcodec_open2(cc_ctx, codec, nullptr);
if (re != 0) {
LOGE("打開解碼器失敗:%s", av_err2str(re));
return;
}
// 獲取視頻的寬高,也可以通過解碼器獲取
AVStream *as = fmt_ctx->streams[v_idx];
int width = as->codecpar->width;
int height = as->codecpar->height;
LOGE("width:%d", width);
LOGE("height:%d", height);
//數據包
AVPacket *pkt;
//數據幀
AVFrame *frame;
//初始化
pkt = av_packet_alloc();
frame = av_frame_alloc();
//1 獲取原始窗口
ANativeWindow *nwin = ANativeWindow_fromSurface(env, surface);
////////////////////
///EGL
//1 EGL display創建和初始化
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (display == EGL_NO_DISPLAY) {
LOGE("eglGetDisplay failed!");
return;
}
if (EGL_TRUE != eglInitialize(display, 0, 0)) {
LOGE("eglInitialize failed!");
return;
}
//2 surface
//2-1 surface窗口配置
//輸出配置
EGLConfig config;
EGLint configNum;
EGLint configSpec[] = {
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_NONE
};
if (EGL_TRUE != eglChooseConfig(display, configSpec, &config, 1, &configNum)) {
LOGE("eglChooseConfig failed!");
return;
}
//創建surface
EGLSurface winsurface = eglCreateWindowSurface(display, config, nwin, 0);
if (winsurface == EGL_NO_SURFACE) {
LOGE("eglCreateWindowSurface failed!");
return;
}
//3 context 創建關聯的上下文
const EGLint ctxAttr[] = {
EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE
};
EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, ctxAttr);
if (context == EGL_NO_CONTEXT) {
LOGE("eglCreateContext failed!");
return;
}
if (EGL_TRUE != eglMakeCurrent(display, winsurface, winsurface, context)) {
LOGE("eglMakeCurrent failed!");
return;
}
LOGE("EGL Init Success!");
//頂點和片元shader初始化
//頂點shader初始化
GLint vsh = InitShader(vertexShader, GL_VERTEX_SHADER);
//片元yuv420 shader初始化
GLint fsh = InitShader(fragYUV420P, GL_FRAGMENT_SHADER);
/////////////////////////////////////////////////////////////
//創建渲染程序
GLint program = glCreateProgram();
if (program == 0) {
LOGE("glCreateProgram failed!");
return;
}
//渲染程序中加入著色器代碼
glAttachShader(program, vsh);
glAttachShader(program, fsh);
//鏈接程序
glLinkProgram(program);
GLint status = 0;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status != GL_TRUE) {
LOGE("glLinkProgram failed!");
return;
}
glUseProgram(program);
LOGE("glLinkProgram success!");
/////////////////////////////////////////////////////////////
//加入三維頂點數據 兩個三角形組成正方形
static float vers[] = {
1.0f, -1.0f, 0.0f,
-1.0f, -1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f,
};
GLuint apos = (GLuint) glGetAttribLocation(program, "aPosition");
glEnableVertexAttribArray(apos);
//傳遞頂點
glVertexAttribPointer(apos, 3, GL_FLOAT, GL_FALSE, 12, vers);
//加入材質坐標數據
static float txts[] = {
1.0f, 0.0f, //右下
0.0f, 0.0f,
1.0f, 1.0f,
0.0, 1.0
};
GLuint atex = (GLuint) glGetAttribLocation(program, "aTexCoord");
glEnableVertexAttribArray(atex);
glVertexAttribPointer(atex, 2, GL_FLOAT, GL_FALSE, 8, txts);
//材質紋理初始化
//設置紋理層
glUniform1i(glGetUniformLocation(program, "yTexture"), 0); //對于紋理第1層
glUniform1i(glGetUniformLocation(program, "uTexture"), 1); //對于紋理第2層
glUniform1i(glGetUniformLocation(program, "vTexture"), 2); //對于紋理第3層
//創建opengl紋理
GLuint texts[3] = {0};
//創建三個紋理
glGenTextures(3, texts);
//設置紋理屬性
glBindTexture(GL_TEXTURE_2D, texts[0]);
//縮小的過濾器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//設置紋理的格式和大小
glTexImage2D(GL_TEXTURE_2D,
0, //細節基本 0默認
GL_LUMINANCE,//gpu內部格式 亮度,灰度圖
width, height, //拉升到全屏
0, //邊框
GL_LUMINANCE,//數據的像素格式 亮度,灰度圖 要與上面一致
GL_UNSIGNED_BYTE, //像素的數據類型
NULL //紋理的數據
);
//設置紋理屬性
glBindTexture(GL_TEXTURE_2D, texts[1]);
//縮小的過濾器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//設置紋理的格式和大小
glTexImage2D(GL_TEXTURE_2D,
0, //細節基本 0默認
GL_LUMINANCE,//gpu內部格式 亮度,灰度圖
width / 2, height / 2, //拉升到全屏
0, //邊框
GL_LUMINANCE,//數據的像素格式 亮度,灰度圖 要與上面一致
GL_UNSIGNED_BYTE, //像素的數據類型
NULL //紋理的數據
);
//設置紋理屬性
glBindTexture(GL_TEXTURE_2D, texts[2]);
//縮小的過濾器
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//設置紋理的格式和大小
glTexImage2D(GL_TEXTURE_2D,
0, //細節基本 0默認
GL_LUMINANCE,//gpu內部格式 亮度,灰度圖
width / 2, height / 2, //拉升到全屏
0, //邊框
GL_LUMINANCE,//數據的像素格式 亮度,灰度圖 要與上面一致
GL_UNSIGNED_BYTE, //像素的數據類型
NULL //紋理的數據
);
//////////////////////////////////////////////////////
////紋理的修改和顯示
unsigned char *buf[3] = {0};
buf[0] = new unsigned char[width * height];
buf[1] = new unsigned char[width * height / 4];
buf[2] = new unsigned char[width * height / 4];
while (av_read_frame(fmt_ctx, pkt) >= 0) {//持續讀幀
// 只解碼視頻流
if (pkt->stream_index == v_idx) {
//發送數據包到解碼器
avcodec_send_packet(cc_ctx, pkt);
//清理
av_packet_unref(pkt);
//這里為什么要使用一個for循環呢?
// 因為avcodec_send_packet和avcodec_receive_frame并不是一對一的關系的
//一個avcodec_send_packet可能會出發多個avcodec_receive_frame
for (;;) {
// 接受解碼的數據
re = avcodec_receive_frame(cc_ctx, frame);
if (re != 0) {
break;
} else {
// 解碼得到YUV數據
// 數據Y
buf[0] = frame->data[0];
memcpy(buf[0],frame->data[0],width*height);
// 數據U
memcpy(buf[1],frame->data[1],width*height/4);
// 數據V
memcpy(buf[2],frame->data[2],width*height/4);
//激活第1層紋理,綁定到創建的opengl紋理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D,texts[0]);
//替換紋理內容
glTexSubImage2D(GL_TEXTURE_2D,0,0,0,width,height,GL_LUMINANCE,GL_UNSIGNED_BYTE,buf[0]);
//激活第2層紋理,綁定到創建的opengl紋理
glActiveTexture(GL_TEXTURE0+1);
glBindTexture(GL_TEXTURE_2D,texts[1]);
//替換紋理內容
glTexSubImage2D(GL_TEXTURE_2D,0,0,0,width/2,height/2,GL_LUMINANCE,GL_UNSIGNED_BYTE,buf[1]);
//激活第2層紋理,綁定到創建的opengl紋理
glActiveTexture(GL_TEXTURE0+2);
glBindTexture(GL_TEXTURE_2D,texts[2]);
//替換紋理內容
glTexSubImage2D(GL_TEXTURE_2D,0,0,0,width/2,height/2,GL_LUMINANCE,GL_UNSIGNED_BYTE,buf[2]);
//三維繪制
glDrawArrays(GL_TRIANGLE_STRIP,0,4);
//窗口顯示
eglSwapBuffers(display,winsurface);
}
}
}
}
//關閉環境
avcodec_free_context(&cc_ctx);
// 釋放資源
av_frame_free(&frame);
av_packet_free(&pkt);
avformat_free_context(fmt_ctx);
LOGE("播放完畢");
env->ReleaseStringUTFChars(video_path, path);
}
遇到的問題
筆者測試了兩個不同的視頻發現一個能播放,另外一個花屏,看不出圖像。
查找了一些資料至今仍找不到問題所在,兩個視頻使用SurfaceView渲染都是可以的,說明可能不是解碼的問題,估計是渲染程序的問題。而且兩個視頻解碼出來的YUV數據格式不一樣,一個yuvj420p,這個可以正常使用OpenGL渲染,一個是yuv420p,這個就不能渲染,花屏。
懇親大神不吝賜教。
結束
最后如果你對音視頻開發感興趣可掃碼關注,筆者在各個知識點學習完畢之后也會使用ffmepg從零開始編寫一個多媒體播放器,包括本地播放及網絡流播放等等。歡迎關注,后續我們共同探討,共同進步。