使用 ExoPlayer 实现 列表视频自动播放

官网地址及github地址

https://github.com/google/ExoPlayer
https://exoplayer.dev/

源码地址

视频播放代码在 .exoplayer 包
https://github.com/wuchao226/Jetpackppjoke

先看效果图

preview.gif

ExoPlayer 的特点及简单介绍

ExoPlayer 是 Google 官方推出的一款开源的应用级别的音视频播放框架,它是一个独立的库,所以我们可以在我们的项目中进行相应的库引用,非常的方便。也可以自己通过开源代码进行定制、修改、扩展。

简单使用

1、项目根目录的build.gradle里添加仓库地址

repositories {
    google()
    jcenter()
}

项目app目录的下build.gradle里添加ExoPlayer库地址。

implementation 'com.google.android.exoplayer:exoplayer:2.X.X'
// 例如我这里使用2.11.4版本:
implementation 'com.google.android.exoplayer:exoplayer:2.11.4'

具体的版本号信息和更新的概要可以在这里查看:https://github.com/google/ExoPlayer/blob/release-v2/RELEASENOTES.md

如果只需要引入其中的几个功能模块的话,我们也可以分拆开进行引用:

implementation 'com.google.android.exoplayer:exoplayer-core:2.X.X'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.X.X'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.X.X'

implementation 'com.google.android.exoplayer:exoplayer-hls:2.X.X'
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.X.X'

整个ExoPlayer库包括5个子库,依赖了整个ExoPlayer库和依赖5个子库是等效的。

  • exoplayer-core:核心功能 (必要)
  • exoplayer-dash:支持DASH内容
  • exoplayer-hls:支持HLS内容
  • exoplayer-smoothstreaming:支持SmoothStreaming内容
  • exoplayer-ui:用于ExoPlayer的UI组件和相关的资源。

根据自己的需要进行引用,core核心包必须引用,ui包也建议引用。
开启Java8语法支持:

compileOptions {
  targetCompatibility JavaVersion.VERSION_1_8
}

ExoPlayer 的 FFmpeg 扩展提供 FfmpegAudioRenderer,使用 FFmpeg 进行解码,并可以呈现各种格式编码的音频。

ExoPlayer库的核心是ExoPlayer接口,ExoPlayer的API暴露了基本上大部分的媒体播放操作功能,比如缓冲媒体、播放、暂停和快进、媒体监听等功能。

基本功能使用的话我们只需要关心这几个类:

  • PlayerView:播放器的渲染界面UI;
  • SimpleExoPlayer/ExoPlayer:播放器核心API类;
  • MediaSource:媒体资源,用于定义要播放的媒体,加载媒体,加载音视频的播放源地址,以及从哪里加载媒体,简单的说,MediaSource就是代表我们要播放的媒体文件,可以是本地资源,可以是网络资源。MediaSource在播放开始的时候,通过ExoPlayer.prepare方法注入。,MediaSource 有很多扩展类,如 ConcatenatingMediaSource、ClippingMediaSource、LoopingMediaSource、MergingMediaSource、DashMediaSource、SsMediaSource、HlsMediaSource、ProgressiveMediaSource等,都有不同的功能。
  • TrackSelector:轨道选择器(音轨设置),用于选择 MediaSource 提供的轨道(tracks),供每个可用的渲染器使用,一般使用 DefaultTrackSelector 即可。
  • Renderer:渲染器,用于渲染媒体文件。当创建播放器的时候,Renderers被注入。
  • LoadControl:用于控制 MediaSource 何时缓冲更多的媒体资源以及缓冲多少媒体资源。LoadControl 在创建播放器的时候被注入。一般使用 DefaultLoadControl 即可。

接下来我们看下具体使用步骤:
布局中引入PlayerView:
如 layout_exo_player_view.xml

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.exoplayer2.ui.PlayerView 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/player_view"
    android:keepScreenOn="true"
    app:use_controller="false"
    app:show_timeout="1000"
    app:keep_content_on_player_reset="false"
    app:surface_type="texture_view"
    app:resize_mode="zoom"
    app:show_buffering="never"
    app:player_layout_id="@layout/layout_simple_exo_player_view">
</com.google.android.exoplayer2.ui.PlayerView>

