最强大的视频弹幕引擎——烈焰弹幕使(DanmakuFlameMaster)使用指南

姚实
2023-12-01

简介

B站在开源视频直播方面做出的贡献太大了,不仅开源视频控件ijkPlayer,还开源了弹幕引擎DanmakuFlameMaster,集齐整套,可谓神器在手,天下我有。

DanmakuFlameMaster 是 Android 上开源弹幕解析绘制引擎项目,也是 Android 上最好的开源弹幕引擎·烈焰弹幕。其架构清晰,简单易用,支持多种高效率绘制方式选择,支持多种自定义功能设置上。

目前,DanmakuFlameMaster 开发包已被包括优酷土豆、开迅视频、MissEvan、echo回声、斗鱼TV、天天动听、被窝声次元、ACFUN 等 APP 使用。

Features
使用多种方式(View/SurfaceView/TextureView)实现高效绘制

B站xml弹幕格式解析

基础弹幕精确还原绘制

支持mode7特殊弹幕

多核机型优化,高效的预缓存机制

支持多种显示效果选项实时切换

实时弹幕显示支持

换行弹幕支持/运动弹幕支持

支持自定义字体

支持多种弹幕参数设置

支持多种方式的弹幕屏蔽

TODO:
继续精确/稳定绘帧周期

增加OpenGL ES绘制方式

改进缓存策略和效率

github地址:
https://github.com/Bilibili/DanmakuFlameMaster

集成方法

使用Android Studio在gradle中集成

repositories {
    jcenter()
}

dependencies {
    compile 'com.github.ctiao:DanmakuFlameMaster:0.4.9'
}

Demo实例

我视频使用的是vitamio,弹幕用的是DanmakuFlameMaster,展示一下弹幕最简单的使用方法

布局文件

布局文件将vitamio和DanmakuFlameMaster包括进来就可以了:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <include layout="@layout/include_base_toolbar"/>


    <FrameLayout
        android:id="@+id/video_frame"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
       >
        <master.flame.danmaku.ui.widget.DanmakuView
            android:id="@+id/danmaku_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/black"
        android:orientation="vertical" >


        <io.vov.vitamio.widget.CenterLayout
            android:id="@+id/cl"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:background="@android:color/black" >

            <io.vov.vitamio.widget.VideoView
                android:id="@+id/video_view"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_centerHorizontal="true"
                android:layout_centerVertical="true" />
        </io.vov.vitamio.widget.CenterLayout>
    </LinearLayout>
    </FrameLayout>>

</LinearLayout>

Activity

这一部分做的工作主要是视频播放器的初始化以及弹幕引擎的初始化,大部分根据各个开源库的sample来整合,其中对vitamio开源库添加了一些自定义方法,烈焰弹幕使也提供了一些自定义方法,在这里还没有使用,待我研究透了再来分享。

package com.lsj.gankapp.ui.Activity;

import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import android.text.style.ImageSpan;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.Toast;

import com.google.android.exoplayer.util.MpegAudioHeader;
import com.lsj.gankapp.R;
import com.lsj.gankapp.app.ToolBarActivity;
import com.lsj.gankapp.presenter.LiveShowPresenter;
import com.lsj.gankapp.ui.View.ILiveShowView;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;

import butterknife.Bind;
import io.vov.vitamio.LibsChecker;
import io.vov.vitamio.MediaPlayer;
import io.vov.vitamio.widget.MediaController;
import io.vov.vitamio.widget.VideoView;
import master.flame.danmaku.controller.IDanmakuView;
import master.flame.danmaku.danmaku.loader.ILoader;
import master.flame.danmaku.danmaku.loader.IllegalDataException;
import master.flame.danmaku.danmaku.loader.android.DanmakuLoaderFactory;
import master.flame.danmaku.danmaku.model.BaseDanmaku;
import master.flame.danmaku.danmaku.model.DanmakuTimer;
import master.flame.danmaku.danmaku.model.IDisplayer;
import master.flame.danmaku.danmaku.model.android.BaseCacheStuffer;
import master.flame.danmaku.danmaku.model.android.DanmakuContext;
import master.flame.danmaku.danmaku.model.android.Danmakus;
import master.flame.danmaku.danmaku.model.android.SpannedCacheStuffer;
import master.flame.danmaku.danmaku.parser.BaseDanmakuParser;
import master.flame.danmaku.danmaku.parser.IDataSource;
import master.flame.danmaku.danmaku.parser.android.BiliDanmukuParser;
import master.flame.danmaku.danmaku.util.IOUtils;
import master.flame.danmaku.ui.widget.DanmakuView;

/**
 * Created by lsj on 2016/8/1.
 */
public class LiveShowActivity extends ToolBarActivity implements ILiveShowView {

    @Bind(R.id.video_view)
    VideoView mVideoView;
    @Bind(R.id.video_frame)
    FrameLayout mVideoFrame;
    @Bind(R.id.danmaku_view)
    DanmakuView mDanmakuView;
    private LiveShowPresenter mPresenter;
    private MediaController mMediaController;
    private BaseDanmakuParser mParser;
    private IDanmakuView IDanmakuView;
    private DanmakuContext mDanmakuContext;

    private HashMap<Integer, Integer> maxLinesPair;// 弹幕最大行数
    private HashMap<Integer, Boolean> overlappingEnablePair;// 设置是否重叠


    private static final String path = "http://www.modrails.com/videos/passenger_nginx.mov";

    @Override
    protected int getLayoutResId() {
        return R.layout.activity_live_show;
    }

    @Override
    protected void initPresenter() {

        mPresenter = new LiveShowPresenter(this, this);
        mPresenter.init();
        mPresenter.loadVideo();

        initDanmaku();
    }

    private void initDanmaku() {
        mDanmakuContext = DanmakuContext.create();

        // 设置最大行数,从右向左滚动(有其它方向可选)
        maxLinesPair=new HashMap<>();
        maxLinesPair.put(BaseDanmaku.TYPE_SCROLL_RL,3);

        // 设置是否禁止重叠
        overlappingEnablePair = new HashMap<>();
        overlappingEnablePair.put(BaseDanmaku.TYPE_SCROLL_LR, true);
        overlappingEnablePair.put(BaseDanmaku.TYPE_FIX_BOTTOM, true);



        mDanmakuContext.setDanmakuStyle(IDisplayer.DANMAKU_STYLE_STROKEN, 3) //设置描边样式
                .setDuplicateMergingEnabled(false)
                .setScrollSpeedFactor(1.2f) //是否启用合并重复弹幕
                .setScaleTextSize(1.2f) //设置弹幕滚动速度系数,只对滚动弹幕有效
                .setCacheStuffer(new SpannedCacheStuffer(), mCacheStufferAdapter) // 图文混排使用SpannedCacheStuffer  设置缓存绘制填充器,
                // 默认使用{@link SimpleTextCacheStuffer}只支持纯文字显示,
                // 如果需要图文混排请设置{@link SpannedCacheStuffer}
                // 如果需要定制其他样式请扩展{@link SimpleTextCacheStuffer}|{@link SpannedCacheStuffer}
                .setMaximumLines(maxLinesPair) //设置最大显示行数
                .preventOverlapping(overlappingEnablePair); //设置防弹幕重叠,null为允许重叠


        if (mDanmakuView != null) {
            mParser = createParser(this.getResources().openRawResource(R.raw.comments)); //创建解析器对象,从raw资源目录下解析comments.xml文本
            mDanmakuView.setCallback(new master.flame.danmaku.controller.DrawHandler.Callback() {
                @Override
                public void updateTimer(DanmakuTimer timer) {
                }

                @Override
                public void drawingFinished() {

                }

                @Override
                public void danmakuShown(BaseDanmaku danmaku) {

                }

                @Override
                public void prepared() {
                    mDanmakuView.start();
                }
            });

            mDanmakuView.prepare(mParser, mDanmakuContext);
            mDanmakuView.showFPS(false); //是否显示FPS
            mDanmakuView.enableDanmakuDrawingCache(true);

        }
    }

