基于ExoPlayer搭建Android网络电视应用

       在Android开发直播应用时,大家都会首先想到找一个开源的第三方播放器框架,只是这些开源框架的更新维护都几乎停滞了,为什么呢?Android已经发展了10多年了,官方的播放器已经非常强大,可以支持当前的主流播放需求。ExoPlayer就是Google推出的官方Android媒体播放组件,具体可以参考:https://developer.android.com/guide/topics/media/exoplayer,Demo源码在:https://github.com/google/ExoPlayer

       现在我们来基于ExoPlayer搭建一个Android网络电视应用:

1. 集成ExoPlayer:

Android集成步骤,参考:https://github.com/google/ExoPlayer

自定义播放器控件:

public class ExoPlayerLayout extends RelativeLayout {

    //播放器相关定义

    private PlayerView mPlayerView;

    private DataSource.Factory mDataSourceFactory;

    private DefaultBandwidthMeter mDefaultBandwidthMeter;

    private SimpleExoPlayer mPlayer;

    private MediaSource mMediaSource;

    protected String mUserAgent;

    private Cache mDownloadCache;

    private boolean mNewPlayFlag = false;

    private long mPlayerSuspendStart = 0; //卡顿开始时间点

    private static final String TAG = "ExoPlayerLayout";

    //节目列表相关定义

    private TVProgramBean mTVProgramBean = null;

    private String mTVProgramUrl = "";

    private Handler mEventHandler;

    public ExoPlayerLayout(Context context) {

        super(context);

    }

    public ExoPlayerLayout(Context context, AttributeSet attrs) {

        super(context, attrs);

    }

    public ExoPlayerLayout(Context context, AttributeSet attrs, int defStyleAttr) {

        super(context, attrs, defStyleAttr);

    }

    public boolean initView() {

        Log.i("ExoPlayerLayout",

                "initView:");

        //播放器控件初始化

        mPlayerView = new PlayerView(this.getContext());

        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(this.getWidth(), this.getHeight());

        mPlayerView.setControllerAutoShow(false);

        mPlayerView.setUseController(false);

        mPlayerView.setFocusable(false);

        this.addView(mPlayerView, RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);

        //事件处理Handler初始化

        mEventHandler = new Handler(Looper.getMainLooper());

        //数据初始化

        mUserAgent = Util.getUserAgent(this.getContext(), "LoveTV");

        mDataSourceFactory = buildDataSourceFactory();

        return true;

    }

    //开始节目播放

    public void startProgram(TVProgramBean tvProgramBean) {

        mNewPlayFlag = true;

        palyTV(tvProgramBean, "");

    }

    //播放节目

    private void palyTV(TVProgramBean tvProgramBean, String defalutTVProgramUrl) {

        mTVProgramBean = tvProgramBean;

        if (null != mPlayer) {

            if (null != mPlayerView) {

                mPlayerView.onPause();

                this.removeView(mPlayerView);

                mPlayerView = new PlayerView(this.getContext());

                RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(this.getWidth(), this.getHeight());

                mPlayerView.setControllerAutoShow(false);

                mPlayerView.setUseController(false);

                mPlayerView.setFocusable(false);

                this.addView(mPlayerView, RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);

            }

            mPlayer.release();

            mPlayer = null;

        }

        mPlayer =

                ExoPlayerFactory.newSimpleInstance(

              /* context= */ this.getContext());

        //设置播放器事件监听

        mPlayer.addListener(new PlayerEventListener());

        mPlayer.setPlayWhenReady(true);

        mPlayerView.setPlayer(mPlayer);

        mTVProgramUrl = defalutTVProgramUrl;

        if (!TextUtils.isEmpty(tvProgramBean.getSrcFHDUrl())) {

            mTVProgramUrl = tvProgramBean.getSrcFHDUrl();

        } else if (!TextUtils.isEmpty(tvProgramBean.getSrcHDUrl())) {

            mTVProgramUrl = tvProgramBean.getSrcHDUrl();

        } else if (!TextUtils.isEmpty(tvProgramBean.getSrcUrl())) {

            mTVProgramUrl = tvProgramBean.getSrcUrl();

        }

        mMediaSource = buildMediaSource(Uri.parse(mTVProgramUrl), null);

        mPlayer.prepare(mMediaSource, true, false);

        startLoading();

    }

    private MediaSource buildMediaSource(Uri uri) {

        return buildMediaSource(uri, null);

    }

    @SuppressWarnings("unchecked")

    private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {

        @C.ContentType int type = Util.inferContentType(uri, overrideExtension);

        List<StreamKey> keysList = new ArrayList<>();

        switch (type) {

            case C.TYPE_DASH:

                return new DashMediaSource.Factory(mDataSourceFactory)

                        .setManifestParser(

                                new FilteringManifestParser<>(new DashManifestParser(), keysList))

                        .createMediaSource(uri);

            case C.TYPE_SS:

                return new SsMediaSource.Factory(mDataSourceFactory)

                        .setManifestParser(

                                new FilteringManifestParser<>(new SsManifestParser(), keysList))

                        .createMediaSource(uri);

            case C.TYPE_HLS:

                return new HlsMediaSource.Factory(mDataSourceFactory)

                        .setPlaylistParserFactory(

                                new DefaultHlsPlaylistParserFactory(keysList))

                        .createMediaSource(uri);

            case C.TYPE_OTHER:

                return new ExtractorMediaSource.Factory(mDataSourceFactory).createMediaSource(uri);

            default: {

                throw new IllegalStateException("Unsupported type: " + type);

            }

        }

    }