属性介绍:

  • android:keepScreenOn="true" :true:屏幕常亮
  • app:use_controller="false" :是否使用 PlayerView 提供的默认控制器(视频加载、播放) false:不使用默认使用的播放控制界面;默认的 PlayerControlView 的控制界面是 R.layout.exo_playback_control_view.xml。可以直接从ExoPlayer库中复制到app的res目录下面,然后做相应的更改即可。
  • app:show_timeout="1000":控制界面自动消失时间是10秒。自定义的播放控制器和 PlayerView 绑定时, 这个播放控制器显示多久之后自动隐藏掉
  • app:keep_content_on_player_reset="false":player 重置时是否需要保留最后一帧, true:列表上下滑动时可能出现上个视频的最后一帧
  • app:fastforward_increment="30000":快进30秒
  • app:rewind_increment="30000":快退30秒
  • app:surface_type="texture_view" :指定显示视频画面 View 的类型
  • app:resize_mode="zoom":视频画面帧 的缩放形式
  • app:show_buffering="never":当视频缓冲加载时是否需要显示默认的loading加载框
    -app:player_layout_id="@layout/layout_simple_exo_player_view":指定 PlayerView 的布局样式

自定义的 layout_simple_exo_player_view.xml:用于指定给 app:player_layout_id, 指定 PlayerView 的布局样,默认的布局样式是R.layout.exo_player_view,可以复制后做相应的更改

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">

    <com.google.android.exoplayer2.ui.AspectRatioFrameLayout
        android:id="@id/exo_content_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center">

        <!-- Video surface will be inserted as the first child of the content frame. -->
    </com.google.android.exoplayer2.ui.AspectRatioFrameLayout>
</merge>

自定义界面

ExoPlayer 默认使用的播放控制界面是PlayerControlView如果完全不想使用这个控制界面,可以在布局文件里面修改

<com.google.android.exoplayer2.ui.PlayerView
   [...]
   app:use_controller="false"/>

这样控制界面就不显示了。
布局中引入PlayerControlView:
如 layout_exo_player_contorller_view.xml(布局层级优化之后的视频播放控制器)

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.exoplayer2.ui.PlayerControlView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/control_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom"
    android:orientation="vertical"
    app:controller_layout_id="@layout/layout_simple_exo_player_controller_view">
</com.google.android.exoplayer2.ui.PlayerControlView>

layout_simple_exo_player_controller_view.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom"
    android:background="#0D000000"
    android:gravity="center_vertical"
    android:layoutDirection="ltr"
    android:orientation="horizontal"
    tools:targetApi="28">


    <TextView
        android:id="@id/exo_position"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:includeFontPadding="false"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:textColor="#ffffff"
        android:textSize="14sp"
        android:textStyle="bold" />

    <com.google.android.exoplayer2.ui.DefaultTimeBar
        android:id="@id/exo_progress"
        android:layout_width="0dp"
        android:layout_height="26dp"
        android:layout_weight="1"
        android:visibility="visible" />

    <TextView
        android:id="@id/exo_duration"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:includeFontPadding="false"
        android:paddingLeft="4dp"
        android:paddingRight="4dp"
        android:textColor="#ffffff"
        android:textSize="14sp"
        android:textStyle="bold" />
</LinearLayout>

主要功能代码

1、首先需要一个关联类,用来管理页面上的视频播放器和显示视频画面的view以及视频播放控制器的管理类

/**
 * @desciption 管理页面上的视频播放器
 */
public class PageListPlay {
    /**
     * 播放器核心API类
     */
    public SimpleExoPlayer exoPlayer;
    public PlayerView playerView;
    public PlayerControlView controlView;
    /**
     * 代表正在播放的视频 url,并用来判断 exoPlayer 之前播放的url和即将要播放的url是否是同一个媒体资源
     * 如果是同一个只需要恢复继续播放即可,反正创建新的 MediaSource 给 exoPlayer 去播放
     */
    public String playUrl;

