前言

如今的视频类app可谓是如日中天,火的不行。比如美拍、快手、VUE、火山小视频、抖音小视频等等。而这类视频的最基础和核心的功能就是视频录制和视频编辑功能。包括了手机视频录制、美白、加滤镜、加水印、给本地视频美白、加水印、加滤镜、视频裁剪、视频拼接和加bgm等等一系列音视频的核心操作。而本系列的文章,就是作者在视频编辑器开发上的一些个人心得,希望能帮助到大家,另外因个人水平有限,难免有不足之处,还希望大家不惜赐教。

本系列的文章,计划包括以下几部分:
1、android视频编辑器之视频录制、断点续录、对焦等
2、android视频编辑器之录制过程中加水印和美白效果
3、android视频编辑器之本地视频加美白效果和加视频水印
4、android视频编辑器之通过OpenGL给视频加各类滤镜
5、android视频编辑器之音频编解码、mono转stereo、音频混音、音频音量调节
6、android视频编辑器之通过OpenGL做本地视频拼接
7、android视频编辑器之音视频裁剪、增加背景音乐等
通过这一系列的文章,大家就能自己开发出一个具有目前市面上完整功能的视频类app最核心功能的视频编辑器了(当然,如果作者能按计划全部写完的话。。。捂脸)。主要涉及到的核心知识点有Android音视频编解码、OpenGL开发、音视频的基础知识等等。整个过程会忽略掉一些基础知识,只会讲解一些比较核心的技术点。所有代码都会上传到github上面。有兴趣的童鞋,可以从文章末尾进行下载。
文章中也借鉴和学习了很多其他小伙伴们分享的知识。 每篇文章我都会贴出不完全的相关连接,非常感谢小伙伴们的分享。
Camera和GLSurface的使用
Opengl入门详解
使用Camera+GLSurfaceView预览摄像头数据并拍照
谷歌工程师编写的grafika
MagicCamera包含四十多种各种滤镜以及录制拍照等功能的一个开源项目

方案选择

     
      关于android平台的视频录制,首先我们要确定我们的需求,录制音视频本地保存为mp4文件。实现音视频录制的方案有很多,比如原生的sdk,通过Camera进行数据采集,注意:是Android.hardware包下的Camera而不是Android.graphics包下的,后者是用于矩阵变换的一个类,而前者才是通过硬件采集摄像头的数据,并且返回到java层。然后使用SurfaceView进行画面预览,使用MediaCodec对数据进行编解码,最后通过MediaMuxer将音视频混合打包成为一个mp4文件。当然也可以直接使用MediaRecorder类进行录制,该类是封装好了的视频录制类,但是不利于功能扩展,比如如果我们想在录制的视频上加上我们自己的logo,也就是常说的加水印,或者是录制一会儿 然后暂停 然后继续录制的功能,也就是断点续录的话 就不是那么容易实现了。而本篇文章,作为后面系列的基础,我们就不讲解常规的视频录制的方案了,有兴趣的可以查看本文前面附上的一些链接。因为我们后期会涉及到给视频加滤镜、加水印、加美颜等功能,所以就不能使用常规的视频录制方案了,而是采用Camera + OpengGL + MediaCodec +进MediaMuxer行视频录制。

视频预览

为了实现录制的效果,首先我们得实现摄像头数据预览的功能。
Camera的使用
android在5.0的版本加载了hardware.camera2包对android平台的视频录制功能进行了增强,但是为了兼容低版本,我们将使用的是Camera类,而不是5.0之后加入的新类。对新api感兴趣的童鞋,可以自行查阅相关资料。

GLSurfaceView的作用

       其实视频的预览大致流程就是,从Camera中拿到当前摄像头返回的数据,然后显示在屏幕上,我们这里是采用的GLSurfaceView类进行图像的显示。GLSurfaceView类有一个Renderer接口,这个Renderer其实就是GLSurfaceView中很重要的一个监听器,你可以把他看成是GLSurfaceView的生命周期的回调。有三个回调函数:
 @Overridepublic void onSurfaceCreated(GL10 gl, EGLConfig config) {}@Overridepublic void onSurfaceChanged(GL10 gl, int width, int height) {}@Overridepublic void onDrawFrame(GL10 gl) {}
