2019-12-25 08:06:31 SImple_a 阅读数 10

代码可以参考: Github地址
本文主要介绍如何通过FFmpeg将MP4格式的视频数据解码为一帧一帧的RGBA像素格式数据来播放。
因为主要是视频的解码及播放,对于音频只是解码出了音频对应的pcm数据,并没有播放pcm。因此也不会涉及到音视频的同步。

主要流程是
解封装—>解码—>像素格式转换—>显示

Java层的主要配置

首先建一个支持cpp的项目

1 app module build.grdle配置

externalNativeBuild {
            cmake {
            	//cpp编译器flag
                cppFlags "-std=c++11"
            }

            ndk{
                //指定所支持的cpu架构
                abiFilters "armeabi-v7a"
            }
        }

        sourceSets{
            main{
                //指定ffmpeg路径
                jniLibs.srcDirs=['libs']
            }
        }

指定cmake路径

android {
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

2 cmake文件配置

#1 声明cmake版本
cmake_minimum_required(VERSION 3.4.1)


#2 添加头文件路径(相对于本文件路径)
include_directories(include)

#3 设置ffmpeg库所在路径的变量
set(FF ${CMAKE_CURRENT_SOURCE_DIR}/libs/${ANDROID_ABI})

# 4添加ffmpeg相关库

# 4.1解码
add_library(avcodec SHARED IMPORTED)
set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION ${FF}/libavcodec.so)

# 4.2格式转换
add_library(avformat SHARED IMPORTED)
set_target_properties(avformat PROPERTIES IMPORTED_LOCATION ${FF}/libavformat.so)

# 4.3基础库
add_library(avutil SHARED IMPORTED)
set_target_properties(avutil PROPERTIES IMPORTED_LOCATION ${FF}/libavutil.so)

# 4.4格式转换
add_library(swscale SHARED IMPORTED)
set_target_properties(swscale PROPERTIES IMPORTED_LOCATION ${FF}/libswscale.so)

#4.5 音频重采样
add_library(swresample SHARED IMPORTED)
set_target_properties(swresample PROPERTIES IMPORTED_LOCATION ${FF}/libswresample.so)

#5 指定本地cpp文件 和 打包对应的so库
add_library(native-lib
            SHARED
            src/main/cpp/native-lib.cpp
            )

find_library(log-lib
              log )
# 7 链接库
target_link_libraries(native-lib
                      avcodec avformat avutil swscale swresample 
                      android
                       ${log-lib} )

3 创建GLSurfaceView的子类

这一步骤主要是在开启的子线程中,将GLSurfaceView的Surface传递到底层来渲染数据

//1 创建GlSurfaceView的子类
public class PlayView  extends GLSurfaceView implements Runnable, SurfaceHolder.Callback, GLSurfaceView.Renderer {

    public PlayView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

   public void start(){
    	//2 手动创建线程
        new Thread(this).start();
    }
    
    @Override
    public void run() {
        String videoPath = Environment.getExternalStorageDirectory() + "/video.mp4";
        //3 在子线程中 将视频url和Surface对象传递到native层
        open(videoPath, getHolder().getSurface());
    }


    @Override
    public void surfaceCreated(SurfaceHolder holder) {
    	//4 android8.0必须调用此方法,否则无法显示
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
            setRenderer(this);
        }
    }
    public native void open(String url, Object surface);

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    }

    @Override
    public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {

    }

    @Override
    public void onSurfaceChanged(GL10 gl10, int i, int i1) {

    }

    @Override
    public void onDrawFrame(GL10 gl10) {

    }
}

Native层的相关代码

1 初始化

首先注册解封装器,打开文件。然后就可以拿到封装数据里面的音频和视频的索引。

 //1 初始化解封装
    av_register_all();

    AVFormatContext *ic = NULL;

    //2 打开文件
    int re = avformat_open_input(&ic, path, 0, 0);

    if (re != 0) {
        LOGEW("avformat_open_input %s success!", path);
    } else {
        LOGEW("avformat_open_input failed!: %s", av_err2str(re));
    }

    //3 获取流信息
    re = avformat_find_stream_info(ic, 0);
    if (re != 0) {
        LOGEW("avformat_find_stream_info failed!");
    }
    LOGEW("duration = %lld nb_streams = %d", ic->duration, ic->nb_streams);

    int fps = 0;
    int videoStream = 0;
    int audioStream = 1;

    //4 获取视频音频流位置
    for (int i = 0; i < ic->nb_streams; i++) {
        AVStream *as = ic->streams[i];
        if (as->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            LOGEW("视频数据");
            videoStream = i;
            fps = r2d(as->avg_frame_rate);

            LOGEW("fps = %d, width = %d height = %d codeid = %d pixformat = %d", fps,
                  as->codecpar->width,
                  as->codecpar->height,
                  as->codecpar->codec_id,
                  as->codecpar->format);

        } else if (as->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            LOGEW("音频数据");
            audioStream = i;
            LOGEW("sample_rate = %d channels = %d sample_format = %d",
                  as->codecpar->sample_rate,
                  as->codecpar->channels,
                  as->codecpar->format);
        }
    }