    public void releaseProgram() {

        if (null != mPlayerView) {

            mPlayerView.onPause();

            this.removeView(mPlayerView);

        }

        if (mPlayer != null) {

            mPlayer.release();

            mPlayer = null;

            mMediaSource = null;

        }

    }

    /**

    * Returns a {@link DataSource.Factory}.

    */

    public DataSource.Factory buildDataSourceFactory() {

        //设置带宽监测

        mDefaultBandwidthMeter = new DefaultBandwidthMeter();

        DefaultDataSourceFactory upstreamFactory = new DefaultDataSourceFactory(this.getContext(),

                mUserAgent, mDefaultBandwidthMeter);

        return buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache());

    }

    private static CacheDataSourceFactory buildReadOnlyCacheDataSource(

            DefaultDataSourceFactory upstreamFactory, Cache cache) {

        return new CacheDataSourceFactory(

                cache,

                upstreamFactory,

                new FileDataSourceFactory(),

        /* cacheWriteDataSinkFactory= */ null,

                CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,

        /* eventListener= */ null);

    }

    private synchronized Cache getDownloadCache() {

        //设置下载缓存

        Log.i("ExoPlayerLayout",

                "getDownloadCache:" + mDownloadCache);

        if (mDownloadCache == null) {

            File downloadContentDirectory = new File(this.getContext().getFilesDir().toString()

                    + "/ExoPlayer/");

            mDownloadCache = new SimpleCache(downloadContentDirectory, new NoOpCacheEvictor());

        }

        return mDownloadCache;

    }

    private class PlayerEventListener implements Player.EventListener {

        @Override

        public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {

            switch (playbackState) {

                case Player.STATE_IDLE:

                case Player.STATE_BUFFERING:

                    startLoading();

                    break;

                case Player.STATE_READY:

                    finishLoading();

                    break;

                case Player.STATE_ENDED:

                    break;

            }

            Log.i("PlayerEventListener",

                    "onPlayerStateChanged:" + playWhenReady + "," + playbackState);

        }

        @Override

        public void onPlayerError(ExoPlaybackException error) {

            String errStr = error.getCause().getMessage();

            Log.d(TAG, mTVProgramUrl);

            Log.d(TAG, error.getLocalizedMessage());

        }

    }

    private void startLoading() {

        try {

            /*增加加載gif动画效果*/

        } catch (Throwable throwable) {

            throwable.printStackTrace();

        }

    }

    private void finishLoading() {

        try {

        } catch (Throwable throwable) {

            throwable.printStackTrace();

        }

    }

}

2. 接入网络电视直播源:

电视的节目源是.m3u8的HLS流,可以在网上找到,如:

"CCTV1-综合",

"http://223.110.245.159/ott.js.chinamobile.com/PLTV/3/224/3221225530/index.m3u8"

3. 监测直播流的下载速度:

网上有很多直播流的下载速率计算方法,但我们只要官方的方法即可:

DefaultBandwidthMeter:ExoPlayer的官方带宽统计类,我们只要调用mDefaultBandwidthMeter.getBitrateEstimate(),即可获取网络的下载速率。

4. 监测播放器的卡顿、视频源错误:

ExoPlayer的Player.EventListener,是官方的播放异常检测类,我们只要重载相关方法即可完成相关事件监测:

private class PlayerEventListener implements Player.EventListener {

        @Override

        public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {

            switch (playbackState) {

                case Player.STATE_IDLE:

                case Player.STATE_BUFFERING:

                    startLoading();

                    break;

                case Player.STATE_READY:

                    finishLoading();

                    break;

                case Player.STATE_ENDED:

                    break;

            }

            Log.i("PlayerEventListener",

                    "onPlayerStateChanged:" + playWhenReady + "," + playbackState);

        }

        @Override

        public void onPlayerError(ExoPlaybackException error) {

            String errStr = error.getCause().getMessage();

            Log.d(TAG, mTVProgramUrl);

            Log.d(TAG, error.getLocalizedMessage());

        }

    }

onPlayerStateChanged:可以用来检测播放器的卡顿问题,只有当播放器处于Player.STATE_READY状态,视频流才处于播放中; Player.STATE_IDLE/Player.STATE_BUFFERING状态,表示播放器处于等待状态。

onPlayerError:可以监测播放过程中的源错误,如:找不到源的404错误,源格式错误等。


总结:

1. ExoPlayer是个优秀的官方播放器方案,可以实现绝大部分的播放需求;

2. 基于系统级的播放器,可以大大减小第三方播放器的库文件大小,同时减小APK的包尺寸;

3. 随着Android系统的日益成熟,视频流的播放难点会由播放器本身转移到网络、服务器能力。

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

推荐阅读更多精彩内容