Android視頻編輯器(三)給本地視頻加水印和美顏濾鏡

前言

         在上兩篇文章中,我們分別實現了通過OpenGL預覽、錄製視頻,以及在預覽和錄製的時候加上視頻水印和美白效果,而作爲一個視頻編輯器,當然不僅僅是錄製視頻,也會有從本地選擇視頻,然後加上視頻水印、美白、濾鏡等效果,再進行發佈的需求。所以作爲系列文章的第三篇,我們就來實現如何給本地視頻加上視頻水印和美顏效果。
       本系列的文章包括如下:
       3、android視頻編輯器之本地視頻加美白效果和加視頻水印
       6、android視頻編輯器之通過OpenGL做本地視頻拼接
       7、android視頻編輯器之音視頻裁剪、增加背景音樂等

        要實現給本地視頻加上視頻水印和美顏效果,肯定離不開視頻編解碼和OpenGL,所以本篇文章會涉及到一些視頻編解碼的知識。當然是純Android平臺的視頻編解碼,還是不會涉及到FFmpeg。在上面的文章中,我們實現利用OpenGL處理視頻的主要思路是,從Camera獲取到數據,然後把每一幀的數據給到OpenGL,然後利用OpenGL的圖像處理技術,對每一幀的畫面進行處理之後,再將視頻數據反饋給我們,我們再通過SurfaceView顯示在屏幕上或者錄製成視頻文件。
       而這篇文章我們要實現對本地已有視頻加上美顏和水印效果,有兩個比較核心的點:
       第一個是如何實現預覽本地視頻的時候加水印和美顏效果
       第二個是如何解碼視頻,加上水印和美顏效果,再進行編碼視頻,保存到本地。
       所以本篇博客,我們就會從這個部分來,一一講解,並實現功能

預覽本地視頻的時候加上水印和美白效果

      當然,我們肯定首先要實現預覽本地視頻,然後讓用戶看有無濾鏡的區別,所以我們先來實現本地視頻的預覽。
       Android端播放本地視頻,我們可以用MediaPlayer+GLSurfaceView簡單實現(因爲我們不考慮多種音視頻格式和android低版本的兼容),而MediaPlayer可以設置一個Surface,而我們就可以通過設置這個Surface將其和OpenGL的SurfaceTexture聯繫起來,也就是又和我們從攝像頭獲取數據類似了,我們通過一個VideoDrawer來控制本地視頻的OpenGL繪製。在ViewDrawer中來加上各種濾鏡,從而實現對原視頻數據進行修改的功能


      首先跟上文中的預覽攝像頭數據一樣,我們需要先自定義一個播放視頻的View,因爲要用到OpenGL,所以該View同樣是繼承自GLSurfaceView。
   public class VideoPreviewView extends GLSurfaceView{
    
   }
     然後在初始化函數中,設置進行OpenGL初始化
   private void init(Context context) {
        setEGLContextClientVersion(2);
        setRenderer(this);
        setRenderMode(RENDERMODE_WHEN_DIRTY);
        setPreserveEGLContextOnPause(false);
        setCameraDistance(100);
        mDrawer = new VideoDrawer(context,getResources());

        //初始化Drawer和VideoPlayer
        mMediaPlayer = new MediaPlayerWrapper();
        mMediaPlayer.setOnCompletionListener(this);
   }
     上面代碼中的VideoDrawer和MediaPlayerWrapper就是控制OpenGL繪製和視頻播放的重點類,其實VideoPreviewView類和我們之前的CameraView類是完全類似的,只不過一個是從攝像頭獲取數據,一個是從視頻解碼器獲取數據而已。下面我們來分別說說mediaPlayerWrapper和VideoDrawer類。