onSurfaceCreated:我们主要在这里面做一些初始化的工作
onSurfaceChanged:就是当surface大小发生变化的时候,会回调,我们主要会在这里面做一些更改相关设置的工作
onDrawFrame:这个就是返回当前帧的数据,我们对帧数据进行处理,主要就是在这里面进行的
所以,我们的大致流程就是按照这三个回调方法来进行的:
      
CameraController类对camera进行控制
      Camera的使用过程,网上已经有很多资料了,这里就不在过多的介绍了。但是 有几个地方需要注意一下,首先就是你设置的视频尺寸摄像头并不一定支持,所以我们要选取摄像头支持的,跟我们预设的相同或者相近的尺寸,主要代码如下
     mCamera = Camera.open(cameraId);if (mCamera != null){/**选择当前设备允许的预览尺寸*/Camera.Parameters param = mCamera.getParameters();preSize = getPropPreviewSize(param.getSupportedPreviewSizes(), mConfig.rate,mConfig.minPreviewWidth);picSize = getPropPictureSize(param.getSupportedPictureSizes(),mConfig.rate,mConfig.minPictureWidth);param.setPictureSize(picSize.width, picSize.height);param.setPreviewSize(preSize.width,preSize.height);mCamera.setParameters(param);}private Camera.Size getPropPictureSize(List<Camera.Size> list, float th, int minWidth){Collections.sort(list, sizeComparator);int i = 0;for(Camera.Size s:list){if((s.height >= minWidth) && equalRate(s, th)){break;}i++;}if(i == list.size()){i = 0;}return list.get(i);}private Camera.Size getPropPreviewSize(List<Camera.Size> list, float th, int minWidth){Collections.sort(list, sizeComparator);int i = 0;for(Camera.Size s:list){if((s.height >= minWidth) && equalRate(s, th)){break;}i++;}if(i == list.size()){i = 0;}return list.get(i);}private static boolean equalRate(Camera.Size s, float rate){float r = (float)(s.width)/(float)(s.height);if(Math.abs(r - rate) <= 0.03) {return true;}else{return false;}}private Comparator<Camera.Size> sizeComparator=new Comparator<Camera.Size>(){public int compare(Camera.Size lhs, Camera.Size rhs) {if(lhs.height == rhs.height){return 0;}else if(lhs.height > rhs.height){return 1;}else{return -1;}}};

这个代码还是相当简单,这里就不过多介绍了,网上也有很多不同的但是类似功能的适配方法,大家可以多了解下,相互对照。
     第二个就是,摄像头取数据的坐标系和屏幕显示的坐标系不太相同,简单的说就是,不管是前置还是后置摄像头,我们都需要对摄像头取的数据进行一些坐标系旋转操作,才能正常的显示到屏幕上,不然的话就会出现画面扭曲的情况。因为我们是采用的OpengGL进行视频录制的,所以我们会有一系列的AFilter来进行shader的加载和画面的渲染工作,所以我们将摄像头数据的旋转也放到这个里面来做。这部分后面再说,CameraController类主要就是Camera的一个包装类,还会包括一些视频尺寸控制等代码,具体的请下载完整demo,进行查看。

AFilter的作用   
       我们在这个项目中,我们使用了AFilter来完成加载shader、绘制图像、清除数据等,主要代码包括如下:

加载asset中的shader

public static int uLoadShader(int shaderType,String source){int shader= GLES20.glCreateShader(shaderType);if(0!=shader){GLES20.glShaderSource(shader,source);GLES20.glCompileShader(shader);int[] compiled=new int[1];GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS,compiled,0);if(compiled[0]==0){glError(1,"Could not compile shader:"+shaderType);glError(1,"GLES20 Error:"+ GLES20.glGetShaderInfoLog(shader));GLES20.glDeleteShader(shader);shader=0;}}return shader;}

Buffer的初始化

/*** Buffer初始化*/protected void initBuffer(){ByteBuffer a= ByteBuffer.allocateDirect(32);a.order(ByteOrder.nativeOrder());mVerBuffer=a.asFloatBuffer();mVerBuffer.put(pos);mVerBuffer.position(0);ByteBuffer b= ByteBuffer.allocateDirect(32);b.order(ByteOrder.nativeOrder());mTexBuffer=b.asFloatBuffer();mTexBuffer.put(coord);mTexBuffer.position(0);}

