项目实训—基于AI的智能视频剪辑器(九)展示视频画面帧并进行视频剪切


前言

对于后端返回的每一个视频片段,前端需要保证用户可以对其进行剪切微调,具体的实现效果如下:
在这里插入图片描述
这里可以将整个过程拆解以下几个步骤:

  • 视频全部画面帧的获取与显示
  • 视频滑动选取框 RangeSeekBarView 的实现
  • 根据起始终止时间进行视频的截取

视频全部画面帧的获取与显示

首先整个页面的布局 xml 文件如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="@color/black">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="4">

        <com.dueeeke.videoplayer.player.VideoView
            android:id="@+id/mVideoView"
            android:layout_width="wrap_content"
            android:layout_height="300dp"
            app:layout_constraintDimensionRatio="16:10"
            android:layout_centerInParent="true"/>
        <TextView
            android:id="@+id/mTvOk"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="完成"
            android:textSize="15sp"
            android:padding="10px"
            android:layout_alignParentRight="true"
            android:layout_marginTop="20dp"
            android:layout_marginRight="15dp"
            android:textColor="@color/xui_btn_blue_normal_color"/>

        <TextView
            android:id="@+id/mTvCancel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="取消"
            android:textSize="15sp"
            android:padding="10px"
            android:layout_alignParentLeft="true"
            android:layout_marginTop="20dp"
            android:layout_marginLeft="15dp"
            android:textColor="@android:color/white"/>

    </RelativeLayout>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="2"
        android:orientation="vertical">
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true">
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/mRecyclerView"
                android:layout_width="match_parent"
                android:layout_height="50dp"
                android:paddingLeft="20dp"
                android:paddingRight="20dp"
                android:clipToPadding="false"
                android:layout_marginTop="10dp" />
            <com.xuexiang.easycut.component.RangeSeekBarView
                android:id="@+id/mRangeSeekBarView"
                android:layout_width="match_parent"
                android:layout_height="60dp"
                android:layout_marginLeft="20dp"
                android:layout_marginRight="20dp"/>
            <!--为两端的空间增加蒙层start-->
            <View
                android:layout_width="20dp"
                android:layout_height="60dp"
                android:background="@color/shadow_color"/>
            <View
                android:layout_width="20dp"
                android:layout_height="60dp"
                android:layout_alignParentRight="true"
                android:background="@color/shadow_color"/>
            <!--为两端的空间增加蒙层end-->
        </RelativeLayout>

    </RelativeLayout>
</LinearLayout>

上部首先是一个视频播放器 VideoView,底部是 RecyclerView 展示全部视频画面帧,以及自定义的 RangeSeekBarView 来完成滑动时长截取

应用到 RecyclerView,则需要对应的适配器来显示数据,定义 FramesAdapter 完成适配画面帧到页面的适配,这里的子 View 是 ImageView,表示画面帧图片

public class FramesAdapter extends RecyclerView.Adapter<FramesAdapter.ViewHolder> {
    private List<String> list = new ArrayList<>();
    private int mWidth = Utils.dp2px(35f);

    public FramesAdapter(){

    }
    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.frames_item_layout,parent,false));
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        Glide.with(holder.mIv.getContext()).load(list.get(position)).into(holder.mIv);
        ViewGroup.LayoutParams layoutParams = holder.mIv.getLayoutParams();
        layoutParams.width = mWidth;
        holder.mIv.setLayoutParams(layoutParams);
    }

    @Override
    public int getItemCount() {
        return list.size();
    }

    public void updateList(@NotNull List<String> list) {
        this.list.clear();
        this.list.addAll(list);
        notifyDataSetChanged();
    }

    public void updateItem(int position, @NotNull String outfile) {
        this.list.set(position,outfile);
        notifyItemChanged(position);
    }

    public void setItemWidth(int mWidth) {
        this.mWidth = mWidth;
    }

    public class ViewHolder extends RecyclerView.ViewHolder{

        private final ImageView mIv;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            mIv = itemView.findViewById(R.id.mIv);
        }
    }
}

