弹幕使用场景
- 直播(
实时性
)
弹幕是直播系统的核心功能之一。一段摘抄的直播弹幕描述(貌似美拍工程师写的)
- 美拍直播弹幕系统从 2015 年 11 月到现在,经过了三个阶段的演进,目前能支撑百万用户同时在线
- 前中期使用 HTTP 轮询方案,中后期替换为长连接方案
- 直播间消息,相对于 IM 的场景,有如下几个特点:
- 消息要求及时,过时的消息对于用户来说不重要
- 松散的群聊,用户随时进群,随时退群
- 用户进群后,离线期间(接听电话)的消息不需要重发
- 视频(
实时性
+弹幕跟帧时间点关联
)
弹幕引擎(B站开源弹幕
)
GitHub:https://github.com/bilibili/DanmakuFlameMaster
DanmakuFlameMaster 是 Android 上开源弹幕解析绘制引擎项目,貌似Android 上最好的开源弹幕引擎·烈焰弹幕
DanmakuFlameMaster
开发包已被包括优酷土豆、开迅视频、MissEvan、echo回声、斗鱼TV、天天动听、被窝声次元、ACFUN 等 APP 使用
DanmakuFlameMaster 特点
- 使用多种方式(View/SurfaceView/TextureView)实现高效绘制
- B站xml弹幕格式解析
- 基础弹幕精确还原绘制
- 支持
mode7
特殊弹幕 - 多核机型优化,高效的预缓存机制
- 支持多种显示效果选项实时切换
- 实时弹幕显示支持
- 换行弹幕支持/运动弹幕支持
- 支持自定义字体
- 支持多种弹幕参数设置
- 支持多种方式的弹幕屏蔽
DanmakuFlameMaster 细节API
- 模式在
BaseDanmaku
里有声明,总结一下就是
public final static int TYPE_SCROLL_RL = 1; // 水平右往左
public final static int TYPE_SCROLL_LR = 6; // 水平左往右
public final static int TYPE_FIX_TOP = 5; // 固定在顶部
public final static int TYPE_FIX_BOTTOM = 4; // 固定在底部
public final static int TYPE_SPECIAL = 7; // 特殊弹幕
-
添加一条图文混合弹幕
请参考:DanmuKuDemo -
展示隐藏弹幕
// 实现了IDanmakuView接口的弹幕View,调用下面2个方法展示或隐藏弹幕
void show();
void hide();
- 如何监听每一个弹幕的移动的x,y位置
查看一下BaseDanmaku.layout
方法的实现就明白了,弹幕库每次绘制前
都会调用这个方法
集成B站弹幕
- 集成配置
repositories {
jcenter()
}
// 如果你不需要兼容x86和armv5就不用添加最下面两行的内容了
dependencies {
api 'com.github.ctiao:DanmakuFlameMaster:0.9.25'
api 'com.github.ctiao:ndkbitmap-armv7a:0.9.21'
// Other ABIs: optional 这个是适配多种架构的,如果你用虚拟机建议加上
api 'com.github.ctiao:ndkbitmap-armv5:0.9.21'
api 'com.github.ctiao:ndkbitmap-x86:0.9.21'
}
DanmakuFlameMaster
使用多种方式(View
/SurfaceView
/TextureView
)实现高效绘制!其中分别对应(DanmakuView/DanmakuSurfaceView/DanmakuTextureView)
<?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:orientation="vertical">
<master.flame.danmaku.ui.widget.DanmakuView
android:id="@+id/sv_danmaku"
android:layout_width="match_parent"
android:layout_height="300dp" />
</RelativeLayout>
- 代码配置
// 设置最大显示行数(滚动弹幕最大显示行数)
HashMap<Integer, Integer> maxLinesPair = new HashMap<>();
maxLinesPair.put(BaseDanmaku.TYPE_SCROLL_RL, 2);
// 设置是否禁止重叠
HashMap<Integer, Boolean> overlappingEnablePair = new HashMap<Integer, Boolean>();
overlappingEnablePair.put(BaseDanmaku.TYPE_SCROLL_RL, true);
overlappingEnablePair.put(BaseDanmaku.TYPE_FIX_TOP, true);
mContext = DanmakuContext.create();
mContext.setDanmakuStyle(IDisplayer.DANMAKU_STYLE_STROKEN, 3)
.setDuplicateMergingEnabled(false)
.setScrollSpeedFactor(1.2f)
// 设置文字的比例
.setScaleTextSize(1.2f)
// 图文混排使用 SpannedCacheStuffer
.setCacheStuffer(new SpannedCacheStuffer(), mCacheStufferAdapter)
// 绘制背景使用 BackgroundCacheStuffer
.setCacheStuffer(new BackgroundCacheStuffer())
.setMaximumLines(maxLinesPair)
.preventOverlapping(overlappingEnablePair).setDanmakuMargin(40);
if (mDanmakuView != null) {
mParser = createParser(null);
mDanmakuView.setCallback(new master.flame.danmaku.controller.DrawHandler.Callback() {
@Override
public void updateTimer(DanmakuTimer timer) {
}
@Override
public void drawingFinished() {
// 弹幕绘制完成时回掉(即弹幕展示完成移出屏幕时调用)
Log.i(TAG, "drawingFinished");
}
@Override
public void danmakuShown(BaseDanmaku danmaku) {
Log.i(TAG, "danmakuShown " + danmaku.text);
}
@Override
public void prepared() {
// 弹幕准备好的时候回掉,这里启动弹幕
Log.i(TAG, "prepared");
mDanmakuView.start();
}
});
mDanmakuView.setOnDanmakuClickListener(new IDanmakuView.OnDanmakuClickListener() {
@Override
public boolean onDanmakuClick(IDanmakus danmakus) {
Log.i(TAG, "onDanmakuClick danmakus size: " + danmakus.size());
BaseDanmaku latest = danmakus.last();
if (null != latest) {
Log.i(TAG, "onDanmakuClick text of latest danmaku: " + latest.text);
return true;
}
return false;
}
@Override
public boolean onDanmakuLongClick(IDanmakus danmakus) {
return false;
}
@Override
public boolean onViewClick(IDanmakuView view) {
return false;
}
});
mDanmakuView.prepare(mParser, mContext);
mDanmakuView.showFPS(true);
mDanmakuView.enableDanmakuDrawingCache(true);
}
- 直播页面按Home键退后台或重新回到前台,相关的生命周期方法必须设置
// View 实现 弹幕
private IDanmakuView mDanmakuView;
@Override
protected void onPause() {
super.onPause();
if (mDanmakuView != null && mDanmakuView.isPrepared()) {
mDanmakuView.pause();
}
}
@Override
protected void onResume() {
super.onResume();
if (mDanmakuView != null && mDanmakuView.isPrepared() && mDanmakuView.isPaused()) {
mDanmakuView.resume();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mDanmakuView != null) {
// dont forget release!
mDanmakuView.release();
mDanmakuView = null;
}
}
@Override
public void onBackPressed() {
super.onBackPressed();
if (mDanmakuView != null) {
// dont forget release!
mDanmakuView.release();
mDanmakuView = null;
}
}
- 添加一条普通文字弹幕
/**
* danmaku.isLive == true的情况下,请在非UI线程中使用此方法,避免可能卡住主线程
* @param item
*/
void addDanmaku(BaseDanmaku item);
private void addDanmaku(boolean isLive) {
// 创建一个弹幕对象,这里后面的属性是设置滚动方向的!
BaseDanmaku danmaku =
mContext.mDanmakuFactory.createDanmaku(BaseDanmaku.TYPE_SCROLL_RL);
if (danmaku == null || mDanmakuView == null) {
return;
}
danmaku.text = "这是一条弹幕";
// 设置相应的边距,这个设置的是四周的边距
danmaku.padding = 5;
// 可能会被各种过滤器过滤并隐藏显示,若是本机发送的弹幕,建议设置成1;
danmaku.priority = 0;
// 是否是直播弹幕
danmaku.isLive = isLive;
danmaku.setTime(mDanmakuView.getCurrentTime() + 1200);
danmaku.textSize = 25f * (mParser.getDisplayer().getDensity() - 0.6f);
// 设置文字颜色
danmaku.textColor = Color.RED;
// 设置阴影的颜色
danmaku.textShadowColor = Color.WHITE;
// danmaku.underlineColor = Color.GREEN;
danmaku.borderColor = Color.RED;
// 添加这条弹幕,也就相当于发送
mDanmakuView.addDanmaku(danmaku);
}