@dan-uni/dan-any 0.0.7 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +10 -1
  2. package/dist/index.js +5956 -1
  3. package/dist/index.umd.min.js +19951 -1
  4. package/dist/index.umd.min.js.LICENSE.txt +17 -0
  5. package/dist/src/ass-gen/__tests__/generate.test.d.ts +1 -0
  6. package/dist/src/ass-gen/ass/create.d.ts +4 -0
  7. package/dist/src/ass-gen/ass/dialogue.d.ts +17 -0
  8. package/dist/src/ass-gen/ass/event.d.ts +3 -0
  9. package/dist/src/ass-gen/ass/info.d.ts +8 -0
  10. package/dist/src/ass-gen/ass/raw.d.ts +10 -0
  11. package/dist/src/ass-gen/ass/style.d.ts +3 -0
  12. package/dist/src/ass-gen/config.d.ts +3 -0
  13. package/dist/src/ass-gen/index.d.ts +26 -0
  14. package/dist/src/ass-gen/types.d.ts +47 -0
  15. package/dist/src/ass-gen/util/color.d.ts +18 -0
  16. package/dist/src/ass-gen/util/danconvert.d.ts +4 -0
  17. package/dist/src/ass-gen/util/index.d.ts +4 -0
  18. package/dist/src/ass-gen/util/lang.d.ts +3 -0
  19. package/dist/src/ass-gen/util/layout.d.ts +4 -0
  20. package/dist/{index.d.ts → src/index.d.ts} +16 -1
  21. package/dist/src/index.test.d.ts +1 -0
  22. package/dist/{utils → src/utils}/dm-gen.d.ts +26 -9
  23. package/dist/src/utils/dm-gen.test.d.ts +1 -0
  24. package/package.json +9 -7
  25. package/rslib.config.ts +81 -0
  26. package/src/ass-gen/__tests__/898651903.xml +1619 -0
  27. package/src/ass-gen/__tests__/898651903.xml.ass +1392 -0
  28. package/src/ass-gen/__tests__/canvas.test.ts +11 -0
  29. package/src/ass-gen/__tests__/generate.test.ts +20 -0
  30. package/src/ass-gen/ass/create.ts +26 -0
  31. package/src/ass-gen/ass/dialogue.ts +91 -0
  32. package/src/ass-gen/ass/event.ts +58 -0
  33. package/src/ass-gen/ass/info.ts +28 -0
  34. package/src/ass-gen/ass/raw.ts +69 -0
  35. package/src/ass-gen/ass/style.ts +67 -0
  36. package/src/ass-gen/config.ts +45 -0
  37. package/src/ass-gen/index.ts +52 -0
  38. package/src/ass-gen/types.ts +52 -0
  39. package/src/ass-gen/util/color.ts +55 -0
  40. package/src/ass-gen/util/danconvert.ts +36 -0
  41. package/src/ass-gen/util/index.ts +10 -0
  42. package/src/ass-gen/util/lang.ts +35 -0
  43. package/src/ass-gen/util/layout.ts +238 -0
  44. package/src/index.test.ts +16 -0
  45. package/src/index.ts +114 -0
  46. package/src/utils/dm-gen.test.ts +66 -0
  47. package/src/utils/dm-gen.ts +153 -34
  48. package/tsconfig.json +7 -7
  49. package/CHANGELOG.md +0 -9
  50. package/dist/index.min.js +0 -1
  51. package/rollup.config.mjs +0 -51
  52. /package/dist/{index.test.d.ts → src/ass-gen/__tests__/canvas.test.d.ts} +0 -0
  53. /package/dist/{proto → src/proto}/gen/bili/dm_pb.d.ts +0 -0
  54. /package/dist/{proto → src/proto}/gen/danuni_pb.d.ts +0 -0
  55. /package/dist/{utils → src/utils}/id-gen.d.ts +0 -0
@@ -0,0 +1,238 @@
1
+ import { createCanvas } from 'canvas'
2
+ import type { UniPool } from '../..'
3
+ import type { Danmaku, SubtitleStyle } from '../types'
4
+
5
+ import { DanmakuType, FontSize } from '../types'
6
+ import { DanmakuList2UniPool, UniPool2DanmakuLists } from './danconvert'
7
+ import { arrayOfLength } from './lang'
8
+
9
+ // 计算一个矩形移进屏幕的时间(头进屏幕到尾巴进屏幕)
10
+ const computeScrollInTime = (
11
+ rectWidth: number,
12
+ screenWidth: number,
13
+ scrollTime: number,
14
+ ) => {
15
+ const speed = (screenWidth + rectWidth) / scrollTime
16
+ return rectWidth / speed
17
+ }
18
+
19
+ // 计算一个矩形在屏幕上的时间(头进屏幕到头离开屏幕)
20
+ const computeScrollOverTime = (
21
+ rectWidth: number,
22
+ screenWidth: number,
23
+ scrollTime: number,
24
+ ) => {
25
+ const speed = (screenWidth + rectWidth) / scrollTime
26
+ return screenWidth / speed
27
+ }
28
+
29
+ interface ScrollGrid {
30
+ start: number
31
+ end: number
32
+ width: number
33
+ }
34
+
35
+ type FixGrid = number
36
+
37
+ interface DanmakuGrids {
38
+ // [DanmakuType.SCROLL]: ScrollGrid[];
39
+ // [DanmakuType.TOP]: FixGrid[];
40
+ // [DanmakuType.BOTTOM]: FixGrid[];
41
+ 1: ScrollGrid[] // DanmakuType.SCROLL
42
+ 3: FixGrid[] // DanmakuType.TOP
43
+ 2: FixGrid[] // DanmakuType.BOTTOM
44
+ }
45
+
46
+ const splitGrids = ({
47
+ fontSize,
48
+ padding,
49
+ playResY,
50
+ bottomSpace,
51
+ }: {
52
+ fontSize: number[]
53
+ padding: number[]
54
+ playResY: number
55
+ bottomSpace: number
56
+ }): DanmakuGrids => {
57
+ const defaultFontSize = fontSize[FontSize.NORMAL]
58
+ const paddingTop = padding[0]
59
+ const paddingBottom = padding[2]
60
+ const linesCount = Math.floor(
61
+ (playResY - bottomSpace) / (defaultFontSize + paddingTop + paddingBottom),
62
+ )
63
+
64
+ // 首先以通用的字号把屏幕的高度分成若干行,字幕只允许落在一个行里
65
+ return {
66
+ // 每一行里的数字是当前在这一行里的最后一条弹幕区域(算入padding)的右边离开屏幕的时间,
67
+ // 这个时间和下一条弹幕的左边离开屏幕的时间相比较,能确定在整个弹幕的飞行过程中是否会相撞(不同长度弹幕飞行速度不同)|,
68
+ // 当每一条弹幕加到一行里时,就会把这个时间算出来,获取新的弹幕时就可以判断哪一行是允许放的就放进去
69
+ 1: arrayOfLength(linesCount, {
70
+ start: 0,
71
+ end: 0,
72
+ width: 0,
73
+ }),
74
+ // 对于固定的弹幕,每一行里都存放弹幕的消失时间,只要这行的弹幕没消失就不能放新弹幕进来
75
+ 3: arrayOfLength(linesCount, 0),
76
+ 2: arrayOfLength(linesCount, 0),
77
+ }
78
+ }
79
+
80
+ export const measureTextWidth = (() => {
81
+ const canvasContext = createCanvas(50, 50).getContext('2d')
82
+ const supportTextMeasure = !!canvasContext.measureText('中')
83
+
84
+ if (supportTextMeasure) {
85
+ return (
86
+ fontName: string,
87
+ fontSize: number,
88
+ bold: boolean,
89
+ text: string,
90
+ ) => {
91
+ canvasContext.font = `${bold ? 'bold' : 'normal'} ${fontSize}px ${fontName}`
92
+ const textWidth = canvasContext.measureText(text).width
93
+ return Math.round(textWidth)
94
+ }
95
+ }
96
+
97
+ console.warn(
98
+ '[Warn] node-canvas is installed without text measure support, layout may not be correct',
99
+ )
100
+ return (_fontName: string, fontSize: number, _bold: boolean, text: string) =>
101
+ text.length * fontSize
102
+ })()
103
+
104
+ // 找到能用的行
105
+ const resolveAvailableFixGrid = (grids: FixGrid[], time: number) => {
106
+ for (const [i, grid] of grids.entries()) {
107
+ if (grid <= time) {
108
+ return i
109
+ }
110
+ }
111
+
112
+ return -1
113
+ }
114
+
115
+ const resolveAvailableScrollGrid = (
116
+ grids: ScrollGrid[],
117
+ rectWidth: number,
118
+ screenWidth: number,
119
+ time: number,
120
+ duration: number,
121
+ ) => {
122
+ for (const [i, previous] of grids.entries()) {
123
+ // 对于滚动弹幕,要算两个位置:
124
+ //
125
+ // 1. 前一条弹幕的尾巴进屏幕之前,后一条弹幕不能开始出现
126
+ // 2. 前一条弹幕的尾巴离开屏幕之前,后一条弹幕的头不能离开屏幕
127
+ const previousInTime =
128
+ previous.start +
129
+ computeScrollInTime(previous.width, screenWidth, duration)
130
+ const currentOverTime =
131
+ time + computeScrollOverTime(rectWidth, screenWidth, duration)
132
+
133
+ if (time >= previousInTime && currentOverTime >= previous.end) {
134
+ return i
135
+ }
136
+ }
137
+
138
+ return -1
139
+ }
140
+
141
+ const initializeLayout = (config: SubtitleStyle) => {
142
+ const {
143
+ playResX,
144
+ playResY,
145
+ fontName,
146
+ fontSize,
147
+ bold,
148
+ padding,
149
+ scrollTime,
150
+ fixTime,
151
+ bottomSpace,
152
+ } = config
153
+ const [paddingTop, paddingRight, paddingBottom, paddingLeft] = padding
154
+
155
+ const defaultFontSize = fontSize[FontSize.NORMAL]
156
+ const grids = splitGrids(config)
157
+ const gridHeight = defaultFontSize + paddingTop + paddingBottom
158
+
159
+ return (danmaku: Danmaku) => {
160
+ const targetGrids = grids[danmaku.type as keyof DanmakuGrids]
161
+ const danmakuFontSize = fontSize[danmaku.fontSizeType]
162
+ const rectWidth =
163
+ measureTextWidth(fontName, danmakuFontSize, bold, danmaku.content) +
164
+ paddingLeft +
165
+ paddingRight
166
+ const verticalOffset =
167
+ paddingTop + Math.round((defaultFontSize - danmakuFontSize) / 2)
168
+
169
+ if (danmaku.type === DanmakuType.SCROLL) {
170
+ const scrollGrids = targetGrids as ScrollGrid[]
171
+ const gridNumber = resolveAvailableScrollGrid(
172
+ scrollGrids,
173
+ rectWidth,
174
+ playResX,
175
+ danmaku.time,
176
+ scrollTime,
177
+ )
178
+ if (gridNumber < 0) {
179
+ // console.warn(`[Warn] Collision ${danmaku.time}: ${danmaku.content}`)
180
+ return null
181
+ }
182
+ targetGrids[gridNumber] = {
183
+ width: rectWidth,
184
+ start: danmaku.time,
185
+ end: danmaku.time + scrollTime,
186
+ }
187
+
188
+ const top = gridNumber * gridHeight + verticalOffset
189
+ const start = playResX + paddingLeft
190
+ const end = -rectWidth
191
+
192
+ return { ...danmaku, top, start, end }
193
+ } else {
194
+ const gridNumber = resolveAvailableFixGrid(
195
+ targetGrids as FixGrid[],
196
+ danmaku.time,
197
+ )
198
+ if (gridNumber < 0) {
199
+ // console.warn(`[Warn] Collision ${danmaku.time}: ${danmaku.content}`)
200
+ return null
201
+ }
202
+ if (danmaku.type === DanmakuType.TOP) {
203
+ targetGrids[gridNumber] = danmaku.time + fixTime
204
+
205
+ const top = gridNumber * gridHeight + verticalOffset
206
+ // 固定弹幕横向按中心点计算
207
+ const left = Math.round(playResX / 2)
208
+
209
+ return { ...danmaku, top, left }
210
+ }
211
+
212
+ targetGrids[gridNumber] = danmaku.time + fixTime
213
+
214
+ // 底部字幕的格子是留出`bottomSpace`的位置后从下往上算的
215
+ const top =
216
+ playResY -
217
+ bottomSpace -
218
+ gridHeight * gridNumber -
219
+ gridHeight +
220
+ verticalOffset
221
+ const left = Math.round(playResX / 2)
222
+
223
+ return { ...danmaku, top, left }
224
+ }
225
+ }
226
+ }
227
+
228
+ export const layoutDanmaku = (
229
+ inputList: UniPool,
230
+ config: SubtitleStyle,
231
+ ): UniPool => {
232
+ const list = [...UniPool2DanmakuLists(inputList)].sort(
233
+ (x, y) => x.time - y.time,
234
+ )
235
+ const layout = initializeLayout(config)
236
+
237
+ return DanmakuList2UniPool(list.map(layout).filter((danmaku) => !!danmaku))
238
+ }
package/src/index.test.ts CHANGED
@@ -48,6 +48,12 @@ describe('转化自', () => {
48
48
  console.info(json)
49
49
  console.info(pool)
50
50
  })