InitViews 方法中首先添加初始化视频播放器和 RecyclerView 的代码

        // VideoView 
        mVideoView = binding.mVideoView;
        StandardVideoController controller = new StandardVideoController(this.getContext());
        controller.setEnableOrientation(true);
        PrepareView prepareView = new PrepareView(this.getContext());//准备播放界面
        prepareView.setClickStart();
        ImageView thumb = prepareView.findViewById(R.id.thumb);//封面图
        Glide.with(this).setDefaultRequestOptions(
                new RequestOptions()
                        .frame(0)
                        .centerCrop()
        ).load(video_url_work).placeholder(android.R.color.darker_gray).into(thumb);
        controller.addControlComponent(prepareView);
        controller.addControlComponent(new CompleteView(this.getContext()));//自动完成播放界面
        controller.addControlComponent(new ErrorView(this.getContext()));//错误界面
        TitleView titleView = new TitleView(this.getContext());//标题栏
        controller.addControlComponent(titleView);
        VodControlView vodControlView = new VodControlView(this.getContext());//点播控制条
        controller.addControlComponent(vodControlView);
        GestureView gestureControlView = new GestureView(this.getContext());//滑动控制视图
        controller.addControlComponent(gestureControlView);
        mVideoView.setVideoController(controller);
        mVideoView.addOnStateChangeListener(mOnStateChangeListener);
        mVideoView.setUrl(video_url_work);
        mVideoView.start();

        // RecyclerView
        mRecyclerView = binding.mRecyclerView;
        mAdapter = new FramesAdapter();
        LinearLayoutManager mLinearLayoutManager  = new LinearLayoutManager(getContext());
        mLinearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
        mRecyclerView.setLayoutManager(mLinearLayoutManager);
        mRecyclerView.setAdapter(mAdapter);

预计1s获取一张画面帧,因此需要首先获得视频的总时长,这里需要注意,只有 VideoView 完成预备状态后才能获得正确的时长,因此这里要在状态监听计算 mFrames

    private VideoView.OnStateChangeListener mOnStateChangeListener = new VideoView.SimpleOnStateChangeListener() {
        @Override
        public void onPlayerStateChanged(int playerState) {
            switch (playerState) {
                case VideoView.PLAYER_NORMAL://小屏
                    break;
                case VideoView.PLAYER_FULL_SCREEN://全屏
                    break;
            }
        }

        @Override
        public void onPlayStateChanged(int playState) {
            switch (playState) {
                case VideoView.STATE_IDLE:
                    break;
                case VideoView.STATE_PREPARING:
                    break;
                case VideoView.STATE_PREPARED:
                    mFrames = mVideoView.getDuration() / 1000;
                    gotoGetFrameAtTime(0);
                    break;
                case VideoView.STATE_PLAYING:
                    break;
                case VideoView.STATE_PAUSED:
                    break;
                case VideoView.STATE_BUFFERING:
                    break;
                case VideoView.STATE_BUFFERED:
                    break;
                case VideoView.STATE_PLAYBACK_COMPLETED:
                    break;
                case VideoView.STATE_ERROR:
                    break;
            }
        }
    };

gotoGetFrameAtTime(int time) 方法就是获取视频中 time 对应时间的这一帧,需要通过 ffmpeg 命令来获取

    // 获取画面帧
    private void gotoGetFrameAtTime(int time){
        if(time >= mFrames)
            return;
        String outfile = frames_path + "/" + time + ".jpg";
        String cmd = "ffmpeg -ss " + time + " -i " + video_url_work + " -preset " + "ultrafast" + " -frames:v 1 -f image2 -s " + mWidth + "x" + mHeight + " -y " + outfile;
        fFmpegCmd.ffmpeg_cmd(cmd);
        if(time == 0){
            for (int i = 0; i<mFrames; i++) {
                list.add(outfile);
            }
            mAdapter.updateList(list);
        }else{
            list.set(time, outfile);
            mAdapter.updateItem(time, outfile);
        }
        gotoGetFrameAtTime(time + 1);
    }