    public PageListPlay() {
        Application application = AppGlobals.getApplication();
        //创建exoplayer播放器实例
        exoPlayer = new SimpleExoPlayer.Builder(application,
                //视频每一这的画面如何渲染,实现默认的实现类
                new DefaultRenderersFactory(application))
                //测量播放过程中的带宽,如果不需要,可以为null
                .setBandwidthMeter(new DefaultBandwidthMeter.Builder(application).build())
                //视频的音视频轨道如何加载,使用默认的轨道选择器
                .setTrackSelector(new DefaultTrackSelector(application))
                //视频缓存控制逻辑,使用默认的即可
                .setLoadControl(new DefaultLoadControl())
                .build();

        //加载咱们布局层级优化之后的能够展示视频画面的View
        playerView = (PlayerView) LayoutInflater.from(application).inflate(R.layout.layout_exo_player_view,
                null, false);

        //加载咱们布局层级优化之后的视频播放控制器
        controlView = (PlayerControlView) LayoutInflater.from(application).inflate(R.layout.layout_exo_player_contorller_view,
                null, false);

        //把播放器实例 和 playerView,controlView相关联
        //如此视频画面才能正常显示,播放进度条才能自动更新
        playerView.setPlayer(exoPlayer);
        controlView.setPlayer(exoPlayer);
    }

    public void release() {
        if (exoPlayer != null) {
            exoPlayer.setPlayWhenReady(false);
            exoPlayer.stop(true);
            exoPlayer.release();
            exoPlayer = null;
        }

        if (playerView != null) {
            playerView.setPlayer(null);
            playerView = null;
        }

        if (controlView != null) {
            controlView.setPlayer(null);
            controlView = null;
        }
    }

    /**
     * 切换与播放器 exoplayer 绑定的 exoplayerView。用于页面切换视频无缝续播的场景
     *
     * @param newPlayerView
     * @param attach
     */
    public void switchPlayerView(PlayerView newPlayerView, boolean attach) {
        playerView.setPlayer(attach ? null : exoPlayer);
        newPlayerView.setPlayer(attach ? exoPlayer : null);
    }
}

2、还需要一个管理类,用来管理每一个页面的 PageListPlay 对象。

/**
 * @desciption 能适应多个页面视频播放的 播放器管理者
 * 每个页面一个播放器
 * 方便管理每个页面的暂停/恢复操作
 */
public class PageListPlayManager {
    /**
     * 播放媒体的MediaSource
     */
    private static final ProgressiveMediaSource.Factory mediaSourceFactory;
    /**
     * 存储每一个页面对应的 PageListPlay 对象
     * key:String类型的,代表每一个页面的生成标志
     */
    private static HashMap<String, PageListPlay> sPageListPlayHashMap = new HashMap<>();

    static {
        Application application = AppGlobals.getApplication();
        //创建http视频资源如何加载的工厂对象
        DefaultHttpDataSourceFactory dataSourceFactory = new DefaultHttpDataSourceFactory(
                Util.getUserAgent(application, application.getPackageName()));
        //创建缓存,指定缓存位置,和缓存策略,为最近最少使用原则,最大为200m
        Cache cache = new SimpleCache(application.getCacheDir(),
                new LeastRecentlyUsedCacheEvictor(1024 * 1024 * 200), new ExoDatabaseProvider(application));
        //把缓存对象cache和负责缓存数据读取、写入的工厂类CacheDataSinkFactory 相关联
        CacheDataSinkFactory cacheDataSinkFactory = new CacheDataSinkFactory(cache, Long.MAX_VALUE);

        /* 创建能够 边播放边缓存的 本地资源加载和http网络数据写入的工厂类
         * public CacheDataSourceFactory(
         *       Cache cache, 缓存写入策略和缓存写入位置的对象
         *       DataSource.Factory upstreamFactory,http视频资源如何加载的工厂对象
         *       DataSource.Factory cacheReadDataSourceFactory,本地缓存数据如何读取的工厂对象
         *       @Nullable DataSink.Factory cacheWriteDataSinkFactory,http网络数据如何写入本地缓存的工厂对象
         *       @CacheDataSource.Flags int flags,加载本地缓存数据进行播放时的策略,如果遇到该文件正在被写入数据,或读取缓存数据发生错误时的策略
         *       @Nullable CacheDataSource.EventListener eventListener  缓存数据读取的回调
         */
        CacheDataSourceFactory cacheDataSourceFactory = new CacheDataSourceFactory(
                cache,
                dataSourceFactory,
                new FileDataSource.Factory(),
                cacheDataSinkFactory,
                CacheDataSource.FLAG_BLOCK_ON_CACHE,
                null);

        //最后 还需要创建一个 MediaSource 媒体资源 加载的工厂类
        //因为由它创建的MediaSource 能够实现边缓冲边播放的效果,
        //如果需要播放hls,m3u8 则需要创建DashMediaSource.Factory()
        mediaSourceFactory = new ProgressiveMediaSource.Factory(cacheDataSourceFactory);
    }

