若是你有学Android 音视频,相机开发的想法,那么这篇文章能够做为一篇不错的参考文章。固然本文为付费文章,收费10元,若是对你有用,文末赞扬缴费便可。若是没有学习音视频,相机的欲望,赶快走,赶快走,不要有一丝停留,由于这篇文章确实枯燥无味且毫无快感可言。若是不知道我讲的是啥,先到Github项目:AndroidCamera去看下效果就知道了。html
请考虑3s赶快决定去留。java
3……android
2……git
1……github
不走,我再扯两句:canvas
这篇是在学习相机音视频开发的时候写的一篇总结。因为涉及的知识点比较多,因此其中部分知识点仅起引导做用。数组
ok,枯燥无味正式开始:缓存
这篇文章计划写的内容覆盖面是很普遍的,涵盖相机开发的大部分知识,并且我对本身写做要求:内容尽可能精炼,不能泛泛而谈。因此时间上来讲很紧凑了。固然,若是文章各方面你们有看不顺眼的地方,但愿你们帮忙指出批评,必定虚心接受,积极改正。若是从此有机会见面,请您喝茶。bash
固然,这个对大部分人来讲都是没什么问题的,可是该篇文章还得照顾大部分初次接触Camera开发的小伙伴,因此请允许我在此多啰嗦一下,若是你有接触过Camera的开发,此部分能够跳过,直接看下一部分。微信
说下Camera的操做步骤,后面给出实例,请结合代码理解分析:
若是你初次开发相机,请按照上面的步骤观看下面代码,若是你已经知道了,请直接过滤掉此基础部分。若是想了解更多预览方式,你能够看个人另外一篇文章经过SurfaceView,TextureView,GlSurfaceView显示相机预览。
public class CameraSurfaceViewShowActivity extends AppCompatActivity implements SurfaceHolder.Callback {
@BindView(R.id.mSurface)
SurfaceView mSurfaceView;
public SurfaceHolder mHolder;
private Camera mCamera;
private Camera.Parameters mParameters;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_base_camera);
ButterKnife.bind(this);
mHolder = mSurfaceView.getHolder();
mHolder.addCallback(this);
mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
// Open the Camera in preview mode
mCamera = Camera.open(0);
mCamera.setDisplayOrientation(90);
mCamera.setPreviewDisplay(holder);
mCamera.startPreview();
} catch (IOException e) {
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
mCamera.autoFocus(new Camera.AutoFocusCallback() {
@Override
public void onAutoFocus(boolean success, Camera camera) {
if (success) {
mParameters = mCamera.getParameters();
mParameters.setPictureFormat(PixelFormat.JPEG); //图片输出格式
// mParameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);//预览持续发光
mParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);//持续对焦模式
mCamera.setParameters(mParameters);
mCamera.startPreview();
mCamera.cancelAutoFocus();
}
}
});
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (mCamera != null) {
mCamera.stopPreview();
mCamera.release();
mCamera = null;
}
}
@OnClick(R.id.btn_change)
public void onViewClicked() {
// PropertyValuesHolder valuesHolder2 = PropertyValuesHolder.ofFloat("rotationX", 0.0f, 360.0f, 0.0F);
PropertyValuesHolder valuesHolder = PropertyValuesHolder.ofFloat("rotationY", 0.0f, 360.0f, 0.0F);
PropertyValuesHolder valuesHolder1 = PropertyValuesHolder.ofFloat("scaleX", 1.0f, 0.5f,1.0f);
PropertyValuesHolder valuesHolder3 = PropertyValuesHolder.ofFloat("scaleY", 1.0f, 0.5f,1.0f);
ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(mSurfaceView, valuesHolder,valuesHolder1,valuesHolder3);
objectAnimator.setDuration(5000).start();
}
}
复制代码
固然,为了使效果好看一点点,我添加了一丢丢效果,效果以下:
好了,到这里为止,咱们的简单Camera预览结束。
OpenGL ES (OpenGL for Embedded Systems) 是OpenGl的子集,针对手机、PDA和游戏主机等嵌入式设备而设计。(我不会偷偷告诉你我是百度滴)
关于OpenGl ES如何绘制一个简单基本图形,下面会作一个简单的讲解,若是你想对OpenGL ES有更深层次的了解,能够看下我写的关于一篇OpenGL绘制简单三角形的文章Android openGl开发详解(一)——简单图形的基本绘制,
首先咱们必须明确咱们要作的是将相机数据显示到设备屏幕上,全部的操做都是为此目的服务的。因此咱们必需要了解OpenGl ES是如何进行渲染的。(若是下面提到的术语你没有概念,或者模棱两可,请看再看一遍Android openGl开发详解(一)——简单图形的基本绘制) 下面是基本步骤:
上面步骤基本能够将Camera的预览数据经过OpenGl ES的方式显示到了GlSurfaceView上。固然,咱们先来看下效果图,再给出源码部分。让你们看一下效果(由于时间缘由,请原谅我拿了以前的图)
这部分源码会在项目中给出,同时在经过SurfaceView,TextureView,GlSurfaceView显示相机预览也有给出,因此,在这里就不贴源码了。
What?EGL?什么东西?可能不少初学的还不是特别了解EGL是什么?若是你使用过OpenGL ES进行渲染,不知道你有没有想过谁为OpenGl ES提供渲染界面?换个方式问?大家知道OpenGL ES渲染的数据到底去哪了么?(请原谅我问得这么生硬) 固然,到GLSurfaceView,GlSurfaceView为其提供了渲染界面,这还用说!
其实OpenGL ES的渲染是在独立线程中,他是经过EGL接口来实现和硬件设备的链接。EGL为OpenGl EG 提供上下文及窗口管理,注意:OpenGl ES全部的命令必须在上下文中进行。因此EGL是OpenGL ES开发必不可少须要了解的知识。可是为何咱们上面的开发中都没有用到EGL呢?这里说明下:由于在Android开发环境中,GlSurfaceView中已经帮咱们配置好了EGL了。 固然,EGL的做用及流程图从官方偷来给你们看一波:
关于EGL的知识内容不少,不想增长本文篇幅,从新写一篇博客专门介绍EGL,有兴趣点这里Android 自定义相机开发(三) —— 了解下EGL。
OpenGl 中的纹理能够用来表示图像,照片,视频画面等数据,在视频渲染中,咱们只须要处理二维的纹理,每一个二维的纹理都由许多小的纹理元素组成,咱们能够将其当作小块的数据。咱们能够简单将纹理理解成电视墙瓷砖,咱们要作一面电视墙,须要由多个小瓷砖磡成,最终成型的才是完美的电视墙。我暂时是这么理解滴。使用纹理,最直接的方式是直接从给一个图像文件加载数据。这里咱们得稍微注意下,OpenGl的二维纹理坐标和咱们的手机屏幕坐标仍是有必定的区别。
OpenGl的纹理坐标的原点是在左下角,而计算机的纹理坐标在左上角。尤为是咱们在添加贴纸的时候须要注意下y值的转换。这里顺便说下OpenGl ES绘制相机数据的时候纹理坐标的变换问题,下次若是使用OpenGl 处理相机数据遇到镜像或者上下颠倒能够对照下图片上所说的规则:
下面咱们来说解下OpenGl纹理使用的步骤:
private int createTextureID() {
int[] tex = new int[1];
//第一个参数表示建立几个纹理对象,并将建立好的纹理对象放置到第二个参数中去,第二个参数里面存放的是纹理ID(纹理索引),第三个偏移值,一般填0便可。
GLES20.glGenTextures(1, tex, 0);
//纹理绑定
GLES20.glBindTexture(GL_TEXTURE_2D, tex[0]);
//设置缩小过滤方式为GL_LINEAR(双线性过滤,目前最主要的过滤方式),固然还有GL_NEAREST(容易出现锯齿效果)和MIP贴图(占用更多内存)
GLES20.glTexParameterf(GL_TEXTURE_2D,
GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
//设置放大过滤为GL_LINEAR,同上
GLES20.glTexParameterf(GL_TEXTURE_2D,
GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
//设置纹理的S方向范围,控制纹理贴纸的范围在(0,1)以内,大于1的设置为1,小于0的设置为0。
GLES20.glTexParameterf(GL_TEXTURE_2D,
GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
//设置纹理的T方向范围,同上
GLES20.glTexParameterf(GL_TEXTURE_2D,
GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
//解除纹理绑定
GLES20.glBindTexture(GL_TEXTURE_2D, 0);
return tex[0];
}
复制代码
这里咱们稍微提一下,若是是相机数据处理,咱们使用GLES11Ext.GL_TEXTURE_EXTERNAL_OES,若是是处理贴纸图片,咱们使用GLES20.GL_TEXTURE_2D。由于相机输出的数据类型是YUV420P格式的,使用GLES11Ext.GL_TEXTURE_EXTERNAL_OES扩展纹理能够实现自动将YUV420P转RGB,咱们就不须要在存储成MP4的时候再进行数据转换了。
void glTexImage2D( int target,
int level,
int internalformat,
int width,
int height,
int border,
int format,
int type,
java.nio.Buffer pixels);
复制代码
简单参数说明 : target:常数GL_TEXTURE_2D。 level: 表示多级分辨率的纹理图像的级数,若只有一种分辨率,则level设为0。 internalformat:表示用哪些颜色用于调整和混合,一般用GLES20.GL_RGBA。 border:字面意思理解应该是边界,边框的意思,一般写0. width/height:纹理的宽/高。 format/type :一个是纹理映射格式(一般填写GLES20.GL_RGBA),一个是数据类型(一般填写GLES20.GL_UNSIGNED_BYTE)。 pixels:纹理图像数据。
固然,Android中最经常使用是使用方式是直接经过texImage2D()方法能够直接将Bitmap数据做为参数传入,方法以下:
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0);
复制代码
上面咱们将相机的预览显示讲完了,接下里咱们讲如何将录制视频。就目前来讲,Android的录制方式就要有下面三中:
MediaCodec官方文档地址 MediaCodec是一个多媒体编解码处理类,可用于访问Android底层的多媒体编解码器。例如,编码器/解码器组件。它是Android底层多媒体支持基础架构的一部分(一般与MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, 以及AudioTrack一块儿使用)。请原谅我后面那一段是从官网搬过来的,知道它是用来处理音视频的That's enough.
MediaCodec究竟是如何将数据进行处理生成.mp4文件的呢?咱们先看下图(在官方图片上进行了部分改动和标记): 既然上面咱们提到MediaCodec是一个编码器处理类,从图上看咱们能够知道,他就是2的输入的数据进行处理,而后输出到3中去保存。每一个编码器都包含一组输入和输出缓存,中间的两条从Codec出发又返回Codec的虚线就表明两组缓存。当编码器启动后,两组缓存便存在。由编码器发送空缓存给输入区(提供数据区),输入区将输入缓存填充满,再返回给编码器进行编码,编码完成以后将数据进行输出,输出以后将缓冲区返回给编码器。
若是你是个吃货你能够这样理解:Codec是榨汁机,在榨汁以前准备两个杯子。一个杯子(输入缓存)用来装苹果一直往榨汁机里面倒,倒完了继续回去装苹果。另外一个杯子(输出缓存)用来装榨出来的苹果汁,不管你将果汁放到哪里去(放一个大瓶子里面或者喝掉),杯子空了你就还回来继续接果汁,知道将榨汁机里面的果汁接完为止。
对,就这么简单,八九不离十的样子,反正我也不知道我说得对不对?
: 没有设置以上前三个属性你能够能会出现如下错误:
Process: com.aserbao.androidcustomcamera, PID: 18501
android.media.MediaCodec$CodecException: Error 0x80001001
at android.media.MediaCodec.native_configure(Native Method)
at android.media.MediaCodec.configure(MediaCodec.java:1909)
……
复制代码
舒适提示:下面实例是经过直接在mediacodec的输入surface上进行绘制,因此不会有上述输入队列的操做。关于MediaCodec的不少细节,官方已经讲得很详细了,这里不过多阐述。
官方地址:MediaCodec MediaCodec中文文档 MediaCodec同步缓存处理方式(来自官方实例,还有异步缓存处理及同步数组的处理方式这里不作多讲解,若是有兴趣到官方查看),配合上面的步骤看会理解更多,若是仍是不明白建议查看下面实例以后再回头来看步骤和实例:
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
MediaFormat outputFormat = codec.getOutputFormat(); // option B
codec.start();
for (;;) {
int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(…);
// fill inputBuffer with valid data
…
codec.queueInputBuffer(inputBufferId, …);
}
int outputBufferId = codec.dequeueOutputBuffer(…);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is identical to outputFormat
// outputBuffer is ready to be processed or rendered.
…
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
outputFormat = codec.getOutputFormat(); // option B
}
}
codec.stop();
codec.release();
复制代码
若是你以前没有使用过MediaCodec录制过视频,这个实例建议你看一下,若是你很是了解了,请跳过。效果图以下:
可贵给下代码,固然,项目中会有更多关于MediaCodec的实例,最后会给出:
public class PrimaryMediaCodecActivity extends BaseActivity {
private static final String TAG = "PrimaryMediaCodecActivi";
private static final String MIME_TYPE = "video/avc";
private static final int WIDTH = 1280;
private static final int HEIGHT = 720;
private static final int BIT_RATE = 4000000;
private static final int FRAMES_PER_SECOND = 4;
private static final int IFRAME_INTERVAL = 5;
private static final int NUM_FRAMES = 4 * 100;
private static final int START_RECORDING = 0;
private static final int STOP_RECORDING = 1;
@BindView(R.id.btn_recording)
Button mBtnRecording;
@BindView(R.id.btn_watch)
Button mBtnWatch;
@BindView(R.id.primary_mc_tv)
TextView mPrimaryMcTv;
public MediaCodec.BufferInfo mBufferInfo;
public MediaCodec mEncoder;
@BindView(R.id.primary_vv)
VideoView mPrimaryVv;
private Surface mInputSurface;
public MediaMuxer mMuxer;
private boolean mMuxerStarted;
private int mTrackIndex;
private long mFakePts;
private boolean isRecording;
private int cuurFrame = 0;
private MyHanlder mMyHanlder = new MyHanlder(this);
public File mOutputFile;
@OnClick({R.id.btn_recording, R.id.btn_watch})
public void onViewClicked(View view) {
switch (view.getId()) {
case R.id.btn_recording:
if (mBtnRecording.getText().equals("开始录制")) {
try {
mOutputFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), System.currentTimeMillis() + ".mp4");
startRecording(mOutputFile);
mPrimaryMcTv.setText("文件保存路径为:" + mOutputFile.toString());
mBtnRecording.setText("中止录制");
isRecording = true;
} catch (IOException e) {
e.printStackTrace();
mBtnRecording.setText("出现异常了,请查明缘由");
}
} else if (mBtnRecording.getText().equals("中止录制")) {
mBtnRecording.setText("开始录制");
stopRecording();
}
break;
case R.id.btn_watch:
String absolutePath = mOutputFile.getAbsolutePath();
if (!TextUtils.isEmpty(absolutePath)) {
if(mBtnWatch.getText().equals("查看视频")) {
mBtnWatch.setText("删除视频");
mPrimaryVv.setVideoPath(absolutePath);
mPrimaryVv.start();
}else if(mBtnWatch.getText().equals("删除视频")){
if (mOutputFile.exists()){
mOutputFile.delete();
mBtnWatch.setText("查看视频");
}
}
}else{
Toast.makeText(this, "请先录制", Toast.LENGTH_SHORT).show();
}
break;
}
}
private static class MyHanlder extends Handler {
private WeakReference<PrimaryMediaCodecActivity> mPrimaryMediaCodecActivityWeakReference;
public MyHanlder(PrimaryMediaCodecActivity activity) {
mPrimaryMediaCodecActivityWeakReference = new WeakReference<PrimaryMediaCodecActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
PrimaryMediaCodecActivity activity = mPrimaryMediaCodecActivityWeakReference.get();
if (activity != null) {
switch (msg.what) {
case START_RECORDING:
activity.drainEncoder(false);
activity.generateFrame(activity.cuurFrame);
Log.e(TAG, "handleMessage: " + activity.cuurFrame);
if (activity.cuurFrame < NUM_FRAMES) {
this.sendEmptyMessage(START_RECORDING);
} else {
activity.drainEncoder(true);
activity.mBtnRecording.setText("开始录制");
activity.releaseEncoder();
}
activity.cuurFrame++;
break;
case STOP_RECORDING:
Log.e(TAG, "handleMessage: STOP_RECORDING");
activity.drainEncoder(true);
activity.mBtnRecording.setText("开始录制");
activity.releaseEncoder();
break;
}
}
}
}
@Override
protected int setLayoutId() {
return R.layout.activity_primary_media_codec;
}
private void startRecording(File outputFile) throws IOException {
cuurFrame = 0;
prepareEncoder(outputFile);
mMyHanlder.sendEmptyMessage(START_RECORDING);
}
private void stopRecording() {
mMyHanlder.removeMessages(START_RECORDING);
mMyHanlder.sendEmptyMessage(STOP_RECORDING);
}
/**
* 准备视频编码器,muxer,和一个输入表面。
*/
private void prepareEncoder(File outputFile) throws IOException {
mBufferInfo = new MediaCodec.BufferInfo();
MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, WIDTH, HEIGHT);
//1. 设置一些属性。没有指定其中的一些可能会致使MediaCodec.configure()调用抛出一个无用的异常。
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);//比特率(比特率越高,音视频质量越高,编码文件越大)
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAMES_PER_SECOND);//设置帧速
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);//设置关键帧间隔时间
//2.建立一个MediaCodec编码器,并配置格式。获取一个咱们能够用于输入的表面,并将其封装处处理EGL工做的类中。
mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mInputSurface = mEncoder.createInputSurface();
mEncoder.start();
//3. 建立一个MediaMuxer。咱们不能在这里添加视频跟踪和开始合成,由于咱们的MediaFormat里面没有缓冲数据。
// 只有在编码器开始处理数据后才能从编码器得到这些数据。咱们实际上对多路复用音频没有兴趣。咱们只是想要
// 将从MediaCodec得到的原始H.264基本流转换为.mp4文件。
mMuxer = new MediaMuxer(outputFile.toString(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
mMuxerStarted = false;
mTrackIndex = -1;
}
private void drainEncoder(boolean endOfStream) {
final int TIMEOUT_USEC = 10000;
if (endOfStream) {
mEncoder.signalEndOfInputStream();//在输入信号end-of-stream。至关于提交一个空缓冲区。视频编码完结
}
ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers();
while (true) {
int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {//没有能够输出的数据使用时
if (!endOfStream) {
break; // out of while
}
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
//输出缓冲区已经更改,客户端必须引用新的
encoderOutputBuffers = mEncoder.getOutputBuffers();
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
//输出格式发生了变化,后续数据将使用新的数据格式。
if (mMuxerStarted) {
throw new RuntimeException("format changed twice");
}
MediaFormat newFormat = mEncoder.getOutputFormat();
mTrackIndex = mMuxer.addTrack(newFormat);
mMuxer.start();
mMuxerStarted = true;
} else if (encoderStatus < 0) {
} else {
ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
if (encodedData == null) {
throw new RuntimeException("encoderOutputBuffer " + encoderStatus +
" was null");
}
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
//当咱们获得的时候,编解码器的配置数据被拉出来,并给了muxer。这时候能够忽略。不作处理
mBufferInfo.size = 0;
}
if (mBufferInfo.size != 0) {
if (!mMuxerStarted) {
throw new RuntimeException("muxer hasn't started");
}
//调整ByteBuffer值以匹配BufferInfo。
encodedData.position(mBufferInfo.offset);
encodedData.limit(mBufferInfo.offset + mBufferInfo.size);
mBufferInfo.presentationTimeUs = mFakePts;
mFakePts += 1000000L / FRAMES_PER_SECOND;
mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
}
mEncoder.releaseOutputBuffer(encoderStatus, false);
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
if (!endOfStream) {
Log.e(TAG, "意外结束");
} else {
Log.e(TAG, "正常结束");
}
isRecording = false;
break;
}
}
}
}
private void generateFrame(int frameNum) {
Canvas canvas = mInputSurface.lockCanvas(null);
try {
int width = canvas.getWidth();
int height = canvas.getHeight();
float sliceWidth = width / 8;
Paint paint = new Paint();
for (int i = 0; i < 8; i++) {
int color = 0xff000000;
if ((i & 0x01) != 0) {
color |= 0x00ff0000;
}
if ((i & 0x02) != 0) {
color |= 0x0000ff00;
}
if ((i & 0x04) != 0) {
color |= 0x000000ff;
}
paint.setColor(color);
canvas.drawRect(sliceWidth * i, 0, sliceWidth * (i + 1), height, paint);
}
paint.setColor(0x80808080);
float sliceHeight = height / 8;
int frameMod = frameNum % 8;
canvas.drawRect(0, sliceHeight * frameMod, width, sliceHeight * (frameMod + 1), paint);
paint.setTextSize(50);
paint.setColor(0xffffffff);
for (int i = 0; i < 8; i++) {
if(i % 2 == 0){
canvas.drawText("aserbao", i * sliceWidth, sliceHeight * (frameMod + 1), paint);
}else{
canvas.drawText("aserbao", i * sliceWidth, sliceHeight * frameMod, paint);
}
}
} finally {
mInputSurface.unlockCanvasAndPost(canvas);
}
}
private void releaseEncoder() {
if (mEncoder != null) {
mEncoder.stop();
mEncoder.release();
mEncoder = null;
}
if (mInputSurface != null) {
mInputSurface.release();
mInputSurface = null;
}
if (mMuxer != null) {
mMuxer.stop();
mMuxer.release();
mMuxer = null;
}
}
}
复制代码
Android下的音频录制主要分两种:
虽然咱们这里只讲第一种,在这里仍是讲下优缺点:
使用AudioRecord录音 优势:能够对语音进行实时处理,好比变音,降噪,增益……,灵活性比较大。 缺点:就是输出的格式是PCM,你录制出来不能用播放器播放,须要用到AudioTrack来处理。
使用 MediaRecorder: 优势:高度封装,操做简单,支持编码,压缩,少许的音频格式文件,灵活性差。 缺点:无法对音频进行实时处理。
/**
*@param audioSource 音频采集的输入源,经常使用的值包括:DEFAULT(默认),VOICE_RECOGNITION(用于语音识别,等同于DEFAULT),MIC(由手机麦克风输入)等等,一般咱们使用MIC
*@param sampleRateInHz 采样率,注意,目前44100Hz是惟一能够保证兼容全部Android手机的采样率。
*@param channelConfig 这个参数是用来配置“数据位宽”的,可选的值也是以常量的形式定义在 AudioFormat 类中,经常使用的是 ENCODING_PCM_16BIT(16bit),ENCODING_PCM_8BIT(8bit),注意,16BIT是能够保证兼容全部Android手机的。
*@param bufferSizeInBytes 它配置的是 AudioRecord 内部的音频缓冲区的大小,该缓冲区的值不能低于一帧“音频帧”(Frame)的大小,一帧音频帧的大小计算以下:int size = 采样率 x 位宽 x 采样时间(取值2.5ms ~ 120ms) x 通道数.
*/
public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,
int bufferSizeInBytes)
复制代码
上面提到的采样时间这里说一下,每一个手机厂商设置的可能都不同,咱们设置的采样时间越短,声音的延时就越小。咱们能够经过getMinBufferSize()方法来肯定咱们须要输入的bufferSizeInBytes值,官方说明是说小于getMinBufferSize()的值就会初始化失败。
AudioRecord.startRecording();//开始采集
AudioRecord.stop();//中止采集
……
AudioRecord.read(byte[] audioData, int offsetInBytes, int sizeInBytes);//读取数据
复制代码
关于AudioRecord录制的音频的例子就不在这里贴出来了,以后项目中会接入录音变音,降噪,增益等功能。都会在代码中给出。
前面讲到了视频和音频的录制,那么如何将他们混合呢? 一样就我所知目前有两种方法:
MediaMuxer官方文档地址 MediaMuxer最多仅支持一个视频track,一个音频的track.若是你想作混音怎么办?用ffmpeg进行混合吧。(目前还在研究FFMPEG这一块,欢迎你们一块来讨论。哈哈哈……),目前MediaMuxer支持MP四、Webm和3GP文件做为输出。视频编码的主要格式用H.264(AVC),音频用AAC编码(关于音频你用其余的在IOS端压根就识别不出来,我就踩过这个坑!)。
官方实例:
MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
// More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat()
// or MediaExtractor.getTrackFormat().
MediaFormat audioFormat = new MediaFormat(...);
MediaFormat videoFormat = new MediaFormat(...);
int audioTrackIndex = muxer.addTrack(audioFormat);
int videoTrackIndex = muxer.addTrack(videoFormat);
ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);
boolean finished = false;
BufferInfo bufferInfo = new BufferInfo();
muxer.start();
while(!finished) {
// getInputBuffer() will fill the inputBuffer with one frame of encoded
// sample from either MediaCodec or MediaExtractor, set isAudioSample to
// true when the sample is audio data, set up all the fields of bufferInfo,
// and return true if there are no more samples.
finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo);
if (!finished) {
int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;
muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);
}
};
muxer.stop();
muxer.release();
复制代码
好了,综上所述知识,已经实现了从预览到录制完成的讲解。
多段视频合成这里提供两种方案:
下面咱们主要来说下两种方式的使用,第一种咱们讲思路,第二种讲如何使用?第三个暂时不讲。
只讲思路及实现步骤,代码在项目中以后给出,目前我还没写进去,原谅我最近偷懒一波。大致思路以下:
差很少就是这样滴,由于这个我是看别人是这么作的,我偷懒用了mp4parser,因此仅能给个位提供思路了,从此有时间再了解下。
上面有提到我如今使用的就是这个,他是开源滴,来来来,点这里给大家传送门。虽然上面对于使用方法都说得很清楚了,虽然个人项目中也会有源代码,可是我仍是要把这部分写出来:
/**
* 对Mp4文件集合进行追加合并(按照顺序一个一个拼接起来)
* @param mp4PathList [输入]Mp4文件路径的集合(支持m4a)(不支持wav)
* @param outPutPath [输出]结果文件所有名称包含后缀(好比.mp4)
* @throws IOException 格式不支持等状况抛出异常
*/
public String mergeVideo(List<String> paths, String filePath) {
long begin = System.currentTimeMillis();
List<Movie> movies = new ArrayList<>();
String filePath = "";
if(paths.size() == 1){
return paths.get(0);
}
try {
for (int i = 0; i < paths.size(); i++) {
if(paths != null && paths.get(i) != null) {
Movie movie = MovieCreator.build(paths.get(i));//视频消息实体类
movies.add(movie);
}
}
List<Track> videoTracks = new ArrayList<>();
List<Track> audioTracks = new ArrayList<>();
for (Movie movie : movies) {
for (Track track : movie.getTracks()) {
if ("vide".equals(track.getHandler())) {
videoTracks.add(track);//从Movie对象中取出视频通道
}
if ("soun".equals(track.getHandler())) {
audioTracks.add(track);//Movie对象中获得的音频轨道
}
}
}
Movie result = new Movie();
if (videoTracks.size() > 0) {
// 将全部视频通道追加合并
result.addTrack(new AppendTrack(videoTracks.toArray(new Track[videoTracks.size()])));
}
if (audioTracks.size() > 0) {
// 将全部音频通道追加合并
result.addTrack(new AppendTrack(audioTracks.toArray(new Track[audioTracks.size()])));
}
Container container = new DefaultMp4Builder().build(result);
filePath = getRecorderPath();
FileChannel fc = new RandomAccessFile(String.format(filePath), "rw").getChannel();//合成并输出到指定文件中
container.writeContainer(fc);
fc.close();
} catch (Exception e) {
e.printStackTrace();
return paths.get(0);
}
long end = System.currentTimeMillis();
return filePath;
}
复制代码
先看下咱们要实现什么功能,以下:
简单分析下,咱们如今须要将整个视频的部分帧拿出在下面显示出来,而且添加上面的动态贴纸显示。
Android平台下主要有两种拿视频帧的方法:
MediaMetadataRetriever mediaMetadata = new MediaMetadataRetriever();
mediaMetadata.setDataSource(mContext, Uri.parse(mInputVideoPath));
mVideoRotation = mediaMetadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
mVideoWidth = Integer.parseInt(mediaMetadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
mVideoHeight = Integer.parseInt(mediaMetadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
mVideoDuration = Integer.parseInt(mediaMetadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
int frameTime = 1000 * 1000;//帧间隔
int frame = mVideoDuration * 1000 / frameTime;//帧总数
mAsyncTask = new AsyncTask<Void, Void, Boolean>() {
@Override
protected Boolean doInBackground(Void... params) {
myHandler.sendEmptyMessage(ClEAR_BITMAP);
for (int x = 0; x < frame; x++) {
//拿到帧图像
Bitmap bitmap = mediaMetadata.getFrameAtTime(frameTime * x, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
}
mediaMetadata.release();//释放别忘记
return true;
}
@Override
protected void onPostExecute(Boolean result) {
myHandler.sendEmptyMessage(SUBMIT);//全部帧拿完了
}
复制代码
拿完全部帧,好了,好了,下一个话题。
看到上面的等撩了么?
先说下为何要将Gif图进行分解操做,由于我在添加动态贴纸的时候是在OpenGl Es的OnDraw方法中经过每次动态修改纹理来达到动态贴纸的效果的。因此必需要将Gif图分解成每帧的形式。怎么将Gif图解析出来呢?Google出来一个工具类GifDecoder!固然,后面我去找了Glide的源码,分析其内部Gif图的显示流程,发现其实原理是同样的。Glide StandardGifDecoder固然,关于Glide的Gif图解析内容仍是蛮多的,这里不作分析(没有太过深刻研究),从此有时间看能不能写一篇文章专门分析。
固然,关于GifDecoder的代码,这里就不贴出来了,会在项目中给出!固然,如今项目中尚未,由于文章写完,我这个项目确定写不完的,最近事太多,忙着开产品讨论会,尽可能在讨论以前5月25号以前能将项目写完。因此这里还请各位多谅解下。
参考文章:1.FFmpeg官网2. 官方项目地址github3. [FFmpeg]ffmpeg各种参数说明与使用示例
若是你有接触到音视频开发这一块,确定据说过FFmpeg这个庞然大物。为何说庞然大物?由于我最近在学习这个,越学越以为本身无知。哎,很少说了,我要加班恶补FFMpeg了。
FFmpeg是一个自由软件,能够运行音频和视频多种格式的录影、转换、流功能[2],包含了libavcodec——这是一个用于多个项目中音频和视频的解码器库,以及libavformat——一个音频与视频格式转换库。(来源wiki),简单点能够将FFmpeg理解成音视频处理软件。能够经过输入命令的方式对视频进行任何操做。没错,是任何(一点都不夸张)!
对于FFmpeg,我只想说,我仍是个小白,但愿各位大大不要在这个问题上抓着我严刑拷打。众所周知的,FFmpge是C实现的,因此生成so文件再调用吧!怎么办?我不会呀?这时候就要去找前人种的树了。这里给一个我参考使用的FFmpeg文件库导入EpMedia,哎,乘凉,感谢这位大大!
固然,若是想了解下FFmpeg的编译,能够看下Android最简单的基于FFmpeg的例子(一)---编译FFmpeg类库](www.ihubin.com/blog/androi…)
如何使用?
//请记住这个cmd,输入命令cmd,咱们就等着行了
EpEditor.execCmd(cmd, 0, new OnEditorListener() {
@Override
public void onSuccess() {
}
@Override
public void onFailure() {
}
@Override
public void onProgress(float v) {
}
});
复制代码
下面是在个人应用中使用到的一些命令:
设置变速值为speed(范围为0.5-2之间);参数值:setpts= 1/speed;atempo=speed 减速:speed = 0.5;
ffmpeg -i /sdcard/WeiXinRecordedDemo/1515059397193/mergeVideo.mp4 -filter_complex [0:v]setpts=2.000000*PTS[v];[0:a]atempo=0.500000[a] -map [v] -map [a] -y /sdcard/WeiXinRecordedDemo/1515059397193/speedVideo.mp4
复制代码
加速:speed = 2;
ffmpeg -i /sdcard/WeiXinRecordedDemo/1515118254029/mergeVideo.mp4 -filter_complex [0:v]setpts=0.500000*PTS[v];[0:a]atempo=2.000000[a] -map [v] -map [a] -y /sdcard/WeiXinRecordedDemo/1515118254029/speedVideo.mp4
复制代码
ffmpeg -i /sdcard/WeiXinRecordedDemo/1515060907399/finish.mp4 -vcodec copy -acodec copy -ss 00:00:00 -t 00:00:01 /sdcard/WeiXinRecordedDemo/1515060907399/1515060998134.mp4
复制代码
String path = "/storage/emulated/0/ych/123.mp4";
String currentOutputVideoPath = "/storage/emulated/0/ych/video/123456.mp4";
String commands ="-y -i " + path + " -strict-2 -vcodec libx264 -preset ultrafast " +
"-crf 24 -acodec aac -ar 44100 -ac 2 -b:a 96k -s 640x480 -aspect 16:9 " + currentOutputVideoPath;
复制代码
ffmpeg -y -i /storage/emulated/0/DCIM/Camera/VID_20180104_121113.mp4 -i /storage/emulated/0/ych/music/A Little Kiss.mp3 -filter_complex [0:a]aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo,volume=1.0[a0];[1:a]aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo,volume=0.5[a1];[a0][a1]amix=inputs=2:duration=first[aout] -map [aout] -ac 2 -c:v copy -map 0:v:0 /storage/emulated/0/ych/music/1515468589128.mp4
复制代码
命令:
ffmpeg -i <input> -filter_complex subtitles=filename=<SubtitleName>-y <output>
复制代码
说明:利用libass来为视频嵌入字幕,字幕是直接嵌入到视频里的硬字幕。
String mCommands ="-y -i "+ videoPath + " -i " + imagePath + " -filter_complex [0:v]scale=iw:ih[outv0];[1:0]scale=240.0:84.0[outv1];[outv0][outv1]overlay=main_w-overlay_w-10:main_h-overlay_h-10 -preset ultrafast " + outVideoPath;
复制代码
说明:imagePath为图片路径,overlay=100:100意义为overlay=x:y,在(x,y)坐标处開始加入水印。scale 为图片的缩放比例
左上角:overlay=10:10
右上角:overlay=main_w-overlay_w-10:10
左下角:overlay=10:main_h-overlay_h-10
右下角:overlay=main_w-overlay_w-10:main_h-overlay_h-10复制代码
视频旋转也能够参考使用OpenCV和FastCV,固然前两种是在线处理,若是是视频录制完成,咱们能够经过mp4parser进行离线处理。参考博客Android进阶之视频录制播放常见问题
命令:
ffmpeg -i <input> -filter_complex transpose=X -y <output>
复制代码
说明:transpose=1为顺时针旋转90°,transpose=2逆时针旋转90°。
在音视频开发的路上,感谢下面的文章及项目的做者,感谢他们的无私奉献,在前面种好大树,让咱们后来者乘凉。
拍摄录制功能:1. grafika 2. WeiXinRecordedDemo
OpenGL 系列:1. 关于OpenGl的学习:AndroidOpenGLDemo LearnOpenGL-CN 2. 关于滤镜的话:android-gpuimage-plus-masterandroid-gpuimage
关于FFmpeg 1.FFmpeg官网2. 官方项目地址github3. [FFmpeg]ffmpeg各种参数说明与使用示例1. ffmpeg-android-java
贴纸 1. StickerView
到这里文章基本上结束了,最后想和各位说的是,实在抱歉,确实最近时间有点紧,天天来公司大部分时间在讨论产品,剩下的一小部分时间不是在路上,就是在吃饭睡觉了。天天能抽半个小时写就很不错了。值得庆幸的是,最终它仍是完成了,但愿经过本文能给你们带来一些实质性的帮助。原本想多写一点,尽可能写详细点,可是精力有限,后面的关于滤镜,美颜,变声,及人脸识别部分的以后会再从新整理。最后,项目地址AndroidCamera。
请注意,如下内容将全都是广告: