Project training - AI based intelligent video clipper displays video frames and cuts videos

preface

For each video clip returned from the back end, the front end needs to ensure that the user can cut and fine tune it. The specific implementation effects are as follows:

Here, the whole process can be disassembled into the following steps:

  • Acquisition and display of all video frames
  • Implementation of video sliding marquee RangeSeekBarView
  • Intercept the video according to the start and end time

Acquisition and display of all video frames

First, the layout xml file of the whole page is as follows:

<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="complete"
            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="cancel"
            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"/>
            <!--Add a mask to the space at both ends 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"/>
            <!--Add a mask to the space at both ends end-->
        </RelativeLayout>

    </RelativeLayout>
</LinearLayout>

At the top is a video player VideoView, and at the bottom is RecyclerView, which displays all video frames, as well as a custom RangeSeekBarView to complete the sliding duration interception

When it is applied to RecyclerView, the corresponding adapter is required to display data, and the frameadapter is defined to complete the adaptation of picture frame to page. The sub View here is ImageView, which represents picture frame picture

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);
        }
    }
}

In the InitViews method, first add the code to initialize the video player and RecyclerView

        // VideoView 
        mVideoView = binding.mVideoView;
        StandardVideoController controller = new StandardVideoController(this.getContext());
        controller.setEnableOrientation(true);
        PrepareView prepareView = new PrepareView(this.getContext());//Ready to play interface
        prepareView.setClickStart();
        ImageView thumb = prepareView.findViewById(R.id.thumb);//Cover picture
        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()));//Auto complete playback interface
        controller.addControlComponent(new ErrorView(this.getContext()));//Error interface
        TitleView titleView = new TitleView(this.getContext());//Title Block
        controller.addControlComponent(titleView);
        VodControlView vodControlView = new VodControlView(this.getContext());//On demand control bar
        controller.addControlComponent(vodControlView);
        GestureView gestureControlView = new GestureView(this.getContext());//Slide control view
        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);