视频滑动选取框 RangeSeekBarView 的实现

RangeSeekBarView 实现的基本思想就是设置监听,获取当前视频选中的起始时间和终止时间,通过 onDraw 方法重绘 RangeSeekBarView 在 RecyclerView 的位置以及时长显示

public class RangeSeekBarView extends View {
    private static final String TAG = RangeSeekBarView.class.getSimpleName();
    public static final int INVALID_POINTER_ID = 255;
    public static final int ACTION_POINTER_INDEX_MASK = 0x0000ff00, ACTION_POINTER_INDEX_SHIFT = 8;
    private static final int TextPositionY = Utils.dp2px(7);
    private static final int paddingTop = Utils.dp2px(10);
    private int mActivePointerId = INVALID_POINTER_ID;

    private long mMinShootTime = 3*1000;//最小剪辑3s,默认
    private double absoluteMinValuePrim, absoluteMaxValuePrim;
    private double normalizedMinValue = 0d;//点坐标占总长度的比例值,范围从0-1
    private double normalizedMaxValue = 1d;//点坐标占总长度的比例值,范围从0-1
    private double normalizedMinValueTime = 0d;
    private double normalizedMaxValueTime = 1d;// normalized:规格化的--点坐标占总长度的比例值,范围从0-1
    private int mScaledTouchSlop;
    private Bitmap thumbImageLeft;
    private Bitmap thumbImageRight;
    private Bitmap thumbPressedImage;
    private Paint paint;
    private Paint rectPaint;
    private final Paint mVideoTrimTimePaintL = new Paint();
    private final Paint mVideoTrimTimePaintR = new Paint();
    private final Paint mShadow = new Paint();
    private int thumbWidth;
    private float thumbHalfWidth;
    private final float padding = 0;
    private long mStartPosition = 0;
    private long mEndPosition = 0;
    private float thumbPaddingTop = 0;
    private boolean isTouchDown;
    private float mDownMotionX;
    private boolean mIsDragging;
    private Thumb pressedThumb;
    private boolean isMin;
    private double min_width = 1;//最小裁剪距离
    private boolean notifyWhileDragging = false;
    private OnRangeSeekBarChangeListener mRangeSeekBarChangeListener;
    private int whiteColorRes = getContext().getResources().getColor(R.color.white);

    public enum Thumb {
        MIN, MAX
    }

    public RangeSeekBarView(Context context) {
        this(context,null);
    }