绑定默认的纹理

/*** 绑定默认纹理*/protected void onBindTexture(){GLES20.glActiveTexture(GLES20.GL_TEXTURE0+textureType);GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,getTextureId());GLES20.glUniform1i(mHTexture,textureType);}

每次绘制前需要清理画布

/*** 清理画布*/protected void onClear(){GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);}

这些代码在不同的filter中其实都是公用的,所以我们通过一个抽象类,来进行管理。

      上面我们说了摄像头的数据需要进行旋转,所以我们通过一个ShowAFilter来进行画面的旋转操作,核心代码如下,通过传入是前置还是后置摄像头的flag来进行画面旋转
public void setFlag(int flag) {super.setFlag(flag);float[] coord;if(getFlag()==1){    //前置摄像头 顺时针旋转90,并上下颠倒coord=new float[]{1.0f, 1.0f,0.0f, 1.0f,1.0f, 0.0f,0.0f, 0.0f,};}else{               //后置摄像头 顺时针旋转90度coord=new float[]{0.0f, 1.0f,1.0f, 1.0f,0.0f, 0.0f,1.0f, 0.0f,};}mTexBuffer.clear();mTexBuffer.put(coord);mTexBuffer.position(0);}

摄像头和AFilter我们都已经准备好了,下一步,就是我们需要把Camera取的数据显示在GLSurfaceView上面了,也就是需要将AFilter、CameraController和GLSurfaceView联系起来。然后,因为我们后续会涉及到很多不同AFilter的管理,所以我们创建一个CameraDraw类,来管理AFilter。让其实现GLSurfaceView.Renderer接口,便于管理。

CameraDraw类
      首先实现GLSurfaceView.Renderer接口
    public class CameraDrawer implements GLSurfaceView.Renderer

然后,在类的构造函数中,进行AFilter的初始化

   public CameraDrawer(Resources resources){//初始化一个滤镜 也可以叫控制器showFilter = new ShowFilter(resources);      }

在onSurfaceCreated中,进行SurfaceTextured的创建,并且和AFilter进行绑定

 @Overridepublic void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {textureID = createTextureID();mSurfaceTextrue = new SurfaceTexture(textureID);showFilter.create();showFilter.setTextureId(textureID);   }

在onSurfaceChanged中,进行一些参数的更改和纹理的重新绑定

    @Overridepublic void onSurfaceChanged(GL10 gl10, int i, int i1) {width = i;height = i1;/**创建一个帧染缓冲区对象*/GLES20.glGenFramebuffers(1,fFrame,0);/**根据纹理数量 返回的纹理索引*/GLES20.glGenTextures(1, fTexture, 0);/**将生产的纹理名称和对应纹理进行绑定*/GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, fTexture[0]);/**根据指定的参数 生产一个2D的纹理 调用该函数前  必须调用glBindTexture以指定要操作的纹理*/GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, mPreviewWidth, mPreviewHeight,0,  GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);useTexParameter();GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0);}

在onDrawFrame中进行图像的绘制工作。

@Overridepublic void onDrawFrame(GL10 gl10) {/**更新界面中的数据*/mSurfaceTextrue.updateTexImage();/**绘制显示的filter*/GLES20.glViewport(0,0,width,height);showFilter.draw();}

CameraDraw目前所做的主要工作就是这样,然后我们将CameraController、CameraDraw和自定义的CameraView控件进行绑定,就可以实现摄像头数据预览了。

