Android 音視頻,相機開發入門教程

若是你有學Android 音視頻,相機開發的想法,那麼這篇文章能夠做爲一篇不錯的參考文章。固然本文爲付費文章,收費10元,若是對你有用,文末讚揚繳費便可。若是沒有學習音視頻,相機的慾望,趕快走,趕快走,不要有一絲停留,由於這篇文章確實枯燥無味且毫無快感可言。若是不知道我講的是啥,先到Github項目:AndroidCamera去看下效果就知道了。html

請考慮3s趕快決定去留。java

3……android

2……git

1……github

不走,我再扯兩句:canvas

這篇是在學習相機音視頻開發的時候寫的一篇總結。因爲涉及的知識點比較多,因此其中部分知識點僅起引導做用。數組

ok,枯燥無味正式開始:緩存

這篇文章計劃寫的內容覆蓋面是很普遍的,涵蓋相機開發的大部分知識,並且我對本身寫做要求:內容儘可能精煉,不能泛泛而談。因此時間上來講很緊湊了。固然,若是文章各方面你們有看不順眼的地方,但願你們幫忙指出批評,必定虛心接受,積極改正。若是從此有機會見面,請您喝茶。bash

1. 從打開一個攝像頭提及

固然,這個對大部分人來講都是沒什麼問題的,可是該篇文章還得照顧大部分初次接觸Camera開發的小夥伴,因此請允許我在此多囉嗦一下,若是你有接觸過Camera的開發,此部分能夠跳過,直接看下一部分。微信

a. 使用Camera的步驟:

說下Camera的操做步驟,後面給出實例,請結合代碼理解分析:

  1. 獲取一個Camera實例,經過open方法,Camera.open(0),0是後置攝像頭,1表示前置攝像頭。
  2. 設置Camera的參數,好比聚焦,是否開閃光燈,預覽高寬,修改Camera的默認參數:mCamera.getParameters() 經過初始化SurfaceHolder去setPreviewDisplay(SurfaceHolder),沒有surface,Camera不能開始預覽。
  3. 調用startPreview方法開始更新預覽到surface,在拍照以前,startPreview必須調用,預覽必須開啓。
  4. 當你想開始拍照時,使用takePicture(Camera.ShutterCallback, Camera.PictureCallback, Camera.PictureCallback, Camera.PictureCallback), 等待回調提供真實的圖像數據 當拍完一張照片時,預覽(preview)將會中止,當你想要拍更多的照片時,需要再一次調用startPreview方法
  5. 當調用stopPreview方法時,將中止更新預覽的surface
  6. 當調用release方法時,將立刻釋放camera

b.使用SurfaceView預覽顯示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();
    }
}

複製代碼

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開發詳解(一)——簡單圖形的基本繪製) 下面是基本步驟:

  1. 佈局文件中添加GlSurfaceView,併爲其指定渲染器Renderer。
  2. 設置畫布大小,清除畫布內容,建立紋理對象,並指定OpenGl ES操做紋理ID。(下面會講到)
  3. 加載頂點着色器(vertex shader)和片元着色器(fragment shader)。
  4. 建立OpenGl ES程序,建立program對象,鏈接頂點和片元着色器,連接program對象。
  5. 打開相機,設置預覽佈局,開啓預覽,並經過glUseProgram()方法將程序添加到OpenGl ES環境中,獲取着色器句柄,經過glVertexAttribPointer()傳入繪製數據並啓用頂點位置句柄。
  6. 在onDrawFrame方法中更新緩衝區幀數據並經過glDrawArrays繪製到GlSurfaceView上。
  7. 操做完成後資源釋放,須要注意的是使用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)對紋理進行解綁。
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的時候再進行數據轉換了。

  1. 若是咱們要給當前紋理添加PNG素材,咱們須要對PNG這種圖片壓縮格式進行解碼操做。最終傳遞RGBA數據格式數據到OpenGl 中紋理中,固然,OpenGL還提供了三個指定函數來指定紋理glTexImage1D(), glTexImage2D(), glTexImage3D().。咱們運用到的主要2D版本,glTexImage2D();
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);
複製代碼
  1. 接來下就如上面OpenGl ES渲染流程所提到的,將紋理繪製到屏幕上。

