bilibili弹幕转ass程序制作思路及过程

b站的弹幕,线下播放还是挺麻烦的,专用的弹幕播放器对其他格式的视频支持不好。我也试着弄个弹幕转字幕的小程序出来。

抓取xml文件的工作就不多说了,很简单的事,只要在播放页面看看源文件就能确定xml文件的地址进行抓取了。

本文主要是讲述xml内的弹幕转字幕的过程。

除去xml文件开头结尾的一些七七八八的东西,弹幕主体是这样的:

<d p="51.593,5,25,16711680,1408852480,0,7fa769b4,576008622">怒求 up 自己配音!</d>
<d p="10.286,1,25,16777215,1408852600,0,a3af4d0d,576011065">颜艺?</d>
<d p="12.65,1,25,16777215,1408852761,0,24570b5a,576014281">我的女神!</d>
<d p="19.033,1,25,16777215,1408852789,0,cb20d1c7,576014847">前!!!</d>
<d p="66.991,1,25,16777215,1408852886,0,a78e484d,576016806">已撸</d>

如果它把弹幕的各种属性分开表示,我就用encoding/xml包来解码,但是丫把弹幕的属性都放在p里面了,所以我使用正则表达式来提取的。

以上表第一条弹幕为例。很明显的,p属性开始的浮点数,与播放时一比对,就能知道,表示的是弹幕应该出现的播放时间。

随后的1和25先不管;

16777215,目测应该是颜色(因为该值表示为十六进制是FFFFFF);

1408852480,在弹幕中是递增的,感觉应该是个unix时间,用这个数(d),求:d/86400/365.2425+1970,结果约为2014.6。看来确实是unix时间。估计是创建弹幕的时间。

0,不知道,抓取了很多视频的弹幕,这个位置都是0,暂且不管它。

7fa769b4,估计是创建者的ID,因为同一xml文件会出现多次,而且看起来是十六进制数,恰好有些hash函数就是返回4字节整数。

576008622,也是递增的,不用猜也知道,这个肯定就是弹幕的ID了。

事后再核对一下,果然,1代表弹幕的类型(从右向左移动啊,出现在下方或者上方啊……),25是字体大小,16777125是字体颜色。

所以,我们就只要捕获每条弹幕的时间、类型、大小、颜色、文本就行了。

正则表达式:

<d\sp="([\d\.]+),([145]),(\d+),(\d+),\d+,\d+,\w+,\d+">([^<>]+?)</d>

捕获弹幕很简单,关键是排布弹幕为字幕的算法。
关于这个算法我就很坑爹的弄了个乱七八糟的算法,采用的是固定移动速度,最小重叠的排布原则。

对游动弹幕,会倾向于选择下面一行的位置,如果会重叠,则选择更下一行(最低行会循环到最上面一行),如果没有不重叠的行,会选择重叠文本最少的行。

对上现隐/下现隐的固定弹幕,会选择最接近上方/下方,且不重叠的行;如果没有不重叠的行,则选择重叠时间最短的行,居中放置字幕。

默认字体微软雅黑,默认大小25,默认白色黑边;默认占满整个屏幕,共计12行;默认屏幕大小640x360。

这么弄,主要是为了让ass字幕的效果更接近原始弹幕的效果。

高级弹幕真的超出我的能力范围了,全部忽略掉。

go源代码如下:

// 将bilibili的xml弹幕文件转换为ass字幕文件。
// xml文件中,弹幕的格式如下:
// <d p="32.066,1,25,16777215,1409046965,0,017d3f58,579516441">地板好评</d>
// p的属性为时间、弹幕类型、字体大小、字体颜色、创建时间、?、创建者ID、弹幕ID。
// p的属性中,后4项对ass字幕无用,舍弃。被<d>和</d>包围的是弹幕文本。
// 只处理右往左、上现隐、下现隐三种类型的普通弹幕。
package main

import (
  "fmt"
  "io"
  "io/ioutil"
  "math"
  "os"
  "regexp"
  "sort"
  "strconv"
  "strings"
)

// ass文件的头部
const header = `[Script Info]
ScriptType: v4.00+
Collisions: Normal
playResX: 640
playResY: 360

[V4+ Styles]
Format: Name, Fontname, Fontsize, primaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default, Microsoft YaHei, 28, &H00FFFFFF, &H00FFFFFF, &H00000000, &H00000000, 0, 0, 0, 0, 100, 100, 0.00, 0.00, 1, 1, 0, 2, 10, 10, 10, 0

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
`

// 正则匹配获取弹幕原始信息
var line = regexp.MustCompile(`<d\sp="([\d\.]+),([145]),(\d+),(\d+),\d+,\d+,\w+,\d+">([^<>]+?)</d>`)

// 用来保管弹幕的信息
type Danmu struct {
  text string
  time float64
  kind byte
  size int
  color int
}