It is estimated that a picture frame will be obtained in 1s, so the total duration of the video needs to be obtained first. It should be noted here that the correct duration can be obtained only after the VideoView completes the preparation state. Therefore, it is necessary to monitor and calculate mFrames in the state

    private VideoView.OnStateChangeListener mOnStateChangeListener = new VideoView.SimpleOnStateChangeListener() {
        @Override
        public void onPlayerStateChanged(int playerState) {
            switch (playerState) {
                case VideoView.PLAYER_NORMAL://Small screen
                    break;
                case VideoView.PLAYER_FULL_SCREEN://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) method is to obtain the frame corresponding to time in the video, which needs to be obtained through ffmpeg command

    // Get picture frame
    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);
    }

Implementation of video sliding marquee RangeSeekBarView

The basic idea of RangeSeekBarView implementation is to set monitoring, obtain the start time and end time of the current video selection, and redraw the position and duration display of RangeSeekBarView in RecyclerView through onDraw method

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;//Minimum clip 3s, default
    private double absoluteMinValuePrim, absoluteMaxValuePrim;
    private double normalizedMinValue = 0d;//The proportional value of point coordinates in the total length, ranging from 0-1
    private double normalizedMaxValue = 1d;//The proportional value of point coordinates in the total length, ranging from 0-1
    private double normalizedMinValueTime = 0d;
    private double normalizedMaxValueTime = 1d;// Normalized: normalized -- the proportional value of point coordinates in the total length, ranging from 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;//Minimum clipping distance
    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);

        //border-top 
        canvas.drawRect(rangeL + thumbHalfWidth, thumbPaddingTop + paddingTop, rangeR - thumbHalfWidth, thumbPaddingTop + Utils.dp2px(2) + paddingTop, rectPaint);

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

        //Draw left thumb
        drawThumb(normalizedToScreen(normalizedMinValue), false, canvas, true);

        //Draw right thumb
        drawThumb(normalizedToScreen(normalizedMaxValue), false, canvas, false);

        //Draw text
        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;// Record the index of click points
        final int action = event.getAction();
        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                //Remember the coordinate x of the point where the last finger clicks on the screen, mDownMotionX
                mActivePointerId = event.getPointerId(event.getPointerCount() - 1);
                pointerIndex = event.findPointerIndex(mActivePointerId);
                mDownMotionX = event.getX(pointerIndex);
                // Judge whether the touch reaches the maximum value thumb or the minimum value thumb
                pressedThumb = evalPressedThumb(mDownMotionX);
                if (pressedThumb == null) return super.onTouchEvent(event);
                setPressed(true);// Set the control to be pressed
                onStartTrackingTouch();// Set mIsDragging to true and start tracking touch events
                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);// The X coordinate of the point of the finger on the control
                        // The finger does not point on the maximum and minimum value, and there is a sliding event on the control
                        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;// When the finger is raised, the thumb touched by the touch is set to be empty
                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);// Get the index of the pressed point
        float x = 0;
        try {
            x = event.getX(pointerIndex);
        } catch (Exception e) {
            return;
        }
        if (Thumb.MIN.equals(pressedThumb)) {
            // Screentonormalized (x) -- > get the normalized value of 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) {//Four exact decimal places greater than 5 minutes
                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));// Ensure that the value is between 0-1, but when is this judgment useful?
            } 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);
    }

    /**
     * Calculate which Thumb is in
     *
     * @param touchX touchX
     * @return Is the minimum or maximum value of touch null
     */
    private Thumb evalPressedThumb(float touchX) {
        Thumb result = null;
        boolean minThumbPressed = isInThumbRange(touchX, normalizedMinValue, 2);// Is the touch point within the minimum picture range
        boolean maxThumbPressed = isInThumbRange(touchX, normalizedMaxValue, 2);
        if (minThumbPressed && maxThumbPressed) {
            // If two thumbs are overlapped and it is impossible to determine which one to drag, do the following
            // If the touch point is on the right side of the screen, it is judged that the touch has reached the minimum value thumb; otherwise, it is judged that the touch has reached the maximum value 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 coordinate of the current touch point - the difference between the X coordinate of the center point of the minimum picture on the screen < = the general width of the minimum picture
        // That is, judge whether the touch point is in a circle with the center of the minimum picture as the origin and half the width as the radius.
        return Math.abs(touchX - normalizedToScreen(normalizedThumbValue)) <= thumbHalfWidth * scale;
    }

    private boolean isInThumbRangeLeft(float touchX, double normalizedThumbValue, double scale) {
        // X coordinate of the current touch point - the difference between the X coordinate of the center point of the minimum picture on the screen < = the general width of the minimum picture
        // That is, judge whether the touch point is in a circle with the center of the minimum picture as the origin and half the width as the radius.
        return Math.abs(touchX - normalizedToScreen(normalizedThumbValue) - thumbWidth) <= thumbHalfWidth * scale;
    }

    /**
     * Trying to tell the parent view not to intercept the child control's drag
     */
    private void attemptClaimDrag() {
        if (getParent() != null) {
            getParent().requestDisallowInterceptTouchEvent(true);
        }
    }

It should be noted here that in addition to dragging RangeSeekBarView to redraw, the current start and end time is also displayed, and the sliding of RecyclerView also needs to redraw RangeSeekBarView. Therefore, the code of initViews needs to add the listening settings of two components

        // Sliding listening of 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();
                // The video player jumps to the clip position
                mVideoView.seekTo((int)mMinTime);
            }
        });

        // Drag monitoring of 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);
                // The video player jumps to the clip position
                mVideoView.seekTo((int)mMinTime);
            }
        });

Intercept the video according to the start and end time

When the user clicks the Save button, it is necessary to officially cut the video and delete the picture frame generated in the middle

    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);
        // Delete all frames
        File dir = new File(frames_path);
        File[] files = dir.listFiles();//All files or folders under the folder
        if (files != null){
            for (int i = 0; i < files.length; i++) {
                files[i].delete();
            }
        }
    }

Tags: Java Android

Posted by melrse on Fri, 13 May 2022 00:59:09 +0300