3. 一塊兒瞭解下使用MediaCodec實現相機錄製

上面咱們將相機的預覽顯示講完了,接下里咱們講如何將錄製視頻。就目前來講,Android的錄製方式就要有下面三中:

  1. 使用MediaRecord進行錄製。(這個不講解)
  2. 使用MediaCodec進行錄製(咱們講這種) 。
  3. 使用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的使用步驟:

  1. 建立MediaFormat,並設置相關屬性,MediaFormat.KEY_COLOR_FORMAT(顏色格式),KEY_BIT_RATE(比特率),KEY_FRAME_RATE(幀速),KEY_I_FRAME_INTERVAL(關鍵幀間隔,0表示要求全部的都是關鍵幀,負值表示除第一幀外無關鍵幀)。

舒適提示

: 沒有設置以上前三個屬性你能夠能會出現如下錯誤:

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)
                      ……
複製代碼
  1. 建立一個MediaCodec的編碼器,並配置格式。
  2. 建立一個MediaMuxer來合成視頻。
  3. 經過dequeueInput/OutputBuffer()獲取輸入輸出緩衝區。
  4. 經過getInputBuffers獲取輸入隊列,而後經過queueInputBuffer把原始YUV數據送入編碼器。
  5. 經過dequeueOutputBuffer方法獲取當前編解碼狀態,根據不一樣的狀態進行處理。
  6. 再而後在輸出隊列端一樣經過dequeueOutputBuffer獲取輸出的h264流。
  7. 處理完輸出數據以後,須要經過releaseOutputBuffer把輸出buffer還給系統,從新放到輸出隊列中。
  8. 使用MediaMuxer混合。

舒適提示:下面實例是經過直接在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();
複製代碼

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

若是你以前沒有使用過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;
        }
    }
}

複製代碼

4. 瞭解下音頻錄製

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

  1. AudioRecord(基於字節流錄音) (咱們主要講這個)。
  2. MediaRecorder(基於文件錄音) :

雖然咱們這裏只講第一種,在這裏仍是講下優缺點:

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

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

1. AudioRecord的工做流程

  1. 建立AudioRecord實例,配置參數,初始化內部的音頻緩衝區。