// 使[]Danmu实现sort.Interface接口,以便排序
type Danmus []Danmu

func (d Danmus) Len() int {
  return len(d)
}
func (d Danmus) Less(i, j int) bool {
  return d[i].time < d[j].time
}
func (d Danmus) Swap(i, j int) {
  d[i], d[j] = d[j], d[i]
}

// 将正则匹配到的数据填写入Danmu类型里
func fill(d *Danmu, s [][]byte) {
  d.time, _ = strconv.ParseFloat(string(s[1]), 64)
  d.kind = s[2][0] - '0'
  d.size, _ = strconv.Atoi(string(s[3]))
  bgr, _ := strconv.Atoi(string(s[4]))
  d.color = ((bgr >> 16) & 255) | (bgr & (255 << 8)) | ((bgr & 255) << 16)
  d.text = string(s[5])
}

// 返回文本的长度,假设ascii字符都是0.5个字长,其余都是1个字长
func length(s string) float64 {
  l := 0.0
  for _, r := range s {
    if r < 127 {
      l += 0.5
    } else {
      l += 1
    }
  }
  return l
}

// 生成时间点的ass格式表示:`0:00:00.00`
func timespot(f float64) string {
  h, f := math.Modf(f / 3600)
  m, f := math.Modf(f * 60)
  return fmt.Sprintf("%d:%02d:%05.2f", int(h), int(m), f*60)
}

// 读取文件并获取其中的弹幕
func open(name string) ([]Danmu, error) {
  data, err := ioutil.ReadFile(name)
  if err != nil {
    return nil, err
  }
  dan := line.FindAllSubmatch(data, -1)
  ans := make([]Danmu, len(dan))
  for i := len(dan) - 1; i >= 0; i-- {
    fill(&ans[i], dan[i])
  }
  return ans, nil
}

// 将弹幕排布并写入w,采用的简单的固定移速、最小重叠排布算法
func save(w io.Writer, dans []Danmu) {
  p1 := make([]float64, 36)
  p2 := make([]float64, 36)
  p3 := make([]float64, 36)
  t := 0
  max := func(x []float64) float64 {
    i := x[0]
    for _, j := range x[1:] {
      if i < j {
        i = j
      }
    }
    return i
  }
  set := func(x []float64, f float64) {
    for i, _ := range x {
      x[i] = f
    }
  }
  find := func(p []float64, f float64, i, d int) int {
    i = (i/d + 1) * d % 36
    m, k := f+10000, 0
    for j := 0; j < 36; j += d {
      t := (i + j) % 36
      if n := max(p[t : t+d]); n <= f {
        k = t
        break
      } else if m > n {
        k = t
        m = n
      }
    }
    return k
  }
  for _, dan := range dans {
    s, l := "", length(dan.text)
    if l == 0 {
      continue
    }
    switch {
    case dan.size < 25:
      dan.size, l, s = 2, l*18, "\\fs18"
    case dan.size == 25:
      dan.size, l = 3, l*28
    case dan.size > 25:
      dan.size, l, s = 4, l*38, "\\fs38"
    }
    if dan.color != 0x00FFFFFF {
      s += fmt.Sprintf("\\c&H%06X", dan.color)
    }
    switch dan.kind {
    case 1: // 右往左
      t := find(p1, dan.time, t, dan.size)
      set(p1[t:t+dan.size], dan.time+8)
      h := (t+dan.size)*10 - 1
      s += fmt.Sprintf("\\move(%d,%d,%d,%d)", 640+int(l/2), h, -int(l/2), h)
      fmt.Fprintf(w, "Dialogue: 1,%s,%s,Default,,0000,0000,0000,,{%s}%s\n",
        timespot(dan.time+0),
        timespot(dan.time+8), s, dan.text)
    case 4: // 下现隐
      j := find(p2, dan.time, 35, dan.size)
      set(p2[j:j+dan.size], dan.time+4)
      s += fmt.Sprintf("\\pos(%d,%d)", 320, (36-j)*10-1)
      fmt.Fprintf(w, "Dialogue: 2,%s,%s,Default,,0000,0000,0000,,{%s}%s\n",
        timespot(dan.time+0),
        timespot(dan.time+4), s, dan.text)
    case 5: // 上现隐
      j := find(p3, dan.time, 35, dan.size)
      set(p3[j:j+dan.size], dan.time+4)
      s += fmt.Sprintf("\\pos(%d,%d)", 320, (j+dan.size)*10-1)
      fmt.Fprintf(w, "Dialogue: 3,%s,%s,Default,,0000,0000,0000,,{%s}%s\n",
        timespot(dan.time+0),
        timespot(dan.time+4), s, dan.text)
    }
  }
}

