一碌冶、緩存
1溺森、使用ExoPlayer自帶的緩存機(jī)制(匹配完整的url地址癞己,相同則使用本地緩存文件播放椒拗,視頻地址具有時(shí)效性參數(shù)時(shí)無(wú)法正確緩存)
創(chuàng)建緩存文件夾
public class CachesUtil {
public static String VIDEO = "video";
/**
* 獲取媒體緩存文件
*
* @param child
* @return
*/
public static File getMediaCacheFile(String child) {
String directoryPath = "";
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
// 外部?jī)?chǔ)存可用
directoryPath = MyApplication.getContext().getExternalFilesDir(child).getAbsolutePath();
} else {
directoryPath = MyApplication.getContext().getFilesDir().getAbsolutePath() + File.separator + child;
}
File file = new File(directoryPath);
//判斷文件目錄是否存在
if (!file.exists()) {
file.mkdirs();
}
LogUtil.d(TAG, "getMediaCacheFile ====> " + directoryPath);
return file;
}
}
創(chuàng)建帶緩存的數(shù)據(jù)解析工廠
// 測(cè)量播放帶寬似将,如果不需要可以傳null
TransferListener<? super DataSource> listener = new DefaultBandwidthMeter();
DefaultDataSourceFactory upstreamFactory = new DefaultDataSourceFactory(this, listener, new DefaultHttpDataSourceFactory("MyApplication", listener));
// 獲取緩存文件夾
File file = CachesUtil.getMediaCacheFile(CachesUtil.VIDEO);
Cache cache = new SimpleCache(file, new NoOpCacheEvictor());
// CacheDataSinkFactory 第二個(gè)參數(shù)為單個(gè)緩存文件大小,如果需要緩存的文件大小超過此限制陡叠,則會(huì)分片緩存玩郊,不影響播放
DataSink.Factory cacheWriteDataSinkFactory = new CacheDataSinkFactory(cache, Long.MAX_VALUE);
CacheDataSourceFactory dataSourceFactory = new CacheDataSourceFactory(cache, upstreamFactory, new FileDataSourceFactory(), cacheWriteDataSinkFactory, CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, null);
使用帶緩存的數(shù)據(jù)解析工廠創(chuàng)建資源肢执,和入門的使用一致
Uri uri = Uri.parse(url);
ExtractorMediaSource mediaSource = new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
player.prepare(mediaSource);
player.setPlayWhenReady(true);
2枉阵、使用第三方庫(kù)AndroidVideoCache進(jìn)行緩存(視頻地址具有時(shí)效性參數(shù)時(shí)使用此緩存方式)
添加AndroidVideoCache依賴
dependencies {
implementation'com.danikula:videocache:2.7.0'
}
自定義緩存文件命名規(guī)則
public class CacheFileNameGenerator implements FileNameGenerator {
private static final String TAG = "CacheFileNameGenerator";
/**
* @param url
* @return
*/
@Override
public String generate(String url) {
Uri uri = Uri.parse(url);
List<String> pathSegList = uri.getPathSegments();
String path = null;
if (pathSegList != null && pathSegList.size() > 0) {
path = pathSegList.get(pathSegList.size() - 1);
} else {
path = url;
}
Log.d(TAG, "generate return " + path);
return path;
}
}
創(chuàng)建單例的AndroidVideoCache實(shí)例的方法
public class HttpProxyCacheUtil {
private static HttpProxyCacheServer videoProxy;
public static HttpProxyCacheServer getVideoProxy() {
if (videoProxy == null) {
videoProxy = new HttpProxyCacheServer.Builder(MyApplication.getContext())
.cacheDirectory(CachesUtil.getMediaCacheFile(CachesUtil.VIDEO))
.maxCacheSize(1024 * 1024 * 1024) // 緩存大小
.fileNameGenerator(new CacheFileNameGenerator())
.build();
}
return videoProxy;
}
}
使用AndroidVideoCache進(jìn)行緩存
HttpProxyCacheServer proxy = HttpProxyCacheUtil.getVideoProxy();
// 將url傳入,AndroidVideoCache判斷是否使用緩存文件
url = proxy.getProxyUrl(url);
// 創(chuàng)建資源预茄,準(zhǔn)備播放
Uri uri = Uri.parse(url);
ExtractorMediaSource mediaSource = new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
player.prepare(mediaSource);
player.setPlayWhenReady(true);
二兴溜、自定義播放界面
1、初級(jí)自定義
- 自定義PlaybackControlView播放控制界面
新建一個(gè)XML布局文件exo_playback_control_view耻陕,在這個(gè)布局文件里面設(shè)計(jì)我們想要的布局樣式拙徽,在SimpleExoPlayerView控件中添加一個(gè):
app:controller_layout_id="布局id"
屬性。來(lái)表明該SimpleExoPlayerView所對(duì)應(yīng)的PlaybackControlView的布局诗宣。
這里要注意幾個(gè)問題:
控件的id不能隨便起膘怕,這些id都是定義好的,要與exoPlayer原來(lái)PlaybackControlView的布局控件id召庞,名稱一致岛心,可通過源碼查看具體有哪些id。現(xiàn)在給出部分id如下:
<item name="exo_play" type="id"/><!--播放-->
<item name="exo_pause " type="id"/><!--暫停-->
<item name="exo_rew " type="id"/><!--后退-->
<item name="exo_ffwd" type="id"/><!--前進(jìn)-->
<item name="exo_prev" type="id"/><!--上一個(gè)-->
<item name="exo_next" type="id"/><!--下一個(gè)-->
<item name="exo_repeat_toggle " type="id"/><!--重復(fù)模式開關(guān)-->
<item name="exo_duration " type="id"/><!--視頻總時(shí)長(zhǎng)-->
<item name="exo_position " type="id"/><!--當(dāng)前播放位置-->
<item name="exo_progress " type="id"/><!--播放總時(shí)長(zhǎng)-->
布局的控件數(shù)量可以少(比如上一個(gè)篮灼,下一個(gè)這個(gè)功能我不想要忘古,就可以不寫,也就不會(huì)展示出來(lái))诅诱,但不能多髓堪,也不能出現(xiàn)沒有定義的id。比如說(shuō):想在控制布局上添加一個(gè)展開全屏的按鈕娘荡,那就實(shí)現(xiàn)不了
*DefaultTimeBar默認(rèn)進(jìn)度條
可以通過xml設(shè)置他的顏色干旁,高度,大小等等
app:bar_height="2dp"
app:buffered_color="#ffffff"
app:played_color="#c15d3e"
app:scrubber_color="#ffffff"
app:scrubber_enabled_size="10dp"
app:unplayed_color="#cdcdcd"
2炮沐、高級(jí)自定義
當(dāng)我們需要添加更多按鈕争群,比如全屏按鈕時(shí),初級(jí)自定義就沒辦法滿足我們的需求央拖,這是需要我們自定義重寫SimpleExoPlayerView和PlaybackControlView這兩個(gè)類祭阀。這里以添加全屏按鈕為例鹉戚。
- 自定義PlaybackControlView,添加全屏按鈕专控,點(diǎn)擊切換橫屏
- 自定義SimpleExoPlayerView抹凳,使用自定義PlaybackControlView
- 切換橫屏?xí)r隱藏其他布局,只顯示視頻控件伦腐,達(dá)到全屏效果
復(fù)制PlaybackControlView代碼赢底,新建ExoVideoPlayBackControlView為我們自定義視頻控制類,復(fù)制SimpleExoPlayerView代碼柏蘑,新建ExoVideoPlayView為我們自定義視頻播放控件幸冻,將其中使用的控制器換成ExoVideoPlayBackControlView。為ExoVideoPlayBackControlView新建XML文件view_exo_video_play_back_control咳焚,添加全屏按鈕,再添加全屏播放時(shí)的標(biāo)題欄布局和控制布局洽损,具體界面按需求實(shí)現(xiàn),并將他們隱藏革半,在全屏播放時(shí)在顯示碑定。這里全屏按鈕的id不在默認(rèn)定義的id列表中,所以使用"@+id/"自己定義
<ImageButton
android:id="@+id/exo_fill"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:background="@null"
android:padding="5dp"
android:scaleType="centerInside"
android:src="@drawable/selector_video_fill" />
在構(gòu)造方法中初始我們的布局和控件又官,給全屏按鈕設(shè)置點(diǎn)擊事件,點(diǎn)擊時(shí)橫屏延刘,調(diào)整界面達(dá)成全屏的效果
public class ExoVideoPlayBackControlView extends FrameLayout {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.ui");
}
...
private final ComponentListener componentListener;// 事件監(jiān)聽
private final View fillButton; //全屏按鈕
private final View exoPlayerControllerBottom; // 默認(rèn)控制器
private final View exoPlayerControllerTopLandscape; // 全屏標(biāo)題
private final View exoPlayerControllerBottomLandscape; // 全屏控制器
...
public ExoVideoPlayBackControlView(Context context, AttributeSet attrs, int defStyleAttr,AttributeSet playbackAttrs) {
super(context, attrs, defStyleAttr);
int controllerLayoutId = R.layout.view_exo_video_play_back_control;
componentListener = new ComponentListener();
...
fillButton = findViewById(R.id.exo_fill);
if (fillButton != null) {
fillButton.setOnClickListener(componentListener);
}
exoPlayerControllerBottom = findViewById(R.id.exoPlayerControllerBottom);
exoPlayerControllerTopLandscape = findViewById(R.id.exoPlayerControllerTopLandscape);
exoPlayerControllerBottomLandscape = findViewById(R.id.exoPlayerControllerBottomLandscape);
}
...
private final class ComponentListener extends Player.DefaultEventListener implements TimeBar.OnScrubListener, OnClickListener {
...
@Override
public void onClick(View view) {
if (player != null) {
if (fillButton == view) {
// 設(shè)置橫屏
changeOrientation(SENSOR_LANDSCAPE);
}
}
...
}
}
在ExoVideoPlayBackControlView切換橫豎屏的方法中執(zhí)行橫豎屏切換回調(diào),重新設(shè)置是否豎屏參數(shù)六敬,修改狀態(tài)欄屬性碘赖,在顯示和隱藏控制器視圖的方法中也要修改狀態(tài)欄屬性
private synchronized void changeOrientation(@OnOrientationChangedListener.SensorOrientationType int orientation) {
if (orientationListener == null) {
return;
}
// 執(zhí)行回調(diào)
orientationListener.onOrientationChanged(orientation);
switch (orientation) {
case SENSOR_PORTRAIT:
// 豎屏
setPortrait(true);
showSystemStatusUi();
break;
case SENSOR_LANDSCAPE:
// 橫屏
setPortrait(false);
showSystemStatusUi();
break;
case SENSOR_UNKNOWN:
default:
break;
}
}
/**
* Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will
* be automatically hidden after this duration of time has elapsed without user input.
*/
public void show() {
if (!isVisible()) {
setVisibility(VISIBLE);
// 顯示狀態(tài)欄
showSystemStatusUi();
if (visibilityListener != null) {
visibilityListener.onVisibilityChange(getVisibility());
}
updateAll();
requestPlayPauseFocus();
}
// Call hideAfterTimeout even if already visible to reset the timeout.
hideAfterTimeout();
}
/**
* Hides the controller.
*/
public void hide() {
if (isVisible()) {
setVisibility(GONE);
if (visibilityListener != null) {
visibilityListener.onVisibilityChange(getVisibility());
}
removeCallbacks(updateProgressAction);
removeCallbacks(hideAction);
hideAtMs = C.TIME_UNSET;
// 收起狀態(tài)欄,全屏播放
hideSystemStatusUi();
}
}
public void setPortrait(boolean portrait) {
this.portrait = portrait;
// 根據(jù)橫豎屏情況顯示控制器視圖
showControllerByDisplayMode();
}
/**
* 在切換橫豎屏?xí)r和顯示控制器視圖顯示狀態(tài)欄
*/
private void showSystemStatusUi() {
if (videoViewAccessor == null) {
return;
}
int flag = View.SYSTEM_UI_FLAG_VISIBLE;
videoViewAccessor.attachVideoView().setSystemUiVisibility(flag);
}
/**
* 隱藏控制器視圖時(shí)收起狀態(tài)欄外构,全屏播放
*/
private void hideSystemStatusUi() {
if (portrait) {
return;
}
if (videoViewAccessor == null) {
return;
}
WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
if (windowManager == null) {
return;
}
int flag = View.SYSTEM_UI_FLAG_LOW_PROFILE
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
flag |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
}
videoViewAccessor.attachVideoView().setSystemUiVisibility(flag);
}
/**
* 橫屏?xí)r設(shè)置橫屏頂部標(biāo)題和橫屏底部控制器可見普泡,豎屏?xí)r設(shè)置豎屏底部控制器可見
*/
private void showControllerByDisplayMode() {
if (exoPlayerControllerTopLandscape != null) {
if (portrait) {
exoPlayerControllerTopLandscape.setVisibility(INVISIBLE);
} else {
exoPlayerControllerTopLandscape.setVisibility(VISIBLE);
}
}
if (exoPlayerControllerBottom != null) {
if (portrait) {
exoPlayerControllerBottom.setVisibility(VISIBLE);
} else {
exoPlayerControllerBottom.setVisibility(INVISIBLE);
}
}
if (exoPlayerControllerBottomLandscape != null) {
if (portrait) {
exoPlayerControllerBottomLandscape.setVisibility(INVISIBLE);
} else {
exoPlayerControllerBottomLandscape.setVisibility(VISIBLE);
}
}
}
自定義切換橫豎屏監(jiān)聽,在activity中定義回調(diào),并逐層傳遞activity -> ExoVideoPlayView -> ExoVideoPlayBackControlView典勇,在回調(diào)中隱藏除了視頻播放空間之外的控件劫哼,設(shè)置Window的flag,在隱藏顯示狀態(tài)欄時(shí)不改變?cè)胁季?/p>
public interface OnOrientationChangedListener {
int SENSOR_UNKNOWN = -1;
int SENSOR_PORTRAIT = SENSOR_UNKNOWN + 1;
int SENSOR_LANDSCAPE = SENSOR_PORTRAIT + 1;
@IntDef({SENSOR_UNKNOWN, SENSOR_PORTRAIT, SENSOR_LANDSCAPE})
@Retention(RetentionPolicy.SOURCE)
@interface SensorOrientationType {
}
void onChanged(@SensorOrientationType int orientation);
}
evpvAlbumPlay.setOrientationListener(new ExoVideoPlayBackControlView.OrientationListener() {
@Override
public void onOrientationChanged(int orientation) {
if (orientation == SENSOR_PORTRAIT) {
changeToPortrait();
} else if (orientation == SENSOR_LANDSCAPE) {
changeToLandscape();
}
}
});
private void changeToPortrait() {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
WindowManager.LayoutParams attr = getWindow().getAttributes();
Window window = getWindow();
window.setAttributes(attr);
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
rlTitle.setVisibility(View.VISIBLE);
llOthersAlbumPlay.setVisibility(View.VISIBLE);
}
private void changeToLandscape() {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
WindowManager.LayoutParams lp = getWindow().getAttributes();
Window window = getWindow();
window.setAttributes(lp);
// 隱藏顯示狀態(tài)欄時(shí)不改變?cè)胁季? window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
rlTitle.setVisibility(View.GONE);
llOthersAlbumPlay.setVisibility(View.GONE);
}
重寫ExoVideoPlayBackControlView的onKeyDown方法,在全屏模式下點(diǎn)擊回退按鈕割笙,應(yīng)切換回豎屏权烧,豎屏?xí)r執(zhí)行回退的回調(diào)
public class ExoVideoPlayBackControlView extends FrameLayout {
public interface ExoClickListener {
boolean onBackClick(@Nullable View view, boolean isPortrait);
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (portrait) {
if (exoClickListener != null) {
exoClickListener.onBackClick(null, portrait);
}
} else {
changeOrientation(SENSOR_PORTRAIT);
return true;
}
}
return super.onKeyDown(keyCode, event);
}
}
evpvAlbumPlay.setBackListener(new ExoVideoPlayBackControlView.ExoClickListener() {
@Override
public boolean onBackClick(@Nullable View view, boolean isPortrait) {
if (isPortrait) {
finish();
}
return false;
}
至此,自定義ExoPlayer伤溉,點(diǎn)擊全屏播放的功能基本完成般码,不過還有一些需要完善的地方,比如在全屏播放時(shí)顯示控制器視圖乱顾,上邊的部分視圖會(huì)被狀態(tài)欄擋住板祝,如果手機(jī)有虛擬導(dǎo)航欄,導(dǎo)航欄會(huì)遮住右邊部分視圖走净,所以還需要獲取狀態(tài)高度和虛擬導(dǎo)航欄高度券时,設(shè)置間距
int navigationHeight = ScreenUtil.getNavigationHeight(context);
exoPlayerControllerBottom = findViewById(R.id.exoPlayerControllerBottom);
exoPlayerControllerTopLandscape = findViewById(R.id.exoPlayerControllerTopLandscape);
exoPlayerControllerTopLandscape.setPadding(0, ScreenUtil.getStatusHeight(context), navigationHeight, 0);
exoPlayerControllerBottomLandscape = findViewById(R.id.exoPlayerControllerBottomLandscape);
View llControllerBottomLandscape = findViewById(R.id.llControllerBottomLandscape);
llControllerBottomLandscape.setPadding(0, 0, navigationHeight, 0);
timeBarLandscape.setPadding(0, 0, navigationHeight, 0);
public class ScreenUtil {
private ScreenUtil() {
private ScreenUtil() {
/* cannot be instantiated */
throw new UnsupportedOperationException("cannot be instantiated");
}
/**
* 獲得狀態(tài)欄的高度
*
* @param context
* @return
*/
public static int getStatusHeight(Context context) {
int statusHeight = -1;
try {
Class<?> clazz = Class.forName("com.android.internal.R$dimen");
Object object = clazz.newInstance();
int height = Integer.parseInt(clazz.getField("status_bar_height")
.get(object).toString());
statusHeight = context.getResources().getDimensionPixelSize(height);
} catch (Exception e) {
e.printStackTrace();
}
return statusHeight;
}
/**
* 獲得NavigationHeight
*
* @param context
* @return
*/
public static int getNavigationHeight(Context context) {
int navigationHeight = 0;
// 屏幕原始尺寸高度孤里,包括虛擬功能鍵高度
int screenHeight = 0;
// 獲取屏幕尺寸,不包括虛擬功能高度
int defaultDisplayHeight = 0;
WindowManager windowManager = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
Display display = windowManager.getDefaultDisplay();
DisplayMetrics dm = new DisplayMetrics();
@SuppressWarnings("rawtypes")
Class c;
try {
c = Class.forName("android.view.Display");
@SuppressWarnings("unchecked")
Method method = c.getMethod("getRealMetrics", DisplayMetrics.class);
method.invoke(display, dm);
screenHeight = dm.heightPixels;
} catch (Exception e) {
e.printStackTrace();
}
Point outSize = new Point();
windowManager.getDefaultDisplay().getSize(outSize);
defaultDisplayHeight = outSize.y;
navigationHeight = screenHeight - defaultDisplayHeight;
return navigationHeight;
}
}
三橘洞、事件監(jiān)聽
ExoPlayer的事件監(jiān)聽EventListener捌袜,通過Player的addListener方法和removeListener方法添加和刪除。
public interface Player {
/**
* Listener of changes in player state.
*/
interface EventListener {
/**
* Called when the timeline and/or manifest has been refreshed.
* <p>
* Note that if the timeline has changed then a position discontinuity may also have occurred.
* For example, the current period index may have changed as a result of periods being added or
* removed from the timeline. This will <em>not</em> be reported via a separate call to
* {@link #onPositionDiscontinuity(int)}.
*
* @param timeline The latest timeline. Never null, but may be empty.
* @param manifest The latest manifest. May be null.
*/
void onTimelineChanged(Timeline timeline, Object manifest);
/**
* Called when the available or selected tracks change.
*
* @param trackGroups The available tracks. Never null, but may be of length zero.
* @param trackSelections The track selections for each renderer. Never null and always of
* length {@link #getRendererCount()}, but may contain null elements.
*/
void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections);
/**
* Called when the player starts or stops loading the source.
*
* @param isLoading Whether the source is currently being loaded.
*/
void onLoadingChanged(boolean isLoading);
/**
* Called when the value returned from either {@link #getPlayWhenReady()} or
* {@link #getPlaybackState()} changes.
*
* @param playWhenReady Whether playback will proceed when ready.
* @param playbackState One of the {@code STATE} constants.
*/
void onPlayerStateChanged(boolean playWhenReady, int playbackState);
/**
* Called when the value of {@link #getRepeatMode()} changes.
*
* @param repeatMode The {@link RepeatMode} used for playback.
*/
void onRepeatModeChanged(@RepeatMode int repeatMode);
/**
* Called when the value of {@link #getShuffleModeEnabled()} changes.
*
* @param shuffleModeEnabled Whether shuffling of windows is enabled.
*/
void onShuffleModeEnabledChanged(boolean shuffleModeEnabled);
/**
* Called when an error occurs. The playback state will transition to {@link #STATE_IDLE}
* immediately after this method is called. The player instance can still be used, and
* {@link #release()} must still be called on the player should it no longer be required.
*
* @param error The error.
*/
void onPlayerError(ExoPlaybackException error);
/**
* Called when a position discontinuity occurs without a change to the timeline. A position
* discontinuity occurs when the current window or period index changes (as a result of playback
* transitioning from one period in the timeline to the next), or when the playback position
* jumps within the period currently being played (as a result of a seek being performed, or
* when the source introduces a discontinuity internally).
* <p>
* When a position discontinuity occurs as a result of a change to the timeline this method is
* <em>not</em> called. {@link #onTimelineChanged(Timeline, Object)} is called in this case.
*
* @param reason The {@link DiscontinuityReason} responsible for the discontinuity.
*/
void onPositionDiscontinuity(@DiscontinuityReason int reason);
/**
* Called when the current playback parameters change. The playback parameters may change due to
* a call to {@link #setPlaybackParameters(PlaybackParameters)}, or the player itself may change
* them (for example, if audio playback switches to passthrough mode, where speed adjustment is
* no longer possible).
*
* @param playbackParameters The playback parameters.
*/
void onPlaybackParametersChanged(PlaybackParameters playbackParameters);
/**
* Called when all pending seek requests have been processed by the player. This is guaranteed
* to happen after any necessary changes to the player state were reported to
* {@link #onPlayerStateChanged(boolean, int)}.
*/
void onSeekProcessed();
}
}
其中onPlayerStateChanged方法返回了是否正在播放和播放狀態(tài)炸枣,播放狀態(tài)一共以下幾種:
public interface Player {
/**
* The player does not have any media to play.
*/
int STATE_IDLE = 1;
/**
* The player is not able to immediately play from its current position. This state typically
* occurs when more data needs to be loaded.
*/
int STATE_BUFFERING = 2;
/**
* The player is able to immediately play from its current position. The player will be playing if
* {@link #getPlayWhenReady()} is true, and paused otherwise.
*/
int STATE_READY = 3;
/**
* The player has finished playing the media.
*/
int STATE_ENDED = 4;
}
具體使用可參考SimpleExoPlayerView和PlaybackControlView虏等,這兩個(gè)類中的ComponentListener類實(shí)現(xiàn)了這個(gè)事件監(jiān)聽。