51
+ it('ass(双向)', () => {
52
+ const pool = UniPool.fromBiliXML(xml)
53
+ const ass = pool.toASS()
54
+ console.info(ass)
55
+ console.info(UniPool.fromASS(ass))
56
+ })
51
57
  })
52
58
 
53
59
  describe('共通值', () => {
@@ -59,3 +65,13 @@ describe('共通值', () => {
59
65
  console.info(pool.split('pool'))
60
66
  })
61
67
  })
68
+
69
+ describe('其它', () => {
70
+ const pool = UniPool.fromBiliXML(xml)
71
+ it('最小化', () => {
72
+ console.info(pool.minify())
73
+ })
74
+ it('合并范围内重复', () => {
75
+ console.info(pool.merge(10).minify())
76
+ })
77
+ })
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { XMLParser } from 'fast-xml-parser'
2
+ import type { Options as AssGenOptions } from './ass-gen'
2
3
  import type { CommandDm as DM_JSON_BiliCommandGrpc } from './proto/gen/bili/dm_pb'
3
4
  // import type * as UniDMType from './utils/dm-gen'
4
5
  import type { platfrom } from './utils/id-gen'
@@ -10,6 +11,7 @@ import {
10
11
  timestampNow,
11
12
  } from '@bufbuild/protobuf/wkt'
