android進階4step2:Android音視訊處理——視訊播放
阿新 • • 發佈:2019-01-04
視訊播放
視訊播放的實現方式
- 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);
}
}