在短视频app源码开发过程中,会有从本地选择视频,然后加上视频水印、美白、滤镜等效果,再进行发布的需求。所以,今天就来了解一下如何给本地视频加上视频水印和美颜效果。
Android端短视频app源码中播放本地视频, 我们可以用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类
首先实现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);
}
然后同样是在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绘制视频水印,如果当前短视频app源码是开启了美颜的话(通过isBeauty进行判断)就通过mBeautyFilter滤镜进行美颜效果的绘制,然后通过mShow进行显示绘制
然后就是对外提供的美颜效果的开关
/**切换开启美白效果*/
public void switchBeauty(){
isBeauty = !isBeauty;
}
VideoDrawer类基本上就是这些,跟CameraDrawer的添加水印和美白效果的方式完全一样,但是少了视频录制的相关过程,因为我们对本地视频的处理并不是实时录制的,而是后面才进行录制,所以其实更加简单了。
MediaPlayerWrapper类
然后就是MediaPlayerWrapper类,该类其实是MediaPlayer这个系统类的一个包装类,播放视频本质上使用的还是MediaPlayer类,不过我们在该类的基础上添加了一些新的功能。为了功能的扩展,其实该类的主要变化是,提供了无缝播放多个视频的功能,利用List进行多个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处理之后,再保存成新的视频文件。
视频的硬解码
之前我们已经说过,并不会涉及到一些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();
}