OpenGL.Shader:志哥教你写一个滤镜直播客户端(1)项目分析

2024-03-03 00:40

本文主要是介绍OpenGL.Shader:志哥教你写一个滤镜直播客户端(1)项目分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

OpenGL.Shader:志哥教你写一个滤镜直播客户端(1)项目分析

0、聊点题外话

2020年绝对是魔幻的一年,澳洲大火还没烧完就迎接我最不喜欢的鼠年了,但由于新型冠状病毒引发的肺炎卷席全国,春节假期得到国人梦寐以求的延长。我也能好好的总结这一系列的文章,完善对应工程项目并对其开源,这也算是自己对21世纪第二个十年的一个总结吧;球星科比(我的青春啊!)突然陨落又使我明白,明天和意外你永远不知道哪个谁会先来,所以请珍惜当下吧骚年。

1、项目分析

在些年前滤镜直播爆发期,我有幸参加了一些类似的项目,参读了开源工程GPUImage并留下了一篇阅读笔记,之后开始慢慢入门OpenGL/OpenCV。一直想做一个具有代表性的开源项目,有助于自己总结,也希望能帮助其他人。

高兴的是现阶段项目已经基本完成了!项目特色包括:是基于Android端,结合多种滤镜效果(多数来自GPUImage和 http://www.zealfilter.com/),摄像头采集视频,麦克风采集音频,然后编码输出字节流的项目工程,大部分都是用使用NDK下的C++写,shader算法的安全性提高,没有使用任何额外的第三方库,方便新手掌握知识点,也方便开源改造移植到 iOS。

工程结构组成大致如下:

整体有个大概认识之后,接下来分析模块组成:

CFEScheduler.java:Camera Filter Encoder三者的调度模块,把一些业务逻辑放在这个调度模块里面,方便规范业务代码。

GpuFilterRender.java:进入Native的入口,接管Surface生命周期,还有Filter Manage等相关接口。

Camera / AudioRecord:系统API,获取视频数据(NV21格式)和音频数据(PCM格式)。

Filter Manage About:滤镜特效相关的一系列cpp文件,是滤镜渲染的核心部分,算法来自GUImage和http://www.zealfilter.com

GL Render:全自定义的GL-Thread-Render模块,是滤镜渲染的主要组成部分。

AMediaCodecEncoder:NDK的MediaCodec,个人觉得比较有争议 / 意义的一部分,因为很多坑和细节都需要注意的地方,但如果把这部分的问题解决,内容吃透,就能掌握很多高阶的知识,甚至可以入门Android的系统开发。

 

2、show me the code

首先从简单的java页面开始,超级简单。界面布局大致如下:

CameraFilterEncoderActivity的逻辑代码也超级简单(我都不想放上来了)

public class CameraFilterEncodeActivity extends Activity {private static final String TAG = "CFEScheduler";private CFEScheduler cfeScheduler;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_camera_filter_encode);SurfaceView surfaceView = findViewById(R.id.camera_view);// 初始化CFEScheduler,传入ctx和surface,用于渲染。if( cfeScheduler==null)cfeScheduler = new CFEScheduler(this, surfaceView);initFilterSpinner();initFilterAdjuster();}SeekBar filterAdjuster;private void initFilterAdjuster() {filterAdjuster = findViewById(R.id.value_seek_bar);filterAdjuster.setMax(100);filterAdjuster.setProgress(0);filterAdjuster.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {cfeScheduler.adjustFilterValue(progress, seekBar.getMax());}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) { }@Overridepublic void onStopTrackingTouch(SeekBar seekBar) { }});}private void initFilterSpinner() {//从CFEScheduler获取当前所支持的滤镜清单String[] mItems = cfeScheduler.getSupportedFiltersName();//String[] mItems = this.getResources().getStringArray(R.array.filter_name);Spinner filterSpinner = findViewById(R.id.filter_spinner);ArrayAdapter<String> adapter=new ArrayAdapter<String>(this, android.R.layout.simple_spinner_dropdown_item, mItems);filterSpinner.setAdapter(adapter);filterSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {@Overridepublic void onItemSelected(AdapterView<?> parent, View view, int position, long id) {// 复原Filter.AdjusterfilterAdjuster.setProgress(0);// 正常是通过getSupportedFilterTypeID(name)查询typeId// 这里的position更好是names[]的索引,所以可以直接通过index查询typeId了int typeId = cfeScheduler.getSupportedFilterTypeID(position);// 设置当前滤镜id,替换滤镜效果cfeScheduler.setFilterType(typeId);}@Overridepublic void onNothingSelected(AdapterView<?> parent) { }});}@Overrideprotected void onResume() {super.onResume();if( cfeScheduler!=null) {cfeScheduler.onResume();}}@Overrideprotected void onPause() {super.onPause();if( cfeScheduler!=null) {cfeScheduler.onPause();}}}

代码意义都写上注释了,比较好理解。接下来顺藤摸瓜进入 CFEScheduler 内部看看实现逻辑。

(PS:先已视频为主线讲解,音频内容稍后追加。)

/*** Created by zzr on 2019/11/27.* CFE : Camera Filter Encode* 这些逻辑代码,如果不嫌弃混乱,可以直接写在CameraFilterEncodeActivity.*/
public class CFEScheduler implements Camera.PreviewCallback, SurfaceHolder.Callback {private static final String TAG = "CFEScheduler";private WeakReference<Activity> mActivityWeakRef;private GpuFilterRender mGpuFilterRender;/*Camera SurfaceView相关*/CFEScheduler(Activity activity, SurfaceView view) {mActivityWeakRef = new WeakReference<>(activity);mGpuFilterRender = new GpuFilterRender(activity);view.getHolder().setFormat(PixelFormat.RGBA_8888);view.getHolder().addCallback(this);}public void onResume() {Log.d(TAG, "onResume ...");setUpCamera(mCurrentCameraId);}public void onPause() {Log.d(TAG, "onPause ...");releaseCamera();}// ... ... holder callback// ... ... (篇幅所限,分开显示)private int mCurrentCameraId = Camera.CameraInfo.CAMERA_FACING_FRONT;private Camera mCameraInstance;private void setUpCamera(final int id) {mCameraInstance = getCameraInstance(id);Camera.Parameters parameters = mCameraInstance.getParameters();// 设置frame data格式-NV21(默认格式)parameters.setPreviewFormat(ImageFormat.NV21);if (parameters.getSupportedFocusModes().contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);}// 这里,我没有设置best的previewsize。// adjust by getting supportedPreviewSizes and then choosing// the best one for screen size (best fill screen)mCameraInstance.setParameters(parameters);Camera.CameraInfo cameraInfo = new Camera.CameraInfo();Camera.getCameraInfo(mCurrentCameraId, cameraInfo);Activity activity = mActivityWeakRef.get();int orientation = getCameraDisplayOrientation(activity, cameraInfo);Log.i(TAG, "getCameraDisplayOrientation : "+orientation);boolean flipHorizontal = cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT;//!!!根据需要设置是否水平翻转,垂直翻转mGpuFilterRender.setRotationCamera(orientation, flipHorizontal, false);}private int getCameraDisplayOrientation(final Activity activity,@NonNull Camera.CameraInfo info) {if(activity == null) return 0;int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();int degrees = 0;switch (rotation) {case Surface.ROTATION_0:degrees = 0;break;case Surface.ROTATION_90:degrees = 90;break;case Surface.ROTATION_180:degrees = 180;break;case Surface.ROTATION_270:degrees = 270;break;}int result;if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {result = (info.orientation + degrees) % 360;} else { // back-facingresult = (info.orientation - degrees + 360) % 360;}return result;}/** A safe way to get an instance of the Camera object. */private Camera getCameraInstance(final int id) {Camera c = null;try {c = Camera.open(id);} catch (Exception e) {e.printStackTrace();}return c;}private void releaseCamera() {mCameraInstance.setPreviewCallback(null);mCameraInstance.release();mCameraInstance = null;}
}

默认我们打开前置摄像头为视频数据源。这里我没有设置best的previewsize,主要是因为参照GPUImage的处理方法,在shader内部处理调整这方面的问题。最后通过GpuFilterRender.setRotationCamera设置镜头的旋转角度,和是否需要进行水平翻转和垂直翻转。接下来就是holder callback 部分。

    // ... holder callbackprivate SurfaceTexture mCameraTexture = null;@Overridepublic void surfaceCreated(SurfaceHolder holder) {Log.d(TAG, "surfaceCreated ... ");try {int[] textures = new int[1];GLES20.glGenTextures(1, textures, 0);mCameraTexture = new SurfaceTexture(textures[0]);mCameraInstance.setPreviewTexture(mCameraTexture);mCameraInstance.setPreviewCallback(this);mCameraInstance.startPreview();} catch (Exception e) {e.printStackTrace();}mGpuFilterRender.onSurfaceCreate(holder.getSurface());}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {Log.d(TAG, "surfaceChanged ... ");mGpuFilterRender.onSurfaceChange(holder.getSurface(), width, height);}@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {Log.d(TAG, "surfaceDestroyed ... ");mGpuFilterRender.onSurfaceDestroy(holder.getSurface());if( mCameraTexture!=null){mCameraTexture.release();}}@Overridepublic void onPreviewFrame(byte[] data, Camera camera) {if( mGpuFilterRender!=null){final Camera.Size previewSize = camera.getParameters().getPreviewSize();mGpuFilterRender.feedVideoData(data.clone(), previewSize.width, previewSize.height);}}

surfaceview的三大回调接口当中,我们也把其绑定到GpuFilterRender当中,便于生命周期的控制。并利用SurfaceTexture设置Camera的预览,在预览数据的回调接口中,把数据的clone传入GpuFilterRender缓存下来。

然后再看看GpuFilterRender.java,以及对应的JNI文件接口。

/*** Created by zzr on 2019/11/27.*/
class GpuFilterRender {static {System.loadLibrary("gpu-filter");}private Context ctx;GpuFilterRender(Context context) {ctx = context;}public native void onSurfaceCreate(Surface surface);public native void onSurfaceChange(Surface surface, int width, int height);public native void onSurfaceDestroy(Surface surface);// 发送视频nv21数据public native void feedVideoData(byte[] data,int width,int height);// 发送音频 pcm数据public native void feedAudioData(byte[] data);/*** 设置摄像头角度和方向* @param rotation 角度* @param flipHorizontal 是否水平翻转* @param flipVertical 是否垂直翻转*/public native void setRotationCamera(final int rotation, final boolean flipHorizontal,final boolean flipVertical);// 设置滤镜类型public native void setFilterType(int typeId);// 调整滤镜效果public native void adjustFilterValue(int value,int max);
}
#include <jni.h>
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include "../egl/GLThread.h"
#include "render/GpuFilterRender.h"GLThread* glThread = NULL;
GpuFilterRender* render = NULL;extern "C" {JNIEXPORT void JNICALL
Java_org_zzrblog_gpufilter_GpuFilterRender_onSurfaceCreate(JNIEnv *env, jobject instance, jobject surface) {ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);if (render == NULL) {render = new GpuFilterRender();}if (glThread == NULL) {glThread = new GLThread();}glThread->setGLRender(render);glThread->onSurfaceCreate(nativeWindow);
}
JNIEXPORT void JNICALL
Java_org_zzrblog_gpufilter_GpuFilterRender_onSurfaceChange(JNIEnv *env, jobject instance, jobject surface,jint width, jint height) {glThread->onSurfaceChange(width, height);
}
JNIEXPORT void JNICALL
Java_org_zzrblog_gpufilter_GpuFilterRender_onSurfaceDestroy(JNIEnv *env, jobject instance, jobject surface) {glThread->onSurfaceDestroy();glThread->release();delete glThread;glThread = NULL;if (render == NULL) {delete render;render = NULL;}
}
JNIEXPORT void JNICALL
Java_org_zzrblog_gpufilter_GpuFilterRender_setRotationCamera(JNIEnv *env, jobject instance,jint rotation, jboolean flipHorizontal,jboolean flipVertical) {// 注意这里flipVertical对应render->setRotationCamera.flipHorizontal// 注意这里flipHorizontal对应render->setRotationCamera.flipVertical// 因为Android的预览帧数据是横着的,仿照GPUImage的处理方式。if (render == NULL) {render = new GpuFilterRender();}render->setRotationCamera(rotation, flipVertical, flipHorizontal);
}
JNIEXPORT void JNICALL
Java_org_zzrblog_gpufilter_GpuFilterRender_setFilterType(JNIEnv *env, jobject instance, jint typeId) {if (render == NULL)render = new GpuFilterRender();render->setFilter(typeId);
}
JNIEXPORT void JNICALL
Java_org_zzrblog_gpufilter_GpuFilterRender_adjustFilterValue(JNIEnv *env, jobject instance, jint value, jint max) {if (render == NULL)render = new GpuFilterRender();render->adjustFilterValue(value, max);
}
JNIEXPORT void JNICALL
Java_org_zzrblog_gpufilter_GpuFilterRender_feedVideoData(JNIEnv *env, jobject instance,jbyteArray array, jint width, jint height) {if (render == NULL) return;jbyte *nv21_buffer = env->GetByteArrayElements(array, NULL);jsize array_len = env->GetArrayLength(array);render->feedVideoData(nv21_buffer, array_len, width, height);env->ReleaseByteArrayElements(array, nv21_buffer, 0);
}
} // extern "C"

