簡介
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,);
// 設定是否禁止重疊
overlappingEnablePair = new HashMap<>();
overlappingEnablePair.put(BaseDanmaku.TYPE_SCROLL_LR, true);
overlappingEnablePair.put(BaseDanmaku.TYPE_FIX_BOTTOM, true);
mDanmakuContext.setDanmakuStyle(IDisplayer.DANMAKU_STYLE_STROKEN, ) //設定描邊樣式
.setDuplicateMergingEnabled(false)
.setScrollSpeedFactor(f) //是否啟用合并重複彈幕
.setScaleTextSize(f) //設定彈幕滾動速度系數,隻對滾動彈幕有效
.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(f);
}
});
// 錯誤監聽
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 = ;
danmaku.priority = ; //0 表示可能會被各種過濾器過濾并隐藏顯示 //1 表示一定會顯示, 一般用于本機發送的彈幕
danmaku.isLive = islive; //是否是直播彈幕
danmaku.time = mDanmakuView.getCurrentTime() + ; //顯示時間
danmaku.textSize = f * (mParser.getDisplayer().getDensity() - f);
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(, , , );
SpannableStringBuilder spannable = createSpannable(drawable);
danmaku.text = spannable;
danmaku.padding = ;
danmaku.priority = ; // 一定會顯示, 一般用于本機發送的彈幕
danmaku.isLive = islive;
danmaku.time = mDanmakuView.getCurrentTime() + ;
danmaku.textSize = f * (mParser.getDisplayer().getDensity() - f);
danmaku.textColor = Color.RED;
danmaku.textShadowColor = ; // 重要:如果有圖文混排,最好不要設定描邊(設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, , text.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
spannableStringBuilder.append("圖文混排");
spannableStringBuilder.setSpan(new BackgroundColorSpan(Color.parseColor("#8A2233B1")), , 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(, , , );
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 = ; // 在背景繪制模式下增加padding
super.measure(danmaku, paint, fromWorkerThread);
}
@Override
public void drawBackground(BaseDanmaku danmaku, Canvas canvas, float left, float top) {
paint.setColor();
canvas.drawRect(left + , top + , left + danmaku.paintWidth - , top + danmaku.paintHeight - , 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高并發實時彈幕系統的實戰之路