VideoDrawer類
      該類,其實和我們之前文章中說過的CameraDrawer類,可以說基本上是一致的,首先實現GLSurfaceView.Renderer接口,當然其實你也可以自定義接口,因爲主要通過該接口的三個函數進行過程控制,而這三個函數其實都是在我們上面的VideoPreviewView裏面自己進行調用的,出於命名的容易理解,所以還是直接用Renderer接口了。
    public class VideoDrawer implements GLSurfaceView.Renderer {
    
    }
      然後,在構造函數中初始化要用到的Filter,包括美顏的MagicBeautyFilter和加水印的WaterMarkFilter
    public VideoDrawer(Context context,Resources res){
        mPreFilter = new RotationOESFilter(res);//旋轉相機操作
        mShow = new NoFilter(res);
        mBeFilter = new GroupFilter(res);
        mBeautyFilter = new MagicBeautyFilter();

        mProcessFilter=new ProcessFilter(res);      

        WaterMarkFilter waterMarkFilter = new WaterMarkFilter(res);
        waterMarkFilter.setWaterMark(BitmapFactory.decodeResource(res, R.mipmap.watermark));       

        waterMarkFilter.setPosition(0,70,0,0);
        mBeFilter.addFilter(waterMarkFilter);
    }
     上面這段代碼就不進行過多解釋了,在該系列文章的第二篇中,有比較詳細的解釋,包括美白Filter和水印Filter的實現原理,不懂的童鞋,請翻閱上篇文章。
      
     然後同樣是在onSurfaceCreated中進行紋理的創建和濾鏡的初始化
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        int texture[]=new int[1];
        GLES20.glGenTextures(1,texture,0);
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES ,texture[0]);
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
        GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
                GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
        surfaceTexture = new SurfaceTexture(texture[0]);
        mPreFilter.create();
        mPreFilter.setTextureId(texture[0]);

        mBeFilter.create();
        mProcessFilter.create();
        mShow.create();
        mBeautyFilter.init();
        mBeautyFilter.setBeautyLevel(3);//默認設置3級的美顏
    }
       在onSurfaceChanged函數中,設置視圖、紋理、濾鏡的寬高
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        viewWidth=width;
        viewHeight=height;
        GLES20.glDeleteFramebuffers(1, fFrame, 0);
        GLES20.glDeleteTextures(1, fTexture, 0);

        GLES20.glGenFramebuffers(1,fFrame,0);
        EasyGlUtils.genTexturesWithParameter(1,fTexture,0, GLES20.GL_RGBA,viewWidth,viewHeight);

        mBeFilter.setSize(viewWidth,viewHeight);
        mProcessFilter.setSize(viewWidth,viewHeight);
        mBeautyFilter.onDisplaySizeChanged(viewWidth,viewHeight);
        mBeautyFilter.onInputSizeChanged(viewWidth,viewHeight);

    }

     然後在onDrawFrame中,對每一幀的視頻數據進行處理,並且顯示
    @Override
    public void onDrawFrame(GL10 gl) {
        surfaceTexture.updateTexImage();
        EasyGlUtils.bindFrameTexture(fFrame[0],fTexture[0]);
        GLES20.glViewport(0,0,viewWidth,viewHeight);
        mPreFilter.draw();
        EasyGlUtils.unBindFrameBuffer();

        mBeFilter.setTextureId(fTexture[0]);
        mBeFilter.draw();

        if (mBeautyFilter != null && isBeauty && mBeautyFilter.getBeautyLevel() != 0){
            EasyGlUtils.bindFrameTexture(fFrame[0],fTexture[0]);
            GLES20.glViewport(0,0,viewWidth,viewHeight);
            mBeautyFilter.onDrawFrame(mBeFilter.getOutputTexture());
            EasyGlUtils.unBindFrameBuffer();
            mProcessFilter.setTextureId(fTexture[0]);
        }else {
            mProcessFilter.setTextureId(mBeFilter.getOutputTexture());
        }
        mProcessFilter.draw();

        GLES20.glViewport(0,0,viewWidth,viewHeight);
        mShow.setTextureId(mProcessFilter.getOutputTexture());
        mShow.draw();
    }
       這段的代碼其實很清晰,首先綁定Frame緩衝區和texture,然後mPreFilter進行預先繪製,通過mBeFilter繪製視頻水印,如果當前是開啓了美顏的話(通過isBeauty進行判斷)就通過mBeautyFilter濾鏡進行美顏效果的繪製,然後通過mShow進行顯示繪製

       然後就是對外提供的美顏效果的開關
    /**切換開啓美白效果*/
    public void switchBeauty(){
        isBeauty = !isBeauty;
    }
       VideoDrawer類基本上就是這些,跟CameraDrawer的添加水印和美白效果的方式完全一樣,但是少了視頻錄製的相關過程,因爲我們對本地視頻的處理並不是實時錄製的,而是後面才進行錄製,所以其實更加簡單了。