对于多路流可以通过遍历的方式拿到音频和视频流的索引地址,也可以直接指定流数据类型来获取索引地址

//5 获取音频流信息 和上面遍历取出视音频的流信息是一样的,这种方式更直接
audioStream = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);

2 视解码器

在这一步骤中,首先定义并初始化解码器,然后视频流索引的地址传给解码器。最后打开解码器。

	//1 软解码器
    AVCodec *vcodec = avcodec_find_decoder(ic->streams[videoStream]->codecpar->codec_id);

    if (!vcodec) {
        LOGEW("avcodec_find failed");
    }

    //2 解码器初始化
    AVCodecContext *vc = avcodec_alloc_context3(vcodec);

    //3 解码器参数赋值
    avcodec_parameters_to_context(vc, ic->streams[videoStream]->codecpar);

   // 定义解码的线程
    vc->thread_count = 8;

    //4 打开解码器
    re = avcodec_open2(vc, 0, 0);
    LOGEW("vc timebase = %d/ %d", vc->time_base.num, vc->time_base.den);

    if (re != 0) {
        LOGEW("avcodec_open2 video failed!");
    }

需要注意的是,这里可以自定义解码的线程,来控制解码速度。可以自定义大小,后续代码会有介绍调整这个值以后,来测试解码速度大小。

另外,这部分只是通过软解码的方式,软解比较消耗CPU,但是兼容性好
硬解不消耗CPU,更省电,但是硬解可能会有兼容性的问题

下面是获取硬解码器

AVCodec *vcodec = avcodec_find_decoder_by_name("h264_mediacodec");

在使用硬解码器,还要额外定义此方法。用于确保在获取硬解码器之前调用av_jni_set_java_vm()函数,通过调用av_jni_set_java_vm()才可以获取到硬解码器。

extern "C"
JNIEXPORT
jint JNI_OnLoad(JavaVM *vm,void *res)
{
    av_jni_set_java_vm(vm,0);
    return JNI_VERSION_1_4;
}

3 音频解码器

和之前的视频解码器的步骤相同,只是部分参数不同

	//1 软解码器
	AVCodec *acodec = avcodec_find_decoder(ic->streams[audioStream]->codecpar->codec_id);

    if (!acodec) {
        LOGEW("avcodec_find failed!");
    }

    //2 解码器初始化
    AVCodecContext *ac = avcodec_alloc_context3(acodec);
    avcodec_parameters_to_context(ac, ic->streams[audioStream]->codecpar);
    ac->thread_count = 1;

    //3 打开解码器
    re = avcodec_open2(ac, 0, 0);
    if (re != 0) {
        LOGEW("avcodec_open2 audio failed!");
    }    

4 开始解码

以下是解码过程的完整代码

 	//1 定义Packet和Frame
    AVPacket *pkt = av_packet_alloc();
    AVFrame *frame = av_frame_alloc();

    //用于测试性能
    long long start = GetNowMs();
    int frameCount = 0;

    //2 像素格式转换的上下文
    SwsContext *vctx = NULL;

    int outwWidth = 1280;
    int outHeight = 720;
    char *rgb = new char[1920*1080*4];
    char *pcm = new char[48000*4*2];

    //3 音频重采样上下文初始化
    SwrContext *actx = swr_alloc();
    actx = swr_alloc_set_opts(actx,
                              av_get_default_channel_layout(2),
                              AV_SAMPLE_FMT_S16,
                              ac->sample_rate,
                              av_get_default_channel_layout(ac->channels),
                              ac->sample_fmt,ac->sample_rate,0,0);

    re = swr_init(actx);
    if(re != 0)
    {
        LOGEW("swr_init failed!");
    }else
    {
        LOGEW("swr_init success!");
    }

    //4 显示窗口初始化
    ANativeWindow *nwin = ANativeWindow_fromSurface(env,surface);
    ANativeWindow_setBuffersGeometry(nwin,outwWidth,outHeight,WINDOW_FORMAT_RGBA_8888);
    ANativeWindow_Buffer wbuf;

    for (;;) {

        //这里是测试每秒解码的帧数  每三秒解码多少帧
        if(GetNowMs() - start >= 3000)
        {
            LOGEW("now decode fps is %d", frameCount/3);
            start = GetNowMs();
            frameCount = 0;
        }

        int re = av_read_frame(ic, pkt);
        if (re != 0) {
            LOGEW("读取到结尾处!");
            int pos = 20 * r2d(ic->streams[videoStream]->time_base);
            av_seek_frame(ic, videoStream, pos, AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME);
            break;
        }

        AVCodecContext *cc = vc;
        if (pkt->stream_index == audioStream) {
            cc = ac;
        }

        //1 发送到线程中解码
        re = avcodec_send_packet(cc, pkt);

        //清理
        int p = pkt->pts;
        av_packet_unref(pkt);

        if (re != 0) {
            LOGEW("avcodec_send_packet failed!");
            continue;
        }

        //每一帧可能对应多个帧数据,所以要遍历取
        for (;;) {
            //2 解帧数据
            re = avcodec_receive_frame(cc, frame);
            if (re != 0) {
                break;
            }
            //LOGEW("avcodec_receive_frame %lld", frame->pts);

            //如果是视频帧
            if(cc == vc){
                frameCount++;

                //3 初始化像素格式转换的上下文
                vctx = sws_getCachedContext(vctx,
                                            frame->width,
                                            frame->height,
                                            (AVPixelFormat)frame->format,
                                            outwWidth,
                                            outHeight,
                                            AV_PIX_FMT_RGBA,
                                            SWS_FAST_BILINEAR,
                                            0,0,0);

                if(!vctx){
                    LOGEW("sws_getCachedContext failed!");
                }else
                {
                    uint8_t  *data[AV_NUM_DATA_POINTERS] = {0};
                    data[0] = (uint8_t *)rgb;
                    int lines[AV_NUM_DATA_POINTERS] = {0};
                    lines[0] = outwWidth * 4;
                    int h = sws_scale(vctx,
                                      (const uint8_t **)frame->data,
                                      frame->linesize,
                                      0,
                                      frame->height,
                                      data,
                                      lines);
                    LOGEW("sws_scale = %d",h);

                    if(h > 0)
                    {
                        ANativeWindow_lock(nwin,&wbuf,0);
                        uint8_t  *dst = (uint8_t*)wbuf.bits;
                        memcpy(dst, rgb, outwWidth*outHeight*4);
                        ANativeWindow_unlockAndPost(nwin);
                    }
                }

            }else //音频帧
            {
                uint8_t  *out[2] = {0};
                out[0] = (uint8_t*)pcm;

                //音频重采样
                int len = swr_convert(actx,out,frame->nb_samples,(const uint8_t**)frame->data,frame->nb_samples);
                LOGEW("swr_convert = %d", len);
            }
        }
    }

    delete rgb;
    delete pcm;

