基于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系统的日益成熟,视频流的播放难点会由播放器本身转移到网络、服务器能力。