Android 使用ExoPlayer視頻播放 (二)

一碌冶、緩存

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)聽。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末适肠,一起剝皮案震驚了整個(gè)濱河市霍衫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌侯养,老刑警劉巖敦跌,帶你破解...
    沈念sama閱讀 222,946評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異沸毁,居然都是意外死亡峰髓,警方通過查閱死者的電腦和手機(jī)傻寂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,336評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門息尺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人疾掰,你說(shuō)我怎么就攤上這事搂誉。” “怎么了静檬?”我有些...
    開封第一講書人閱讀 169,716評(píng)論 0 364
  • 文/不壞的土叔 我叫張陵炭懊,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我拂檩,道長(zhǎng)侮腹,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,222評(píng)論 1 300
  • 正文 為了忘掉前任稻励,我火速辦了婚禮父阻,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘望抽。我一直安慰自己加矛,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,223評(píng)論 6 398
  • 文/花漫 我一把揭開白布煤篙。 她就那樣靜靜地躺著斟览,像睡著了一般。 火紅的嫁衣襯著肌膚如雪辑奈。 梳的紋絲不亂的頭發(fā)上苛茂,一...
    開封第一講書人閱讀 52,807評(píng)論 1 314
  • 那天已烤,我揣著相機(jī)與錄音,去河邊找鬼妓羊。 笑死草戈,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的侍瑟。 我是一名探鬼主播唐片,決...
    沈念sama閱讀 41,235評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼涨颜!你這毒婦竟也來(lái)了费韭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,189評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤庭瑰,失蹤者是張志新(化名)和其女友劉穎星持,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體弹灭,經(jīng)...
    沈念sama閱讀 46,712評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡督暂,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,775評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了穷吮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片逻翁。...
    茶點(diǎn)故事閱讀 40,926評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖捡鱼,靈堂內(nèi)的尸體忽然破棺而出八回,到底是詐尸還是另有隱情,我是刑警寧澤驾诈,帶...
    沈念sama閱讀 36,580評(píng)論 5 351
  • 正文 年R本政府宣布缠诅,位于F島的核電站,受9級(jí)特大地震影響乍迄,放射性物質(zhì)發(fā)生泄漏管引。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,259評(píng)論 3 336
  • 文/蒙蒙 一闯两、第九天 我趴在偏房一處隱蔽的房頂上張望褥伴。 院中可真熱鬧,春花似錦生蚁、人聲如沸噩翠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,750評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)伤锚。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間屯援,已是汗流浹背猛们。 一陣腳步聲響...
    開封第一講書人閱讀 33,867評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留狞洋,地道東北人弯淘。 一個(gè)月前我還...
    沈念sama閱讀 49,368評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像吉懊,于是被迫代替她去往敵國(guó)和親庐橙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,930評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程借嗽,因...
    小菜c閱讀 6,451評(píng)論 0 17
  • 客戶問題:幫我看看水晶編程成功了嗎态鳖? 我問高我,「該怎麼做恶导?」 「進(jìn)入他們的心裡浆竭。」 我就連結(jié)水晶惨寿,進(jìn)入水晶裡面邦泄,...
    古茜妲樹二閱讀 365評(píng)論 0 0
  • 細(xì)火慢燉,燉一鍋湯水裂垦。時(shí)間是最好的調(diào)味品顺囊,食物也好,感情也罷缸废,其實(shí)在漸漸逝去的光陰里都慢慢的入了味包蓝。 清冷而細(xì)雨綿...
    只聞清香不見花閱讀 913評(píng)論 37 18
  • 書名:AI.未來(lái)
    經(jīng)營(yíng)你的人生閱讀 160評(píng)論 0 0