1 这段代码中,外层循环通过av_read_frame方法来给packet赋值,因为一个packet可能对应多个frame,所以packet每次通过avcodec_send_packet()方法发送到解码线程后,需要多次调用avcodec_receive_frame()来获取frame。
2 测试性能部分通过每三秒解码的帧数,除以3来计算平均每秒解码的帧数。
下面是获取当前时间的方法

long long GetNowMs()
{
    struct timeval tv;
    gettimeofday(&tv,NULL);
    int sec = tv.tv_sec%360000;
    long long t = sec*1000+tv.tv_usec/1000;
    return t;
}

3 对于获取播放时间戳pts
在ffmpeg中用的是分数的时间基AVRational来表示时间的基本单位。
AVRational有两个变量分子和分母

typedef struct AVRational{
    int num; ///< Numerator
    int den; ///< Denominator
} AVRational;

例如时间基为1/1000。
通过转换可以把此分数转换为浮点数

static double r2d(AVRational rational)
{
    return rational.num == 0 || rational.den == 0 ? 0.:(double)rational.num/ (double)rational.den;
}

这样就能算出每一帧对应的时间戳pts

 pkt->pts = pkt->pts * (1000*r2d(ic->streams[pkt->stream_index]->time_base));

获取解码时间戳dts同理

5 硬解码和多线层解码性能测试

1 单线程解码平局速度

now decode fps is 18
now decode fps is 18
now decode fps is 19

2 六线程解码均速

now decode fps is 105

3 硬解码均速

now decode fps is 95
now decode fps is 92
2015-05-04 11:56:26 liujunjie612 阅读数 3220

第一次写blog,呵呵,有点激动,主要是方便自己以后复习,写的不好请见谅。。。


这次遇到的问题纠结我好久,关键是对Android不熟悉,下面我来说一下我的问题:

用Unity制作Android上的悬浮窗口,窗口背景透明,用Unity4.2很简单,只要把摄像机的Clear Flags属性设置成Solid Color,然后BackGround属性的RGBA均设置为零,

然后再在Android里设置一下背景透明就可以了。

Android的设置:

private LinearLayout view;
private GLSurfaceView mUnityView;

