前言:閑來無事,想自己做一個視頻編輯器,能夠滿足自己本身日常需要,而不是依賴于其他商業(yè)的app,部分功能用的是七牛提供的短視頻sdk,對比了阿里,騰訊的短視頻sdk,感覺七牛提供的sdk功能強大一些,但是后面真正用起來的時候,發(fā)現(xiàn)很多明顯的bug,現(xiàn)在只用七牛能用的功能咯
整個app主要有的功能就是:視頻裁切,視頻合成,視頻中添加文字,語音,圖片,涂鴉等功能,使用起來也是超級的方便,直接秒殺其他收費類app。
因為本項目開源,代碼公開,所以只講一講項目中的難點,以及在開發(fā)的時候需要注意的地方
本項目主要的難點在于:
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(); 停止視頻播放,不然切換會有聲音繼續(xù)
3.不要想到使用多個播放器去實現(xiàn)1的問題,要求連續(xù)播放不同視頻,不能使用多個播放器,影響性能
4.因為視頻有裁切,合成是在最后執(zhí)行,如果需要判斷獲取播放的時間點,以及裁切起始結束都應該使用0.1秒為單位,防止偏移,為什么不以秒為單位,后面會說
/**
* 獲取視頻播放的時間
*/
@SuppressLint("HandlerLeak")
public Handler getCurrentHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 1:
//需要監(jiān)聽播放的時間點,播放下一段視頻 ,這個需要按照秒來計算獲取,毫秒的話,可能精確不到 ?????
//采用四舍五入
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, "播放完成后,繼續(xù)播放下一段視頻=" + getnowVideo);
getVideoMsIng = mDataVideoList.get(getnowVideo).getStartTime();
MyLog.i(TAG, "后面切換的時候再補上?????????????????????????????");
// reIdlePlay();
reIdleReStartPlay();
} else {
//循環(huán)發(fā)送消息, 攜帶進度
msg = Message.obtain();
getCurrentHandler.removeMessages(1);
msg.what = 1;
getCurrentHandler.sendMessageDelayed(msg, delayMillisCurrent);
}
} else {
if (getCurrentTime + 1 >= getEndTime) {
MyLog.i(TAG, "播放到最后一段視頻,回到第一段視頻暫停");
//需要切換視頻,通過傳遞位置,設置播放狀態(tài)
reIdleReStartPlay();
} else {
//循環(huán)發(fā)送消息, 攜帶進度
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,他們的生命周期是分開的,并行,并不會說執(zhí)行了fragment之后再繼續(xù)往下執(zhí)行。需要在切換的fragment添加
@Override
public void onStop() {
super.onStop();
mShortVideoEditor.pausePlayback();
MyLog.i(TAG, "onStop");
}
6.如果有多個狀態(tài)需要判斷,使用枚舉比int整型,更加直觀,比如播放的狀態(tài)就有很多種
private VideoPlayStatus mVideoPlayStatus = VideoPlayStatus.Idle;
//需要切換視頻的狀態(tài)
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();執(zhí)行了之后再去執(zhí)行其他添加編輯方法,七牛那邊要求,比如添加了文字,貼圖,標注等,需要是要預覽需要先執(zhí)行startPlayback();
9.在添加多段的視頻中,可以拖動視頻來排序,每一段視頻可以裁切,如果刪除一段視頻,刪除recyclerView一個item,再添加一個新的item,會出現(xiàn)舊的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];
//滑動控件的位置-視頻區(qū)域的位置,就是滑動控件位于視頻區(qū)域的偏移量
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);
//重新保存視頻數(shù)據(jù)。裁切作品和計算時間
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);
// 當前的視頻參數(shù)需要修改
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為單位雖然好判斷,但是進度條會出現(xiàn)一卡一卡的效果,體驗不好,以ms 單位進度條會流暢很多,但是毫秒時間太短,判斷邏輯處理的時間可能都不夠,會錯失時間點,所以最后衡量了一下,使用了0.1秒這個相對于中間值
11.視頻合成的時候,需求要求是能夠在指定的點插入音頻,而且可以插入多段音頻(需要用到ffmpeg混音,解決思路就是:1.先裁切出每一段音頻,2.再把合成后的視頻的音頻截取出來,3.再把1和2的音頻混合成一個新的音頻文件,4.分離出來的無音頻的視頻插入3的音頻文件)android音頻編輯之音頻合成
12.不能用第三方的播放器,進度條需要自己定義,因為可以拖動滾動條實時預覽,獲取進度條值的關鍵代碼
//設置進度條拖拽的監(jiān)聽
mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
int getProgress = (int) (progress * delayMillisCurrent);
//判斷是否是由用戶拖拽發(fā)生的變化
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,這里說明一下,七牛短視頻這個產(chǎn)品,在七牛所有的產(chǎn)品中,不是屬于核心產(chǎn)品,團隊也比較小,而需求也是有客戶定的,如果客戶反饋一個功能,他們覺得有必要,就會在下個版本中添加,而且在使用的過程中sdk功能還是很單一,不太能滿足需求,
這里用到了好幾個開源項目: