Android MediaPlayer+SurfaceView+自定义控制器实现视频播放

Android提供了多种视频播放的方式,如下:

1、MediaController+VideoView实现方式

这种方式是最简单的实现方式。VideoView继承了SurfaceView同时实现了MediaPlayerControl接口,MediaController则是安卓封装的辅助控制器,带有暂停,播放,停止,进度条等控件。通过VideoView+MediaController可以很轻松的实现视频播放、停止、快进、快退等功能。 

2、MediaPlayer+SurfaceView+MediaController控制器或VideoView+自定义控制器

这种方式多少都方便一点,要求不高的时候可以使用。

3、MediaPlayer+SurfaceView+自定义控制器

这种方式最为复杂,但可以自由控制播放器的大小、位置以及各种事件,更为灵活。                                                        

本demo基于Android11开发,实现了简单的视频播放功能,可以拖动进度条,快进/快退和播放上一个视频/播放下一个视频,同时通过悬浮窗的形式查看视频详情。点击视频任意处即可暂停,播放中三秒不操作隐藏所有按钮。

         Android MediaPlayer+SurfaceView+自定义控制器实现视频播放Android MediaPlayer+SurfaceView+自定义控制器实现视频播放

之所以采用MediaPlayer+SurfaceView来播放是因为拖动进度条可以跳到最近的一帧,而VideoView只能跳到关键帧,但这种播放的实现方式更为复杂,具体步骤如下:

1、创建MediaPlayer对象,让它加载指定的视频文件。可以是应用的资源文件或本地文件路径。并添加setOnPreparedListener,setOnVideoSizeChangedListener和setOnCompletionListener三个监听分别用来监听视频装载完成事件,视频大小改变事件(屏幕适配)和视频播放完毕事件。

mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                vseekBar.setProgress(0);
                vgress.setText(Tool.millisToStringShort(0));//mediaplay的时间要进行处理
                total=mediaPlayer.getDuration();
                vtotal.setText(Tool.millisToStringShort(total));//mediaplay的时间要进行处理
                vseekBar.setMax(total);
            }
        });
        mediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() { //尺寸变化回调
            @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、在界面布局文件中定义SurfaceView组件,并为SurfaceView的SurfaceHolder添加Callback监听器。监听器中继承surfaceCreated方法,在该方法中调用MediaPlayer的setDisplay()和setDataSource()将所播放的视频图像输出到指定的SurfaceView组件并利用prepareAsync()方法装载流媒体文件。利用setOnTouchListener为surfaceView设置点击监听,用来控制按钮的隐藏和点击暂停/播放。

surfaceHolder=surfaceView.getHolder();
surfaceHolder.addCallback(new SurfaceHolder.Callback() {
            //当SurfaceView中Surface创建时回掉
            @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();
                }
            }
            //当SurfaceView的大小发生改变时候触发该方法
            @Override
            public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
            }
            //Surface销毁时回调
            @Override
            public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
                if (mediaPlayer!=null) {
                    mediaPlayer.stop();
                    mediaPlayer.release();
                }
            }
        });
//设置 surfaceView点击监听
        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、调用callback接口来完成Handler的实例化,视频播放时接受消息传入并同步更新进度条和视频。同时为Seekbar组件添加setOnSeekBarChangeListener监听,用以实现进度条的拖动事件。

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);   //设置延迟
                }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、为其他按钮设置点击事件,快进快退只需要对当前进度条的时间进行加减,3秒后不操作隐藏按钮要用到handler.postDelayed()来实现延时。左右切换需要先调用ContentResolver.query()查询本地所有的视频,然后找到当前播放视频在所有视频列表中的索引,再重新加载切换后的mediaplay。

//用Thread类会报错
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 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));// 路径
            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、将展示视频详情的布局及组件初始化,创建WindowManager对象并设置窗口对应的值,将设置好的值和布局添加进WindowManager对象。

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("文件名: "+ vFile.getName().substring(0, index));
            formatTv.setText("类型: "+ vFile.getName().substring(index));
        } else {
            nameTv.setText("文件名: "+ vFile.getName());
            formatTv.setText("类型: 未知");
        }
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        if(!switchlist){
            createTv.setText("新建时间: 无法识别");
            resolutionTv.setText("分辨率:无法识别");
            sizeTv.setText("文件大小: 无法识别");
            pathTv.setText("文件路径: "+ vPath);
        }else {
            createTv.setText("新建时间: "+ format.format(vFile.lastModified()));
            MediaMetadataRetriever retr = new MediaMetadataRetriever();
            retr.setDataSource(vPath);
            String height = retr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT); // 视频高度
            String width = retr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH); // 视频宽度
            if (height!=null&&width!=null){
                resolutionTv.setText("分辨率:"+width+"*"+height);
            }else {
                resolutionTv.setText("分辨率:无法识别");
            }
            sizeTv.setText("文件大小: "+ Tool.formatSize(vFile.length()));
            pathTv.setText("文件路径: "+ 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);
    }

最后,MainActivity通过Intent将视频路径传给VideoActivity,因为是Android11,还要动态申请权限。

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, "未打开管理所有文件权限", 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, "未打开悬浮窗权限", Toast.LENGTH_SHORT).show();
            Intent intentoverlay = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
            intentoverlay.setData(Uri.parse("package:" + BuildConfig.APPLICATION_ID));
            startActivity(intentoverlay);
        }

    }
}

本文来自网络,不代表协通编程立场,如若转载,请注明出处:https://net2asp.com/480c65b99b.html