MediaPlayerWrapper類
       然後就是MediaPlayerWrapper類,該類其實是MediaPlayer這個系統類的一個包裝類,播放視頻本質上使用的還是MediaPlayer類,不過我們在該類的基礎上添加了一些新的功能。爲了功能的擴展,其實該類的主要變化是,提供了無縫播放多個視頻的功能,利用List進行多個MediaPlayer的保存,無縫切換視頻播放。不熟悉MediaPlayer的請查閱相關資料。
       我們在構造函數中,初始化了兩個ArrayList,用於保存多個Player和VideoInfo
    private List<MediaPlayer> mPlayerList;  //player list
    private List<String> mSrcList;          //video src list
    private List<VideoInfo> mInfoList;      //video info list
    public MediaPlayerWrapper() {
        mPlayerList = new ArrayList<>();
        mInfoList = new ArrayList<>();
    }
      提供了setDataSource方法,用於設置視頻的播放地址   
   public void setDataSource(List<String> dataSource) {
        this.mSrcList = dataSource;
        MediaMetadataRetriever retr = new MediaMetadataRetriever();
        for (int i = 0; i < dataSource.size(); i++) {
            VideoInfo info = new VideoInfo();
            String path=dataSource.get(i);
            retr.setDataSource(path);
            String rotation = retr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
            String width = retr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
            String height = retr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
            String duration = retr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);

            info.path=path;
            info.rotation = Integer.parseInt(rotation);
            info.width = Integer.parseInt(width);
            info.height = Integer.parseInt(height);
            info.duration = Integer.parseInt(duration);

            mInfoList.add(info);
        }
    }
       這裏的VideoInfo,其實就是自定義的一個視頻信息的bean

       然後就是prepare方法,初始化多個播放器,並且添加到mPlayerList中
    public void prepare() throws IOException {
        for (int i = 0; i < mSrcList.size(); i++) {
            MediaPlayer player = new MediaPlayer();
            player.setAudioStreamType(AudioManager.STREAM_MUSIC);
            player.setOnCompletionListener(this);
            player.setOnErrorListener(this);
            player.setOnPreparedListener(this);
            player.setDataSource(mSrcList.get(i));
            player.prepare();
            mPlayerList.add(player);
            if (i == 0) {
                mCurMediaPlayer = player;
                if (mCallback != null) {
                    mCallback.onVideoChanged(mInfoList.get(0));
                }
            }
        }
    }
       然後是視頻的start、pause和stop,我們有多個MediaPlayer當然不可能同時進行播放,所以有一個mCurmediaPlayer,用來控制當前播放的是哪個視頻。     
 public void start() {
        mCurMediaPlayer.setSurface(surface);
        mCurMediaPlayer.start();
        if (mCallback != null) {
            mCallback.onVideoStart();
        }
 }

 public void pause() {
     mCurMediaPlayer.pause();
     if (mCallback != null) {
           mCallback.onVideoPause();
     }
 }
 public void stop() {
      mCurMediaPlayer.stop();
 }
       然後,就是不同播放器的切換,當播放完一個之後,通過switchPlayer切換到下一個播放器
   @Override
    public void onCompletion(MediaPlayer mp) {
        curIndex++;
        if (curIndex >= mSrcList.size()) {
            curIndex = 0;
            if (mCallback != null) {
                mCallback.onCompletion(mp);
            }
        }
        switchPlayer(mp);

    }

    private void switchPlayer(MediaPlayer mp) {
        mp.setSurface(null);
        if (mCallback != null) {
            mCallback.onVideoChanged(mInfoList.get(curIndex));
        }
        mCurMediaPlayer = mPlayerList.get(curIndex);
        mCurMediaPlayer.setSurface(surface);
        mCurMediaPlayer.start();
    }
      上面代碼中,我們看到給當前的player設置了一個顯示錶面,surface,而這個surface就是在VideoPreviewView中設置的,將MediaPlayer和OpenGL聯繫起來的關鍵   
 public void setSurface(Surface surface) {
        this.surface = surface;
 }
     然後我們的MediaPlayerWrapper類,還有就是一些接口的定義,基本上就是這樣了。

     MediaPlayerWrapper和VideoDrawer都解釋完成了,現在我們需要來看如何在ViewPreviewView中,將它們進行聯繫,從而對本地視頻進行解碼,然後通過OpenGL進行繪製,顯示在屏幕上。
     