自定义的CameraView控件
首先,在构造函数中进行OpenGL、CameraController、CameraDraw的初始化

    private void init() {/**初始化OpenGL的相关信息*/setEGLContextClientVersion(2);//设置版本setRenderer(this);//设置RenderersetRenderMode(RENDERMODE_WHEN_DIRTY);//主动调用渲染setPreserveEGLContextOnPause(true);//保存Context当pause时setCameraDistance(100);//相机距离/**初始化Camera的绘制类*/mCameraDrawer = new CameraDrawer(getResources());/**初始化相机的管理类*/mCamera = new CameraController();}

然后,分别在三个生命周期的函数中调用CameraController和CameraDrawer的相关方法,以及打开摄像头

    @Overridepublic void onSurfaceCreated(GL10 gl, EGLConfig config) {mCameraDrawer.onSurfaceCreated(gl,config);if (!isSetParm){open(0);stickerInit();}mCameraDrawer.setPreviewSize(dataWidth,dataHeight);}@Overridepublic void onSurfaceChanged(GL10 gl, int width, int height) {mCameraDrawer.onSurfaceChanged(gl,width,height);}@Overridepublic void onDrawFrame(GL10 gl) {if (isSetParm){mCameraDrawer.onDrawFrame(gl);}}

然后在onFrameAvailable函数中,调用即可

    @Overridepublic void onFrameAvailable(SurfaceTexture surfaceTexture) {this.requestRender();}

我们的视频预览的主体流程就是这样,然后我们可以直接在布局中使用CameraView类即可。

视频录制和断点录制

在上面部分,我们实现了通过OpenGL预览视频,下面部分,我们就需要实现录制视频了.我们使用的opengl录制视频方案,采用的是谷歌的工程师编写的grafika 这个项目,项目链接 在文章开头部分。这个项目包含了很多GLSurfaceView和视频编解码的知识,还是非常值得学习一下的。主要核心的类就是TextureMovieEncoder和VideoEncoderCore类。采用的是MediaMuxer和MeidaCodec类进行视频的编码和音视频合成。后面我们涉及到音视频编解码、视频拼接、音视频裁剪的时候会详细的介绍一下android里面音视频编解码的相关类的用法。这里就暂时先不深入讲解了。当然音视频的编解码也可以使用FFmpeg进行软编码,但是因为硬编码的速度比软编码要快得多,所以我们这个项目,不会涉及到FFmpeg的使用。
        好了,现在回到我们的从GLSurfaceView读取数据,通过TexureMovieEncoder进行视频的录制和断点续录。
上面我们说了我们通过AFilter的相关类,进行opengl的相关操作,实现了视频的预览,这里我们还需要一个AFilter类将摄像头的数据交给我们的编码类进行编码。所以初始化的时候 再初始化一个
     drawFilter = new ShowFilter(resources);

这里需要注意一下,为了显示在屏幕上是正常的,我们进行了旋转的操作。所以,我们在录制的AFilter里面需要加上矩阵翻转的控制。

    OM= MatrixUtils.getOriginalMatrix();MatrixUtils.flip(OM,false,true);//矩阵上下翻转drawFilter.setMatrix(OM);

然后同样分别进行drawFilter的create,在onDrawFrame里面讲textureId进行绑定以及绘制。还有就是添加录制控制的相关代码

    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fFrame[0]);GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,GLES20.GL_TEXTURE_2D, fTexture[0], 0);GLES20.glViewport(0,0,mPreviewWidth,mPreviewHeight);drawFilter.setTextureId(fTexture[0]);drawFilter.draw();//解绑GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER,0);if (recordingEnabled){/**说明是录制状态*/switch (recordingStatus){case RECORDING_OFF:videoEncoder = new TextureMovieEncoder();videoEncoder.setPreviewSize(mPreviewWidth,mPreviewHeight);videoEncoder.startRecording(new TextureMovieEncoder.EncoderConfig(savePath, mPreviewWidth, mPreviewHeight,3500000, EGL14.eglGetCurrentContext(),null));recordingStatus = RECORDING_ON;break;case RECORDING_ON:case RECORDING_PAUSED:break;case RECORDING_RESUMED:videoEncoder.updateSharedContext(EGL14.eglGetCurrentContext());videoEncoder.resumeRecording();recordingStatus = RECORDING_ON;break;case RECORDING_RESUME:videoEncoder.resumeRecording();recordingStatus=RECORDING_ON;break;case RECORDING_PAUSE:videoEncoder.pauseRecording();recordingStatus=RECORDING_PAUSED;break;default:throw new RuntimeException("unknown recording status "+recordingStatus);}}else {switch (recordingStatus) {case RECORDING_ON:case RECORDING_RESUMED:case RECORDING_PAUSE:case RECORDING_RESUME:case RECORDING_PAUSED:videoEncoder.stopRecording();recordingStatus = RECORDING_OFF;break;case RECORDING_OFF:break;default:throw new RuntimeException("unknown recording status " + recordingStatus);}}if (videoEncoder != null && recordingEnabled && recordingStatus == RECORDING_ON){videoEncoder.setTextureId(fTexture[0]);videoEncoder.frameAvailable(mSurfaceTextrue);}