public FloatWindowSmallView(Context context) {
		super(context);
		mContext = context;
		windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
		LayoutInflater.from(context).inflate(R.layout.float_window_small, this);
		view = (LinearLayout) findViewById(R.id.small_window_layout);
		viewWidth = view.getLayoutParams().width;
		viewHeight = view.getLayoutParams().height;
		
		LayoutParams lp = new LayoutParams (LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
		
//主要是这里
		mUnityView = new GLSurfaceView(Common.application);
        mUnityView.setEGLContextClientVersion(2);
        mUnityView.setZOrderOnTop(true);
        mUnityView.setZOrderMediaOverlay(true);
        mUnityView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
        mUnityView.setRenderer(Common.mUnityPlayer);
        mUnityView.getHolder().setFormat(PixelFormat.TRANSLUCENT);
		
		view.addView(mUnityView,0,lp);
		
		Common.mUnityPlayer.requestFocus();
		Common.mUnityPlayer.resume();
		
		//TextView percentView = (TextView) findViewById(R.id.percent);
		//percentView.setText(MyWindowManager.getUsedPercentValue(context));
		
		mLongPressRunnable = new Runnable() {  
            
            @Override  
            public void run() {               
                //performLongClick();  
            	openBigWindow();
            }  
        }; 
        
	}
但是Unity4.6这样设置就不行,背景是一片黑色:

所以换了个思路,将Unity视图输出成一个纹理,见纹理编译成bity[]传给Android,Android再解析成Imageview显示出来。

1、将摄像机视图输出成纹理:新建一个Render Texture ,将其拖给摄像机的Target Texture 

2、将Render Texture 转换成byte[]

 this.jc = new AndroidJavaClass("com.gesture.util.ToUnity");

放在Update每桢更新
		#if UNITY_ANDROID && !UNITY_EDITOR
		byte[] bts = UnityView();
		this.jc.CallStatic("picture", bts);
		#endif

	public byte[] UnityView()
	{
		Texture2D myTexture2D = new Texture2D(pic.width,pic.height);
		RenderTexture.active = pic;
		myTexture2D.ReadPixels(new Rect(0, 0, pic.width, pic.height), 0, 0);
		myTexture2D.Apply();
		RenderTexture.active = null;
		byte[] bytes = myTexture2D.EncodeToPNG();
		Destroy (myTexture2D);

		return bytes;
	}

3、Android的设置

XML配置文档:

<?xml version="1.0" encoding="UTF-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/small_window_layout"
	android:layout_width="120dp"
	android:layout_height="120dp" >  
	
    <ImageView
	android:id="@+id/image_view"
	android:layout_width="119dp"
	android:layout_height="119dp"/> 
       
</LinearLayout>


<pre name="code" class="java">private LinearLayout view;	
private static ImageView image;

public FloatWindowSmallView(Context context) {
	super(context);
	windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
	LayoutInflater.from(context).inflate(R.layout.float_window_small, this);
	view = (LinearLayout) findViewById(R.id.small_window_layout);
	viewWidth = view.getLayoutParams().width;
	viewHeight = view.getLayoutParams().height;

	image = (ImageView) findViewById(R.id.image_view);
		
	view.addView(Common.mUnityPlayer.getView());
		
	Common.mUnityPlayer.requestFocus();
	Common.mUnityPlayer.resume();
}





//把传输过来的字节流转换成Bitmap
	public static Bitmap getBitmaoByStream(byte[] imageByte)
	{
		if (imageByte != null)
		{
			ByteArrayInputStream byteImage = new ByteArrayInputStream (imageByte);
			return BitmapFactory.decodeStream(byteImage);
		}
		else
		{
			return null;
		}
	}
	
	//更新Imageview
	public static void ShowTexture(byte[] b)
	{
	     Bitmap bitmap = getBitmaoByStream(b);
	     image.setImageBitmap(bitmap);
	}



4、Unity端调用Android端方法

  //显示unity中Render Texture的内容, 供unity调用
    public static void picture(final byte[] b)
	{
    	new Handler(Looper.getMainLooper()).post(new Runnable()
		{
			
			@Override
			public void run()
			{
				FloatWindowSmallView.ShowTexture(b);		
			}
		});
	}
Unity端每桢调用此方法,来刷新ImageView来显示Unity视图,达成效果!!!



但是目前还有一点小问题待解决,就是ImageView不能完全覆盖LinearLayout,否则Unity不运行,还有就是运行效率问题待提高,目前就这样,后续再更新。。。


2017-06-26 19:43:03 lidec 阅读数 4869

背景

Android 开发中,当得到一张yuv图需要显示时,之前的做法是利用ffmpeg自带的方法将其转换为RGB565或者RGBA,然后将RGB数据拷贝到aNativeWindow的图像缓冲区,达到显示的目的。这样做比较耗CPU, 最近在阅读ijkplayer源码时,整理了一下OpenGL直接渲染YUV420P相关流程,参考网上一些代码,总结了一个最简单的小例子。

例子下载地址
http://download.csdn.net/detail/lidec/9880093

流程

这里首先提出一个问题,对比之前将RGB数据拷贝到显示缓冲区,OpenGL最终也应该有一个存放最终RGB数据的缓冲区,那么我们如何拿到这个缓冲区中的内容并且将其拷贝到显示缓冲区呢?

这里需要用到EGL的相关接口。EGL是OpenGL ES和平台系统视窗之间的接口,我们可以通过它的Api建立OpenGL ES和显示窗口见的关系。OpenGL渲染管线中存储了我们设定的参数,调用渲染方法时会根据这些参数将内容渲染到指定的窗口,Android系统在java层提供了GLSurfaceView类,可以直接在其onDrawFrame方法中设置OpenGL相关命令并对其进行绘制。这样屏蔽了很多细节操作,但是也会是我们产生一些困惑,不知道RGB内存是如何被拷贝到显示缓冲区中的。同时也难以和直接将YUV转RGB并拷贝的那种流程所兼容。

下面总结一下相关流程。
1.初始化EGL相关变量,包括display surface 和 contex
2.初始化OpenGL相关
3.设置 OpenGL绘制相关
4.调用eglSwapBuffers 将后台的显示缓存显示到屏幕上

上述方法都在native层完成,java层只需要传入要绘制的surface对象即可。

初始化EGL

这里首先介绍一下EGL中三个比较重要的三个数据结构。

EGLContext

Opengl ES的状态的上下文,用于存储opengl状态,执行Opengl ES指令都会作用这个Context上,所以必须初始化完毕这个变量后才可以调用Opengl相关指令。

EGLDisplay

用来作为本地显示视窗的引用,是EGL接口抽象出来的一个与平台无关的结构。

EGLDisplay eglGetDisplay (NativeDisplayType display);

用来初始化EGLDisplay,display为显示id,一般取默认值。

EGLSurface

可以看作是帧缓存的引用,缓存gl绘制的画面,最终将这里的信息swap到EGLDisplay中完成显示。

eglCreateWindowSurface

这个方法会建立Android本地窗口与EGLSurface之间的关系,具体使用见下文代码。

EGL初始化代码实现

这里首先从java层接收一个 Surface对象,将surface对象转换为Android的native window,之后将这个native window与EGL关联,初始化EGL相关。eglMakeCurrent成功之后,就可以执行gl相关指令了。

//指定EGLDisplay属性 这里是使用rgb888显示
static const EGLint configAttribs[] = {
        EGL_RENDERABLE_TYPE,    EGL_OPENGL_ES2_BIT,
        EGL_SURFACE_TYPE,       EGL_WINDOW_BIT,
        EGL_BLUE_SIZE,          8,
        EGL_GREEN_SIZE,         8,
        EGL_RED_SIZE,           8,
        EGL_NONE
};

//指定EGLContext属性,这里使用opengles2
static const EGLint contextAttribs[] = {
        EGL_CONTEXT_CLIENT_VERSION, 2,      //指定context为opengles2
        EGL_NONE
};

JNIEXPORT
void
Java_com_cm_opengles_CmOpenGLES_setSurface(JNIEnv *env, jobject obj, jobject jsurface, jbyteArray yuvDatas, jint size){

    //在native层获取surface的引用
    window = ANativeWindow_fromSurface(env, jsurface);

    EGLint numConfigs;
    EGLConfig config;

    EGLint format;
    EGLint width;
    EGLint height;

    //egl存储opengl管线状态 必须先初始化context,之后再创建和操作gl相关
    EGLContext context;
    //egl对本地显示窗口的抽象
    EGLDisplay display;
    //egl对显示buffer的抽象
    EGLSurface surface;

    //获取一个EGLDisplay对象
    if((display = eglGetDisplay(EGL_DEFAULT_DISPLAY)) == EGL_NO_DISPLAY){
        LOGI_EU("eglGetDisplay() returned error %d", eglGetError());
        return ;
    }
    //初始化EGLDisplay display, 后面两个参数是指定支持的版本
    if(!eglInitialize(display, 0, 0)){
        LOGI_EU("eglInitialize() returned error %d", eglGetError());
        return ;
    }
    //为display指定显示buffer的格式,具体内容在configAttribs中
    if(!eglChooseConfig(display, configAttribs, &config, 1, &numConfigs)){
        LOGI_EU("eglChooseConfig() returned error %d", eglGetError());
        return ;
    }

    //从config获取显示格式
    if(!eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format)){
        LOGI_EU("eglGetConfigAttrib() returned error %d", eglGetError());
        return;
    }
    //使用上面的格式,根据Android窗口,对显示进行拉伸
    uint32_t window_width  = ANativeWindow_getWidth(window);
    uint32_t window_height = ANativeWindow_getWidth(window);
    int ret = ANativeWindow_setBuffersGeometry(window, window_width, window_height, format);
    if(ret){
        LOGI_EU("ANativeWindow_setBuffersGeometry(format) returned error %d", ret);
        return;
    }

    //用上面构造的display获取一个surface,这个surface可以认为是当前的帧缓存
    if(!(surface = eglCreateWindowSurface(display, config, window, NULL))){
        LOGI_EU("eglCreateWindowSurface() returned error %d", eglGetError());
        return;
    }

    //创建一个EGLContext,用来保存gl状态机中相关信息
    if(!(context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs))){
        LOGI_EU("eglCreateContext() returned error %d", eglGetError());
        return;
    }

    //将display,surface与context绑定,后面就可以进行opengl相关操作
    if(EGL_FALSE == eglMakeCurrent(display, surface, surface, context)){
        LOGI_EU("eglMakeCurrent() returned error %d", eglGetError());
        return;
    }else{
        LOGI_EU("EGL_INIT_OK");
    }

    if(!eglQuerySurface(display, surface, EGL_WIDTH, &width) ||
            !eglQuerySurface(display, surface, EGL_HEIGHT, &height)){
        LOGI_EU("eglQuerySurface() returned error %d", eglGetError());
        return;
    }

    eglInstance.surface = surface;
    eglInstance.display = display;
    eglInstance.context = context;

    glClearColor(0.0f, 0.0f, 0.5f, 1.0f);
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);
    glDisable(GL_DEPTH_TEST);
}