    public RangeSeekBarView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public RangeSeekBarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.absoluteMinValuePrim = 0*1000;
        this.absoluteMaxValuePrim = 10*1000;
        setFocusable(true);
        setFocusableInTouchMode(true);
        init();
    }

    private void init() {
//        mScaledTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
        thumbImageLeft = BitmapFactory.decodeResource(getResources(), R.drawable.ic_video_thumb_handle);

        int width = thumbImageLeft.getWidth();
        int height = thumbImageLeft.getHeight();
        int newWidth = Utils.dp2px(12.5f);
        int newHeight = Utils.dp2px(50f);
        float scaleWidth = newWidth * 1.0f / width;
        float scaleHeight = newHeight * 1.0f / height;
        Matrix matrix = new Matrix();
        matrix.postScale(scaleWidth, scaleHeight);
        thumbImageLeft = Bitmap.createBitmap(thumbImageLeft, 0, 0, width, height, matrix, true);
        thumbImageRight = thumbImageLeft;
        thumbPressedImage = thumbImageLeft;
        thumbWidth = newWidth;
        thumbHalfWidth = thumbWidth / 2f;
        int shadowColor = getContext().getResources().getColor(R.color.shadow_color);
        mShadow.setAntiAlias(true);
        mShadow.setColor(shadowColor);

        paint = new Paint(Paint.ANTI_ALIAS_FLAG);
        rectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        rectPaint.setStyle(Paint.Style.FILL);
        rectPaint.setColor(whiteColorRes);

        mVideoTrimTimePaintL.setStrokeWidth(3);
        mVideoTrimTimePaintL.setARGB(255, 51, 51, 51);
        mVideoTrimTimePaintL.setTextSize(28);
        mVideoTrimTimePaintL.setAntiAlias(true);
        mVideoTrimTimePaintL.setColor(whiteColorRes);
        mVideoTrimTimePaintL.setTextAlign(Paint.Align.LEFT);

        mVideoTrimTimePaintR.setStrokeWidth(3);
        mVideoTrimTimePaintR.setARGB(255, 51, 51, 51);
        mVideoTrimTimePaintR.setTextSize(28);
        mVideoTrimTimePaintR.setAntiAlias(true);
        mVideoTrimTimePaintR.setColor(whiteColorRes);
        mVideoTrimTimePaintR.setTextAlign(Paint.Align.RIGHT);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = 300;
        if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(widthMeasureSpec)) {
            width = MeasureSpec.getSize(widthMeasureSpec);
        }
        int height = 120;
        if (MeasureSpec.UNSPECIFIED != MeasureSpec.getMode(heightMeasureSpec)) {
            height = MeasureSpec.getSize(heightMeasureSpec);
        }
        setMeasuredDimension(width, height);
    }

    @SuppressLint("DrawAllocation")
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        float bg_middle_left = 0;
        float bg_middle_right = getWidth() - getPaddingRight();
        float rangeL = normalizedToScreen(normalizedMinValue);
        float rangeR = normalizedToScreen(normalizedMaxValue);
        Rect leftRect = new Rect((int) bg_middle_left, getHeight(), (int) rangeL, 0);
        Rect rightRect = new Rect((int) rangeR, getHeight(), (int) bg_middle_right, 0);
        canvas.drawRect(leftRect, mShadow);
        canvas.drawRect(rightRect, mShadow);

        //上边框
        canvas.drawRect(rangeL + thumbHalfWidth, thumbPaddingTop + paddingTop, rangeR - thumbHalfWidth, thumbPaddingTop + Utils.dp2px(2) + paddingTop, rectPaint);

        //下边框
        canvas.drawRect(rangeL + thumbHalfWidth, getHeight() - Utils.dp2px(2), rangeR - thumbHalfWidth, getHeight(), rectPaint);

        //画左边thumb
        drawThumb(normalizedToScreen(normalizedMinValue), false, canvas, true);

        //画右thumb
        drawThumb(normalizedToScreen(normalizedMaxValue), false, canvas, false);

        //绘制文字
        drawVideoTrimTimeText(canvas);
    }

    private void drawThumb(float screenCoord, boolean pressed, Canvas canvas, boolean isLeft) {
        canvas.drawBitmap(pressed ? thumbPressedImage : (isLeft ? thumbImageLeft : thumbImageRight), screenCoord - (isLeft ? 0 : thumbWidth), paddingTop, paint);
    }

    private void drawVideoTrimTimeText(Canvas canvas) {
        String leftThumbsTime = Utils.convertSecondsToTime(mStartPosition);
        String rightThumbsTime = Utils.convertSecondsToTime(mEndPosition);
        canvas.drawText(leftThumbsTime, normalizedToScreen(normalizedMinValue), TextPositionY, mVideoTrimTimePaintL);
        canvas.drawText(rightThumbsTime, normalizedToScreen(normalizedMaxValue), TextPositionY, mVideoTrimTimePaintR);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (isTouchDown) {
            return super.onTouchEvent(event);
        }
        if (event.getPointerCount() > 1) {
            return super.onTouchEvent(event);
        }
        if (!isEnabled()) return false;
        if (absoluteMaxValuePrim <= mMinShootTime) {
            return super.onTouchEvent(event);
        }
        int pointerIndex;// 记录点击点的index
        final int action = event.getAction();
        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                //记住最后一个手指点击屏幕的点的坐标x,mDownMotionX
                mActivePointerId = event.getPointerId(event.getPointerCount() - 1);
                pointerIndex = event.findPointerIndex(mActivePointerId);
                mDownMotionX = event.getX(pointerIndex);
                // 判断touch到的是最大值thumb还是最小值thumb
                pressedThumb = evalPressedThumb(mDownMotionX);
                if (pressedThumb == null) return super.onTouchEvent(event);
                setPressed(true);// 设置该控件被按下了
                onStartTrackingTouch();// 置mIsDragging为true,开始追踪touch事件
                trackTouchEvent(event);
                attemptClaimDrag();
                if (mRangeSeekBarChangeListener != null) {
                    mRangeSeekBarChangeListener.onRangeSeekBarValuesChanged(this, getSelectedMinValue(), getSelectedMaxValue(), MotionEvent.ACTION_DOWN, isMin, pressedThumb);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (pressedThumb != null) {
                    if (mIsDragging) {
                        trackTouchEvent(event);
                    } else {
                        // Scroll to follow the motion event
                        pointerIndex = event.findPointerIndex(mActivePointerId);
                        final float x = event.getX(pointerIndex);// 手指在控件上点的X坐标
                        // 手指没有点在最大最小值上,并且在控件上有滑动事件
                        if (Math.abs(x - mDownMotionX) > mScaledTouchSlop) {
                            setPressed(true);
                            invalidate();
                            onStartTrackingTouch();
                            trackTouchEvent(event);
                            attemptClaimDrag();
                        }
                    }
                    if (notifyWhileDragging && mRangeSeekBarChangeListener != null) {
                        mRangeSeekBarChangeListener.onRangeSeekBarValuesChanged(this, getSelectedMinValue(), getSelectedMaxValue(), MotionEvent.ACTION_MOVE, isMin, pressedThumb);
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                if (mIsDragging) {
                    trackTouchEvent(event);
                    onStopTrackingTouch();
                    setPressed(false);
                } else {
                    onStartTrackingTouch();
                    trackTouchEvent(event);
                    onStopTrackingTouch();
                }

                invalidate();
                if (mRangeSeekBarChangeListener != null) {
                    mRangeSeekBarChangeListener.onRangeSeekBarValuesChanged(this, getSelectedMinValue(), getSelectedMaxValue(), MotionEvent.ACTION_UP, isMin,
                            pressedThumb);
                }
                pressedThumb = null;// 手指抬起,则置被touch到的thumb为空
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                final int index = event.getPointerCount() - 1;
                // final int index = ev.getActionIndex();
                mDownMotionX = event.getX(index);
                mActivePointerId = event.getPointerId(index);
                invalidate();
                break;
            case MotionEvent.ACTION_POINTER_UP:
                onSecondaryPointerUp(event);
                invalidate();
                break;
            case MotionEvent.ACTION_CANCEL:
                if (mIsDragging) {
                    onStopTrackingTouch();
                    setPressed(false);
                }
                invalidate(); // see above explanation
                break;
            default:
                break;
        }
        return true;
    }

    private void onSecondaryPointerUp(MotionEvent ev) {
        final int pointerIndex = (ev.getAction() & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT;
        final int pointerId = ev.getPointerId(pointerIndex);
        if (pointerId == mActivePointerId) {
            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
            mDownMotionX = ev.getX(newPointerIndex);
            mActivePointerId = ev.getPointerId(newPointerIndex);
        }
    }

    private void trackTouchEvent(MotionEvent event) {
        if (event.getPointerCount() > 1) return;
        final int pointerIndex = event.findPointerIndex(mActivePointerId);// 得到按下点的index
        float x = 0;
        try {
            x = event.getX(pointerIndex);
        } catch (Exception e) {
            return;
        }
        if (Thumb.MIN.equals(pressedThumb)) {
            // screenToNormalized(x)-->得到规格化的0-1的值
            setNormalizedMinValue(screenToNormalized(x, 0));
        } else if (Thumb.MAX.equals(pressedThumb)) {
            setNormalizedMaxValue(screenToNormalized(x, 1));
        }
    }

    private double screenToNormalized(float screenCoord, int position) {
        int width = getWidth();
        if (width <= 2 * padding) {
            // prevent division by zero, simply return 0.
            return 0d;
        } else {
            isMin = false;
            double current_width = screenCoord;
            float rangeL = normalizedToScreen(normalizedMinValue);
            float rangeR = normalizedToScreen(normalizedMaxValue);
            double min = mMinShootTime / (absoluteMaxValuePrim - absoluteMinValuePrim) * (width - thumbWidth * 2);

            if (absoluteMaxValuePrim > 5 * 60 * 1000) {//大于5分钟的精确小数四位
                DecimalFormat df = new DecimalFormat("0.0000");
                min_width = Double.parseDouble(df.format(min));
            } else {
                min_width = Math.round(min + 0.5d);
            }
            if (position == 0) {
                if (isInThumbRangeLeft(screenCoord, normalizedMinValue, 0.5)) {
                    return normalizedMinValue;
                }

                float rightPosition = (getWidth() - rangeR) >= 0 ? (getWidth() - rangeR) : 0;
                double left_length = getValueLength() - (rightPosition + min_width);

                if (current_width > rangeL) {
                    current_width = rangeL + (current_width - rangeL);
                } else if (current_width <= rangeL) {
                    current_width = rangeL - (rangeL - current_width);
                }

                if (current_width > left_length) {
                    isMin = true;
                    current_width = left_length;
                }

                if (current_width < thumbWidth * 2 / 3) {
                    current_width = 0;
                }

                double resultTime = (current_width - padding) / (width - 2 * thumbWidth);
                normalizedMinValueTime = Math.min(1d, Math.max(0d, resultTime));
                double result = (current_width - padding) / (width - 2 * padding);
                return Math.min(1d, Math.max(0d, result));// 保证该该值为0-1之间,但是什么时候这个判断有用呢?
            } else {
                if (isInThumbRange(screenCoord, normalizedMaxValue, 0.5)) {
                    return normalizedMaxValue;
                }

                double right_length = getValueLength() - (rangeL + min_width);
                if (current_width > rangeR) {
                    current_width = rangeR + (current_width - rangeR);
                } else if (current_width <= rangeR) {
                    current_width = rangeR - (rangeR - current_width);
                }

                double paddingRight = getWidth() - current_width;

                if (paddingRight > right_length) {
                    isMin = true;
                    current_width = getWidth() - right_length;
                    paddingRight = right_length;
                }

                if (paddingRight < thumbWidth * 2 / 3) {
                    current_width = getWidth();
                    paddingRight = 0;
                }

                double resultTime = (paddingRight - padding) / (width - 2 * thumbWidth);
                resultTime = 1 - resultTime;
                normalizedMaxValueTime = Math.min(1d, Math.max(0d, resultTime));
                double result = (current_width - padding) / (width - 2 * padding);
                return Math.min(1d, Math.max(0d, result));
            }
        }
    }

    private int getValueLength() {
        return (getWidth() - 2 * thumbWidth);
    }

    /**
     * 计算位于哪个Thumb内
     *
     * @param touchX touchX
     * @return 被touch的是空还是最大值或最小值
     */
    private Thumb evalPressedThumb(float touchX) {
        Thumb result = null;
        boolean minThumbPressed = isInThumbRange(touchX, normalizedMinValue, 2);// 触摸点是否在最小值图片范围内
        boolean maxThumbPressed = isInThumbRange(touchX, normalizedMaxValue, 2);
        if (minThumbPressed && maxThumbPressed) {
            // 如果两个thumbs重叠在一起,无法判断拖动哪个,做以下处理
            // 触摸点在屏幕右侧,则判断为touch到了最小值thumb,反之判断为touch到了最大值thumb
            result = (touchX / getWidth() > 0.5f) ? Thumb.MIN : Thumb.MAX;
        } else if (minThumbPressed) {
            result = Thumb.MIN;
        } else if (maxThumbPressed) {
            result = Thumb.MAX;
        }
        return result;
    }

    private boolean isInThumbRange(float touchX, double normalizedThumbValue, double scale) {
        // 当前触摸点X坐标-最小值图片中心点在屏幕的X坐标之差<=最小点图片的宽度的一般
        // 即判断触摸点是否在以最小值图片中心为原点,宽度一半为半径的圆内。
        return Math.abs(touchX - normalizedToScreen(normalizedThumbValue)) <= thumbHalfWidth * scale;
    }

    private boolean isInThumbRangeLeft(float touchX, double normalizedThumbValue, double scale) {
        // 当前触摸点X坐标-最小值图片中心点在屏幕的X坐标之差<=最小点图片的宽度的一般
        // 即判断触摸点是否在以最小值图片中心为原点,宽度一半为半径的圆内。
        return Math.abs(touchX - normalizedToScreen(normalizedThumbValue) - thumbWidth) <= thumbHalfWidth * scale;
    }

    /**
     * 试图告诉父view不要拦截子控件的drag
     */
    private void attemptClaimDrag() {
        if (getParent() != null) {
            getParent().requestDisallowInterceptTouchEvent(true);
        }
    }

这里需要注意到,除了拖动 RangeSeekBarView 需要进行重绘外,由于还有当前起止时间的显示,RecyclerView 的滑动也需要对 RangeSeekBarView 进行重绘。因此 initViews 的代码中需要增加两个组件的监听设置

        // RecyclerView 的滑动监听
        mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                LinearLayoutManager lm = (LinearLayoutManager)recyclerView.getLayoutManager();
                mFirstPosition = lm.findFirstVisibleItemPosition();
                mMinTime = mRangeSeekBarView.getSelectedMinValue() + (mFirstPosition * 1000);
                mMaxTime = mRangeSeekBarView.getSelectedMaxValue() + (mFirstPosition * 1000);
                mRangeSeekBarView.setStartEndTime(mMinTime, mMaxTime);
                mRangeSeekBarView.invalidate();
                // 视频播放器跳转到剪辑位置
                mVideoView.seekTo((int)mMinTime);
            }
        });

        // RangeSeekBarView 的拖动监听
        mRangeSeekBarView.setSelectedMinValue(mMinTime);
        mRangeSeekBarView.setSelectedMaxValue(mMaxTime);
        mRangeSeekBarView.setStartEndTime(mMinTime, mMaxTime);
        mRangeSeekBarView.setNotifyWhileDragging(true);
        mRangeSeekBarView.setOnRangeSeekBarChangeListener(new RangeSeekBarView.OnRangeSeekBarChangeListener(){
            @Override
            public void onRangeSeekBarValuesChanged(RangeSeekBarView bar, long minValue, long maxValue, int action, boolean isMin, RangeSeekBarView.Thumb pressedThumb) {
                mMinTime = minValue + (mFirstPosition * 1000);
                mMaxTime = maxValue + (mFirstPosition * 1000);
                mRangeSeekBarView.setStartEndTime(mMinTime, mMaxTime);
                // 视频播放器跳转到剪辑位置
                mVideoView.seekTo((int)mMinTime);
            }
        });

根据起始终止时间进行视频的截取

当用户点击保存按钮后,需要正式对视频进行切剪,并删除中间产生的画面帧

    private void trimVideo(){
        String outfile = work_path;
        long start =  mMinTime/1000;
        long end =  mMaxTime/1000;
        String cmd = "ffmpeg -ss " + start + " -to " + end + " -accurate_seek" + " -i " + video_url_work + " -to " + (end - start) + " -preset " + "superfast" + " -crf 23 -c:a copy -avoid_negative_ts 0 -y " + outfile;
        fFmpegCmd.ffmpeg_cmd(cmd);
        // 删除所有帧
        File dir = new File(frames_path);
        File[] files = dir.listFiles();//文件夹下的所有文件或文件夹
        if (files != null){
            for (int i = 0; i < files.length; i++) {
                files[i].delete();
            }
        }
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值