forked from nilaoda/BBDown
-
Notifications
You must be signed in to change notification settings - Fork 0
/
DanmakuUtil.cs
242 lines (228 loc) · 9.78 KB
/
DanmakuUtil.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
using static BBDown.Core.Logger;
using System.Text;
using System.Xml;
namespace BBDown.Core
{
public class DanmakuUtil
{
private const int MONITOR_WIDTH = 1920; //渲染字幕时的渲染范围的高度
private const int MONITOR_HEIGHT = 1080; //渲染字幕时的渲染范围的高度
private const int FONT_SIZE = 40; //字体大小
private const double MOVE_SPEND_TIME = 8.00; //单条条滚动弹幕存在时间(控制速度)
private const double TOP_SPEND_TIME = 4.00; //单条顶部或底部弹幕存在时间
private const int PROTECT_LENGTH = 50; //滚动弹幕屏占百分比
public static readonly DanmakuComparer comparer = new();
/*public static async Task DownloadAsync(Page p, string xmlPath, bool aria2c, string aria2cProxy)
{
string danmakuUrl = "https://comment.bilibili.com/" + p.cid + ".xml";
await DownloadFile(danmakuUrl, xmlPath, aria2c, aria2cProxy);
}*/
public static DanmakuItem[]? ParseXml(string xmlPath)
{
// 解析xml文件
XmlDocument xmlFile = new();
XmlReaderSettings settings = new()
{
IgnoreComments = true//忽略文档里面的注释
};
var danmakus = new List<DanmakuItem>();
using (var reader = XmlReader.Create(xmlPath, settings))
{
try
{
xmlFile.Load(reader);
}
catch (Exception ex)
{
LogDebug("解析字幕xml时出现异常: {0}", ex.ToString());
return null;
}
}
XmlNode? rootNode = xmlFile.SelectSingleNode("i");
if (rootNode != null)
{
XmlElement rootElement = (XmlElement)rootNode;
XmlNodeList? dNodeList = rootElement.SelectNodes("d");
if (dNodeList != null)
{
foreach (XmlNode node in dNodeList)
{
XmlElement dElement = (XmlElement)node;
string attr = dElement.GetAttribute("p").ToString();
if (attr != null)
{
string[] vs = attr.Split(',');
if (vs.Length >= 8)
{
DanmakuItem danmaku = new(vs, dElement.InnerText);
danmakus.Add(danmaku);
}
}
}
}
}
return danmakus.ToArray();
}
/// <summary>
/// 保存为ASS字幕文件
/// </summary>
/// <param name="danmakus">弹幕</param>
/// <param name="outputPath">保存路径</param>
/// <returns></returns>
public static async Task SaveAsAssAsync(DanmakuItem[] danmakus, string outputPath)
{
var sb = new StringBuilder();
// ASS字幕文件头
sb.AppendLine("[Script Info]");
sb.AppendLine("Script Updated By: BBDown(https://github.com/nilaoda/BBDown)");
sb.AppendLine("ScriptType: v4.00+");
sb.AppendLine($"PlayResX: {MONITOR_WIDTH}");
sb.AppendLine($"PlayResY: {MONITOR_HEIGHT}");
sb.AppendLine($"Aspect Ratio: {MONITOR_WIDTH}:{MONITOR_HEIGHT}");
sb.AppendLine("Collisions: Normal");
sb.AppendLine("WrapStyle: 2");
sb.AppendLine("ScaledBorderAndShadow: yes");
sb.AppendLine("YCbCr Matrix: TV.601");
sb.AppendLine("[V4+ Styles]");
sb.AppendLine("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding");
sb.AppendLine($"Style: BBDOWN_Style, 黑体, {FONT_SIZE}, &H00FFFFFF, &H00FFFFFF, &H00000000, &H00000000, 0, 0, 0, 0, 100, 100, 0.00, 0.00, 1, 2, 0, 7, 0, 0, 0, 0");
sb.AppendLine("[Events]");
sb.AppendLine("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text");
PositionController controller = new(); // 弹幕位置控制器
Array.Sort(danmakus, comparer);
foreach (DanmakuItem danmaku in danmakus)
{
int height = controller.UpdatePosition(danmaku.DanmakuMode, danmaku.Second, danmaku.Content.Length);
if (height == -1) continue;
string effect = "";
effect += danmaku.DanmakuMode switch
{
3 => $"\\an8\\pos({MONITOR_WIDTH / 2}, {MONITOR_HEIGHT - FONT_SIZE - height})",
2 => $"\\an8\\pos({MONITOR_WIDTH / 2}, {height})",
_ => $"\\move({MONITOR_WIDTH}, {height}, {-danmaku.Content.Length * FONT_SIZE}, {height})",
};
if (danmaku.Color != "FFFFFF")
{
effect += $"\\c&{danmaku.Color}&";
}
sb.AppendLine($"Dialogue: 2,{danmaku.StartTime},{danmaku.EndTime},BBDOWN_Style,,0000,0000,0000,,{{{effect}}}{danmaku.Content}");
}
await File.WriteAllTextAsync(outputPath, sb.ToString(), Encoding.UTF8);
}
protected class PositionController
{
readonly int maxLine = MONITOR_HEIGHT * PROTECT_LENGTH / FONT_SIZE / 100; //总行数
// 三个位置的弹幕队列,记录弹幕结束时间
readonly List<double> moveQueue = new();
readonly List<double> topQueue = new();
readonly List<double> bottomQueue = new();
public PositionController()
{
for (int i = 0; i < maxLine; i++)
{
moveQueue.Add(0.00);
topQueue.Add(0.00);
bottomQueue.Add(0.00);
}
}
public int UpdatePosition(int type, double time, int length)
{
// 获取可用位置
List<double> vs;
double displayTime = TOP_SPEND_TIME;
if (type == POS_BOTTOM)
{
vs = bottomQueue;
}
else if (type == POS_TOP)
{
vs = topQueue;
}
else
{
vs = moveQueue;
displayTime = MOVE_SPEND_TIME * (length + 5) * FONT_SIZE / (MONITOR_WIDTH + (length * MOVE_SPEND_TIME));
}
for (int i = 0; i < maxLine; i++)
{
if (time >= vs[i])
{ // 此条弹幕已结束,更新该位置信息
vs[i] = time + displayTime;
return i * FONT_SIZE;
}
}
return -1;
}
}
public class DanmakuItem
{
public DanmakuItem(string[] attrs, string content)
{
DanmakuMode = attrs[1] switch
{
"4" => POS_BOTTOM,
"5" => POS_TOP,
_ => POS_MOVE,
};
try
{
double second = double.Parse(attrs[0]);
Second = second;
StartTime = ComputeTime(second);
EndTime = ComputeTime(second + (DanmakuMode == 1 ? MOVE_SPEND_TIME : TOP_SPEND_TIME));
}
catch (Exception e)
{
Log(e.Message);
}
FontSize = attrs[2];
try
{
int colorD = int.Parse(attrs[3]);
Color = string.Format("{0:X6}", colorD);
}
catch (FormatException e)
{
Log(e.Message);
}
Timestamp = attrs[4];
Content = content;
}
private static string ComputeTime(double second)
{
int hour = (int)second / 3600;
int minute = (int)(second - (hour * 3600)) / 60;
second -= (hour * 3600) + (minute * 60);
return hour.ToString() + string.Format(":{0:D2}:", minute) + string.Format("{0:00.00}", second);
}
public string Content { get; set; } = "";
// 弹幕内容
public string StartTime { get; set; } = "";
// 出现时间
public double Second { get; set; } = 0.00;
// 出现时间(秒为单位)
public string EndTime { get; set; } = "";
// 消失时间
public int DanmakuMode { get; set; } = POS_MOVE;
// 弹幕类型
public string FontSize { get; set; } = "";
// 字号
public string Color { get; set; } = "";
// 颜色
public string Timestamp { get; set; } = "";
// 时间戳
}
public class DanmakuComparer : IComparer<DanmakuItem>
{
public int Compare(DanmakuItem? x, DanmakuItem? y)
{
if (x == null) return -1;
if (y == null) return 1;
return x.Second.CompareTo(y.Second);
}
}
private const int POS_MOVE = 1; //滚动弹幕
private const int POS_TOP = 2; //顶部弹幕
private const int POS_BOTTOM = 3; //底部弹幕
}
}