android 视频裁切,拼接和合成,添加滤镜,修改视频播放速度,插入音频,添加文字,贴图,标注

前言:闲来无事,想自己做一个视频编辑器,能够满足自己本身日常需要,而不是依赖于其他商业的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链接地址

你可能感兴趣的:(android 视频裁切,拼接和合成,添加滤镜,修改视频播放速度,插入音频,添加文字,贴图,标注)