android 視頻裁切,拼接和合成,添加濾鏡,修改視頻播放速度,插入音頻,添加文字,貼圖,標注

前言:閑來無事,想自己做一個視頻編輯器,能夠滿足自己本身日常需要,而不是依賴于其他商業(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功能還是很單一,不太能滿足需求,

這里用到了好幾個開源項目:

好了最后把開源的項目地址貢獻出來,如果喜歡請記得在github中star哦,

github鏈接地址

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容