前言

         在上两篇文章中,我们分别实现了通过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地址


Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