前言:闲来无事,想自己做一个视频编辑器,能够满足自己本身日常需要,而不是依赖于其他商业的app,部分功能用的是七牛提供的短视频sdk,对比了阿里,腾讯的短视频sdk,感觉七牛提供的sdk功能强大一些,但是后面真正用起来的时候,发现很多明显的bug,现在只用七牛能用的功能咯
整个app主要有的功能就是:视频裁切,视频合成,视频中添加文字,语音,图片,涂鸦等功能,使用起来也是超级的方便,直接秒杀其他收费类app。
因为本项目开源,代码公开,所以只讲一讲项目中的难点,以及在开发的时候需要注意的地方
本项目主要的难点在于:
1,需要想要像本人那样,在同一个页面添加多个视频,裁切后拼接预览,PLShortVideoEditor只能实例化一次,七牛裁切使用的是GLSurfaceView。而他在Acivitiy中只能存在一个,并且需要渲染器,所以后面决定了使用fragment,在fragment里面渲染,后面再把PLShortVideoEditor传入到Acivitiy
/**
* @dec fragment中的实例化七牛视频类PLShortVideoEditor
* @author fanqie
* @date 2018/8/28 16:28
*/
private void initShortVideoEditor(String mMp4path) {
MyLog.i(TAG, "editing file: " + mMp4path);
setting.setSourceFilepath(mMp4path);
// 视频源文件路径
setting.setDestFilepath(Config.EDITED_FILE_PATH);
// 编辑保存后,是否保留源文件
setting.setKeepOriginFile(true);
//编辑后保存的目标文件路径
//SquareGLSurfaceView srlQiqiuVideo = new SquareGLSurfaceView(context);
//mSrlQiqiuVideoInlude.removeAllViews();
//mSrlQiqiuVideoInlude.addView(srlQiqiuVideo);
mShortVideoEditor = new PLShortVideoEditor(mGlsvVideoCommon, setting);
((BekidMainActivity)getActivity()).getShortVideoEditor(mShortVideoEditor);
}
2.切换fragment的时候,需要 mShortVideoEditor.stopPlayback(); 停止视频播放,不然切换会有声音继续
3.不要想到使用多个播放器去实现1的问题,要求连续播放不同视频,不能使用多个播放器,影响性能
4.因为视频有裁切,合成是在最后执行,如果需要判断获取播放的时间点,以及裁切起始结束都应该使用0.1秒为单位,防止偏移,为什么不以秒为单位,后面会说
/**
* 获取视频播放的时间
*/
@SuppressLint("HandlerLeak")
public Handler getCurrentHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 1:
//需要监听播放的时间点,播放下一段视频 ,这个需要按照秒来计算获取,毫秒的话,可能精确不到 ?????
//采用四舍五入
int getCurrentTime = MathSwitch(mShortVideoEditor.getCurrentPosition());
int getEndTime = MathSwitch(mDataVideoList.get(getnowVideo).getEndTime());
MyLog.i("tangpeng", "getCurrentTime=" + getCurrentTime);
MyLog.i("tangpeng", "getEndTime=" + getEndTime);
if (getnowVideo < mDataVideoList.size() - 1) {
if (getCurrentTime + 1 >= getEndTime) {
//如果播放到截取的时间,播放下一个视频,算播放完成
getnowVideo++;
Log.i(TAG, "播放完成后,继续播放下一段视频=" + getnowVideo);
getVideoMsIng = mDataVideoList.get(getnowVideo).getStartTime();
MyLog.i(TAG, "后面切换的时候再补上?????????????????????????????");
// reIdlePlay();
reIdleReStartPlay();
} else {
//循环发送消息, 携带进度
msg = Message.obtain();
getCurrentHandler.removeMessages(1);
msg.what = 1;
getCurrentHandler.sendMessageDelayed(msg, delayMillisCurrent);
}
} else {
if (getCurrentTime + 1 >= getEndTime) {
MyLog.i(TAG, "播放到最后一段视频,回到第一段视频暂停");
//需要切换视频,通过传递位置,设置播放状态
reIdleReStartPlay();
} else {
//循环发送消息, 携带进度
msg = Message.obtain();
getCurrentHandler.removeMessages(1);
msg.what = 1;
getCurrentHandler.sendMessageDelayed(msg, delayMillisCurrent);
}
}
//实时播放音乐
if (mDataMusicList.size() > 0) {
for (int i = 0; i < mDataMusicList.size(); i++) {
//这里暂时是算一个视频
int mVideoStartTime = MathSwitch(mDataVideoList.get(0).getStartTime());
int voiceStartTime = MathSwitch(mDataMusicList.get(i).getStartInsertTime()) + mVideoStartTime;
int voiceEnd = MathSwitch(mDataMusicList.get(i).getEndTime()+mVideoStartTime);
MyLog.i(TAG, "voiceStartTime=" + voiceStartTime);
MyLog.i(TAG, "voiceEnd=" + voiceEnd);
MyLog.i(TAG, "getCurrentTime=" + getCurrentTime);
//播放声音
if (getCurrentTime == voiceStartTime) {
mUPlayerMusic.start(mDataMusicList.get(i).getMusicUrl(), (int) mDataMusicList.get(i).getStartTime());
} else if (getCurrentTime == voiceStartTime) {//这里需要注意,结束时间是开始播放的时间+裁切后的结束时间
mUPlayerMusic.stop();
}
}
}
//实时播放声音
if (mDataListVoice.size() > 0) {
for (int i = 0; i < mDataListVoice.size(); i++) {
//这里暂时是算一个视频
int mVideoStartTime = MathSwitch(mDataVideoList.get(0).getStartTime());
int voiceStartTime = MathSwitch(mDataListVoice.get(i).getStartInsertTime()) + mVideoStartTime;
int voiceEnd = MathSwitch(mDataListVoice.get(i).getEndTime()+mVideoStartTime);
//播放声音
if (getCurrentTime == voiceStartTime) {
mUPlayerVoice.start(mDataListVoice.get(i).getMusicUrl(), (int) mDataListVoice.get(i).getStartTime());
} else if (getCurrentTime == voiceStartTime + voiceEnd) {//这里需要注意,结束时间是开始播放的时间+裁切后的结束时间
mUPlayerVoice.stop();
}
}
}
break;
default:
break;
}
}
};
5.需要注意的地方,activty里面添加fragment,他们的生命周期是分开的,并行,并不会说执行了fragment之后再继续往下执行。需要在切换的fragment添加
@Override
public void onStop() {
super.onStop();
mShortVideoEditor.pausePlayback();
MyLog.i(TAG, "onStop");
}
6.如果有多个状态需要判断,使用枚举比int整型,更加直观,比如播放的状态就有很多种
private VideoPlayStatus mVideoPlayStatus = VideoPlayStatus.Idle;
//需要切换视频的状态
private enum VideoPlayStatus {
Idle,//默认
playPlay,//播放
pausePlay,//暂停播放
stopPlay,//停止播放
resumePlay,//从0开始播放
reStartPlay,//播放完了之后,回到第一段暂停
}
7.视频有多个,每一个需要需要单独判断再相加,不能相加后再判断,比如说总时间 (int)4.5+5.2+5.8的结果和(int)4.8+(int)5.2+(int)5.8的结果是不一样的哦
8.mShortVideoEditor.startPlayback();执行了之后再去执行其他添加编辑方法,七牛那边要求,比如添加了文字,贴图,标注等,需要是要预览需要先执行startPlayback();
9.在添加多段的视频中,可以拖动视频来排序,每一段视频可以裁切,如果删除一段视频,删除recyclerView一个item,再添加一个新的item,会出现旧的item的缓存,记得 mMenuRecyclerView.removeViewAt(position);这里把裁切视频的动画关键代码贴出来,供参考
//拖动左边
holder.mHandlerLeft.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
float viewX = v.getX();//相对于父类的x的坐标
float movedX = event.getX();//getX()即表示的点击的位置相对于本身的坐标 ,getX()会突然变大,导致偏移??????????????
// float finalX = viewX + movedX;
holder.rlGetVideoHandler.getLocationInWindow(rlGetVideoHandlerPosition);
float finalX = event.getRawX()-rlGetVideoHandlerPosition[0];
//滑动控件的位置-视频区域的位置,就是滑动控件位于视频区域的偏移量
updateHandlerLeftPosition(holder.tvFrgmentCutTime, mDurationMs, holder.handlerLeftAlpha, holder.handlerRightAlpha, holder.mFrameListView, holder.mHandlerLeft, holder.mHandlerRight, finalX,mRlVideoHandlerLeft,mSlicesTotalLength);
if(action==MotionEvent.ACTION_DOWN){
MyLog.i(TAG,"ACTION_DOWN");
holder.rlFrgmentCutFuncNormal.setVisibility(View.GONE);
holder.rlFrgmentCutFuncSelect.setVisibility(View.VISIBLE);
}
if (action == MotionEvent.ACTION_UP) {
MyLog.i(TAG,"ACTION_UP");
holder.rlFrgmentCutFuncNormal.setVisibility(View.VISIBLE);
calculateRange(holder.handlerLeftAlpha, holder.handlerRightAlpha, holder.mHandlerLeft, holder.mHandlerRight, holder.mFrameListView, mDurationMs, position);
}
return true;
}
});
public void updateHandlerLeftPosition(TextView tvFrgmentCutTime, long mDurationMs, View mHandlerLeftAlpha, View mHandlerRightAlpha, LinearLayout mFrameListView, View mHandlerLeft, View mHandlerRight, float movedPosition, RelativeLayout mRlVideoHandlerLeft, int mSlicesTotalLength) {
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) mHandlerLeft.getLayoutParams();
lp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);//因为需要有阴影效果,所以靠右对齐,这个时候的起始点其实是图标的右边
if ((movedPosition) > mHandlerRight.getX()) {//这个时候的起始点其实是图标的右边
lp.rightMargin = (int) (mRlVideoHandlerLeft.getWidth() - mHandlerRight.getX());
} else if (movedPosition < mHandlerLeft.getWidth()) {
lp.rightMargin = mRlVideoHandlerLeft.getWidth() - (mHandlerLeft.getWidth());
} else {
lp.rightMargin = (int) (mRlVideoHandlerLeft.getWidth() - movedPosition);
}
mHandlerLeft.setLayoutParams(lp);
//使用滑动的阴影
float beginPercent = 1.0f * ((mHandlerLeft.getX() + mHandlerLeft.getWidth() / 2) - mFrameListView.getX()) / mSlicesTotalLength;
MyLog.i(TAG, "beginPercent=" + beginPercent);
//获取裁切的时间
Long mSelectedBeginMs = (long) (beginPercent * mDurationMs);
tvFrgmentCutTime.setText(Tools.getTimeZone(mSelectedBeginMs) + "");
}
/**
* 获取到裁切范围
*
* @param mHandlerLeft
* @param mHandlerRight
* @param mFrameListView
* @param mDurationMs
*/
private void calculateRange(View mHandlerLeftAlpha, View mHandlerRightAlpha, View mHandlerLeft, View mHandlerRight, LinearLayout mFrameListView, long mDurationMs, int position) {
float beginPercent = 1.0f * ((mHandlerLeft.getX() + mHandlerLeft.getWidth() / 2) - mFrameListView.getX()) / mSlicesTotalLength;
float endPercent = 1.0f * ((mHandlerRight.getX() + mHandlerRight.getWidth() / 2) - mFrameListView.getX()) / mSlicesTotalLength;
beginPercent = QiniuTool.clamp(beginPercent);
endPercent = QiniuTool.clamp(endPercent);
Long mSelectedBeginMs = (long) (beginPercent * mDurationMs);
Long mSelectedEndMs = (long) (endPercent * mDurationMs);
Log.i(TAG, "begin percent: " + beginPercent + " end percent: " + endPercent);
Log.i(TAG, "mDurationMs: " + mDurationMs);
Log.i(TAG, "new range: " + mSelectedBeginMs + "-" + mSelectedEndMs);
//重新保存视频数据。裁切作品和计算时间
videobean mvideobean = new videobean();
mvideobean.setStartTime(mSelectedBeginMs);
mvideobean.setEndTime(mSelectedEndMs);
mvideobean.setVideoUrl(mDataVideoList.get(position).getVideoUrl());
mvideobean.setVideoSize((mSelectedEndMs - mSelectedBeginMs));
mvideobean.setGetAllTime(mDurationMs);
mDataVideoList.set(position, mvideobean);
// 当前的视频参数需要修改
mDurationMsAll = 0;
//总的进度条时间需要修改
for (int i = 0; i < mDataVideoList.size(); i++) {
mDurationMsAll = mDurationMsAll + mDataVideoList.get(i).getVideoSize();
}
MyLog.i(TAG, "mDurationMsAll=" + mDurationMsAll);//
((BekidMainActivity) mContext).updateSeekBar(position);
((BekidMainActivity) mContext).reIdleReStartPlay();
}
10.关于在播放的时候,获取视频播放的时间,是以ms为单位还是以s单位,以s为单位虽然好判断,但是进度条会出现一卡一卡的效果,体验不好,以ms 单位进度条会流畅很多,但是毫秒时间太短,判断逻辑处理的时间可能都不够,会错失时间点,所以最后衡量了一下,使用了0.1秒这个相对于中间值
11.视频合成的时候,需求要求是能够在指定的点插入音频,而且可以插入多段音频(需要用到ffmpeg混音,解决思路就是:1.先裁切出每一段音频,2.再把合成后的视频的音频截取出来,3.再把1和2的音频混合成一个新的音频文件,4.分离出来的无音频的视频插入3的音频文件)android音频编辑之音频合成
12.不能用第三方的播放器,进度条需要自己定义,因为可以拖动滚动条实时预览,获取进度条值的关键代码
//设置进度条拖拽的监听
mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
int getProgress = (int) (progress * delayMillisCurrent);
//判断是否是由用户拖拽发生的变化
if (fromUser) {
mPausePlayback.setImageResource(R.drawable.qa1);
mIvPlaybackPlay.setImageResource(R.drawable.q1);
Log.i(TAG, "这里是进度条是按照s的,但是视频是按照ms的所以需要转换=" + getProgress);
//需要判断是拖动到了第几段视频了
for (int i = 0; i < mDataVideoList.size(); i++) {
if (getProgress < mDataVideoList.get(i).getVideoSize()) {
getnowVideo = i;//一旦小于,就代表第几段,退出
if (getnowVideo == 0) {
getVideoMsIng = getProgress + mDataVideoList.get(i).getStartTime();//播放起始时间,需要加上裁切的时间
} else {
//选择从第几段的,多少秒开始播放视频
getVideoMsIng = getProgress + mDataVideoList.get(i).getStartTime() - mDataVideoList.get(getnowVideo - 1).getVideoSize();//
}
MyLog.i(TAG, "reIdlePlay=拖动后需要关闭声音");
mShortVideoEditor.seekTo((int) getVideoMsIng);
if (mVideoPlayStatus == VideoPlayStatus.playPlay) {
pausePlayback();
}
//暂时如果拖动的话,就先暂停视频
// onStopVoice();
// reIdlePlay();
// if (getProgress != 0) {
// handler.removeMessages(1);
// Message message = Message.obtain();
// message.what = 1;
// message.arg1 = getProgress;
// message.arg2 = (int) mDurationMsAll;
// handler.sendMessageDelayed(message, delayMillis);
// }
return;
}
}
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
13视频的裁切,拼接和合成,给视频添加滤镜,修改视频播放速度,添加声音(只能添加一段),添加文字,贴图,标注,最后合成视频,这一块功能都是用的七牛提供的sdk,这里说明一下,七牛短视频这个产品,在七牛所有的产品中,不是属于核心产品,团队也比较小,而需求也是有客户定的,如果客户反馈一个功能,他们觉得有必要,就会在下个版本中添加,而且在使用的过程中sdk功能还是很单一,不太能满足需求,
这里用到了好几个开源项目:
- 6.0权限判断 项目地址
- 封装recyclerview-swipes 项目地址
- mp3recorder 项目地址
好了最后把开源的项目地址贡献出来,如果喜欢请记得在github中star哦,
github链接地址