OpenGL ES相关处理

YUV与RGB的转换

YUV格式的图像没办法直接显示在显示屏上,所以必须将其转换为RGB格式,如果在CPU中直接操作,会有大量的计算,这里可以将转换过程放到OpenGL的渲染管线中,让GPU在渲染之前先完成转换操作,CPU的工作就是设定好shader,传入YUV数据,之后就可以撒手干其他事情了,RGBbuffer计算完毕后,调用egl的sawp操作,图像就会显示到画布了。
YUV与RGB的转换公式见下图
这里写图片描述
下面就是如何在shader中实现这个算法了。片元着色器会对每个像素点进行着色计算,所以这个操作可以在片元着色器中进行。与普通纹理贴图不同的是,这里要同时使用Y,U,V三个分量的数据,也就是需要绑定3个纹理贴图,分别存入Y,U,V三个分量的buffer。

shader实现

顶点着色器

const char * codeVertexShader = GET_STR(
    attribute vec3 aPosition;
    uniform mat4 uMVPMatrix;
    attribute vec2 aTexCoor;
    varying vec2 vTexCoor;
    void main()
    {
        gl_Position = uMVPMatrix * vec4(aPosition, 1);
        vTexCoor = aTexCoor;
    }
);

片元着色器

const char * codeFragShader = GET_STR(
        precision mediump float;
        uniform sampler2D yTexture;
        uniform sampler2D uTexture;
        uniform sampler2D vTexture;
        varying vec2 vTexCoor;
        void main()
        {
            float y = texture2D(yTexture, vTexCoor).r;
            float u = texture2D(uTexture, vTexCoor).r - 0.5;
            float v = texture2D(vTexture, vTexCoor).r - 0.5;
            vec3 yuv = vec3(y, u, v);
            vec3 rgb;
            rgb = mat3( 1,       1,         1,
                        0,       -0.39465,  2.03211,
                        1.13983, -0.58060,  0) * yuv;
            gl_FragColor = vec4(rgb, 1);
        }
);