ViewPreviewView類
     在上面的ViewPreviewView的初始化函數中,我們setRendered,然後有三個我們很熟悉的回調函數
     onSurfaceCreated
     onSurfaceChanged
     onDrawFrame
     而我們的視頻播放的控件的核心其實也就是在這三個方法中
     首先第一個方法中,onSurfaceCreated,
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        mDrawer.onSurfaceCreated(gl,config);
        SurfaceTexture surfaceTexture = mDrawer.getSurfaceTexture();
        surfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
            @Override
            public void onFrameAvailable(SurfaceTexture surfaceTexture) {
                requestRender();
            }
        });
        Surface surface = new Surface(surfaceTexture);
        mMediaPlayer.setSurface(surface);
        try {
            mMediaPlayer.prepare();
        } catch (IOException e) {
            e.printStackTrace();
        }
        mMediaPlayer.start();
    }
     我們來解釋一下,在上面的代碼中我們主要做了些什麼,
     首先調用了ViewDrawer的onSurfaceCreated方法,上面已經說了其主要進行了紋理的創建和綁定,濾鏡的初始化等等,
     然後從ViewDrawer獲取到當前綁定的紋理SurfaceTexture。
     然後利用這個紋理,創建一個表面對象Surface,
     然後把這個Surface對象設置給MediaPlayer,然後就開始播放視頻

     然後在onSurfaceChanged和onDrawFrame方法中,主要是調用了VideoDrawer的方法
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        mDrawer.onSurfaceChanged(gl,width,height);
    }

    @Override
    public void onDrawFrame(GL10 gl) {
        mDrawer.onDrawFrame(gl);
    }
      如此方式,我們就很容易的實現了利用MediaPlayer解碼視頻,然後利用OpenGL對視頻數據進行二次處理,再顯示到我們的GLSurfaceView上面。
      當然我們這裏是一個視頻播放的控件,肯定還有一些對外提供的接口和回調函數。就不一一解釋了。然後就是在預覽界面的使用 ,這個也不多說了,主要將控件寫在xml中,然後給該控件設置視頻的播放地址,然後進行播放即可。

本地視頻解碼,OpenGL美顏,視頻數據編碼成文件

     在上一部分的內容中,我們已經實現了本地視頻的播放和加水印、美顏效果的播放。那麼這一部分內容中,我們就要實現視頻的編解碼,並且把視頻數據通過OpenGL處理之後,再保存成新的視頻文件。這個和上一篇文章有一點不同就是,上篇文章是通過Camera獲取數據,實時進行顯示,並且實時進行錄製。而現在是選定好本地視頻和效果之後再進行編解碼。

