如果你有學Android 音視頻,相機開發的想法,那麼這篇文章可以作為一篇不錯的參考文章。當然本文為付費文章,收費10元,如果看完覺得對你有用,文末讚賞繳費即可。如果沒有學習音視頻,相機的欲望,趕快走,趕快走,不要有一絲停留,因為這篇文章很長,而且枯燥無味還會讓你閱讀起來毫無快感可言。
請考慮3s趕快決定去留。
3……
2……
1……
好吧,不走我再扯兩句:
這篇是在學習相機音視頻開發的時候寫的一篇總結。由於涉及的知識點比較多,所以其中部分知識點僅起引導作用。當然,微信有很多連結不能導過來,如果要點擊連結的話請到原文:https://gitbook.cn/new/gitchat/activity/5aeb03e3af08a333483d71c1 去查看:
ok,枯燥無味正式開始:
昨天發Chat之前還仔細看了時間計劃表,時間是5.24提交文章,時間在計劃之內,所以才提交的。結果很快通過審核。意料之外的是今天上午11:20就提示我預訂人數已經達標了。感謝大家對我的認可,非常感謝。結果我點進去一看,文章提交時間到居然提前到5.18了提交了,人數達標提前一周完成,所以文章提前了一周提交(有點小情緒,平臺沒有提前告訴我這個,不過工作人員的解答還是緩和了我的暴脾氣,好評)。最後到我這壓力就大了。因為這篇文章計劃寫的內容覆蓋面是很廣泛的,涵蓋相機開發的大部分知識,而且我對自己寫作要求:內容儘量精煉,不能泛泛而談。所以時間上來說很緊湊了。當然,如果文章各方面大家有看不順眼的地方,希望大家幫忙指出批評,一定虛心接受,積極改正。如果今後有機會見面,請你喝茶。項目地址https://github.com/aserbao/AndroidCamera:
還有三篇相關的文章也先列出來吧, 如果你真看下去是有需要滴:
使用OpenGl ES繪製簡單圖形
通過SurfaceView,TextureView,GlSurfaceView顯示相機預覽
Android 自定義相機開發(三) —— 了解下EGL
當然,這個對大部分人來說都是沒什麼問題的,但是該篇文章還得照顧大部分初次接觸Camera開發的小夥伴,所以請容許我在此多囉嗦一下,如果你有接觸過Camera的開發,此部分可以跳過,直接看下一部分。
a. 使用Camera的步驟:說下Camera的操作步驟,後面給出實例,請結合代碼理解分析:
獲取一個Camera實例,通過open方法,Camera.open(0),0是後置攝像頭,1表示前置攝像頭。
設置Camera的參數,比如聚焦,是否開閃光燈,預覽高寬,修改Camera的默認參數:mCamera.getParameters()通過初始化SurfaceHolder去setPreviewDisplay(SurfaceHolder),沒有surface,Camera不能開始預覽。
調用startPreview方法開始更新預覽到surface,在拍照之前,startPreview必須調用,預覽必須開啟。
當你想開始拍照時,使用takePicture(Camera.ShutterCallback, Camera.PictureCallback, Camera.PictureCallback, Camera.PictureCallback), 等待回調提供真實的圖像數據
當拍完一張照片時,預覽(preview)將會停止,當你想要拍更多的照片時,須要再一次調用startPreview方法
當調用stopPreview方法時,將停止更新預覽的surface
當調用release方法時,將馬上釋放camera
b.使用SurfaceView預覽顯示Camera數據如果你初次開發相機,請按照上面的步驟觀看下面代碼,如果你已經知道了,請直接過濾掉此基礎部分。如果想了解更多預覽方式,你可以看我的另一篇文章通過SurfaceView,TextureView,GlSurfaceView顯示相機預覽。
1public class CameraSurfaceViewShowActivity extends AppCompatActivity implements SurfaceHolder.Callback {
2 @BindView(R.id.mSurface)
3 SurfaceView mSurfaceView;
4
5 public SurfaceHolder mHolder;
6 private Camera mCamera;
7 private Camera.Parameters mParameters;
8
9 @Override
10 protected void onCreate(Bundle savedInstanceState) {
11 super.onCreate(savedInstanceState);
12 setContentView(R.layout.activity_base_camera);
13 ButterKnife.bind(this);
14 mHolder = mSurfaceView.getHolder();
15 mHolder.addCallback(this);
16 mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
17 }
18
19 @Override
20 public void surfaceCreated(SurfaceHolder holder) {
21 try {
22
23 mCamera = Camera.open(0);
24 mCamera.setDisplayOrientation(90);
25 mCamera.setPreviewDisplay(holder);
26 mCamera.startPreview();
27 } catch (IOException e) {
28 }
29 }
30
31 @Override
32 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
33 mCamera.autoFocus(new Camera.AutoFocusCallback() {
34 @Override
35 public void onAutoFocus(boolean success, Camera camera) {
36 if (success) {
37 mParameters = mCamera.getParameters();
38 mParameters.setPictureFormat(PixelFormat.JPEG);
39
40 mParameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
41 mCamera.setParameters(mParameters);
42 mCamera.startPreview();
43 mCamera.cancelAutoFocus();
44 }
45 }
46 });
47 }
48
49 @Override
50 public void surfaceDestroyed(SurfaceHolder holder) {
51 if (mCamera != null) {
52 mCamera.stopPreview();
53 mCamera.release();
54 mCamera = null;
55 }
56 }
57
58 @OnClick(R.id.btn_change)
59 public void onViewClicked() {
60
61 PropertyValuesHolder valuesHolder = PropertyValuesHolder.ofFloat("rotationY", 0.0f, 360.0f, 0.0F);
62 PropertyValuesHolder valuesHolder1 = PropertyValuesHolder.ofFloat("scaleX", 1.0f, 0.5f,1.0f);
63 PropertyValuesHolder valuesHolder3 = PropertyValuesHolder.ofFloat("scaleY", 1.0f, 0.5f,1.0f);
64 ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(mSurfaceView, valuesHolder,valuesHolder1,valuesHolder3);
65 objectAnimator.setDuration(5000).start();
66 }
67}
當然,為了使效果好看一點點,我添加了一丟丟效果,效果如下:
好了,到這裡為止,我們的簡單Camera預覽結束。
2. 使用OpenGl ES預覽相機數據OpenGL ES (OpenGL for Embedded Systems) 是OpenGl的子集,針對手機、PDA和遊戲主機等嵌入式設備而設計。(我不會偷偷告訴你我是百度滴)
關於OpenGl ES如何繪製一個簡單基本圖形,下面會做一個簡單的講解,如果你想對OpenGL ES有更深層次的了解,可以看下我寫的關於一篇OpenGL繪製簡單三角形的文章Android openGl開發詳解(一)——簡單圖形的基本繪製,
1. 使用OpenGl ES繪製相機數據必備的基本知識1. 關於OpenGl ES渲染流程了解下:首先我們必須明確我們要做的是將相機數據顯示到設備屏幕上,所有的操作都是為此目的服務的。所以我們必須要了解OpenGl ES是如何進行渲染的。(如果下面提到的術語你沒有概念,或者模稜兩可,請看再看一遍Android openGl開發詳解(一)——簡單圖形的基本繪製)
下面是基本步驟:
布局文件中添加GlSurfaceView,並為其指定渲染器Renderer。
設置畫布大小,清除畫布內容,創建紋理對象,並指定OpenGl ES操作紋理ID。(下面會講到)
加載頂點著色器(vertex shader)和片元著色器(fragment shader)。
創建OpenGl ES程序,創建program對象,連接頂點和片元著色器,連結program對象。
打開相機,設置預覽布局,開啟預覽,並通過glUseProgram()方法將程序添加到OpenGl ES環境中,獲取著色器句柄,通過glVertexAttribPointer()傳入繪製數據並啟用頂點位置句柄。
在onDrawFrame方法中更新緩衝區幀數據並通過glDrawArrays繪製到GlSurfaceView上。
操作完成後資源釋放,需要注意的是使用GlsurfaceView的時候需要注意onResume()和onPause()的調用。
上面步驟基本可以將Camera的預覽數據通過OpenGl ES的方式顯示到了GlSurfaceView上。當然,我們先來看下效果圖,再給出源碼部分。讓大家看一下效果(因為時間原因,請原諒我拿了之前的圖)
這部分源碼會在項目中給出,同時在通過SurfaceView,TextureView,GlSurfaceView顯示相機預覽也有給出,所以,在這裡就不貼源碼了。
2. 了解下EGLWhat?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。
3. 了解下OpenGl ES中的紋理OpenGl 中的紋理可以用來表示圖像,照片,視頻畫面等數據,在視頻渲染中,我們只需要處理二維的紋理,每個二維的紋理都由許多小的紋理元素組成,我們可以將其看成小塊的數據。我們可以簡單將紋理理解成電視牆瓷磚,我們要做一面電視牆,需要由多個小瓷磚磡成,最終成型的才是完美的電視牆。我暫時是這麼理解滴。使用紋理,最直接的方式是直接從給一個圖像文件加載數據。這裡我們得稍微注意下,OpenGl的二維紋理坐標和我們的手機屏幕坐標還是有一定的區別。
OpenGl的紋理坐標的原點是在左下角,而計算機的紋理坐標在左上角。尤其是我們在添加貼紙的時候需要注意下y值的轉換。這裡順便說下OpenGl ES繪製相機數據的時候紋理坐標的變換問題,下次如果使用OpenGl 處理相機數據遇到鏡像或者上下顛倒可以對照下圖片上所說的規則:
下面我們來講解下OpenGl紋理使用的步驟:
1. 首先我們需要創建一個紋理對象,通過glGenTextures()方法獲取到紋理對象ID,接下來我們就可以操作紋理了對象,但是我們需要告訴OpenGl 我們操作的是哪個紋理,所以我們需要通過glBindTexture()告訴OpenGl操作紋理的ID,當紋理綁定之後,我們還需要為這個紋理對象設置一些參數(紋理的過濾方式),當我們需要將紋理對象渲染到物體表面時,我們需要通過紋理對象的紋理過濾器通過glTexParameterf()方法來指明,最後當我們操作當前紋理完成之後,我們可以通過調用一次GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0)對紋理進行解綁。
1private int createTextureID() {
2 int[] tex = new int[1];
3
4 GLES20.glGenTextures(1, tex, 0);
5
6 GLES20.glBindTexture(GL_TEXTURE_2D, tex[0]);
7
8 GLES20.glTexParameterf(GL_TEXTURE_2D,
9 GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
10
11 GLES20.glTexParameterf(GL_TEXTURE_2D,
12 GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
13
14 GLES20.glTexParameterf(GL_TEXTURE_2D,
15 GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
16
17 GLES20.glTexParameterf(GL_TEXTURE_2D,
18 GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
19
20 GLES20.glBindTexture(GL_TEXTURE_2D, 0);
21 return tex[0];
22 }
這裡我們稍微提一下,如果是相機數據處理,我們使用GLES11Ext.GL_TEXTURE_EXTERNAL_OES,如果是處理貼紙圖片,我們使用GLES20.GL_TEXTURE_2D。因為相機輸出的數據類型是YUV420P格式的,使用GLES11Ext.GL_TEXTURE_EXTERNAL_OES擴展紋理可以實現自動將YUV420P轉RGB,我們就不需要在存儲成MP4的時候再進行數據轉換了。
2. 如果我們要給當前紋理添加PNG素材,我們需要對PNG這種圖片壓縮格式進行解碼操作。最終傳遞RGBA數據格式數據到OpenGl 中紋理中,當然,OpenGL還提供了三個指定函數來指定紋理glTexImage1D(), glTexImage2D(), glTexImage3D().。我們運用到的主要2D版本,glTexImage2D();
1void glTexImage2D( int target,
2 int level,
3 int internalformat,
4 int width,
5 int height,
6 int border,
7 int format,
8 int type,
9 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數據作為參數傳入,方法如下:
1 GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, mBitmap, 0);
接來下就如上面OpenGl ES渲染流程所提到的,將紋理繪製到屏幕上。
3. 一起了解下使用MediaCodec實現相機錄製上面我們將相機的預覽顯示講完了,接下裡我們講如何將錄製視頻。就目前來說,Android的錄製方式就要有下面三中:
使用MediaRecord進行錄製。(這個不講解)
使用MediaCodec進行錄製(我們講這種) 。
使用FFMpeg+x264/openh264。(軟編碼的方式,後面出專門的文章講解到這部分)。
1. 什麼是MediaCodec?MediaCodec官方文檔地址
MediaCodec是一個多媒體編解碼處理類,可用於訪問Android底層的多媒體編解碼器。例如,編碼器/解碼器組件。它是Android底層多媒體支持基礎架構的一部分(通常與MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, 以及AudioTrack一起使用)。請原諒我後面那一段是從官網搬過來的,知道它是用來處理音視頻的That's enough.
MediaCodec到底是如何將數據進行處理生成.mp4文件的呢?我們先看下圖(在官方圖片上進行了部分改動和標記):
對,就這麼簡單,八九不離十的樣子,反正我也不知道我說得對不對?
創建MediaFormat,並設置相關屬性,
MediaFormat.KEY_COLOR_FORMAT(顏色格式),
KEY_BIT_RATE(比特率),
KEY_FRAME_RATE(幀速),
KEY_I_FRAME_INTERVAL(關鍵幀間隔,0表示要求所有的都是關鍵幀,負值表示除第一幀外無關鍵幀)。
溫馨提示:沒有設置以上前三個屬性你可以能會出現以下錯誤:
1 Process: com.aserbao.androidcustomcamera, PID: 18501
2 android.media.MediaCodec$CodecException: Error 0x80001001
3 at android.media.MediaCodec.native_configure(Native Method)
4 at android.media.MediaCodec.configure(MediaCodec.java:1909)
5 ……
創建一個MediaCodec的編碼器,並配置格式。
創建一個MediaMuxer來合成視頻。
通過dequeueInput/OutputBuffer()獲取輸入輸出緩衝區。
通過getInputBuffers獲取輸入隊列,然後通過queueInputBuffer把原始YUV數據送入編碼器。
通過dequeueOutputBuffer方法獲取當前編解碼狀態,根據不同的狀態進行處理。
再然後在輸出隊列端同樣通過dequeueOutputBuffer獲取輸出的h264流。
處理完輸出數據之後,需要通過releaseOutputBuffer把輸出buffer還給系統,重新放到輸出隊列中。
使用MediaMuxer混合。
溫馨提示:下面實例是通過直接在mediacodec的輸入surface上進行繪製,所以不會有上述輸入隊列的操作。關於MediaCodec的很多細節,官方已經講得很詳細了,這裡不過多闡述。
官方地址:MediaCodec
MediaCodec中文文檔
MediaCodec同步緩存處理方式(來自官方實例,還有異步緩存處理及同步數組的處理方式這裡不做多講解,如果有興趣到官方查看),配合上面的步驟看會理解更多,如果還是不明白建議查看下面實例之後再回頭來看步驟和實例:
1MediaCodec codec = MediaCodec.createByCodecName(name);
2 codec.configure(format, …);
3 MediaFormat outputFormat = codec.getOutputFormat();
4 codec.start();
5 for (;;) {
6 int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
7 if (inputBufferId >= 0) {
8 ByteBuffer inputBuffer = codec.getInputBuffer(…);
9
10 …
11 codec.queueInputBuffer(inputBufferId, …);
12 }
13 int outputBufferId = codec.dequeueOutputBuffer(…);
14 if (outputBufferId >= 0) {
15 ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
16 MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId);
17
18
19 …
20 codec.releaseOutputBuffer(outputBufferId, …);
21 } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
22
23
24 outputFormat = codec.getOutputFormat();
25 }
26 }
27 codec.stop();
28 codec.release();
如果你之前沒有使用過MediaCodec錄製過視頻,這個實例建議你看一下,如果你非常了解了,請跳過。效果圖如下:
難得給下代碼,當然,項目中會有更多關於MediaCodec的實例,最後會給出:
1public class PrimaryMediaCodecActivity extends BaseActivity {
2 private static final String TAG = "PrimaryMediaCodecActivi";
3 private static final String MIME_TYPE = "video/avc";
4 private static final int WIDTH = 1280;
5 private static final int HEIGHT = 720;
6 private static final int BIT_RATE = 4000000;
7 private static final int FRAMES_PER_SECOND = 4;
8 private static final int IFRAME_INTERVAL = 5;
9
10 private static final int NUM_FRAMES = 4 * 100;
11 private static final int START_RECORDING = 0;
12 private static final int STOP_RECORDING = 1;
13
14 @BindView(R.id.btn_recording)
15 Button mBtnRecording;
16 @BindView(R.id.btn_watch)
17 Button mBtnWatch;
18 @BindView(R.id.primary_mc_tv)
19 TextView mPrimaryMcTv;
20 public MediaCodec.BufferInfo mBufferInfo;
21 public MediaCodec mEncoder;
22 @BindView(R.id.primary_vv)
23 VideoView mPrimaryVv;
24 private Surface mInputSurface;
25 public MediaMuxer mMuxer;
26 private boolean mMuxerStarted;
27 private int mTrackIndex;
28 private long mFakePts;
29 private boolean isRecording;
30
31 private int cuurFrame = 0;
32
33 private MyHanlder mMyHanlder = new MyHanlder(this);
34 public File mOutputFile;
35
36 @OnClick({R.id.btn_recording, R.id.btn_watch})
37 public void onViewClicked(View view) {
38 switch (view.getId()) {
39 case R.id.btn_recording:
40 if (mBtnRecording.getText().equals("開始錄製")) {
41 try {
42 mOutputFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), System.currentTimeMillis() + ".mp4");
43 startRecording(mOutputFile);
44 mPrimaryMcTv.setText("文件保存路徑為:" + mOutputFile.toString());
45 mBtnRecording.setText("停止錄製");
46 isRecording = true;
47 } catch (IOException e) {
48 e.printStackTrace();
49 mBtnRecording.setText("出現異常了,請查明原因");
50 }
51 } else if (mBtnRecording.getText().equals("停止錄製")) {
52 mBtnRecording.setText("開始錄製");
53 stopRecording();
54 }
55 break;
56 case R.id.btn_watch:
57 String absolutePath = mOutputFile.getAbsolutePath();
58 if (!TextUtils.isEmpty(absolutePath)) {
59 if(mBtnWatch.getText().equals("查看視頻")) {
60 mBtnWatch.setText("刪除視頻");
61 mPrimaryVv.setVideoPath(absolutePath);
62 mPrimaryVv.start();
63 }else if(mBtnWatch.getText().equals("刪除視頻")){
64 if (mOutputFile.exists()){
65 mOutputFile.delete();
66 mBtnWatch.setText("查看視頻");
67 }
68 }
69 }else{
70 Toast.makeText(this, "請先錄製", Toast.LENGTH_SHORT).show();
71 }
72 break;
73 }
74 }
75
76 private static class MyHanlder extends Handler {
77 private WeakReference<PrimaryMediaCodecActivity> mPrimaryMediaCodecActivityWeakReference;
78
79 public MyHanlder(PrimaryMediaCodecActivity activity) {
80 mPrimaryMediaCodecActivityWeakReference = new WeakReference<PrimaryMediaCodecActivity>(activity);
81 }
82
83 @Override
84 public void handleMessage(Message msg) {
85 PrimaryMediaCodecActivity activity = mPrimaryMediaCodecActivityWeakReference.get();
86 if (activity != null) {
87 switch (msg.what) {
88 case START_RECORDING:
89 activity.drainEncoder(false);
90 activity.generateFrame(activity.cuurFrame);
91 Log.e(TAG, "handleMessage: " + activity.cuurFrame);
92 if (activity.cuurFrame < NUM_FRAMES) {
93 this.sendEmptyMessage(START_RECORDING);
94 } else {
95 activity.drainEncoder(true);
96 activity.mBtnRecording.setText("開始錄製");
97 activity.releaseEncoder();
98 }
99 activity.cuurFrame++;
100 break;
101 case STOP_RECORDING:
102 Log.e(TAG, "handleMessage: STOP_RECORDING");
103 activity.drainEncoder(true);
104 activity.mBtnRecording.setText("開始錄製");
105 activity.releaseEncoder();
106 break;
107 }
108 }
109 }
110 }
111
112 @Override
113 protected int setLayoutId() {
114 return R.layout.activity_primary_media_codec;
115 }
116
117
118 private void startRecording(File outputFile) throws IOException {
119 cuurFrame = 0;
120 prepareEncoder(outputFile);
121 mMyHanlder.sendEmptyMessage(START_RECORDING);
122 }
123
124 private void stopRecording() {
125 mMyHanlder.removeMessages(START_RECORDING);
126 mMyHanlder.sendEmptyMessage(STOP_RECORDING);
127 }
128
129
132 private void prepareEncoder(File outputFile) throws IOException {
133 mBufferInfo = new MediaCodec.BufferInfo();
134 MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, WIDTH, HEIGHT);
135
136
137 format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
138 MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
139 format.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
140 format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAMES_PER_SECOND);
141 format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
142
143
144 mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
145 mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
146 mInputSurface = mEncoder.createInputSurface();
147 mEncoder.start();
148
149
150
151 mMuxer = new MediaMuxer(outputFile.toString(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
152
153 mMuxerStarted = false;
154 mTrackIndex = -1;
155 }
156
157 private void drainEncoder(boolean endOfStream) {
158 final int TIMEOUT_USEC = 10000;
159 if (endOfStream) {
160 mEncoder.signalEndOfInputStream();
161 }
162 ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers();
163 while (true) {
164 int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
165 if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
166 if (!endOfStream) {
167 break;
168 }
169 } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
170
171 encoderOutputBuffers = mEncoder.getOutputBuffers();
172 } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
173
174 if (mMuxerStarted) {
175 throw new RuntimeException("format changed twice");
176 }
177 MediaFormat newFormat = mEncoder.getOutputFormat();
178 mTrackIndex = mMuxer.addTrack(newFormat);
179 mMuxer.start();
180 mMuxerStarted = true;
181 } else if (encoderStatus < 0) {
182 } else {
183 ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
184 if (encodedData == null) {
185 throw new RuntimeException("encoderOutputBuffer " + encoderStatus +
186 " was null");
187 }
188 if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
189
190 mBufferInfo.size = 0;
191 }
192 if (mBufferInfo.size != 0) {
193 if (!mMuxerStarted) {
194 throw new RuntimeException("muxer hasn't started");
195 }
196
197 encodedData.position(mBufferInfo.offset);
198 encodedData.limit(mBufferInfo.offset + mBufferInfo.size);
199 mBufferInfo.presentationTimeUs = mFakePts;
200 mFakePts += 1000000L / FRAMES_PER_SECOND;
201
202 mMuxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
203 }
204 mEncoder.releaseOutputBuffer(encoderStatus, false);
205 if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
206 if (!endOfStream) {
207 Log.e(TAG, "意外結束");
208 } else {
209 Log.e(TAG, "正常結束");
210 }
211 isRecording = false;
212 break;
213 }
214 }
215 }
216 }
217
218 private void generateFrame(int frameNum) {
219 Canvas canvas = mInputSurface.lockCanvas(null);
220 try {
221 int width = canvas.getWidth();
222 int height = canvas.getHeight();
223 float sliceWidth = width / 8;
224 Paint paint = new Paint();
225 for (int i = 0; i < 8; i++) {
226 int color = 0xff000000;
227 if ((i & 0x01) != 0) {
228 color |= 0x00ff0000;
229 }
230 if ((i & 0x02) != 0) {
231 color |= 0x0000ff00;
232 }
233 if ((i & 0x04) != 0) {
234 color |= 0x000000ff;
235 }
236 paint.setColor(color);
237 canvas.drawRect(sliceWidth * i, 0, sliceWidth * (i + 1), height, paint);
238 }
239
240 paint.setColor(0x80808080);
241 float sliceHeight = height / 8;
242 int frameMod = frameNum % 8;
243 canvas.drawRect(0, sliceHeight * frameMod, width, sliceHeight * (frameMod + 1), paint);
244 paint.setTextSize(50);
245 paint.setColor(0xffffffff);
246
247 for (int i = 0; i < 8; i++) {
248 if(i % 2 == 0){
249 canvas.drawText("aserbao", i * sliceWidth, sliceHeight * (frameMod + 1), paint);
250 }else{
251 canvas.drawText("aserbao", i * sliceWidth, sliceHeight * frameMod, paint);
252 }
253 }
254 } finally {
255 mInputSurface.unlockCanvasAndPost(canvas);
256 }
257 }
258
259 private void releaseEncoder() {
260 if (mEncoder != null) {
261 mEncoder.stop();
262 mEncoder.release();
263 mEncoder = null;
264 }
265 if (mInputSurface != null) {
266 mInputSurface.release();
267 mInputSurface = null;
268 }
269 if (mMuxer != null) {
270 mMuxer.stop();
271 mMuxer.release();
272 mMuxer = null;
273 }
274 }
275}
Android下的音頻錄製主要分兩種:
AudioRecord(基於字節流錄音) (我們主要講這個)。
MediaRecorder(基於文件錄音) :
雖然我們這裡只講第一種,在這裡還是講下優缺點:
使用AudioRecord錄音
優點:可以對語音進行實時處理,比如變音,降噪,增益……,靈活性比較大。
缺點:就是輸出的格式是PCM,你錄製出來不能用播放器播放,需要用到AudioTrack來處理。
使用 MediaRecorder:
優點:高度封裝,操作簡單,支持編碼,壓縮,少量的音頻格式文件,靈活性差。
缺點:沒法對音頻進行實時處理。
1. 創建AudioRecord實例,配置參數,初始化內部的音頻緩衝區。
1
7
8 public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat,
9 int bufferSizeInBytes)
10
上面提到的採樣時間這裡說一下,每個手機廠商設置的可能都不一樣,我們設置的採樣時間越短,聲音的延時就越小。我們可以通過getMinBufferSize()方法來確定我們需要輸入的bufferSizeInBytes值,官方說明是說小於getMinBufferSize()的值就會初始化失敗。
2. 開始採集音頻。
這個比較簡單:
1AudioRecord.startRecording();
2AudioRecord.stop();
3……
4AudioRecord.read(byte[] audioData, int offsetInBytes, int sizeInBytes);
3. 開啟線程,將數據保存為pcm文件。
4. 停止採集,資源釋放。
關於AudioRecord錄製的音頻的例子就不在這裡貼出來了,之後項目中會接入錄音變音,降噪,增益等功能。都會在代碼中給出。
5. 了解下音視頻混合前面講到了視頻和音頻的錄製,那麼如何將他們混合呢?
同樣就我所知目前有兩種方法:
使用MediaMuxer進行混合。(我們將下這種,也是市面上最常用的)。
使用FFmpeg進行混合。(目前不講,後面添加背景音樂會提到)
1. 了解下MediaMuxerMediaMuxer官方文檔地址
MediaMuxer最多僅支持一個視頻track,一個音頻的track.如果你想做混音怎麼辦?用ffmpeg進行混合吧。(目前還在研究FFMPEG這一塊,歡迎大家一塊來討論。哈哈哈……),目前MediaMuxer支持MP4、Webm和3GP文件作為輸出。視頻編碼的主要格式用H.264(AVC),音頻用AAC編碼(關於音頻你用其他的在IOS端壓根就識別不出來,我就踩過這個坑!)。
創建MediaMuxer對象。
添加媒體通道,並將MediaFormat添加到MediaMuxer中去。
通過start()開始混合。
writeSampleData()方法向mp4文件中寫入數據。
stop()混合關閉並進行資源釋放。
官方實例:
1MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
2
3
4 MediaFormat audioFormat = new MediaFormat(...);
5 MediaFormat videoFormat = new MediaFormat(...);
6 int audioTrackIndex = muxer.addTrack(audioFormat);
7 int videoTrackIndex = muxer.addTrack(videoFormat);
8 ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);
9 boolean finished = false;
10 BufferInfo bufferInfo = new BufferInfo();
11
12 muxer.start();
13 while(!finished) {
14
15
16
17
18 finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo);
19 if (!finished) {
20 int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;
21 muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);
22 }
23 };
24 muxer.stop();
25 muxer.release();
好了,綜上所述知識,已經實現了從預覽到錄製完成的講解。
多段視頻合成這裡提供兩種方案:
使用MediaCodec,MediaExtractor,MediaMuxer.(講思路)。
使用mp4parser合成視頻。(將使用)。
使用FFMpeg來實現。(音視頻這一塊找它就沒錯了,基本沒有它實現不了的)。
下面我們主要來講下兩種方式的使用,第一種我們講思路,第二種講如何使用?第三個暫時不講。
1. 講下如何使用Android原生實現視頻合成。只講思路及實現步驟,代碼在項目中之後給出,目前我還沒寫進去,原諒我最近偷懶一波。大體思路如下:
我們通過MediaExtractor將媒體文件分解並找到軌道及幀數據。
將分解後的數據填充到MediaCodec的緩衝區中去。
通過MediaMuxer將MediaCodec中的數據和找到的音軌進行混合。
遍歷第二個視頻文件。
差不多就是這樣滴,因為這個我是看別人是這麼做的,我偷懶用了mp4parser,所以僅能給個位提供思路了,今後有時間再了解下。
上面有提到我現在使用的就是這個,他是開源滴,來來來,點這裡給你們傳送門。雖然上面對於使用方法都說得很清楚了,雖然我的項目中也會有原始碼,但是我還是要把這部分寫出來:
1
7 public String mergeVideo(List<String> paths, String filePath) {
8 long begin = System.currentTimeMillis();
9 List<Movie> movies = new ArrayList<>();
10 String filePath = "";
11 if(paths.size() == 1){
12 return paths.get(0);
13 }
14 try {
15 for (int i = 0; i < paths.size(); i++) {
16 if(paths != null && paths.get(i) != null) {
17 Movie movie = MovieCreator.build(paths.get(i));
18 movies.add(movie);
19 }
20 }
21 List<Track> videoTracks = new ArrayList<>();
22 List<Track> audioTracks = new ArrayList<>();
23 for (Movie movie : movies) {
24 for (Track track : movie.getTracks()) {
25 if ("vide".equals(track.getHandler())) {
26 videoTracks.add(track);
27 }
28 if ("soun".equals(track.getHandler())) {
29 audioTracks.add(track);
30 }
31 }
32 }
33 Movie result = new Movie();
34 if (videoTracks.size() > 0) {
35
36 result.addTrack(new AppendTrack(videoTracks.toArray(new Track[videoTracks.size()])));
37 }
38 if (audioTracks.size() > 0) {
39
40 result.addTrack(new AppendTrack(audioTracks.toArray(new Track[audioTracks.size()])));
41 }
42 Container container = new DefaultMp4Builder().build(result);
43 filePath = getRecorderPath();
44 FileChannel fc = new RandomAccessFile(String.format(filePath), "rw").getChannel();
45 container.writeContainer(fc);
46 fc.close();
47 } catch (Exception e) {
48 e.printStackTrace();
49 return paths.get(0);
50 }
51 long end = System.currentTimeMillis();
52 return filePath;
53 }
先看下我們要實現什麼功能,如下:
簡單分析下,我們現在需要將整個視頻的部分幀拿出在下面顯示出來,並且添加上面的動態貼紙顯示。
1. 如何拿出視頻幀?Android平臺下主要有兩種拿視頻幀的方法:
使用ThumbnailUtils,一般用來拿去視頻縮略圖。
使用MediaMetadataRetriever的getFrameAtTime()拿視頻幀(我們用的這種方式)。
1MediaMetadataRetriever mediaMetadata = new MediaMetadataRetriever();
2 mediaMetadata.setDataSource(mContext, Uri.parse(mInputVideoPath));
3 mVideoRotation = mediaMetadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
4 mVideoWidth = Integer.parseInt(mediaMetadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
5 mVideoHeight = Integer.parseInt(mediaMetadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
6 mVideoDuration = Integer.parseInt(mediaMetadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
7 int frameTime = 1000 * 1000;
8 int frame = mVideoDuration * 1000 / frameTime;
9 mAsyncTask = new AsyncTask<Void, Void, Boolean>() {
10 @Override
11 protected Boolean doInBackground(Void... params) {
12 myHandler.sendEmptyMessage(ClEAR_BITMAP);
13 for (int x = 0; x < frame; x++) {
14
15 Bitmap bitmap = mediaMetadata.getFrameAtTime(frameTime * x, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
16 }
17 mediaMetadata.release();
18 return true;
19 }
20
21 @Override
22 protected void onPostExecute(Boolean result) {
23 myHandler.sendEmptyMessage(SUBMIT);
24 }
拿完所有幀,好了,好了,下一個話題。
2. 如何分解Gif圖?看到上面的等撩了麼?
先說下為什麼要將Gif圖進行分解操作,因為我在添加動態貼紙的時候是在OpenGl Es的OnDraw方法中通過每次動態修改紋理來達到動態貼紙的效果的。所以必須要將Gif圖分解成每幀的形式。怎麼將Gif圖解析出來呢?Google出來一個工具類GifDecoder!當然,後面我去找了Glide的源碼,分析其內部Gif圖的顯示流程,發現其實原理是一樣的。Glide StandardGifDecoder當然,關於Glide的Gif圖解析內容還是蠻多的,這裡不做分析(沒有太過深入研究),今後有時間看能不能寫一篇文章專門分析。
當然,關於GifDecoder的代碼,這裡就不貼出來了,會在項目中給出!當然,現在項目中還沒有,因為文章寫完,我這個項目肯定寫不完的,最近事太多,忙著開產品討論會,儘量在討論之前5月25號之前能將項目寫完。所以這裡還請各位多諒解下。
7. 了解下FFmpeg參考文章:1.FFmpeg官網2. 官方項目地址github3. [FFmpeg]ffmpeg各類參數說明與使用示例
如果你有接觸到音視頻開發這一塊,肯定聽說過FFmpeg這個龐然大物。為什麼說龐然大物?因為我最近在學習這個,越學越覺得自己無知。哎,不多說了,我要加班惡補FFMpeg了。
1. 了解下什麼是FFmpegFFmpeg是一個自由軟體,可以運行音頻和視頻多種格式的錄影、轉換、流功能[2],包含了libavcodec——這是一個用於多個項目中音頻和視頻的解碼器庫,以及libavformat——一個音頻與視頻格式轉換庫。(來源wiki),簡單點可以將FFmpeg理解成音視頻處理軟體。可以通過輸入命令的方式對視頻進行任何操作。沒錯,是任何(一點都不誇張)!
2. 如何在Android下使用FFmpeg對於FFmpeg,我只想說,我還是個小白,希望各位大大不要在這個問題上抓著我嚴刑拷打。眾所周知的,FFmpge是C實現的,所以生成so文件再調用吧!怎麼辦?我不會呀?這時候就要去找前人種的樹了。這裡給一個我參考使用的FFmpeg文件庫導入EpMedia,哎,乘涼,感謝這位大大!
當然,如果想了解下FFmpeg的編譯,可以看下Android最簡單的基於FFmpeg的例子(一)---編譯FFmpeg類庫](http://www.ihubin.com/blog/android-ffmpeg-demo-1/)
如何使用?
1
2 EpEditor.execCmd(cmd, 0, new OnEditorListener() {
3 @Override
4 public void onSuccess() {
5
6 }
7
8 @Override
9 public void onFailure() {
10
11 }
12
13 @Override
14 public void onProgress(float v) {
15 }
16 });
下面是在我的應用中使用到的一些命令:
1. 視頻加減速命令:設置變速值為speed(範圍為0.5-2之間);參數值:setpts= 1/speed;atempo=speed
減速:speed = 0.5;
1ffmpeg -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;
1ffmpeg -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
1ffmpeg -i /sdcard/WeiXinRecordedDemo/1515060907399/finish.mp4 -vcodec copy -acodec copy -ss 00:00:00 -t 00:00:01 /sdcard/WeiXinRecordedDemo/1515060907399/1515060998134.mp4
1 String path = "/storage/emulated/0/ych/123.mp4";
2 String currentOutputVideoPath = "/storage/emulated/0/ych/video/123456.mp4";
3 String commands ="-y -i " + path + " -strict-2 -vcodec libx264 -preset ultrafast " +
4 "-crf 24 -acodec aac -ar 44100 -ac 2 -b:a 96k -s 640x480 -aspect 16:9 " + currentOutputVideoPath;
1 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
命令:
1ffmpeg -i <input> -filter_complex subtitles=filename=<SubtitleName>-y <output>
說明:利用libass來為視頻嵌入字幕,字幕是直接嵌入到視頻裡的硬字幕。
6. 加水印1 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 為圖片的縮放比例
1左上角:overlay=10:10
2
3右上角:overlay=main_w-overlay_w-10:10
4
5左下角:overlay=10:main_h-overlay_h-10
6
7右下角:overlay=main_w-overlay_w-10:main_h-overlay_h-10
視頻旋轉也可以參考使用OpenCV和FastCV,當然前兩種是在線處理,如果是視頻錄製完成,我們可以通過mp4parser進行離線處理。參考博客Android進階之視頻錄製播放常見問題
命令:
1ffmpeg -i <input> -filter_complex transpose=X -y <output>
說明:transpose=1為順時針旋轉90°,transpose=2逆時針旋轉90°。
8. 參考連結及項目在音視頻開發的路上,感謝下面的文章及項目的作者,感謝他們的無私奉獻,在前面種好大樹,讓我們後來者乘涼。
參考學習對象(排名無先後)
雷霄驊 湖廣午王 逆流的魚yuiop 小碼哥_WS
感謝四位老哥的博客,給予了我很大幫助。
拍攝錄製功能: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
9. 結束語到這裡文章基本上結束了,最後想和各位說的是,實在抱歉,確實最近時間有點緊,每天來公司大部分時間在討論產品,剩下的一小部分時間不是在路上,就是在吃飯睡覺了。每天能抽半個小時寫就很不錯了。值得慶幸的是,最終它還是完成了,希望通過本文能給大家帶來一些實質性的幫助。本來想多寫一點,儘量寫詳細點,但是精力有限,後面的關於濾鏡,美顏,變聲,及人臉識別部分的之後會再重新整理。最後,項目地址:
https://github.com/aserbao/AndroidCamera。