(GLThread部分请参考以前的文章 https://blog.csdn.net/a360940265a/article/details/88600962)

下一章进入GpuFilterRender.cpp,深入分析Java_org_zzrblog_gpufilter_GpuFilterRender_setRotationCamera的注意事项,还有 核心部分——Filter的内容。

这篇关于OpenGL.Shader:志哥教你写一个滤镜直播客户端(1)项目分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/767915

相关文章

Java Websocket实例【服务端与客户端实现全双工通讯】

Java Websocket实例【服务端与客户端实现全双工通讯】 现很多网站为了实现即时通讯,所用的技术都是轮询(polling)。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发 出HTTP request,然后由服务器返回最新的数据给客服端的浏览器。这种传统的HTTP request 的模式带来很明显的缺点 – 浏 览器需要不断的向服务器发出请求,然而HTTP

速盾:直播 cdn 服务器带宽?

在当今数字化时代,直播已经成为了一种非常流行的娱乐和商业活动形式。为了确保直播的流畅性和高质量,直播平台通常会使用 CDN(Content Delivery Network,内容分发网络)服务器来分发直播流。而 CDN 服务器的带宽则是影响直播质量的一个重要因素。下面我们就来探讨一下速盾视角下的直播 CDN 服务器带宽问题。 一、直播对带宽的需求 高清视频流 直播通常需要传输高清视频

OPENGL顶点数组, glDrawArrays,glDrawElements

顶点数组, glDrawArrays,glDrawElements  前两天接触OpenGL ES的时候发现里面没有了熟悉的glBegin(), glEnd(),glVertex3f()函数,取而代之的是glDrawArrays()。有问题问google,终于找到答案:因为OpenGL ES是针对嵌入式设备这些对性能要求比较高的平台,因此把很多影响性能的函数都去掉了,上述的几个函数都被移除了。接

OpenGL ES学习总结:基础知识简介

什么是OpenGL ES? OpenGL ES (为OpenGL for Embedded System的缩写) 为适用于嵌入式系统的一个免费二维和三维图形库。 为桌面版本OpenGL 的一个子集。 OpenGL ES管道(Pipeline) OpenGL ES 1.x 的工序是固定的,称为Fix-Function Pipeline,可以想象一个带有很多控制开关的机器,尽管加工

OpenGL雾(fog)

使用fog步骤: 1. enable. glEnable(GL_FOG); // 使用雾气 2. 设置雾气颜色。glFogfv(GL_FOG_COLOR, fogColor); 3. 设置雾气的模式. glFogi(GL_FOG_MODE, GL_EXP); // 还可以选择GL_EXP2或GL_LINEAR 4. 设置雾的密度. glFogf(GL_FOG_DENSITY, 0

opengl纹理操作

我们在前一课中,学习了简单的像素操作,这意味着我们可以使用各种各样的BMP文件来丰富程序的显示效果,于是我们的OpenGL图形程序也不再像以前总是只显示几个多边形那样单调了。——但是这还不够。虽然我们可以将像素数据按照矩形进行缩小和放大,但是还不足以满足我们的要求。例如要将一幅世界地图绘制到一个球体表面,只使用glPixelZoom这样的函数来进行缩放显然是不够的。OpenGL纹理映射功能支持将

OpenGL ES 2.0渲染管线

http://codingnow.cn/opengles/1504.html Opengl es 2.0实现了可编程的图形管线,比起1.x的固定管线要复杂和灵活很多,由两部分规范组成:Opengl es 2.0 API规范和Opengl es着色语言规范。下图是Opengl es 2.0渲染管线,阴影部分是opengl es 2.0的可编程阶段。   1. 顶点着色器(Vert

Redis 客户端Jedis使用---连接池

Jedis 是Redis 的Java客户端,通过一段时间的使用,jedis基本实现redis的所有功能,并且jedis在客户端实现redis数据分片功能,Redis本身是没有数据分布功能。 一、下载jedis 代码 jedis 代码地址:https://github.com/xetorthio/jedis 再次感受到开源的强大。呵呵,大家有时间可以看看源码。 二、项目中如何使用Jedi

Java Socket服务器端与客户端的编程步骤总结

一,InetAddress类: InetAddress类没有构造方法,所以不能直接new出一个对象; 可以通过InetAddress类的静态方法获得InetAddress的对象; InetAddress.getLocalHost(); InetAddress.getByName(""); 类主要方法: String - address.getHostName(); String - addre

9.7(UDP局域网多客户端聊天室)

服务器端 #include<myhead.h>#define SERIP "192.168.0.132"#define SERPORT 8888#define MAX 50//定义用户结构体typedef struct{struct sockaddr_in addr;int flag;}User;User users[MAX];//用户列表void add_user(struct s