    @Override
    public void startLiveShow() {
        mVideoView.setVideoPath(path);
        // 特殊处理,重构MediaController的位置
        mMediaController = new MediaController(this, true, mVideoFrame);
        mVideoView.setMediaController(mMediaController);
        mMediaController.setVisibility(View.GONE);
        mVideoView.requestFocus();

        // 预处理监听
        mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
            @Override
            public void onPrepared(MediaPlayer mp) {
                mp.setPlaybackSpeed(1.0f);
            }
        });

        // 错误监听
        mVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
            @Override
            public boolean onError(MediaPlayer mp, int what, int extra) {
                return false;
            }
        });

        //addDanmaku(true);

    }


    @Override
    public void initView() {
        if (!LibsChecker.checkVitamioLibs(this)) {
            Toast.makeText(this, "找不到包", Toast.LENGTH_SHORT).show();
        }


    }


    /**
     * 添加文本弹幕
     * @param islive
     */
    private void addDanmaku(boolean islive) {
        BaseDanmaku danmaku = mDanmakuContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
        if (danmaku == null || mDanmakuView == null) {
            return;
        }

        danmaku.text = "这是一条弹幕" + System.nanoTime();
        danmaku.padding = 5;
        danmaku.priority = 0;  //0 表示可能会被各种过滤器过滤并隐藏显示 //1 表示一定会显示, 一般用于本机发送的弹幕
        danmaku.isLive = islive; //是否是直播弹幕
        danmaku.time = mDanmakuView.getCurrentTime() + 1200; //显示时间
        danmaku.textSize = 25f * (mParser.getDisplayer().getDensity() - 0.6f);
        danmaku.textColor = Color.RED;
        danmaku.textShadowColor = Color.WHITE; //阴影/描边颜色
        danmaku.borderColor = Color.GREEN; //边框颜色,0表示无边框
        mDanmakuView.addDanmaku(danmaku);

    }

    /**
     * 添加图文混排弹幕
     * @param islive
     */
    private void addDanmaKuShowTextAndImage(boolean islive) {
        BaseDanmaku danmaku = mDanmakuContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
        Drawable drawable = getResources().getDrawable(R.mipmap.ic_launcher);
        drawable.setBounds(0, 0, 100, 100);
        SpannableStringBuilder spannable = createSpannable(drawable);
        danmaku.text = spannable;
        danmaku.padding = 5;
        danmaku.priority = 1;  // 一定会显示, 一般用于本机发送的弹幕
        danmaku.isLive = islive;
        danmaku.time = mDanmakuView.getCurrentTime() + 1200;
        danmaku.textSize = 25f * (mParser.getDisplayer().getDensity() - 0.6f);
        danmaku.textColor = Color.RED;
        danmaku.textShadowColor = 0; // 重要:如果有图文混排,最好不要设置描边(设textShadowColor=0),否则会进行两次复杂的绘制导致运行效率降低
        danmaku.underlineColor = Color.GREEN;
        mDanmakuView.addDanmaku(danmaku);
    }

    /**
     * 创建图文混排模式
     * @param drawable
     * @return
     */
    private SpannableStringBuilder createSpannable(Drawable drawable) {
        String text = "bitmap";
        SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text);
        ImageSpan span = new ImageSpan(drawable);//ImageSpan.ALIGN_BOTTOM);
        spannableStringBuilder.setSpan(span, 0, text.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableStringBuilder.append("图文混排");
        spannableStringBuilder.setSpan(new BackgroundColorSpan(Color.parseColor("#8A2233B1")), 0, spannableStringBuilder.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
        return spannableStringBuilder;
    }


    private BaseCacheStuffer.Proxy mCacheStufferAdapter = new BaseCacheStuffer.Proxy() {

        private Drawable mDrawable;

        /**
         * 在弹幕显示前使用新的text,使用新的text
         * @param danmaku
         * @param fromWorkerThread 是否在工作(非UI)线程,在true的情况下可以做一些耗时操作(例如更新Span的drawblae或者其他IO操作)
         * @return 如果不需重置,直接返回danmaku.text
         */

        public void prepareDrawing(final BaseDanmaku danmaku, boolean fromWorkerThread) {
            if (danmaku.text instanceof Spanned) { // 根据你的条件检查是否需要需要更新弹幕
                // FIXME 这里只是简单启个线程来加载远程url图片,请使用你自己的异步线程池,最好加上你的缓存池
                new Thread() {

                    @Override
                    public void run() {
                        String url = "http://www.bilibili.com/favicon.ico";
                        InputStream inputStream = null;
                        Drawable drawable = mDrawable;
                        if (drawable == null) {
                            try {
                                URLConnection urlConnection = new URL(url).openConnection();
                                inputStream = urlConnection.getInputStream();
                                drawable = BitmapDrawable.createFromStream(inputStream, "bitmap");
                                mDrawable = drawable;
                            } catch (MalformedURLException e) {
                                e.printStackTrace();
                            } catch (IOException e) {
                                e.printStackTrace();
                            } finally {
                                IOUtils.closeQuietly(inputStream);
                            }
                        }
                        if (drawable != null) {
                            drawable.setBounds(0, 0, 100, 100);
                            SpannableStringBuilder spannable = createSpannable(drawable);
                            danmaku.text = spannable;
                            if (mDanmakuView != null) {
                                mDanmakuView.invalidateDanmaku(danmaku, false);
                            }
                            return;
                        }
                    }
                }.start();
            }
        }

        @Override
        public void releaseResource(BaseDanmaku danmaku) {
            // TODO 重要:清理含有ImageSpan的text中的一些占用内存的资源 例如drawable
        }
    };

    /**
     * 创建解析器对象,解析输入流
     * @param stream
     * @return
     */
    private BaseDanmakuParser createParser(InputStream stream) {

        if (stream == null) {
            return new BaseDanmakuParser() {

                @Override
                protected Danmakus parse() {
                    return new Danmakus();
                }
            };
        }

        // DanmakuLoaderFactory.create(DanmakuLoaderFactory.TAG_BILI) //xml解析
        // DanmakuLoaderFactory.create(DanmakuLoaderFactory.TAG_ACFUN) //json文件格式解析
        ILoader loader = DanmakuLoaderFactory.create(DanmakuLoaderFactory.TAG_BILI);

        try {
            loader.load(stream);
        } catch (IllegalDataException e) {
            e.printStackTrace();
        }
        BaseDanmakuParser parser = new BiliDanmukuParser();
        IDataSource<?> dataSource = loader.getDataSource();
        parser.load(dataSource);
        return parser;

    }

    @Override
    protected void onDestroy() {
        // 释放弹幕资源
        super.onDestroy();
        if (mDanmakuView != null) {
            // dont forget release!
            mDanmakuView.release();
            mDanmakuView = null;
        }
    }
}