片元着色器中拿到三个贴图,从而分别获取该点处Y,U,V三个分量的数据。这里texture2D(vTexture, vTexCoor).r和.g和.b的效果是一样的,不清楚为什么。

最终将生成的RGB数据补上最后一个通道的数据传递给 gl_FragColor。

这里有个小技巧,在编写shader时经常将作为字符串,如果直接加引号拼接会显得非常混乱,这里参考了ijk的方法,定义一个宏,将宏里的代码都认为是字符串,写法如下

#define GET_STR(x) #x

这样GET_STR里的内容就会被直接当作字符串了。

OpenGL ES初始化

这里主要分以下几个步骤:
1.首先需要编译和连接shader,生成program
2.获取顶点坐标,纹理坐标,采样器的索引,用于后面绘制时给这些量传值
3.生成3个纹理的索引,绘制时用这些索引作为纹理id,进行纹理的绑定

代码实现

JNIEXPORT
void
Java_com_cm_opengles_CmOpenGLES_init(JNIEnv *env, jobject obj, jint pWidth, jint pHeight)
{
    LOGI_EU("init()");
    //创建一个引用
    instance = (Instance *)malloc(sizeof(Instance));
    memset(instance, 0, sizeof(Instance));

    GLuint shaders[2] = {0};
    //创建顶点shader和片元shader
    shaders[0] = initShader(codeVertexShader, GL_VERTEX_SHADER);
    shaders[1] = initShader(codeFragShader, GL_FRAGMENT_SHADER);
    //编译链接shader
    instance->pProgram = initProgram(shaders, 2);

    //获取mvp矩阵的索引
    instance->maMVPMatrixHandle = glGetUniformLocation( instance->pProgram, "uMVPMatrix");
    //获取顶点坐标索引
    instance->maPositionHandle = glGetAttribLocation(instance->pProgram, "aPosition");
    //获取纹理坐标索引
    instance->maTexCoorHandle = glGetAttribLocation(instance->pProgram, "aTexCoor");
    //获取采样器索引
    instance->myTextureHandle = glGetUniformLocation(instance->pProgram, "yTexture");
    instance->muTextureHandle = glGetUniformLocation(instance->pProgram, "uTexture");
    instance->mvTextureHandle = glGetUniformLocation(instance->pProgram, "vTexture");

    //获取对象名称 这里分别返回1个用于纹理对象的名称,后面为对应纹理赋值时将以这个名称作为索引
    glGenTextures(1, &instance->yTexture);
    glGenTextures(1, &instance->uTexture);
    glGenTextures(1, &instance->vTexture);

    LOGI_EU("init() yT = %d, uT = %d, vT = %d.", instance->yTexture, instance->uTexture, instance->vTexture);
    LOGI_EU("%s %d error = %d", __FILE__,__LINE__, glGetError());

    //为yuv数据分配存储空间
    instance->yBufferSize = sizeof(char) * pWidth * pHeight;
    instance->uBufferSize = sizeof(char) * pWidth / 2 * pHeight / 2;
    instance->vBufferSize = sizeof(char) * pWidth / 2 * pHeight / 2;
    instance->yBuffer = (char *)malloc(instance->yBufferSize);
    instance->uBuffer = (char *)malloc(instance->uBufferSize);
    instance->vBuffer = (char *)malloc(instance->vBufferSize);
    memset(instance->yBuffer, 0, instance->yBufferSize);
    memset(instance->uBuffer, 0, instance->uBufferSize);
    memset(instance->vBuffer, 0, instance->vBufferSize);
    //指定图像大小
    instance->pHeight = pHeight;
    instance->pWidth = pWidth;
    LOGI_EU("width = %d, height = %d", instance->pWidth, instance->pHeight);

    glClearColor(0.5f, 0.5f, 0.5f, 1.0f);

//  glEnable(GL_DEPTH_TEST);
    LOGI_EU("%s %d error = %d", __FILE__,__LINE__, glGetError());
}

