饺子播放器的自定义(弹幕)
自定义包含弹幕的播放器准备工作
- 需要的开源库
- 引入饺子播放器后,编写类继承饺子播放器的JzvdStd
class MyJzvd extends JzvdStd
实现该有的构造方法,并重写父类getLayoutId方法。换上自己的自定义布局。但是对控件id有一定的要求,如果是以lib形式引入饺子播放器则可以自行修改JzvdStd。布局参照官方给出的demo里的播放器布局。
/**
*重写父类Jzstd的getLayout方法
*/
@Override
public int getLayoutId() {
return R.layout.player_test;
}
关于play_test布局,嫌麻烦的话可以直接前往饺子demo里的布局文件查找。这里贴上自己的布局。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:descendantFocusability="afterDescendants">
<!-- 这里可以直接include官方的library里的jz_layout_std布局,也可以直接copy到自己的工程目录下-->
<include layout="@layout/jz_layout_std"/>
<!-- 烈焰弹幕使 -->
<master.flame.danmaku.ui.widget.DanmakuView
android:layout_width="match_parent"
android:id="@+id/danmuview"
android:layout_marginTop="15dp"
android:layout_height="match_parent"/>
</RelativeLayout>
在自定义的MyJzvd里进行弹幕控件绑定
/**
*重写父类的init方法,父类也是在进行播放器控件绑定
*我们新添了播放器控件弹幕
*/
private DanmakuView danmakuView;
@Override
public void init(Context context) {
super.init(context);
playerOpreationEvent = new PlayerOpreationEvent();
gson = new Gson();
mDanmakuContext = DanmakuContext.create();
danmakuView = findViewById(R.id.danmuview);
}
准备工作告一段落
利用EventBus对播放状态进行监听
使用过饺子播放器的朋友都知道,饺子播放器是没有任何listener的。它没有将其暴露出来,当然前提是我们使用默认播放器引擎的情况下。
- jzvd能够使用的播放器引擎
ExoPlayer
ijkPlayer
原生Media
- 饺子自定义播放器引擎的方法。
这里我不一一赘述,直接以demo里的CuoustomMedia里作为参照,这里我选择了JZMediaExo,实现了jzvd规定的JZMediaInterface里的方法,然后通过反射出获取实例。jzvd只需要关心调用JZMediaInterface里的方法和变量就行了。当然你也可以自己来实现。
定义Event实体,包含了视频播放的几种状态以及要处理的数据属性
/**
* 播放行为事件
*/
public class PlayerOpreationEvent
{
//暂停
public static final int PAUSE=0x00ff;
//恢复播放
public static final int PLAY=0x00dd;
//改变播放地址
public static final int ONCHANGEURL=0x00ffe;
//停止播放
public static final int STOP=0x00ffd;
//拖动Seekbar跳转
public static final int SEEKTO=0x00ffa;
//弹幕加载完成
public static final int DANMUCOMPLETE=0x00ffaaa;
//准备播放(如果是网络视频就是缓冲完毕准备播放,会触发多次)
public static final int READY_TO_PALY=0x00ffaa;
//播放结束
public static final int END=0x00fffaaa;
//跳转的时间
private long seekTime;
//自行拓展
private int type;
//视频总时长
private long allTime;
//get&set
}
然后对demo里的JZMediaExo类进行在对应回调里的sendEvent
/**
* 播放器的一些处理逻辑我给删掉了,方便演示如何监听视频的情况
* 这里已经很明了了,因为这个类已经实现了ExoPlayer视频相关的
* 监听器,我们只需要关心怎么把对应的状态通知给要操作的ui线程
* 这里我使用的EventBus事件通讯框架
*/
public class JZMediaExo extends JZMediaInterface implements Player.EventListener, VideoListener {
private SimpleExoPlayer simpleExoPlayer;
private Runnable callback;
private String TAG = "JZMediaExo";
private long previousSeek = 0;
private boolean isinitpaly = false;
//我们自定义好的事件
private PlayerOpreationEvent playerOpreationEvent;
public JZMediaExo(Jzvd jzvd) {
super(jzvd);
//在构造方法里面初始化
playerOpreationEvent = new PlayerOpreationEvent();
}
@Override
public void start() {
//对于Exoplayer来说。这里则是用户恢复点击播放的回调(不是自动播放)
//设置事件
playerOpreationEvent.setType(PlayerOpreationEvent.PLAY);
//获取当前暂停的时间
playerOpreationEvent.setSeekTime(simpleExoPlayer.getContentPosition());
sendEvent(playerOpreationEvent);
simpleExoPlayer.setPlayWhenReady(true);
}
@Override
public void prepare() {
//这里是处理url和准备播放源的地方(JZMediaInterface里的方法和我们需要的无关)
}
@Override
public void onRenderedFirstFrame() {
Log.e(TAG, "onRenderedFirstFrame");
}
@Override
public void pause() {
//这里是暂停
playerOpreationEvent.setType(PlayerOpreationEvent.PAUSE);
//获取当前暂停的时间
playerOpreationEvent.setSeekTime(simpleExoPlayer.getContentPosition());
sendEvent(playerOpreationEvent);
simpleExoPlayer.setPlayWhenReady(false);
}
@Override
public boolean isPlaying() {
return simpleExoPlayer.getPlayWhenReady();
}
@Override
public void seekTo(long time) {
//跳转
if (time != previousSeek) {
simpleExoPlayer.seekTo(time);
playerOpreationEvent.setType(PlayerOpreationEvent.SEEKTO);
playerOpreationEvent.setSeekTime(time);
sendEvent(playerOpreationEvent);
previousSeek = time;
jzvd.seekToInAdvance = time;
}
}
@Override
public void release() {
playerOpreationEvent.setType(PlayerOpreationEvent.STOP);
playerOpreationEvent.setSeekTime(simplayer.getCurrentPostion());
sendEvent(playerOpreationEvent);
}
@Override
public long getCurrentPosition() {
if (simpleExoPlayer != null)
return simpleExoPlayer.getCurrentPosition();
else return 0;
}
/**
* 播放器状态
*/
@Override
public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
switch (playbackState) {
case Player.STATE_IDLE: {
//播放器挂起的操作
}
break;
case Player.STATE_BUFFERING: {
Log.e("Exo播放器","缓冲中");
}
break;
case Player.STATE_READY: {
//视频准备播放在这里
//这里是相对于缓冲完成的准备完成
//所以我们要判断是否是第一次缓冲完成
if (playWhenReady) {
Log.e("Exo播放器","缓冲完成-播放");
//这个状态需要指定
//因为这里是能够缓冲到可以播放了,所以暂停过后再播放也会走这里
} else {
//暂停也会走这里,因为simplayer里的setPlayWenRead为false就是暂停。
//需要在parpre方法里设置playWenReady为false,不设置自动播放
//在缓冲完毕后在通知界面进行布局
if (!isinitpaly)
{
Log.e("Exo播放器","第一次缓冲完成,初次播放");
simpleExoPlayer.setPlayWhenReady(true);
playerOpreationEvent.setType(PlayerOpreationEvent.READY_TO_PALY);
playerOpreationEvent.setAllTime(simpleExoPlayer.getDuration());
sendEvent(playerOpreationEvent);
//更改判断标签
isinitpaly = true;
}
}
}
break;
case Player.STATE_ENDED: {
Log.e("Exo播放器","播放结束");
//自行添加对应的事件
}
break;
}
}
/**
* 发送事件到主页面
* @param event
*/
private void sendEvent(PlayerOpreationEvent event)
{
EventBus.getDefault().post(event);
}
//暂时无关的方法已省略,因为如果混在demo里的话,代码就太多了。
}
再回到对应的视频界面的Fragment或Activity
/**
* 接收到当前播放状态
* @param event
*/
@Subscribe(threadMode = ThreadMode.MAIN)
public void playState(PlayerOpreationEvent event)
{
switch (event.getType())
{
case PlayerOpreationEvent.READY_TO_PALY:
//视频缓冲可以播放了
//处理弹幕
Log.e("播放","缓冲完成");
jzVideo.playDanmu();
break;
case PlayerOpreationEvent.PAUSE:
//暂停播放了
long nowplaytime = event.getSeekTime();
Log.e("当前暂停播放时间",nowplaytime+"");
jzVideo.onPauseDanmu();
break;
case PlayerOpreationEvent.PLAY:
//恢复播放
jzVideo.setDanmuPostion(event.getSeekTime());
break;
case PlayerOpreationEvent.SEEKTO:
//跳转(对应用户拖动进度条或者记忆播放)
Log.e("当前播放跳转时间",event.getSeekTime()+"");
jzVideo.setDanmuPostion(event.getSeekTime());
break;
case PlayerOpreationEvent.END:
Log.e("当前播放已完成",event.getSeekTime()+"");
break;
case PlayerOpreationEvent.STOP:
Log.e("当前播放停止","");
break;
//该事件是MyJzvd发起的。
case
PlayerOpreationEvent.DANMUCOMPLETE:
Log.e("弹幕引擎加载完毕","开始加载视频");
//videoUtils.initMessage("弹幕引擎加载完毕");
danmu_enable = true;
break;
}
}
处理网络弹幕数据
- 烈焰弹幕使
网络上关于烈焰弹幕使的使用方法大部分都是去处理b站弹幕和acfun里的弹幕,但是自定义处理弹幕的文章少之又少。并且烈焰弹幕使默认处理弹幕是通过文件流的形式。增加了不少业务逻辑处理。
烈焰弹幕使的初始化
初始化在我们自定义继承的jzvd类,MyJzvd。关于烈焰弹幕使的基础初始化可以参照这里,这是最简洁明了的初始化。代码也不多。
烈焰弹幕使自定义数据处理
我们重点关心的是BaseDanmakuParser,ILoader,IDataSource<?>这三个类。
- BaseDanmakuParser
所有的弹幕解析都要继承该类进行修改,比如它自带的AcfunDanmakuParser,BilbilDanmaKuParser,此类是专门处理弹幕数据的,我们这里采用直接将json序列化实体数据来解析。
public class MyDanmakuParser extends BaseDanmakuParser {
private Gson gson;
@Override
public Danmakus parse() {
gson = new Gson();
try {
if (mDataSource != null) {
//使用reader来处理更长的字符串
//其实这里也可直接用Gson
StringReader reader = (StringReader) mDataSource.data();
BufferedReader in = new BufferedReader(reader);
StringBuffer buffer = new StringBuffer();
String line = " ";
while ((line = in.readLine()) != null){
buffer.append(line);
}
String jsondate = buffer.toString();
List<DanmuBean> danmuBeanList = gson.fromJson(jsondate,new TypeToken<List<DanmuBean>>(){}.getType());
return doParse(danmuBeanList);
}
}catch (Exception e)
{
e.printStackTrace();
}
return new Danmakus();
}
/**
* @param danmakuListData 弹幕数据
* @return 转换后的Danmakus
*/
private Danmakus doParse(List<DanmuBean> danmakuListData) {
Danmakus danmakus = new Danmakus();
if (danmakuListData == null || danmakuListData.size() == 0) {
return danmakus;
}
danmakus = _parse(danmakuListData, danmakus);
return danmakus;
}
private Danmakus _parse(List<DanmuBean> danmuBeanList, Danmakus danmakus) {
if (danmakus == null) {
danmakus = new Danmakus();
}
if (danmuBeanList == null || danmuBeanList.size() == 0) {
return danmakus;
}
for (int i=0;i<danmuBeanList.size();i++)
{
long showtime = danmuBeanList.get(i).getShowTime();
String content = danmuBeanList.get(i).getContent();
BaseDanmaku item = mContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL, mContext);
if (item != null) {
item.setTime(showtime);
item.textSize = 16 * (mDispDensity - 0.6f);
item.textColor = Color.WHITE;
item.index = i;
item.flags = mContext.mGlobalFlagValues;
item.setTimer(mTimer);
item.text = content;
danmakus.addItem(item);
}
}
return danmakus;
}
}
- IDataSource<?>
一个泛型接口,用于让Parser处理的规范。我们这里要讲拿到的json数据转为reader。比如创建JsonDataSource实现IDataSource接口
/**
* 弹幕数据可能较多,为避免String溢出,使用reader代替String
* Gson可以直接使用reader,而且Reader方便在String和InputStream之间转换
*/
public class JsonSource implements IDataSource<Reader> {
Reader reader;
public JsonSource(Reader reader) {
this.reader = reader;
}
@Override
public Reader data() {
return reader;
}
@Override
public void release() {
reader = null;
}
}
- ILoader
这个接口定义了如何处理数据的规范。它原来里面就只有对流和url的处理,再无其他,我们要添加一个自己的解析函数。比如创建DanmuJsonLoader实现ILoader接口,然后重载一个load方法。
public class DanmuJsonLoader implements ILoader {
private static volatile DanmuJsonLoader instance;
private JsonDataSource source;
public static ILoader instance() {
if(instance == null){
synchronized (DanmuJsonLoader.class){
if(instance == null)
instance = new DanmuJsonLoader();
}
}
return instance;
}
@Override
public JsonDataSource getDataSource() {
return source;
}
@Override
public void load(InputStream in) throws IllegalDataException {
InputStreamReader reader = new InputStreamReader(in);
load(reader);
}
/**
* 重载的方法在这里
* 定义加载json
* @param json
*/
public void load(String json) {
try {
Reader reader = new StringReader(json);
load(reader);
} catch (IllegalDataException e) {
e.printStackTrace();
}
}
public void load(Reader reader) throws IllegalDataException {
source = new JsonDataSource(reader);
}
}
- 弹幕数据模型
/**
* 当前弹幕信息
*/
public class DanmuBean
{
//控制弹幕显示的(暂时未用)
int type ;
long showTime;
//当前秒数
private double seconds;
//内容
private String content;
//get&set
}
- 创建Parser
/**
* 组合加载好的数据
* @param json
* @return
*/
private BaseDanmakuParser createParser(String json) {
ILoader loader = DanmuJsonLoader.instance();
try {
loader.load(json);
} catch (IllegalDataException e) {
e.printStackTrace();
}
MyDanmakuParser parser = new MyDanmakuParser();
IDataSource<?> dataSource = loader.getDataSource();
parser.load(dataSource);
return parser;
}
弹幕加载时机
结合之前的播放器状态回调,弹幕可以在播放器初始化完成第一次播放的时候进行绘制。
弹幕对应播放器的操作
播放器暂停时保存一下弹幕播放的位置。
public void onPauseDanmu()
{
if (danmakuView!=null)
{
pausetime = danmakuView.getCurrentTime();
danmakuView.pause();
}
}
恢复播放时
public void startDanmu()
{
if (danmakuView!=null)
{
danmakuView.start(pausetime);
}
}
也可以结合seekTo的事件设置弹幕跳转位置
public void setDanmuPostion(long time)
{
if (danmakuView!=null)
{
danmakuView.start(time);
}
}
除此之外,还可以调用它的show,hide来控制隐藏(不会暂停弹幕和清除弹幕),另外请与播放器的生命周期进行绑定。