另外,自定义弹幕背景样式的方法如下
>

    /**
     * 绘制背景(自定义弹幕样式)
     */
    private static class BackgroundCacheStuffer extends SpannedCacheStuffer {
        // 通过扩展SimpleTextCacheStuffer或SpannedCacheStuffer个性化你的弹幕样式
        final Paint paint = new Paint();

        @Override
        public void measure(BaseDanmaku danmaku, TextPaint paint, boolean fromWorkerThread) {
            danmaku.padding = 10;  // 在背景绘制模式下增加padding
            super.measure(danmaku, paint, fromWorkerThread);
        }

        @Override
        public void drawBackground(BaseDanmaku danmaku, Canvas canvas, float left, float top) {
            paint.setColor(0x8125309b);
            canvas.drawRect(left + 2, top + 2, left + danmaku.paintWidth - 2, top + danmaku.paintHeight - 2, paint);
        }

        @Override
        public void drawStroke(BaseDanmaku danmaku, String lineText, Canvas canvas, float left, float top, Paint paint) {
            // 禁用描边绘制
        }
    }

作品下载:
http://fir.im/zxam
源码地址:
我整理一下,刚在github上,马上奉上
https://github.com/JasonLinkinBright/GankApp

相关参考:
bilibili高并发实时弹幕系统的实战之路

 类似资料: