Android 相機,音視頻開發入門篇

2021-02-14 aserbao

如果你有學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


好了好了,現在開始看目錄吧:

1. 從打開一個攝像頭說起

當然,這個對大部分人來說都是沒什麼問題的,但是該篇文章還得照顧大部分初次接觸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}

c. 效果展示

當然,為了使效果好看一點點,我添加了一丟丟效果,效果如下:

好了,到這裡為止,我們的簡單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. 了解下EGL

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。

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.

2. MediaCodec的操作原理?

MediaCodec到底是如何將數據進行處理生成.mp4文件的呢?我們先看下圖(在官方圖片上進行了部分改動和標記):

這裡寫圖片描述
既然上面我們提到MediaCodec是一個編碼器處理類,從圖上看我們可以知道,他就是2的輸入的數據進行處理,然後輸出到3中去保存。每個編碼器都包含一組輸入和輸出緩存,中間的兩條從Codec出發又返回Codec的虛線就代表兩組緩存。當編碼器啟動後,兩組緩存便存在。由編碼器發送空緩存給輸入區(提供數據區),輸入區將輸入緩存填充滿,再返回給編碼器進行編碼,編碼完成之後將數據進行輸出,輸出之後將緩衝區返回給編碼器。如果你是個吃貨你可以這樣理解:Codec是榨汁機,在榨汁之前準備兩個杯子。一個杯子(輸入緩存)用來裝蘋果一直往榨汁機裡面倒,倒完了繼續回去裝蘋果。另一個杯子(輸出緩存)用來裝榨出來的蘋果汁,無論你將果汁放到哪裡去(放一個大瓶子裡面或者喝掉),杯子空了你就還回來繼續接果汁,知道將榨汁機裡面的果汁接完為止。

對,就這麼簡單,八九不離十的樣子,反正我也不知道我說得對不對?

4. MediaCodec的使用步驟:

創建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();

5. 講個實例,使用MediaCodec錄製一段繪製到Surface上的數據

如果你之前沒有使用過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}

4. 了解下音頻錄製

Android下的音頻錄製主要分兩種:

AudioRecord(基於字節流錄音) (我們主要講這個)。

MediaRecorder(基於文件錄音) :

雖然我們這裡只講第一種,在這裡還是講下優缺點:

使用AudioRecord錄音
優點:可以對語音進行實時處理,比如變音,降噪,增益……,靈活性比較大。
缺點:就是輸出的格式是PCM,你錄製出來不能用播放器播放,需要用到AudioTrack來處理。

使用 MediaRecorder:
優點:高度封裝,操作簡單,支持編碼,壓縮,少量的音頻格式文件,靈活性差。
缺點:沒法對音頻進行實時處理。

1. AudioRecord的工作流程

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. 了解下MediaMuxer

MediaMuxer官方文檔地址
MediaMuxer最多僅支持一個視頻track,一個音頻的track.如果你想做混音怎麼辦?用ffmpeg進行混合吧。(目前還在研究FFMPEG這一塊,歡迎大家一塊來討論。哈哈哈……),目前MediaMuxer支持MP4、Webm和3GP文件作為輸出。視頻編碼的主要格式用H.264(AVC),音頻用AAC編碼(關於音頻你用其他的在IOS端壓根就識別不出來,我就踩過這個坑!)。

2. MediaMuxer的工作流程

創建MediaMuxer對象。

添加媒體通道,並將MediaFormat添加到MediaMuxer中去。

通過start()開始混合。

writeSampleData()方法向mp4文件中寫入數據。

stop()混合關閉並進行資源釋放。

官方實例:

1MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);


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();

好了,綜上所述知識,已經實現了從預覽到錄製完成的講解。

這裡寫圖片描述6. 了解下多段視頻拼接合成

多段視頻合成這裡提供兩種方案:

使用MediaCodec,MediaExtractor,MediaMuxer.(講思路)。

使用mp4parser合成視頻。(將使用)。

使用FFMpeg來實現。(音視頻這一塊找它就沒錯了,基本沒有它實現不了的)。

下面我們主要來講下兩種方式的使用,第一種我們講思路,第二種講如何使用?第三個暫時不講。

1. 講下如何使用Android原生實現視頻合成。

只講思路及實現步驟,代碼在項目中之後給出,目前我還沒寫進去,原諒我最近偷懶一波。大體思路如下:

我們通過MediaExtractor將媒體文件分解並找到軌道及幀數據。