    public static MediaSource createMediaSource(String url) {
        return mediaSourceFactory.createMediaSource(Uri.parse(url));
    }
     /**
     * 获取每一个页面的 PageListPlay 对象
     */
    public static PageListPlay get(String pageName) {
        PageListPlay pageListPlay = sPageListPlayHashMap.get(pageName);
        if (pageListPlay == null) {
            pageListPlay = new PageListPlay();
            sPageListPlayHashMap.put(pageName, pageListPlay);
        }
        return pageListPlay;
    }
    /**
     * 销毁
     */
    public static void release(String pageName) {
        PageListPlay pageListPlay = sPageListPlayHashMap.get(pageName);
        if (pageListPlay != null) {
            pageListPlay.release();
        }
    }
}

3、列表滚动时自动播放的检测逻辑,并写个接口,面向接口来编程

/**
 * @desciption 视频播放的 接口
 */
public interface IPlayTarget {
    /**
     * 得到 PlayerView 所在的容器,得到 View 后才能在列表滚动的时候去检测它的位置是否满足自动播放
     *
     * @return ViewGroup
     */
    ViewGroup getOwner();

    /**
     * 活跃状态 视频可播放(满足自动播放时回调)
     */
    void onActive();

    /**
     * 非活跃状态,暂停它(列表滚出屏幕时回调,恢复状态停止播放)
     */
    void inActive();

    /**
     * 当前 PlayTarget 是否在播放,帮助我们完成自动播放检测逻辑
     *
     * @return boolean
     */
    boolean isPlaying();
}

/**
 * @desciption 列表视频自动播放 检测逻辑
 */
public class PageListPlayDetector {

