视频播放
视频播放的实现方式
- 1、使用系统中已安装的播放器app
- 2、使用VideoView配合MediaController实现 (系统的控制键)
- 3、使用SurfaceView配合MediaPlayer实现(可自定义控制键,灵活度最高)
1、使用Intent播放视频
xml 中添加 <provider> 标签的内容 android7.0新特性的FileProvider的需要
具体了解:Android 7.0 行为变更 通过FileProvider在应用间共享文件吧
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.demo.videodemo">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.demo.videodemo.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
在res 新建 xml'文件夹 新建 file_paths.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!--因为是根路径 不写path 即为根目录的文件 /storage/emulated/0/ 下的文件 -->
<!--这里写video 根目录下的video目录下的文件-->
<external-path name="external" path="video" />
</paths>
Java代码: 下面是三种方式的模板 只实现了第一种 接下来的方法都是在这个代码上的了
public class MainActivity extends AppCompatActivity {
private ListView mListView;
//三种播放视频方式
private List<String> mDatas = Arrays.asList("Use Intent",
"Use VideoView", "Use MediaPlayer & SurfaceView");
private static final int REQ_CODE_STORAGE = 0X110;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = findViewById(R.id.id_listview);
//设置适配器
mListView.setAdapter(new ArrayAdapter<String>(this, R.layout.item_main, mDatas));
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
switch (position) {
case 0:
checkPermissionAndPlayVideo();
break;
case 1:
break;
case 2:
break;
default:
break;
}
}
});
}
/**
* 利用intent 播放 本地视频
* 加内存 写 的权限
* android 6.0 之后要动态申请权限
*/
private void playVideoUseIntent() {
//找到内存卡里面的视频 参数1:文件路径 参数2:文件名称
File file = new File(Environment.getExternalStorageDirectory().getPath(), "lalala.mp4");
Intent intent = new Intent(Intent.ACTION_VIEW);
if (Build.VERSION.SDK_INT >= 24) {
//android7.0之后的特性 fileprovider 使用content://Uri 爱替换 file://Uri 否则报错
//完成 file 到 contentUri的转换
Uri contentUri = FileProvider.getUriForFile(this, "com.demo.videodemo.fileprovider", file);
intent.setDataAndType(contentUri, "video/*");
//添加Uri权限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} else {//7.0之前的操作
//设置数据源和类型 fileUri
intent.setDataAndType(Uri.fromFile(file), "video/*");
}
startActivity(intent);
}
/**
* 动态申请权限
*/
private void checkPermissionAndPlayVideo() {
//如果读内存卡的权限没被申请
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
//申请权限 onRequestPermissionsResult 拿到结果
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQ_CODE_STORAGE);
} else {//如果已经有权限了
playVideoUseIntent();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case REQ_CODE_STORAGE:
//如果权限被授予
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//执行播放视频的操作
playVideoUseIntent();
} else {
Toast.makeText(this, "该功能需要SDCard权限", Toast.LENGTH_SHORT).show();
}
return;
}
}
}
效果:自带进度条 和横竖屏切换
2、VideoView配合MediaController
- 1、VideoView播放视频(VideoView 继承 SurfaceView)
- 2、MediaController控制视频暂停、快进、快退
- 3、考虑点击Home键视频的暂停与恢复
第一步:播放视频的实现
VideoViewActivity.java
public class VideoViewActivity extends AppCompatActivity {
private VideoView mVideoView;
private MediaController mMediaController;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_view);
mVideoView = findViewById(R.id.id_video_view);
//找到内存卡里面的视频 参数1:文件路径 参数2:文件名称 /storage/emulated/0/video的.mp4文件
File file = new File(Environment.getExternalStorageDirectory().getPath() + "/video", "lala.mp4");
//1.完成视频的播放
mVideoView.setVideoPath(file.getAbsolutePath());
mMediaController = new MediaController(this);
//2.MediaController 与VideoView结合
mVideoView.setMediaController(mMediaController);
mVideoView.start();
}
/**
* MainActivity对启用的方法
* @param context
*/
public static void start(Context context) {
Intent intent = new Intent(context, VideoViewActivity.class);
context.startActivity(intent);
}
}
MainActivity.java 中点击之后开启视频
case 1:
VideoViewActivity.start(MainActivity.this);
break;
效果:视频可以正常播放 还有系统自带的暂停、快进等控件,
但是点击home 键再回来之后 就黑屏了(没有画面,其实是surfcaseView被销毁重建了) 怎么解决?
在onResume的时候执行start();方法
@Override
protected void onResume() {
super.onResume();
mVideoView.start();
}
解决了上面的一个问题,新问题:按home键后视频会自动重启播放,但是不会保存之前播放的状态
因为点击home键之后再进来SurfaceView会销毁重建 所以视频是被初始化了一遍
1.点击暂停之后 home键回来还是暂停的状态
2.并跳转到之前记录的进度值
//记录当前视频播放的进度值
private int mCurrentPos;
//判断是否处于暂停状态
private boolean mIsPause;
....
@Override
protected void onResume() {
super.onResume();
//跳转到当前进度值
mVideoView.seekTo(mCurrentPos);
if (!mIsPause) {
mVideoView.start();
}
}
@Override
protected void onPause() {
super.onPause();
//记录暂停时视频的进度值
mCurrentPos = mVideoView.getCurrentPosition();
mIsPause = !mVideoView.isPlaying();
}
目前效果:
新问题:当我们旋转屏幕,视频又重头播放了 ,因为Activity被销毁重建了
状态的存储和恢复
//bundle的key值
private static final String KEY_CUR_POS = "key_cur_pos";
private static final String KEY_IS_PAUSE = "key_is_pause";
/**
* 状态存储
* @param outState
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(KEY_CUR_POS, mCurrentPos);
outState.putBoolean(KEY_IS_PAUSE, mIsPause);
}
/**
* 状态恢复
* @param savedInstanceState
*/
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mCurrentPos = savedInstanceState.getInt(KEY_CUR_POS);
mIsPause = savedInstanceState.getBoolean(KEY_IS_PAUSE);
}
3、SurfaceView配合MediaPlayer
- 1、MediaPlayer播放视频
- 2、SurfaceView显示视频画面
- 3、自定义控制器实现:快进、快退、暂停等
- 4、考虑点击Home键视频的暂停与恢复
MediaPlayerActivity.java
实现视频播放和点击home键后可以继续播放
/**
* VideoView mMediaPlayer是和SurfaceView绑定的
* 现在这个 mMediaPlayer是和SurfaceView 分开的
*/
public class MediaPlayerActivity extends AppCompatActivity {
private RelativeLayout mRlContainer;
private SurfaceView mSurfaceView;
private MediaPlayer mMediaPlayer;
//标识符
private boolean mIsprepared;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_media_player);
initViews();
initMediaPlayer();
initEvents();
}
//暂停
@Override
protected void onPause() {
super.onPause();
mMediaPlayer.pause();
}
//销毁 释放
@Override
protected void onDestroy() {
super.onDestroy();
mMediaPlayer.release();
}
/**
* 初始化视图
*/
private void initViews() {
mRlContainer = findViewById(R.id.id_rl_container);
mSurfaceView = findViewById(R.id.id_surface_view);
}
private void initMediaPlayer() {
mMediaPlayer = new MediaPlayer();
//找到内存卡里面的视频 参数1:文件路径 参数2:文件名称 /storage/emulated/0/video的.mp4文件
File file = new File(Environment.getExternalStorageDirectory().getPath() + "/video", "lala.mp4");
try {
//每一步严格遵守MediaPlayer的状态
mMediaPlayer.setDataSource(file.getAbsolutePath());
mMediaPlayer.prepareAsync();
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mMediaPlayer.start();
//更新状态
mIsprepared = true;
}
});
//这里拿到视频的宽高
mMediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
@Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
//动态设置RlContainer的高度
ViewGroup.LayoutParams lp = mRlContainer.getLayoutParams();
//拿到控件的宽度和视频的宽度比值再乘以视频的宽高得到同等比例下控件的高
lp.height = (int) (mRlContainer.getWidth() * 1.0f / width * height);
mRlContainer.setLayoutParams(lp);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 初始化事件
*/
private void initEvents() {
//将MediaPlayer和SurfaceView进行绑定
mSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
//在surface创建成功
mMediaPlayer.setDisplay(holder);
//如果是prepared之后才能start()
if (!mIsprepared) {
return;
}
//如果mMediaPlayer不是在播放的时候再start
if (!mMediaPlayer.isPlaying()) {
//点击home键回重新销毁创建surfaceView 也会执行这个方法
//所以在这里重新开启
mMediaPlayer.start();
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
});
}
/**
* MainActivity对启用的方法
*
* @param context
*/
public static void start(Context context) {
Intent intent = new Intent(context, MediaPlayerActivity.class);
context.startActivity(intent);
}
}
布局文件:activity_media_player.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
tools:context=".MediaPlayerActivity">
<RelativeLayout
android:id="@+id/id_rl_container"
android:layout_width="match_parent"
android:layout_height="200dp">
<SurfaceView
android:id="@+id/id_surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
</RelativeLayout>
效果:
添加SeekBar 进度条暂停按键 和 阻止旋转屏幕销毁重建activity(状态保存恢复的另一种方式):
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
tools:context=".MediaPlayerActivity">
<RelativeLayout
android:id="@+id/id_rl_container"
android:layout_width="match_parent"
android:layout_height="200dp">
<SurfaceView
android:id="@+id/id_surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<SeekBar
android:id="@+id/id_seekbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="4dp" />
<Button
android:id="@+id/id_btn_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:enabled="false"
android:minHeight="0dp"
android:minWidth="0dp"
android:text="暂停" />
</RelativeLayout>
</RelativeLayout>
清单文件:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.demo.videodemo">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.demo.videodemo.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<activity android:name=".VideoViewActivity" />
<!--当发生方向变化时自己处理 阻止屏幕切换 会销毁重建activity了 只会回答java代码中的 onConfigurationChanged放到-->
<activity android:name=".MediaPlayerActivity"
android:configChanges="orientation|screenSize"></activity>
</application>
</manifest>
完整java代码:
/**
* VideoView mMediaPlayer是和SurfaceView绑定的
* 现在这个 mMediaPlayer是和SurfaceView 分开的
*/
public class MediaPlayerActivity extends AppCompatActivity {
private RelativeLayout mRlContainer;
private SurfaceView mSurfaceView;
private MediaPlayer mMediaPlayer;
private SeekBar mSeekBar;
private Button mBtnPlay;
//标识符
private boolean mIsprepared;
private boolean mIsPause;
///记录控件的高宽比
private float mRatioHW;
//自动更新SeekBar 进度条的位置
private Handler mHandler = new Handler();
private Runnable mUpdateProgressRunnable = new Runnable() {
@Override
public void run() {
if (mMediaPlayer == null) {
return;
} else {
int currentPosition = mMediaPlayer.getCurrentPosition();
int duration = mMediaPlayer.getDuration();
if (mSeekBar != null && duration > 0) {
//拿到当前的进度值
int progress = (int) (currentPosition * 1.0f / duration * 1000);
mSeekBar.setProgress(progress);
//如果视频处于播放的时候才有改变进度条
if (mMediaPlayer.isPlaying()) {
mHandler.postDelayed(mUpdateProgressRunnable, 1000);
}
}
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_media_player);
initViews();
initMediaPlayer();
initEvents();
}
@Override
protected void onPause() {
super.onPause();
mMediaPlayer.pause();
mHandler.removeCallbacks(mUpdateProgressRunnable);
}
@Override
protected void onDestroy() {
super.onDestroy();
mMediaPlayer.release();
}
/**
* 初始化视图
*/
private void initViews() {
mRlContainer = findViewById(R.id.id_rl_container);
mSurfaceView = findViewById(R.id.id_surface_view);
mSeekBar = findViewById(R.id.id_seekbar);
mBtnPlay = findViewById(R.id.id_btn_play);
mSeekBar.setMax(1000);
}
//阻止屏幕切换时,销毁重建activity 在xml中 添加
//android:configChanges="orientation|screenSize" 自己处理 这样系统就不会销毁重建activity了
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
//收到设置container的高度
ViewTreeObserver observer = mRlContainer.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mRlContainer.getViewTreeObserver().removeGlobalOnLayoutListener(this);
ViewGroup.LayoutParams lp = mRlContainer.getLayoutParams();
//通过之前记录的高宽比恢复控件的高
lp.height = (int) (mRatioHW * mRlContainer.getWidth());
mRlContainer.setLayoutParams(lp);
}
});
}
private void initMediaPlayer() {
mMediaPlayer = new MediaPlayer();
//找到内存卡里面的视频 参数1:文件路径 参数2:文件名称 /storage/emulated/0/video的.mp4文件
File file = new File(Environment.getExternalStorageDirectory().getPath() + "/video", "lala.mp4");
try {
//每一步严格遵守MediaPlayer的状态
mMediaPlayer.setDataSource(file.getAbsolutePath());
mMediaPlayer.prepareAsync();
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mBtnPlay.setEnabled(true);
//更新状态
mIsprepared = true;
if (!mMediaPlayer.isPlaying()) {
//开启视频播放
mMediaPlayer.start();
mHandler.post(mUpdateProgressRunnable);
mBtnPlay.setText("暂停");
}
}
});
//这里拿到视频的宽高
mMediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
@Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
//动态设置RlContainer的高度
ViewGroup.LayoutParams lp = mRlContainer.getLayoutParams();
//记录高宽比
mRatioHW = height * 1.0f / width;
//拿到控件的宽度和视频的宽度比值再乘以视频的宽高得到同等比例下控件的高
lp.height = (int) (mRlContainer.getWidth() * 1.0f / width * height);
mRlContainer.setLayoutParams(lp);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 初始化事件
*/
private void initEvents() {
//将MediaPlayer和SurfaceView进行绑定
mSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
//在surface创建成功
mMediaPlayer.setDisplay(holder);
//如果是prepared之后才能start()
if (!mIsprepared) {
return;
}
//如果是暂停状态 点击home键 不需要重新start 直接跳转 return 避免执行下面start的方法
if (mIsPause) {
mMediaPlayer.seekTo(mMediaPlayer.getCurrentPosition());
return;
}
//如果mMediaPlayer不是在播放的时候再start
if (!mMediaPlayer.isPlaying()) {
//点击home键回重新销毁创建surfaceView 也会执行这个方法
//所以在这里重新开启
mMediaPlayer.start();
mBtnPlay.setText("暂停");
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
});
//设置进度条的拖动
mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
mHandler.removeCallbacks(mUpdateProgressRunnable);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
//拿到视频的总长度
long duration = mMediaPlayer.getDuration();
//拿到当前进度条的比例(0-1000)占视频总长度的位置
int target = (int) (mSeekBar.getProgress() * 1.0f / 1000 * duration);
//跳到当前进度
mMediaPlayer.seekTo(target);
//拖动进度如果是播放的时候再重新更新进度条
if (mMediaPlayer.isPlaying()) {
mHandler.post(mUpdateProgressRunnable);
}
}
});
//视频播放完成之后
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
mSeekBar.setProgress(1000);
mHandler.removeCallbacks(mUpdateProgressRunnable);
mBtnPlay.setText("播放");
mIsPause = true;
}
});
//暂停播放的控制
mBtnPlay.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mMediaPlayer.isPlaying()) {//如果正在播放
mMediaPlayer.pause();
mBtnPlay.setText("播放");
mHandler.removeCallbacks(mUpdateProgressRunnable);
mIsPause = true;
} else {//如果不是播放
mMediaPlayer.start();
mBtnPlay.setText("暂停");
mHandler.post(mUpdateProgressRunnable);
mIsPause = false;
}
}
});
}
/**
* MainActivity对启用的方法
*
* @param context
*/
public static void start(Context context) {
Intent intent = new Intent(context, MediaPlayerActivity.class);
context.startActivity(intent);
}
}