OpenGL ES绘制

主要步骤如下
1.use第一步生成的program
2.如果需要旋转缩放等操作,给gl传入mvp矩阵
3.传入顶点坐标,传入纹理坐标
4.绑定纹理。这里首先要激活上一步生成的纹理id,之后进行绑定,设置参数,最后将存放纹理的buffer传入。
5.将片元shader中定义的三个纹理设置为3个层
6.使能顶点坐标和纹理坐标
7.绘制上面所设置的内容

代码实现

void
drawFrame(void* ins)
{
    if(DEBUG)
    {
        LOGI_EU("%s", __FUNCTION__);
    }

    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK);
    glDisable(GL_DEPTH_TEST);

    Instance * instance = (Instance *)ins;
    if (instance == 0)
    {
        LOGW_EU("%s Program is NULL return!", __FUNCTION__);
        return;
    }

    //使用编译好的program
    glUseProgram(instance->pProgram);
    //图像旋转270度
    float * maMVPMatrix = getRotateM(NULL, 0, 270, 0, 0, 1);
    //float * maMVPMatrix = getRotateM(NULL, 0, 0, 0, 0, 1);
    //传入mvp矩阵
    glUniformMatrix4fv(instance->maMVPMatrixHandle, 1, GL_FALSE, maMVPMatrix);

    free(maMVPMatrix);
    //传入顶点坐标
    glVertexAttribPointer(instance->maPositionHandle,
                          3,//GLint size X Y Z
                          GL_FLOAT,//GLenum type
                          GL_FALSE,//GLboolean normalized
                          3 * 4,//GLsizei stride  dataVertex中三个数据一组
                          dataVertex//const GLvoid * ptr
    );
    //传入纹理坐标
    glVertexAttribPointer(instance->maTexCoorHandle,
                          2,//S T
                          GL_FLOAT,//GLenum type
                          GL_FALSE,//GLboolean normalized
                          2 * 4,//GLsizei stride   dataTexCoor中两个数据一组
                          dataTexCoor//const GLvoid * ptr
    );

    //绑定纹理
    bindTexture(GL_TEXTURE0, instance->yTexture, instance->pWidth, instance->pHeight, instance->yBuffer);
    bindTexture(GL_TEXTURE1, instance->uTexture, instance->pWidth / 2, instance->pHeight / 2, instance->uBuffer);
    bindTexture(GL_TEXTURE2, instance->vTexture, instance->pWidth / 2, instance->pHeight / 2, instance->vBuffer);

    //片元中uniform 2维均匀变量赋值
    glUniform1i(instance->myTextureHandle, 0); //对应纹理第1层
    glUniform1i(instance->muTextureHandle, 1); //对应纹理第2层
    glUniform1i(instance->mvTextureHandle, 2); //对应纹理第3层

    //enable之后这些引用才能在shader中生效
    glEnableVertexAttribArray(instance->maPositionHandle);
    glEnableVertexAttribArray(instance->maTexCoorHandle);

    //绘制 从顶点0开始绘制,总共四个顶点,组成两个三角形,两个三角形拼接成一个矩形纹理,也就是我们的画面
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}

传入数据与显示

现在,就可以传入 YUV数据并进行显示了,这里调用eglSwapBuffers,当gl 绘制完毕后,显示缓存surface填充完毕,我们就可以通过eglSwapBuffers,让display也就是显示设备去显示surface中的内容了。
代码实现