12
13
 
14
+ import { generateASS, parseAssRawField } from './ass-gen'
13
15
  import {
14
16
  // DanmakuElem as DM_JSON_BiliGrpc,
15
17
  DmSegMobileReplySchema,
@@ -71,6 +73,7 @@ export type DM_format =
71
73
  | 'dplayer.json'
72
74
  | 'artplayer.json'
73
75
  | 'ddplay.json'
76
+ | 'common.ass'
74
77
 
75
78
  type shareItems = Partial<
76
79
  Pick<
@@ -95,6 +98,9 @@ export class UniPool {
95
98
  static create() {
96
99
  return new UniPool([])
97
100
  }
101
+ /**
102
+ * 合并弹幕/弹幕库
103
+ */
98
104
  assign(dans: UniPool | UniDM | UniDM[]) {
99
105
  if (dans instanceof UniPool) {
100
106
  return new UniPool([...this.dans, ...dans.dans])
@@ -104,6 +110,9 @@ export class UniPool {
104
110
  return new UniPool([...this.dans, ...dans])
105
111
  } else return this
106
112
  }
113
+ /**
114
+ * 按共通属性拆分弹幕库
115
+ */
107
116
  split(key: keyof shareItems) {
108
117
  if (this.shared[key]) return [this]
109
118
  const set = new Set(this.dans.map((d) => d[key]))
@@ -111,6 +120,105 @@ export class UniPool {
111
120
  return new UniPool(this.dans.filter((d) => d[key] === v))
112
121
  })
113
122
  }
123
+ /**
124
+ * 合并一定时间段内的重复弹幕,防止同屏出现过多
125
+ * @param lifetime 查重时间区段,单位秒 (默认为0,表示不查重)
126
+ */
127
+ merge(lifetime = 0) {
128
+ if (!this.shared.FCID) {
129
+ console.error(
130
+ "本功能仅支持同弹幕库内使用,可先 .split('FCID') 在分别使用",
131
+ )
132
+ return this
133
+ }
134
+ if (lifetime <= 0) return this
135
+ const mergeContext = this.dans.reduce<
136
+ [
137
+ UniDM[],
138
+ Record<string, UniDM>,
139
+ Record<string, UniDMTools.ExtraDanUniMerge>,
140
+ ]
141
+ >(
142
+ ([result, cache, mergeObj], danmaku) => {
143
+ const key = ['content', 'mode', 'platform', 'pool', 'SPMO']
144
+ .map((k) => danmaku[k as keyof UniDM])
145
+ .join('|')
146
+ const cached = cache[key]
147
+ const lastAppearTime = cached?.progress || 0
148
+ if (
149
+ cached &&
150
+ danmaku.progress - lastAppearTime <= lifetime &&
151
+ danmaku.isSameAs(cached)
152
+ ) {
153
+ const senders = mergeObj[key].senders
154
+ senders.push(danmaku.senderID)
155
+ const extra = danmaku.extra
156
+ extra.danuni = extra.danuni || {}
157
+ extra.danuni.merge = {
158
+ count: senders.length,
159
+ duration: danmaku.progress - cached.progress,
160
+ senders,
161
+ }
162
+ danmaku.extraStr = JSON.stringify(extra)
163
+ cache[key] = danmaku
164
+ mergeObj[key] = extra.danuni.merge
165
+ return [result, cache, mergeObj]
166
+ } else {
167
+ mergeObj[key] = {
168
+ count: 1,
169
+ duration: 0,
170
+ senders: [danmaku.senderID],
171
+ }
172
+ cache[key] = danmaku
173
+ // 初始化merge信息,包含第一个sender
174
+ const extra = danmaku.extra
175
+ extra.danuni = extra.danuni || {}
176
+ extra.danuni.merge = mergeObj[key]
177
+ danmaku.extraStr = JSON.stringify(extra)
178
+ result.push(danmaku)
179
+ return [result, cache, mergeObj]
180
+ }
181
+ },
182
+ [[], {}, {}],
183
+ )
184
+ // 处理结果,删除senders<=1的merge字段
185
+ const [result, _cache, mergeObj] = mergeContext
186
+ result.forEach((danmaku, i) => {
187
+ const key = ['content', 'mode', 'platform', 'pool', 'SPMO']
188
+ .map((k) => danmaku[k as keyof UniDM])
189
+ .join('|')
190
+ const extra = result[i].extra,
191
+ mergeData = mergeObj[key]
192
+ result[i].extraStr = JSON.stringify({
193
+ ...extra,
194
+ danuni: {
195
+ ...extra.danuni,
196
+ merge: mergeData,
197
+ },
198
+ } satisfies UniDMTools.Extra)
199
+ if (mergeData?.count) {
200
+ if (mergeData.count <= 1) {
201
+ const updatedExtra = { ...extra }
202
+ if (updatedExtra.danuni) {
203
+ delete updatedExtra.danuni.merge
204
+ if (Object.keys(updatedExtra.danuni).length === 0) {
205
+ delete updatedExtra.danuni
206
+ }
207
+ }
208
+ result[i].extraStr =
209
+ Object.keys(updatedExtra).length > 0
210
+ ? JSON.stringify(updatedExtra)
211
+ : undefined
212
+ } else {
213
+ result[i].senderID = 'merge@bot'
214
+ }
215
+ }
216
+ })
217
+ return new UniPool(result)
218
+ }
219
+ minify() {
220
+ return this.dans.map((d) => d.minify())
221
+ }
114
222
  convert2(format: DM_format) {
115
223
  switch (format) {
116
224
  case 'danuni.json':
@@ -339,6 +447,12 @@ export class UniPool {
339
447
  }),
340
448
  }
341
449
  }
450
+ static fromASS(ass: string) {
451
+ return parseAssRawField(ass)
452
+ }
453
+ toASS(options: AssGenOptions = { substyle: {} }): string {
454
+ return generateASS(this, options)
455
+ }
342
456
  }
