Files
Archive/bbdown/BBDown.Core/DanmakuUtil.cs
2024-11-10 19:32:12 +01:00

241 lines
8.9 KiB
C#

using static BBDown.Core.Logger;
using System.Text;
using System.Xml;
namespace BBDown.Core;
public static 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; //底部弹幕
}