若是你有學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。
請注意,如下內容將全都是廣告: