Android MediaPlayer+SurfaceView+custom controller implements video playback

Android provides a variety of video playback methods, as follows:

1. MediaController + VideoView implementation

This method is the simplest way to implement it. VideoView inherits SurfaceView and implements the MediaPlayerControl interface. MediaController is an auxiliary controller encapsulated by Android, with controls such as pause, play, stop, and progress bars. VideoView + MediaController can easily implement video playback, stop, fast forward, rewind and other functions.

2. MediaPlayer + SurfaceView + MediaController controller or VideoView + custom controller

This method is more or less convenient and can be used when the requirements are not high.

3. MediaPlayer + SurfaceView + custom controller

This method is the most complicated, but it can freely control the size, position and various events of the player, making it more flexible.

This demo is developed based on Android11 and implements a simple video playback function. You can drag the progress bar, fast forward/rewind, and play the previous video/play the next video. At the same time, you can view the video details through a floating window. Click anywhere on the video to pause it, and all buttons will be hidden if you don’t operate it for three seconds during playback.

The reason why MediaPlayer + SurfaceView is used for playback is because dragging the progress bar can jump to the latest frame, while VideoView can only jump to key frames. However, the implementation of this playback is more complicated. The specific steps are as follows:

1. Create a MediaPlayer object and let it load the specified video file. Can be an application’s resource file or a local file path. And add three listeners: setOnPreparedListener, setOnVideoSizeChangedListener and setOnCompletionListener to monitor video loading completion events, video size change events (screen adaptation) and video playback completion events respectively.

mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                vseekBar.setProgress(0);
                vgress.setText(Tool.millisToStringShort(0));//mediaplay time needs to be processed
                total=mediaPlayer.getDuration();
                vtotal.setText(Tool.millisToStringShort(total));//mediaplay time needs to be processed
                vseekBar.setMax(total);
            }
        });
        mediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() { //size change callback
            @Override
            public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
                changeVideoSize();
            }
        });
        mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mp) {
                vseekBar.setProgress(0);
                vpause.setVisibility(View.GONE);
                vplay.setVisibility(View.VISIBLE);
                relativeLayout1.setVisibility(View.VISIBLE);
                relativeLayout2.setVisibility(View.VISIBLE);
            }
        });

2. Define the SurfaceView component in the interface layout file and add a Callback listener to the SurfaceHolder of SurfaceView. The listener inherits the surfaceCreated method, in which it calls MediaPlayer’s setDisplay() and setDataSource() to output the played video image to the specified SurfaceView component and uses the prepareAsync() method to load the streaming media file. Use setOnTouchListener to set click listening for surfaceView to control the hiding of buttons and click pause/play.

surfaceHolder=surfaceView.getHolder();
surfaceHolder.addCallback(new SurfaceHolder.Callback() {
            //Return when Surface is created in SurfaceView
            @Override
            public void surfaceCreated(@NonNull SurfaceHolder holder) {
                mediaPlayer.reset();
                try {
                    mediaPlayer.setDisplay(holder);
                    mediaPlayer.setDataSource(context, Uri.parse(vPath));
                    mediaPlayer.prepareAsync();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            //This method is triggered when the size of SurfaceView changes
            @Override
            public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
            }
            //Callback when Surface is destroyed
            @Override
            public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
                if (mediaPlayer!=null) {
                    mediaPlayer.stop();
                    mediaPlayer.release();
                }
            }
        });
//Set up surfaceView click monitoring
        surfaceView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        if (mediaPlayer.isPlaying()) {
                            mediaPlayer.pause();
                            relativeLayout1.setVisibility(View.VISIBLE);
                            relativeLayout2.setVisibility(View.VISIBLE);
                            vplay.setVisibility(View.VISIBLE);
                            vpause.setVisibility(View.GONE);
                        } else {
                            mediaPlayer.start();
                            relativeLayout1.setVisibility(View.GONE);
                            relativeLayout2.setVisibility(View.GONE);
                            vplay.setVisibility(View.GONE);
                            vpause.setVisibility(View.GONE);
                        }
                        break;
                }
                return true;
            }
        });

3. Call the callback interface to complete the instantiation of the Handler. When the video is playing, it accepts incoming messages and updates the progress bar and video synchronously. At the same time, add a setOnSeekBarChangeListener listener to the Seekbar component to implement the drag event of the progress bar.

Handler handler=new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(@NonNull Message msg) { /
            if(msg.what==1){
                try {
                    nowtime=mediaPlayer.getCurrentPosition();
                    vgress.setText(Tool.millisToStringShort(nowtime));
                    vseekBar.setProgress(nowtime);
                    handler.sendEmptyMessageDelayed(1,10); //Set the delay
                }catch (IllegalStateException e){
                    e.printStackTrace();
                }

            }
            return false;
        }
    });
public SeekBar.OnSeekBarChangeListener seekBarChangeListener=new SeekBar.OnSeekBarChangeListener() {
        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            if (!mediaPlayer.isPlaying()){
                mediaPlayer.seekTo(seekBar.getProgress(),MediaPlayer.SEEK_CLOSEST);
            }
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
            handler.removeMessages(1);
            mediaPlayer.pause();
        }

        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            mediaPlayer.start();
            handler.sendEmptyMessage(1);
        }
    };