//渲染数据
JNIEXPORT
void
Java_com_cm_opengles_CmOpenGLES_drawFrame(JNIEnv *env, jobject obj, jbyteArray yuvDatas, jint size)
{
    //将yuv数据分别copy到对应的buffer中
    jbyte * srcp = (*env)->GetByteArrayElements(env, yuvDatas, 0);

    memcpy(instance->yBuffer, srcp, instance->yBufferSize);
    memcpy(instance->uBuffer, srcp+instance->yBufferSize, instance->uBufferSize);
    memcpy(instance->vBuffer, srcp+instance->yBufferSize+instance->uBufferSize, instance->vBufferSize);

    (*env)->ReleaseByteArrayElements(env, yuvDatas, srcp, JNI_ABORT);

    //opengl绘制
    drawFrame(instance);

    //交换display中显示图像缓存的地址和后台图像缓存的地址,将当前计算出的图像缓存显示
    EGLBoolean res = eglSwapBuffers(eglInstance.display, eglInstance.surface);

    if(res == EGL_FALSE){
        LOGI_EU("eglSwapBuffers Error %d", eglGetError());
    }else{
        LOGI_EU("eglSwapBuffers Ok");
    }
}

//释放资源
JNIEXPORT
void
Java_com_cm_opengles_CmOpenGLES_release(JNIEnv *env, jobject obj)
{
    LOGI_EU("release()");
    if(instance != 0)
        {
            eglMakeCurrent(eglInstance.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
            eglDestroyContext(eglInstance.display, eglInstance.context);
            eglDestroySurface(eglInstance.display, eglInstance.surface);
            eglTerminate(eglInstance.display);

            free(instance->yBuffer);
            free(instance->uBuffer);
            free(instance->vBuffer);
            instance->yBuffer = 0;
            free(instance);
            instance = 0;
        }
}

这样就完成了OpenGL对YUV420P的渲染。

2017-11-17 15:32:10 Jason101123 阅读数 327

一,同样的,利用封装好的库收集图像
package com.example.dgxq008.opencv_readpixel;

import android.app.Activity;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.SurfaceView;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;

import org.opencv.android.CameraBridgeViewBase;
import org.opencv.android.OpenCVLoader;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.imgproc.Imgproc;

public class MainActivity extends Activity implements View.OnClickListener , CameraBridgeViewBase.CvCameraViewListener2 {
private Button btnProc;
private ImageView imageView;
private Bitmap bmp;

private CameraBridgeViewBase mOpenCvCameraView;
private Mat rgba;

static {
    System.loadLibrary("native-lib");
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);


    btnProc = (Button) findViewById(R.id.btn_gray_process);
    btnProc.setOnClickListener(this);

    mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.jcv);
    mOpenCvCameraView.setVisibility(SurfaceView.VISIBLE);
    mOpenCvCameraView.setCvCameraViewListener(this);
}

@Override
public void onCameraViewStarted(int width, int height) {
    rgba = new Mat(width,height, CvType.CV_8UC4);
}

@Override
public void onCameraViewStopped() {
    rgba.release();
}

@Override
public Mat onCameraFrame(CameraBridgeViewBase.CvCameraViewFrame inputFrame) {

    //rgba = inputFrame.gray();
    rgba = inputFrame.rgba();
    nativeRgbaToBlue(rgba.nativeObj);//40ms以内就不会影响预览效果
    return rgba;
}

/**
 * A native method that is implemented by the 'native-lib' native library,
 * which is packaged with this application.
 */
public static native int[] grayProc(int[] pixels, int w, int h);

public static native void nativeRgba(long rgba);

public static native void nativeRgbaToBlue(long rgba);


@Override
public void onClick(View view) {
}


@Override
protected void onResume() {
    super.onResume();
    if (!OpenCVLoader.initDebug()) {
    } else {
        mOpenCvCameraView.enableView();
    }
}

@Override
protected void onPause() {
    super.onPause();
    if (mOpenCvCameraView != null){
        mOpenCvCameraView.disableView();
    }
}

@Override
protected void onDestroy() {
    super.onDestroy();
    if (mOpenCvCameraView != null){
        mOpenCvCameraView.disableView();
    }
}

}

二, 实现Jni接口,完成过滤颜色

extern “C”
JNIEXPORT void JNICALL
Java_com_example_dgxq008_opencv_1readpixel_MainActivity_nativeRgbaToBlue(JNIEnv *env, jclass type,
jlong jrgba) {
Mat& rgba = * (Mat*)jrgba;
uchar* rgbaData = rgba.data;
int height = rgba.rows;
int weight = rgba.cols;

int length = height * weight;

for (int i = 0; i < length; ++i) {
    //RGBA, 只取一种,其他重置为零,alpha忽略
    rgbaData[4*i+0] = rgbaData[4*i+0];
    rgbaData[4*i+1] = 0;
    rgbaData[4*i+2] = 0;
}

}

没有更多推荐了,返回首页