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实现获取客户端IP地址

《使用Java实现获取客户端IP地址》这篇文章主要为大家详细介绍了如何使用Java实现获取客户端IP地址,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 首先是获取 IP,直接上代码import org.springframework.web.context.request.Requ

nginx-rtmp-module构建流媒体直播服务器实战指南

《nginx-rtmp-module构建流媒体直播服务器实战指南》本文主要介绍了nginx-rtmp-module构建流媒体直播服务器实战指南,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有... 目录1. RTMP协议介绍与应用RTMP协议的原理RTMP协议的应用RTMP与现代流媒体技术的关系2

Python手搓邮件发送客户端

《Python手搓邮件发送客户端》这篇文章主要为大家详细介绍了如何使用Python手搓邮件发送客户端,支持发送邮件,附件,定时发送以及个性化邮件正文,感兴趣的可以了解下... 目录1. 简介2.主要功能2.1.邮件发送功能2.2.个性签名功能2.3.定时发送功能2. 4.附件管理2.5.配置加载功能2.6.

Redis连接失败:客户端IP不在白名单中的问题分析与解决方案

《Redis连接失败:客户端IP不在白名单中的问题分析与解决方案》在现代分布式系统中,Redis作为一种高性能的内存数据库,被广泛应用于缓存、消息队列、会话存储等场景,然而,在实际使用过程中,我们可能... 目录一、问题背景二、错误分析1. 错误信息解读2. 根本原因三、解决方案1. 将客户端IP添加到Re

SpringBoot实现websocket服务端及客户端的详细过程

《SpringBoot实现websocket服务端及客户端的详细过程》文章介绍了WebSocket通信过程、服务端和客户端的实现,以及可能遇到的问题及解决方案,感兴趣的朋友一起看看吧... 目录一、WebSocket通信过程二、服务端实现1.pom文件添加依赖2.启用Springboot对WebSocket

QT实现TCP客户端自动连接

《QT实现TCP客户端自动连接》这篇文章主要为大家详细介绍了QT中一个TCP客户端自动连接的测试模型,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录版本 1:没有取消按钮 测试效果测试代码版本 2:有取消按钮测试效果测试代码版本 1:没有取消按钮 测试效果缺陷:无法手动停

Nacos客户端本地缓存和故障转移方式

《Nacos客户端本地缓存和故障转移方式》Nacos客户端在从Server获得服务时,若出现故障,会通过ServiceInfoHolder和FailoverReactor进行故障转移,ServiceI... 目录1. ServiceInfoHolder本地缓存目录2. FailoverReactorinit

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是针对嵌入式设备这些对性能要求比较高的平台,因此把很多影响性能的函数都去掉了,上述的几个函数都被移除了。接