上面主要逻辑是,在数据返回的视频判断当前的录制状态,如果是正在录制,就将SurfaceTexture给到VideoEncoder进行数据的编码,如果没有录制,就跳过该帧,这样就可以实现断点续录,即录制 —>暂停录制—>继续录制,而且这样录制出来的是一个整体的视频文件。
       这里就不贴出TexureMovieEncoder和VideoEncoderCore类的详细代码了。

       这样我们就完成了,摄像头数据的预览和视频的录制功能。然后呢,还有一些额外的功能。

Camera的手动对焦

不管是视频录制还是拍照的时候,对焦是非常重要的,如果没有对焦功能,那录制出来的视频的效果会非常的差,但是网上的很多讲解android的摄像头对焦功能的文章,其实并不准确,他们实现的功能其实有一些小问题的,主要是涉及到摄像头坐标系和屏幕显示坐标系的变化。手动聚焦,主要是点击屏幕 然后就调用Camera聚焦的相关函数 进行对焦。
     聚焦的主要代码如下,在CameraConroller类里面

   Camera.Parameters parameters = mCamera.getParameters();boolean supportFocus=true;boolean supportMetering=true;//不支持设置自定义聚焦,则使用自动聚焦,返回if (parameters.getMaxNumFocusAreas() <= 0) {supportFocus=false;}if (parameters.getMaxNumMeteringAreas() <= 0){supportMetering=false;}List<Camera.Area> areas = new ArrayList<Camera.Area>();List<Camera.Area> areas1 = new ArrayList<Camera.Area>();//再次进行转换point.x= (int) (((float)point.x)/ MyApplication.screenWidth*2000-1000);point.y= (int) (((float)point.y)/MyApplication.screenHeight*2000-1000);int left = point.x - 300;int top = point.y - 300;int right = point.x + 300;int bottom = point.y + 300;left = left < -1000 ? -1000 : left;top = top < -1000 ? -1000 : top;right = right > 1000 ? 1000 : right;bottom = bottom > 1000 ? 1000 : bottom;areas.add(new Camera.Area(new Rect(left, top, right, bottom), 100));areas1.add(new Camera.Area(new Rect(left, top, right, bottom), 100));if(supportFocus){parameters.setFocusAreas(areas);}if(supportMetering){parameters.setMeteringAreas(areas1);}try {mCamera.setParameters(parameters);// 部分手机 会出Exception(红米)mCamera.autoFocus(callback);} catch (Exception e) {e.printStackTrace();}

主要涉及到了一下坐标变换,因为大部分的手机的前置摄像头不支持对焦功能,所以我们不进行前置摄像头的对焦。

      

结语

      到这里的话,本篇文章的主要内容就已经结束了,再次回顾一下,我们其实本篇文章主要涉及到的内容有通过OpenGl预览视频,通过MediaCodec录制视频,以及一些其他的知识点。这里并没有讲解OpenGL的一些基础知识,比如顶点着色器等等,这部分如果要涉及到的话,也是一个很庞大的内容,所以就不会在系列文章中进行介绍了。大家不太清楚的话,请自行查询相关资料。
     本篇仅仅是一个开始,下一篇文章,我们就会在录制视频的时候通过opengl加上水印和美白效果。请大家持续关注。
     因为个人水平有限,难免有错误和不足之处,还望大家能包涵和提醒。谢谢啦!!!
其他
      项目的github地址
      VideoEditor-For-Android