視頻的硬解碼
     之前我們已經說過,並不會涉及到一些C/C++的音視頻解碼庫,所以我們這裏解碼視頻通過的是Android本身的相關api,當然這些api很多都是4.1之後纔出現的,所以我們並不能兼容低版本,當然我也沒準備兼容低版本。我們主要使用到的api包括MediaCodec,MediaMuxer,MediaFormat等等。
     我們的主要思路是,通過MediaCodec的解碼器,將原視頻解碼成幀數據,然後通過OpenGL對視頻數據進行處理,再通過MediaCodec的編碼器對處理後的數據進行編碼,保存成一個視頻文件。

    
VideoClipper類
     我們建立一個VideoClipper的類,用於處理本地視頻
     首先,我們需要初始化兩個解碼器,兩個編碼器
   public VideoClipper() {
        try {
            videoDecoder = MediaCodec.createDecoderByType("video/avc");
            videoEncoder = MediaCodec.createEncoderByType("video/avc");
            audioDecoder = MediaCodec.createDecoderByType("audio/mp4a-latm");
            audioEncoder = MediaCodec.createEncoderByType("audio/mp4a-latm");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
      通過名字就可以看出來,我們分別初始化了音頻、視頻的解碼器和編碼器,這篇我們主要講的是視頻的操作,所以暫時不管音頻。     
      在編解碼器開始正式工作的時候,我們需要先對編解碼器進行初始化:
    private void initVideoCodec() {
        //不對視頻進行壓縮
        int encodeW = videoWidth;
        int encodeH = videoHeight;
        //設置視頻的編碼參數
        MediaFormat mediaFormat = MediaFormat.createVideoFormat("video/avc", encodeW, encodeH);
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 3000000);
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
        videoEncoder.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        inputSurface = new InputSurface(videoEncoder.createInputSurface());
        inputSurface.makeCurrent();
        videoEncoder.start();


        VideoInfo info = new VideoInfo();
        info.width = videoWidth;
        info.height = videoHeight;
        info.rotation = videoRotation;
        outputSurface = new OutputSurface(info);
        outputSurface.isBeauty(isOpenBeauty);
        videoDecoder.configure(videoFormat, outputSurface.getSurface(), null, 0);
        videoDecoder.start();//解碼器啓動
    }
       在上面的代碼中,就包括了很關鍵的代碼,就是創建了一個InputSurface和一個OutputSurface。而這兩個類原型其實來自於谷歌工程師編寫的一個音視頻處理的項目grafika。然後我們進行了一些改造。
       這兩個類主要涉及到OpenGL和EGL相關的知識點,我們暫時只講我們會涉及到部分。
       從上面的代碼,我們看到了在OutputSurface中,我們通過isBeauty(boolean isBeauty)方法設置了是否開啓美顏,而這裏的isOpenBeauty,就是VideoClipper對外提供的設置接口。   
   /** 開啓美顏 */
    public void showBeauty(){
        isOpenBeauty = true;
    }
      然後我們去看OutputSurface的isBeauty方法
    public void isBeauty(boolean isBeauty){
        mDrawer.isOpenBeauty(isBeauty);
    }
      是不是發現了一個眼熟的東西,對就是mDrawer。該mDrawer其實就是,在OuputSurface初始化的時候創建的一個VideoDrawer
    mDrawer = new VideoDrawer(MyApplication.getContext(),MyApplication.getContext().getResources());
     而這個VideoDrawer在OutputSurface中的主要用法如下,首先在setup函數中進行初始化   
   private void setup(VideoInfo info) {
     mDrawer = new VideoDrawer(MyApplication.getContext(),MyApplication.getContext().getResources());
     mDrawer.onSurfaceCreated(null,null);
     mDrawer.onSurfaceChanged(null,info.width,info.height);
    //在VideoDrawer創建surfaceTexture之後,提供出來
     mSurfaceTexture = mDrawer.getSurfaceTexture();
     mSurfaceTexture.setOnFrameAvailableListener(this);
     mSurface = new Surface(mSurfaceTexture);
   }
      主要就是先初始化,然後把他內部創建綁定的紋理,提供出來,並且創建一個Surface,這個Surface,通過下面的代碼提供出去
    /** Returns the Surface that we draw onto.*/
    public Surface getSurface() {
        return mSurface;
    }
       並且,最終是設置在瞭解碼器裏面。
    videoDecoder.configure(videoFormat, outputSurface.getSurface(), null, 0);
      如此一來,我們通過解碼器解碼的數據,就會經過OutputSurface和VideoDrawer,然後不多說,和上面一樣的,在VideoDrawer裏面對數據進行處理。加上美白和水印等
      然後在OutputSurface的drawImage方法中,調用mDrawer的onDrawFrame方法進行圖像處理    
    /** Draws the data from SurfaceTexture onto the current EGL surface.*/
    public void drawImage() {
        mDrawer.onDrawFrame(null);
    } 
      其實這裏的原理和上面的預覽視頻是一樣的
      預覽視頻是通過MediaPlayer解碼視頻,然後返回到一個Surface上面,再經VideoDrawer的處理,最終呈現到界面上。
      這裏編解碼視頻,是由我們自己初始化解碼器,對視頻進行解碼,然後通過一個Surface,把數據傳遞到VideoDrawer上進行處理,最後再通過InputSurface把處理後的數據給到編碼器,進行二次編碼,形成新的視頻文件
     然後,我們在視頻的解碼器,解出每一幀數據的時候,對數據進行OpenGL的處理,也就是調用outputSurface和inputSurface的相關方法即可    
 boolean doRender = (info.size != 0 && info.presentationTimeUs -  firstSampleTime > startPosition);
 decoder.releaseOutputBuffer(index, doRender);
 if (doRender) {
         // This waits for the image and renders it after it arrives.
         outputSurface.awaitNewImage();
         outputSurface.drawImage();
         // Send it to the encoder.
         inputSurface.setPresentationTime(info.presentationTimeUs * 1000);
         inputSurface.swapBuffers();
  }
       相應的,音視頻的編解碼代碼,網上有許多了,我們這裏暫時不做講解。可能會在後面的文章中,對這部分的內容進行補充。

     本地視頻預覽效果
<Image_1>

     處理後視頻播放效果
<Image_2>

    從截圖可以看到,其實我們這裏還存在一些小問題
    第一個就是預覽的時候水印圖片較小,而二次編碼完成的水印變大了。原因是,我們的水印是一個bitmap圖片,他的寬高的像素點是固定的,預覽時候界面的寬高是全屏的,也就是說是手機屏幕的寬高,而編碼時候,視頻的寬高是原視頻的寬高,我們並沒有對原視頻大小進行縮放,所以會存在水印大小位置不太對的情況,可以通過對水印大小進行縮放來修改,實現所見即所得。

     第二個就是,我們在VideoDrawer中對水印和美顏效果的繪製流程是先繪製水印,然後再繪製美顏效果,這樣其實不太好,因爲美顏是全局的,也會對水印的展示效果造成影響,所以最好是修改一下加美顏濾鏡和加水印的流程,來避免這個問題。可以先繪製美顏濾鏡,再加上水印。

總結

      通過上面兩部分的講解,我們很輕鬆的就實現了本地視頻預覽,然後預覽過程中加上美顏和水印,然後解碼視頻,通過OpenGL處理視頻數據,再編碼視頻。其實很核心的一個點就是MediaPlayer和視頻解碼器MediaCodec都可以傳入一個Surface,對解碼出來的數據進行接收,就給了我們通過OpenGL處理數據的機會。非常重要的一個類就是VideoDrawer,涉及到很多OpenGL的使用。
       本篇我們實現的是如何給本地視頻加上水印和美顏濾鏡,下一篇文章,就是在這兩篇的基礎上,實現給視頻加上各種亂七八糟好看或者不好看的濾鏡。
       因爲個人水平有限,難免有錯誤和不足之處,還望大家能包涵和提醒。謝謝啦!!!

其他
     項目的github地址


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章