iOS弹幕(源码)实现原理解析

By | 2015年3月30日

  HJDanmaku 2.0 重磅发布 ,欢迎升级体验!

  弹幕,国内流行于视频网站A站和B站。网上关于弹幕的实现方法有很多,目前Android平台已经有比较成熟的解决方案DanmakuFlameMaster 。而iOS平台尚无比较成熟的开源库,在借鉴DanmakuFlameMaster的实现思想后,特分享iOS平台弹幕解决方案HJDanmakuDemo。本文将介绍弹幕的大致实现原理。

看过DanmakuFlameMaster源码的朋友都知道,弹幕实现主要需要解决以下几个问题

  1. 弹幕绘制方式
  2. 弹幕时间控制
  3. 弹幕碰撞检测原理
  4. 弹幕暂停及恢复

本文主要从以上4个方面介绍弹幕的详细实现原理。首先是弹幕绘制方式,在DanmakuFlameMaster库中,它主要通过view的自定义draw一帧一帧的绘制来完成弹幕的显示,这种方式最大的问题在于性能以及动画的不流畅。弹幕流畅的前提要求每秒绘制的帧数在30帧以上,而移动设备性能千差万别,当同一时刻需要绘制大量弹幕的时候,对于低端设备就会出现卡帧不流畅的情况,这会大大降低用户的体验。因此,在本项目中放弃采用自定义绘制帧的方式,而是采用系统动画的方式来实现弹幕文本的滚动。

其次,就是弹幕时间的控制。由于采用系统动画的方式,所以不需要时刻计算每一个弹幕的显示时间以及其X坐标(假设弹幕横向滚动),我们需要做的就是在弹幕需要出现的时候创建它,然后设定弹幕存活的时间,剩余滚动动画交给系统负责,当然,弹幕剩余时间需要我们来更新。本项目中,创建了一个0.5s间隔的定时器,主要负责创建新的弹幕并更新已显示弹幕的剩余时间,也就是说0.5s执行一次计算,如果需要,可以将刷新间隔设置成5s或者更长。

然后,就是弹幕碰撞检测的问题。碰撞检测主要难点在于检测横向滚动弹幕之间的碰撞,弹幕存活时间由其显示时间和存活长短决定,因此,弹幕之间是否碰撞只需检测开始和消失是否碰撞即可。

最后,弹幕的暂停及恢复。由于弹幕滚动采用系统动画,所以在解决弹幕暂停前需要先了解系统动画的实现原理,有兴趣的朋友可以参考动画解释这篇文章,在此就不做过多介绍。暂停的基本原理就是通过view的presentationLayer获取对象的当前坐标并赋给其frame,然后移除layer动画,考虑到缓冲,可以在当前坐标基础上-1

有兴趣的童鞋可以下载HJDanmakuDemo查看具体使用方法,有什么疑问可以在后面留言。 如果你喜欢,希望能在github上为本demo点上一赞,感谢你的来访!

