直播接口提供的弹幕是通过消息的形式发送过来的, 在我们播放器进行展示的时候, 需要实现弹幕的移动与排布。实现的过程中将弹幕进行抽象,最终抽象出了的三个对象,其层次关系如图:
图1 弹幕管理抽象出的对象层次结构
文本对象: 负责文本的显示、隐藏与移动, 提供是否进入弹道和是否离开弹道的接口, 提供文本移动的TICK接口。
弹道对象:包含多个文本对象, 并对这些文本对象进行管理, 提供添加文本对象、删除文本对象,获得当前显示的文本对象个数, 当前是否可添加文本对象,清空弹道, 更新所有文本对象等接口。
弹幕对象:包含多个弹道对象,并对弹道对象进行管理, 提供依据屏幕大小自动分配弹道个数,隐藏和显示弹道, 弹幕消息队列缓存, 依据消息多少动态调整显示弹道个数, 以及消息选择弹道显示等接口。
三个层次的对象, 每个对象在对应层负责相应的功能, 各司其职, 就能完成整个弹幕显示的工作。
class TextSwimmer(object):
"""
弹幕携带对象, 弹幕泳道的游泳者
"""
def __init__(self, text_widget, msg, start_pos, end_pos):
"""
游泳者的初始化
"""
self.text = msg
self.start_pos = start_pos
self.end_pos = end_pos
self.extern_dis = 30
self.text_content = self.construct_show_text(msg)
self.text_widget = text_widget.clone()
self.text_widget.setAnchorPoint(cc.Vec2(0.0, 0.5))
utils.set_content_rich(self.text_widget, self.text_content)
shadow_color = utils.color_code_2_rgb('000000', 255)
self.text_widget.enableShadow(shadow_color, cc.Size(3, -3), False)
self.text_widget.setVisible(False)
self.text_size = self.text_widget.getContentSize()
self.last_move_time = time.time()
self.interval = 50
self.is_arrive = False
self.start_swim()
def construct_show_text(self, msg):
"""
构建需要显示的信息
"""
nick = '[' + msg.get('nick') + ']'
msg_body = msg.get('msg_body')
text_str = '#cFDFA02%s#n' % nick
text_str += '#cFFFFFF%s#n' % msg_body
return text_str
def start_swim(self):
"""
弹幕文本移动, 开始游泳
"""
self.text_widget.setVisible(True)
pos_x = self.start_pos.x + self.extern_dis
self.text_widget.setPosition(cc.Vec2(pos_x, 0))
self.last_move_time = time.time()
def hide_msg(self):
"""
隐藏弹幕
"""
self.text_widget.setVisible(False)
def tick(self, speed):
"""
保持移动
"""
# 计算移动距离
now = time.time()
delta_time = now - self.last_move_time
dis = delta_time * speed
self.last_move_time = now
# 进行移动, 并判定是否结束
cur_pos = self.text_widget.getPosition()
cur_pos.x += dis
if cur_pos.x + self.text_size.width / 2.0 + self.extern_dis < self.end_pos.x:
self.end_swim()
else:
self.text_widget.setPosition(cur_pos)
def end_swim(self):
"""
游泳结束
"""
self.text_widget.setVisible(False)
self.is_arrive = True
def is_offshore(self):
"""
下水的游泳者是否已经离岸, 下一个是否可以下水
"""
cur_pos = self.text_widget.getPosition()
text_size = self.text_widget.getContentSize()
text_width = text_size.width * self.text_widget.getScaleX()
return cur_pos.x + text_width + self.interval < self.start_pos.x
class SwimLine(object):
"""
弹幕泳道
"""
def __init__(self, screen_widget, text_widget, position):
"""
弹幕泳道
"""
self.screen_widget = screen_widget
self.text_widget = text_widget
self.screen_size = screen_widget.getContentSize()
self.root_node = ccui.Text.create()
self.root_node.setAnchorPoint(cc.Vec2(0, 0.5))
self.screen_widget.addChild(self.root_node)
self.root_node.setPosition(position)
self.position = position
self.start_pos, self.end_pos = self.get_line_pos()
self.last_text_swimmer = None
self.text_swimmer_list = []
self.speed = self.screen_size.width / float(random.uniform(5, 10))
def add_text_swimmer(self, msg):
"""
添加游泳者
"""
# 游泳对象创建
text_swimmer = TextSwimmer(self.text_widget, msg, self.start_pos, self.end_pos)
self.text_swimmer_list.append(text_swimmer)
self.last_text_swimmer = text_swimmer
# 游泳对象控件的添加
self.root_node.addChild(text_swimmer.text_widget)
def remove_text_swimmer(self, text_swimmer):
"""
移除游泳者
"""
self.root_node.removeChild(text_swimmer.text_widget)
self.text_swimmer_list.remove(text_swimmer)
if self.last_text_swimmer is text_swimmer:
self.last_text_swimmer = None
def get_line_pos(self):
"""
获取应道的开始与结束位置
"""
x = self.screen_size.width
y = self.position.y
return cc.Vec2(x, y), cc.Vec2(0, y)
def get_cur_speed(self):
"""
获取泳道的速度
"""
return -self.speed
def get_swimmer_num(self):
"""
获取当前游泳在人的数量
"""
return len(self.text_swimmer_list)
def set_last_swimmer(self, text_swimmer):
"""
设置上一个下水的游泳者
"""
self.last_text_swimmer = text_swimmer
def is_swim_line_available(self):
"""
当前泳道是都可下水
"""
if not self.last_text_swimmer:
return True
return self.last_text_swimmer.is_offshore()
def hide_msg(self):
"""
隐藏弹幕
"""
self.text_swimmer_list = []
self.last_text_swimmer = None
self.root_node.removeAllChildren()
def tick(self):
"""
泳道的tick
"""
speed = self.get_cur_speed()
# 每个游泳者进行游动
arrive_swimmer_list = []
for text_swimmer in self.text_swimmer_list:
text_swimmer.tick(speed)
if text_swimmer.is_arrive:
arrive_swimmer_list.append(text_swimmer)
# 剔除上岸的游泳者
for text_swimmer in arrive_swimmer_list:
self.remove_text_swimmer(text_swimmer)
class BarrageManager(object):
"""
弹幕管理器
"""
def __init__(self, screen_widget, text_widget, max_line):
"""
弹幕管理器初始化
@ screen_widget: 视频屏幕绑定的image控件
@ open_line: 常开的弹幕通道数
@ max_line: 弹幕较多时最多开的通道数
"""
# 弹幕管理器的状态
self.enable = True
# 初始化弹幕参数
self.screen_widget = screen_widget
self.min_line = max(int(max_line * 0.3), 1)
self.max_line = max_line
self.open_line = self.min_line
self.max_msg_count = 1000
self.msg_cache = Queue.Queue(maxsize=self.max_msg_count)
self.swim_line_list = []
# 初始化弹幕通道
self.screen_size = self.screen_widget.getContentSize()
self.gap_interval = self.screen_size.height / float(max(max_line, 1))
for i in xrange(max_line):
position = cc.Vec2(0, self.screen_size.height-self.gap_interval* (i+0.5))
position.y *= self.screen_widget.getScaleX()
swim_line = SwimLine(self.screen_widget, text_widget, position)
self.swim_line_list.append(swim_line)
def show_msg(self, msg):
"""
显示弹幕MSG
"""
if not self.enable:
return
if self.msg_cache.full():
return
try: self.msg_cache.put(msg, block=False)
except: pass
def hide_msg(self):
"""
隐藏弹幕MSG
"""
self.msg_cache.queue.clear()
for swim_line in self.swim_line_list:
if not swim_line:
continue
swim_line.hide_msg()
def get_available_lines(self):
"""
获取当前可添加弹幕的弹道, 并进行排序
"""
# 获得可下水的泳道
available_lines = []
for i in xrange(self.open_line):
swim_line = self.swim_line_list[i]
if swim_line.is_swim_line_available():
available_lines.append([swim_line, swim_line.get_swimmer_num()])
available_lines.sort(key= lambda x: x[1])
return available_lines
def put_message_in_line(self, available_lines):
"""
将弹幕放入弹道进行展示
"""
# 获得需要下水的消息
msg_list = []
for i in xrange(len(available_lines)):
if self.msg_cache.empty():
break
try: msg_list.append(self.msg_cache.get(block=False))
except: pass
# 安排消息下水, 松散优先, 相同随机
for msg in msg_list:
candidate_line_list = []
for swim_line in available_lines:
if swim_line[1] == available_lines[0][1]:
candidate_line_list.append(swim_line)
swim_line_tuple = random.choice(candidate_line_list)
swim_line_tuple[0].add_text_swimmer(msg)
available_lines.remove(swim_line_tuple)
def tick(self):
"""
弹幕通道的tick
"""
# 各各泳道的tick
for swim_line in self.swim_line_list:
swim_line.tick()
# 获取可添加弹幕的弹道
self.open_line = min(self.max_line, max(self.min_line, (self.msg_cache.qsize()-1)/5 + 1))
available_lines = self.get_available_lines()
# 将弹幕放入弹道
self.put_message_in_line(available_lines)