我的播放器叫做JsPlayer,喜歡的話,就給個star嘍^_^github.com/shuaijia/Js…
這裡我只介紹播放器封裝思路,會貼出部分程式碼,如果大家想檢視完整程式碼,可以去github檢視,有不清楚或錯誤或改進的地方,可以issues 我!
寫在之前
先上效果圖:(1.5版本新增彈幕功能)
為什麼要用SurfaceView
它繼承自類View,因此它本質上是一個View。但與普通View不同的是,它有自己的Surface。而SurfaceView自帶一個Surface,這個Surface在WMS中有自己對應的WindowState,在SF中也會有自己的Layer。雖然在App端它仍在View hierachy中,但在Server端(WMS和SF)中,它與宿主視窗是分離的。這樣的好處是對這個Surface的渲染可以放到單獨執行緒去做,渲染時可以有自己的GL context。這對於一些遊戲、視訊等效能相關的應用非常有益,因為它不會影響主執行緒對事件的響應。
SurfaceView內部自己持有surface,surface 建立、銷燬、大小改變時系統來處理的,通過surfaceHolder 的callback回撥通知。當畫布建立好時,可以將surface繫結到MediaPlayer中。SurfaceView如果為使用者可見的時候,建立SurfaceView的SurfaceHolder用於顯示視訊流解析的幀圖片,如果發現SurfaceView變為使用者不可見的時候,則立即銷燬SurfaceView的SurfaceHolder,以達到節約系統資源的目的。
關於更多SurfaceView的介紹,可參考我寫的另一片文章:blog.csdn.net/jiashuai94/…
MediaPlayer
MediaPlayer其實是一個封裝的很好的音訊、視訊流媒體操作類,如果檢視其原始碼,會發現其內部是呼叫的native方法,所以它其實是有C++實現的。既然是一個流媒體操作類,那麼必然涉及到,播放、暫停、停止等操作,實際上MediaPlayer也為我們提供了相應的方法來直接操作流媒體。
- void statr():開始或恢復播放。
- void stop():停止播放。
- void pause():暫停播放。
- void setDataSource(String path):通過一個媒體資源的地址指定MediaPlayer的資料來源,這裡的path可以是一個本地路徑,也可以是網路路徑。
當然還有其他很多的方法,例如獲取視訊時長、獲取當前位置、定位到某個位置等等方法,就不再一一列舉,閱讀JsPlayer的原始碼便會有所瞭解。
播放器結構
UML圖
已經對SurfaceView+MediaPlayer封裝視屏播放器有了大致的瞭解,接下來就開始視屏播放器的封裝之旅吧!
1、工具類
工欲善其事,必先利其器!
想封裝結構清晰,使用方便的視訊播放器,工具類是少不了的!JsPlayer主要用了以下幾個工具類:
- DisplayUtils
- NetworkUtils
- StringUtils
DisplayUtils:負責介面展示相關工具,例如px、dp、sp的相互轉換;獲取螢幕寬高度;切換橫屏、豎屏等;
NetworkUtils:判斷手機是否聯網;是否為wifi;是否是流量;網路狀態等;
StringUtils:主要將long型毫秒轉換為時間格式的字串。 程式碼就不貼了,很簡單。大家想了解,去github中檢視吧。
2、實體類
為了在使用視訊播放器時規範傳入的資料,同時也方便使用者呼叫和封裝,故定義了視訊詳情的介面:其包含兩個抽象方法,分別返回視訊地址和視訊標題。
/**
* 視訊資料類
* 請實現本介面
*/
public interface IVideoInfo extends Serializable {
/**
* 視訊標題
*/
String getVideoTitle();
/**
* 視訊播放路徑(本地或網路)
*/
String getVideoPath();
}
複製程式碼
使用者可根據專案實際情況對其進行擴充套件(需實現此介面即可),比如預設圖地址,點贊數,是否購買,彈幕資訊等等。但視訊標題和視訊地址必須返回!
3、回撥相關
大家都知道,VideoView或其他視訊播放器在使用時,有準備好監聽、播放完成監聽、錯誤監聽等等,可供開發者在對應情況進行對應處理;而且我們有時也需要在使用者點選播放暫停、全屏、拖動進度條等情況下獲得操作回撥。因此,我們封裝了兩個回撥介面:
- OnVideoControlListener:視訊控制回撥
- OnPlayerCallback:視訊狀態回撥
/**
* 視訊控制監聽
*/
public interface OnVideoControlListener {
/**
* 開始播放按鈕
*/
void onStartPlay();
/**
* 返回
*/
void onBack();
/**
* 全屏
*/
void onFullScreen();
/**
* 錯誤後的重試
*/
void onRetry(int errorStatus);
}
複製程式碼
/**
* 視訊操作回撥,是將系統MediaPlayer的常見回撥封裝
*/
public interface OnPlayerCallback {
/**
* 準備好
*/
void onPrepared(MediaPlayer mp);
/**
* 視訊size變化
*/
void onVideoSizeChanged(MediaPlayer mp, int width, int height);
/**
* 快取更新變化
*
* @param percent 緩衝百分比
*/
void onBufferingUpdate(MediaPlayer mp, int percent);
/**
* 播放完成
*/
void onCompletion(MediaPlayer mp);
/**
* 視訊錯誤
* @param what 錯誤型別
* @param extra 特殊錯誤碼
*/
void onError(MediaPlayer mp, int what, int extra);
/**
* 視訊載入狀態變化
*
* @param isShow 是否顯示loading
*/
void onLoadingChanged(boolean isShow);
/**
* 視訊狀態變化
*/
void onStateChanged(int curState);
}
複製程式碼
當然了,各位使用上述兩個回撥時,必須先實現、再使用,當然也可以基於它擴充了!
4、自定義view
關於播放器中涉及到的、需要自定義的view主要有手勢調節進度、音量、亮度時的彈框、控制器介面、錯誤介面。
當然我們的JsPlayer視訊播放器也是一自定義view,其手勢控制也封裝了一個view,這些我們稍後會詳細介紹。
- JsVideoProgressOverlay: 調節進度 框
- JsVideoSystemOverlay: 調節音量、亮度 框
- JsVideoErrorView: 錯誤介面
- JsVideoControllerView: 控制器
我的思路是這樣的:將錯誤介面JsVideoErrorView再封裝到控制器中JsVideoControllerView,這樣便於在出錯時的處理;而調節進度等彈框、控制器,當然還有SurfaceView,載入中等,它們會一同封裝到視訊播放器JsPlayer的自定義View中。
JsVideoProgressOverlay
/**
* 滑動快進快退進度框
*/
public class JsVideoProgressOverlay extends FrameLayout {
private ImageView mSeekIcon;
private TextView mSeekCurProgress;
private TextView mSeekDuration;
private int mDuration = -1;
private int mDelSeekDialogProgress = -1;
private int mSeekDialogStartProgress = -1;
public JsVideoProgressOverlay(Context context) {
super(context);
init();
}
public JsVideoProgressOverlay(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public JsVideoProgressOverlay(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
LayoutInflater.from(getContext()).inflate(R.layout.video_overlay_progress, this);
mSeekIcon = (ImageView) findViewById(R.id.iv_seek_direction);
mSeekCurProgress = (TextView) findViewById(R.id.tv_seek_current_progress);
mSeekDuration = (TextView) findViewById(R.id.tv_seek_duration);
}
/**
* 顯示進度框
*
* @param delProgress 進度變化值
* @param curPosition player當前進度
* @param duration player總長度
*/
public void show(int delProgress, int curPosition, int duration) {
if (duration <= 0) return;
// 獲取第一次顯示時的開始進度
if (mSeekDialogStartProgress == -1) {
Log.i("DDD", "show: start seek = " + mSeekDialogStartProgress);
mSeekDialogStartProgress = curPosition;
}
if (getVisibility() != View.VISIBLE) {
setVisibility(View.VISIBLE);
}
mDuration = duration;
mDelSeekDialogProgress -= delProgress;
int targetProgress = getTargetProgress();
if (delProgress > 0) {
// 回退
mSeekIcon.setImageResource(R.mipmap.ic_video_back);
} else {
// 前進
mSeekIcon.setImageResource(R.mipmap.ic_video_speed);
}
mSeekCurProgress.setText(StringUtils.stringForTime(targetProgress));
mSeekDuration.setText(StringUtils.stringForTime(mDuration));
}
/**
* 獲取滑動結束後的目標進度
*/
public int getTargetProgress() {
if (mDuration == -1) {
return -1;
}
int newSeekProgress = mSeekDialogStartProgress + mDelSeekDialogProgress;
if (newSeekProgress <= 0) newSeekProgress = 0;
if (newSeekProgress >= mDuration) newSeekProgress = mDuration;
return newSeekProgress;
}
public void hide() {
mDuration = -1;
mSeekDialogStartProgress = -1;
mDelSeekDialogProgress = -1;
setVisibility(GONE);
}
}
複製程式碼
調節系統屬性彈框JsVideoSystemOverlay就不再貼出程式碼了,與上類似,這裡我們只分享設計思路。
注意:
- mDelSeekDialogProgress -= delProgress,因為向右滑動時傳進來的delProgress是負數、向左滑動是正數,所以這裡計算變化時是在自減。
- if (newSeekProgress <= 0) newSeekProgress = 0; if (newSeekProgress >= mDuration) newSeekProgress = mDuration; 做了邊界控制,防止計算出的資料超出範圍而導致出錯。
JsVideoErrorView
從介面來看很簡單了!
定義所有錯誤碼常量(可換為列舉):
// 正常狀態
public static final int STATUS_NORMAL = 0;
// 普通一場
public static final int STATUS_VIDEO_DETAIL_ERROR = 1;
// 資源錯誤
public static final int STATUS_VIDEO_SRC_ERROR = 2;
// 無WIFI
public static final int STATUS_UN_WIFI_ERROR = 3;
// 無網路
public static final int STATUS_NO_NETWORK_ERROR = 4;
複製程式碼
另外就是顯示的控制:
switch (status) {
case STATUS_VIDEO_DETAIL_ERROR:
video_error_info.setText("視訊載入失敗");
video_error_retry.setText("點此重試");
break;
case STATUS_VIDEO_SRC_ERROR:
video_error_info.setText("視訊載入失敗");
video_error_retry.setText("點此重試");
break;
case STATUS_NO_NETWORK_ERROR:
video_error_info.setText("網路連線異常,請檢查網路設定後重試");
video_error_retry.setText("重試");
break;
case STATUS_UN_WIFI_ERROR:
video_error_info.setText("溫馨提示:您正在使用非WiFi網路,播放將產生流量費用");
video_error_retry.setText("繼續播放");
break;
}
複製程式碼
注意:對重試按鈕的點選事件:錯誤view內建了視訊控制回撥OnVideoControlListener,點選重試時執行回撥的重試按鈕。
JsVideoControllerView
先看佈局:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/video_controller_bottom"
layout="@layout/video_controller_bottom" />
<ImageView
android:id="@+id/player_lock_screen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:src="@mipmap/video_unlock"
android:visibility="gone" />
<com.jia.jsplayer.view.JsVideoErrorView
android:id="@+id/video_controller_error"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<include
android:id="@+id/video_controller_title"
layout="@layout/video_controller_title" />
<ImageView
android:id="@+id/video_back"
android:layout_width="32dp"
android:layout_height="44dp"
android:layout_alignTop="@id/video_controller_title"
android:padding="12dp"
android:scaleType="fitCenter"
android:src="@mipmap/ic_back_white" />
<RelativeLayout
android:id="@+id/rl_pre"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000"
android:visibility="gone">
<ImageView
android:id="@+id/iv_pre_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_video_play"
android:layout_centerInParent="true"/>
</RelativeLayout>
</RelativeLayout>
複製程式碼
能夠看出:主要分為底部控制部分(播放按鈕、當前位置、總時長、進度條),頭部控制部分(返回鍵、標題),出錯介面、鎖屏按鈕和填充全屏的預設圖。
對控制器來說,我們應該關心這些:
首先我們必須傳入MediaPlayer物件(關於它的封裝稍後會詳細介紹),因為我們各點選事件和拖動事件都在控制MediaPlayer,如播放按鈕的點選事件,在控制視訊的播放與暫停,進度條拖動完時,應控制視訊定位 等。
注意:
- 對UI的更新全部提出方法,方便其他地方呼叫
- 是否鎖屏、控制器顯示時長等都應有預設值
- 顯示控制器時,視訊當前位置和更新的精度
- 對全屏按鈕、重試按鈕的點選交給OnVideoControlListener來做
我想著重講一下控制器的隱藏和顯示:
1、控制器一顯示,就獲取MediaPlayer的當前位置,更新UI(進度條,當前播放位置),並將當前位置返回:
/**
* 設定進度,同時也返回進度
*
* @return
*/
private int setProgress() {
if (mPlayer == null || mDragging) {
return 0;
}
int position = mPlayer.getCurrentPosition();
int duration = mPlayer.getDuration();
if (mPlayerSeekBar != null) {
if (duration > 0) {
// use long to avoid overflow
long pos = 1000L * position / duration;
mPlayerSeekBar.setProgress((int) pos);
}
// 設定緩衝進度
int percent = mPlayer.getBufferPercentage();
mPlayerSeekBar.setSecondaryProgress(percent * 10);
}
mVideoProgress.setText(StringUtils.stringForTime(position));
mVideoDuration.setText(StringUtils.stringForTime(duration));
return position;
}
複製程式碼
2、控制各UI佈局顯示,開始傳送訊息
/**
* 顯示控制器
*
* @param timeout 顯示時長
*/
public void show(int timeout) {
setProgress();
if (!isScreenLock) {
mControllerBack.setVisibility(VISIBLE);
mControllerTitle.setVisibility(VISIBLE);
mControllerBottom.setVisibility(VISIBLE);
} else {
if (!DisplayUtils.isPortrait(getContext())) {
mControllerBack.setVisibility(GONE);
}
mControllerTitle.setVisibility(GONE);
mControllerBottom.setVisibility(GONE);
}
if (!DisplayUtils.isPortrait(getContext())) {
mScreenLock.setVisibility(VISIBLE);
}
mShowing = true;
updatePausePlay();
// 開始顯示
post(mShowProgress);
if (timeout > 0) {
// 先移除之前的隱藏非同步操作
removeCallbacks(mFadeOut);
//timeout後隱藏
postDelayed(mFadeOut, timeout);
}
}
複製程式碼
/**
* 非同步操作隱藏
*/
private final Runnable mFadeOut = new Runnable() {
@Override
public void run() {
hide();
}
};
/**
* 非同步操作顯示
*/
private final Runnable mShowProgress = new Runnable() {
@Override
public void run() {
int pos = setProgress();
if (!mDragging && mShowing && mPlayer.isPlaying()) {
// 解決1秒之內的誤差,使得傳送訊息正好卡在整秒
Log.e("TAG", "run: " + (1000 - (pos % 1000)));
postDelayed(mShowProgress, 1000 - (pos % 1000));
}
}
};
複製程式碼
- 首先注意,每當開始傳送訊息,都應強制將之前的訊息全部移除;
- 傳送兩個訊息:一個是計時的訊息,每隔大約一秒獲取當前位置並且更新UI,另一個是延遲顯示時長後隱藏控制器;
- 為什麼每隔大約1秒更新一次UI呢,postDelayed(mShowProgress, 1000 - (pos % 1000)); 我做了一個修正操作,因為各訊息可能會互相影響,其次就是傳送訊息時沒有卡在視訊的整秒位置上,而我們確實整1秒傳送一條訊息,會導致誤差!
如果大家還想了解其他功能,可以去github閱讀我的原始碼github.com/shuaijia/Js…
5、MediaPlayer封裝
主要封裝了
- openVideo:播放視訊,處理各回撥
- start:開始播放
- pause:暫停播放
- seekTo:定位到
- reset:視訊重置
- stop:停止播放
- isPlaying:是否正在播放
- getDuration:獲取總時長
- getCurrentPosition:獲取當前進度
- getBufferPercentage:獲取緩衝進度 等
定義了視訊播放的所用狀態值常量
//出錯狀態
public static final int STATE_ERROR = -1;
//通常狀態
public static final int STATE_IDLE = 0;
//視訊正在準備
public static final int STATE_PREPARING = 1;
//視訊已經準備好
public static final int STATE_PREPARED = 2;
//視訊正在播放
public static final int STATE_PLAYING = 3;
//視訊暫停
public static final int STATE_PAUSED = 4;
//視訊播放完成
public static final int STATE_PLAYBACK_COMPLETED = 5;
複製程式碼
// 播放核心使用MediaPlayer
private MediaPlayer player;
// 當前狀態
private int curState = STATE_IDLE;
// 當前緩衝進度
private int currentBufferPercentage;
// *視訊路徑
private String path;
// 播放監聽
private OnPlayerCallback onPlayerListener;
// 播放視訊承載的view
private SurfaceHolder surfaceHolder;
複製程式碼
封裝了視訊播放狀態的判斷
public boolean isInPlaybackState() {
return (player != null &&
curState != STATE_ERROR &&
curState != STATE_IDLE &&
curState != STATE_PREPARING);
}
複製程式碼
此方法會在其他的所有方法執行之前判斷,如果返回false,則不進行開始播放、重新播放、拖動定位等操作。
同時這些操作執行完後都會更新當前播放狀態,防止視訊不能播的情況下操作報錯。如
/**
* 開始播放
*/
public void start() {
if (isInPlaybackState()) {
player.start();
setCurrentState(STATE_PLAYING);
}
}
複製程式碼
在openVideo中:
public void openVideo() {
if (path == null || surfaceHolder == null) {
return;
}
reset();
player = new MediaPlayer();
// 準備好的監聽
player.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
//因為後面播放時要判斷當前視訊狀態,所以在此一定要先將狀態改變為STATE_PREPARED
//即已經準備好,否則在第一次開啟視訊時無法自動播放
setCurrentState(STATE_PREPARED);
if (onPlayerListener != null) {
onPlayerListener.onPrepared(mp);
}
}
});
// 緩衝監聽
player.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
@Override
public void onBufferingUpdate(MediaPlayer mp, int percent) {
if (onPlayerListener != null) {
onPlayerListener.onBufferingUpdate(mp, percent);
}
currentBufferPercentage = percent;
}
});
// 播放完成監聽
player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
if (onPlayerListener != null) {
onPlayerListener.onCompletion(mp);
}
setCurrentState(STATE_PLAYBACK_COMPLETED);
}
});
// 資訊監聽
player.setOnInfoListener(new MediaPlayer.OnInfoListener() {
@Override
public boolean onInfo(MediaPlayer mp, int what, int extra) {
if (onPlayerListener != null) {
// 701 載入中
if (what == MediaPlayer.MEDIA_INFO_BUFFERING_START) {
onPlayerListener.onLoadingChanged(true);
// 702 載入完成
} else if (what == MediaPlayer.MEDIA_INFO_BUFFERING_END) {
onPlayerListener.onLoadingChanged(false);
}
}
return false;
}
});
// 出錯監聽
player.setOnErrorListener(onErrorListener);
// 視訊大小切換監聽
player.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
@Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
if (onPlayerListener != null) {
onPlayerListener.onVideoSizeChanged(mp, width, height);
}
}
});
currentBufferPercentage = 0;
try {
/**
* 在這裡開始真正的播放
*/
player.setDataSource(path);
player.setDisplay(surfaceHolder);
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
player.setScreenOnWhilePlaying(true);
player.prepareAsync();
Log.e(TAG, "openVideo: " );
setCurrentState(STATE_PREPARING);
} catch (Exception e) {
Log.e(TAG, "openVideo: " + e.toString());
setCurrentState(STATE_ERROR);
onErrorListener.onError(player, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
}
}
複製程式碼
openVideo就是播放視訊的核心方法:新建MediaPlayer物件;將視訊播放的各回撥交給OnPlayerCallback處理;將外部傳進來的SurfaceHolder設定給MediaPlayer,並且prepareAsync之後就可以播放了,當然,不要忘了更新狀態!
SurfaceHolder是surface的抽象介面,使你可以控制surface的大小和格式, 以及在surface上編輯畫素,和監視surace的改變。 SurfaceHolder用於顯示視訊流解析的幀圖片,如果發現SurfaceView變為使用者不可見的時候,則立即銷燬SurfaceView的SurfaceHolder,以達到節約系統資源的目的。
6、手勢控制
說到手勢控制,主要是手勢控制視訊進度,手勢控制音量和螢幕亮度。
對於手勢控制,我自定義了BehaviorView:讓其實現GestureDetector的OnGestureListener
public class VideoBehaviorView extends FrameLayout implements GestureDetector.OnGestureListener{
複製程式碼
在此view中定義以下方法,實現更新UI,交由子類去複寫:
// 更新進度UI,由子類重寫
protected void updateSeekUI(int delProgress) {
// sub
}
// 更新音量UI,由子類重寫
protected void updateVolumeUI(int max, int progress) {
// sub
}
// 更新亮度UI,由子類重寫
protected void updateLightUI(int max, int progress) {
// sub
}
複製程式碼
我的思路是將view的觸控事件全部交給GestureDetector處理:
@Override
public boolean onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_OUTSIDE:
case MotionEvent.ACTION_CANCEL:
endGesture(mFingerBehavior);
break;
}
return true;
}
複製程式碼
當手指按下時,重置手指行為,獲取當前音量、亮度
@Override
public boolean onDown(MotionEvent e) {
//重置 手指行為
mFingerBehavior = -1;
mCurrentVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC);
try {
mCurrentBrightness = (int) (activity.getWindow().getAttributes().screenBrightness * mMaxBrightness);
} catch (Exception exception) {
exception.printStackTrace();
}
return false;
}
複製程式碼
在onScroll方法中:
判斷決定當前為何種型別手勢:左右滑動為調節進度,左半屏上下滑動為調節亮度,右半屏上下滑動為調節音量
/**
* 根據手勢起始2個點斷言 後續行為. 規則如下:
* 螢幕切分為:
* 1.左右扇形區域為視訊進度調節
* 2.上下扇形區域 左半屏亮度調節 後半屏音量調節.
*/
if (mFingerBehavior < 0) {
float moveX = e2.getX() - e1.getX();
float moveY = e2.getY() - e1.getY();
// 如果橫向滑動距離大於縱向滑動距離,則認為在調節進度
if (Math.abs(moveX) >= Math.abs(moveY))
mFingerBehavior = FINGER_BEHAVIOR_PROGRESS;
// 否則為調節音量或亮度
// 按下位置在螢幕左半邊,則是調節亮度
else if (e1.getX() <= width / 2) mFingerBehavior = FINGER_BEHAVIOR_BRIGHTNESS;
// 按下位置在螢幕右半邊,則是在調節音量
else mFingerBehavior = FINGER_BEHAVIOR_VOLUME;
}
複製程式碼
手勢處理
switch (mFingerBehavior) {
case FINGER_BEHAVIOR_PROGRESS: { // 進度變化
// 預設滑動一個螢幕 視訊移動八分鐘.
int delProgress = (int) (1.0f * distanceX / width * 480 * 1000);
// 更新快進彈框
updateSeekUI(delProgress);
break;
}
case FINGER_BEHAVIOR_VOLUME: { // 音量變化
float progress = mMaxVolume * (distanceY / height) + mCurrentVolume;
// 控制調節臨界範圍
if (progress <= 0) progress = 0;
if (progress >= mMaxVolume) progress = mMaxVolume;
am.setStreamVolume(AudioManager.STREAM_MUSIC, Math.round(progress), 0);
updateVolumeUI(mMaxVolume, Math.round(progress));
// 更新當前值
mCurrentVolume = progress;
break;
}
case FINGER_BEHAVIOR_BRIGHTNESS: { // 亮度變化
try {
// 如果系統亮度為自動調節,則改為手動調節
if (Settings.System.getInt(getContext().getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE)
== Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC) {
Settings.System.putInt(getContext().getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE,
Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
}
int progress = (int) (mMaxBrightness * (distanceY / height) + mCurrentBrightness);
// 控制調節臨界範圍
if (progress <= 0) progress = 0;
if (progress >= mMaxBrightness) progress = mMaxBrightness;
Window window = activity.getWindow();
WindowManager.LayoutParams params = window.getAttributes();
params.screenBrightness = progress / (float) mMaxBrightness;
window.setAttributes(params);
updateLightUI(mMaxBrightness, progress);
// 更新當前值
mCurrentBrightness = progress;
} catch (Exception e) {
e.printStackTrace();
}
break;
}
}
複製程式碼
注意:
- 所有的更新UI操作全部交由子類實現
- 注意臨界範圍的控制
- 控制進度時,百分比最後乘以8分鐘,以達到較為適中的使用者體驗,防止視訊時長過大或太小情況下,拖動調節進度變化太過明顯或效果不明顯。
7、播放器JsPlayer封裝
先來看看佈局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:id="@+id/video_surface"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.jia.jsplayer.view.JsVideoControllerView
android:id="@+id/video_controller"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<include
android:id="@+id/video_loading"
layout="@layout/video_controller_loading" />
<com.jia.jsplayer.view.JsVideoSystemOverlay
android:id="@+id/video_system_overlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone"/>
<com.jia.jsplayer.view.JsVideoProgressOverlay
android:id="@+id/video_progress_overlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone"/>
</RelativeLayout>
複製程式碼
JsPlayer視訊播放器整合自上一步中的VideoBehaviorView,注意複寫VideoBehaviorView的更新UI方法。
private SurfaceView surfaceView;
private View loadingView;
private JsVideoProgressOverlay progressView;
private JsVideoSystemOverlay systemView;
private JsVideoControllerView mediaController;
private JsMediaPlayer mMediaPlayer;
複製程式碼
內建封裝過得JsMediaPlayer 物件,控制器、和SurfaceView,還有網路狀態廣播接收器。
初始化player,建立JsMediaPlayer物件,設定視訊播放回撥處理,然後將其設定給ControllerView。
注意:
- 在準備好的監聽中,mediaPlayer執行開始播放,控制器展示,錯誤介面隱藏。
- 在播放出錯時控制器檢查錯誤型別並展示
- 在載入狀態發生改變時隱藏和展示載入中
private void initPlayer() {
mMediaPlayer = new JsMediaPlayer();
// todo 這裡可以優化,將這些回撥全部暴露出去
mMediaPlayer.setOnPlayerListener(new OnPlayerCallback() {
@Override
public void onPrepared(MediaPlayer mp) {
Log.e(TAG, "onPrepared: " );
mMediaPlayer.start();
mediaController.show();
mediaController.hideErrorView();
}
@Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
}
@Override
public void onBufferingUpdate(MediaPlayer mp, int percent) {
}
@Override
public void onCompletion(MediaPlayer mp) {
mediaController.updatePausePlay();
}
@Override
public void onError(MediaPlayer mp, int what, int extra) {
mediaController.checkShowError(false);
}
@Override
public void onLoadingChanged(boolean isShow) {
if (isShow) showLoading();
else hideLoading();
}
@Override
public void onStateChanged(int curState) {
switch (curState) {
case JsMediaPlayer.STATE_IDLE:
am.abandonAudioFocus(null);
break;
case JsMediaPlayer.STATE_PREPARING:
am.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
break;
}
}
});
mediaController.setMediaPlayer(mMediaPlayer);
}
複製程式碼
給SurfaceView設定Callback,返回SurfaceHolder後設定給JsMediaPlayer
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.e(TAG, "surfaceCreated: " );
initWidth = getWidth();
initHeight = getHeight();
if (mMediaPlayer != null) {
mMediaPlayer.setSurfaceHolder(holder);
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
});
複製程式碼
設定路徑,開始播放
public void setPath(final IVideoInfo video) {
if (video == null) {
return;
}
mMediaPlayer.reset();
String videoPath = video.getVideoPath();
mediaController.setVideoInfo(video);
mMediaPlayer.setPath(videoPath);
}
public void startPlay(){
mMediaPlayer.openVideo();
}
複製程式碼
更新UI
@Override
protected void updateSeekUI(int delProgress) {
progressView.show(delProgress, mMediaPlayer.getCurrentPosition(), mMediaPlayer.getDuration());
}
@Override
protected void updateVolumeUI(int max, int progress) {
systemView.show(JsVideoSystemOverlay.SystemType.VOLUME, max, progress);
}
@Override
protected void updateLightUI(int max, int progress) {
systemView.show(JsVideoSystemOverlay.SystemType.BRIGHTNESS, max, progress);
}
複製程式碼
當然不會忘記封裝播放、暫停、停止、定位、獲取總時長等等的基本方法,這裡就不再累贅。
8、使用
涉及到播放網路視訊,許可權少不了
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
複製程式碼
播放本地視訊別忘了6.0許可權適配
佈局中新增
程式碼中
player = (JsPlayer) findViewById(R.id.player);
player.setOnVideoControlListener(new OnVideoControlListener() {
@Override
public void onStartPlay() {
player.startPlay();
}
@Override
public void onBack() {
}
@Override
public void onFullScreen() {
DisplayUtils.toggleScreenOrientation(MainActivity.this);
}
@Override
public void onRetry(int errorStatus) {
}
});
player.setPath(new VideoInfo("藝術人生", path));
複製程式碼
生命週期繫結
@Override
protected void onStop() {
super.onStop();
player.onStop();
}
@Override
protected void onDestroy() {
super.onDestroy();
player.onDestroy();
}
複製程式碼
全屏操作
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
} else if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
}
@Override
public void onBackPressed() {
if (!DisplayUtils.isPortrait(this)) {
if (!player.isLock()) {
DisplayUtils.toggleScreenOrientation(this);
}
} else {
super.onBackPressed();
}
}
複製程式碼
注意所在Activity在清單檔案中應設定android:configChanges="orientation|keyboardHidden|screenSize"
這樣就ok了,播放器封裝完美完成!
希望對大家有所幫助!
獲取更多精彩內容,請關注我的微信公眾號————Android機動車