110 thoughts on “iOS弹幕(源码)实现原理解析

    1. 匿名

      这个缓冲可以去掉,缓冲的原因是动画系统与 Runloop 的脱离,因此将每个弹幕的 layer 计算拆开到其他 runloop 中,就不会有位置上的偏移了

      Reply
  1. 翟强

    尽管是有了另一番思路,但是自己知识储备量还是不够,具体实现还是不太明白,哎,惭愧

    Reply
    1. 庞海礁 Post author

      感谢您的来访,最近我会去掉这个网址广告!

      Reply
      1. 匿名

        问下,我给每条弹幕信息加了一个点击手势,点击没反应

        Reply
        1. 庞海礁 Post author

          很遗憾,考虑到通用性,目前danmukuview是忽略任何手势响应的

          Reply
  2. wangcheng

    你好, 首先非常感谢分享,好像不能动态改变文字大小, 屏蔽顶端或是底端弹幕

    Reply
    1. 庞海礁 Post author

      恩,这些需求目前都还不支持,以后会慢慢加入的,谢谢你的来访!

      Reply
  3. 谢东华

    你好..谢谢你的分享.. 我也是按照这种思路写的弹幕.. 但是有些问题.. 可以交流下么..QQ.304977710.

    Reply
  4. yake

    你好,现在的代码通过send发出来的弹幕还是会带红色的下划线啊

    Reply
    1. 庞海礁 Post author

      带红色下划线是为了强调该条记录为刚才所发!

      Reply
  5. 杜昕晓

    感谢分享,不过我这边项目也需要一个简单的弹幕,不过和ls一样需要动态改变字体大小,我感觉你吧中字体大字体那个参数直接改成字体大小就好啦。看来要自己写一个了T T

    Reply
      1. 杜昕晓

        只能选择大字体中字体的大小吧,但是每条弹幕没法自己设定大小

        Reply
        1. 庞海礁 Post author

          动态设置字体大小的确还不支持,只能提前在DanmakuConfiguration对象里设置好两种字体大小,然后在DanmakuSource里指定该条弹幕是中字体还是大字体!

          Reply
  6. 谢东华

    你好..我说说我的思路吧.
    1. 进入的时候创建一个数组存放30个Label的用于弹幕显示.
    2. 设定一个定时器 每隔一段时间取出一个弹幕数据与一个Label显示.
    [UIView animateWithDuration:animateTime delay:0 options:UIViewAnimationOptionCurveLinear animations:^(){
    lab.frame = CGRectMake(-self.view.frame.size.width, self.view.frame.origin.y, self.view.frame.size.width, DANMULABHEIGHT);
    } completion:^(BOOL finished){
    //进行弹幕Label的回收.
    .........
    }];
    在不与其它使用opengl模块的功能一起工作时跟你的CPU效率差不多.. 但是一起工作时比你的CPU占用高一些.

    Reply
    1. 庞海礁 Post author

      影响CPU性能除了定时器设定间隔,Label回收再利用还有过滤算法都有关系,至于你说的opengl,这个应该与动画渲染有关,不知道是不是由于定时器间隔过密导致的,还有过滤过程尽量在子线程完成!

      Reply
  7. 一代骑侠

    你好,我也在做视频直接+弹幕,非常感谢你分享出你的想法来。等我看了代码,如果有想法跟一交流的话再回来跟你交流。再次感谢。

    Reply
    1. 庞海礁 Post author

      感谢来访,有什么问题都可以在下面跟我留言!

      Reply
  8. 王义凯

    我想问一下,这个初始化的时候必须给一个xml的文件么?

    Reply
    1. 庞海礁 Post author

      你可以查看deme了解具体使用方法,初始化时是不需要使用xml文件的,你可以定义自己的格式,创建一个DanmakuSource数组传进来就可以了,当然也可以实时发送弹幕

      Reply
      1. 王义凯

        prepareDanmakus这个方法是初始化时必须执行的么,我这个数组是每隔5分钟获取一次的,所以初始化的时候没有数据,注掉demo里初始化数组那的代码,发现isprepare这个属性一直都是no.如何解决呢,求教大神….

        Reply
  9. 影孤清

    我要做的是实时弹幕,当socket接到一条的时候,就是显示出来,如果用sendDanmakuSource方法的话,就每条都有红线,怎么解决????谢谢.

    Reply
    1. 黄瓜

      在- (void)drawDanmakus:(NSArray *)danmakus time:(DanmakuTime *)time isBuffering:(BOOL)isBuffering 方法中,
      danmaku.remainTime = danmaku.time-time.time+danmaku.duration;
      创建弹幕的时候,为什么弹幕的剩余时间要设置成这样而不直接使用danmuku.duration ?

      Reply
  10. 影孤清

    你好,新版本中的isShowLineWhenSelf属性,怎么不放到DanmakuSource里.这样就可以控制每条要发的是否要显示是自己发的.

    Reply
    1. 庞海礁 Post author

      这个可以为DanmakuSource增加一个新的属性,而isShowLineWhenSelf类似于全局开关,好建议,下个版本可以考虑为DanmakuSource增加是否自己的属性

      Reply
    1. 庞海礁 Post author

      目前还没有时间表,为啥弹幕还要支持图片,这么复杂~

      Reply
  11. 匿名

    楼主,对于这样的好东西,还是分享分享,让大家都学习学习嘛

    Reply
  12. 匿名

    你好,谢谢你的分享,不过我发现在更新xcode7 之后,弹幕库会引起死机啊,请帮忙分析一下,谢谢。

    Reply
    1. 庞海礁 Post author

      行,我这边最近会使用xcode7重新编译,目前我这边使用xcode7编译的库不会出现卡死的问题,等更新后你再验证下

      Reply
  13. 匿名

    你好,改变DanmakuView的frame,弹幕出现的位置却没有变化,当需要改变frame时只能重新alloc一个吗?

    Reply
    1. 庞海礁 Post author

      你是怎么修改的,建议直接代码修改danmakuview的frame就可以了

      Reply
  14. 匿名

    你好,请问当需要改变DanmakuView的大小时只能重新alloc一个对象吗?

    Reply
  15. Pingback: iOS弹幕基本实现及原理介绍-IT大道

  16. 匿名

    新建一个和弹幕Lbl一样大小的View然后在View上面给弹幕String绘制一个空心描边然后加到原来的弹幕Lbl上面,就简单的实现的弹幕渲染,测试了B站几个爆炸弹幕的视频都无压力~不知道博主是不是这种思路

    Reply
  17. 吴风

    赶上好时候了 开源不说 还增加了这么多功能 非常感谢楼主的慷慨

    Reply
  18. 刘辉

    感谢博主分享的弹幕代码,我后来又给加上了字体描黑边效果,感觉弹幕效果更好了些

    Reply
    1. 庞海礁 Post author

      恩,开源后效果可以自定义了,多多来拜访

      Reply
      1. star

        你好,请问可以支持点击事件吗?有没有支持点击事件的方案?
        During an animation, user interactions are temporarily disabled for all views involved in the animation, regardless of the value in this property. You can disable this behavior by specifying the UIViewAnimationOptionAllowUserInteraction option when configuring the animation.

        设置UIViewAnimationOptionAllowUserInteraction后也不支持移动中点击弹幕,使用animation实现的弹幕是否可以支持点击事件?

        Reply
  19. 匿名

    你好,请问可以支持点击事件吗?有没有支持点击事件的方案?
    During an animation, user interactions are temporarily disabled for all views involved in the animation, regardless of the value in this property. You can disable this behavior by specifying the UIViewAnimationOptionAllowUserInteraction option when configuring the animation.

    设置UIViewAnimationOptionAllowUserInteraction后也不支持移动中点击弹幕,使用animation实现的弹幕是否可以支持点击事件?

    Reply
    1. 庞海礁 Post author

      是不是设置了正在缓冲状态?其次,播放器进度正确吗?

      Reply
  20. fero2004

    博主你好,我用了你的弹幕代码.发现发弹幕的时候不是立即发送的,你设置了弹幕出现时间要比当前发送时间延迟1秒
    就是这句代码 int time = ([self danmakuViewGetPlayTime:nil]+1)*1000;

    我想立即发送,把+1删除了,但是弹幕不显示了.我看了源码,弹幕其实是加到了数组里面的.
    然后 你的源码里 有这个判断
    – (NSArray *)filterDanmakus:(NSArray *)danmakus Time:(DanmakuTime *)time
    {
    if (danmakus.count<1) {
    return nil;
    }
    DanmakuBaseModel *lastDanmaku = danmakus.lastObject;
    if (![lastDanmaku isDraw:time.time]) {
    return nil;
    }
    DanmakuBaseModel *firstDanmaku = danmakus.firstObject;
    if ([firstDanmaku isDraw:time.time]) {
    return danmakus;
    }
    return [self cutWithDanmakus:danmakus Time:time];
    }
    那个isDraw方法里那个时间判断永远返回空了.
    不知道怎么改了,求助….

    Reply
    1. 庞海礁 Post author

      原机制是基于每个弹幕时间决定是否需要显示的,如果你把时间设置的跟当前时间一直,由于计算时间有误差,计算的时候就会被当做已经显示过,最简单的就是对于新发的弹幕增加一个属性,如果该属性为YES,就忽略时间检测,直接显示,然后再设为NO,通过时间戳的方式显示就OK了

      Reply
      1. fero2004

        我现在在DanmakuBaseModel里加了一个属性immediatelyShow来控制是否立即显示
        然后在
        – (BOOL)isDraw:(float)curTime
        { if(self.immediatelyShow == YES)
        {
        return YES;
        }
        return self.time>=curTime;
        }
        这个方法里加了这个判断,如果是立即显示,就不判断时间
        然后在
        – (void)drawDanmakus:(NSArray *)danmakus Time:(DanmakuTime *)time IsBuffering:(BOOL)isBuffering
        这个方法里将danmaku.immediatelyShow = NO; 设置为NO

        字幕倒是立即显示了,但是又出现一个问题.字幕在屏幕右边停顿了一小会然后再往左边移动.这是什么情况呢?

        Reply
  21. fero2004

    我发现另外一个问题,如果弹幕view加到另一个view上,如果把弹幕view remove了以后,隔0.5秒再加到原来的view上,弹幕就消失了.
    我调试了下,发现render里面的UIView animateWithDuration是调用了的.
    但是动画貌似不起作用了,弹幕lable的坐标直接变成负值了,所以看起来就像消失了一样.
    这个问题怎么解决呢?

    Reply
  22. fero2004

    额,我发的评论消失了.
    再发一次
    ====

    我发现弹幕view从一个view上移除了后,上面的动画不会继续进行了.我想把弹幕view从一个view上加到另外一个view上,而且动画会继续进行,应该怎么做呢?

    Reply
    1. 庞海礁 Post author

      为啥需要从一个view移到另一个view了?如果加到另一个view,肯定需要重新加载动画的

      Reply
  23. fanxc

    把大牛您的项目加到vvitamio的视频播放器里面 弹幕就是不显示 不知道为啥

    Reply
  24. 匿名

    - (NSArray *)cutWithDanmakus:(NSArray *)danmakus Time:(DanmakuTime *)time
    {
    NSUInteger count = danmakus.count;
    NSUInteger index, minIndex=0, maxIndex = count-1;
    DanmakuBaseModel *danmaku = nil;
    while (maxIndex-minIndex>1) {
    index = (maxIndex+minIndex)/2;
    danmaku = danmakus[index];
    if ([danmaku isDraw:time.time]) {
    maxIndex = index;
    } else {
    minIndex = index;
    }
    }

    return [danmakus subarrayWithRange:NSMakeRange(maxIndex, count-maxIndex)];
    }
    这段代码是干什么用的?原谅我小白!

    Reply
    1. 庞海礁 Post author

      二分法找到当前播放进度之后即将被渲染的弹幕

      Reply
  25. ZZZ

    对了,可以多加一点注释,也让人更快熟悉,参与进来这样利于发展。

    Reply
  26. 梁同桌

    写的非常棒, 接口写的也很好。。。
    pod 更新不成, 只能直接拉进来了 。。。。

    非常感谢 作者, 为弹幕做出的贡献

    谢谢 鞠躬

    Reply
  27. susie

    希望大神可以多加点注释,要不对我等小菜,理解起来好吃力的说~

    Reply
  28. 潘莹莹

    现在我创建了一个定时器,5秒接收一次服务器传过来的数据,数据虽然可以显示了,但是当我点击暂停,5秒弹幕自动的开始滚动!请问这是什么情况?

    Reply
    1. 庞海礁 Post author

      源码是开源的,太粗不是字体的问题,而是加了描边所致

      Reply
      1. 匿名

        怎样实现并行显示呢? 比如一行显示两个弹幕,三个弹幕

        Reply
    1. 庞海礁 Post author

      一行显示多个弹幕什么意思?现在不都是一行一行显示的

      Reply
      1. 匿名

        就像B站一样, 一行显示多个弹幕,而不是等这一行的弹幕消失了再显示另一个. 项目有这个需求,怎样改呢?

        Reply
  29. 匿名

    已实现一行多个弹幕, 我想问自己发的弹幕怎样加下划线, 我看到你代码有这方面的打算,现在实现了吗?

    Reply
      1. 匿名

        configuration.isShowLineWhenSelf = YES; 是这样吗? 设置为YES 之后, 自己发的弹幕和之前一样,没有下划线,怎么整?谢谢

        Reply
  30. 匿名

    你好,我把你的弹幕控件,放在UITableView的cell中使用,滚动界面后,弹幕又会重新加载,怎样可以让它不重新加载

    Reply
  31. 匿名

    你好,我把你的弹幕控件,放在UITableView的cell中使用,滚动界面后,弹幕又会重新加载,怎样可以让它不重新加载字幕

    Reply
  32. kang

    你好,我把你的弹幕控件,放在UITableView的cell中使用,滚动界面后,弹幕又会重新加载,怎样可以让它不重新加载字幕

    Reply
  33. 黄瓜

    博主,

    在- (void)drawDanmakus:(NSArray *)danmakus time:(DanmakuTime *)time isBuffering:(BOOL)isBuffering 方法中,
    danmaku.remainTime = danmaku.time-time.time+danmaku.duration;
    创建弹幕的时候,为什么弹幕的剩余时间要设置成这样(弹幕进入显示的时间 – 当前时间轴时间 + 弹幕的持续时间) 而不直接使用danmuku.duration ?

    Reply
    1. 庞海礁 Post author

      你是在demo里面还是自己工程里?看看是不是真的重复发送了~

      Reply
  34. 韦韦韦

    大神,请问如何让弹幕密集起来,每一行的弹幕之间的间隔短一点 10个像素就可以

    Reply
  35. 匿名

    你好 cocoa pods 搜索的时候 搜索不到HJDanmaku是什么情况呢

    Reply
  36. 匿名

    你好 cocoa pods 搜索HJDanmaku的时候 搜不到是什么情况呢

    Reply
    1. 庞海礁 Post author

      因为HJDanmaku还没有加入到官方cocoapods仓库里面,等2.0最终Release发布再上传到cocoapods里面,你可以直接在podfile里面直接依赖该pod. pod ‘HJDanmaku’, :git => ‘https://github.com/panghaijiao/HJDanmakuDemo.git’

      Reply
  37. 匿名

    你好 HJDanmakuModel里面没有关于文字的属性呢 如果发弹幕 应该怎么发呢

    Reply
    1. 庞海礁 Post author

      参考demo,创建HJDanmakuModel的子类,自定义你自己的业务属性即可!

      Reply

刘辉进行回复 取消回复

电子邮件地址不会被公开。