// 主函数,实现了命令行
func main() {
  if len(os.Args) <= 1 {
    os.Exit(0)
  }
  for _, name := range os.Args[1:] {
    dans, err := open(name)
    if err != nil {
      os.Exit(1)
    }
    if n := strings.LastIndex(name, "."); n != -1 {
      name = name[:n]
    }
    name += ".ass"
    file, err := os.Create(name)
    if err != nil {
      os.Exit(2)
    }
    file.WriteString(header)
    sort.Sort(Danmus(dans))
    save(file, dans)
    file.Close()
  }
}

2014.9.2 9:30am更新:对字体排布进行了修正。

2014.9.2 9:50am更新:算法修改为固定出现时间,最小重叠排布,最终版本。

over。欢迎各位评论,倒不如各位多多评论啊。

(0)

相关推荐

  • 实例解析如何在Android应用中实现弹幕动画效果

    在B站或者其他视频网站看视频时,常常会打开弹幕效果,边看节目边看大家的吐槽.弹幕看起来很有意思,今天我们就来实现一个简单的弹幕效果. 从直观上,弹幕效果就是在一个ViewGroup上增加一些View,然后让这些View移动起来.所以,整体的实现思路大概是这样的: 1.定义一个RelativeLayout,在里面动态添加TextView. 2.这些TextView的字体大小.颜色.移动速度.初始位置都是随机的. 3.将TextView添加到RelativeLayout的右边缘,每隔一段时间添加一个

  • JavaScript直播评论发弹幕切图功能点集合效果代码

    一.代码 html+js <!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <title>数发直播平台</title> <link rel="stylesheet" type="text/css" href="css/common.css"> <link rel="styl

  • 终于实现了!精彩的jquery弹幕效果

    本文实例为大家分享了jquery弹幕效果,供大家参考,具体内容如下 页面效果如下: html页面如下: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"

  • Android实现自定义的弹幕效果

    一.效果图 先来看看效果图吧~~ 二.实现原理方案 1.自定义ViewGroup-XCDanmuView,继承RelativeLayout来实现,当然也可以继承其他三大布局类哈 2.初始化若干个TextView(弹幕的item View,这里以TextView 为例,当然也可以其他了~),然后通过addView添加到自定义View中 3.通过addView添加到XCDanmuView中,位置在坐标,为了实现 从屏幕外移动进来的效果 我们还需要修改添加进来TextView的位置,以从右向左移动方向

  • JQuery和HTML5 Canvas实现弹幕效果

    JQuery和HTML5 Canvas两种方法实现弹幕效果: 方法一,JQuery实现. 源码: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" cont

  • 又一枚精彩的弹幕效果jQuery实现

    简易弹幕效果:将发布的内容随机显示在弹幕右侧,逐渐左移最后消失. 涉及知识点:val().random().height().css().append().remove()等,主要是元素的操作 html代码: <a href="#">弹幕技术</a> <div class="mask"> <a href="#" class="button">X</a> </di

  • IOS 实现简单的弹幕功能

    前言 简单实现弹幕功能,表跟我谈效率,但也有用队列控制同时弹的数量. 正文 代码实现: let DANMAKU_SPEED: CGFloat = 150 // 弹幕每秒移动速度 let DANMAKU_SPACE_TIME: NSTimeInterval = 1 // 弹幕之间的时间间隔 let DANMAKU_MAX_ROW = 3 // 最多同时弹幕行数 let danmakuFont = UIFont.systemFontOfSize(18) // 弹幕字体大小 var rowArray

  • Android 实现仿网络直播弹幕功能详解及实例

    Android 网络直播弹幕                最近看好多网络电视,播放器及直播都有弹幕功能,自己周末捣鼓下并实现,以下是网上的资料,大家可以看下. 现在网络直播越来越火,网络主播也逐渐成为一种新兴职业,对于网络直播,弹幕功能是必须要有的,如下图: 首先来分析一下,这个弹幕功能是怎么实现的,首先在最下面肯定是一个游戏界面View,然后游戏界面上有弹幕View,弹幕的View必须要做成完全透明的,这样即使覆盖在游戏界面的上方也不会影响到游戏的正常观看,只有当有人发弹幕消息时,再将消息绘

  • 基于jquery实现弹幕效果

    用js写的一个弹幕 效果图: 源码: <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"&

  • 很棒的Android弹幕效果实例

    很多项目需要用到弹幕效果,尤其是在播放视频的时候需要一起显示别人发的弹幕,也包括自己的发的. 今天就试着写了一下这个效果. 思路就是将从右往左的动画效果,字体内容,字体大小,弹幕平移速度等属性一起与TextView封装成BarrageItem,并将控制效果与BarrageItem绑定在BarrageView进行显示.思路还是比较简单的.这里没有考虑到带有表情的弹幕,我会持续更新的. 先看效果: 项目目录结构: 接下来定义Barrageitem.class : 这个类就将TextView与从右往左

随机推荐