343
457
 
344
458
  export {
@@ -0,0 +1,66 @@
1
+ //基于以下注释,根据vitest生成测试用例
2
+ import { describe, expect, it } from 'vitest'
3
+ import type { UniDMObj } from './dm-gen'
4
+
5
+ import { UniDM, UniPool } from '..'
6
+
7
+ const xml = `<i>
8
+ <chatserver>chat.bilibili.com</chatserver>
9
+ <chatid>1156756312</chatid>
10
+ <mission>0</mission>
11
+ <maxlimit>2947</maxlimit>
12
+ <state>0</state>
13
+ <real_name>0</real_name>
14
+ <source>k-v</source>
15
+ <d p="13.213,1,25,16777215,1686314041,3,ff41173d,1335658005672492032">喜欢</d>
16
+ <d p="13.331,1,25,16777215,1686948453,3,56a3c5d5,1340979831550069760">不喜欢</d>
17
+ <d p="13.374,1,25,16777215,1686300770,3,647fe355,1335546672880933888">不喜欢</d>
18
+ <d p="13.499,1,25,16777215,1686301548,3,2848bf1c,1335553202649003264">不喜欢</d>
19
+ </i>`
20
+
21
+ describe('其它', () => {
22
+ const pool = UniPool.fromBiliXML(xml)
23
+ it('比较(常规)', () => {
24
+ // 确保测试用例为预期值
25
+ expect(pool.dans[0].content).toBe('喜欢')
26
+ expect(pool.dans[1].content).toBe('不喜欢')
27
+ // 正式测试
28
+ const a = pool.dans[0].isSameAs(pool.dans[1]),
29
+ b = pool.dans[1].isSameAs(pool.dans[2]),
30
+ c = pool.dans[1].isSameAs(pool.dans[3])
31
+ console.info(a, b, c)
32
+ expect(a).toBe(false)
33
+ expect(b).toBe(true)
34
+ expect(c).toBe(true)
35
+ })
36
+ it('比较(extra)', () => {
37
+ const commonSample = {
38
+ FCID: 'test@du',
39
+ content: 'T Sample',
40
+ extra: {
41
+ danuni: {
42
+ merge: {
43
+ count: 1,
44
+ duration: 0,
45
+ senders: ['test@du'],
46
+ },
47
+ },
48
+ },
49
+ } satisfies Partial<UniDMObj>
50
+ const pool2 = [
51
+ UniDM.create({ ...commonSample, extra: undefined }),
52
+ UniDM.create({ ...commonSample, extra: {} }),
53
+ UniDM.create({ ...commonSample, extra: { danuni: {} } }),
54
+ UniDM.create({ ...commonSample }),
55
+ UniDM.create({ ...commonSample, extra: { artplayer: { border: true } } }),
56
+ ]
57
+ for (const pool of pool2) {
58
+ console.info(pool.extraStr)
59
+ console.info(pool2[0].isSameAs(pool))
60
+ }
61
+ expect(pool2[0].isSameAs(pool2[1])).toBe(true)
62
+ expect(pool2[0].isSameAs(pool2[2])).toBe(true)
63
+ expect(pool2[0].isSameAs(pool2[3])).toBe(true)
64
+ expect(pool2[0].isSameAs(pool2[4])).toBe(false)
65
+ })
66
+ })