播放器+弹幕自定义

饺子播放器的自定义(弹幕)

自定义包含弹幕的播放器准备工作
  • 需要的开源库

1.饺子视频播放器地址(主角)
2.烈焰弹幕使地址
3.eventBus地址

  • 引入饺子播放器后,编写类继承饺子播放器的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来控制隐藏(不会暂停弹幕和清除弹幕),另外请与播放器的生命周期进行绑定。

效果演示

弹幕演示

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值