    /**
     * 收集一个个的能够进行视频播放的 对象,面向接口
     */
    private List<IPlayTarget> mTargets = new ArrayList<>();
    private RecyclerView mRecyclerView;
    /**
     * 正在播放的那个
     */
    private IPlayTarget mPlayingTarget;
    /**
     * RecyclerView 在屏幕上的位置
     */
    private Pair<Integer, Integer> rvLocation = null;
    private Runnable delayAutoPlay = new Runnable() {
        @Override
        public void run() {
            autoPlay();
        }
    };
    private RecyclerView.AdapterDataObserver mDataObserver = new RecyclerView.AdapterDataObserver() {
        /**
         * 数据添加到 RecyclerView 后 回调该方法
         */
        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            super.onItemRangeInserted(positionStart, itemCount);
            autoPlay();
        }
    };
    private RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
        @Override
        public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
            super.onScrollStateChanged(recyclerView, newState);
            //SCROLL_STATE_IDLE 代表RecyclerView现在不是滚动状态
            //SCROLL_STATE_DRAGGING 代表RecyclerView处于被外力引导的滚动状态,比如手指正在拖着进行滚动。
            //SCROLL_STATE_SETTLING 代表RecyclerView处于自动滚动的状态,此时手指已经离开屏幕,RecyclerView的滚动是自身的惯性在维持
            if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                autoPlay();
            }
        }

        /**
         * 获取RecyclerView的滚动距离
         */
        @Override
        public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
            super.onScrolled(recyclerView, dx, dy);
            //当dx > 0 时,代表手指向左拖动,RecyclerView则从右向左滚动。
            //当dx < 0时,代表手指向右拖动,RecyclerView则从左向右滚动。
            //当dy > 0时,代表手指向上拖动,RecyclerView则从上向下滚动(就是我们最常见的,从顶部开始往下滚动)。
            //当dy < 0时,代表手指向下拖动,RecyclerView则从下向上滚动(就是从列表底部往回挥动)。

            if (dx == 0 && dy == 0) {
                //时序问题。当执行了AdapterDataObserver#onItemRangeInserted  可能还没有被布局到RecyclerView上。
                //所以此时 recyclerView.getChildCount()还是等于0的。
                //等childView 被布局到RecyclerView上之后,会执行onScrolled()方法
                //并且此时 dx,dy都等于0
                postAutoPlay();
            } else {
                //如果有正在播放的,且滑动时被划出了屏幕 则 停止他
                if (mPlayingTarget != null && mPlayingTarget.isPlaying() && !isTargetInBounds(mPlayingTarget)) {
                    mPlayingTarget.inActive();
                }
            }
        }
    };

    public PageListPlayDetector(LifecycleOwner owner, RecyclerView recyclerView) {
        mRecyclerView = recyclerView;
        //监听生命周期
        owner.getLifecycle().addObserver(new LifecycleEventObserver() {
            @Override
            public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
                if (event == Lifecycle.Event.ON_DESTROY) {
                    mPlayingTarget = null;
                    mTargets.clear();
                    recyclerView.removeOnScrollListener(mScrollListener);
                    owner.getLifecycle().removeObserver(this);
                }
            }
        });
        //监听有新的数据添加到 RecyclerView
        recyclerView.getAdapter().registerAdapterDataObserver(mDataObserver);
        recyclerView.addOnScrollListener(mScrollListener);
    }

    private void postAutoPlay() {
        mRecyclerView.post(delayAutoPlay);
    }

    /**
     * 自动播放 检测
     */
    private void autoPlay() {
        //判断屏幕上是否已经有视屏类型的 item
        if (mTargets.size() <= 0 || mRecyclerView.getChildCount() <= 0) {
            return;
        }
        //上一个 target 正在播放并且处于屏幕内,不需要检测新的 target
        if (mPlayingTarget != null && mPlayingTarget.isPlaying() && isTargetInBounds(mPlayingTarget)) {
            return;
        }

        IPlayTarget activeTarget = null;
        for (IPlayTarget target : mTargets) {
            //判断 PlayTarget 是否有一半以上的 View 处在屏幕内
            boolean inBounds = isTargetInBounds(target);
            if (inBounds) {
                //找到满足自动播放条件的 target
                activeTarget = target;
                break;
            }
        }
        if (activeTarget != null) {
            //把上一个满足自动播放条件的 target 关闭
            if (mPlayingTarget != null && mPlayingTarget.isPlaying()) {
                //停止播放
                mPlayingTarget.inActive();
            }
            //找到满足自动播放条件的 target,进行全局保存
            mPlayingTarget = activeTarget;
            //播放
            activeTarget.onActive();
        }
    }

    /**
     * 检测 IPlayTarget 所在的 viewGroup 是否至少还有一半的大小在屏幕内
     *
     * @param target IPlayTarget
     * @return boolean
     */
    private boolean isTargetInBounds(IPlayTarget target) {
        //得到 PlayerView 所在的容器
        ViewGroup owner = target.getOwner();
        //RecyclerView 在屏幕上的位置
        ensureRecyclerViewLocation();
        //如果 owner 没有被展示出来或者没有 Attached 到 Window 上面
        if (!owner.isShown() || !owner.isAttachedToWindow()) {
            return false;
        }
        //计算 owner 在屏幕上的位置
        int[] location = new int[2];
        owner.getLocationOnScreen(location);
        //计算 owner 的中心在屏幕上的位置
        int center = location[1] + owner.getHeight() / 2;

        //承载视频播放画面的ViewGroup它需要至少一半的大小 在RecyclerView上下范围内
        return center >= rvLocation.first && center <= rvLocation.second;
    }

    private Pair<Integer, Integer> ensureRecyclerViewLocation() {
        if (rvLocation == null) {
            int[] location = new int[2];
            mRecyclerView.getLocationOnScreen(location);
            int top = location[1];
            int bottom = top + mRecyclerView.getHeight();
            rvLocation = new Pair(top, bottom);
        }
        return rvLocation;
    }

    public void addTarget(IPlayTarget target) {
        mTargets.add(target);
    }

    public void removeTarget(IPlayTarget target) {
        mTargets.remove(target);
    }

    public void onPause() {
        if (mPlayingTarget != null) {
            mPlayingTarget.inActive();
        }
    }

    public void onResume() {
        if (mPlayingTarget != null) {
            mPlayingTarget.onActive();
        }
    }
}

用于列表视频播放 ListPlayerView

/**
 * @desciption: 列表视频播放专用
 */