將分解後的數據填充到MediaCodec的緩衝區中去。

通過MediaMuxer將MediaCodec中的數據和找到的音軌進行混合。

遍歷第二個視頻文件。

差不多就是這樣滴,因為這個我是看別人是這麼做的,我偷懶用了mp4parser,所以僅能給個位提供思路了,今後有時間再了解下。

2. 講下如何使用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    }

7. 了解下如何獲取視頻幀?

先看下我們要實現什麼功能,如下:

這裡寫圖片描述

簡單分析下,我們現在需要將整個視頻的部分幀拿出在下面顯示出來,並且添加上面的動態貼紙顯示。

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.  了解下什麼是FFmpeg

FFmpeg是一個自由軟體,可以運行音頻和視頻多種格式的錄影、轉換、流功能[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

2. 視頻剪切命令:

1ffmpeg -i /sdcard/WeiXinRecordedDemo/1515060907399/finish.mp4 -vcodec copy -acodec copy -ss 00:00:00 -t 00:00:01 /sdcard/WeiXinRecordedDemo/1515060907399/1515060998134.mp4

3. 視頻壓縮命令:

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;

4.給視頻添加背景音樂

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

5. 加字幕

命令:

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

7. 旋轉

視頻旋轉也可以參考使用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。

相關焦點

  • Android相機開發詳解
    本篇來自 Glumes 的投稿,分享了Android相機開發的相關知識,希望對大家有所幫助。其中尺寸指的是:相機顯示預覽幀的尺寸相機拍攝幀的尺寸Android 顯示相機預覽內容的控制項尺寸而方向指的是相機顯示預覽幀的方向相機拍攝幀的方向Android 手機自身的方向在開發中要處理好這三個方向和三個尺寸各自的關係才行,這裡以 Camera 1.0 版本的 API 作為示例
  • 音視頻開發之旅(八)GLSL及Shader的渲染流程
    圖片來自《音視頻開發進階指南》下面看下如何創建創建在GPU可執行程序(Program)我們通過上圖可以看到分為四個環節創建program :glCreateProgramattachShader: 創建頂點和片元著色器、設置source、編譯,attach到program連結:glLinkProgram
  • 一看就懂的Android APP開發入門教程
    工作中有做過手機App項目,前端和android或ios程式設計師配合完成整個項目的開發,開發過程中與ios程序配合基本沒什麼問題,而 android
  • 作為一個Android程式設計師,精通音視頻開發,寒冬再冷也凍不到你
    下面正是要給大家分享我花了86天整理的關於音視頻開發入門到精通,已整理成PDF文檔,有需要完整版的可以加入我們音視頻技術交流群 933971311 免費獲取。一、初級入門篇(一)繪製圖片 1. ImageView 繪製圖片2. SurfaceView 繪製圖片3.
  • 一些優秀的 Android 開發專欄推薦
    當一個已經入門的開發者,想要成為一個更好的 Android 開發者的時候,就會發現網際網路的資料太瑣碎,而且資料的好壞也難辨。常常都會困惑我要如何提高自己,哪裡有好的學習資料。 接下來我們就會推薦一些最值得訂閱的優秀專欄。
  • Android新手入門-Android中文SDK
    Android新手入門本文引用地址:http://www.eepw.com.cn/article/201610/305797.htmAndroid新手入門 (Getting Started with Android)新手入門Android,請首先閱讀下面的章節 (To get started with Android
  • Android OpenGL ES 從入門到精通系統性學習教程
    該教程分為基礎篇和應用篇,基礎篇主要是講解 GLES 3.0 的主要核心知識點,而應用篇主要是利用基礎篇的知識實現一些常見的特效和功能。開發(15):立方體貼圖(天空盒)OpenGL ES 3.0 開發(16):相機預覽OpenGL ES 3.0 開發(17):相機基礎濾鏡OpenGL ES 3.0 開發(18):相機 LUT 濾鏡OpenGL ES 3.0 開發(19):相機抖音濾鏡OpenGL ES 3.0 開發(20
  • Android開發必備的「80」個開源庫
    wiki 周刊https://github.com/bboyfeiyu/android-tech-frontier/wiki值得閱讀的 Android 技術文章https://github.com/bboyfeiyu/Worth-Reading-the-Android-technical-articles整理一些比較好的 Android 開發教程
  • 音視頻開發入門必備之基礎知識
    網際網路信息的傳播與娛樂方式經歷了從文字到圖片再到音視頻的轉變,現如今抖音、快手等短視頻更是如日中天,特別是5G時代的到來,筆者相信網際網路對音視頻開發者的需求會迎來更大的增長需求,何況音視頻開發者因為其稀缺性薪酬本來就比較高。在學習音視頻開發之前,我們先來了解一下音視頻的基本知識。
  • 五十音圖免費入門視頻課
    新學期,給大家帶來一個五十音圖入門的視頻課,此次五十音圖的課是詳細講解了五十音圖的構成和發音規律等,希望能夠幫到大家。
  • 打通網絡協議的任督二脈系列——音視頻篇之Camera2
    從5.0開始(API Level 21),可以完全控制Android設備相機的新api Camera2(android.hardware.Camera2)被引入了進來。在以前的Camera api(android.hardware.Camera)中,對相機的手動控制需要更改系統才能實現,而且api也不友好。
  • 好課分享單眼相機新手入門 單眼相機入門教程視頻高清完整
    好課分享單眼相機新手入門 單眼相機入門教程視頻高清完整(全網精品好課,低價分享給你
  • 寫給Android開發的Gradle知識體系
    Gradle入門前奏Groovy快速入門看這篇就夠了看似無用,實則重要的Gradle Wrapper通俗易懂的Gradle插件講解通俗易懂的自定義Gradle插件講解1.什麼是Gradle的Android插件在通俗易懂的Gradle插件講解這篇文章中我們知道,Gradle有很多插件,為了支持Android
  • Android Jetpack CameraX 庫 Beta 版正式發布!
    CameraX是一個Jetpack支持庫,旨在幫助您簡化相機應用的開發工作。它提供一致且易於使用的API界面,適用於大多數Android設備,並可向後兼容至Android5.0(API級別21)。CameraX的Beta版本正式發布,我們向為此作出貢獻的全體開發者社區成員致謝,這是我們共同努力的結果。
  • 教程丨攻克大舌音、顫音、彈舌(入門篇)
    即使發不出正確的音,也已經理解了原理,缺少的是方法。第三天,正好看到李笑來老師「用筷子」教學,依葫蘆畫瓢,寫下大舌音的重點。大舌音就是震動聲帶往外吹氣,氣流震動舌尖發出來的聲音。重點:舌尖放鬆。震動聲帶往外吹氣。接下來繼續嘗試,不斷練習,分開練,合起來練,舌頭都練麻了。發現自己的問題是舌頭無法放鬆。
  • 從零開始的Android新項目7 - Data Binding入門篇
    原文:http://blog.zhaiyifan.cn/2016/06/16/android-new-project-from-0-p7/本文是MarkZhai同學系列文章的第7篇,早前已發布,為配合今天推送的 《從零開始的Android新項目8 - Data Binding高級篇》,因此在這裡轉載給還沒看過的同學,讀完此文後,可以繼續閱讀公眾號推送的最新第8篇。
  • 【學習】Android入門開發​​2-5RadioButton
    package com.example.helloworld;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.widget.RadioButton;import android.widget.RadioGroup;import
  • Android 11強制用戶使用內置相機應用 谷歌讓安卓更封閉了嗎?
    在即將到來的 Android 11 版本中,用戶將無法選擇第三方相機應用,為其他應用拍攝照片或視頻。換言之,用戶將只能使用內置相機應用。而由於這些 App 本身並不提供拍攝功能,因此用戶可以選擇調用原生相機應用或第三方相機應用拍攝上傳圖像。這背後涉及到的就是 Android 的 Intent 系統。
  • Android 11 強制用戶使用內置相機應用,谷歌讓安卓更封閉了嗎?
    在即將到來的 Android 11 版本中,用戶將無法選擇第三方相機應用,為其他應用拍攝照片或視頻。換言之,用戶將只能使用內置相機應用。 從 Android 11 版本開始,只有預安裝的系統相機應用才能響應以下 Intent 操作: android.media.action.VIDEO_CAPTURE android.media.action.IMAGE_CAPTURE android.media.action.IMAGE_CAPTURE_SECURE
  • 最佳初學者單眼相機推薦:7款入門級單眼相機
    你剛剛接觸攝影,覺得手機已經滿足不了你的拍攝需求,正想買一款入門級單眼相機嗎?那麼這篇文章將問你提供購買建議。現在無反相機如日中天,但單眼相機遠遠沒有到消亡的境地,與大多數無反相機相比,單眼相機仍然有著強大的拍攝能力和操控性能,以及出色的物理處理能力和電池續航。