/**
  *@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()的值就會初始化失敗。

  1. 開始採集音頻。 這個比較簡單:
AudioRecord.startRecording();//開始採集
AudioRecord.stop();//中止採集
……
AudioRecord.read(byte[] audioData, int offsetInBytes, int sizeInBytes);//讀取數據
複製代碼
  1. 開啓線程,將數據保存爲pcm文件。
  2. 中止採集,資源釋放。

關於AudioRecord錄製的音頻的例子就不在這裏貼出來了,以後項目中會接入錄音變音,降噪,增益等功能。都會在代碼中給出。

5. 瞭解下音視頻混合

前面講到了視頻和音頻的錄製,那麼如何將他們混合呢? 一樣就我所知目前有兩種方法:

  1. 使用MediaMuxer進行混合。(咱們將下這種,也是市面上最經常使用的)。
  2. 使用FFmpeg進行混合。(目前不講,後面添加背景音樂會提到)

1. 瞭解下MediaMuxer

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

2. MediaMuxer的工做流程

  1. 建立MediaMuxer對象。
  2. 添加媒體通道,並將MediaFormat添加到MediaMuxer中去。
  3. 經過start()開始混合。
  4. writeSampleData()方法向mp4文件中寫入數據。
  5. stop()混合關閉並進行資源釋放。

官方實例:

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();
複製代碼

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

這裏寫圖片描述

6. 瞭解下多段視頻拼接合成

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

  1. 使用MediaCodec,MediaExtractor,MediaMuxer.(講思路)。
  2. 使用mp4parser合成視頻。(將使用)。
  3. 使用FFMpeg來實現。(音視頻這一塊找它就沒錯了,基本沒有它實現不了的)。

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

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

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

  1. 咱們經過MediaExtractor將媒體文件分解並找到軌道及幀數據。
  2. 將分解後的數據填充到MediaCodec的緩衝區中去。
  3. 經過MediaMuxer將MediaCodec中的數據和找到的音軌進行混合。
  4. 遍歷第二個視頻文件。

差很少就是這樣滴,由於這個我是看別人是這麼作的,我偷懶用了mp4parser,因此僅能給個位提供思路了,從此有時間再瞭解下。

這裏寫圖片描述

2. 講下如何使用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;
    }
複製代碼

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

先看下咱們要實現什麼功能,以下:

這裏寫圖片描述

簡單分析下,咱們如今須要將整個視頻的部分幀拿出在下面顯示出來,而且添加上面的動態貼紙顯示。

1. 如何拿出視頻幀?

Android平臺下主要有兩種拿視頻幀的方法:

  1. 使用ThumbnailUtils,通常用來拿去視頻縮略圖。
  2. 使用MediaMetadataRetriever的getFrameAtTime()拿視頻幀(咱們用的這種方式)。
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);//全部幀拿完了
            }
複製代碼

拿完全部幀,好了,好了,下一個話題。

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類庫](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) {
            }
        });
複製代碼

下面是在個人應用中使用到的一些命令:

1. 視頻加減速命令:

設置變速值爲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
複製代碼

2. 視頻剪切命令:

ffmpeg -i /sdcard/WeiXinRecordedDemo/1515060907399/finish.mp4 -vcodec copy -acodec copy -ss 00:00:00 -t 00:00:01 /sdcard/WeiXinRecordedDemo/1515060907399/1515060998134.mp4
複製代碼

3. 視頻壓縮命令:

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;
複製代碼

4.給視頻添加背景音樂

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. 加字幕

命令:

ffmpeg -i <input> -filter_complex subtitles=filename=<SubtitleName>-y <output>
複製代碼

說明:利用libass來爲視頻嵌入字幕,字幕是直接嵌入到視頻裏的硬字幕。

6. 加水印

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複製代碼

7. 旋轉

視頻旋轉也能夠參考使用OpenCV和FastCV,固然前兩種是在線處理,若是是視頻錄製完成,咱們能夠經過mp4parser進行離線處理。參考博客Android進階之視頻錄製播放常見問題

命令:

ffmpeg -i <input> -filter_complex transpose=X -y <output>
複製代碼

說明:transpose=1爲順時針旋轉90°,transpose=2逆時針旋轉90°。

8. 參考連接及項目

在音視頻開發的路上,感謝下面的文章及項目的做者,感謝他們的無私奉獻,在前面種好大樹,讓咱們後來者乘涼。

  1. 參考學習對象(排名無前後) 雷霄驊 湖廣午王 逆流的魚yuiop小碼哥_WS 感謝四位老哥的博客,給予了我很大幫助。

  2. 拍攝錄製功能:1. grafika 2. WeiXinRecordedDemo

  3. OpenGL 系列:1. 關於OpenGl的學習:AndroidOpenGLDemo LearnOpenGL-CN 2. 關於濾鏡的話:android-gpuimage-plus-masterandroid-gpuimage

  4. 關於FFmpeg 1.FFmpeg官網2. 官方項目地址github3. [FFmpeg]ffmpeg各種參數說明與使用示例1. ffmpeg-android-java

  5. 貼紙 1. StickerView

9. 結束語

到這裏文章基本上結束了,最後想和各位說的是,實在抱歉,確實最近時間有點緊,天天來公司大部分時間在討論產品,剩下的一小部分時間不是在路上,就是在吃飯睡覺了。天天能抽半個小時寫就很不錯了。值得慶幸的是,最終它仍是完成了,但願經過本文能給你們帶來一些實質性的幫助。原本想多寫一點,儘可能寫詳細點,可是精力有限,後面的關於濾鏡,美顏,變聲,及人臉識別部分的以後會再從新整理。最後,項目地址AndroidCamera

10. 廣告

請注意,如下內容將全都是廣告:

  1. aserbao的簡書
  2. aserbao的csdn
  3. 個人同名微信公衆號aserbao,分享音視頻技術及Android開發小技巧。

這裏寫圖片描述

相關文章
相關標籤/搜索