美文网首页
Android笔记(弹幕倍速)

Android笔记(弹幕倍速)

作者: 坑逼的严 | 来源:发表于2021-10-15 18:56 被阅读0次

    记录一下,万一以后用到了呢


    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("&amp;")) {
                    title = title.replace("&amp;", "&");
                }
                if (title.contains("&quot;")) {
                    title = title.replace("&quot;", "\"");
                }
                if (title.contains("&gt;")) {
                    title = title.replace("&gt;", ">");
                }
                if (title.contains("&lt;")) {
                    title = title.replace("&lt;", "<");
                }
                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"
    

    权限记得加上。

    相关文章

      网友评论

          本文标题:Android笔记(弹幕倍速)

          本文链接:https://www.haomeiwen.com/subject/alwmoltx.html