mirror of
https://github.com/bolucat/Archive.git
synced 2025-09-26 20:21:35 +08:00
241 lines
8.9 KiB
C#
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; //底部弹幕
|
|
} |