4. Set click events for other buttons. To fast forward and rewind, you only need to add or subtract the time of the current progress bar. If you do not operate the hidden button after 3 seconds, you need to use handler.postDelayed() to implement delay. To switch left and right, you need to first call ContentResolver.query() to query all local videos, then find the index of the currently playing video in the list of all videos, and then reload the switched mediaplay.

//Using the Thread class will report an error
handler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            relativeLayout1.setVisibility(View.GONE);
                            relativeLayout2.setVisibility(View.GONE);
                            vpause.setVisibility(View.GONE);
                            vplay.setVisibility(View.GONE);
                        }
                    }, 3000);

public static ArrayList<DataValue> getVideo() {
        Cursor cursor = null;
        cursor = ContentResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
                null, null, null, MediaStore.Video.Media.DEFAULT_SORT_ORDER);
        while (cursor.moveToNext()) {
            String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA));// path
            File file = new File(path);
            if (file == null || !file.exists()) {
                continue;
            }
            DataValue value = new DataValue();
            value.fileName = file.getName();
            if (file.getName().length()>=30){
                String name=value.fileName;
                String name1=name.substring(0,4);
                int b=name.lastIndexOf(".");
                String name2=name.substring(b,name.length());
                String name3=name1 + "************" + name2;
                value.fileName = name3;
            }
            value.filePath = path;
            Videolist.add(value);
        }
        return Videolist;
    }

5. Initialize the layout and components for displaying video details, create a WindowManager object and set the values corresponding to the window, and add the set values and layout to the WindowManager object.

private void initInfoView() {
        infoView = View.inflate(this, R.layout.information, null);
        nameTv = infoView.findViewById(R.id.tv_name);
        createTv = infoView.findViewById(R.id.tv_create);
        sizeTv = infoView.findViewById(R.id.tv_size);
        formatTv = infoView.findViewById(R.id.tv_format);
        resolutionTv = infoView.findViewById(R.id.tv_resolution);
        pathTv = infoView.findViewById(R.id.tv_path);
        int index = vFile.getName().lastIndexOf(".");
        if (index > 0) {
            nameTv.setText("File name: " + vFile.getName().substring(0, index));
            formatTv.setText("Type: " + vFile.getName().substring(index));
        } else {
            nameTv.setText("File name: " + vFile.getName());
            formatTv.setText("Type: Unknown");
        }
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        if(!switchlist){
            createTv.setText("New time: Unrecognized");
            resolutionTv.setText("Resolution: Unrecognized");
            sizeTv.setText("File size: Unrecognized");
            pathTv.setText("File path: " + vPath);
        }else {
            createTv.setText("New time: " + format.format(vFile.lastModified()));
            MediaMetadataRetriever retr = new MediaMetadataRetriever();
            retr.setDataSource(vPath);
            String height = retr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); // Video height
            String width = retr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); // Video width
            if (height!=null & amp; & amp;width!=null){
                resolutionTv.setText("Resolution:" + width + "*" + height);
            }else {
                resolutionTv.setText("Resolution: Unrecognized");
            }
            sizeTv.setText("File size: " + Tool.formatSize(vFile.length()));
            pathTv.setText("File path: " + vPath);
        }

    }

    private void createInfoDialog() {
        DisplayMetrics dm= getResources().getDisplayMetrics();
        windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
        WindowManager.LayoutParams params = new WindowManager.LayoutParams();
        params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        params.gravity = Gravity.TOP | Gravity.START;
        params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
        params.width = (int) (dm.widthPixels*0.3);
        params.height = (int) (dm.heightPixels*0.2);
        params.x = dm.widthPixels;
        params.y = (int) (dm.heightPixels*0.6);
        params.format = PixelFormat.RGBA_8888;
        windowManager.addView(infoView, params);
    }

Finally, MainActivity passes the video path to VideoActivity through Intent. Because it is Android11, it needs to dynamically apply for permissions.

public class MainActivity extends AppCompatActivity {
    public static Context context;
    private EditText mvideofile;
    private Button mfileplay,mexamplay;
    private String file;
    private Intent intentexam,intentfile;

    @SuppressLint("MissingInflatedId")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        requestPermission();
        context=getApplicationContext();

        mexamplay=findViewById(R.id.exampleplay);
        mexamplay.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                intentexam=new Intent(context,VideoActivity.class);
                String exfile= "android.resource://" + getPackageName() + "/" + R.raw.examvideo;
                intentexam.putExtra("path",exfile);
                intentexam.putExtra("switch",false);
                startActivity(intentexam);
            }
        });
        mvideofile=findViewById(R.id.videofile);
        mfileplay=findViewById(R.id.fileplay);
        mfileplay.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                file=mvideofile.getText().toString();
                intentfile = new Intent(context, VideoActivity.class);
                intentfile.putExtra("path", file);
                intentfile.putExtra("switch",true);
                startActivity(intentfile);
            }
        });
    }

    private void requestPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            if (!Environment.isExternalStorageManager()) {
                Toast.makeText(this, "Managing all file permissions is not turned on", Toast.LENGTH_SHORT).show();
                Intent intentall = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
                intentall.setData(Uri.parse("package:" + this.getPackageName()));
                startActivity(intentall);
            }
        }
        if (!canDrawOverlays(this)) {
            Toast.makeText(this, "suspended window permission is not opened", Toast.LENGTH_SHORT).show();
            Intent intentoverlay = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
            intentoverlay.setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID));
            startActivity(intentoverlay);
        }

    }
}