public class ListPlayerView extends FrameLayout implements IPlayTarget, PlayerControlView.VisibilityListener,
        Player.EventListener {

    public View bufferView;
    public PPImageView cover, blur;
    protected AppCompatImageView playBtn;
    protected String mCategory;
    protected String mVideoUrl;
    protected boolean isPlaying;
    protected int mWidthPx;
    protected int mHeightPx;

    public ListPlayerView(@NonNull Context context) {
        this(context, null);
    }

    public ListPlayerView(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ListPlayerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public ListPlayerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        LayoutInflater.from(context).inflate(R.layout.layout_player_view, this, true);

        //缓冲转圈圈的view
        bufferView = findViewById(R.id.buffer_view);
        //封面view
        cover = findViewById(R.id.cover);
        //高斯模糊背景图,防止出现两边留嘿
        blur = findViewById(R.id.blur_background);
        //播放盒暂停的按钮
        playBtn = findViewById(R.id.play_btn);

        playBtn.setOnClickListener(v -> {
            if (isPlaying()) {
                inActive();
            } else {
                onActive();
            }
        });

        this.setTransitionName("listPlayerView");
    }

    /**
     * 视频播放状态
     *
     * @param playWhenReady 播放是否继续
     * @param playbackState 播放状态
     */
    @Override
    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
        //监听视频播放的状态
        PageListPlay pageListPlay = PageListPlayManager.get(mCategory);
        SimpleExoPlayer exoPlayer = pageListPlay.exoPlayer;
        //视频已经开始播放,exoPlayer 缓存区不等于0
        if (playbackState == Player.STATE_READY && exoPlayer.getBufferedPosition() != 0 && playWhenReady) {
            //隐藏封面图
            cover.setVisibility(GONE);
            //隐藏缓冲的转圈图
            bufferView.setVisibility(GONE);
        } else if (playbackState == Player.STATE_BUFFERING) {
            //视频在缓冲

            //显示缓冲图
            bufferView.setVisibility(VISIBLE);
        }
        isPlaying = playbackState == Player.STATE_READY && exoPlayer.getBufferedPosition() != 0 && playWhenReady;
        playBtn.setImageResource(isPlaying ? R.drawable.icon_video_pause : R.drawable.icon_video_play);
    }

    public void bindData(String category, int widthPx, int heightPx, String coverUrl, String videoUrl) {
        mCategory = category;
        mVideoUrl = videoUrl;
        mWidthPx = widthPx;
        mHeightPx = heightPx;
        cover.setImageUrl(coverUrl);

        //如果该视频的宽度小于高度,则高斯模糊背景图显示出来
        if (widthPx < heightPx) {
            PPImageView.setBlurImageUrl(blur, coverUrl, 10);
            blur.setVisibility(VISIBLE);
        } else {
            blur.setVisibility(INVISIBLE);
        }
        setSize(widthPx, heightPx);
    }

    protected void setSize(int widthPx, int heightPx) {
        //这里主要是做视频宽大与高,或者高大于宽时  视频的等比缩放
        int maxWidth = PixUtils.getScreenWidth();
        int maxHeight = maxWidth;

        int layoutWidth = maxWidth;
        int layoutHeight = 0;

        int coverWidth;
        int coverHeight;
        if (widthPx >= heightPx) {
            coverWidth = maxWidth;
            layoutHeight = coverHeight = (int) (heightPx / (widthPx * 1.0f / maxWidth));
        } else {
            layoutHeight = coverHeight = maxHeight;
            coverWidth = (int) (widthPx / (heightPx * 1.0f / maxHeight));
        }

        ViewGroup.LayoutParams params = getLayoutParams();
        params.width = layoutWidth;
        params.height = layoutHeight;
        setLayoutParams(params);

        ViewGroup.LayoutParams blurParams = blur.getLayoutParams();
        blurParams.width = layoutWidth;
        blurParams.height = layoutHeight;
        blur.setLayoutParams(blurParams);

        FrameLayout.LayoutParams coverParams = (LayoutParams) cover.getLayoutParams();
        coverParams.width = coverWidth;
        coverParams.height = coverHeight;
        coverParams.gravity = Gravity.CENTER;
        cover.setLayoutParams(coverParams);

        FrameLayout.LayoutParams playBtnParams = (LayoutParams) playBtn.getLayoutParams();
        playBtnParams.gravity = Gravity.CENTER;
        playBtn.setLayoutParams(playBtnParams);
    }

    @Override
    public void onVisibilityChange(int visibility) {
        playBtn.setVisibility(visibility);
        playBtn.setImageResource(isPlaying() ? R.drawable.icon_video_pause : R.drawable.icon_video_play);
    }

    /**
     * 得到 PlayerView 所在的容器,得到 View 后才能在列表滚动的时候去检测它的位置是否满足自动播放
     */
    @Override
    public ViewGroup getOwner() {
        return this;
    }

    /**
     * 活跃状态 视频可播放(满足自动播放时回调)
     */
    @Override
    public void onActive() {
        //视频播放,或恢复播放

        //通过该View所在页面的mCategory(比如首页列表tab_all,沙发tab的tab_video,标签帖子聚合的tag_feed) 字段,
        //取出管理该页面的 Exoplayer 播放器,ExoplayerView 播放 View,控制器对象 PageListPlay
        PageListPlay pageListPlay = PageListPlayManager.get(mCategory);
        PlayerView playerView = pageListPlay.playerView;
        PlayerControlView controlView = pageListPlay.controlView;
        SimpleExoPlayer exoPlayer = pageListPlay.exoPlayer;
        if (playerView == null) {
            return;
        }
        //此处我们需要主动调用一次 switchPlayerView,把播放器Exoplayer和展示视频画面的View ExoplayerView相关联
        //为什么呢?因为在列表页点击视频Item跳转到视频详情页的时候,详情页会复用列表页的播放器Exoplayer,然后和新创建的展示视频画面的View ExoplayerView相关联,达到视频无缝续播的效果
        //如果 我们再次返回列表页,则需要再次把播放器和ExoplayerView相关联
        pageListPlay.switchPlayerView(playerView, true);

        ViewParent parent = playerView.getParent();
        if (parent != this) {
            //把展示视频画面的View添加到ItemView的容器上
            if (parent != null) {
                ((ViewGroup) parent).removeView(playerView);
                //还应该暂停掉列表上正在播放的那个
                ((ListPlayerView) parent).inActive();
            }
            ViewGroup.LayoutParams coverParams = cover.getLayoutParams();
            this.addView(playerView, 1, coverParams);
        }

        ViewParent ctrlParent = controlView.getParent();
        if (ctrlParent != this) {
            //把视频控制器 添加到ItemView的容器上
            if (ctrlParent != null) {
                ((ViewGroup) ctrlParent).removeView(controlView);
            }
            FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
            params.gravity = Gravity.BOTTOM;
            this.addView(controlView, params);
        }

        //如果是同一个视频资源,则不需要从重新创建mediaSource。
        //但需要onPlayerStateChanged 否则不会触发onPlayerStateChanged()
        if (TextUtils.equals(pageListPlay.playUrl, mVideoUrl)) {
            onPlayerStateChanged(true, Player.STATE_READY);
        } else {
            MediaSource mediaSource = PageListPlayManager.createMediaSource(mVideoUrl);
            exoPlayer.prepare(mediaSource);
            //循环播放模式
            exoPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
             //开启新的视频播放后把视频的url保存到 PageListPlay 对象里;
            //用来判断 exoPlayer 之前播放的url和即将要播放的url是否是同一个媒体资源,
            //如果是同一个只需要恢复继续播放即可,反正创建新的 MediaSource 给 exoPlayer 去播放
            pageListPlay.playUrl = mVideoUrl;
        }
        controlView.show();
        controlView.addVisibilityListener(this);
        exoPlayer.addListener(this);
        //视频缓冲好后,立马播放
        exoPlayer.setPlayWhenReady(true);
    }

    /**
     * 非活跃状态,暂停它(列表滚出屏幕时回调,恢复状态停止播放)
     */
    @Override
    public void inActive() {
        //暂停视频的播放并让封面图和 开始播放按钮 显示出来
        PageListPlay pageListPlay = PageListPlayManager.get(mCategory);
        if (pageListPlay.controlView == null || pageListPlay.exoPlayer == null) {
            return;
        }
        //暂停视频播放
        pageListPlay.exoPlayer.setPlayWhenReady(false);
        pageListPlay.controlView.removeVisibilityListener(this);
        pageListPlay.exoPlayer.removeListener(this);
        cover.setVisibility(VISIBLE);
        playBtn.setVisibility(VISIBLE);
        playBtn.setImageResource(R.drawable.icon_video_play);
    }

    /**
     * 当前 PlayTarget 是否在播放,帮助我们完成自动播放检测逻辑
     */
    @Override
    public boolean isPlaying() {
        return isPlaying;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //点击该区域时 我们诸主动让视频控制器显示出来
        PageListPlay pageListPlay = PageListPlayManager.get(mCategory);
        pageListPlay.controlView.show();
        return true;
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        isPlaying = false;
        bufferView.setVisibility(GONE);
        cover.setVisibility(VISIBLE);
        playBtn.setVisibility(VISIBLE);
        playBtn.setImageResource(R.drawable.icon_video_play);
    }

    /**
     * 获取视频播放控制器
     */
    public View getPlayController() {
        PageListPlay listPlay = PageListPlayManager.get(mCategory);
        return listPlay.controlView;
    }
}

