记录一下,万一以后用到了呢
1634524065373.gif
弹幕现在开源的,用到的,就是B站的
demo地址https://codechina.csdn.net/mirrors/bilibili/danmakuflamemaster/-/tree/master
引入
//弹幕
implementation 'com.github.ctiao:DanmakuFlameMaster:0.9.25'
implementation 'com.github.ctiao:ndkbitmap-armv7a:0.9.21'
implementation 'com.github.ctiao:ndkbitmap-armv5:0.9.21'
初始化
var maxLinesPair = HashMap<Int, Int>();
maxLinesPair[BaseDanmaku.TYPE_SCROLL_LR] = 3; // 滚动弹幕最大显示3行
var overlappingEnablePair = HashMap<Int, Boolean>();
overlappingEnablePair[BaseDanmaku.TYPE_SCROLL_LR] = true;
overlappingEnablePair[BaseDanmaku.TYPE_FIX_BOTTOM] = true;
danmakuContext.setDanmakuStyle(IDisplayer.DANMAKU_STYLE_STROKEN, 3f) //设置描边样式
.setDuplicateMergingEnabled(false)
.setScrollSpeedFactor(1.2f) //是否启用合并重复弹幕
.setScaleTextSize(1.2f) //设置弹幕滚动速度系数,只对滚动弹幕有效
.setMaximumLines(maxLinesPair) //设置最大显示行数
.setCacheStuffer(SpannedCacheStuffer(), mCacheStufferAdapter) // 图文混排
.preventOverlapping(overlappingEnablePair); //设置防弹幕重叠,null为允许重叠
值得多说的就是setCacheStuffer(SpannedCacheStuffer(), mCacheStufferAdapter),他是定义复杂弹幕显示用的。SpannableStringBuilder大家不会陌生,以前做图标与文本混排的时候用过,他的demo里面也是用的这个。
解析弹幕
private val danmakuContext by lazy {
DanmakuContext.create()
}
private val mParser: BaseDanmakuParser by lazy {
createParser(this.resources.openRawResource(R.raw.comments));
}
private fun createParser(stream: InputStream):BaseDanmakuParser {
if (stream == null) {
return object : BaseDanmakuParser() {
override fun parse(): Danmakus {
return Danmakus()
}
}
}
val loader = DanmakuLoaderFactory.create(DanmakuLoaderFactory.TAG_BILI)
try {
loader.load(stream)
} catch (e: IllegalDataException) {
e.printStackTrace()
}
val parser: BaseDanmakuParser = BiliDanmukuParser()
val dataSource = loader.dataSource
parser.load(dataSource)
return parser
}
/**
* 解析已有弹幕
*/
private fun getAllDanMuItem() {
if (danmakuView != null) {
danmakuView.setCallback(object :master.flame.danmaku.controller.DrawHandler.Callback{
override fun prepared() {
danmakuView.start()
}
override fun updateTimer(timer: DanmakuTimer?) {
timer?.update(player.currentPosition)
}
override fun danmakuShown(danmaku: BaseDanmaku?) {
}
override fun drawingFinished() {
}
});
danmakuView.prepare(mParser, danmakuContext);
danmakuView.showFPS(true); //是否显示FPS
danmakuView.enableDanmakuDrawingCache(true);
}
}
这里他的demo把原始数据放在了raw下采取xml的解析方式,注意这里有个知识点,B站采用的是xml,A站使用的是json,原因看DanmakuLoaderFactory.TAG_BILI代码,网上也能搜到答案,所以我们如果要用json格式请使用DanmakuLoaderFactory.TAG_ACFUN方式,有人会问,他的demo没有json格式的示例代码,其实他是有的,可以看他的0.6.0版本,他是把json文件放在assets下面的如图:
image.png
弹幕倍速在哪设置?请看timer?.update(player.currentPosition)代码。每次弹幕要跟新时调用updateTimer方法,所以在这里把弹幕和视频的位置统一,能够达到视频倍速情况下,也能同步。
好了,不想打字了,上完整代码
完整代码
activity
class DanMuActivity : AppCompatActivity() {
private var speed = 1.0f
private val player: SimpleExoPlayer by lazy {
ExoPlayerFactory.newSimpleInstance(
DefaultRenderersFactory(this),
DefaultTrackSelector(), DefaultLoadControl()
)
}
private val danmakuContext by lazy {
DanmakuContext.create()
}
private val mCacheStufferAdapter by lazy {
BaseCacheStufferImp(danmakuView)
}
private val mParser: BaseDanmakuParser by lazy {
createParser(this.resources.openRawResource(R.raw.comments));
}
private fun createParser(stream: InputStream):BaseDanmakuParser {
if (stream == null) {
return object : BaseDanmakuParser() {
override fun parse(): Danmakus {
return Danmakus()
}
}
}
val loader = DanmakuLoaderFactory.create(DanmakuLoaderFactory.TAG_BILI)
try {
loader.load(stream)
} catch (e: IllegalDataException) {
e.printStackTrace()
}
val parser: BaseDanmakuParser = BiliDanmukuParser()
val dataSource = loader.dataSource
parser.load(dataSource)
return parser
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_dan_mu)
initView()
initDanMu()
}
private fun initView() {
initializePlayer(Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera/11.mp4")
mBtnSpeed?.setOnClickListener {
if(speed == 1.0f){
speed += 0.5f
}else if(speed == 1.5f){
speed+= 0.5f
}else if(speed == 2.0f){
speed = 0.5f
}else if(speed == 0.5f){
speed += 0.5f
}else{
speed = 1.0f
}
mBtnSpeed?.text = "$speed+x"
if(player != null) {
var playbackParameters = PlaybackParameters(speed, 1.0f);
player.playbackParameters = playbackParameters;
}
}
}
private fun initializePlayer(path: String) {
mVideoView?.player = player
player.playWhenReady = true
//创建一个mp4媒体文件
val uri: Uri = Uri.parse(path)
val mediaSource: MediaSource = buildMediaSource(uri)
player.prepare(mediaSource, true, false)
player.addListener(object :PlayerEventListenerImp(){
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
if(playWhenReady){
//播放
if (danmakuView != null && danmakuView.isPrepared() && danmakuView.isPaused()) {
danmakuView?.resume()
}
}else{
danmakuView?.pause()
}
}
})
}
private fun buildMediaSource(uri: Uri): MediaSource {
var dataSourceFactory = DefaultDataSourceFactory(this, "com.example.demo");
var videoSource = ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
return videoSource
}
private fun initDanMu() {
var maxLinesPair = HashMap<Int, Int>();
maxLinesPair[BaseDanmaku.TYPE_SCROLL_LR] = 3; // 滚动弹幕最大显示3行
var overlappingEnablePair = HashMap<Int, Boolean>();
overlappingEnablePair[BaseDanmaku.TYPE_SCROLL_LR] = true;
overlappingEnablePair[BaseDanmaku.TYPE_FIX_BOTTOM] = true;
danmakuContext.setDanmakuStyle(IDisplayer.DANMAKU_STYLE_STROKEN, 3f) //设置描边样式
.setDuplicateMergingEnabled(false)
.setScrollSpeedFactor(1.2f) //是否启用合并重复弹幕
.setScaleTextSize(1.2f) //设置弹幕滚动速度系数,只对滚动弹幕有效
.setMaximumLines(maxLinesPair) //设置最大显示行数
.setCacheStuffer(SpannedCacheStuffer(), mCacheStufferAdapter) // 图文混排
.preventOverlapping(overlappingEnablePair); //设置防弹幕重叠,null为允许重叠
getAllDanMuItem()
}
/**
* 解析已有弹幕
*/
private fun getAllDanMuItem() {
if (danmakuView != null) {
danmakuView.setCallback(object :master.flame.danmaku.controller.DrawHandler.Callback{
override fun prepared() {
danmakuView.start()
}
override fun updateTimer(timer: DanmakuTimer?) {
timer?.update(player.currentPosition)
}
override fun danmakuShown(danmaku: BaseDanmaku?) {
}
override fun drawingFinished() {
}
});
danmakuView.prepare(mParser, danmakuContext);
danmakuView.showFPS(true); //是否显示FPS
danmakuView.enableDanmakuDrawingCache(true);
}
}
override fun onDestroy() {
super.onDestroy()
if (player != null) {
player.release();
}
danmakuView?.release()
}
override fun onPause() {
super.onPause()
if (player != null) {
player.playWhenReady = false;
}
}
}
布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".danMu.DanMuActivity">
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/mVideoView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"/>
<master.flame.danmaku.ui.widget.DanmakuView
android:id="@+id/danmakuView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<Button
android:id="@+id/mBtnSpeed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:text="1.0x"/>
</RelativeLayout>
BaseCacheStufferImp
public class BaseCacheStufferImp extends BaseCacheStuffer.Proxy {
private IDanmakuView mDanmakuView;
private Drawable mDrawable;
public BaseCacheStufferImp(IDanmakuView mDanmakuView){
this.mDanmakuView = mDanmakuView;
}
@Override
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
}
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;
}
}
BiliDanmukuParser
public class BiliDanmukuParser extends BaseDanmakuParser {
static {
System.setProperty("org.xml.sax.driver", "org.xmlpull.v1.sax2.Driver");
}
protected float mDispScaleX;
protected float mDispScaleY;
@Override
public Danmakus parse() {
if (mDataSource != null) {
AndroidFileSource source = (AndroidFileSource) mDataSource;
try {
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
XmlContentHandler contentHandler = new XmlContentHandler();
xmlReader.setContentHandler(contentHandler);
xmlReader.parse(new InputSource(source.data()));
return contentHandler.getResult();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
public class XmlContentHandler extends DefaultHandler {
private static final String TRUE_STRING = "true";
public Danmakus result;
public BaseDanmaku item = null;
public boolean completed = false;
public int index = 0;
public Danmakus getResult() {
return result;
}
@Override
public void startDocument() throws SAXException {
result = new Danmakus(ST_BY_TIME, false, mContext.getBaseComparator());
}
@Override
public void endDocument() throws SAXException {
completed = true;
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
String tagName = localName.length() != 0 ? localName : qName;
tagName = tagName.toLowerCase(Locale.getDefault()).trim();
if (tagName.equals("d")) {
// <d p="23.826000213623,1,25,16777215,1422201084,0,057075e9,757076900">我从未见过如此厚颜无耻之猴</d>
// 0:时间(弹幕出现时间)
// 1:类型(1从右至左滚动弹幕|6从左至右滚动弹幕|5顶端固定弹幕|4底端固定弹幕|7高级弹幕|8脚本弹幕)
// 2:字号
// 3:颜色
// 4:时间戳 ?
// 5:弹幕池id
// 6:用户hash
// 7:弹幕id
String pValue = attributes.getValue("p");
// parse p value to danmaku
String[] values = pValue.split(",");
if (values.length > 0) {
long time = (long) (parseFloat(values[0]) * 1000); // 出现时间
int type = parseInteger(values[1]); // 弹幕类型
float textSize = parseFloat(values[2]); // 字体大小
int color = (int) ((0x00000000ff000000 | parseLong(values[3])) & 0x00000000ffffffff); // 颜色
// int poolType = parseInteger(values[5]); // 弹幕池类型(忽略
item = mContext.mDanmakuFactory.createDanmaku(type, mContext);
if (item != null) {
item.setTime(time);
item.textSize = textSize * (mDispDensity - 0.6f);
item.textColor = color;
item.textShadowColor = color <= Color.BLACK ? Color.WHITE : Color.BLACK;
}
}
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if (item != null && item.text != null) {
if (item.duration != null) {
String tagName = localName.length() != 0 ? localName : qName;
if (tagName.equalsIgnoreCase("d")) {
item.setTimer(mTimer);
item.flags = mContext.mGlobalFlagValues;
Object lock = result.obtainSynchronizer();
synchronized (lock) {
result.addItem(item);
}
}
}
item = null;
}
}
@Override
public void characters(char[] ch, int start, int length) {
if (item != null) {
DanmakuUtils.fillText(item, decodeXmlString(new String(ch, start, length)));
item.index = index++;
// initial specail danmaku data
String text = String.valueOf(item.text).trim();
if (item.getType() == BaseDanmaku.TYPE_SPECIAL && text.startsWith("[")
&& text.endsWith("]")) {
//text = text.substring(1, text.length() - 1);
String[] textArr = null;//text.split(",", -1);
try {
JSONArray jsonArray = new JSONArray(text);
textArr = new String[jsonArray.length()];
for (int i = 0; i < textArr.length; i++) {
textArr[i] = jsonArray.getString(i);
}
} catch (JSONException e) {
e.printStackTrace();
}
if (textArr == null || textArr.length < 5 || TextUtils.isEmpty(textArr[4])) {
item = null;
return;
}
DanmakuUtils.fillText(item, textArr[4]);
float beginX = parseFloat(textArr[0]);
float beginY = parseFloat(textArr[1]);
float endX = beginX;
float endY = beginY;
String[] alphaArr = textArr[2].split("-");
int beginAlpha = (int) (AlphaValue.MAX * parseFloat(alphaArr[0]));
int endAlpha = beginAlpha;
if (alphaArr.length > 1) {
endAlpha = (int) (AlphaValue.MAX * parseFloat(alphaArr[1]));
}
long alphaDuraion = (long) (parseFloat(textArr[3]) * 1000);
long translationDuration = alphaDuraion;
long translationStartDelay = 0;
float rotateY = 0, rotateZ = 0;
if (textArr.length >= 7) {
rotateZ = parseFloat(textArr[5]);
rotateY = parseFloat(textArr[6]);
}
if (textArr.length >= 11) {
endX = parseFloat(textArr[7]);
endY = parseFloat(textArr[8]);
if (!"".equals(textArr[9])) {
translationDuration = parseInteger(textArr[9]);
}
if (!"".equals(textArr[10])) {
translationStartDelay = (long) (parseFloat(textArr[10]));
}
}
if (isPercentageNumber(textArr[0])) {
beginX *= DanmakuFactory.BILI_PLAYER_WIDTH;
}
if (isPercentageNumber(textArr[1])) {
beginY *= DanmakuFactory.BILI_PLAYER_HEIGHT;
}
if (textArr.length >= 8 && isPercentageNumber(textArr[7])) {
endX *= DanmakuFactory.BILI_PLAYER_WIDTH;
}
if (textArr.length >= 9 && isPercentageNumber(textArr[8])) {
endY *= DanmakuFactory.BILI_PLAYER_HEIGHT;
}
item.duration = new Duration(alphaDuraion);
item.rotationZ = rotateZ;
item.rotationY = rotateY;
mContext.mDanmakuFactory.fillTranslationData(item, beginX,
beginY, endX, endY, translationDuration, translationStartDelay, mDispScaleX, mDispScaleY);
mContext.mDanmakuFactory.fillAlphaData(item, beginAlpha, endAlpha, alphaDuraion);
if (textArr.length >= 12) {
// 是否有描边
if (!TextUtils.isEmpty(textArr[11]) && TRUE_STRING.equalsIgnoreCase(textArr[11])) {
item.textShadowColor = Color.TRANSPARENT;
}
}
if (textArr.length >= 13) {
//TODO 字体 textArr[12]
}
if (textArr.length >= 14) {
// Linear.easeIn or Quadratic.easeOut
((SpecialDanmaku) item).isQuadraticEaseOut = ("0".equals(textArr[13]));
}
if (textArr.length >= 15) {
// 路径数据
if (!"".equals(textArr[14])) {
String motionPathString = textArr[14].substring(1);
if (!TextUtils.isEmpty(motionPathString)) {
String[] pointStrArray = motionPathString.split("L");
if (pointStrArray.length > 0) {
float[][] points = new float[pointStrArray.length][2];
for (int i = 0; i < pointStrArray.length; i++) {
String[] pointArray = pointStrArray[i].split(",");
if (pointArray.length >= 2) {
points[i][0] = parseFloat(pointArray[0]);
points[i][1] = parseFloat(pointArray[1]);
}
}
mContext.mDanmakuFactory.fillLinePathData(item, points, mDispScaleX,
mDispScaleY);
}
}
}
}
}
}
}
private String decodeXmlString(String title) {
if (title.contains("&")) {
title = title.replace("&", "&");
}
if (title.contains(""")) {
title = title.replace(""", "\"");
}
if (title.contains(">")) {
title = title.replace(">", ">");
}
if (title.contains("<")) {
title = title.replace("<", "<");
}
return title;
}
}
private boolean isPercentageNumber(String number) {
//return number >= 0f && number <= 1f;
return number != null && number.contains(".");
}
private float parseFloat(String floatStr) {
try {
return Float.parseFloat(floatStr);
} catch (NumberFormatException e) {
return 0.0f;
}
}
private int parseInteger(String intStr) {
try {
return Integer.parseInt(intStr);
} catch (NumberFormatException e) {
return 0;
}
}
private long parseLong(String longStr) {
try {
return Long.parseLong(longStr);
} catch (NumberFormatException e) {
return 0;
}
}
@Override
public BaseDanmakuParser setDisplayer(IDisplayer disp) {
super.setDisplayer(disp);
mDispScaleX = mDispWidth / DanmakuFactory.BILI_PLAYER_WIDTH;
mDispScaleY = mDispHeight / DanmakuFactory.BILI_PLAYER_HEIGHT;
return this;
}
}
PlayerEventListenerImp
public class PlayerEventListenerImp implements Player.EventListener {
@Override
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
}
@Override
public void onLoadingChanged(boolean isLoading) {
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
}
@Override
public void onRepeatModeChanged(int repeatMode) {
}
@Override
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
}
@Override
public void onPlayerError(ExoPlaybackException error) {
}
@Override
public void onPositionDiscontinuity(int reason) {
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
}
@Override
public void onSeekProcessed() {
}
}
这里的视频播放器是exoplayer
//视频播放
implementation 'com.google.android.exoplayer:exoplayer:2.8.2'
implementation 'com.google.android.exoplayer:extension-rtmp:2.8.2'
视频为本地的,
Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera/11.mp4"
权限记得加上。
网友评论