一、缓存

1、使用ExoPlayer自带的缓存机制(匹配完整的url地址,相同则使用本地缓存文件播放,视频地址具有时效性参数时无法正确缓存)

创建缓存文件夹

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)) {            // 外部储存可用            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;    }}

创建带缓存的数据解析工厂

// 测量播放带宽,如果不需要可以传nullTransferListener<? 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 第二个参数为单个缓存文件大小,如果需要缓存的文件大小超过此限制,则会分片缓存,不影响播放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);

使用带缓存的数据解析工厂创建资源,和入门的使用一致

 Uri uri = Uri.parse(url);ExtractorMediaSource mediaSource = new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);player.prepare(mediaSource);player.setPlayWhenReady(true);

2、使用第三方库AndroidVideoCache进行缓存(视频地址具有时效性参数时使用此缓存方式)

添加AndroidVideoCache依赖

dependencies {    implementation'com.danikula:videocache:2.7.0'}

自定义缓存文件命名规则

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 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;    }}

创建单例的AndroidVideoCache实例的方法

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进行缓存

HttpProxyCacheServer proxy = HttpProxyCacheUtil.getVideoProxy();// 将url传入,AndroidVideoCache判断是否使用缓存文件url = proxy.getProxyUrl(url);// 创建资源,准备播放Uri uri = Uri.parse(url);ExtractorMediaSource mediaSource = new ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);player.prepare(mediaSource);player.setPlayWhenReady(true);

二、自定义播放界面

1、初级自定义

  • 自定义PlaybackControlView播放控制界面
    新建一个XML布局文件exo_playback_control_view,在这个布局文件里面设计我们想要的布局样式,在SimpleExoPlayerView控件中添加一个:
    app:controller_layout_id=”布局id”
    属性。来表明该SimpleExoPlayerView所对应的PlaybackControlView的布局。

这里要注意几个问题:

控件的id不能随便起,这些id都是定义好的,要与exoPlayer原来PlaybackControlView的布局控件id,名称一致,可通过源码查看具体有哪些id。现在给出部分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"/><item name="exo_prev" type="id"/><item name="exo_next" type="id"/><item name="exo_repeat_toggle " type="id"/><item name="exo_duration " type="id"/><item name="exo_position " type="id"/><item name="exo_progress  " type="id"/>

布局的控件数量可以少(比如上一个,下一个这个功能我不想要,就可以不写,也就不会展示出来),但不能多,也不能出现没有定义的id。比如说:想在控制布局上添加一个展开全屏的按钮,那就实现不了
*DefaultTimeBar默认进度条
可以通过xml设置他的颜色,高度,大小等等

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、高级自定义

当我们需要添加更多按钮,比如全屏按钮时,初级自定义就没办法满足我们的需求,这是需要我们自定义重写SimpleExoPlayerView和PlaybackControlView这两个类。这里以添加全屏按钮为例。
* 自定义PlaybackControlView,添加全屏按钮,点击切换横屏
* 自定义SimpleExoPlayerView,使用自定义PlaybackControlView
* 切换横屏时隐藏其他布局,只显示视频控件,达到全屏效果
复制PlaybackControlView代码,新建ExoVideoPlayBackControlView为我们自定义视频控制类,复制SimpleExoPlayerView代码,新建ExoVideoPlayView为我们自定义视频播放控件,将其中使用的控制器换成ExoVideoPlayBackControlView。为ExoVideoPlayBackControlView新建XML文件view_exo_video_play_back_control,添加全屏按钮,再添加全屏播放时的标题栏布局和控制布局,具体界面按需求实现,并将他们隐藏,在全屏播放时在显示。这里全屏按钮的id不在默认定义的id列表中,所以使用”@+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" />

在构造方法中初始我们的布局和控件,给全屏按钮设置点击事件,点击时横屏,调整界面达成全屏的效果

public class ExoVideoPlayBackControlView extends FrameLayout {    static {        ExoPlayerLibraryInfo.registerModule("goog.exo.ui");    }     ...    private final ComponentListener componentListener;// 事件监听    private final View fillButton; //全屏按钮    private final View exoPlayerControllerBottom; // 默认控制器    private final View exoPlayerControllerTopLandscape; // 全屏标题    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) {                // 设置横屏                changeOrientation(SENSOR_LANDSCAPE);            }        }    ...    }}

在ExoVideoPlayBackControlView切换横竖屏的方法中执行横竖屏切换回调,重新设置是否竖屏参数,修改状态栏属性,在显示和隐藏控制器视图的方法中也要修改状态栏属性

    private synchronized void changeOrientation(@OnOrientationChangedListener.SensorOrientationType int orientation) {        if (orientationListener == null) {            return;        }        // 执行回调        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);            // 显示状态栏            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;            // 收起状态栏,全屏播放            hideSystemStatusUi();        }    }    public void setPortrait(boolean portrait) {        this.portrait = portrait;        // 根据横竖屏情况显示控制器视图        showControllerByDisplayMode();    }    /**     * 在切换横竖屏时和显示控制器视图显示状态栏     */    private void showSystemStatusUi() {        if (videoViewAccessor == null) {            return;        }        int flag = View.SYSTEM_UI_FLAG_VISIBLE;        videoViewAccessor.attachVideoView().setSystemUiVisibility(flag);    }    /**     * 隐藏控制器视图时收起状态栏,全屏播放     */    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);    }    /**     * 横屏时设置横屏顶部标题和横屏底部控制器可见,竖屏时设置竖屏底部控制器可见     */    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);            }        }    }

自定义切换横竖屏监听,在activity中定义回调,并逐层传递activity -> ExoVideoPlayView -> ExoVideoPlayBackControlView,在回调中隐藏除了视频播放空间之外的控件,设置Window的flag,在隐藏显示状态栏时不改变原有布局

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);        // 隐藏显示状态栏时不改变原有布局        window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);        rlTitle.setVisibility(View.GONE);        llOthersAlbumPlay.setVisibility(View.GONE);    }

重写ExoVideoPlayBackControlView的onKeyDown方法,在全屏模式下点击回退按钮,应切换回竖屏,竖屏时执行回退的回调

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,点击全屏播放的功能基本完成,不过还有一些需要完善的地方,比如在全屏播放时显示控制器视图,上边的部分视图会被状态栏挡住,如果手机有虚拟导航栏,导航栏会遮住右边部分视图,所以还需要获取状态高度和虚拟导航栏高度,设置间距

        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");    }    /**     * 获得状态栏的高度     *     * @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;    }}

三、事件监听

ExoPlayer的事件监听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.     * 

* 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 not 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). *

* When a position discontinuity occurs as a result of a change to the timeline this method is * not 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方法返回了是否正在播放和播放状态,播放状态一共以下几种:

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,这两个类中的ComponentListener类实现了这个事件监听。

更多相关文章

  1. Android(安卓)豆瓣电影- RecyclerView
  2. 关于Android(安卓)列表多布局的那些事
  3. 第一行代码(三)
  4. android日记-
  5. 显示gif动画(帧动画的播放)
  6. android 公共顶部栏
  7. Android中设置启动动画
  8. Android开发--玩转WebView
  9. Android常用布局样式介绍

随机推荐

  1. Android(安卓)flutter Json转Dart Model
  2. android中的sqlite操作
  3. android识别鼠标左键,右键操作
  4. Android(安卓)界面设计工具 droiddraw
  5. Android基于TitleBar页面导航实现
  6. Business mobile application developmen
  7. android 实现图片加载效果
  8. Android(安卓)自定义正方形布局
  9. Android(安卓)中歌曲录制。。。
  10. android