ListPlayerView 的具体使用

1、xml 布局

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="feed"
            type="com.wuc.jetpackppjoke.model.Feed" />

    </data>

    <androidx.appcompat.widget.LinearLayoutCompat
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/color_white"
        android:orientation="vertical"
        android:paddingTop="@dimen/dp_10">

        <include
            layout="@layout/layout_feed_author"
            app:user="@{feed.author}" />

        <include
            layout="@layout/layout_feed_text"
            app:feedText="@{feed.feeds_text}"
            app:lines="@{3}" />

        <!--   视频区域-->
        <com.wuc.jetpackppjoke.view.ListPlayerView
            android:id="@+id/list_player_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="@dimen/dp_10" />

        <include
            layout="@layout/layout_feed_tag"
            app:tagText="@{feed.activityText}" />

        <include
            layout="@layout/layout_feed_top_comment"
            app:comment="@{feed.topComment}" />

        <include
            android:id="@+id/interaction_binding"
            layout="@layout/layout_feed_interaction"
            app:feed="@{feed}" />
    </androidx.appcompat.widget.LinearLayoutCompat>
</layout>

2、adapter 适配器中的使用
关键代码

LayoutFeedTypeVideoBinding videoBinding = (LayoutFeedTypeVideoBinding) mBinding;
videoBinding.listPlayerView.bindData(mCategory, item.width, item.height, item.cover, item.url);
listPlayerView = videoBinding.listPlayerView;

