基本概念
码率: 码率就是数据传输时单位时间传送的数据位数,单位一般是kbps即千位每秒。码率影响体积,与体积成正比:码率越大,体积越大;码率越小,体积越小。码率还影响清晰度,码率越高清晰度也就越高。码率超过一定数值,对图像的质量没有多大影响。
帧率: 帧率或者称FPS(Frames Per Second,帧/秒),是指每秒显示的图片数,或者GPU处理时每秒能够更新的次数。越高的帧速率可以得到更流畅逼真的画面。
分辨率: 分辨率是用于度量图像内数据量多少的一个参数,通常表示成ppi,影响图像大小,与图像大小成正比:分辨率越高,图像越大;分辨率越低,图像越小。
一、视频录制
Android视频录制的方式有两种,一种是用MediaRecorder类来进行录制,这种方式录制处理出来的是封装好的mp4视频格式。另外一种就是通过Android.hardware包下的Camera采集摄像头数据,数据放回到java层,开发者在java在对数据进行处理存储。接下来我们就讲一下第二种录制方式的过程。
1. 打开摄像头
打开摄像头时,相机默认打开的是后置摄像头,通过调用以下方法打开摄像头
mCamera = Camera.open();
要是想使用前置摄像头的话就要用先找到找到前置摄像头,然后再调用Camera.open()方法
Camera.CameraInfo info = new Camera.CameraInfo();
int numCameras = Camera.getNumberOfCameras();//拿到摄像头的数量
for (int i = 0; i < numCameras; i++) {
Camera.getCameraInfo(i, info);
//寻找前置摄像头
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
//找到并打开摄像头
mCamera = Camera.open(i);
break;
}
}
在找到摄像头之后接下来的一步也是比较重要的一步,设置摄像头预览的大小,因为手机的屏幕尺寸有很多,摄像头支持的尺寸也很多,所以一定要找到合适的尺寸才能拿到最佳的预览效果。设置摄像头尺寸大小的代码如下
//找到摄像头的默认尺寸
Camera.Size ppsfv = parms.getPreferredPreviewSizeForVideo();
if (ppsfv != null) {
Log.d(TAG, "Camera preferred preview size for video is " +
ppsfv.width + "x" + ppsfv.height);
}
// 判断摄像头支持的尺寸是否包含手机屏幕尺寸
for (Camera.Size size : parms.getSupportedPreviewSizes()) {
if (size.width == width && size.height == height) {
//设置屏幕尺寸
parms.setPreviewSize(width, height);
return;
}
}
// 找不到就是用默认的尺寸
Log.w(TAG, "Unable to set preview size to " + width + "x" + height);
if (ppsfv != null) {
parms.setPreviewSize(ppsfv.width, ppsfv.height);
}
//设置录像提示,参数为true时,屏幕左下角会有录像提示,这个对影响帧率有一定影响
parms.setRecordingHint(true);
mCamera.setParameters(parms);
在设置完尺寸之后要设置展示的角度
//拿到系统展示时的信息
Display display = ((WindowManager)getSystemService(WINDOW_SERVICE)).getDefaultDisplay();
// 这里这设置了竖屏和横屏时摄像头的展示角度
if(display.getRotation() == Surface.ROTATION_0) {
mCamera.setDisplayOrientation(90);
layout.setAspectRatio((double) mCameraPreviewHeight / mCameraPreviewWidth);
} else if(display.getRotation() == Surface.ROTATION_270) {
layout.setAspectRatio((double) mCameraPreviewHeight/ mCameraPreviewWidth);
mCamera.setDisplayOrientation(180);
} else {
// Set the preview aspect ratio.
layout.setAspectRatio((double) mCameraPreviewWidth / mCameraPreviewHeight);
}
2. 设置预览
摄像头预览一般是放在GLSurfaceView或者TextureView中,什么是GLSurfaceView呢?官方文档解释是:GLSurfaceView是使用专用曲面显示OpenGL渲染的SurfaceView实现。
它的功能主要如下
- 管理表面,这是可以组合到Android视图系统中的特殊内存。
- 管理EGL显示,使OpenGL可以渲染到表面中。
- 接受执行实际渲染的用户提供的Renderer对象。
- 在专用线程上进行渲染,以将渲染性能与UI线程分离。
- 支持按需渲染和连续渲染。
- 可选地包装,跟踪和/或错误检查渲染器的OpenGL调用。
GLSurfaceView类有一个Renderer接口,这个接口可以看成是GLSurfaceView的生命周期一个,它主要有三个回调函数:
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
}
@Override
public void onDrawFrame(GL10 gl) {
}
onSurfaceCreated() 这里面主要是一些初始化的操作,例如是 创建SurfaceTexture
onSurfaceChanged() 这里就是党surface大小发生变化时会回调
onDrawFrame() 这里是返回当前帧的数据,在这里我们可以对帧数据进行处理。
GLSurfaceView的初始化流程大致如下:
mGLView = (GLSurfaceView) findViewById(R.id.cameraPreview_surfaceView);
mGLView.setEGLContextClientVersion(2); // 选择EGL的版本
//创建Renderer
mRenderer = new CameraSurfaceRenderer(mCameraHandler, sVideoEncoder, outputFile);
//设置Renderer
mGLView.setRenderer(mRenderer);
//设置渲染的模式
mGLView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
Camera跟GLSurfaceView是怎么样关联起来的呢,重点是在Renderer里面,上面是一个自定义的Renderer,里面实现了刷新GLSurfaceView显示的画面,视频编码,以及将编码后 的视频输出到文件中。我们可以看一下CameraSurfaceRenderer的对应上面三个方法的代码
@Override
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
Log.d(TAG, "onSurfaceCreated");
/
//判断视频编码是否在进行
mRecordingEnabled = mVideoEncoder.isRecording();
if (mRecordingEnabled) {
mRecordingStatus = RECORDING_RESUMED;
} else {
mRecordingStatus = RECORDING_OFF;
}
//创建一个全屏着色器和texture一起渲染
mFullScreen = new FullFrameRect(
new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT));
mTextureId = mFullScreen.createTextureObject();
//创建一个表面纹理来对图像流数据进行处理
mSurfaceTexture = new SurfaceTexture(mTextureId);
//通知UI线程,这里创建了SurfaceTexture
mCameraHandler.sendMessage(mCameraHandler.obtainMessage(
CameraCaptureActivity.CameraHandler.MSG_SET_SURFACE_TEXTURE, mSurfaceTexture));
}
在主线程中的handleMessage
public void handleMessage(Message inputMessage) {
int what = inputMessage.what;
Log.d(TAG, "CameraHandler [" + this + "]: what=" + what);
CameraCaptureActivity activity = mWeakActivity.get();
if (activity == null) {
Log.w(TAG, "CameraHandler.handleMessage: activity is null");
return;
}
switch (what) {
case MSG_SET_SURFACE_TEXTURE:
activity.handleSetSurfaceTexture((SurfaceTexture) inputMessage.obj);
break;
default:
throw new RuntimeException("unknown msg " + what);
}
}
//摄像机设置预览
private void handleSetSurfaceTexture(SurfaceTexture st) {
st.setOnFrameAvailableListener(this);
try {
mCamera.setPreviewTexture(st);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
mCamera.startPreview();
}
在onSurfaceCreated()方法中,为什么要创建一个SurfaceTexture呢?它跟GLSurfaceView是什么关系呢?下面给大家讲一下
SurfaceView: 本质上是一个view,但是它有自己的Layer,它的显示也不受view的属性控制。如下图所示
SurfaceTexture: 它类似于中间件,不直接显示图像流数据,而是对图像流进行二次处理(类似于加滤镜,美颜)并转化成openGL的纹理,后可以交给GLSurfaceView显示,也可以交给TextureView显示
TextureView: 它不同于SurfaceView,不会在WMS中单独创建窗口,而是作为一个普通的view接受其管理,它可以将内容流直接投影到view中可以实现live preview功能,但要注意的是,TextureView必须在硬件加速的窗口中。
上面说完OnSurfaceCreated()方法,下面我们再看一下另外两个方法
@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
Log.d(TAG, "onSurfaceChanged " + width + "x" + height);
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
@Override
public void onDrawFrame(GL10 unused) {
if (VERBOSE) Log.d(TAG, "onDrawFrame tex=" + mTextureId);
boolean showBox = false;
//拿到最新的帧
mSurfaceTexture.updateTexImage();
//根据录制状态做相应的操作
if (mRecordingEnabled) {
switch (mRecordingStatus) {
case RECORDING_OFF:
Log.d(TAG, "START recording");
// start recording
mVideoEncoder.startRecording(new TextureMovieEncoder.EncoderConfig(
mOutputFile, 640, 480, 1000000, EGL14.eglGetCurrentContext()));
mRecordingStatus = RECORDING_ON;
break;
case RECORDING_RESUMED:
Log.d(TAG, "RESUME recording");
mVideoEncoder.updateSharedContext(EGL14.eglGetCurrentContext());
mRecordingStatus = RECORDING_ON;
break;
case RECORDING_ON:
// yay
break;
default:
throw new RuntimeException("unknown status " + mRecordingStatus);
}
} else {
switch (mRecordingStatus) {
case RECORDING_ON:
case RECORDING_RESUMED:
// stop recording
Log.d(TAG, "STOP recording");
mVideoEncoder.stopRecording();
mRecordingStatus = RECORDING_OFF;
break;
case RECORDING_OFF:
// yay
break;
default:
throw new RuntimeException("unknown status " + mRecordingStatus);
}
}
mVideoEncoder.setTextureId(mTextureId);
//告诉编码器,当前帧可用
mVideoEncoder.frameAvailable(mSurfaceTexture);
}
二、视频处理
上面讲完视频录制,接下将一下视频处理。视频处理的方式有很多种,例如人脸美白,添加滤镜,添加背景音乐等等,甚至是抖音前段时间挺火的一个添加游戏都可以。下面讲一下添加滤镜的方法。 上面说了,处理帧数据的方法是在onDrawFrame()中的,所以添加滤镜也会在此方法中添加。下面我们看看代码
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
@Override
public void onDrawFrame(GL10 unused) {
...
if (mIncomingWidth <= 0 || mIncomingHeight <= 0) {
//如果长宽不对则返回
Log.i(TAG, "Drawing before incoming texture size set; skipping");
return;
}
//在此更新滤镜
if (mCurrentFilter != mNewFilter) {
updateFilter();
}
if (mIncomingSizeUpdated) {
mFullScreen.getProgram().setTexSize(mIncomingWidth, mIncomingHeight);
mIncomingSizeUpdated = false;
}
// 将从SurfaceTexture得到的数据添加到GLSurfaceView上
mSurfaceTexture.getTransformMatrix(mSTMatrix);
mFullScreen.drawFrame(mTextureId, mSTMatrix);
···
}
updateFilter()的方法中做了什么呢,我们看一下
public void updateFilter() {
Texture2dProgram.ProgramType programType;
float[] kernel = null;
float colorAdj = 0.0f;
Log.d(TAG, "Updating filter to " + mNewFilter);
switch (mNewFilter) {
case CameraCaptureActivity.FILTER_NONE:
programType = Texture2dProgram.ProgramType.TEXTURE_EXT;
break;
case CameraCaptureActivity.FILTER_BLACK_WHITE:
programType = Texture2dProgram.ProgramType.TEXTURE_EXT_BW;
break;
case CameraCaptureActivity.FILTER_BLUR:
programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT;
kernel = new float[] {
1f/16f, 2f/16f, 1f/16f,
2f/16f, 4f/16f, 2f/16f,
1f/16f, 2f/16f, 1f/16f };
break;
case CameraCaptureActivity.FILTER_SHARPEN:
programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT;
kernel = new float[] {
0f, -1f, 0f,
-1f, 5f, -1f,
0f, -1f, 0f };
break;
case CameraCaptureActivity.FILTER_EDGE_DETECT:
programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT;
kernel = new float[] {
-1f, -1f, -1f,
-1f, 8f, -1f,
-1f, -1f, -1f };
break;
case CameraCaptureActivity.FILTER_EMBOSS:
programType = Texture2dProgram.ProgramType.TEXTURE_EXT_FILT;
kernel = new float[] {
2f, 0f, 0f,
0f, -1f, 0f,
0f, 0f, -1f };
colorAdj = 0.5f;
break;
default:
throw new RuntimeException("Unknown filter mode " + mNewFilter);
}
if (programType != mFullScreen.getProgram().getProgramType()) {
mFullScreen.changeProgram(new Texture2dProgram(programType));
mIncomingSizeUpdated = true;
}
if (kernel != null) {
mFullScreen.getProgram().setKernel(kernel, colorAdj);
}
mCurrentFilter = mNewFilter;
}
代码中,设置了不同滤镜的kernel值,然后在setKernel()方法中,将对应值从到openGL对应方法中进行处理,这些方法如下
GLES20.glUniform1fv(muKernelLoc, KERNEL_SIZE, mKernel, 0);
GLES20.glUniform2fv(muTexOffsetLoc, KERNEL_SIZE, mTexOffset, 0);
GLES20.glUniform1f(muColorAdjustLoc, mColorAdjust);
这些方法是更改OpenGL Uniform变量的值,从而显示不同的颜色效果。 (对于Uniform变量的定义及使用可以看博客:OpenGL 图形库使用(四) —— Uniform及更多属性 )
三、编码以及输出
常用的视频编码有
- MPEG系列 :MPEG1,MPEG2,MPEG4等等
- H.26X系列:现在常用H.264,因为在同等图像质量下,采用H.264技术压缩后的数据量只有MPEG2的1/8,MPEG4的1/3。H.264的最大优势是具有很高的数据压缩率,可以有效减少用户带宽的占用。
常见的视频格式有mp4、3GP、avi、rmvb、apple quicktime的mov、以及各大播放器自己封装的格式
mVideoEncoder.startRecording(new TextureMovieEncoder.EncoderConfig( mOutputFile, 640, 480, 1000000, EGL14.eglGetCurrentContext()));
这个方法的最终调用是下面这个方法
private static final String MIME_TYPE = "video/avc"; // H.264 Advanced Video Coding
public VideoEncoderCore(int width, int height, int bitRate, File outputFile)
throws IOException {
mBufferInfo = new MediaCodec.BufferInfo();
MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
//设置一些码率,帧率,以及取I帧的间隔时间等
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
if (VERBOSE) Log.d(TAG, "format: " + format);
//新建编码器以及创建一个Surface进行数据的输入
mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mInputSurface = mEncoder.createInputSurface();
mEncoder.start();
//封装成mp4输出到文件中
mMuxer = new MediaMuxer(outputFile.toString(),
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
mTrackIndex = -1;
mMuxerStarted = false;
}
在上面的代码中编码器创建了一个surface来作为数据源的输入,因为surface保存了当前窗口的像素数据。同时作为输入的surface还要求带有openGL ES渲染接口。
总结
上面的demo是通过Camera + OpengGL + MediaCodec +进MediaMuxer行视频录制、处理、编码以及封装,这样做的好处就是能在录制视频的过程中进行处理,也可以将视频用自己想要的格式进行编码封装。除此之外项目中的代码均来自google工程师的grafika项目,其项目还有很多值得大家学习的点,大家可以多多阅读和实践。
OpenGL的学习网站
google 工程师的grafika项目
滤镜相关:
GPUImage
MagicCamera