Android视频编辑器(一)通过OpenGL预览、录制视频以及断点续录等相关推荐

  1. Android视频编辑器(二)预览、录制视频加上水印和美白磨皮效果

    前言      这是视频编辑器系列的第二篇文章,在上篇文章中,我们讲解了利用OpenGl和SurfaceView进行视频预览,MediaCodec和MeidaMuxer进行视频录制和断点续录.而这篇主 ...

  2. Android 音视频开发(三) -- Camera2 实现预览、拍照功能

    音视频 系列文章 Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音):AudioTrack播放音频 Android 音视频开发(二) – Camera1 实现预览.拍 ...

  3. Android 音视频开发(二) -- Camera1 实现预览、拍照功能

    音视频 系列文章 Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音):AudioTrack播放音频 Android 音视频开发(二) – Camera1 实现预览.拍 ...

  4. android 与后台实时视频,Android实时监控项目第四篇:后台线程发送预览帧视频数据...

    还记得上篇提到的setPreviewCallback(Camera.PreviewCallback cb)函数吗?我们在开始预览帧视频之前,调用的它,这里要注意其内部的Camera.PreviewCa ...

  5. Android Camera2 教程 · 第三章 · 预览

    Android Camera2 教程 · 第三章 · 预览 DarylGo关注 Android Camera 上一章<Camera2 开启相机>我们学习了如何开启和关闭相机,接下来我们来学 ...

  6. Android OpenGL添加水印并录制视频--抖音视频录制原理

    Android OpenGL添加水印并录制视频–抖音视频录制原理 简单的视频录制,我们可以使用MediaRecorder,具体示例可以参考Gitee: Camera2VideoJava 本文将介绍采集 ...

  7. 谷歌发布 Android 8.1 首个开发者预览版,优化内存效率

    今晨,谷歌推出了 Android 8.1 首个开发者预览版,此次升级涵盖了针对多个功能的提升优化,其中包含对 Android Go (设备运行内存小于等于 1 GB)和加速设备上对机器学习的全新神经网 ...

  8. Android摄像头:只拍摄SurfaceView预览界面特定区域内容(矩形框)---完整实现(原理:底层SurfaceView+上层绘制ImageView)

    [后注:]下载代码的注意,我的手机是4.3寸的屏,华为U9200.如果不能运行的请修改参数.看前文的第四条.Y的,省的说我传的代码不能用  最近一直在审视以前做过的东西,关于android摄像头预览, ...

  9. Android摄像头:只拍摄SurfaceView预览界面特定区域内容(矩形框)---完整(原理:底层SurfaceView+上层绘制ImageView)...

    Android摄像头:只拍摄SurfaceView预览界面特定区域内容(矩形框)---完整实现(原理:底层SurfaceView+上层绘制ImageView) 分类: Android开发 Androi ...

最新文章

  1. 3、kubernetes应用快速入门190625
  2. java虚拟机内存告警_Java虚拟机总结
  3. K8S部署Kuboard V3
  4. uniapp防抖操作
  5. 51单片机并行I/O口工作原理
  6. 机器学习笔记II: 决策树
  7. 使用 PDB 避免 Kubernetes 集群中断
  8. 关于人行acs对账不及时_记工记账新方法,不用本子不用笔,一个手机全搞定
  9. ImageUtils.java:图片处理工具类[裁剪/图片水印/文字水印/缩放补白/Base64加密解密]
  10. (并查集)~APTX4869(fzu 2233)
  11. 线性代数笔记【矩阵与线性方程组】
  12. 贴个图,讲下技巧如何进入9008模式:记一次救砖小米note3-9008刷小米note3-小米note3miui10降级miui9
  13. opensuse12.2 KDE 使用环境配置
  14. java get set写法_java get set方法的使用
  15. 马士兵网络安全大师班薪选课程
  16. Websocket和PHP Socket编程
  17. 2016弱校联盟十一专场10.2部分题解
  18. XYOJ1259: 找零钱(除法 余数)
  19. 调用U9系统里的新增杂收服务服务
  20. Stacked Queries(堆叠注入)

热门文章

  1. 多商户商城系统功能拆解07讲-平台端商品管理
  2. 人工智能技术在金融风控领域中的应用
  3. 提取wux-weapp的微信小程序筛选框:FilterBar
  4. 如果流浪地球,信息科学能做什么
  5. 1st anniversary for my first job
  6. orgchart组织机构图
  7. 【MySQL 8.0 OCP 1Z0-908认证考试】 题库精讲--第二讲mysql主从
  8. GBDT(MART)概念简介
  9. 《操作系统》2022年期末A卷
  10. 来自北京大学NOIP金牌选手yxc的常用代码模板2,图灵学院和享学课堂