public boolean isVideoItem() {
   return mBinding instanceof LayoutFeedTypeVideoBinding;
}

public ListPlayerView getListPlayerView() {
   return listPlayerView;
}

3、在 Fragment 中使用

 private PageListPlayDetector playDetector;
 private boolean shouldPause = true;

 @Override
 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
     super.onViewCreated(view, savedInstanceState);
     playDetector = new PageListPlayDetector(this, mRecyclerView);
 }

new FeedAdapter(getContext(), feedType){
    @Override
    public void onViewAttachedToWindow(@NonNull ViewHolder holder) {
       super.onViewAttachedToWindow(holder);
        if (holder.isVideoItem()){
            playDetector.addTarget(holder.getListPlayerView());
        }
    }

    @Override
    public void onViewDetachedFromWindow(@NonNull ViewHolder holder) {
        super.onViewDetachedFromWindow(holder);
        playDetector.removeTarget(holder.getListPlayerView());
    }
};

 @Override
    public void onResume() {
        super.onResume();
        playDetector.onResume();
        /*shouldPause = true;
        //由于沙发Tab的几个子页面 复用了HomeFragment。
        //我们需要判断下 当前页面 它是否有ParentFragment.
        //当且仅当 它和它的ParentFragment均可见的时候,才能恢复视频播放
        if (getParentFragment() != null) {
            if (getParentFragment().isVisible() && isVisible()) {
                playDetector.onResume();
            }
        } else {
            if (isVisible()) {
                playDetector.onResume();
            }
        }*/
    }

    @Override
    public void onPause() {
        //如果是跳转到详情页,咱们就不需要 暂停视频播放了
        //如果是前后台切换 或者去别的页面了 都是需要暂停视频播放的
        if (shouldPause) {
            playDetector.onPause();
        }
        super.onPause();
    }

    @Override
    public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        if (hidden) {
            playDetector.onPause();
        } else {
            playDetector.onResume();
        }
    }

    @Override
    public void onDestroy() {
        //记得销毁
        PageListPlayManager.release(feedType);
        super.onDestroy();
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339